Files
water-management-system/src/iot/mqtt_adapter.py
T
bot_dev1 15d2f9cee7 实现IoT模块 - 完成Issue #28: MQTT协议适配器+设备注册/发现API
- 新增设备管理器 (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的核心要求。
2026-06-15 12:59:27 +08:00

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)
}