feat: mqtt daemon automations and presets
This commit is contained in:
parent
16e16a6180
commit
ee5c001572
@ -4,7 +4,7 @@
|
||||
|
||||
- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system.
|
||||
- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates.
|
||||
- New `mqtt-bridge-monitor` daemon keeps adapter metadata (port/bus/health) synced and logs detection events for SecuBox.
|
||||
- New `mqtt-bridge` daemon keeps adapter metadata (port/bus/health) synced, updates stats, and runs automation rules/templates.
|
||||
- Unified Monitoring + Modules filters and Help view with SecuNav styling.
|
||||
- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks.
|
||||
- Verified on router (scp + cache reset) and tagged release v0.5.0-A.
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
# TODO – MQTT Bridge
|
||||
|
||||
1. **Daemon Integration**
|
||||
- Implement `/usr/sbin/mqtt-bridge` watcher handling USB serial adapters.
|
||||
- Emit stats to `uci set mqtt-bridge.stats.*` for UI refresh.
|
||||
|
||||
2. **Security**
|
||||
1. **Security**
|
||||
- Support TLS options (CA, client certs) in Settings.
|
||||
- Add access control for pairing window.
|
||||
|
||||
3. **Automations**
|
||||
- Add topic templates per device type (Zigbee, Modbus).
|
||||
- Provide rules to forward payloads into SecuBox Alerts.
|
||||
2. **Automations**
|
||||
- Expand rules to trigger SecuBox Alerts via ubus +/- integrate with alerting UI.
|
||||
- Wire topic templates into actual payload routing once the MQTT daemon is implemented.
|
||||
|
||||
4. **Profiles**
|
||||
- Allow LuCI to edit adapter entries (enable/disable, rename, override serial port).
|
||||
- Surface per-adapter health metrics/uptime graphs and expose actions (rescan, reset).
|
||||
3. **Profiles**
|
||||
- Visualize adapter health trends (sparklines) and expose multi-port mapping options.
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
- Added RPC backend (`luci.mqtt-bridge`) and UCI defaults for broker/bridge stats.
|
||||
- Added Zigbee/SMSC USB2134B preset detection (USB VID/PID scan, tty hinting, LuCI cards + docs).
|
||||
- Added `/usr/sbin/mqtt-bridge-monitor` + init.d service to keep adapter sections (port/bus/health) in sync.
|
||||
- Promoted the monitor into `/usr/sbin/mqtt-bridge` daemon with stats tracking, automation rules, topic templates, and LuCI-side preset import/rescan/reset actions.
|
||||
|
||||
## In Progress
|
||||
- Flesh out real USB discovery and MQTT client integration.
|
||||
|
||||
@ -76,9 +76,38 @@ The package now installs a lightweight watcher (`/usr/sbin/mqtt-bridge-monitor`)
|
||||
- Writes state transitions to the system log (`logread -e mqtt-bridge-monitor`).
|
||||
- Updates each adapter section with `detected`, `port`, `bus`, `device`, `health`, and `last_seen`, which the LuCI Devices tab now surfaces.
|
||||
- The MQTT Settings view exposes the same adapter entries so you can enable/disable presets, rename labels, or override `/dev/tty*` assignments without leaving the UI.
|
||||
- Buttons in the Settings view let you trigger a rescan (`API.rescanAdapters`) or clear cached data for a specific adapter (`API.resetAdapter`), which is helpful after re-flashing dongles or moving USB ports.
|
||||
|
||||
Use `uci show mqtt-bridge.adapter` to inspect the persisted metadata, or `ubus call luci.mqtt-bridge status` to see the JSON payload consumed by the UI.
|
||||
|
||||
## Templates & automation rules
|
||||
|
||||
`/etc/config/mqtt-bridge` now includes `config template` definitions for Zigbee and Modbus devices:
|
||||
|
||||
```uci
|
||||
config template 'zigbee_default'
|
||||
option device_type 'zigbee'
|
||||
option topic 'secubox/zigbee/{id}/state'
|
||||
option qos '1'
|
||||
option retain '1'
|
||||
```
|
||||
|
||||
These are exported through the `status` RPC (`templates` array) so LuCI or external clients can suggest topic patterns per device type. Add your own sections to cover other buses or naming schemes.
|
||||
|
||||
Automation rules (`config rule`) can react to adapter state transitions:
|
||||
|
||||
```uci
|
||||
config rule 'zigbee_disconnect'
|
||||
option type 'adapter_status'
|
||||
option adapter 'zigbee_usb2134'
|
||||
option when 'missing'
|
||||
option action 'alert'
|
||||
option message 'Zigbee USB bridge disconnected'
|
||||
option topic 'alerts/mqtt/zigbee'
|
||||
```
|
||||
|
||||
When the daemon notices the adapter go `missing` or `online`, matching rules write to syslog and `/tmp/mqtt-bridge-alerts.log`, making it easy to forward events into SecuBox Alerts or any other pipeline.
|
||||
|
||||
## Next steps
|
||||
|
||||
- Add real daemon integration with Mosquitto.
|
||||
|
||||
@ -22,7 +22,15 @@ The LuCI views depend on the SecuBox theme bundle included in `luci-theme-secubo
|
||||
|
||||
## Daemon / Monitor
|
||||
|
||||
`/usr/sbin/mqtt-bridge-monitor` (started via `/etc/init.d/mqtt-bridge`) polls configured adapter presets, logs plug/unplug events, and updates `/etc/config/mqtt-bridge` with `detected`, `port`, `bus`, `device`, and `health` metadata. The Devices view consumes those values to surface Zigbee/serial presets along with `dmesg` hints for `/dev/tty*` alignment.
|
||||
`/usr/sbin/mqtt-bridge` (started via `/etc/init.d/mqtt-bridge`) polls configured adapter presets, logs plug/unplug events, and updates `/etc/config/mqtt-bridge` with `detected`, `port`, `bus`, `device`, `health`, and `last_seen` metadata. The daemon also keeps `mqtt-bridge.stats.*` fresh (clients, messages/sec, uptime) and executes automation rules defined in the config. The Devices/Settings views consume those values to surface Zigbee/serial presets along with `dmesg` hints for `/dev/tty*` alignment.
|
||||
|
||||
Legacy `/usr/sbin/mqtt-bridge-monitor` is kept as a wrapper for backwards compatibility and now simply execs the unified daemon.
|
||||
|
||||
## Topic templates & rules
|
||||
|
||||
`/etc/config/mqtt-bridge` ships with starter `config template` entries (Zigbee/Modbus) describing MQTT topic patterns per device type. You can add/override templates and the RPC API exposes them so LuCI (or automation tooling) can build device-specific topics dynamically.
|
||||
|
||||
`config rule` sections define automation hooks. The daemon currently supports `type adapter_status` with `action alert|rescan`. When adapter health transitions (e.g. online → missing) the matching rule logs to syslog and appends to `/tmp/mqtt-bridge-alerts.log`, which you can ingest into SecuBox Alerts or other systems.
|
||||
|
||||
## Development Notes
|
||||
|
||||
|
||||
@ -25,9 +25,22 @@ var callApplySettings = rpc.declare({
|
||||
method: 'apply_settings'
|
||||
});
|
||||
|
||||
var callRescanAdapters = rpc.declare({
|
||||
object: 'luci.mqtt-bridge',
|
||||
method: 'rescan_adapters',
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callResetAdapter = rpc.declare({
|
||||
object: 'luci.mqtt-bridge',
|
||||
method: 'reset_adapter'
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
listDevices: callListDevices,
|
||||
triggerPairing: callTriggerPairing,
|
||||
applySettings: callApplySettings
|
||||
applySettings: callApplySettings,
|
||||
rescanAdapters: callRescanAdapters,
|
||||
resetAdapter: callResetAdapter
|
||||
});
|
||||
|
||||
@ -729,6 +729,45 @@ pre {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mb-adapter-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mb-adapter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mb-health {
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--mb-border);
|
||||
}
|
||||
|
||||
.mb-health.online {
|
||||
color: #22c55e;
|
||||
border-color: rgba(34, 197, 94, 0.6);
|
||||
background: rgba(34, 197, 94, 0.12);
|
||||
}
|
||||
|
||||
.mb-health.missing {
|
||||
color: #f97316;
|
||||
border-color: rgba(249, 115, 22, 0.6);
|
||||
background: rgba(249, 115, 22, 0.12);
|
||||
}
|
||||
|
||||
.mb-health.disabled,
|
||||
.mb-health.unknown {
|
||||
color: var(--mb-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mqtt-bridge-dashboard {
|
||||
padding: 16px;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
'require ui';
|
||||
'require form';
|
||||
'require dom';
|
||||
'require dom';
|
||||
|
||||
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||
@ -86,7 +87,9 @@ return view.extend({
|
||||
E('strong', {}, adapter.label || id || _('Adapter')),
|
||||
E('div', { 'class': 'mb-profile-meta' }, [
|
||||
adapter.vendor && adapter.product ? _('VID:PID ') + adapter.vendor + ':' + adapter.product : null,
|
||||
adapter.port ? _('Port ') + adapter.port : null
|
||||
adapter.port ? _('Port ') + adapter.port : null,
|
||||
adapter.health ? _('Health ') + adapter.health : null,
|
||||
adapter.last_seen ? _('Last seen ') + adapter.last_seen : null
|
||||
].filter(Boolean).map(function(entry) {
|
||||
return E('span', {}, entry);
|
||||
}))
|
||||
@ -102,6 +105,21 @@ return view.extend({
|
||||
]),
|
||||
this.input(this.makeAdapterInputId(id, 'custom-label'), _('Display label'), adapter.label || id),
|
||||
this.input(this.makeAdapterInputId(id, 'custom-port'), _('Preferred /dev/tty*'), adapter.port || '', 'text'),
|
||||
E('div', { 'class': 'mb-adapter-footer' }, [
|
||||
E('span', {
|
||||
'class': 'mb-health ' + this.healthClass(adapter.health)
|
||||
}, (adapter.health || _('unknown')).toString()),
|
||||
E('div', { 'class': 'mb-adapter-actions' }, [
|
||||
E('button', {
|
||||
'class': 'mb-btn mb-btn-secondary',
|
||||
'click': this.handleRescan.bind(this, id)
|
||||
}, ['🔄 ', _('Rescan')]),
|
||||
E('button', {
|
||||
'class': 'mb-btn mb-btn-secondary',
|
||||
'click': this.handleReset.bind(this, id)
|
||||
}, ['♻️ ', _('Reset')])
|
||||
])
|
||||
]),
|
||||
adapter.notes ? E('p', { 'class': 'mb-profile-notes' }, adapter.notes) : null
|
||||
]);
|
||||
},
|
||||
@ -168,6 +186,10 @@ return view.extend({
|
||||
return (id || '').replace(/[^a-z0-9_-]/ig, '_') || 'adapter_' + Math.random().toString(36).slice(2, 7);
|
||||
},
|
||||
|
||||
healthClass: function(val) {
|
||||
return (val || 'unknown').toString().toLowerCase().replace(/[^a-z0-9_-]/g, '-');
|
||||
},
|
||||
|
||||
cloneAdapters: function(list) {
|
||||
var cloned = [];
|
||||
(list || []).forEach(function(item) {
|
||||
@ -245,12 +267,52 @@ return view.extend({
|
||||
product: profile.product || '',
|
||||
port: profile.port || '',
|
||||
enabled: profile.detected ? 1 : 0,
|
||||
detected: profile.detected ? 1 : 0,
|
||||
health: profile.detected ? 'online' : 'missing',
|
||||
preset: profile.id || profile.preset || ''
|
||||
});
|
||||
this.refreshAdapterGrid();
|
||||
ui.addNotification(null, E('p', {}, _('Preset added. Remember to save preferences.')), 'info');
|
||||
},
|
||||
|
||||
handleRescan: function(id) {
|
||||
ui.showModal(_('Rescanning adapters'), [
|
||||
E('p', {}, _('Triggering daemon rescan…')),
|
||||
E('div', { 'class': 'spinning' })
|
||||
]);
|
||||
return API.rescanAdapters().then(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Rescan triggered. Refresh status after a few seconds.')), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleReset: function(id) {
|
||||
if (!id)
|
||||
return;
|
||||
var self = this;
|
||||
ui.showModal(_('Reset adapter'), [
|
||||
E('p', {}, _('Clear cached detection info for ') + id + '?'),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': function() {
|
||||
API.resetAdapter({ adapter: id }).then(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Adapter reset. Wait for next daemon scan.')), 'info');
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||||
});
|
||||
}
|
||||
}, _('Reset'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
savePreferences: function() {
|
||||
var payload = this.collectSettings();
|
||||
payload.adapters = this.collectAdapters();
|
||||
|
||||
@ -24,6 +24,34 @@ config adapter 'zigbee_usb2134'
|
||||
option device ''
|
||||
option health 'unknown'
|
||||
|
||||
config template 'zigbee_default'
|
||||
option device_type 'zigbee'
|
||||
option topic 'secubox/zigbee/{id}/state'
|
||||
option qos '1'
|
||||
option retain '1'
|
||||
|
||||
config template 'modbus_default'
|
||||
option device_type 'modbus'
|
||||
option topic 'secubox/modbus/{id}/state'
|
||||
option qos '0'
|
||||
option retain '0'
|
||||
|
||||
config rule 'zigbee_disconnect'
|
||||
option type 'adapter_status'
|
||||
option adapter 'zigbee_usb2134'
|
||||
option when 'missing'
|
||||
option action 'alert'
|
||||
option message 'Zigbee USB bridge disconnected'
|
||||
option topic 'alerts/mqtt/zigbee'
|
||||
|
||||
config rule 'zigbee_online'
|
||||
option type 'adapter_status'
|
||||
option adapter 'zigbee_usb2134'
|
||||
option when 'online'
|
||||
option action 'alert'
|
||||
option message 'Zigbee USB bridge is online again'
|
||||
option topic 'alerts/mqtt/zigbee'
|
||||
|
||||
config stats 'stats'
|
||||
option clients '0'
|
||||
option mps '0'
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
START=95
|
||||
USE_PROCD=1
|
||||
SERVICE_NAME="mqtt-bridge-monitor"
|
||||
SERVICE_NAME="mqtt-bridge"
|
||||
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/sbin/mqtt-bridge-monitor
|
||||
procd_set_param command /usr/sbin/mqtt-bridge
|
||||
procd_set_param respawn 0 5 5
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
@ -113,6 +113,56 @@ append_configured_adapters() {
|
||||
json_close_array
|
||||
}
|
||||
|
||||
add_template_json() {
|
||||
local section="$1"
|
||||
local device_type topic qos retain
|
||||
config_get device_type "$section" device_type
|
||||
config_get topic "$section" topic
|
||||
config_get qos "$section" qos
|
||||
config_get retain "$section" retain
|
||||
json_add_object
|
||||
json_add_string "id" "$section"
|
||||
[ -n "$device_type" ] && json_add_string "device_type" "$device_type"
|
||||
[ -n "$topic" ] && json_add_string "topic" "$topic"
|
||||
[ -n "$qos" ] && json_add_string "qos" "$qos"
|
||||
[ -n "$retain" ] && json_add_string "retain" "$retain"
|
||||
json_close_object
|
||||
}
|
||||
|
||||
append_templates() {
|
||||
json_add_array "templates"
|
||||
config_load mqtt-bridge
|
||||
config_foreach add_template_json template
|
||||
json_close_array
|
||||
}
|
||||
|
||||
add_rule_json() {
|
||||
local section="$1"
|
||||
local type adapter when action message topic
|
||||
config_get type "$section" type
|
||||
config_get adapter "$section" adapter
|
||||
config_get when "$section" when
|
||||
config_get action "$section" action
|
||||
config_get message "$section" message
|
||||
config_get topic "$section" topic
|
||||
json_add_object
|
||||
json_add_string "id" "$section"
|
||||
[ -n "$type" ] && json_add_string "type" "$type"
|
||||
[ -n "$adapter" ] && json_add_string "adapter" "$adapter"
|
||||
[ -n "$when" ] && json_add_string "when" "$when"
|
||||
[ -n "$action" ] && json_add_string "action" "$action"
|
||||
[ -n "$message" ] && json_add_string "message" "$message"
|
||||
[ -n "$topic" ] && json_add_string "topic" "$topic"
|
||||
json_close_object
|
||||
}
|
||||
|
||||
append_rules() {
|
||||
json_add_array "rules"
|
||||
config_load mqtt-bridge
|
||||
config_foreach add_rule_json rule
|
||||
json_close_array
|
||||
}
|
||||
|
||||
apply_adapter_settings() {
|
||||
local adapter_keys
|
||||
json_get_keys adapter_keys
|
||||
@ -189,20 +239,22 @@ status() {
|
||||
done
|
||||
json_close_array
|
||||
|
||||
json_add_object "settings"
|
||||
json_add_string "host" "$(uci -q get mqtt-bridge.broker.host || echo '127.0.0.1')"
|
||||
json_add_int "port" "$(uci -q get mqtt-bridge.broker.port || echo 1883)"
|
||||
json_add_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')"
|
||||
json_add_string "password" ""
|
||||
json_add_string "base_topic" "$(uci -q get mqtt-bridge.bridge.base_topic || echo 'secubox/+/state')"
|
||||
json_add_int "retention" "$(uci -q get mqtt-bridge.bridge.retention || echo 7)"
|
||||
json_close_object
|
||||
json_add_object "settings"
|
||||
json_add_string "host" "$(uci -q get mqtt-bridge.broker.host || echo '127.0.0.1')"
|
||||
json_add_int "port" "$(uci -q get mqtt-bridge.broker.port || echo 1883)"
|
||||
json_add_string "username" "$(uci -q get mqtt-bridge.broker.username || echo '')"
|
||||
json_add_string "password" ""
|
||||
json_add_string "base_topic" "$(uci -q get mqtt-bridge.bridge.base_topic || echo 'secubox/+/state')"
|
||||
json_add_int "retention" "$(uci -q get mqtt-bridge.bridge.retention || echo 7)"
|
||||
json_close_object
|
||||
|
||||
json_add_array "profiles"
|
||||
append_zigbee_profile
|
||||
json_close_array
|
||||
|
||||
append_configured_adapters
|
||||
append_templates
|
||||
append_rules
|
||||
|
||||
json_dump
|
||||
}
|
||||
@ -266,6 +318,42 @@ apply_settings() {
|
||||
json_dump
|
||||
}
|
||||
|
||||
rescan_adapters() {
|
||||
/usr/sbin/mqtt-bridge --rescan >/dev/null 2>&1 &
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "rescan_triggered"
|
||||
json_dump
|
||||
}
|
||||
|
||||
reset_adapter() {
|
||||
read input
|
||||
json_load "$input"
|
||||
json_get_var adapter adapter
|
||||
json_cleanup
|
||||
|
||||
if [ -z "$adapter" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "missing_adapter"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
uci delete mqtt-bridge.adapter."$adapter".port >/dev/null 2>&1
|
||||
uci delete mqtt-bridge.adapter."$adapter".bus >/dev/null 2>&1
|
||||
uci delete mqtt-bridge.adapter."$adapter".device >/dev/null 2>&1
|
||||
uci delete mqtt-bridge.adapter."$adapter".detected >/dev/null 2>&1
|
||||
uci delete mqtt-bridge.adapter."$adapter".health >/dev/null 2>&1
|
||||
uci delete mqtt-bridge.adapter."$adapter".last_seen >/dev/null 2>&1
|
||||
uci commit mqtt-bridge
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "adapter_reset"
|
||||
json_dump
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<'JSON'
|
||||
@ -273,7 +361,9 @@ case "$1" in
|
||||
"status": {},
|
||||
"list_devices": {},
|
||||
"trigger_pairing": {},
|
||||
"apply_settings": {}
|
||||
"apply_settings": {},
|
||||
"rescan_adapters": {},
|
||||
"reset_adapter": {}
|
||||
}
|
||||
JSON
|
||||
;;
|
||||
@ -283,6 +373,8 @@ JSON
|
||||
list_devices) list_devices ;;
|
||||
trigger_pairing) trigger_pairing ;;
|
||||
apply_settings) apply_settings ;;
|
||||
rescan_adapters) rescan_adapters ;;
|
||||
reset_adapter) reset_adapter ;;
|
||||
*)
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
|
||||
275
luci-app-mqtt-bridge/root/usr/sbin/mqtt-bridge
Executable file
275
luci-app-mqtt-bridge/root/usr/sbin/mqtt-bridge
Executable file
@ -0,0 +1,275 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SecuBox MQTT Bridge daemon
|
||||
# Handles USB adapter discovery, stats tracking, and automation hooks.
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
UCI_NAMESPACE="mqtt-bridge"
|
||||
LOGTAG="mqtt-bridge"
|
||||
SCAN_INTERVAL=10
|
||||
COMMIT_NEEDED=0
|
||||
START_TIME="$(date +%s)"
|
||||
PREV_PAYLOAD_COUNT=0
|
||||
PREV_TIMESTAMP="$START_TIME"
|
||||
LAST_EVENT=""
|
||||
|
||||
log_msg() {
|
||||
logger -t "$LOGTAG" "$*"
|
||||
}
|
||||
|
||||
format_duration() {
|
||||
local seconds="$1"
|
||||
local h=$((seconds / 3600))
|
||||
local m=$(( (seconds % 3600) / 60 ))
|
||||
local s=$((seconds % 60))
|
||||
printf '%02dh %02dm %02ds' "$h" "$m" "$s"
|
||||
}
|
||||
|
||||
load_interval() {
|
||||
config_load "$UCI_NAMESPACE"
|
||||
config_get interval monitor interval
|
||||
[ -n "$interval" ] && SCAN_INTERVAL="$interval"
|
||||
}
|
||||
|
||||
find_usb_device() {
|
||||
local vendor="$1"
|
||||
local product="$2"
|
||||
local dev
|
||||
|
||||
for dev in /sys/bus/usb/devices/*; do
|
||||
[ -f "$dev/idVendor" ] || continue
|
||||
[ -f "$dev/idProduct" ] || continue
|
||||
local idVendor idProduct
|
||||
idVendor="$(cat "$dev/idVendor" 2>/dev/null)"
|
||||
idProduct="$(cat "$dev/idProduct" 2>/dev/null)"
|
||||
[ "$idVendor" = "$vendor" ] || continue
|
||||
[ "$idProduct" = "$product" ] || continue
|
||||
echo "$dev"
|
||||
return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
find_usb_tty() {
|
||||
local base="$1"
|
||||
local path node
|
||||
for path in "$base" "$base"/* "$base"/*/*; do
|
||||
[ -d "$path/tty" ] || continue
|
||||
for node in "$path"/tty/*; do
|
||||
[ -e "$node" ] || continue
|
||||
local tty
|
||||
tty="$(basename "$node")"
|
||||
[ -e "/dev/$tty" ] && { echo "/dev/$tty"; return 0; }
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
set_option_if_changed() {
|
||||
local section="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local current
|
||||
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
|
||||
[ "$current" = "$value" ] && return
|
||||
uci set ${UCI_NAMESPACE}.adapter.${section}.${key}="$value"
|
||||
COMMIT_NEEDED=1
|
||||
}
|
||||
|
||||
clear_option_if_needed() {
|
||||
local section="$1"
|
||||
local key="$2"
|
||||
local current
|
||||
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
|
||||
[ -z "$current" ] && return
|
||||
uci delete ${UCI_NAMESPACE}.adapter.${section}.${key}
|
||||
COMMIT_NEEDED=1
|
||||
}
|
||||
|
||||
publish_alert() {
|
||||
local message="$1"
|
||||
local topic="$2"
|
||||
[ -n "$message" ] || return
|
||||
log_msg "ALERT: $message"
|
||||
if [ -n "$topic" ]; then
|
||||
printf '%s %s\n' "$(date -Iseconds)" "$message" >> /tmp/mqtt-bridge-alerts.log
|
||||
fi
|
||||
}
|
||||
|
||||
RULE_MATCH_ADAPTER=""
|
||||
RULE_MATCH_STATE=""
|
||||
|
||||
apply_rule() {
|
||||
local section="$1"
|
||||
local target when action message topic
|
||||
config_get target "$section" adapter
|
||||
config_get when "$section" when
|
||||
config_get action "$section" action
|
||||
config_get message "$section" message
|
||||
config_get topic "$section" topic
|
||||
[ "$RULE_MATCH_ADAPTER" = "$target" ] || return
|
||||
[ "$RULE_MATCH_STATE" = "$when" ] || return
|
||||
|
||||
case "$action" in
|
||||
alert)
|
||||
publish_alert "$message" "$topic"
|
||||
;;
|
||||
rescan)
|
||||
log_msg "Rule $section triggered rescan for $target"
|
||||
run_detection_once
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_rules() {
|
||||
RULE_MATCH_ADAPTER="$1"
|
||||
RULE_MATCH_STATE="$2"
|
||||
config_foreach apply_rule rule
|
||||
}
|
||||
|
||||
update_adapter_section() {
|
||||
local section="$1"
|
||||
local enabled vendor product title preset
|
||||
|
||||
config_get enabled "$section" enabled "1"
|
||||
config_get vendor "$section" vendor
|
||||
config_get product "$section" product
|
||||
config_get preset "$section" preset
|
||||
config_get title "$section" title
|
||||
|
||||
if [ "$enabled" != "1" ]; then
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "disabled"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$vendor" ] || [ -z "$product" ]; then
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "unknown"
|
||||
return
|
||||
fi
|
||||
|
||||
local dev_path
|
||||
dev_path="$(find_usb_device "$vendor" "$product")" || dev_path=""
|
||||
|
||||
local prev_detected
|
||||
prev_detected="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.detected 2>/dev/null)"
|
||||
|
||||
if [ -n "$dev_path" ]; then
|
||||
local bus devnum port ts
|
||||
bus="$(cat "$dev_path/busnum" 2>/dev/null)"
|
||||
devnum="$(cat "$dev_path/devnum" 2>/dev/null)"
|
||||
port="$(find_usb_tty "$dev_path")"
|
||||
ts="$(date -Iseconds)"
|
||||
|
||||
set_option_if_changed "$section" detected "1"
|
||||
set_option_if_changed "$section" health "online"
|
||||
[ -n "$bus" ] && set_option_if_changed "$section" bus "$bus"
|
||||
[ -n "$devnum" ] && set_option_if_changed "$section" device "$devnum"
|
||||
if [ -n "$port" ]; then
|
||||
set_option_if_changed "$section" port "$port"
|
||||
else
|
||||
clear_option_if_needed "$section" port
|
||||
fi
|
||||
set_option_if_changed "$section" last_seen "$ts"
|
||||
|
||||
if [ "$prev_detected" != "1" ]; then
|
||||
LAST_EVENT="$ts"
|
||||
log_msg "Adapter $section ($title) detected on bus $bus dev $devnum $port"
|
||||
run_rules "$section" "online"
|
||||
fi
|
||||
else
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "missing"
|
||||
clear_option_if_needed "$section" port
|
||||
clear_option_if_needed "$section" bus
|
||||
clear_option_if_needed "$section" device
|
||||
if [ "$prev_detected" = "1" ]; then
|
||||
LAST_EVENT="$(date -Iseconds)"
|
||||
log_msg "Adapter $section ($title) disconnected"
|
||||
run_rules "$section" "missing"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
count_enabled_clients() {
|
||||
local count=0
|
||||
config_foreach _count_client adapter "$1"
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
_count_client() {
|
||||
local section="$1"
|
||||
local enabled detected
|
||||
config_get enabled "$section" enabled 1
|
||||
config_get detected "$section" detected 0
|
||||
if [ "$enabled" = "1" ] && [ "$detected" = "1" ]; then
|
||||
count=$((count + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
payload_count() {
|
||||
uci -q show mqtt-bridge.payloads 2>/dev/null | grep -c '=payload'
|
||||
}
|
||||
|
||||
update_stats() {
|
||||
local now payloads clients delta count elapsed
|
||||
now="$(date +%s)"
|
||||
|
||||
config_load "$UCI_NAMESPACE"
|
||||
count=0
|
||||
config_foreach _count_client adapter
|
||||
clients="$count"
|
||||
|
||||
payloads="$(payload_count)"
|
||||
elapsed=$((now - PREV_TIMESTAMP))
|
||||
if [ "$elapsed" -gt 0 ]; then
|
||||
delta=$((payloads - PREV_PAYLOAD_COUNT))
|
||||
if [ "$delta" -lt 0 ]; then
|
||||
delta=0
|
||||
fi
|
||||
local mps=$((delta / elapsed))
|
||||
uci set ${UCI_NAMESPACE}.stats.mps="$mps"
|
||||
fi
|
||||
PREV_PAYLOAD_COUNT="$payloads"
|
||||
PREV_TIMESTAMP="$now"
|
||||
|
||||
uci set ${UCI_NAMESPACE}.stats.clients="$clients"
|
||||
uci set ${UCI_NAMESPACE}.stats.retained="${payloads:-0}"
|
||||
uci set ${UCI_NAMESPACE}.stats.uptime="$(format_duration $((now - START_TIME)))"
|
||||
[ -n "$LAST_EVENT" ] && uci set ${UCI_NAMESPACE}.stats.last_event="$LAST_EVENT"
|
||||
|
||||
uci commit ${UCI_NAMESPACE}
|
||||
}
|
||||
|
||||
run_detection_once() {
|
||||
COMMIT_NEEDED=0
|
||||
config_load "$UCI_NAMESPACE"
|
||||
config_foreach update_adapter_section adapter
|
||||
if [ "$COMMIT_NEEDED" -eq 1 ]; then
|
||||
uci commit "$UCI_NAMESPACE"
|
||||
fi
|
||||
update_stats
|
||||
}
|
||||
|
||||
daemon_loop() {
|
||||
while true; do
|
||||
load_interval
|
||||
run_detection_once
|
||||
sleep "$SCAN_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
--rescan)
|
||||
run_detection_once
|
||||
;;
|
||||
--oneshot)
|
||||
run_detection_once
|
||||
;;
|
||||
*)
|
||||
daemon_loop
|
||||
;;
|
||||
esac
|
||||
@ -1,149 +1,3 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# MQTT Bridge monitor daemon
|
||||
# Scans configured USB adapters/presets and updates UCI with live metadata.
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
UCI_NAMESPACE="mqtt-bridge"
|
||||
LOGTAG="mqtt-bridge-monitor"
|
||||
SCAN_INTERVAL=10
|
||||
COMMIT_NEEDED=0
|
||||
|
||||
log_msg() {
|
||||
logger -t "$LOGTAG" "$*"
|
||||
}
|
||||
|
||||
find_usb_device() {
|
||||
local vendor="$1"
|
||||
local product="$2"
|
||||
local dev
|
||||
|
||||
for dev in /sys/bus/usb/devices/*; do
|
||||
[ -f "$dev/idVendor" ] || continue
|
||||
[ -f "$dev/idProduct" ] || continue
|
||||
local idVendor idProduct
|
||||
idVendor="$(cat "$dev/idVendor" 2>/dev/null)"
|
||||
idProduct="$(cat "$dev/idProduct" 2>/dev/null)"
|
||||
[ "$idVendor" = "$vendor" ] || continue
|
||||
[ "$idProduct" = "$product" ] || continue
|
||||
echo "$dev"
|
||||
return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
find_usb_tty() {
|
||||
local base="$1"
|
||||
local path node
|
||||
for path in "$base" "$base"/* "$base"/*/*; do
|
||||
[ -d "$path/tty" ] || continue
|
||||
for node in "$path"/tty/*; do
|
||||
[ -e "$node" ] || continue
|
||||
local tty
|
||||
tty="$(basename "$node")"
|
||||
[ -e "/dev/$tty" ] && { echo "/dev/$tty"; return 0; }
|
||||
done
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
set_option_if_changed() {
|
||||
local section="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local current
|
||||
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
|
||||
[ "$current" = "$value" ] && return
|
||||
uci set ${UCI_NAMESPACE}.adapter.${section}.${key}="$value"
|
||||
COMMIT_NEEDED=1
|
||||
}
|
||||
|
||||
clear_option_if_needed() {
|
||||
local section="$1"
|
||||
local key="$2"
|
||||
local current
|
||||
current="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.${key} 2>/dev/null)"
|
||||
[ -z "$current" ] && return
|
||||
uci delete ${UCI_NAMESPACE}.adapter.${section}.${key}
|
||||
COMMIT_NEEDED=1
|
||||
}
|
||||
|
||||
update_adapter_section() {
|
||||
local section="$1"
|
||||
local enabled vendor product title preset
|
||||
|
||||
config_get enabled "$section" enabled "1"
|
||||
config_get vendor "$section" vendor
|
||||
config_get product "$section" product
|
||||
config_get preset "$section" preset
|
||||
config_get title "$section" title
|
||||
|
||||
if [ "$enabled" != "1" ]; then
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "disabled"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -z "$vendor" ] || [ -z "$product" ]; then
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "unknown"
|
||||
return
|
||||
fi
|
||||
|
||||
local dev_path
|
||||
dev_path="$(find_usb_device "$vendor" "$product")" || dev_path=""
|
||||
|
||||
local prev_detected
|
||||
prev_detected="$(uci -q get ${UCI_NAMESPACE}.adapter.${section}.detected 2>/dev/null)"
|
||||
|
||||
if [ -n "$dev_path" ]; then
|
||||
local bus devnum port ts
|
||||
bus="$(cat "$dev_path/busnum" 2>/dev/null)"
|
||||
devnum="$(cat "$dev_path/devnum" 2>/dev/null)"
|
||||
port="$(find_usb_tty "$dev_path")"
|
||||
ts="$(date -Iseconds)"
|
||||
|
||||
set_option_if_changed "$section" detected "1"
|
||||
set_option_if_changed "$section" health "online"
|
||||
[ -n "$bus" ] && set_option_if_changed "$section" bus "$bus"
|
||||
[ -n "$devnum" ] && set_option_if_changed "$section" device "$devnum"
|
||||
if [ -n "$port" ]; then
|
||||
set_option_if_changed "$section" port "$port"
|
||||
else
|
||||
clear_option_if_needed "$section" port
|
||||
fi
|
||||
set_option_if_changed "$section" last_seen "$ts"
|
||||
|
||||
if [ "$prev_detected" != "1" ]; then
|
||||
log_msg "Adapter $section ($title) detected on bus $bus dev $devnum $port"
|
||||
fi
|
||||
else
|
||||
set_option_if_changed "$section" detected "0"
|
||||
set_option_if_changed "$section" health "missing"
|
||||
clear_option_if_needed "$section" port
|
||||
clear_option_if_needed "$section" bus
|
||||
clear_option_if_needed "$section" device
|
||||
if [ "$prev_detected" = "1" ]; then
|
||||
log_msg "Adapter $section ($title) disconnected"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
scan_loop() {
|
||||
while true; do
|
||||
COMMIT_NEEDED=0
|
||||
config_load "$UCI_NAMESPACE"
|
||||
local interval
|
||||
config_get interval monitor interval
|
||||
[ -n "$interval" ] && SCAN_INTERVAL="$interval"
|
||||
config_foreach update_adapter_section adapter
|
||||
if [ "$COMMIT_NEEDED" -eq 1 ]; then
|
||||
uci commit "$UCI_NAMESPACE"
|
||||
fi
|
||||
sleep "$SCAN_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
scan_loop
|
||||
# Legacy wrapper – execute the unified MQTT bridge daemon.
|
||||
exec /usr/sbin/mqtt-bridge "$@"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user