VWED_server/services/online_script/async_mqtt_service.py

354 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
异步MQTT客户端服务模块
提供高性能的异步MQTT连接、发布、订阅等功能
支持自动重连和异步消息处理
"""
import asyncio
import json
import time
import sys
from typing import Dict, Any, Optional, Callable, List
from utils.logger import get_logger
# Windows兼容性修复
if sys.platform == 'win32':
# 在Windows上使用SelectorEventLoop以支持add_reader/add_writer
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
# try:
# import aiomqtt
# except ImportError:
# try:
# # 尝试导入旧版本的asyncio-mqtt
#
# except ImportError:
# aiomqtt = None
# 如果都没有安装回退到paho-mqtt
import aiomqtt
from config.tf_api_config import MQTT_CONFIG
logger = get_logger("services.async_mqtt_service")
class AsyncMQTTService:
"""异步MQTT客户端服务"""
def __init__(self, config: Dict[str, Any] = None):
self.config = config or MQTT_CONFIG
self.client = None
self.connected = False
self.reconnect_attempts = 0
self.message_handlers: Dict[str, List[Callable]] = {}
self._running = False
self._connection_task = None
self._message_task = None
self._use_aiomqtt = aiomqtt is not None
if not self._use_aiomqtt:
logger.warning("aiomqtt未安装将使用paho-mqtt的异步包装")
async def connect(self):
"""连接MQTT服务器"""
try:
if self._use_aiomqtt:
await self._connect_aiomqtt()
else:
await self._connect_paho_async()
return True
except Exception as e:
logger.error(f"连接MQTT服务器失败: {e}")
return False
async def _connect_aiomqtt(self):
"""使用aiomqtt连接"""
client_config = {
"hostname": self.config['host'],
"port": self.config['port'],
# "keepalive": self.config.get('keepalive', 60),
}
if self.config.get('username'):
client_config['username'] = self.config['username']
client_config['password'] = self.config.get('password', '')
# try:
self.client = aiomqtt.Client(**client_config)
# except TypeError as e:
# # 处理版本兼容性问题,移除不支持的参数
# logger.warning(f"aiomqtt版本兼容性问题重试连接: {e}")
# # 创建基础配置,只包含核心参数
# basic_config = {
# "hostname": self.config['host'],
# "port": self.config['port']
# }
# if self.config.get('username'):
# basic_config['username'] = self.config['username']
# basic_config['password'] = self.config.get('password', '')
# self.client = aiomqtt.Client(**basic_config)
self._running = True
self._connection_task = asyncio.create_task(self._maintain_connection())
logger.info(f"正在连接MQTT服务器 {self.config['host']}:{self.config['port']}...")
# 等待连接建立
timeout = 10
start_time = time.time()
while not self.connected and (time.time() - start_time) < timeout:
await asyncio.sleep(0.1)
if self.connected:
logger.info("异步MQTT连接建立成功")
else:
logger.error("异步MQTT连接建立超时")
async def _maintain_connection(self):
"""维护MQTT连接仅用于aiomqtt"""
while self._running:
try:
async with self.client:
self.connected = True
self.reconnect_attempts = 0
logger.info(f"MQTT连接成功: {self.config['host']}:{self.config['port']}")
# 启动消息处理任务
self._message_task = asyncio.create_task(self._handle_messages())
# 重新订阅所有topics
await self._resubscribe_all_topics()
# 等待消息处理任务完成或连接断开
await self._message_task
except Exception as e:
self.connected = False
if self._message_task:
self._message_task.cancel()
if self._running:
self.reconnect_attempts += 1
if self.reconnect_attempts <= self.config.get('max_retries', 3):
delay = self.config.get('reconnect_delay', 5) * self.reconnect_attempts
logger.warning(f"MQTT连接断开: {e}, 将在 {delay} 秒后重连(第{self.reconnect_attempts}次尝试)")
await asyncio.sleep(delay)
else:
logger.error("MQTT重连次数已达上限停止重连")
break
else:
break
async def _handle_messages(self):
"""处理接收到的消息仅用于aiomqtt"""
try:
async for message in self.client.messages:
topic = str(message.topic)
payload = message.payload.decode('utf-8')
# 异步处理消息
await self._process_message(topic, payload)
except asyncio.CancelledError:
logger.debug("消息处理任务被取消")
except Exception as e:
logger.error(f"处理MQTT消息失败: {e}", exc_info=True)
async def _process_message(self, topic: str, payload: str):
"""异步处理单个消息"""
try:
handlers = self.message_handlers.get(topic, [])
for handler in handlers:
try:
if asyncio.iscoroutinefunction(handler):
await handler(topic, payload)
else:
# 对于同步处理器,在线程池中执行
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, handler, topic, payload)
except Exception as e:
logger.error(f"MQTT消息处理器执行失败: {e}", exc_info=True)
except Exception as e:
logger.error(f"处理MQTT消息异常: {e}", exc_info=True)
async def _resubscribe_all_topics(self):
"""重新订阅所有topics"""
for topic in self.message_handlers.keys():
try:
await self.client.subscribe(topic)
logger.debug(f"重新订阅MQTT主题: {topic}")
except Exception as e:
logger.error(f"重新订阅主题失败 {topic}: {e}")
async def disconnect(self):
"""断开连接"""
self._running = False
self.connected = False
if self._message_task:
self._message_task.cancel()
try:
await self._message_task
except asyncio.CancelledError:
pass
if self._connection_task:
self._connection_task.cancel()
try:
await self._connection_task
except asyncio.CancelledError:
pass
logger.info("异步MQTT连接已断开")
async def subscribe(self, topic: str, handler: Callable = None):
"""订阅主题"""
try:
if not self.connected:
logger.warning(f"MQTT未连接无法订阅主题: {topic}")
return False
if self._use_aiomqtt and self.client:
await self.client.subscribe(topic)
logger.info(f"成功订阅MQTT主题: {topic}")
# 添加消息处理器
if handler:
if topic not in self.message_handlers:
self.message_handlers[topic] = []
if handler not in self.message_handlers[topic]:
self.message_handlers[topic].append(handler)
return True
else:
logger.error("MQTT客户端未初始化")
return False
except Exception as e:
logger.error(f"订阅MQTT主题异常: {e}")
return False
async def unsubscribe(self, topic: str):
"""取消订阅主题"""
try:
if not self.connected:
logger.debug(f"MQTT未连接跳过取消订阅主题: {topic}")
return False
if self._use_aiomqtt and self.client:
await self.client.unsubscribe(topic)
logger.info(f"取消订阅MQTT主题: {topic}")
# 清理消息处理器
if topic in self.message_handlers:
del self.message_handlers[topic]
return True
else:
logger.warning(f"MQTT客户端未初始化无法取消订阅主题: {topic}")
return False
except Exception as e:
logger.error(f"取消订阅MQTT主题异常: {e}")
return False
async def publish(self, topic: str, payload: Any, qos: int = 0, retain: bool = False):
"""发布消息"""
try:
if not self.connected:
logger.warning(f"MQTT未连接无法发布消息到: {topic}")
return False
# 处理不同类型的payload
payload_str = self._serialize_payload(payload)
if self._use_aiomqtt and self.client:
await self.client.publish(topic, payload_str, qos=qos, retain=retain)
logger.debug(f"MQTT消息发布成功: {topic}")
return True
else:
logger.error("MQTT客户端未初始化")
return False
except Exception as e:
logger.error(f"发布MQTT消息异常: {e}")
return False
def _serialize_payload(self, payload: Any) -> str:
"""序列化消息载荷"""
try:
if isinstance(payload, (str, int, float, bool)):
return str(payload)
elif isinstance(payload, dict):
return json.dumps(payload, ensure_ascii=False, default=self._json_default)
elif isinstance(payload, list):
return json.dumps(payload, ensure_ascii=False, default=self._json_default)
elif hasattr(payload, 'to_dict') and callable(payload.to_dict):
# 如果对象有to_dict方法优先使用它
return json.dumps(payload.to_dict(), ensure_ascii=False, default=self._json_default)
elif hasattr(payload, 'dict'):
# 如果是Pydantic模型等尝试使用dict方法
if callable(payload.dict):
return json.dumps(payload.dict(), ensure_ascii=False, default=self._json_default)
else:
return json.dumps(payload.dict, ensure_ascii=False, default=self._json_default)
elif hasattr(payload, '__dict__'):
# 对于自定义对象,尝试序列化其属性
return json.dumps(payload.__dict__, ensure_ascii=False, default=self._json_default)
else:
# 最后尝试直接转换为字符串
return str(payload)
except Exception as e:
logger.warning(f"序列化载荷失败,使用字符串表示: {e}")
return str(payload)
def _json_default(self, obj):
"""JSON序列化的默认处理器处理特殊类型"""
from enum import Enum
if isinstance(obj, Enum):
return obj.value
elif hasattr(obj, 'to_dict') and callable(obj.to_dict):
return obj.to_dict()
elif hasattr(obj, '__dict__'):
return obj.__dict__
else:
return str(obj)
def add_message_handler(self, topic: str, handler: Callable):
"""为指定主题添加消息处理器"""
if topic not in self.message_handlers:
self.message_handlers[topic] = []
if handler not in self.message_handlers[topic]:
self.message_handlers[topic].append(handler)
def remove_message_handler(self, topic: str, handler: Callable):
"""移除指定主题的消息处理器"""
if topic in self.message_handlers:
if handler in self.message_handlers[topic]:
self.message_handlers[topic].remove(handler)
if not self.message_handlers[topic]:
del self.message_handlers[topic]
def is_connected(self) -> bool:
"""检查连接状态"""
return self.connected
def get_connection_info(self) -> Dict[str, Any]:
"""获取连接信息"""
return {
"host": self.config['host'],
"port": self.config['port'],
"connected": self.connected,
"reconnect_attempts": self.reconnect_attempts,
"running": self._running,
"subscribed_topics": list(self.message_handlers.keys()),
"async_mode": self._use_aiomqtt
}
def create_async_mqtt_service(config: Dict[str, Any] = None) -> AsyncMQTTService:
"""创建异步MQTT服务实例"""
return AsyncMQTTService(config)