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
|
## Features
|
||||||
|
|
||||||
- Displays service/container status, enablement, and quick actions (start/stop/restart/update).
|
- 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).
|
- 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.
|
- Streams Docker logs directly in LuCI.
|
||||||
- Uses SecuBox design system and RPCD backend (`luci.zigbee2mqtt`).
|
- 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`).
|
- Follow SecuBox design tokens (see `DOCS/DEVELOPMENT-GUIDELINES.md`).
|
||||||
- Keep RPC filenames aligned with ubus object name (`luci.zigbee2mqtt`).
|
- Keep RPC filenames aligned with ubus object name (`luci.zigbee2mqtt`).
|
||||||
- Validate with `./secubox-tools/validate-modules.sh`.
|
- 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('secubox-theme/secubox-theme.css') }),
|
||||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('zigbee2mqtt/common.css') }),
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('zigbee2mqtt/common.css') }),
|
||||||
this.renderHeader(config),
|
this.renderHeader(config),
|
||||||
|
this.renderSetup(config),
|
||||||
this.renderForm(config),
|
this.renderForm(config),
|
||||||
this.renderLogs()
|
this.renderLogs()
|
||||||
]);
|
]);
|
||||||
@ -30,6 +31,7 @@ return view.extend({
|
|||||||
return API.getStatus().then(L.bind(function(newData) {
|
return API.getStatus().then(L.bind(function(newData) {
|
||||||
config = newData;
|
config = newData;
|
||||||
this.updateHeader(config);
|
this.updateHeader(config);
|
||||||
|
this.updateDiagnostics(config.diagnostics);
|
||||||
}, this));
|
}, this));
|
||||||
}, this), 10);
|
}, this), 10);
|
||||||
|
|
||||||
@ -51,6 +53,8 @@ return view.extend({
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'z2m-actions' }, [
|
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.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.handleUpdate.bind(this) }, _('Update Image')),
|
||||||
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'restart') }, _('Restart')),
|
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) {
|
renderForm: function(cfg) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var inputs = [
|
var inputs = [
|
||||||
@ -202,5 +251,46 @@ return view.extend({
|
|||||||
ui.hideModal();
|
ui.hideModal();
|
||||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
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'
|
method: 'update'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callInstall = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'install'
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCheck = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'check'
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getStatus: callStatus,
|
getStatus: callStatus,
|
||||||
applyConfig: callApply,
|
applyConfig: callApply,
|
||||||
getLogs: callLogs,
|
getLogs: callLogs,
|
||||||
control: callControl,
|
control: callControl,
|
||||||
update: callUpdate
|
update: callUpdate,
|
||||||
|
install: callInstall,
|
||||||
|
runCheck: callCheck
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,3 +92,43 @@
|
|||||||
border-color: rgba(248, 113, 113, 0.4);
|
border-color: rgba(248, 113, 113, 0.4);
|
||||||
background: rgba(248, 113, 113, 0.12);
|
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)"
|
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() {
|
status() {
|
||||||
json_init
|
json_init
|
||||||
load_config
|
load_config
|
||||||
@ -28,6 +39,7 @@ status() {
|
|||||||
json_add_boolean "enabled" "$( "$SERVICE" enabled >/dev/null 2>&1 && echo 1 || echo 0 )"
|
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_add_boolean "running" "$( "$SERVICE" status >/dev/null 2>&1 && echo 1 || echo 0 )"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
diagnostics
|
||||||
json_add_array "container"
|
json_add_array "container"
|
||||||
docker ps -a --filter "name=secbx-zigbee2mqtt" --format '{{.Names}}|{{.Status}}' 2>/dev/null | while IFS='|' read -r name st; do
|
docker ps -a --filter "name=secbx-zigbee2mqtt" --format '{{.Names}}|{{.Status}}' 2>/dev/null | while IFS='|' read -r name st; do
|
||||||
json_add_object
|
json_add_object
|
||||||
@ -116,6 +128,30 @@ update() {
|
|||||||
json_dump
|
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
|
case "$1" in
|
||||||
list)
|
list)
|
||||||
cat <<'JSON'
|
cat <<'JSON'
|
||||||
@ -124,7 +160,9 @@ case "$1" in
|
|||||||
"apply": {},
|
"apply": {},
|
||||||
"logs": {},
|
"logs": {},
|
||||||
"control": {},
|
"control": {},
|
||||||
"update": {}
|
"update": {},
|
||||||
|
"install": {},
|
||||||
|
"check": {}
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
;;
|
;;
|
||||||
@ -135,6 +173,8 @@ JSON
|
|||||||
logs) logs ;;
|
logs) logs ;;
|
||||||
control) control ;;
|
control) control ;;
|
||||||
update) update ;;
|
update) update ;;
|
||||||
|
install) install ;;
|
||||||
|
check) check ;;
|
||||||
*) json_init; json_add_string "error" "unknown method"; json_dump ;;
|
*) json_init; json_add_string "error" "unknown method"; json_dump ;;
|
||||||
esac
|
esac
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -17,7 +17,9 @@
|
|||||||
"luci.zigbee2mqtt": [
|
"luci.zigbee2mqtt": [
|
||||||
"apply",
|
"apply",
|
||||||
"control",
|
"control",
|
||||||
"update"
|
"update",
|
||||||
|
"install",
|
||||||
|
"check"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"file": {
|
"file": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user