实现设备自动发现规则管理系统,包含以下主要功能: 1. 新增规则管理页面和API,支持创建、编辑和删除主题匹配规则 2. 添加规则匹配引擎,支持+和#通配符匹配设备主题 3. 实现Broker设备注册表,自动发现并管理符合规则的设备 4. 扩展仪表盘显示Broker信息和活跃主题 5. 修改设备卡片和详情页以区分规则发现的设备 6. 添加相关测试用例确保功能稳定性
108 lines
3.8 KiB
Python
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()
|