feat: mqtt daemon automations and presets

This commit is contained in:
CyberMind-FR 2025-12-29 15:18:46 +01:00
parent 16e16a6180
commit ee5c001572
13 changed files with 570 additions and 174 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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
});

View File

@ -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;

View File

@ -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();

View File

@ -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'

View File

@ -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
}

View File

@ -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

View 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

View File

@ -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 "$@"