zigbee2mqtt: add installer checks in LuCI

This commit is contained in:
CyberMind-FR 2025-12-29 16:46:10 +01:00
parent 9d14dc7fec
commit 8134e6b852
6 changed files with 193 additions and 3 deletions

View File

@ -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`.

View File

@ -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);
});
}
});

View File

@ -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
};

View File

@ -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;
}

View File

@ -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
;;

View File

@ -17,7 +17,9 @@
"luci.zigbee2mqtt": [
"apply",
"control",
"update"
"update",
"install",
"check"
]
},
"file": {