- 新增设备管理器 (DeviceManager):支持设备CRUD、设备影子管理、设备发现 - 新增设备控制器 (DeviceController):提供REST API接口 - 新增设备模型 (Device, DeviceShadow):定义统一设备模型结构 - 新增OTA管理器 (OtaManager):支持设备固件升级管理 - 新增OTA控制器 (OtaController):提供OTA升级API接口 - 新增MQTT适配器 (MqttAdapter):支持MQTT协议连接和消息处理 - 新增IoT配置模块:支持MQTT、数据库等配置管理 - 集成IoT模块到主应用:在main.py中集成所有IoT功能 - 新增IoT模块测试:验证设备管理、影子更新、设备发现等功能 实现的功能: 1. MQTT协议适配器 - 支持连接管理、主题订阅/发布、消息处理 2. 设备注册/发现API - REST接口支持设备CRUD操作、设备影子管理 3. 统一设备模型 - 包含device_sn/type/area/position/geom等字段 4. OTA固件升级 - 支持升级任务管理、进度跟踪、状态监控 5. 设备统计分析 - 提供设备类型、状态等统计信息 完成Issue #28的核心要求。
352 lines
12 KiB
Python
352 lines
12 KiB
Python
"""
|
|
MQTT 协议适配器
|
|
负责MQTT连接管理、消息订阅/发布、消息解析等功能
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import paho.mqtt.client as mqtt
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional, Callable, List
|
|
from .models import MqttMessage
|
|
from threading import Lock
|
|
|
|
|
|
class MqttAdapter:
|
|
"""MQTT适配器"""
|
|
|
|
def __init__(self,
|
|
broker_host: str = "localhost",
|
|
broker_port: int = 1883,
|
|
username: Optional[str] = None,
|
|
password: Optional[str] = None,
|
|
client_id: str = "water-management-system"):
|
|
"""
|
|
初始化MQTT适配器
|
|
|
|
Args:
|
|
broker_host: MQTT broker地址
|
|
broker_port: MQTT broker端口
|
|
username: 用户名
|
|
password: 密码
|
|
client_id: 客户端ID
|
|
"""
|
|
self.broker_host = broker_host
|
|
self.broker_port = broker_port
|
|
self.username = username
|
|
self.password = password
|
|
self.client_id = client_id
|
|
|
|
self.client = mqtt.Client(client_id=client_id)
|
|
self.message_handlers: Dict[str, Callable] = {}
|
|
self.connected = False
|
|
self.lock = Lock()
|
|
|
|
# 配置MQTT客户端
|
|
if username and password:
|
|
self.client.username_pw_set(username, password)
|
|
|
|
# 设置回调函数
|
|
self.client.on_connect = self._on_connect
|
|
self.client.on_disconnect = self._on_disconnect
|
|
self.client.on_message = self._on_message
|
|
self.client.on_publish = self._on_publish
|
|
self.client.on_subscribe = self._on_subscribe
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
def _on_connect(self, client, userdata, flags, rc):
|
|
"""连接回调"""
|
|
if rc == 0:
|
|
self.connected = True
|
|
self.logger.info(f"Connected to MQTT broker at {self.broker_host}:{self.broker_port}")
|
|
else:
|
|
self.logger.error(f"Failed to connect to MQTT broker, return code {rc}")
|
|
|
|
def _on_disconnect(self, client, userdata, rc):
|
|
"""断开连接回调"""
|
|
self.connected = False
|
|
self.logger.warning(f"Disconnected from MQTT broker, return code {rc}")
|
|
|
|
def _on_message(self, client, userdata, msg):
|
|
"""消息接收回调"""
|
|
try:
|
|
# 解析消息
|
|
payload = json.loads(msg.payload.decode('utf-8')) if msg.payload else {}
|
|
|
|
message = MqttMessage(
|
|
topic=msg.topic,
|
|
payload=payload,
|
|
qos=msg.qos,
|
|
retain=msg.retain
|
|
)
|
|
|
|
self.logger.debug(f"Received message: {message.topic} - {message.payload}")
|
|
|
|
# 查找对应的消息处理器
|
|
for topic_pattern, handler in self.message_handlers.items():
|
|
if self._topic_matches(msg.topic, topic_pattern):
|
|
try:
|
|
handler(message)
|
|
except Exception as e:
|
|
self.logger.error(f"Error in message handler for {msg.topic}: {e}")
|
|
|
|
except json.JSONDecodeError as e:
|
|
self.logger.error(f"Failed to parse JSON message from {msg.topic}: {e}")
|
|
except Exception as e:
|
|
self.logger.error(f"Error processing message from {msg.topic}: {e}")
|
|
|
|
def _on_publish(self, client, userdata, mid):
|
|
"""发布消息回调"""
|
|
self.logger.debug(f"Message published with mid: {mid}")
|
|
|
|
def _on_subscribe(self, client, userdata, mid, granted_qos):
|
|
"""订阅回调"""
|
|
self.logger.debug(f"Subscribed with mid: {mid}, granted_qos: {granted_qos}")
|
|
|
|
def _topic_matches(self, topic: str, pattern: str) -> bool:
|
|
"""检查主题是否匹配模式"""
|
|
# 简单的通配符匹配实现
|
|
# 支持单层通配符 + 和多层通配符 #
|
|
pattern_parts = pattern.split('/')
|
|
topic_parts = topic.split('/')
|
|
|
|
if len(pattern_parts) != len(topic_parts):
|
|
return False
|
|
|
|
for p_part, t_part in zip(pattern_parts, topic_parts):
|
|
if p_part == '+' or p_part == '#':
|
|
continue
|
|
if p_part != t_part:
|
|
return False
|
|
|
|
return True
|
|
|
|
def connect(self) -> bool:
|
|
"""
|
|
连接到MQTT broker
|
|
|
|
Returns:
|
|
bool: 是否连接成功
|
|
"""
|
|
try:
|
|
self.client.connect(self.broker_host, self.broker_port, 60)
|
|
self.client.loop_start()
|
|
return True
|
|
except Exception as e:
|
|
self.logger.error(f"Failed to connect to MQTT broker: {e}")
|
|
return False
|
|
|
|
def disconnect(self):
|
|
"""断开MQTT连接"""
|
|
if self.connected:
|
|
self.client.loop_stop()
|
|
self.client.disconnect()
|
|
|
|
def is_connected(self) -> bool:
|
|
"""
|
|
检查是否已连接
|
|
|
|
Returns:
|
|
bool: 是否已连接
|
|
"""
|
|
return self.connected
|
|
|
|
def subscribe(self, topic: str, qos: int = 0) -> bool:
|
|
"""
|
|
订阅主题
|
|
|
|
Args:
|
|
topic: 主题
|
|
qos: QoS等级
|
|
|
|
Returns:
|
|
bool: 是否订阅成功
|
|
"""
|
|
try:
|
|
result = self.client.subscribe(topic, qos)
|
|
if result[0] == mqtt.MQTT_ERR_SUCCESS:
|
|
self.logger.info(f"Subscribed to topic: {topic}")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Failed to subscribe to topic: {topic}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error subscribing to topic {topic}: {e}")
|
|
return False
|
|
|
|
def unsubscribe(self, topic: str) -> bool:
|
|
"""
|
|
取消订阅主题
|
|
|
|
Args:
|
|
topic: 主题
|
|
|
|
Returns:
|
|
bool: 是否取消订阅成功
|
|
"""
|
|
try:
|
|
result = self.client.unsubscribe(topic)
|
|
if result[0] == mqtt.MQTT_ERR_SUCCESS:
|
|
self.logger.info(f"Unsubscribed from topic: {topic}")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Failed to unsubscribe from topic: {topic}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error unsubscribing from topic {topic}: {e}")
|
|
return False
|
|
|
|
def publish(self, topic: str, payload: Any, qos: int = 0, retain: bool = False) -> bool:
|
|
"""
|
|
发布消息
|
|
|
|
Args:
|
|
topic: 主题
|
|
payload: 消息内容
|
|
qos: QoS等级
|
|
retain: 是否保留消息
|
|
|
|
Returns:
|
|
bool: 是否发布成功
|
|
"""
|
|
try:
|
|
if isinstance(payload, dict):
|
|
payload = json.dumps(payload)
|
|
elif not isinstance(payload, str):
|
|
payload = str(payload)
|
|
|
|
result = self.client.publish(topic, payload, qos, retain)
|
|
if result[0] == mqtt.MQTT_ERR_SUCCESS:
|
|
self.logger.debug(f"Published to topic: {topic}")
|
|
return True
|
|
else:
|
|
self.logger.error(f"Failed to publish to topic: {topic}")
|
|
return False
|
|
except Exception as e:
|
|
self.logger.error(f"Error publishing to topic {topic}: {e}")
|
|
return False
|
|
|
|
def add_message_handler(self, topic_pattern: str, handler: Callable[[MqttMessage], None]):
|
|
"""
|
|
添加消息处理器
|
|
|
|
Args:
|
|
topic_pattern: 主题模式(支持通配符)
|
|
handler: 消息处理函数
|
|
"""
|
|
with self.lock:
|
|
self.message_handlers[topic_pattern] = handler
|
|
self.logger.info(f"Added message handler for pattern: {topic_pattern}")
|
|
|
|
def remove_message_handler(self, topic_pattern: str):
|
|
"""
|
|
移除消息处理器
|
|
|
|
Args:
|
|
topic_pattern: 主题模式
|
|
"""
|
|
with self.lock:
|
|
if topic_pattern in self.message_handlers:
|
|
del self.message_handlers[topic_pattern]
|
|
self.logger.info(f"Removed message handler for pattern: {topic_pattern}")
|
|
|
|
def subscribe_device_topics(self, device_manager):
|
|
"""
|
|
订阅设备相关主题
|
|
|
|
Args:
|
|
device_manager: 设备管理器实例
|
|
"""
|
|
# 设备状态上报
|
|
self.add_message_handler("devices/+/status", self._handle_device_status)
|
|
|
|
# 设备数据上报
|
|
self.add_message_handler("devices/+/data", self._handle_device_data)
|
|
|
|
# 设备控制命令响应
|
|
self.add_message_handler("devices/+/command/response", self._handle_command_response)
|
|
|
|
# 设备OTA状态
|
|
self.add_message_handler("devices/+/ota/status", self._handle_ota_status)
|
|
|
|
def _handle_device_status(self, message: MqttMessage):
|
|
"""处理设备状态消息"""
|
|
topic_parts = message.topic.split('/')
|
|
if len(topic_parts) >= 2:
|
|
device_sn = topic_parts[1]
|
|
status = message.payload.get('status', 'unknown')
|
|
|
|
# 更新设备状态
|
|
device = device_manager.get_device(device_sn)
|
|
if device:
|
|
from .models import DeviceStatus
|
|
try:
|
|
device.status = DeviceStatus(status)
|
|
device.last_seen = datetime.now()
|
|
device_manager.logger.info(f"Device {device_sn} status updated to {status}")
|
|
except ValueError:
|
|
device_manager.logger.warning(f"Unknown status: {status}")
|
|
|
|
def _handle_device_data(self, message: MqttMessage):
|
|
"""处理设备数据消息"""
|
|
topic_parts = message.topic.split('/')
|
|
if len(topic_parts) >= 2:
|
|
device_sn = topic_parts[1]
|
|
data = message.payload
|
|
|
|
# 更新设备影子
|
|
device_manager.update_device_shadow(device_sn, data)
|
|
device_manager.logger.debug(f"Device {device_sn} data updated")
|
|
|
|
def _handle_command_response(self, message: MqttMessage):
|
|
"""处理命令响应消息"""
|
|
topic_parts = message.topic.split('/')
|
|
if len(topic_parts) >= 2:
|
|
device_sn = topic_parts[1]
|
|
command_id = message.payload.get('command_id')
|
|
result = message.payload.get('result')
|
|
|
|
device_manager.logger.info(f"Device {device_sn} command response: {command_id} -> {result}")
|
|
|
|
def _handle_ota_status(self, message: MqttMessage):
|
|
"""处理OTA状态消息"""
|
|
topic_parts = message.topic.split('/')
|
|
if len(topic_parts) >= 2:
|
|
device_sn = topic_parts[1]
|
|
status = message.payload.get('status')
|
|
progress = message.payload.get('progress', 0)
|
|
|
|
device_manager.logger.info(f"Device {device_sn} OTA status: {status}, progress: {progress}%")
|
|
|
|
def send_command(self, device_sn: str, command: Dict[str, Any]) -> bool:
|
|
"""
|
|
发送设备控制命令
|
|
|
|
Args:
|
|
device_sn: 设备序列号
|
|
command: 命令内容
|
|
|
|
Returns:
|
|
bool: 是否发送成功
|
|
"""
|
|
topic = f"devices/{device_sn}/command"
|
|
command['command_id'] = f"cmd_{datetime.now().timestamp()}"
|
|
command['timestamp'] = datetime.now().isoformat()
|
|
|
|
return self.publish(topic, command, qos=1)
|
|
|
|
def get_connection_status(self) -> Dict[str, Any]:
|
|
"""
|
|
获取连接状态
|
|
|
|
Returns:
|
|
Dict[str, Any]: 连接状态信息
|
|
"""
|
|
return {
|
|
"connected": self.connected,
|
|
"broker_host": self.broker_host,
|
|
"broker_port": self.broker_port,
|
|
"client_id": self.client_id,
|
|
"message_handlers_count": len(self.message_handlers)
|
|
} |