secubox-openwrt/package/secubox/secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl
CyberMind-FR eebc84d0b9 fix(zigbee2mqtt): Fix adapter type, config format, and add MQTT dependency
The z2m 2.x breaking changes required three fixes discovered during
live deployment testing on the router:
- Adapter renamed from `ezsp` to `ember` in zigbee-herdsman 4.0.0
- Config format needs `version: 4` and nested `homeassistant.enabled`
- Start script needs `ZIGBEE2MQTT_DATA` env var for correct config path
- Add `mosquitto-nossl` as package dependency (MQTT broker required)
- Direct `/dev/ttyUSB0` passthrough works; socat TCP bridge does not

Also updates project planning files (HISTORY.md, TODO.md, WIP.md,
CLAUDE.md) and rebuilds bonus feed with latest IPKs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:29:59 +01:00

481 lines
12 KiB
Bash

#!/bin/sh
# SecuBox Zigbee2MQTT Controller — LXC-based (KISS)
# Alpine container with Node.js + zigbee2mqtt
# USB dongle passed through via cgroup device rules
CONFIG="zigbee2mqtt"
LXC_NAME="zigbee2mqtt"
LXC_PATH="/srv/lxc"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
DATA_PATH="/srv/zigbee2mqtt"
Z2M_VERSION="2.3.0"
# Logging
log_info() { echo "[INFO] $*"; logger -t zigbee2mqtt "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t zigbee2mqtt -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t zigbee2mqtt -p err "$*"; }
# Helpers
require_root() { [ "$(id -u)" -eq 0 ] || { log_error "Root required"; exit 1; }; }
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
uci_get() { uci -q get ${CONFIG}.$1; }
load_config() {
serial_port=$(uci_get main.serial_port) || serial_port="/dev/ttyUSB0"
mqtt_host=$(uci_get main.mqtt_host) || mqtt_host="mqtt://127.0.0.1:1883"
mqtt_user=$(uci_get main.mqtt_username)
mqtt_pass=$(uci_get main.mqtt_password)
base_topic=$(uci_get main.base_topic) || base_topic="zigbee2mqtt"
frontend_port=$(uci_get main.frontend_port) || frontend_port="8099"
channel=$(uci_get main.channel) || channel="11"
data_path=$(uci_get main.data_path) || data_path="$DATA_PATH"
permit_join=$(uci_get main.permit_join) || permit_join="0"
}
usage() {
cat <<'EOF'
SecuBox Zigbee2MQTT Controller (LXC)
Usage: zigbee2mqttctl <command>
Commands:
install Create Alpine LXC container with Node.js + zigbee2mqtt
uninstall Remove container (keeps data)
update Update zigbee2mqtt inside container
status Show service status
logs [N] Show last N lines of logs (default 50)
check Validate USB dongle and prerequisites
service-run Internal: invoked by procd
service-stop Stop the container
shell Open shell inside container
help Show this message
EOF
}
# ════════════════════════════════════════════
# LXC Management
# ════════════════════════════════════════════
lxc_running() { lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; }
lxc_exists() { [ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]; }
lxc_exec() { lxc-attach -n "$LXC_NAME" -- "$@"; }
lxc_stop() {
if lxc_running; then
log_info "Stopping zigbee2mqtt container..."
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
sleep 2
fi
}
lxc_create_rootfs() {
log_info "Creating Alpine rootfs for Zigbee2MQTT..."
ensure_dir "$LXC_PATH/$LXC_NAME"
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armv7" ;;
esac
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/$arch/alpine-minirootfs-3.21.2-$arch.tar.gz"
local rootfs_tar="/tmp/alpine-z2m.tar.gz"
log_info "Downloading Alpine rootfs..."
wget -q -O "$rootfs_tar" "$alpine_url" || {
log_error "Failed to download Alpine rootfs"
return 1
}
log_info "Extracting rootfs..."
ensure_dir "$LXC_ROOTFS"
tar -xzf "$rootfs_tar" -C "$LXC_ROOTFS" || {
log_error "Failed to extract rootfs"
return 1
}
rm -f "$rootfs_tar"
# Configure Alpine
cat > "$LXC_ROOTFS/etc/resolv.conf" << 'RESOLVEOF'
nameserver 1.1.1.1
nameserver 8.8.8.8
RESOLVEOF
cat > "$LXC_ROOTFS/etc/apk/repositories" << 'REPOEOF'
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
REPOEOF
# Install Node.js and dependencies
log_info "Installing Node.js and build dependencies..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apk update
apk add --no-cache nodejs npm make gcc g++ linux-headers python3 git
" || {
log_error "Failed to install Node.js"
return 1
}
# Install zigbee2mqtt
log_info "Installing zigbee2mqtt (this may take a few minutes)..."
ensure_dir "$LXC_ROOTFS/opt/zigbee2mqtt"
chroot "$LXC_ROOTFS" /bin/sh -c "
cd /opt/zigbee2mqtt
npm init -y >/dev/null 2>&1
npm install zigbee2mqtt@$Z2M_VERSION
" || {
log_error "Failed to install zigbee2mqtt"
return 1
}
# Clean up build deps to save space
log_info "Cleaning up build dependencies..."
chroot "$LXC_ROOTFS" /bin/sh -c "
apk del make gcc g++ linux-headers python3 git 2>/dev/null || true
rm -rf /var/cache/apk/*
"
log_info "Rootfs created successfully"
}
lxc_create_config() {
load_config
local arch="x86_64"
case "$(uname -m)" in
aarch64) arch="aarch64" ;;
armv7l) arch="armhf" ;;
esac
# Get USB device major:minor for cgroup passthrough
local usb_major usb_minor
if [ -c "$serial_port" ]; then
usb_major=$(stat -c '%t' "$serial_port" 2>/dev/null)
usb_minor=$(stat -c '%T' "$serial_port" 2>/dev/null)
# Convert hex to decimal
usb_major=$((0x${usb_major:-bc}))
usb_minor=$((0x${usb_minor:-0}))
else
usb_major=188 # ttyUSB default major
usb_minor=0
fi
cat > "$LXC_CONFIG" << EOF
# Zigbee2MQTT LXC Configuration
lxc.uts.name = $LXC_NAME
lxc.rootfs.path = dir:$LXC_ROOTFS
lxc.arch = $arch
# Network: use host network (for MQTT and frontend)
lxc.net.0.type = none
# Mount points
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.mount.entry = $data_path/data opt/zigbee2mqtt/data none bind,create=dir 0 0
# USB serial passthrough
lxc.cgroup2.devices.allow = c $usb_major:* rwm
lxc.mount.entry = $serial_port dev/$(basename $serial_port) none bind,create=file 0 0
# Security
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time
# Resource limits
lxc.cgroup2.memory.max = 512000000
# Init command
lxc.init.cmd = /opt/start-zigbee2mqtt.sh
EOF
log_info "LXC config created"
}
generate_config() {
load_config
ensure_dir "$data_path/data"
# Generate zigbee2mqtt configuration.yaml
cat > "$data_path/data/configuration.yaml" << EOF
version: 4
homeassistant:
enabled: false
permit_join: $([ "$permit_join" = "1" ] && echo "true" || echo "false")
mqtt:
base_topic: $base_topic
server: $mqtt_host
$([ -n "$mqtt_user" ] && echo " user: $mqtt_user")
$([ -n "$mqtt_pass" ] && echo " password: $mqtt_pass")
serial:
port: /dev/$(basename $serial_port)
adapter: ember
advanced:
channel: $channel
log_level: info
log_output:
- console
frontend:
enabled: true
port: $frontend_port
host: 0.0.0.0
EOF
chmod 600 "$data_path/data/configuration.yaml"
log_info "Configuration generated"
}
generate_start_script() {
load_config
cat > "$LXC_ROOTFS/opt/start-zigbee2mqtt.sh" << 'STARTEOF'
#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export NODE_PATH=/opt/zigbee2mqtt/node_modules
export ZIGBEE2MQTT_DATA=/opt/zigbee2mqtt/data
cd /opt/zigbee2mqtt
LOG_FILE="/opt/zigbee2mqtt/data/zigbee2mqtt.log"
echo "=== Zigbee2MQTT startup: $(date) ===" >> "$LOG_FILE"
# Check serial device
SERIAL=$(grep -o 'port: /dev/[^ ]*' data/configuration.yaml 2>/dev/null | cut -d/ -f2-)
if [ -n "$SERIAL" ] && [ ! -c "/dev/$SERIAL" ]; then
echo "[ERROR] Serial device /dev/$SERIAL not found" >> "$LOG_FILE"
sleep 5
exit 1
fi
# Run zigbee2mqtt
exec node node_modules/zigbee2mqtt/index.js 2>&1 | tee -a "$LOG_FILE"
STARTEOF
chmod +x "$LXC_ROOTFS/opt/start-zigbee2mqtt.sh"
}
# ════════════════════════════════════════════
# Commands
# ════════════════════════════════════════════
cmd_install() {
require_root
load_config
# Check USB dongle
if [ ! -c "$serial_port" ]; then
log_warn "Serial device $serial_port not found."
log_warn "Plug in your Zigbee USB dongle and ensure kmod-usb-serial-cp210x is installed."
fi
# Create container if missing
if ! lxc_exists; then
lxc_create_rootfs || exit 1
else
log_info "Container already exists, skipping rootfs creation"
fi
# Setup data dir and config
ensure_dir "$data_path/data"
generate_config
lxc_create_config
generate_start_script
# Enable service
uci set ${CONFIG}.main.enabled='1'
uci commit ${CONFIG}
/etc/init.d/zigbee2mqtt enable 2>/dev/null
log_info "Installation complete."
log_info "Start with: /etc/init.d/zigbee2mqtt start"
log_info "Frontend will be at: http://192.168.255.1:${frontend_port}"
}
cmd_uninstall() {
require_root
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
log_info "Removing container (data at $DATA_PATH preserved)..."
rm -rf "$LXC_PATH/$LXC_NAME"
fi
uci set ${CONFIG}.main.enabled='0'
uci commit ${CONFIG}
/etc/init.d/zigbee2mqtt disable 2>/dev/null
log_info "Uninstalled. Data preserved at $DATA_PATH"
}
cmd_update() {
require_root
load_config
if ! lxc_exists; then
log_error "Container not installed. Run: zigbee2mqttctl install"
return 1
fi
lxc_stop
log_info "Updating zigbee2mqtt..."
chroot "$LXC_ROOTFS" /bin/sh -c "
cd /opt/zigbee2mqtt
npm install zigbee2mqtt@latest
" || {
log_error "Update failed"
return 1
}
log_info "Update complete. Restarting..."
/etc/init.d/zigbee2mqtt restart 2>/dev/null
}
cmd_status() {
load_config
echo "Zigbee2MQTT Status"
echo "=================="
local enabled=$(uci_get main.enabled)
echo "Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")"
if lxc_running; then
echo "Running: yes"
else
echo "Running: no"
fi
echo "Serial Port: $serial_port"
if [ -c "$serial_port" ]; then
echo "Dongle: detected"
else
echo "Dongle: NOT found"
fi
echo "Frontend: http://192.168.255.1:$frontend_port"
echo "MQTT Broker: $mqtt_host"
echo "Base Topic: $base_topic"
echo "Channel: $channel"
echo "Data Path: $data_path"
if lxc_exists; then
echo "Container: installed"
else
echo "Container: not installed"
fi
}
cmd_logs() {
local lines="${1:-50}"
local logfile="$DATA_PATH/data/zigbee2mqtt.log"
if [ -f "$logfile" ]; then
tail -n "$lines" "$logfile"
else
echo "No log file found at $logfile"
# Try container journal
if lxc_running; then
lxc_exec tail -n "$lines" /opt/zigbee2mqtt/data/zigbee2mqtt.log 2>/dev/null
fi
fi
}
cmd_check() {
load_config
echo "Zigbee2MQTT Prerequisites"
echo "========================="
# USB serial module
if lsmod 2>/dev/null | grep -q "cp210x"; then
echo "[OK] cp210x kernel module loaded"
else
echo "[!!] cp210x kernel module NOT loaded"
echo " Fix: opkg install kmod-usb-serial-cp210x && modprobe cp210x"
fi
# Serial device
if [ -c "$serial_port" ]; then
echo "[OK] Serial device $serial_port present"
else
echo "[!!] Serial device $serial_port NOT found"
fi
# USB devices
local usb_count=0
for d in /sys/bus/usb/devices/[0-9]*; do
local v=$(cat "$d/idVendor" 2>/dev/null)
local p=$(cat "$d/idProduct" 2>/dev/null)
local n=$(cat "$d/product" 2>/dev/null)
[ "$v" = "10c4" ] && {
echo "[OK] Sonoff/CP210x USB device: $n ($v:$p)"
usb_count=$((usb_count + 1))
}
done
[ "$usb_count" -eq 0 ] && echo "[!!] No Sonoff/CP210x USB devices detected"
# LXC
if command -v lxc-start >/dev/null 2>&1; then
echo "[OK] LXC available"
else
echo "[!!] LXC not installed"
fi
# Container
if lxc_exists; then
echo "[OK] Container installed"
else
echo "[--] Container not installed (run: zigbee2mqttctl install)"
fi
# Data directory
if [ -d "$data_path/data" ]; then
echo "[OK] Data directory exists"
else
echo "[--] Data directory not created yet"
fi
}
cmd_service_run() {
require_root
load_config
if ! lxc_exists; then
log_error "Container not installed. Run: zigbee2mqttctl install"
exit 1
fi
# Regenerate config and start script each run
generate_config
lxc_create_config
generate_start_script
lxc_stop
log_info "Starting zigbee2mqtt container..."
exec lxc-start -n "$LXC_NAME" -F
}
cmd_service_stop() {
require_root
lxc_stop
}
# ════════════════════════════════════════════
# Main
# ════════════════════════════════════════════
case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
check) shift; cmd_check "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
shell) shift; lxc-attach -n "$LXC_NAME" -- /bin/sh ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
esac