zigbee2mqtt: add installer checks in LuCI
This commit is contained in:
parent
9d14dc7fec
commit
8134e6b852
@ -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`.
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
;;
|
||||
|
||||
@ -17,7 +17,9 @@
|
||||
"luci.zigbee2mqtt": [
|
||||
"apply",
|
||||
"control",
|
||||
"update"
|
||||
"update",
|
||||
"install",
|
||||
"check"
|
||||
]
|
||||
},
|
||||
"file": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user