#!/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