From 8134e6b852208f2c79b9f151f50fe092b37273e5 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 16:46:10 +0100 Subject: [PATCH] zigbee2mqtt: add installer checks in LuCI --- luci-app-zigbee2mqtt/README.md | 6 ++ .../resources/view/zigbee2mqtt/overview.js | 90 +++++++++++++++++++ .../luci-static/resources/zigbee2mqtt/api.js | 14 ++- .../resources/zigbee2mqtt/common.css | 40 +++++++++ .../root/usr/libexec/rpcd/luci.zigbee2mqtt | 42 ++++++++- .../rpcd/acl.d/luci-app-zigbee2mqtt.json | 4 +- 6 files changed, 193 insertions(+), 3 deletions(-) diff --git a/luci-app-zigbee2mqtt/README.md b/luci-app-zigbee2mqtt/README.md index 397e32da..68af2200 100644 --- a/luci-app-zigbee2mqtt/README.md +++ b/luci-app-zigbee2mqtt/README.md @@ -9,6 +9,7 @@ LuCI interface for managing the Docker-based Zigbee2MQTT service packaged in `se ## Features - Displays service/container status, enablement, and quick actions (start/stop/restart/update). +- Runs prerequisite checks and full Docker installation (dockerd/containerd/image pull) via LuCI buttons. - Provides a form to edit `/etc/config/zigbee2mqtt` (serial port, MQTT host, credentials, base topic, frontend port, channel, data path, docker image, timezone). - Streams Docker logs directly in LuCI. - Uses SecuBox design system and RPCD backend (`luci.zigbee2mqtt`). @@ -51,3 +52,8 @@ Access via LuCI: **Services → SecuBox → Zigbee2MQTT**. - Follow SecuBox design tokens (see `DOCS/DEVELOPMENT-GUIDELINES.md`). - Keep RPC filenames aligned with ubus object name (`luci.zigbee2mqtt`). - Validate with `./secubox-tools/validate-modules.sh`. + +## Documentation + +- Deployment walkthrough: [`docs/embedded/zigbee2mqtt-docker.md`](../docs/embedded/zigbee2mqtt-docker.md) +- CLI helper (`zigbee2mqttctl`) is packaged by `secubox-app-zigbee2mqtt`. diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js index f4adc554..e13dda10 100644 --- a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js @@ -22,6 +22,7 @@ return view.extend({ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), E('link', { 'rel': 'stylesheet', 'href': L.resource('zigbee2mqtt/common.css') }), this.renderHeader(config), + this.renderSetup(config), this.renderForm(config), this.renderLogs() ]); @@ -30,6 +31,7 @@ return view.extend({ return API.getStatus().then(L.bind(function(newData) { config = newData; this.updateHeader(config); + this.updateDiagnostics(config.diagnostics); }, this)); }, this), 10); @@ -51,6 +53,8 @@ return view.extend({ ]) ]), E('div', { 'class': 'z2m-actions' }, [ + E('button', { 'class': 'sh-btn-secondary', 'click': this.handleCheck.bind(this) }, _('Run checks')), + E('button', { 'class': 'sh-btn-secondary', 'click': this.handleInstall.bind(this) }, _('Install prerequisites')), E('button', { 'class': 'sh-btn-secondary', 'click': this.handleLogs.bind(this) }, _('Refresh logs')), E('button', { 'class': 'sh-btn-secondary', 'click': this.handleUpdate.bind(this) }, _('Update Image')), E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'restart') }, _('Restart')), @@ -74,6 +78,51 @@ return view.extend({ } }, + renderSetup: function(cfg) { + var diag = cfg.diagnostics || {}; + return E('div', { 'class': 'z2m-card' }, [ + E('div', { 'class': 'z2m-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, _('Prerequisites & Health')) + ]), + this.renderDiagnostics(diag) + ]); + }, + + renderDiagnostics: function(diag) { + var items = [ + { key: 'cgroups', label: _('cgroups mounted') }, + { key: 'docker', label: _('Docker daemon') }, + { key: 'usb_module', label: _('cdc_acm module') }, + { key: 'serial_device', label: _('Serial device') }, + { key: 'service_file', label: _('Service script') } + ]; + return E('div', { 'class': 'z2m-diag-list' }, items.map(function(item) { + var ok = diag[item.key]; + return E('div', { + 'class': 'z2m-diag-chip ' + (ok ? 'ok' : 'bad'), + 'id': 'z2m-diag-' + item.key + }, [ + E('span', { 'class': 'z2m-diag-label' }, item.label), + E('span', { 'class': 'z2m-diag-value' }, ok ? _('OK') : _('Missing')) + ]); + })); + }, + + updateDiagnostics: function(diag) { + var keys = ['cgroups', 'docker', 'usb_module', 'serial_device', 'service_file']; + diag = diag || {}; + keys.forEach(function(key) { + var el = document.getElementById('z2m-diag-' + key); + if (el) { + var ok = diag[key]; + el.className = 'z2m-diag-chip ' + (ok ? 'ok' : 'bad'); + var valueEl = el.querySelector('.z2m-diag-value'); + if (valueEl) + valueEl.textContent = ok ? _('OK') : _('Missing'); + } + }); + }, + renderForm: function(cfg) { var self = this; var inputs = [ @@ -202,5 +251,46 @@ return view.extend({ ui.hideModal(); ui.addNotification(null, E('p', {}, err.message || err), 'error'); }); + }, + + handleInstall: function() { + this.runCommand(_('Installing prerequisites…'), API.install); + }, + + handleCheck: function() { + this.runCommand(_('Running prerequisite checks…'), API.runCheck); + }, + + runCommand: function(title, fn) { + var self = this; + ui.showModal(title, [ + E('p', {}, title), + E('div', { 'class': 'spinning' }) + ]); + fn().then(function(result) { + ui.hideModal(); + self.showCommandOutput(result, title); + self.refreshStatus(); + }).catch(function(err) { + ui.hideModal(); + self.showCommandOutput({ success: 0, output: err && err.message ? err.message : err }, title); + }); + }, + + showCommandOutput: function(result, title) { + var output = (result && result.output) ? result.output : _('Command finished.'); + var tone = (result && result.success) ? 'info' : 'error'; + ui.addNotification(null, E('div', {}, [ + E('strong', {}, title), + E('pre', { 'style': 'white-space:pre-wrap;margin-top:8px;' }, output) + ]), tone); + }, + + refreshStatus: function() { + var self = this; + return API.getStatus().then(function(newData) { + self.updateHeader(newData); + self.updateDiagnostics(newData.diagnostics); + }); } }); diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js index 8fef3a45..f1161cba 100644 --- a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js @@ -31,10 +31,22 @@ var callUpdate = rpc.declare({ method: 'update' }); +var callInstall = rpc.declare({ + object: 'luci.zigbee2mqtt', + method: 'install' +}); + +var callCheck = rpc.declare({ + object: 'luci.zigbee2mqtt', + method: 'check' +}); + return { getStatus: callStatus, applyConfig: callApply, getLogs: callLogs, control: callControl, - update: callUpdate + update: callUpdate, + install: callInstall, + runCheck: callCheck }; diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css index e1a7ac3f..5a9ffbef 100644 --- a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css @@ -92,3 +92,43 @@ border-color: rgba(248, 113, 113, 0.4); background: rgba(248, 113, 113, 0.12); } + +.z2m-diag-list { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.z2m-diag-chip { + display: flex; + flex-direction: column; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(148, 163, 184, 0.2); + min-width: 150px; +} + +.z2m-diag-chip.ok { + border-color: rgba(74, 222, 128, 0.4); + background: rgba(21, 128, 61, 0.12); + color: #4ade80; +} + +.z2m-diag-chip.bad { + border-color: rgba(248, 113, 113, 0.4); + background: rgba(248, 113, 113, 0.1); + color: #f87171; +} + +.z2m-diag-label { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.4px; + color: #94a3b8; +} + +.z2m-diag-value { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + margin-top: 4px; +} diff --git a/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt b/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt index 6fc10635..84765deb 100755 --- a/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt +++ b/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt @@ -21,6 +21,17 @@ load_config() { json_add_boolean "enabled" "$( [ "$(uci -q get ${CONFIG}.main.enabled || echo 0)" = "1" ] && echo 1 || echo 0)" } +diagnostics() { + local serial="$(uci -q get ${CONFIG}.main.serial_port || echo /dev/ttyACM0)" + json_add_object "diagnostics" + json_add_boolean "cgroups" "$( [ -d /sys/fs/cgroup ] && echo 1 || echo 0 )" + json_add_boolean "docker" "$( command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 && echo 1 || echo 0 )" + json_add_boolean "serial_device" "$( [ -c "$serial" ] && echo 1 || echo 0 )" + json_add_boolean "usb_module" "$( lsmod 2>/dev/null | grep -q 'cdc_acm' && echo 1 || echo 0 )" + json_add_boolean "service_file" "$( [ -x "$SERVICE" ] && echo 1 || echo 0 )" + json_close_object +} + status() { json_init load_config @@ -28,6 +39,7 @@ status() { json_add_boolean "enabled" "$( "$SERVICE" enabled >/dev/null 2>&1 && echo 1 || echo 0 )" json_add_boolean "running" "$( "$SERVICE" status >/dev/null 2>&1 && echo 1 || echo 0 )" json_close_object + diagnostics json_add_array "container" docker ps -a --filter "name=secbx-zigbee2mqtt" --format '{{.Names}}|{{.Status}}' 2>/dev/null | while IFS='|' read -r name st; do json_add_object @@ -116,6 +128,30 @@ update() { json_dump } +run_helper() { + local command="$1" + shift + local output + output=$("$CTL" "$command" "$@" 2>&1) + local rc=$? + json_init + if [ "$rc" -eq 0 ]; then + json_add_boolean "success" 1 + else + json_add_boolean "success" 0 + fi + [ -n "$output" ] && json_add_string "output" "$output" + json_dump +} + +install() { + run_helper install +} + +check() { + run_helper check +} + case "$1" in list) cat <<'JSON' @@ -124,7 +160,9 @@ case "$1" in "apply": {}, "logs": {}, "control": {}, - "update": {} + "update": {}, + "install": {}, + "check": {} } JSON ;; @@ -135,6 +173,8 @@ JSON logs) logs ;; control) control ;; update) update ;; + install) install ;; + check) check ;; *) json_init; json_add_string "error" "unknown method"; json_dump ;; esac ;; diff --git a/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json b/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json index fde90ad1..af96f432 100644 --- a/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json +++ b/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json @@ -17,7 +17,9 @@ "luci.zigbee2mqtt": [ "apply", "control", - "update" + "update", + "install", + "check" ] }, "file": {