From 40e937a919bdafe525275df8cd0a54d6119a32ad Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 15:55:12 +0100 Subject: [PATCH] feat: add luci interface for zigbee2mqtt --- luci-app-zigbee2mqtt/Makefile | 14 ++ luci-app-zigbee2mqtt/README.md | 53 +++++ .../resources/view/zigbee2mqtt/overview.js | 206 ++++++++++++++++++ .../luci-static/resources/zigbee2mqtt/api.js | 40 ++++ .../resources/zigbee2mqtt/common.css | 94 ++++++++ .../luasrc/controller/secubox/zigbee2mqtt.lua | 15 ++ .../root/usr/libexec/rpcd/luci.zigbee2mqtt | 141 ++++++++++++ .../luci/menu.d/luci-app-zigbee2mqtt.json | 10 + .../rpcd/acl.d/luci-app-zigbee2mqtt.json | 28 +++ 9 files changed, 601 insertions(+) create mode 100644 luci-app-zigbee2mqtt/Makefile create mode 100644 luci-app-zigbee2mqtt/README.md create mode 100644 luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js create mode 100644 luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js create mode 100644 luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css create mode 100644 luci-app-zigbee2mqtt/luasrc/controller/secubox/zigbee2mqtt.lua create mode 100755 luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt create mode 100644 luci-app-zigbee2mqtt/root/usr/share/luci/menu.d/luci-app-zigbee2mqtt.json create mode 100644 luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json diff --git a/luci-app-zigbee2mqtt/Makefile b/luci-app-zigbee2mqtt/Makefile new file mode 100644 index 00000000..5033b877 --- /dev/null +++ b/luci-app-zigbee2mqtt/Makefile @@ -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 diff --git a/luci-app-zigbee2mqtt/README.md b/luci-app-zigbee2mqtt/README.md new file mode 100644 index 00000000..397e32da --- /dev/null +++ b/luci-app-zigbee2mqtt/README.md @@ -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`. diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js new file mode 100644 index 00000000..f4adc554 --- /dev/null +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/view/zigbee2mqtt/overview.js @@ -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'); + }); + } +}); diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js new file mode 100644 index 00000000..8fef3a45 --- /dev/null +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/api.js @@ -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 +}; diff --git a/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css new file mode 100644 index 00000000..e1a7ac3f --- /dev/null +++ b/luci-app-zigbee2mqtt/htdocs/luci-static/resources/zigbee2mqtt/common.css @@ -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); +} diff --git a/luci-app-zigbee2mqtt/luasrc/controller/secubox/zigbee2mqtt.lua b/luci-app-zigbee2mqtt/luasrc/controller/secubox/zigbee2mqtt.lua new file mode 100644 index 00000000..ee6e476c --- /dev/null +++ b/luci-app-zigbee2mqtt/luasrc/controller/secubox/zigbee2mqtt.lua @@ -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 diff --git a/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt b/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt new file mode 100755 index 00000000..6fc10635 --- /dev/null +++ b/luci-app-zigbee2mqtt/root/usr/libexec/rpcd/luci.zigbee2mqtt @@ -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 diff --git a/luci-app-zigbee2mqtt/root/usr/share/luci/menu.d/luci-app-zigbee2mqtt.json b/luci-app-zigbee2mqtt/root/usr/share/luci/menu.d/luci-app-zigbee2mqtt.json new file mode 100644 index 00000000..a42fa235 --- /dev/null +++ b/luci-app-zigbee2mqtt/root/usr/share/luci/menu.d/luci-app-zigbee2mqtt.json @@ -0,0 +1,10 @@ +{ + "admin/secubox/zigbee2mqtt": { + "title": "Zigbee2MQTT", + "order": 50, + "action": { + "type": "view", + "path": "zigbee2mqtt/overview" + } + } +} diff --git a/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json b/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json new file mode 100644 index 00000000..fde90ad1 --- /dev/null +++ b/luci-app-zigbee2mqtt/root/usr/share/rpcd/acl.d/luci-app-zigbee2mqtt.json @@ -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" ] + } + } + } +}