Files
HomeOS/src/mqtt_home/broker_devices.py
walkpan 3ea47d471e feat(规则管理): 添加主题规则自动发现设备功能
实现设备自动发现规则管理系统,包含以下主要功能:
1. 新增规则管理页面和API,支持创建、编辑和删除主题匹配规则
2. 添加规则匹配引擎,支持+和#通配符匹配设备主题
3. 实现Broker设备注册表,自动发现并管理符合规则的设备
4. 扩展仪表盘显示Broker信息和活跃主题
5. 修改设备卡片和详情页以区分规则发现的设备
6. 添加相关测试用例确保功能稳定性
2026-03-30 19:28:22 +08:00

108 lines
3.8 KiB
Python

import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Optional
from mqtt_home.topic_matcher import match_topic, extract_device_id, build_command_topic, extract_state_value
logger = logging.getLogger(__name__)
@dataclass
class BrokerDevice:
"""In-memory broker device, not persisted to DB"""
id: str # "broker:{device_id}" e.g. "broker:fire"
name: str # Human-readable name, e.g. "fire"
type: str # Device type from rule, e.g. "switch"
protocol: str = "topic_rule"
mqtt_topic: str = "" # The matched topic, e.g. "home/fire"
command_topic: Optional[str] = None # Built from template, e.g. "home/fire/set"
state: Optional[str] = None # Latest payload (or extracted value)
is_online: bool = False
last_seen: Optional[datetime] = None
rule_id: int = 0
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
class BrokerDeviceRegistry:
"""In-memory registry for devices discovered via topic format rules"""
def __init__(self):
self._devices: dict[str, BrokerDevice] = {}
def update_or_create(self, topic: str, payload: str, rule) -> Optional[BrokerDevice]:
"""Try to match topic against rule pattern, create/update device if matched.
Args:
topic: The MQTT topic that received a message
payload: The message payload string
rule: TopicFormatRule instance (SQLAlchemy model or dict-like)
Returns:
Updated or created BrokerDevice, or None if topic doesn't match rule
"""
# 1. Check if topic matches the rule's pattern
pattern = rule.topic_pattern
if not match_topic(topic, pattern):
return None
# 2. Extract device_id from topic
device_id_raw = extract_device_id(topic, pattern)
if not device_id_raw:
return None
device_id = f"broker:{device_id_raw}"
# 3. Extract state value from payload
state = extract_state_value(payload, rule.state_value_path)
# 4. Build command topic from template
command_topic = build_command_topic(rule.command_template, device_id_raw)
now = datetime.now(timezone.utc)
# 5. Update existing or create new
if device_id in self._devices:
device = self._devices[device_id]
device.state = state
device.is_online = True
device.last_seen = now
device.updated_at = now
# Update command_topic in case rule changed
if command_topic:
device.command_topic = command_topic
else:
device = BrokerDevice(
id=device_id,
name=device_id_raw,
type=rule.device_type,
mqtt_topic=topic,
command_topic=command_topic,
state=state,
is_online=True,
last_seen=now,
rule_id=rule.id,
)
self._devices[device_id] = device
logger.info("Broker device discovered: %s (topic=%s, rule=%d)", device_id, topic, rule.id)
return device
def get_all(self) -> list[BrokerDevice]:
return list(self._devices.values())
def get(self, device_id: str) -> Optional[BrokerDevice]:
return self._devices.get(device_id)
def remove_by_rule(self, rule_id: int) -> list[str]:
"""Remove all devices associated with a rule. Returns list of removed device IDs."""
to_remove = [did for did, d in self._devices.items() if d.rule_id == rule_id]
for did in to_remove:
del self._devices[did]
return to_remove
def clear(self):
self._devices.clear()