实现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的核心要求。
This commit is contained in:
@@ -16,6 +16,7 @@ from src.websocket.websocket_server import websocket_server
|
||||
from src.batch.batch_import import batch_manager
|
||||
from src.utils.data_utils import data_converter, data_formatter, quality_checker
|
||||
from src.models.models import validator
|
||||
from src.iot.app import create_iot_app
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@@ -70,9 +71,24 @@ class WaterManagementSystem:
|
||||
import uvicorn
|
||||
logger.info("启动REST API服务器...")
|
||||
|
||||
# 在新的事件循环中运行uvicorn
|
||||
# 创建组合应用(包含所有模块)
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
from werkzeug.serving import WSGIRequestHandler
|
||||
|
||||
# IoT应用
|
||||
iot_app = create_iot_app()
|
||||
|
||||
# 组合所有应用
|
||||
def combined_app(environ, start_response):
|
||||
path = environ.get('PATH_INFO', '')
|
||||
if path.startswith('/api/iot/'):
|
||||
return iot_app(environ, start_response)
|
||||
else:
|
||||
return rest_api_app(environ, start_response)
|
||||
|
||||
# 配置uvicorn
|
||||
api_config = uvicorn.Config(
|
||||
app=rest_api_app,
|
||||
app=combined_app,
|
||||
host=self.config["api"]["host"],
|
||||
port=self.config["api"]["port"],
|
||||
log_level="info"
|
||||
@@ -264,7 +280,9 @@ def create_requirements():
|
||||
"openpyxl==3.1.2",
|
||||
"aiofiles==23.2.1",
|
||||
"python-multipart==0.0.6",
|
||||
"jinja2==3.1.2"
|
||||
"jinja2==3.1.2",
|
||||
"paho-mqtt==1.6.1",
|
||||
"flask==2.3.3"
|
||||
]
|
||||
|
||||
with open("requirements.txt", 'w') as f:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
IoT Module - 物联网平台核心模块
|
||||
包含MQTT协议适配器、设备注册/发现API、统一设备模型等
|
||||
"""
|
||||
|
||||
from .device_manager import DeviceManager
|
||||
from .device_controller import DeviceController
|
||||
|
||||
__all__ = ['DeviceManager', 'DeviceController']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+177
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
IoT模块Flask应用
|
||||
集成MQTT适配器、设备管理器、OTA管理等核心组件
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from flask import Flask
|
||||
from .device_manager import DeviceManager
|
||||
from .mqtt_adapter import MqttAdapter
|
||||
from .device_controller import DeviceController
|
||||
from .ota_manager import OtaManager
|
||||
from .ota_controller import OtaController
|
||||
from .config import MqttConfig, DatabaseConfig
|
||||
|
||||
|
||||
def create_iot_app(config=None):
|
||||
"""
|
||||
创建IoT模块Flask应用
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Returns:
|
||||
Flask: Flask应用实例
|
||||
"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 加载配置
|
||||
if config is None:
|
||||
config = {
|
||||
'mqtt': MqttConfig.from_env(),
|
||||
'database': DatabaseConfig.from_env()
|
||||
}
|
||||
|
||||
# 初始化组件
|
||||
device_manager = DeviceManager()
|
||||
mqtt_adapter = MqttAdapter(
|
||||
broker_host=config['mqtt'].broker_host,
|
||||
broker_port=config['mqtt'].broker_port,
|
||||
username=config['mqtt'].username,
|
||||
password=config['mqtt'].password,
|
||||
client_id=config['mqtt'].client_id
|
||||
)
|
||||
ota_manager = OtaManager()
|
||||
|
||||
# 创建控制器
|
||||
device_controller = DeviceController(device_manager, mqtt_adapter)
|
||||
ota_controller = OtaController(ota_manager)
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(device_controller.get_blueprint())
|
||||
app.register_blueprint(ota_controller.get_blueprint())
|
||||
|
||||
@app.route('/api/iot/status')
|
||||
def get_status():
|
||||
"""获取IoT模块状态"""
|
||||
return {
|
||||
"mqtt": mqtt_adapter.get_connection_status(),
|
||||
"device_statistics": device_manager.get_device_statistics(),
|
||||
"ota_statistics": ota_manager.get_update_statistics(),
|
||||
"devices_count": len(device_manager.devices),
|
||||
"shadows_count": len(device_manager.shadows)
|
||||
}
|
||||
|
||||
@app.route('/api/iot/mqtt/connect', methods=['POST'])
|
||||
def connect_mqtt():
|
||||
"""连接MQTT"""
|
||||
try:
|
||||
success = mqtt_adapter.connect()
|
||||
return {
|
||||
"success": success,
|
||||
"message": "MQTT connected" if success else "MQTT connection failed"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT connection error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.route('/api/iot/mqtt/disconnect', methods=['POST'])
|
||||
def disconnect_mqtt():
|
||||
"""断开MQTT连接"""
|
||||
try:
|
||||
mqtt_adapter.disconnect()
|
||||
return {"success": True, "message": "MQTT disconnected"}
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT disconnection error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.route('/api/iot/mqtt/reconnect', methods=['POST'])
|
||||
def reconnect_mqtt():
|
||||
"""重连MQTT"""
|
||||
try:
|
||||
mqtt_adapter.disconnect()
|
||||
success = mqtt_adapter.connect()
|
||||
return {
|
||||
"success": success,
|
||||
"message": "MQTT reconnected" if success else "MQTT reconnection failed"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT reconnection error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.route('/api/iot/mqtt/publish', methods=['POST'])
|
||||
def publish_message():
|
||||
"""发布MQTT消息"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
topic = data.get('topic')
|
||||
payload = data.get('payload', {})
|
||||
qos = data.get('qos', 0)
|
||||
retain = data.get('retain', False)
|
||||
|
||||
success = mqtt_adapter.publish(topic, payload, qos, retain)
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Message published" if success else "Message publish failed"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"MQTT publish error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@app.route('/api/iot/initialize', methods=['POST'])
|
||||
def initialize_iot():
|
||||
"""初始化IoT模块"""
|
||||
try:
|
||||
# 连接MQTT
|
||||
mqtt_success = mqtt_adapter.connect()
|
||||
|
||||
# 订阅设备主题
|
||||
mqtt_adapter.subscribe_device_topics(device_manager)
|
||||
|
||||
# 初始化一些示例设备
|
||||
if len(device_manager.devices) == 0:
|
||||
sample_devices = [
|
||||
{
|
||||
'device_sn': 'LL-001',
|
||||
'device_type': 'flow_meter',
|
||||
'name': '流量计-001',
|
||||
'description': 'A区入口流量计',
|
||||
'area': 'A区',
|
||||
'position': '入口处',
|
||||
'manufacturer': '华为',
|
||||
'model': 'LL-100'
|
||||
},
|
||||
{
|
||||
'device_sn': 'YL-001',
|
||||
'device_type': 'pressure_meter',
|
||||
'name': '压力表-001',
|
||||
'description': 'B区主压力表',
|
||||
'area': 'B区',
|
||||
'position': '主管道',
|
||||
'manufacturer': '西门子',
|
||||
'model': 'YL-200'
|
||||
}
|
||||
]
|
||||
|
||||
for device_data in sample_devices:
|
||||
device_manager.register_device(device_data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"mqtt_connected": mqtt_success,
|
||||
"devices_count": len(device_manager.devices),
|
||||
"message": "IoT module initialized successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IoT initialization error: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
IoT模块配置
|
||||
包含MQTT配置、数据库配置、设备类型映射等
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MqttConfig:
|
||||
"""MQTT配置"""
|
||||
broker_host: str = "localhost"
|
||||
broker_port: int = 1883
|
||||
username: str = ""
|
||||
password: str = ""
|
||||
client_id: str = "water-management-system"
|
||||
keep_alive: int = 60
|
||||
clean_session: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_env(cls):
|
||||
"""从环境变量加载配置"""
|
||||
return cls(
|
||||
broker_host=os.getenv('MQTT_BROKER_HOST', 'localhost'),
|
||||
broker_port=int(os.getenv('MQTT_BROKER_PORT', 1883)),
|
||||
username=os.getenv('MQTT_USERNAME', ''),
|
||||
password=os.getenv('MQTT_PASSWORD', ''),
|
||||
client_id=os.getenv('MQTT_CLIENT_ID', 'water-management-system'),
|
||||
keep_alive=int(os.getenv('MQTT_KEEP_ALIVE', 60)),
|
||||
clean_session=os.getenv('MQTT_CLEAN_SESSION', 'true').lower() == 'true'
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DatabaseConfig:
|
||||
"""数据库配置"""
|
||||
host: str = "localhost"
|
||||
port: int = 3306
|
||||
database: str = "water_management"
|
||||
username: str = "root"
|
||||
password: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_env(cls):
|
||||
"""从环境变量加载配置"""
|
||||
return cls(
|
||||
host=os.getenv('DB_HOST', 'localhost'),
|
||||
port=int(os.getenv('DB_PORT', 3306)),
|
||||
database=os.getenv('DB_NAME', 'water_management'),
|
||||
username=os.getenv('DB_USER', 'root'),
|
||||
password=os.getenv('DB_PASSWORD', '')
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceTypeMapping:
|
||||
"""设备类型映射"""
|
||||
# 水利行业标准设备类型映射
|
||||
MAPPINGS = {
|
||||
# 流量计
|
||||
'LL': 'flow_meter', # 流量计
|
||||
'Q': 'flow_meter', # 流量
|
||||
'QL': 'flow_meter', # 瞬时流量
|
||||
|
||||
# 压力表
|
||||
'YL': 'pressure_meter', # 压力表
|
||||
'P': 'pressure_meter', # 压力
|
||||
'PL': 'pressure_meter', # 瞬时压力
|
||||
|
||||
# 水位计
|
||||
'SW': 'level_meter', # 水位计
|
||||
'L': 'level_meter', # 水位
|
||||
'WL': 'level_meter', # 瞬时水位
|
||||
|
||||
# 水质仪
|
||||
'ZD': 'quality_meter', # 浊度
|
||||
'PH': 'quality_meter', # pH值
|
||||
'DO': 'quality_meter', # 溶解氧
|
||||
'COD': 'quality_meter', # 化学需氧量
|
||||
'NH3': 'quality_meter', # 氨氮
|
||||
'TEMP': 'quality_meter', # 温度
|
||||
|
||||
# 阀门
|
||||
'FV': 'valve', # 阀门
|
||||
'BV': 'valve', # 球阀
|
||||
'GV': 'valve', # 闸阀
|
||||
|
||||
# 水泵
|
||||
'PUMP': 'pump', # 水泵
|
||||
'CP': 'pump', # 循环泵
|
||||
'FP': 'pump', # 给水泵
|
||||
|
||||
# 传感器
|
||||
'SENSOR': 'sensor', # 传感器
|
||||
'TS': 'sensor', # 温度传感器
|
||||
'HS': 'sensor', # 湿度传感器
|
||||
|
||||
# 摄像头
|
||||
'CAM': 'camera', # 摄像头
|
||||
'IPC': 'camera', # 网络摄像头
|
||||
|
||||
# 其他
|
||||
'OTHER': 'other' # 其他设备
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def map_device_type(cls, standard_type: str) -> str:
|
||||
"""
|
||||
将标准设备类型映射为内部类型
|
||||
|
||||
Args:
|
||||
standard_type: 标准设备类型
|
||||
|
||||
Returns:
|
||||
str: 内部设备类型
|
||||
"""
|
||||
return cls.MAPPINGS.get(standard_type.upper(), 'other')
|
||||
|
||||
@classmethod
|
||||
def get_all_standard_types(cls) -> list:
|
||||
"""获取所有标准设备类型"""
|
||||
return list(cls.MAPPINGS.keys())
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnitConversion:
|
||||
"""单位转换配置"""
|
||||
# 水利行业常用单位转换
|
||||
CONVERSIONS = {
|
||||
# 流量单位
|
||||
'm³/h': {'m³/s': 1/3600, 'L/s': 1000/3600},
|
||||
'm³/s': {'m³/h': 3600, 'L/s': 1000},
|
||||
'L/s': {'m³/h': 3600/1000, 'm³/s': 1/1000},
|
||||
|
||||
# 压力单位
|
||||
'MPa': {'kPa': 1000, 'Pa': 1000000},
|
||||
'kPa': {'MPa': 1/1000, 'Pa': 1000},
|
||||
'Pa': {'MPa': 1/1000000, 'kPa': 1/1000},
|
||||
|
||||
# 水位单位
|
||||
'm': {'cm': 100, 'mm': 1000},
|
||||
'cm': {'m': 1/100, 'mm': 10},
|
||||
'mm': {'m': 1/1000, 'cm': 1/10},
|
||||
|
||||
# 水质单位
|
||||
'NTU': {'': 1}, # 浊度无标准转换
|
||||
'pH': {'': 1}, # pH值无标准转换
|
||||
'mg/L': {'ppm': 1}, # mg/L 和 ppm 等价
|
||||
'ppm': {'mg/L': 1}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def convert_unit(cls, value: float, from_unit: str, to_unit: str) -> float:
|
||||
"""
|
||||
单位转换
|
||||
|
||||
Args:
|
||||
value: 数值
|
||||
from_unit: 源单位
|
||||
to_unit: 目标单位
|
||||
|
||||
Returns:
|
||||
float: 转换后的数值
|
||||
"""
|
||||
if from_unit == to_unit:
|
||||
return value
|
||||
|
||||
if from_unit in cls.CONVERSIONS and to_unit in cls.CONVERSIONS[from_unit]:
|
||||
factor = cls.CONVERSIONS[from_unit][to_unit]
|
||||
return value * factor
|
||||
|
||||
# 如果没有找到转换关系,尝试反向查找
|
||||
for unit, conversions in cls.CONVERSIONS.items():
|
||||
if to_unit in conversions and from_unit in conversions:
|
||||
# 相同基准单位的转换
|
||||
factor = conversions[from_unit] / conversions[to_unit]
|
||||
return value * factor
|
||||
|
||||
# 无法转换,返回原值
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_supported_units(cls) -> list:
|
||||
"""获取支持的所有单位"""
|
||||
units = set()
|
||||
for conversions in cls.CONVERSIONS.values():
|
||||
units.update(conversions.keys())
|
||||
return list(units)
|
||||
@@ -0,0 +1,260 @@
|
||||
"""
|
||||
设备控制器
|
||||
提供设备注册/发现的REST API接口
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from flask import Blueprint, request, jsonify
|
||||
from .device_manager import DeviceManager
|
||||
from .models import DeviceType, DeviceStatus
|
||||
|
||||
|
||||
class DeviceController:
|
||||
"""设备控制器"""
|
||||
|
||||
def __init__(self, device_manager: DeviceManager):
|
||||
"""
|
||||
初始化设备控制器
|
||||
|
||||
Args:
|
||||
device_manager: 设备管理器
|
||||
"""
|
||||
self.device_manager = device_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建Blueprint
|
||||
self.blueprint = Blueprint('device', __name__, url_prefix='/api/iot/device')
|
||||
|
||||
# 注册路由
|
||||
self._register_routes()
|
||||
|
||||
def _register_routes(self):
|
||||
"""注册API路由"""
|
||||
|
||||
@self.blueprint.route('/', methods=['GET'])
|
||||
def list_devices():
|
||||
"""获取设备列表"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
device_type_str = request.args.get('type')
|
||||
status_str = request.args.get('status')
|
||||
area = request.args.get('area')
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = int(request.args.get('per_page', 10))
|
||||
|
||||
# 过滤条件
|
||||
device_type = DeviceType(device_type_str) if device_type_str else None
|
||||
status = DeviceStatus(status_str) if status_str else None
|
||||
|
||||
# 获取设备列表
|
||||
devices = self.device_manager.list_devices(device_type, status, area)
|
||||
|
||||
# 分页
|
||||
total = len(devices)
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_devices = devices[start:end]
|
||||
|
||||
# 转换为字典格式
|
||||
device_list = [device.to_dict() for device in paginated_devices]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": device_list,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
"pages": (total + per_page - 1) // per_page
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error listing devices: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>', methods=['GET'])
|
||||
def get_device(device_sn):
|
||||
"""获取设备详情"""
|
||||
try:
|
||||
device = self.device_manager.get_device(device_sn)
|
||||
if not device:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
# 获取设备影子
|
||||
shadow = self.device_manager.get_device_shadow(device_sn)
|
||||
device_data = device.to_dict()
|
||||
if shadow:
|
||||
device_data['shadow'] = shadow.to_dict()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": device_data
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting device {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/', methods=['POST'])
|
||||
def register_device():
|
||||
"""注册新设备"""
|
||||
try:
|
||||
device_data = request.get_json()
|
||||
|
||||
# 验证必要字段
|
||||
required_fields = ['device_sn', 'device_type', 'name']
|
||||
for field in required_fields:
|
||||
if field not in device_data:
|
||||
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
|
||||
|
||||
# 检查设备是否已存在
|
||||
existing_device = self.device_manager.get_device(device_data['device_sn'])
|
||||
if existing_device:
|
||||
return jsonify({"success": False, "error": "Device already exists"}), 409
|
||||
|
||||
# 注册设备
|
||||
device = self.device_manager.register_device(device_data)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": device.to_dict(),
|
||||
"message": "Device registered successfully"
|
||||
}), 201
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error registering device: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>', methods=['PUT'])
|
||||
def update_device(device_sn):
|
||||
"""更新设备信息"""
|
||||
try:
|
||||
device = self.device_manager.get_device(device_sn)
|
||||
if not device:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
updates = request.get_json()
|
||||
|
||||
# 更新设备
|
||||
updated_device = self.device_manager.update_device(device_sn, updates)
|
||||
if not updated_device:
|
||||
return jsonify({"success": False, "error": "Failed to update device"}), 400
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": updated_device.to_dict(),
|
||||
"message": "Device updated successfully"
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating device {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>', methods=['DELETE'])
|
||||
def delete_device(device_sn):
|
||||
"""删除设备"""
|
||||
try:
|
||||
success = self.device_manager.delete_device(device_sn)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Device deleted successfully"
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting device {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>/shadow', methods=['GET'])
|
||||
def get_device_shadow(device_sn):
|
||||
"""获取设备影子"""
|
||||
try:
|
||||
shadow = self.device_manager.get_device_shadow(device_sn)
|
||||
if not shadow:
|
||||
return jsonify({"success": False, "error": "Device shadow not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": shadow.to_dict()
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting device shadow {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>/shadow', methods=['PUT'])
|
||||
def update_device_shadow(device_sn):
|
||||
"""更新设备影子"""
|
||||
try:
|
||||
state = request.get_json()
|
||||
|
||||
success = self.device_manager.update_device_shadow(device_sn, state)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Device shadow updated successfully"
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating device shadow {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/discover', methods=['POST'])
|
||||
def discover_devices():
|
||||
"""设备发现"""
|
||||
try:
|
||||
discovered = self.device_manager.discover_devices()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": discovered,
|
||||
"message": f"Discovered {len(discovered)} devices"
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering devices: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/<device_sn>/command', methods=['POST'])
|
||||
def send_device_command(device_sn):
|
||||
"""发送设备控制命令"""
|
||||
try:
|
||||
command = request.get_json()
|
||||
|
||||
# 验证设备存在
|
||||
device = self.device_manager.get_device(device_sn)
|
||||
if not device:
|
||||
return jsonify({"success": False, "error": "Device not found"}), 404
|
||||
|
||||
# 发送命令
|
||||
success = self.mqtt_adapter.send_command(device_sn, command)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Failed to send command"}), 500
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "Command sent successfully",
|
||||
"device_sn": device_sn,
|
||||
"command": command
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending command to device {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/statistics', methods=['GET'])
|
||||
def get_device_statistics():
|
||||
"""获取设备统计信息"""
|
||||
try:
|
||||
statistics = self.device_manager.get_device_statistics()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": statistics
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting device statistics: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
def get_blueprint(self):
|
||||
"""获取Blueprint"""
|
||||
return self.blueprint
|
||||
@@ -0,0 +1,219 @@
|
||||
"""
|
||||
设备管理服务
|
||||
负责设备的CRUD操作、设备影子管理、设备发现等功能
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from .models import Device, DeviceShadow, DeviceStatus, DeviceType
|
||||
|
||||
|
||||
class DeviceManager:
|
||||
"""设备管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.devices: Dict[str, Device] = {} # device_sn -> Device
|
||||
self.shadows: Dict[str, DeviceShadow] = {} # device_sn -> DeviceShadow
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def register_device(self, device_data: Dict[str, Any]) -> Device:
|
||||
"""
|
||||
注册设备
|
||||
|
||||
Args:
|
||||
device_data: 设备数据字典
|
||||
|
||||
Returns:
|
||||
Device: 注册的设备对象
|
||||
"""
|
||||
device = Device(
|
||||
device_sn=device_data['device_sn'],
|
||||
device_type=DeviceType(device_data.get('device_type', 'other')),
|
||||
name=device_data.get('name', ''),
|
||||
description=device_data.get('description', ''),
|
||||
area=device_data.get('area', ''),
|
||||
position=device_data.get('position', ''),
|
||||
geom=device_data.get('geom'),
|
||||
manufacturer=device_data.get('manufacturer', ''),
|
||||
model=device_data.get('model', ''),
|
||||
firmware_version=device_data.get('firmware_version', ''),
|
||||
hardware_version=device_data.get('hardware_version', ''),
|
||||
metadata=device_data.get('metadata', {})
|
||||
)
|
||||
|
||||
self.devices[device.device_sn] = device
|
||||
|
||||
# 创建设备影子
|
||||
shadow = DeviceShadow(device_sn=device.device_sn)
|
||||
self.shadows[device.device_sn] = shadow
|
||||
|
||||
self.logger.info(f"Device registered: {device.device_sn}")
|
||||
return device
|
||||
|
||||
def get_device(self, device_sn: str) -> Optional[Device]:
|
||||
"""
|
||||
获取设备信息
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
|
||||
Returns:
|
||||
Device: 设备对象,如果不存在返回None
|
||||
"""
|
||||
return self.devices.get(device_sn)
|
||||
|
||||
def update_device(self, device_sn: str, updates: Dict[str, Any]) -> Optional[Device]:
|
||||
"""
|
||||
更新设备信息
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
updates: 更新的字段
|
||||
|
||||
Returns:
|
||||
Device: 更新后的设备对象,如果不存在返回None
|
||||
"""
|
||||
device = self.devices.get(device_sn)
|
||||
if not device:
|
||||
return None
|
||||
|
||||
# 更新设备属性
|
||||
for key, value in updates.items():
|
||||
if hasattr(device, key):
|
||||
setattr(device, key, value)
|
||||
|
||||
device.updated_at = datetime.now()
|
||||
self.logger.info(f"Device updated: {device_sn}")
|
||||
return device
|
||||
|
||||
def delete_device(self, device_sn: str) -> bool:
|
||||
"""
|
||||
删除设备
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
|
||||
Returns:
|
||||
bool: 是否删除成功
|
||||
"""
|
||||
if device_sn in self.devices:
|
||||
del self.devices[device_sn]
|
||||
if device_sn in self.shadows:
|
||||
del self.shadows[device_sn]
|
||||
self.logger.info(f"Device deleted: {device_sn}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_devices(self,
|
||||
device_type: Optional[DeviceType] = None,
|
||||
status: Optional[DeviceStatus] = None,
|
||||
area: Optional[str] = None) -> List[Device]:
|
||||
"""
|
||||
列出设备
|
||||
|
||||
Args:
|
||||
device_type: 设备类型过滤
|
||||
status: 设备状态过滤
|
||||
area: 区域过滤
|
||||
|
||||
Returns:
|
||||
List[Device]: 设备列表
|
||||
"""
|
||||
devices = list(self.devices.values())
|
||||
|
||||
if device_type:
|
||||
devices = [d for d in devices if d.device_type == device_type]
|
||||
|
||||
if status:
|
||||
devices = [d for d in devices if d.status == status]
|
||||
|
||||
if area:
|
||||
devices = [d for d in devices if d.area == area]
|
||||
|
||||
return devices
|
||||
|
||||
def update_device_shadow(self, device_sn: str, state: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
更新设备影子
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
state: 设备状态
|
||||
|
||||
Returns:
|
||||
bool: 是否更新成功
|
||||
"""
|
||||
if device_sn not in self.shadows:
|
||||
return False
|
||||
|
||||
shadow = self.shadows[device_sn]
|
||||
shadow.state.update(state)
|
||||
shadow.timestamp = datetime.now()
|
||||
self.logger.debug(f"Device shadow updated: {device_sn}")
|
||||
return True
|
||||
|
||||
def get_device_shadow(self, device_sn: str) -> Optional[DeviceShadow]:
|
||||
"""
|
||||
获取设备影子
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
|
||||
Returns:
|
||||
DeviceShadow: 设备影子对象
|
||||
"""
|
||||
return self.shadows.get(device_sn)
|
||||
|
||||
def discover_devices(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
设备发现 - 扫描网络中的设备
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 发现的设备列表
|
||||
"""
|
||||
discovered = []
|
||||
|
||||
# 模拟设备发现过程
|
||||
# 在实际实现中,这里可以包含网络扫描、协议握手等逻辑
|
||||
for device_sn, device in self.devices.items():
|
||||
if device.status == DeviceStatus.OFFLINE:
|
||||
# 模拟设备上线
|
||||
device.status = DeviceStatus.ONLINE
|
||||
device.last_seen = datetime.now()
|
||||
device.ip_address = f"192.168.1.{hash(device_sn) % 255 + 1}"
|
||||
|
||||
discovered.append({
|
||||
"device_sn": device_sn,
|
||||
"name": device.name,
|
||||
"type": device.device_type.value,
|
||||
"ip_address": device.ip_address,
|
||||
"status": device.status.value
|
||||
})
|
||||
|
||||
self.logger.info(f"Discovered {len(discovered)} devices")
|
||||
return discovered
|
||||
|
||||
def get_device_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取设备统计信息
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 统计信息
|
||||
"""
|
||||
total = len(self.devices)
|
||||
online = sum(1 for d in self.devices.values() if d.status == DeviceStatus.ONLINE)
|
||||
offline = total - online
|
||||
|
||||
by_type = {}
|
||||
for device in self.devices.values():
|
||||
device_type = device.device_type.value
|
||||
by_type[device_type] = by_type.get(device_type, 0) + 1
|
||||
|
||||
return {
|
||||
"total_devices": total,
|
||||
"online_devices": online,
|
||||
"offline_devices": offline,
|
||||
"devices_by_type": by_type
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
IoT 设备模型定义
|
||||
包含设备实体、设备影子、OTA升级等核心数据模型
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Any
|
||||
import uuid
|
||||
|
||||
|
||||
class DeviceStatus(Enum):
|
||||
"""设备状态枚举"""
|
||||
ONLINE = "online"
|
||||
OFFLINE = "offline"
|
||||
MAINTENANCE = "maintenance"
|
||||
FAULT = "fault"
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
"""设备类型枚举"""
|
||||
FLOW_METER = "flow_meter" # 流量计
|
||||
PRESSURE_METER = "pressure_meter" # 压力表
|
||||
LEVEL_METER = "level_meter" # 水位计
|
||||
QUALITY_METER = "quality_meter" # 水质仪
|
||||
VALVE = "valve" # 阀门
|
||||
PUMP = "pump" # 水泵
|
||||
SENSOR = "sensor" # 传感器
|
||||
CAMERA = "camera" # 摄像头
|
||||
OTHER = "other" # 其他
|
||||
|
||||
|
||||
@dataclass
|
||||
class Device:
|
||||
"""设备实体模型"""
|
||||
# 必需字段(无默认值)
|
||||
device_sn: str # 设备序列号(唯一标识)
|
||||
device_type: DeviceType # 设备类型
|
||||
name: str # 设备名称
|
||||
|
||||
# 可选字段(有默认值)
|
||||
description: str = "" # 设备描述
|
||||
area: str = "" # 区域
|
||||
position: str = "" # 位置
|
||||
geom: Optional[str] = None # 地理坐标(GeoJSON格式)
|
||||
manufacturer: str = "" # 厂商
|
||||
model: str = "" # 型号
|
||||
firmware_version: str = "" # 固件版本
|
||||
hardware_version: str = "" # 硬件版本
|
||||
status: DeviceStatus = DeviceStatus.OFFLINE
|
||||
last_seen: Optional[datetime] = None
|
||||
ip_address: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
id: Optional[int] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"device_sn": self.device_sn,
|
||||
"device_type": self.device_type.value,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"area": self.area,
|
||||
"position": self.position,
|
||||
"geom": self.geom,
|
||||
"manufacturer": self.manufacturer,
|
||||
"model": self.model,
|
||||
"firmware_version": self.firmware_version,
|
||||
"hardware_version": self.hardware_version,
|
||||
"status": self.status.value,
|
||||
"last_seen": self.last_seen.isoformat() if self.last_seen else None,
|
||||
"ip_address": self.ip_address,
|
||||
"port": self.port,
|
||||
"metadata": self.metadata,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceShadow:
|
||||
"""设备影子模型"""
|
||||
# 必需字段
|
||||
device_sn: str # 设备序列号
|
||||
|
||||
# 可选字段
|
||||
state: Dict[str, Any] = field(default_factory=dict) # 设备状态
|
||||
desired_state: Dict[str, Any] = field(default_factory=dict) # 期望状态
|
||||
reported_state: Dict[str, Any] = field(default_factory=dict) # 报告状态
|
||||
timestamp: datetime = field(default_factory=datetime.now) # 时间戳
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"device_sn": self.device_sn,
|
||||
"state": self.state,
|
||||
"desired_state": self.desired_state,
|
||||
"reported_state": self.reported_state,
|
||||
"timestamp": self.timestamp.isoformat()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class OtaUpdate:
|
||||
"""OTA升级记录"""
|
||||
# 必需字段
|
||||
device_sn: str # 设备序列号
|
||||
version: str # 目标版本
|
||||
file_url: str # 固件文件URL
|
||||
file_size: int # 文件大小
|
||||
checksum: str # 文件校验和
|
||||
|
||||
# 可选字段
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
status: str = "pending" # 状态:pending/downloading/installed/failed
|
||||
progress: int = 0 # 进度百分比
|
||||
error_message: Optional[str] = None # 错误信息
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"device_sn": self.device_sn,
|
||||
"version": self.version,
|
||||
"file_url": self.file_url,
|
||||
"file_size": self.file_size,
|
||||
"checksum": self.checksum,
|
||||
"status": self.status,
|
||||
"progress": self.progress,
|
||||
"error_message": self.error_message,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MqttMessage:
|
||||
"""MQTT消息模型"""
|
||||
# 必需字段
|
||||
topic: str # 主题
|
||||
payload: Dict[str, Any] # 消息内容
|
||||
|
||||
# 可选字段
|
||||
qos: int = 0 # QoS等级
|
||||
retain: bool = False # 是否保留消息
|
||||
timestamp: datetime = field(default_factory=datetime.now) # 时间戳
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"topic": self.topic,
|
||||
"payload": self.payload,
|
||||
"qos": self.qos,
|
||||
"retain": self.retain,
|
||||
"timestamp": self.timestamp.isoformat()
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
"""
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
OTA固件升级控制器
|
||||
提供OTA升级相关的REST API接口
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from flask import Blueprint, request, jsonify
|
||||
from .ota_manager import OtaManager
|
||||
from .models import OtaUpdate
|
||||
|
||||
|
||||
class OtaController:
|
||||
"""OTA控制器"""
|
||||
|
||||
def __init__(self, ota_manager: OtaManager):
|
||||
"""
|
||||
初始化OTA控制器
|
||||
|
||||
Args:
|
||||
ota_manager: OTA管理器
|
||||
"""
|
||||
self.ota_manager = ota_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建Blueprint
|
||||
self.blueprint = Blueprint('ota', __name__, url_prefix='/api/iot/ota')
|
||||
|
||||
# 注册路由
|
||||
self._register_routes()
|
||||
|
||||
def _register_routes(self):
|
||||
"""注册API路由"""
|
||||
|
||||
@self.blueprint.route('/updates', methods=['GET'])
|
||||
def list_updates():
|
||||
"""获取OTA更新列表"""
|
||||
try:
|
||||
# 获取查询参数
|
||||
device_sn = request.args.get('device_sn')
|
||||
status = request.args.get('status')
|
||||
page = int(request.args.get('page', 1))
|
||||
per_page = int(request.args.get('per_page', 10))
|
||||
|
||||
# 过滤条件
|
||||
updates = list(self.ota_manager.updates.values())
|
||||
if device_sn:
|
||||
updates = [u for u in updates if u.device_sn == device_sn]
|
||||
if status:
|
||||
updates = [u for u in updates if u.status == status]
|
||||
|
||||
# 分页
|
||||
total = len(updates)
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_updates = updates[start:end]
|
||||
|
||||
# 转换为字典格式
|
||||
update_list = [update.to_dict() for update in paginated_updates]
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": update_list,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"total": total,
|
||||
"pages": (total + per_page - 1) // per_page
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error listing OTA updates: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates/<update_id>', methods=['GET'])
|
||||
def get_update(update_id):
|
||||
"""获取OTA更新详情"""
|
||||
try:
|
||||
update = self.ota_manager.get_update(update_id)
|
||||
if not update:
|
||||
return jsonify({"success": False, "error": "Update not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": update.to_dict()
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting OTA update {update_id}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates', methods=['POST'])
|
||||
def create_update():
|
||||
"""创建OTA更新任务"""
|
||||
try:
|
||||
update_data = request.get_json()
|
||||
|
||||
# 验证必要字段
|
||||
required_fields = ['device_sn', 'version', 'file_url', 'file_size', 'checksum']
|
||||
for field in required_fields:
|
||||
if field not in update_data:
|
||||
return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
|
||||
|
||||
# 创建更新
|
||||
update = self.ota_manager.create_update(
|
||||
device_sn=update_data['device_sn'],
|
||||
version=update_data['version'],
|
||||
file_url=update_data['file_url'],
|
||||
file_size=update_data['file_size'],
|
||||
checksum=update_data['checksum']
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": update.to_dict(),
|
||||
"message": "OTA update created successfully"
|
||||
}), 201
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating OTA update: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates/<update_id>/start', methods=['POST'])
|
||||
def start_update(update_id):
|
||||
"""开始OTA更新"""
|
||||
try:
|
||||
success = self.ota_manager.start_update(update_id)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Failed to start update"}), 400
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "OTA update started successfully",
|
||||
"update_id": update_id
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting OTA update {update_id}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates/<update_id>/progress', methods=['PUT'])
|
||||
def update_progress(update_id):
|
||||
"""更新OTA进度"""
|
||||
try:
|
||||
progress_data = request.get_json()
|
||||
|
||||
progress = progress_data.get('progress')
|
||||
error_message = progress_data.get('error_message')
|
||||
|
||||
if progress is None:
|
||||
return jsonify({"success": False, "error": "Progress is required"}), 400
|
||||
|
||||
success = self.ota_manager.update_progress(update_id, progress, error_message)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Update not found"}), 404
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "OTA progress updated successfully",
|
||||
"update_id": update_id,
|
||||
"progress": progress
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating OTA progress {update_id}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates/<update_id>/cancel', methods=['POST'])
|
||||
def cancel_update(update_id):
|
||||
"""取消OTA更新"""
|
||||
try:
|
||||
success = self.ota_manager.cancel_update(update_id)
|
||||
if not success:
|
||||
return jsonify({"success": False, "error": "Failed to cancel update"}), 400
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "OTA update cancelled successfully",
|
||||
"update_id": update_id
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error cancelling OTA update {update_id}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/updates/device/<device_sn>', methods=['GET'])
|
||||
def get_device_updates(device_sn):
|
||||
"""获取设备的OTA更新记录"""
|
||||
try:
|
||||
updates = self.ota_manager.get_updates_by_device(device_sn)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": [update.to_dict() for update in updates],
|
||||
"device_sn": device_sn
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting updates for device {device_sn}: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@self.blueprint.route('/statistics', methods=['GET'])
|
||||
def get_statistics():
|
||||
"""获取OTA统计信息"""
|
||||
try:
|
||||
statistics = self.ota_manager.get_update_statistics()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": statistics
|
||||
})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting OTA statistics: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
def get_blueprint(self):
|
||||
"""获取Blueprint"""
|
||||
return self.blueprint
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
OTA固件升级管理器
|
||||
负责设备OTA升级流程、版本管理、升级状态跟踪等功能
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from .models import OtaUpdate
|
||||
|
||||
|
||||
class OtaManager:
|
||||
"""OTA管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.updates: Dict[str, OtaUpdate] = {} # update_id -> OtaUpdate
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def create_update(self, device_sn: str, version: str, file_url: str,
|
||||
file_size: int, checksum: str) -> OtaUpdate:
|
||||
"""
|
||||
创建OTA升级任务
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
version: 目标版本
|
||||
file_url: 固件文件URL
|
||||
file_size: 文件大小
|
||||
checksum: 文件校验和
|
||||
|
||||
Returns:
|
||||
OtaUpdate: OTA升级对象
|
||||
"""
|
||||
update = OtaUpdate(
|
||||
device_sn=device_sn,
|
||||
version=version,
|
||||
file_url=file_url,
|
||||
file_size=file_size,
|
||||
checksum=checksum
|
||||
)
|
||||
|
||||
self.updates[update.id] = update
|
||||
self.logger.info(f"Created OTA update: {update.id} for device {device_sn}")
|
||||
|
||||
return update
|
||||
|
||||
def get_update(self, update_id: str) -> Optional[OtaUpdate]:
|
||||
"""
|
||||
获取OTA升级信息
|
||||
|
||||
Args:
|
||||
update_id: 更新ID
|
||||
|
||||
Returns:
|
||||
OtaUpdate: OTA升级对象
|
||||
"""
|
||||
return self.updates.get(update_id)
|
||||
|
||||
def get_updates_by_device(self, device_sn: str) -> List[OtaUpdate]:
|
||||
"""
|
||||
获取设备的OTA升级记录
|
||||
|
||||
Args:
|
||||
device_sn: 设备序列号
|
||||
|
||||
Returns:
|
||||
List[OtaUpdate]: OTA升级列表
|
||||
"""
|
||||
return [update for update in self.updates.values() if update.device_sn == device_sn]
|
||||
|
||||
def start_update(self, update_id: str) -> bool:
|
||||
"""
|
||||
开始OTA升级
|
||||
|
||||
Args:
|
||||
update_id: 更新ID
|
||||
|
||||
Returns:
|
||||
bool: 是否开始成功
|
||||
"""
|
||||
update = self.updates.get(update_id)
|
||||
if not update:
|
||||
return False
|
||||
|
||||
if update.status != "pending":
|
||||
self.logger.warning(f"Update {update_id} is not in pending state")
|
||||
return False
|
||||
|
||||
update.status = "downloading"
|
||||
update.started_at = datetime.now()
|
||||
self.logger.info(f"Started OTA update: {update_id}")
|
||||
|
||||
return True
|
||||
|
||||
def update_progress(self, update_id: str, progress: int, error_message: Optional[str] = None) -> bool:
|
||||
"""
|
||||
更新OTA升级进度
|
||||
|
||||
Args:
|
||||
update_id: 更新ID
|
||||
progress: 进度百分比
|
||||
error_message: 错误信息
|
||||
|
||||
Returns:
|
||||
bool: 是否更新成功
|
||||
"""
|
||||
update = self.updates.get(update_id)
|
||||
if not update:
|
||||
return False
|
||||
|
||||
update.progress = progress
|
||||
|
||||
if error_message:
|
||||
update.error_message = error_message
|
||||
update.status = "failed"
|
||||
self.logger.error(f"OTA update {update_id} failed: {error_message}")
|
||||
elif progress >= 100:
|
||||
update.status = "installed"
|
||||
update.completed_at = datetime.now()
|
||||
self.logger.info(f"OTA update {update_id} completed successfully")
|
||||
else:
|
||||
# 保持下载中状态
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def cancel_update(self, update_id: str) -> bool:
|
||||
"""
|
||||
取消OTA升级
|
||||
|
||||
Args:
|
||||
update_id: 更新ID
|
||||
|
||||
Returns:
|
||||
bool: 是否取消成功
|
||||
"""
|
||||
update = self.updates.get(update_id)
|
||||
if not update:
|
||||
return False
|
||||
|
||||
if update.status in ["installed", "failed"]:
|
||||
self.logger.warning(f"Cannot cancel completed update {update_id}")
|
||||
return False
|
||||
|
||||
update.status = "failed"
|
||||
update.error_message = "Cancelled by user"
|
||||
update.completed_at = datetime.now()
|
||||
|
||||
self.logger.info(f"Cancelled OTA update: {update_id}")
|
||||
return True
|
||||
|
||||
def get_update_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取OTA统计信息
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 统计信息
|
||||
"""
|
||||
total = len(self.updates)
|
||||
pending = sum(1 for u in self.updates.values() if u.status == "pending")
|
||||
downloading = sum(1 for u in self.updates.values() if u.status == "downloading")
|
||||
installed = sum(1 for u in self.updates.values() if u.status == "installed")
|
||||
failed = sum(1 for u in self.updates.values() if u.status == "failed")
|
||||
|
||||
return {
|
||||
"total_updates": total,
|
||||
"pending": pending,
|
||||
"downloading": downloading,
|
||||
"installed": installed,
|
||||
"failed": failed
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
IoT模块测试脚本
|
||||
用于验证MQTT适配器、设备管理器和API功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.iot.device_manager import DeviceManager
|
||||
from src.iot.mqtt_adapter import MqttAdapter
|
||||
from src.iot.device_controller import DeviceController
|
||||
from src.iot.models import DeviceType, DeviceStatus
|
||||
|
||||
|
||||
async def test_device_manager():
|
||||
"""测试设备管理器"""
|
||||
print("=== 测试设备管理器 ===")
|
||||
|
||||
device_manager = DeviceManager()
|
||||
|
||||
# 注册设备
|
||||
device_data = {
|
||||
'device_sn': 'LL-001',
|
||||
'device_type': 'flow_meter',
|
||||
'name': '流量计-001',
|
||||
'description': 'A区入口流量计',
|
||||
'area': 'A区',
|
||||
'position': '入口处',
|
||||
'manufacturer': '华为',
|
||||
'model': 'LL-100'
|
||||
}
|
||||
|
||||
device = device_manager.register_device(device_data)
|
||||
print(f"注册设备: {device.device_sn} - {device.name}")
|
||||
|
||||
# 获取设备
|
||||
retrieved_device = device_manager.get_device('LL-001')
|
||||
print(f"获取设备: {retrieved_device.name}")
|
||||
|
||||
# 更新设备
|
||||
updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
|
||||
print(f"更新设备状态: {updated_device.status}")
|
||||
|
||||
# 列出设备
|
||||
devices = device_manager.list_devices()
|
||||
print(f"设备列表: {len(devices)}个设备")
|
||||
|
||||
# 更新设备影子
|
||||
device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
|
||||
shadow = device_manager.get_device_shadow('LL-001')
|
||||
print(f"设备影子: {shadow.state}")
|
||||
|
||||
# 获取统计信息
|
||||
stats = device_manager.get_device_statistics()
|
||||
print(f"设备统计: {stats}")
|
||||
|
||||
print("设备管理器测试完成\n")
|
||||
|
||||
|
||||
async def test_mqtt_adapter():
|
||||
"""测试MQTT适配器"""
|
||||
print("=== 测试MQTT适配器 ===")
|
||||
|
||||
# 创建MQTT适配器(不实际连接)
|
||||
mqtt_adapter = MqttAdapter(
|
||||
broker_host="localhost",
|
||||
broker_port=1883,
|
||||
client_id="test-client"
|
||||
)
|
||||
|
||||
# 测试消息发布
|
||||
test_payload = {"message": "Hello IoT", "timestamp": "2024-01-01T00:00:00"}
|
||||
success = mqtt_adapter.publish("test/topic", test_payload)
|
||||
print(f"消息发布测试: {'成功' if success else '失败'}")
|
||||
|
||||
# 测试连接状态
|
||||
status = mqtt_adapter.get_connection_status()
|
||||
print(f"MQTT状态: {status}")
|
||||
|
||||
print("MQTT适配器测试完成\n")
|
||||
|
||||
|
||||
async def test_device_controller():
|
||||
"""测试设备控制器"""
|
||||
print("=== 测试设备控制器 ===")
|
||||
|
||||
# 创建组件
|
||||
device_manager = DeviceManager()
|
||||
mqtt_adapter = MqttAdapter()
|
||||
device_controller = DeviceController(device_manager, mqtt_adapter)
|
||||
|
||||
# 注册一些测试设备
|
||||
test_devices = [
|
||||
{
|
||||
'device_sn': 'LL-001',
|
||||
'device_type': 'flow_meter',
|
||||
'name': '流量计-001',
|
||||
'area': 'A区',
|
||||
'manufacturer': '华为'
|
||||
},
|
||||
{
|
||||
'device_sn': 'YL-001',
|
||||
'device_type': 'pressure_meter',
|
||||
'name': '压力表-001',
|
||||
'area': 'B区',
|
||||
'manufacturer': '西门子'
|
||||
}
|
||||
]
|
||||
|
||||
for device_data in test_devices:
|
||||
device_manager.register_device(device_data)
|
||||
|
||||
# 模拟API请求
|
||||
print("测试设备注册:")
|
||||
print(f"已注册设备数量: {len(device_manager.devices)}")
|
||||
|
||||
# 模拟设备发现
|
||||
discovered = device_manager.discover_devices()
|
||||
print(f"发现设备数量: {len(discovered)}")
|
||||
|
||||
print("设备控制器测试完成\n")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("开始 IoT 模块测试...\n")
|
||||
|
||||
try:
|
||||
# 测试各个组件
|
||||
await test_device_manager()
|
||||
await test_mqtt_adapter()
|
||||
await test_device_controller()
|
||||
|
||||
print("✅ 所有测试完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
IoT模块简化测试脚本
|
||||
仅测试设备管理器功能,不依赖MQTT
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from src.iot.device_manager import DeviceManager
|
||||
from src.iot.models import DeviceType, DeviceStatus
|
||||
|
||||
|
||||
async def test_device_manager():
|
||||
"""测试设备管理器"""
|
||||
print("=== 测试设备管理器 ===")
|
||||
|
||||
device_manager = DeviceManager()
|
||||
|
||||
# 注册设备
|
||||
device_data = {
|
||||
'device_sn': 'LL-001',
|
||||
'device_type': 'flow_meter',
|
||||
'name': '流量计-001',
|
||||
'description': 'A区入口流量计',
|
||||
'area': 'A区',
|
||||
'position': '入口处',
|
||||
'manufacturer': '华为',
|
||||
'model': 'LL-100'
|
||||
}
|
||||
|
||||
device = device_manager.register_device(device_data)
|
||||
print(f"注册设备: {device.device_sn} - {device.name}")
|
||||
|
||||
# 获取设备
|
||||
retrieved_device = device_manager.get_device('LL-001')
|
||||
print(f"获取设备: {retrieved_device.name}")
|
||||
|
||||
# 更新设备
|
||||
updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
|
||||
print(f"更新设备状态: {updated_device.status}")
|
||||
|
||||
# 列出设备
|
||||
devices = device_manager.list_devices()
|
||||
print(f"设备列表: {len(devices)}个设备")
|
||||
|
||||
# 更新设备影子
|
||||
device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
|
||||
shadow = device_manager.get_device_shadow('LL-001')
|
||||
print(f"设备影子: {shadow.state}")
|
||||
|
||||
# 获取统计信息
|
||||
stats = device_manager.get_device_statistics()
|
||||
print(f"设备统计: {stats}")
|
||||
|
||||
# 测试设备发现
|
||||
discovered = device_manager.discover_devices()
|
||||
print(f"发现设备: {len(discovered)}个设备")
|
||||
|
||||
print("设备管理器测试完成\n")
|
||||
|
||||
|
||||
async def test_device_filtering():
|
||||
"""测试设备过滤功能"""
|
||||
print("=== 测试设备过滤 ===")
|
||||
|
||||
device_manager = DeviceManager()
|
||||
|
||||
# 注册多个设备
|
||||
test_devices = [
|
||||
{'device_sn': 'LL-001', 'device_type': 'flow_meter', 'name': '流量计-001', 'area': 'A区'},
|
||||
{'device_sn': 'YL-001', 'device_type': 'pressure_meter', 'name': '压力表-001', 'area': 'A区'},
|
||||
{'device_sn': 'SW-001', 'device_type': 'level_meter', 'name': '水位计-001', 'area': 'B区'},
|
||||
{'device_sn': 'LL-002', 'device_type': 'flow_meter', 'name': '流量计-002', 'area': 'B区'},
|
||||
]
|
||||
|
||||
for device_data in test_devices:
|
||||
device_manager.register_device(device_data)
|
||||
|
||||
# 测试按类型过滤
|
||||
flow_meters = device_manager.list_devices(device_type=DeviceType.FLOW_METER)
|
||||
print(f"流量计数量: {len(flow_meters)}")
|
||||
|
||||
# 测试按区域过滤
|
||||
a_zone_devices = device_manager.list_devices(area='A区')
|
||||
print(f"A区设备数量: {len(a_zone_devices)}")
|
||||
|
||||
# 测试按状态过滤
|
||||
online_devices = device_manager.list_devices(status=DeviceStatus.ONLINE)
|
||||
print(f"在线设备数量: {len(online_devices)}")
|
||||
|
||||
print("设备过滤测试完成\n")
|
||||
|
||||
|
||||
async def test_device_shadow():
|
||||
"""测试设备影子功能"""
|
||||
print("=== 测试设备影子 ===")
|
||||
|
||||
device_manager = DeviceManager()
|
||||
|
||||
# 注册设备
|
||||
device_data = {
|
||||
'device_sn': 'LL-001',
|
||||
'device_type': 'flow_meter',
|
||||
'name': '流量计-001'
|
||||
}
|
||||
device_manager.register_device(device_data)
|
||||
|
||||
# 更新设备影子
|
||||
shadow_data = {
|
||||
'temperature': 25.5,
|
||||
'pressure': 0.8,
|
||||
'flow_rate': 100.5
|
||||
}
|
||||
|
||||
success = device_manager.update_device_shadow('LL-001', shadow_data)
|
||||
print(f"影子更新成功: {success}")
|
||||
|
||||
# 获取设备影子
|
||||
shadow = device_manager.get_device_shadow('LL-001')
|
||||
print(f"设备影子状态: {shadow.state}")
|
||||
|
||||
print("设备影子测试完成\n")
|
||||
|
||||
|
||||
async def main():
|
||||
"""主测试函数"""
|
||||
print("开始 IoT 模块简化测试...\n")
|
||||
|
||||
try:
|
||||
# 测试各个组件
|
||||
await test_device_manager()
|
||||
await test_device_filtering()
|
||||
await test_device_shadow()
|
||||
|
||||
print("✅ 所有测试完成!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user