229 lines
5.3 KiB
Bash
229 lines
5.3 KiB
Bash
#!/bin/sh
|
|
#
|
|
# SecuBox Zigbee2MQTT manager
|
|
# Handles prerequisite checks, Docker installation, container lifecycle,
|
|
# and configuration generation using UCI.
|
|
|
|
CONFIG="zigbee2mqtt"
|
|
CONTAINER="secbx-zigbee2mqtt"
|
|
OPKG_UPDATED=0
|
|
|
|
warn() { printf '[WARN] %s\n' "$*" >&2; }
|
|
info() { printf '[INFO] %s\n' "$*"; }
|
|
err() { printf '[ERROR] %s\n' "$*" >&2; }
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: zigbee2mqttctl <command>
|
|
|
|
Commands:
|
|
install Run prerequisite checks, install Docker packages, prepare data dir
|
|
check Validate kernel modules, cgroups, storage, and serial access
|
|
update Pull the latest Zigbee2MQTT image and restart the service
|
|
status Show container and service status
|
|
logs Show Docker logs (pass -f to follow)
|
|
service-run Internal: invoked by procd to run the container
|
|
service-stop Stop the running container
|
|
help Show this message
|
|
EOF
|
|
}
|
|
|
|
require_root() {
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "This command requires root privileges." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
uci_get() {
|
|
uci -q get "${CONFIG}.main.$1"
|
|
}
|
|
|
|
defaults() {
|
|
serial_port="$(uci_get serial_port || echo /dev/ttyACM0)"
|
|
mqtt_host="$(uci_get mqtt_host || echo mqtt://127.0.0.1:1883)"
|
|
mqtt_user="$(uci_get mqtt_username || printf '')"
|
|
mqtt_pass="$(uci_get mqtt_password || printf '')"
|
|
base_topic="$(uci_get base_topic || echo zigbee2mqtt)"
|
|
frontend_port="$(uci_get frontend_port || echo 8080)"
|
|
channel="$(uci_get channel || echo 11)"
|
|
image="$(uci_get image || echo ghcr.io/koenkk/zigbee2mqtt:latest)"
|
|
data_path="$(uci_get data_path || echo /srv/zigbee2mqtt)"
|
|
timezone="$(uci_get timezone || echo UTC)"
|
|
}
|
|
|
|
ensure_dir() {
|
|
local path="$1"
|
|
[ -d "$path" ] || mkdir -p "$path"
|
|
}
|
|
|
|
ensure_packages() {
|
|
local pkgs="$*"
|
|
require_root
|
|
for pkg in $pkgs; do
|
|
if ! opkg status "$pkg" >/dev/null 2>&1; then
|
|
if [ "$OPKG_UPDATED" -eq 0 ]; then
|
|
opkg update || return 1
|
|
OPKG_UPDATED=1
|
|
fi
|
|
opkg install "$pkg" || return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
check_storage() {
|
|
local target="$1"
|
|
local free_kb
|
|
free_kb=$(df -Pk "${target:-/overlay}" | awk 'NR==2 {print $4}')
|
|
[ -z "$free_kb" ] && free_kb=0
|
|
if [ "$free_kb" -lt 102400 ]; then
|
|
echo "[WARN] Less than 100MB free on ${target:-overlay}. Docker images may fail." >&2
|
|
fi
|
|
}
|
|
|
|
check_cgroups() {
|
|
if [ ! -d /sys/fs/cgroup ]; then
|
|
echo "[ERROR] /sys/fs/cgroup missing. Enable cgroups in the kernel." >&2
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
check_serial() {
|
|
local port="$1"
|
|
if [ ! -c "$port" ]; then
|
|
echo "[WARN] Serial device $port not found. Plug the Zigbee coordinator first." >&2
|
|
fi
|
|
}
|
|
|
|
check_prereqs() {
|
|
defaults
|
|
check_storage "$data_path"
|
|
check_cgroups || return 1
|
|
check_serial "$serial_port"
|
|
ensure_packages kmod-usb-acm || return 1
|
|
if ! lsmod 2>/dev/null | grep -q 'cdc_acm'; then
|
|
warn "cdc_acm kernel module is not loaded; USB coordinator may not be detected."
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
ensure_docker() {
|
|
ensure_packages containerd docker dockerd || return 1
|
|
/etc/init.d/dockerd enable >/dev/null 2>&1
|
|
/etc/init.d/dockerd start >/dev/null 2>&1
|
|
if ! docker info >/dev/null 2>&1; then
|
|
warn "Docker daemon not ready yet. Retry once dockerd has finished starting."
|
|
fi
|
|
}
|
|
|
|
generate_configuration() {
|
|
defaults
|
|
ensure_dir "$data_path/data"
|
|
cat > "$data_path/data/configuration.yaml" <<EOF
|
|
homeassistant: true
|
|
permit_join: false
|
|
mqtt:
|
|
base_topic: "${base_topic}"
|
|
server: "${mqtt_host}"
|
|
user: "${mqtt_user}"
|
|
password: "${mqtt_pass}"
|
|
serial:
|
|
port: "${serial_port}"
|
|
advanced:
|
|
channel: ${channel}
|
|
frontend:
|
|
port: ${frontend_port}
|
|
EOF
|
|
chmod 600 "$data_path/data/configuration.yaml"
|
|
}
|
|
|
|
pull_image() {
|
|
defaults
|
|
docker pull "$image"
|
|
}
|
|
|
|
stop_container() {
|
|
docker stop "$CONTAINER" >/dev/null 2>&1 || true
|
|
docker rm "$CONTAINER" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
cmd_install() {
|
|
require_root
|
|
check_prereqs || exit 1
|
|
ensure_docker || exit 1
|
|
ensure_dir "$data_path"
|
|
generate_configuration
|
|
pull_image
|
|
uci set ${CONFIG}.main.enabled='1'
|
|
uci commit ${CONFIG}
|
|
/etc/init.d/zigbee2mqtt enable
|
|
info "Installation complete. Start with: /etc/init.d/zigbee2mqtt start"
|
|
}
|
|
|
|
cmd_check() {
|
|
check_prereqs
|
|
info "Prerequisite check completed."
|
|
}
|
|
|
|
cmd_update() {
|
|
require_root
|
|
defaults
|
|
pull_image || exit 1
|
|
if /etc/init.d/zigbee2mqtt enabled >/dev/null 2>&1; then
|
|
/etc/init.d/zigbee2mqtt restart
|
|
else
|
|
echo "Image updated. Restart manually to apply."
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
err "docker command missing. Run zigbee2mqttctl install first."
|
|
return 1
|
|
fi
|
|
docker ps -a --filter "name=$CONTAINER"
|
|
}
|
|
|
|
cmd_logs() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
err "docker command missing."
|
|
return 1
|
|
fi
|
|
docker logs "$@" "$CONTAINER"
|
|
}
|
|
|
|
cmd_service_run() {
|
|
require_root
|
|
check_prereqs || exit 1
|
|
ensure_docker || exit 1
|
|
generate_configuration
|
|
stop_container
|
|
defaults
|
|
exec docker run --rm \
|
|
--name "$CONTAINER" \
|
|
--device "$serial_port" \
|
|
-p "${frontend_port}:8080" \
|
|
-v "$data_path/data:/app/data" \
|
|
-e TZ="$timezone" \
|
|
"$image"
|
|
}
|
|
|
|
cmd_service_stop() {
|
|
require_root
|
|
stop_container
|
|
}
|
|
|
|
case "$1" in
|
|
install) shift; cmd_install "$@";;
|
|
check) shift; cmd_check "$@";;
|
|
update) shift; cmd_update "$@";;
|
|
status) shift; cmd_status "$@";;
|
|
logs) shift; cmd_logs "$@";;
|
|
service-run) shift; cmd_service_run "$@";;
|
|
service-stop) shift; cmd_service_stop "$@";;
|
|
help|--help|-h|"") usage;;
|
|
*) echo "Unknown command: $1" >&2; usage >&2; exit 1;;
|
|
esac
|