feat: add luci interface for zigbee2mqtt
This commit is contained in:
parent
ec81952db1
commit
40e937a919
14
luci-app-zigbee2mqtt/Makefile
Normal file
14
luci-app-zigbee2mqtt/Makefile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-zigbee2mqtt
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Support for SecuBox Zigbee2MQTT App
|
||||||
|
LUCI_DESCRIPTION:=Graphical interface for managing the Zigbee2MQTT docker application.
|
||||||
|
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +secubox-app-zigbee2mqtt
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
# call BuildPackage - OpenWrt buildroot
|
||||||
53
luci-app-zigbee2mqtt/README.md
Normal file
53
luci-app-zigbee2mqtt/README.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# LuCI App – Zigbee2MQTT
|
||||||
|
|
||||||
|
**Version:** 1.0.0
|
||||||
|
**Last Updated:** 2025-12-28
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
LuCI interface for managing the Docker-based Zigbee2MQTT service packaged in `secubox-app-zigbee2mqtt`.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Displays service/container status, enablement, and quick actions (start/stop/restart/update).
|
||||||
|
- 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`).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `secubox-app-zigbee2mqtt` package installed (provides CLI + procd service).
|
||||||
|
- Docker runtime (`dockerd`, `docker`, `containerd`) available on the router.
|
||||||
|
- Zigbee coordinator connected (e.g., `/dev/ttyACM0`).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
opkg update
|
||||||
|
opkg install secubox-app-zigbee2mqtt luci-app-zigbee2mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
Access via LuCI: **Services → SecuBox → Zigbee2MQTT**.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `htdocs/luci-static/resources/view/zigbee2mqtt/overview.js` | Main LuCI view. |
|
||||||
|
| `htdocs/luci-static/resources/zigbee2mqtt/api.js` | RPC bindings. |
|
||||||
|
| `root/usr/libexec/rpcd/luci.zigbee2mqtt` | RPC backend interacting with UCI and `zigbee2mqttctl`. |
|
||||||
|
| `root/usr/share/luci/menu.d/luci-app-zigbee2mqtt.json` | Menu entry. |
|
||||||
|
| `root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json` | ACL defaults. |
|
||||||
|
|
||||||
|
## RPC Methods
|
||||||
|
|
||||||
|
- `status` – Return UCI config, service enable/running state, Docker container list.
|
||||||
|
- `apply` – Update UCI fields, commit, and restart the service.
|
||||||
|
- `logs` – Tail container logs.
|
||||||
|
- `control` – Start/stop/restart service via init script.
|
||||||
|
- `update` – Pull latest image and restart.
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
- 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`.
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require zigbee2mqtt/api as API';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return API.getStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var config = data || {};
|
||||||
|
var container = E('div', { 'class': 'z2m-dashboard' }, [
|
||||||
|
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.renderForm(config),
|
||||||
|
this.renderLogs()
|
||||||
|
]);
|
||||||
|
|
||||||
|
poll.add(L.bind(function() {
|
||||||
|
return API.getStatus().then(L.bind(function(newData) {
|
||||||
|
config = newData;
|
||||||
|
this.updateHeader(config);
|
||||||
|
}, this));
|
||||||
|
}, this), 10);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHeader: function(cfg) {
|
||||||
|
var header = E('div', { 'class': 'z2m-card', 'id': 'z2m-status-card' }, [
|
||||||
|
E('div', { 'class': 'z2m-card-header' }, [
|
||||||
|
E('div', { 'class': 'sh-page-title' }, [
|
||||||
|
E('span', { 'class': 'sh-page-title-icon' }, '🧩'),
|
||||||
|
_('Zigbee2MQTT')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'z2m-status-badges' }, [
|
||||||
|
E('div', { 'class': 'z2m-badge ' + ((cfg.service && cfg.service.running) ? 'on' : 'off'), 'id': 'z2m-badge-running' },
|
||||||
|
cfg.service && cfg.service.running ? _('Running') : _('Stopped')),
|
||||||
|
E('div', { 'class': 'z2m-badge ' + ((cfg.service && cfg.service.enabled) ? 'on' : 'off'), 'id': 'z2m-badge-enabled' },
|
||||||
|
cfg.service && cfg.service.enabled ? _('Enabled') : _('Disabled'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'z2m-actions' }, [
|
||||||
|
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')),
|
||||||
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'start') }, _('Start')),
|
||||||
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'stop') }, _('Stop'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
return header;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateHeader: function(cfg) {
|
||||||
|
var runBadge = document.getElementById('z2m-badge-running');
|
||||||
|
var enBadge = document.getElementById('z2m-badge-enabled');
|
||||||
|
if (runBadge) {
|
||||||
|
runBadge.className = 'z2m-badge ' + ((cfg.service && cfg.service.running) ? 'on' : 'off');
|
||||||
|
runBadge.textContent = (cfg.service && cfg.service.running) ? _('Running') : _('Stopped');
|
||||||
|
}
|
||||||
|
if (enBadge) {
|
||||||
|
enBadge.className = 'z2m-badge ' + ((cfg.service && cfg.service.enabled) ? 'on' : 'off');
|
||||||
|
enBadge.textContent = (cfg.service && cfg.service.enabled) ? _('Enabled') : _('Disabled');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderForm: function(cfg) {
|
||||||
|
var self = this;
|
||||||
|
var inputs = [
|
||||||
|
self.input('enabled', _('Enable service'), cfg.enabled ? '1' : '0', 'checkbox'),
|
||||||
|
self.input('serial_port', _('Serial device'), cfg.serial_port || '/dev/ttyACM0'),
|
||||||
|
self.input('mqtt_host', _('MQTT host URL'), cfg.mqtt_host || 'mqtt://127.0.0.1:1883'),
|
||||||
|
self.input('mqtt_username', _('MQTT username'), cfg.mqtt_username || ''),
|
||||||
|
self.input('mqtt_password', _('MQTT password'), cfg.mqtt_password || '', 'password'),
|
||||||
|
self.input('base_topic', _('Base topic'), cfg.base_topic || 'zigbee2mqtt'),
|
||||||
|
self.input('frontend_port', _('Frontend port'), cfg.frontend_port || '8080', 'number'),
|
||||||
|
self.input('channel', _('Zigbee channel'), cfg.channel || '11', 'number'),
|
||||||
|
self.input('data_path', _('Data path'), cfg.data_path || '/srv/zigbee2mqtt'),
|
||||||
|
self.input('image', _('Docker image'), cfg.image || 'ghcr.io/koenkk/zigbee2mqtt:latest'),
|
||||||
|
self.input('timezone', _('Timezone'), cfg.timezone || 'UTC')
|
||||||
|
];
|
||||||
|
|
||||||
|
return E('div', { 'class': 'z2m-card' }, [
|
||||||
|
E('div', { 'class': 'z2m-card-header' }, [
|
||||||
|
E('div', { 'class': 'sh-card-title' }, _('Configuration'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'z2m-form-grid', 'id': 'z2m-form-grid' }, inputs),
|
||||||
|
E('div', { 'class': 'z2m-actions' }, [
|
||||||
|
E('button', { 'class': 'sh-btn-primary', 'click': this.handleSave.bind(this) }, _('Save & Apply'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
input: function(id, label, value, type) {
|
||||||
|
type = type || 'text';
|
||||||
|
var attrs = { 'class': 'z2m-input', 'id': id, 'value': value };
|
||||||
|
if (type === 'checkbox') {
|
||||||
|
attrs.type = 'checkbox';
|
||||||
|
if (value === '1' || value === 1 || value === true) attrs.checked = true;
|
||||||
|
} else {
|
||||||
|
attrs.type = type;
|
||||||
|
}
|
||||||
|
if (id === 'mqtt_password')
|
||||||
|
attrs.autocomplete = 'new-password';
|
||||||
|
return E('div', { 'class': 'z2m-input-group' }, [
|
||||||
|
E('label', { 'for': id }, label),
|
||||||
|
E('input', attrs)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLogs: function() {
|
||||||
|
return E('div', { 'class': 'z2m-card' }, [
|
||||||
|
E('div', { 'class': 'z2m-card-header' }, [
|
||||||
|
E('div', { 'class': 'sh-card-title' }, _('Logs')),
|
||||||
|
E('div', { 'class': 'z2m-actions' }, [
|
||||||
|
E('input', { 'class': 'z2m-input', 'type': 'number', 'id': 'z2m-log-tail', 'value': '200', 'style': 'width:90px;' }),
|
||||||
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleLogs.bind(this) }, _('Refresh'))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('pre', { 'class': 'z2m-log', 'id': 'z2m-log-output' }, _('Logs will appear here.'))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSave: function() {
|
||||||
|
var payload = {
|
||||||
|
enabled: document.getElementById('enabled').checked ? '1' : '0',
|
||||||
|
serial_port: document.getElementById('serial_port').value,
|
||||||
|
mqtt_host: document.getElementById('mqtt_host').value,
|
||||||
|
mqtt_username: document.getElementById('mqtt_username').value,
|
||||||
|
mqtt_password: document.getElementById('mqtt_password').value,
|
||||||
|
base_topic: document.getElementById('base_topic').value,
|
||||||
|
frontend_port: document.getElementById('frontend_port').value,
|
||||||
|
channel: document.getElementById('channel').value,
|
||||||
|
data_path: document.getElementById('data_path').value,
|
||||||
|
image: document.getElementById('image').value,
|
||||||
|
timezone: document.getElementById('timezone').value
|
||||||
|
};
|
||||||
|
ui.showModal(_('Applying configuration'), [
|
||||||
|
E('p', {}, _('Saving settings and restarting service…')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
API.applyConfig(payload).then(function() {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, _('Configuration applied.')), 'info');
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleLogs: function() {
|
||||||
|
var tail = parseInt(document.getElementById('z2m-log-tail').value, 10) || 200;
|
||||||
|
API.getLogs(tail).then(function(result) {
|
||||||
|
var box = document.getElementById('z2m-log-output');
|
||||||
|
if (box && result && result.lines) {
|
||||||
|
box.textContent = result.lines.join('\n');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleControl: function(action) {
|
||||||
|
ui.showModal(_('Executing action'), [
|
||||||
|
E('p', {}, _('Performing %s…').format(action)),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
API.control(action).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Action completed: %s').format(action)), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Action failed')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUpdate: function() {
|
||||||
|
ui.showModal(_('Updating image'), [
|
||||||
|
E('p', {}, _('Pulling latest Zigbee2MQTT image…')),
|
||||||
|
E('div', { 'class': 'spinning' })
|
||||||
|
]);
|
||||||
|
API.update().then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Image updated. Service restarted.')), 'info');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Update failed')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/* global rpc */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callStatus = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'status',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callApply = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'apply'
|
||||||
|
});
|
||||||
|
|
||||||
|
var callLogs = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'logs',
|
||||||
|
params: ['tail']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callControl = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'control',
|
||||||
|
params: ['action']
|
||||||
|
});
|
||||||
|
|
||||||
|
var callUpdate = rpc.declare({
|
||||||
|
object: 'luci.zigbee2mqtt',
|
||||||
|
method: 'update'
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getStatus: callStatus,
|
||||||
|
applyConfig: callApply,
|
||||||
|
getLogs: callLogs,
|
||||||
|
control: callControl,
|
||||||
|
update: callUpdate
|
||||||
|
};
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
.z2m-dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-card {
|
||||||
|
background: rgba(11, 15, 28, 0.92);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 18px 30px rgba(2, 6, 23, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-input-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-input {
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-log {
|
||||||
|
background: #020617;
|
||||||
|
color: #9efc6a;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-status-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-badge.on {
|
||||||
|
color: #4ade80;
|
||||||
|
border-color: rgba(74, 222, 128, 0.4);
|
||||||
|
background: rgba(22, 163, 74, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.z2m-badge.off {
|
||||||
|
color: #f87171;
|
||||||
|
border-color: rgba(248, 113, 113, 0.4);
|
||||||
|
background: rgba(248, 113, 113, 0.12);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function index()
|
||||||
|
if not nixio.fs.access('/etc/config/zigbee2mqtt') then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local root = node('admin', 'secubox')
|
||||||
|
if not root then
|
||||||
|
root = entry({'admin', 'secubox'}, firstchild(), _('SecuBox'), 10)
|
||||||
|
end
|
||||||
|
|
||||||
|
entry({'admin', 'secubox', 'zigbee2mqtt'}, firstchild(), _('Zigbee2MQTT'), 50).dependent = false
|
||||||
|
entry({'admin', 'secubox', 'zigbee2mqtt', 'overview'}, view('zigbee2mqtt/overview'), _('Overview'), 10).leaf = true
|
||||||
|
end
|
||||||
141
luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt
Executable file
141
luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt
Executable file
@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
CONFIG="zigbee2mqtt"
|
||||||
|
SERVICE="/etc/init.d/zigbee2mqtt"
|
||||||
|
CTL="/usr/sbin/zigbee2mqttctl"
|
||||||
|
|
||||||
|
load_config() {
|
||||||
|
json_init
|
||||||
|
json_add_string "serial_port" "$(uci -q get ${CONFIG}.main.serial_port || echo /dev/ttyACM0)"
|
||||||
|
json_add_string "mqtt_host" "$(uci -q get ${CONFIG}.main.mqtt_host || echo mqtt://127.0.0.1:1883)"
|
||||||
|
json_add_string "mqtt_username" "$(uci -q get ${CONFIG}.main.mqtt_username || printf '')"
|
||||||
|
json_add_string "mqtt_password" "$(uci -q get ${CONFIG}.main.mqtt_password || printf '')"
|
||||||
|
json_add_string "base_topic" "$(uci -q get ${CONFIG}.main.base_topic || echo zigbee2mqtt)"
|
||||||
|
json_add_string "frontend_port" "$(uci -q get ${CONFIG}.main.frontend_port || echo 8080)"
|
||||||
|
json_add_string "channel" "$(uci -q get ${CONFIG}.main.channel || echo 11)"
|
||||||
|
json_add_string "data_path" "$(uci -q get ${CONFIG}.main.data_path || echo /srv/zigbee2mqtt)"
|
||||||
|
json_add_string "image" "$(uci -q get ${CONFIG}.main.image || echo ghcr.io/koenkk/zigbee2mqtt:latest)"
|
||||||
|
json_add_string "timezone" "$(uci -q get ${CONFIG}.main.timezone || echo UTC)"
|
||||||
|
json_add_boolean "enabled" "$( [ "$(uci -q get ${CONFIG}.main.enabled || echo 0)" = "1" ] && echo 1 || echo 0)"
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
json_init
|
||||||
|
load_config
|
||||||
|
json_add_object "service"
|
||||||
|
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
|
||||||
|
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
|
||||||
|
json_add_string "name" "$name"
|
||||||
|
json_add_string "status" "$st"
|
||||||
|
json_close_object
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
apply() {
|
||||||
|
read input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var serial_port serial_port
|
||||||
|
json_get_var mqtt_host mqtt_host
|
||||||
|
json_get_var mqtt_username mqtt_username
|
||||||
|
json_get_var mqtt_password mqtt_password
|
||||||
|
json_get_var base_topic base_topic
|
||||||
|
json_get_var frontend_port frontend_port
|
||||||
|
json_get_var channel channel
|
||||||
|
json_get_var data_path data_path
|
||||||
|
json_get_var image image
|
||||||
|
json_get_var timezone timezone
|
||||||
|
json_get_var enabled enabled
|
||||||
|
|
||||||
|
[ -n "$serial_port" ] && uci set ${CONFIG}.main.serial_port="$serial_port"
|
||||||
|
[ -n "$mqtt_host" ] && uci set ${CONFIG}.main.mqtt_host="$mqtt_host"
|
||||||
|
[ -n "$mqtt_username" ] && uci set ${CONFIG}.main.mqtt_username="$mqtt_username"
|
||||||
|
[ -n "$mqtt_password" ] && uci set ${CONFIG}.main.mqtt_password="$mqtt_password"
|
||||||
|
[ -n "$base_topic" ] && uci set ${CONFIG}.main.base_topic="$base_topic"
|
||||||
|
[ -n "$frontend_port" ] && uci set ${CONFIG}.main.frontend_port="$frontend_port"
|
||||||
|
[ -n "$channel" ] && uci set ${CONFIG}.main.channel="$channel"
|
||||||
|
[ -n "$data_path" ] && uci set ${CONFIG}.main.data_path="$data_path"
|
||||||
|
[ -n "$image" ] && uci set ${CONFIG}.main.image="$image"
|
||||||
|
[ -n "$timezone" ] && uci set ${CONFIG}.main.timezone="$timezone"
|
||||||
|
[ -n "$enabled" ] && uci set ${CONFIG}.main.enabled="$enabled"
|
||||||
|
uci commit ${CONFIG}
|
||||||
|
|
||||||
|
if [ "$enabled" = "1" ]; then
|
||||||
|
"$SERVICE" enable >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
"$SERVICE" disable >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$SERVICE" restart >/dev/null 2>&1
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
logs() {
|
||||||
|
read input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var tail tail
|
||||||
|
tail=${tail:-200}
|
||||||
|
json_init
|
||||||
|
json_add_array "lines"
|
||||||
|
$CTL logs --tail "$tail" 2>&1 | while IFS= read -r line; do
|
||||||
|
json_add_string "" "$line"
|
||||||
|
done
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
control() {
|
||||||
|
read input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var action action
|
||||||
|
case "$action" in
|
||||||
|
start) "$SERVICE" start ;;
|
||||||
|
stop) "$SERVICE" stop ;;
|
||||||
|
restart) "$SERVICE" restart ;;
|
||||||
|
*) json_init; json_add_boolean "success" 0; json_add_string "error" "invalid action"; json_dump; return ;;
|
||||||
|
esac
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
$CTL update >/dev/null 2>&1
|
||||||
|
json_init
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
cat <<'JSON'
|
||||||
|
{
|
||||||
|
"status": {},
|
||||||
|
"apply": {},
|
||||||
|
"logs": {},
|
||||||
|
"control": {},
|
||||||
|
"update": {}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status) status ;;
|
||||||
|
apply) apply ;;
|
||||||
|
logs) logs ;;
|
||||||
|
control) control ;;
|
||||||
|
update) update ;;
|
||||||
|
*) json_init; json_add_string "error" "unknown method"; json_dump ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"admin/secubox/zigbee2mqtt": {
|
||||||
|
"title": "Zigbee2MQTT",
|
||||||
|
"order": 50,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "zigbee2mqtt/overview"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"luci-app-zigbee2mqtt": {
|
||||||
|
"description": "Access control for Zigbee2MQTT LuCI module",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.zigbee2mqtt": [
|
||||||
|
"status",
|
||||||
|
"logs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"/etc/config/zigbee2mqtt": [ "read" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.zigbee2mqtt": [
|
||||||
|
"apply",
|
||||||
|
"control",
|
||||||
|
"update"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"file": {
|
||||||
|
"/etc/config/zigbee2mqtt": [ "write" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user