secubox-openwrt/package/secubox/secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl

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