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