from typing import Optional class MqttTopic: """ 封装 MQTT topic,根据规则: project/domain/deviceType/deviceId/resource """ LEVELS = 5 def __init__(self, topic: str): self.raw = str(topic) parts = self.raw.split("/") # 不足的层级用 None 补齐,避免属性缺失 parts += [None] * (self.LEVELS - len(parts)) self.dept_id: Optional[str] = parts[0] self.domain: Optional[str] = parts[1] self.device_type: Optional[str] = parts[2] self.device_id: Optional[str] = parts[3] self.resource: Optional[str] = parts[4] @classmethod def from_parts( cls, dept_id: Optional[str] = None, domain: Optional[str] = None, device_type: Optional[str] = None, device_id: Optional[str] = None, resource: Optional[str] = None, ) -> "MqttTopic": """ 通过结构化参数构造 topic None -> '+' """ def _v(v: Optional[str]) -> str: return "+" if v is None else str(v) topic = "/".join( map( _v, [ dept_id, domain, device_type, device_id, resource, ], ) ) return cls(topic) def to_topic(self) -> str: """ 根据当前字段生成 topic(允许 '+') """ def _v(v: Optional[str]) -> str: return "+" if v is None else v return "/".join( map( _v, [ self.dept_id, self.domain, self.device_type, self.device_id, self.resource, ], ) ) def build(self) -> str: """ 生成严格 topic(不允许 None / '+') 用于 publish 场景 """ parts = [ self.dept_id, self.domain, self.device_type, self.device_id, self.resource, ] if any(p in (None, "+") for p in parts): raise ValueError( f"Cannot build strict topic, wildcard exists: {self.to_topic()}" ) return "/".join(parts) def is_wildcard(self) -> bool: return "+" in self.to_topic() or "#" in self.to_topic() def __repr__(self): return f"" def is_status(self) -> bool: return self.domain == "status" and self.resource == "info" def is_cmd(self) -> bool: return self.domain == "cmd"