ソースを参照

实现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的核心要求。
bot_dev1 4 日 前
コミット
15d2f9cee7

+ 21
- 3
main.py ファイルの表示

@@ -16,6 +16,7 @@ from src.websocket.websocket_server import websocket_server
16 16
 from src.batch.batch_import import batch_manager
17 17
 from src.utils.data_utils import data_converter, data_formatter, quality_checker
18 18
 from src.models.models import validator
19
+from src.iot.app import create_iot_app
19 20
 
20 21
 # 配置日志
21 22
 logging.basicConfig(
@@ -70,9 +71,24 @@ class WaterManagementSystem:
70 71
         import uvicorn
71 72
         logger.info("启动REST API服务器...")
72 73
         
73
-        # 在新的事件循环中运行uvicorn
74
+        # 创建组合应用(包含所有模块)
75
+        from werkzeug.middleware.dispatcher import DispatcherMiddleware
76
+        from werkzeug.serving import WSGIRequestHandler
77
+        
78
+        # IoT应用
79
+        iot_app = create_iot_app()
80
+        
81
+        # 组合所有应用
82
+        def combined_app(environ, start_response):
83
+            path = environ.get('PATH_INFO', '')
84
+            if path.startswith('/api/iot/'):
85
+                return iot_app(environ, start_response)
86
+            else:
87
+                return rest_api_app(environ, start_response)
88
+        
89
+        # 配置uvicorn
74 90
         api_config = uvicorn.Config(
75
-            app=rest_api_app,
91
+            app=combined_app,
76 92
             host=self.config["api"]["host"],
77 93
             port=self.config["api"]["port"],
78 94
             log_level="info"
@@ -264,7 +280,9 @@ def create_requirements():
264 280
         "openpyxl==3.1.2",
265 281
         "aiofiles==23.2.1",
266 282
         "python-multipart==0.0.6",
267
-        "jinja2==3.1.2"
283
+        "jinja2==3.1.2",
284
+        "paho-mqtt==1.6.1",
285
+        "flask==2.3.3"
268 286
     ]
269 287
     
270 288
     with open("requirements.txt", 'w') as f:

+ 9
- 0
src/iot/__init__.py ファイルの表示

@@ -0,0 +1,9 @@
1
+"""
2
+IoT Module - 物联网平台核心模块
3
+包含MQTT协议适配器、设备注册/发现API、统一设备模型等
4
+"""
5
+
6
+from .device_manager import DeviceManager
7
+from .device_controller import DeviceController
8
+
9
+__all__ = ['DeviceManager', 'DeviceController']

バイナリ
src/iot/__pycache__/__init__.cpython-312.pyc ファイルの表示


バイナリ
src/iot/__pycache__/device_controller.cpython-312.pyc ファイルの表示


バイナリ
src/iot/__pycache__/device_manager.cpython-312.pyc ファイルの表示


バイナリ
src/iot/__pycache__/models.cpython-312.pyc ファイルの表示


バイナリ
src/iot/__pycache__/mqtt_adapter.cpython-312.pyc ファイルの表示


+ 177
- 0
src/iot/app.py ファイルの表示

@@ -0,0 +1,177 @@
1
+"""
2
+IoT模块Flask应用
3
+集成MQTT适配器、设备管理器、OTA管理等核心组件
4
+"""
5
+
6
+import logging
7
+import os
8
+from flask import Flask
9
+from .device_manager import DeviceManager
10
+from .mqtt_adapter import MqttAdapter
11
+from .device_controller import DeviceController
12
+from .ota_manager import OtaManager
13
+from .ota_controller import OtaController
14
+from .config import MqttConfig, DatabaseConfig
15
+
16
+
17
+def create_iot_app(config=None):
18
+    """
19
+    创建IoT模块Flask应用
20
+    
21
+    Args:
22
+        config: 配置字典
23
+        
24
+    Returns:
25
+        Flask: Flask应用实例
26
+    """
27
+    app = Flask(__name__)
28
+    
29
+    # 配置日志
30
+    logging.basicConfig(
31
+        level=logging.INFO,
32
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
33
+    )
34
+    logger = logging.getLogger(__name__)
35
+    
36
+    # 加载配置
37
+    if config is None:
38
+        config = {
39
+            'mqtt': MqttConfig.from_env(),
40
+            'database': DatabaseConfig.from_env()
41
+        }
42
+    
43
+    # 初始化组件
44
+    device_manager = DeviceManager()
45
+    mqtt_adapter = MqttAdapter(
46
+        broker_host=config['mqtt'].broker_host,
47
+        broker_port=config['mqtt'].broker_port,
48
+        username=config['mqtt'].username,
49
+        password=config['mqtt'].password,
50
+        client_id=config['mqtt'].client_id
51
+    )
52
+    ota_manager = OtaManager()
53
+    
54
+    # 创建控制器
55
+    device_controller = DeviceController(device_manager, mqtt_adapter)
56
+    ota_controller = OtaController(ota_manager)
57
+    
58
+    # 注册蓝图
59
+    app.register_blueprint(device_controller.get_blueprint())
60
+    app.register_blueprint(ota_controller.get_blueprint())
61
+    
62
+    @app.route('/api/iot/status')
63
+    def get_status():
64
+        """获取IoT模块状态"""
65
+        return {
66
+            "mqtt": mqtt_adapter.get_connection_status(),
67
+            "device_statistics": device_manager.get_device_statistics(),
68
+            "ota_statistics": ota_manager.get_update_statistics(),
69
+            "devices_count": len(device_manager.devices),
70
+            "shadows_count": len(device_manager.shadows)
71
+        }
72
+    
73
+    @app.route('/api/iot/mqtt/connect', methods=['POST'])
74
+    def connect_mqtt():
75
+        """连接MQTT"""
76
+        try:
77
+            success = mqtt_adapter.connect()
78
+            return {
79
+                "success": success,
80
+                "message": "MQTT connected" if success else "MQTT connection failed"
81
+            }
82
+        except Exception as e:
83
+            logger.error(f"MQTT connection error: {e}")
84
+            return {"success": False, "error": str(e)}
85
+    
86
+    @app.route('/api/iot/mqtt/disconnect', methods=['POST'])
87
+    def disconnect_mqtt():
88
+        """断开MQTT连接"""
89
+        try:
90
+            mqtt_adapter.disconnect()
91
+            return {"success": True, "message": "MQTT disconnected"}
92
+        except Exception as e:
93
+            logger.error(f"MQTT disconnection error: {e}")
94
+            return {"success": False, "error": str(e)}
95
+    
96
+    @app.route('/api/iot/mqtt/reconnect', methods=['POST'])
97
+    def reconnect_mqtt():
98
+        """重连MQTT"""
99
+        try:
100
+            mqtt_adapter.disconnect()
101
+            success = mqtt_adapter.connect()
102
+            return {
103
+                "success": success,
104
+                "message": "MQTT reconnected" if success else "MQTT reconnection failed"
105
+            }
106
+        except Exception as e:
107
+            logger.error(f"MQTT reconnection error: {e}")
108
+            return {"success": False, "error": str(e)}
109
+    
110
+    @app.route('/api/iot/mqtt/publish', methods=['POST'])
111
+    def publish_message():
112
+        """发布MQTT消息"""
113
+        try:
114
+            data = request.get_json()
115
+            topic = data.get('topic')
116
+            payload = data.get('payload', {})
117
+            qos = data.get('qos', 0)
118
+            retain = data.get('retain', False)
119
+            
120
+            success = mqtt_adapter.publish(topic, payload, qos, retain)
121
+            return {
122
+                "success": success,
123
+                "message": "Message published" if success else "Message publish failed"
124
+            }
125
+        except Exception as e:
126
+            logger.error(f"MQTT publish error: {e}")
127
+            return {"success": False, "error": str(e)}
128
+    
129
+    @app.route('/api/iot/initialize', methods=['POST'])
130
+    def initialize_iot():
131
+        """初始化IoT模块"""
132
+        try:
133
+            # 连接MQTT
134
+            mqtt_success = mqtt_adapter.connect()
135
+            
136
+            # 订阅设备主题
137
+            mqtt_adapter.subscribe_device_topics(device_manager)
138
+            
139
+            # 初始化一些示例设备
140
+            if len(device_manager.devices) == 0:
141
+                sample_devices = [
142
+                    {
143
+                        'device_sn': 'LL-001',
144
+                        'device_type': 'flow_meter',
145
+                        'name': '流量计-001',
146
+                        'description': 'A区入口流量计',
147
+                        'area': 'A区',
148
+                        'position': '入口处',
149
+                        'manufacturer': '华为',
150
+                        'model': 'LL-100'
151
+                    },
152
+                    {
153
+                        'device_sn': 'YL-001',
154
+                        'device_type': 'pressure_meter',
155
+                        'name': '压力表-001',
156
+                        'description': 'B区主压力表',
157
+                        'area': 'B区',
158
+                        'position': '主管道',
159
+                        'manufacturer': '西门子',
160
+                        'model': 'YL-200'
161
+                    }
162
+                ]
163
+                
164
+                for device_data in sample_devices:
165
+                    device_manager.register_device(device_data)
166
+            
167
+            return {
168
+                "success": True,
169
+                "mqtt_connected": mqtt_success,
170
+                "devices_count": len(device_manager.devices),
171
+                "message": "IoT module initialized successfully"
172
+            }
173
+        except Exception as e:
174
+            logger.error(f"IoT initialization error: {e}")
175
+            return {"success": False, "error": str(e)}
176
+    
177
+    return app

+ 189
- 0
src/iot/config.py ファイルの表示

@@ -0,0 +1,189 @@
1
+"""
2
+IoT模块配置
3
+包含MQTT配置、数据库配置、设备类型映射等
4
+"""
5
+
6
+import os
7
+from dataclasses import dataclass
8
+
9
+
10
+@dataclass
11
+class MqttConfig:
12
+    """MQTT配置"""
13
+    broker_host: str = "localhost"
14
+    broker_port: int = 1883
15
+    username: str = ""
16
+    password: str = ""
17
+    client_id: str = "water-management-system"
18
+    keep_alive: int = 60
19
+    clean_session: bool = True
20
+    
21
+    @classmethod
22
+    def from_env(cls):
23
+        """从环境变量加载配置"""
24
+        return cls(
25
+            broker_host=os.getenv('MQTT_BROKER_HOST', 'localhost'),
26
+            broker_port=int(os.getenv('MQTT_BROKER_PORT', 1883)),
27
+            username=os.getenv('MQTT_USERNAME', ''),
28
+            password=os.getenv('MQTT_PASSWORD', ''),
29
+            client_id=os.getenv('MQTT_CLIENT_ID', 'water-management-system'),
30
+            keep_alive=int(os.getenv('MQTT_KEEP_ALIVE', 60)),
31
+            clean_session=os.getenv('MQTT_CLEAN_SESSION', 'true').lower() == 'true'
32
+        )
33
+
34
+
35
+@dataclass
36
+class DatabaseConfig:
37
+    """数据库配置"""
38
+    host: str = "localhost"
39
+    port: int = 3306
40
+    database: str = "water_management"
41
+    username: str = "root"
42
+    password: str = ""
43
+    
44
+    @classmethod
45
+    def from_env(cls):
46
+        """从环境变量加载配置"""
47
+        return cls(
48
+            host=os.getenv('DB_HOST', 'localhost'),
49
+            port=int(os.getenv('DB_PORT', 3306)),
50
+            database=os.getenv('DB_NAME', 'water_management'),
51
+            username=os.getenv('DB_USER', 'root'),
52
+            password=os.getenv('DB_PASSWORD', '')
53
+        )
54
+
55
+
56
+@dataclass
57
+class DeviceTypeMapping:
58
+    """设备类型映射"""
59
+    # 水利行业标准设备类型映射
60
+    MAPPINGS = {
61
+        # 流量计
62
+        'LL': 'flow_meter',           # 流量计
63
+        'Q': 'flow_meter',            # 流量
64
+        'QL': 'flow_meter',           # 瞬时流量
65
+        
66
+        # 压力表
67
+        'YL': 'pressure_meter',       # 压力表
68
+        'P': 'pressure_meter',        # 压力
69
+        'PL': 'pressure_meter',       # 瞬时压力
70
+        
71
+        # 水位计
72
+        'SW': 'level_meter',          # 水位计
73
+        'L': 'level_meter',           # 水位
74
+        'WL': 'level_meter',          # 瞬时水位
75
+        
76
+        # 水质仪
77
+        'ZD': 'quality_meter',        # 浊度
78
+        'PH': 'quality_meter',        # pH值
79
+        'DO': 'quality_meter',        # 溶解氧
80
+        'COD': 'quality_meter',       # 化学需氧量
81
+        'NH3': 'quality_meter',       # 氨氮
82
+        'TEMP': 'quality_meter',      # 温度
83
+        
84
+        # 阀门
85
+        'FV': 'valve',                # 阀门
86
+        'BV': 'valve',                # 球阀
87
+        'GV': 'valve',                # 闸阀
88
+        
89
+        # 水泵
90
+        'PUMP': 'pump',               # 水泵
91
+        'CP': 'pump',                 # 循环泵
92
+        'FP': 'pump',                 # 给水泵
93
+        
94
+        # 传感器
95
+        'SENSOR': 'sensor',           # 传感器
96
+        'TS': 'sensor',               # 温度传感器
97
+        'HS': 'sensor',               # 湿度传感器
98
+        
99
+        # 摄像头
100
+        'CAM': 'camera',              # 摄像头
101
+        'IPC': 'camera',              # 网络摄像头
102
+        
103
+        # 其他
104
+        'OTHER': 'other'             # 其他设备
105
+    }
106
+    
107
+    @classmethod
108
+    def map_device_type(cls, standard_type: str) -> str:
109
+        """
110
+        将标准设备类型映射为内部类型
111
+        
112
+        Args:
113
+            standard_type: 标准设备类型
114
+            
115
+        Returns:
116
+            str: 内部设备类型
117
+        """
118
+        return cls.MAPPINGS.get(standard_type.upper(), 'other')
119
+    
120
+    @classmethod
121
+    def get_all_standard_types(cls) -> list:
122
+        """获取所有标准设备类型"""
123
+        return list(cls.MAPPINGS.keys())
124
+
125
+
126
+@dataclass
127
+class UnitConversion:
128
+    """单位转换配置"""
129
+    # 水利行业常用单位转换
130
+    CONVERSIONS = {
131
+        # 流量单位
132
+        'm³/h': {'m³/s': 1/3600, 'L/s': 1000/3600},
133
+        'm³/s': {'m³/h': 3600, 'L/s': 1000},
134
+        'L/s': {'m³/h': 3600/1000, 'm³/s': 1/1000},
135
+        
136
+        # 压力单位
137
+        'MPa': {'kPa': 1000, 'Pa': 1000000},
138
+        'kPa': {'MPa': 1/1000, 'Pa': 1000},
139
+        'Pa': {'MPa': 1/1000000, 'kPa': 1/1000},
140
+        
141
+        # 水位单位
142
+        'm': {'cm': 100, 'mm': 1000},
143
+        'cm': {'m': 1/100, 'mm': 10},
144
+        'mm': {'m': 1/1000, 'cm': 1/10},
145
+        
146
+        # 水质单位
147
+        'NTU': {'': 1},  # 浊度无标准转换
148
+        'pH': {'': 1},   # pH值无标准转换
149
+        'mg/L': {'ppm': 1},  # mg/L 和 ppm 等价
150
+        'ppm': {'mg/L': 1}
151
+    }
152
+    
153
+    @classmethod
154
+    def convert_unit(cls, value: float, from_unit: str, to_unit: str) -> float:
155
+        """
156
+        单位转换
157
+        
158
+        Args:
159
+            value: 数值
160
+            from_unit: 源单位
161
+            to_unit: 目标单位
162
+            
163
+        Returns:
164
+            float: 转换后的数值
165
+        """
166
+        if from_unit == to_unit:
167
+            return value
168
+        
169
+        if from_unit in cls.CONVERSIONS and to_unit in cls.CONVERSIONS[from_unit]:
170
+            factor = cls.CONVERSIONS[from_unit][to_unit]
171
+            return value * factor
172
+        
173
+        # 如果没有找到转换关系,尝试反向查找
174
+        for unit, conversions in cls.CONVERSIONS.items():
175
+            if to_unit in conversions and from_unit in conversions:
176
+                # 相同基准单位的转换
177
+                factor = conversions[from_unit] / conversions[to_unit]
178
+                return value * factor
179
+        
180
+        # 无法转换,返回原值
181
+        return value
182
+    
183
+    @classmethod
184
+    def get_supported_units(cls) -> list:
185
+        """获取支持的所有单位"""
186
+        units = set()
187
+        for conversions in cls.CONVERSIONS.values():
188
+            units.update(conversions.keys())
189
+        return list(units)

+ 260
- 0
src/iot/device_controller.py ファイルの表示

@@ -0,0 +1,260 @@
1
+"""
2
+设备控制器
3
+提供设备注册/发现的REST API接口
4
+"""
5
+
6
+import json
7
+import logging
8
+from datetime import datetime
9
+from typing import Dict, Any, List, Optional
10
+from flask import Blueprint, request, jsonify
11
+from .device_manager import DeviceManager
12
+from .models import DeviceType, DeviceStatus
13
+
14
+
15
+class DeviceController:
16
+    """设备控制器"""
17
+    
18
+    def __init__(self, device_manager: DeviceManager):
19
+        """
20
+        初始化设备控制器
21
+        
22
+        Args:
23
+            device_manager: 设备管理器
24
+        """
25
+        self.device_manager = device_manager
26
+        self.logger = logging.getLogger(__name__)
27
+        
28
+        # 创建Blueprint
29
+        self.blueprint = Blueprint('device', __name__, url_prefix='/api/iot/device')
30
+        
31
+        # 注册路由
32
+        self._register_routes()
33
+    
34
+    def _register_routes(self):
35
+        """注册API路由"""
36
+        
37
+        @self.blueprint.route('/', methods=['GET'])
38
+        def list_devices():
39
+            """获取设备列表"""
40
+            try:
41
+                # 获取查询参数
42
+                device_type_str = request.args.get('type')
43
+                status_str = request.args.get('status')
44
+                area = request.args.get('area')
45
+                page = int(request.args.get('page', 1))
46
+                per_page = int(request.args.get('per_page', 10))
47
+                
48
+                # 过滤条件
49
+                device_type = DeviceType(device_type_str) if device_type_str else None
50
+                status = DeviceStatus(status_str) if status_str else None
51
+                
52
+                # 获取设备列表
53
+                devices = self.device_manager.list_devices(device_type, status, area)
54
+                
55
+                # 分页
56
+                total = len(devices)
57
+                start = (page - 1) * per_page
58
+                end = start + per_page
59
+                paginated_devices = devices[start:end]
60
+                
61
+                # 转换为字典格式
62
+                device_list = [device.to_dict() for device in paginated_devices]
63
+                
64
+                return jsonify({
65
+                    "success": True,
66
+                    "data": device_list,
67
+                    "pagination": {
68
+                        "page": page,
69
+                        "per_page": per_page,
70
+                        "total": total,
71
+                        "pages": (total + per_page - 1) // per_page
72
+                    }
73
+                })
74
+            except Exception as e:
75
+                self.logger.error(f"Error listing devices: {e}")
76
+                return jsonify({"success": False, "error": str(e)}), 500
77
+        
78
+        @self.blueprint.route('/<device_sn>', methods=['GET'])
79
+        def get_device(device_sn):
80
+            """获取设备详情"""
81
+            try:
82
+                device = self.device_manager.get_device(device_sn)
83
+                if not device:
84
+                    return jsonify({"success": False, "error": "Device not found"}), 404
85
+                
86
+                # 获取设备影子
87
+                shadow = self.device_manager.get_device_shadow(device_sn)
88
+                device_data = device.to_dict()
89
+                if shadow:
90
+                    device_data['shadow'] = shadow.to_dict()
91
+                
92
+                return jsonify({
93
+                    "success": True,
94
+                    "data": device_data
95
+                })
96
+            except Exception as e:
97
+                self.logger.error(f"Error getting device {device_sn}: {e}")
98
+                return jsonify({"success": False, "error": str(e)}), 500
99
+        
100
+        @self.blueprint.route('/', methods=['POST'])
101
+        def register_device():
102
+            """注册新设备"""
103
+            try:
104
+                device_data = request.get_json()
105
+                
106
+                # 验证必要字段
107
+                required_fields = ['device_sn', 'device_type', 'name']
108
+                for field in required_fields:
109
+                    if field not in device_data:
110
+                        return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
111
+                
112
+                # 检查设备是否已存在
113
+                existing_device = self.device_manager.get_device(device_data['device_sn'])
114
+                if existing_device:
115
+                    return jsonify({"success": False, "error": "Device already exists"}), 409
116
+                
117
+                # 注册设备
118
+                device = self.device_manager.register_device(device_data)
119
+                
120
+                return jsonify({
121
+                    "success": True,
122
+                    "data": device.to_dict(),
123
+                    "message": "Device registered successfully"
124
+                }), 201
125
+            except Exception as e:
126
+                self.logger.error(f"Error registering device: {e}")
127
+                return jsonify({"success": False, "error": str(e)}), 500
128
+        
129
+        @self.blueprint.route('/<device_sn>', methods=['PUT'])
130
+        def update_device(device_sn):
131
+            """更新设备信息"""
132
+            try:
133
+                device = self.device_manager.get_device(device_sn)
134
+                if not device:
135
+                    return jsonify({"success": False, "error": "Device not found"}), 404
136
+                
137
+                updates = request.get_json()
138
+                
139
+                # 更新设备
140
+                updated_device = self.device_manager.update_device(device_sn, updates)
141
+                if not updated_device:
142
+                    return jsonify({"success": False, "error": "Failed to update device"}), 400
143
+                
144
+                return jsonify({
145
+                    "success": True,
146
+                    "data": updated_device.to_dict(),
147
+                    "message": "Device updated successfully"
148
+                })
149
+            except Exception as e:
150
+                self.logger.error(f"Error updating device {device_sn}: {e}")
151
+                return jsonify({"success": False, "error": str(e)}), 500
152
+        
153
+        @self.blueprint.route('/<device_sn>', methods=['DELETE'])
154
+        def delete_device(device_sn):
155
+            """删除设备"""
156
+            try:
157
+                success = self.device_manager.delete_device(device_sn)
158
+                if not success:
159
+                    return jsonify({"success": False, "error": "Device not found"}), 404
160
+                
161
+                return jsonify({
162
+                    "success": True,
163
+                    "message": "Device deleted successfully"
164
+                })
165
+            except Exception as e:
166
+                self.logger.error(f"Error deleting device {device_sn}: {e}")
167
+                return jsonify({"success": False, "error": str(e)}), 500
168
+        
169
+        @self.blueprint.route('/<device_sn>/shadow', methods=['GET'])
170
+        def get_device_shadow(device_sn):
171
+            """获取设备影子"""
172
+            try:
173
+                shadow = self.device_manager.get_device_shadow(device_sn)
174
+                if not shadow:
175
+                    return jsonify({"success": False, "error": "Device shadow not found"}), 404
176
+                
177
+                return jsonify({
178
+                    "success": True,
179
+                    "data": shadow.to_dict()
180
+                })
181
+            except Exception as e:
182
+                self.logger.error(f"Error getting device shadow {device_sn}: {e}")
183
+                return jsonify({"success": False, "error": str(e)}), 500
184
+        
185
+        @self.blueprint.route('/<device_sn>/shadow', methods=['PUT'])
186
+        def update_device_shadow(device_sn):
187
+            """更新设备影子"""
188
+            try:
189
+                state = request.get_json()
190
+                
191
+                success = self.device_manager.update_device_shadow(device_sn, state)
192
+                if not success:
193
+                    return jsonify({"success": False, "error": "Device not found"}), 404
194
+                
195
+                return jsonify({
196
+                    "success": True,
197
+                    "message": "Device shadow updated successfully"
198
+                })
199
+            except Exception as e:
200
+                self.logger.error(f"Error updating device shadow {device_sn}: {e}")
201
+                return jsonify({"success": False, "error": str(e)}), 500
202
+        
203
+        @self.blueprint.route('/discover', methods=['POST'])
204
+        def discover_devices():
205
+            """设备发现"""
206
+            try:
207
+                discovered = self.device_manager.discover_devices()
208
+                
209
+                return jsonify({
210
+                    "success": True,
211
+                    "data": discovered,
212
+                    "message": f"Discovered {len(discovered)} devices"
213
+                })
214
+            except Exception as e:
215
+                self.logger.error(f"Error discovering devices: {e}")
216
+                return jsonify({"success": False, "error": str(e)}), 500
217
+        
218
+        @self.blueprint.route('/<device_sn>/command', methods=['POST'])
219
+        def send_device_command(device_sn):
220
+            """发送设备控制命令"""
221
+            try:
222
+                command = request.get_json()
223
+                
224
+                # 验证设备存在
225
+                device = self.device_manager.get_device(device_sn)
226
+                if not device:
227
+                    return jsonify({"success": False, "error": "Device not found"}), 404
228
+                
229
+                # 发送命令
230
+                success = self.mqtt_adapter.send_command(device_sn, command)
231
+                if not success:
232
+                    return jsonify({"success": False, "error": "Failed to send command"}), 500
233
+                
234
+                return jsonify({
235
+                    "success": True,
236
+                    "message": "Command sent successfully",
237
+                    "device_sn": device_sn,
238
+                    "command": command
239
+                })
240
+            except Exception as e:
241
+                self.logger.error(f"Error sending command to device {device_sn}: {e}")
242
+                return jsonify({"success": False, "error": str(e)}), 500
243
+        
244
+        @self.blueprint.route('/statistics', methods=['GET'])
245
+        def get_device_statistics():
246
+            """获取设备统计信息"""
247
+            try:
248
+                statistics = self.device_manager.get_device_statistics()
249
+                
250
+                return jsonify({
251
+                    "success": True,
252
+                    "data": statistics
253
+                })
254
+            except Exception as e:
255
+                self.logger.error(f"Error getting device statistics: {e}")
256
+                return jsonify({"success": False, "error": str(e)}), 500
257
+    
258
+    def get_blueprint(self):
259
+        """获取Blueprint"""
260
+        return self.blueprint

+ 219
- 0
src/iot/device_manager.py ファイルの表示

@@ -0,0 +1,219 @@
1
+"""
2
+设备管理服务
3
+负责设备的CRUD操作、设备影子管理、设备发现等功能
4
+"""
5
+
6
+import json
7
+import logging
8
+from datetime import datetime
9
+from typing import List, Optional, Dict, Any
10
+from .models import Device, DeviceShadow, DeviceStatus, DeviceType
11
+
12
+
13
+class DeviceManager:
14
+    """设备管理器"""
15
+    
16
+    def __init__(self):
17
+        self.devices: Dict[str, Device] = {}      # device_sn -> Device
18
+        self.shadows: Dict[str, DeviceShadow] = {}  # device_sn -> DeviceShadow
19
+        self.logger = logging.getLogger(__name__)
20
+    
21
+    def register_device(self, device_data: Dict[str, Any]) -> Device:
22
+        """
23
+        注册设备
24
+        
25
+        Args:
26
+            device_data: 设备数据字典
27
+            
28
+        Returns:
29
+            Device: 注册的设备对象
30
+        """
31
+        device = Device(
32
+            device_sn=device_data['device_sn'],
33
+            device_type=DeviceType(device_data.get('device_type', 'other')),
34
+            name=device_data.get('name', ''),
35
+            description=device_data.get('description', ''),
36
+            area=device_data.get('area', ''),
37
+            position=device_data.get('position', ''),
38
+            geom=device_data.get('geom'),
39
+            manufacturer=device_data.get('manufacturer', ''),
40
+            model=device_data.get('model', ''),
41
+            firmware_version=device_data.get('firmware_version', ''),
42
+            hardware_version=device_data.get('hardware_version', ''),
43
+            metadata=device_data.get('metadata', {})
44
+        )
45
+        
46
+        self.devices[device.device_sn] = device
47
+        
48
+        # 创建设备影子
49
+        shadow = DeviceShadow(device_sn=device.device_sn)
50
+        self.shadows[device.device_sn] = shadow
51
+        
52
+        self.logger.info(f"Device registered: {device.device_sn}")
53
+        return device
54
+    
55
+    def get_device(self, device_sn: str) -> Optional[Device]:
56
+        """
57
+        获取设备信息
58
+        
59
+        Args:
60
+            device_sn: 设备序列号
61
+            
62
+        Returns:
63
+            Device: 设备对象,如果不存在返回None
64
+        """
65
+        return self.devices.get(device_sn)
66
+    
67
+    def update_device(self, device_sn: str, updates: Dict[str, Any]) -> Optional[Device]:
68
+        """
69
+        更新设备信息
70
+        
71
+        Args:
72
+            device_sn: 设备序列号
73
+            updates: 更新的字段
74
+            
75
+        Returns:
76
+            Device: 更新后的设备对象,如果不存在返回None
77
+        """
78
+        device = self.devices.get(device_sn)
79
+        if not device:
80
+            return None
81
+        
82
+        # 更新设备属性
83
+        for key, value in updates.items():
84
+            if hasattr(device, key):
85
+                setattr(device, key, value)
86
+        
87
+        device.updated_at = datetime.now()
88
+        self.logger.info(f"Device updated: {device_sn}")
89
+        return device
90
+    
91
+    def delete_device(self, device_sn: str) -> bool:
92
+        """
93
+        删除设备
94
+        
95
+        Args:
96
+            device_sn: 设备序列号
97
+            
98
+        Returns:
99
+            bool: 是否删除成功
100
+        """
101
+        if device_sn in self.devices:
102
+            del self.devices[device_sn]
103
+            if device_sn in self.shadows:
104
+                del self.shadows[device_sn]
105
+            self.logger.info(f"Device deleted: {device_sn}")
106
+            return True
107
+        return False
108
+    
109
+    def list_devices(self, 
110
+                    device_type: Optional[DeviceType] = None,
111
+                    status: Optional[DeviceStatus] = None,
112
+                    area: Optional[str] = None) -> List[Device]:
113
+        """
114
+        列出设备
115
+        
116
+        Args:
117
+            device_type: 设备类型过滤
118
+            status: 设备状态过滤
119
+            area: 区域过滤
120
+            
121
+        Returns:
122
+            List[Device]: 设备列表
123
+        """
124
+        devices = list(self.devices.values())
125
+        
126
+        if device_type:
127
+            devices = [d for d in devices if d.device_type == device_type]
128
+        
129
+        if status:
130
+            devices = [d for d in devices if d.status == status]
131
+        
132
+        if area:
133
+            devices = [d for d in devices if d.area == area]
134
+        
135
+        return devices
136
+    
137
+    def update_device_shadow(self, device_sn: str, state: Dict[str, Any]) -> bool:
138
+        """
139
+        更新设备影子
140
+        
141
+        Args:
142
+            device_sn: 设备序列号
143
+            state: 设备状态
144
+            
145
+        Returns:
146
+            bool: 是否更新成功
147
+        """
148
+        if device_sn not in self.shadows:
149
+            return False
150
+        
151
+        shadow = self.shadows[device_sn]
152
+        shadow.state.update(state)
153
+        shadow.timestamp = datetime.now()
154
+        self.logger.debug(f"Device shadow updated: {device_sn}")
155
+        return True
156
+    
157
+    def get_device_shadow(self, device_sn: str) -> Optional[DeviceShadow]:
158
+        """
159
+        获取设备影子
160
+        
161
+        Args:
162
+            device_sn: 设备序列号
163
+            
164
+        Returns:
165
+            DeviceShadow: 设备影子对象
166
+        """
167
+        return self.shadows.get(device_sn)
168
+    
169
+    def discover_devices(self) -> List[Dict[str, Any]]:
170
+        """
171
+        设备发现 - 扫描网络中的设备
172
+        
173
+        Returns:
174
+            List[Dict[str, Any]]: 发现的设备列表
175
+        """
176
+        discovered = []
177
+        
178
+        # 模拟设备发现过程
179
+        # 在实际实现中,这里可以包含网络扫描、协议握手等逻辑
180
+        for device_sn, device in self.devices.items():
181
+            if device.status == DeviceStatus.OFFLINE:
182
+                # 模拟设备上线
183
+                device.status = DeviceStatus.ONLINE
184
+                device.last_seen = datetime.now()
185
+                device.ip_address = f"192.168.1.{hash(device_sn) % 255 + 1}"
186
+                
187
+                discovered.append({
188
+                    "device_sn": device_sn,
189
+                    "name": device.name,
190
+                    "type": device.device_type.value,
191
+                    "ip_address": device.ip_address,
192
+                    "status": device.status.value
193
+                })
194
+        
195
+        self.logger.info(f"Discovered {len(discovered)} devices")
196
+        return discovered
197
+    
198
+    def get_device_statistics(self) -> Dict[str, Any]:
199
+        """
200
+        获取设备统计信息
201
+        
202
+        Returns:
203
+            Dict[str, Any]: 统计信息
204
+        """
205
+        total = len(self.devices)
206
+        online = sum(1 for d in self.devices.values() if d.status == DeviceStatus.ONLINE)
207
+        offline = total - online
208
+        
209
+        by_type = {}
210
+        for device in self.devices.values():
211
+            device_type = device.device_type.value
212
+            by_type[device_type] = by_type.get(device_type, 0) + 1
213
+        
214
+        return {
215
+            "total_devices": total,
216
+            "online_devices": online,
217
+            "offline_devices": offline,
218
+            "devices_by_type": by_type
219
+        }

+ 163
- 0
src/iot/models.py ファイルの表示

@@ -0,0 +1,163 @@
1
+"""
2
+IoT 设备模型定义
3
+包含设备实体、设备影子、OTA升级等核心数据模型
4
+"""
5
+
6
+from dataclasses import dataclass, field
7
+from datetime import datetime
8
+from enum import Enum
9
+from typing import Dict, List, Optional, Any
10
+import uuid
11
+
12
+
13
+class DeviceStatus(Enum):
14
+    """设备状态枚举"""
15
+    ONLINE = "online"
16
+    OFFLINE = "offline"
17
+    MAINTENANCE = "maintenance"
18
+    FAULT = "fault"
19
+
20
+
21
+class DeviceType(Enum):
22
+    """设备类型枚举"""
23
+    FLOW_METER = "flow_meter"      # 流量计
24
+    PRESSURE_METER = "pressure_meter"  # 压力表
25
+    LEVEL_METER = "level_meter"    # 水位计
26
+    QUALITY_METER = "quality_meter"  # 水质仪
27
+    VALVE = "valve"                # 阀门
28
+    PUMP = "pump"                  # 水泵
29
+    SENSOR = "sensor"              # 传感器
30
+    CAMERA = "camera"              # 摄像头
31
+    OTHER = "other"               # 其他
32
+
33
+
34
+@dataclass
35
+class Device:
36
+    """设备实体模型"""
37
+    # 必需字段(无默认值)
38
+    device_sn: str                    # 设备序列号(唯一标识)
39
+    device_type: DeviceType           # 设备类型
40
+    name: str                         # 设备名称
41
+    
42
+    # 可选字段(有默认值)
43
+    description: str = ""             # 设备描述
44
+    area: str = ""                    # 区域
45
+    position: str = ""               # 位置
46
+    geom: Optional[str] = None        # 地理坐标(GeoJSON格式)
47
+    manufacturer: str = ""           # 厂商
48
+    model: str = ""                  # 型号
49
+    firmware_version: str = ""       # 固件版本
50
+    hardware_version: str = ""        # 硬件版本
51
+    status: DeviceStatus = DeviceStatus.OFFLINE
52
+    last_seen: Optional[datetime] = None
53
+    ip_address: Optional[str] = None
54
+    port: Optional[int] = None
55
+    metadata: Dict[str, Any] = field(default_factory=dict)
56
+    created_at: datetime = field(default_factory=datetime.now)
57
+    updated_at: datetime = field(default_factory=datetime.now)
58
+    id: Optional[int] = None
59
+    
60
+    def to_dict(self) -> Dict[str, Any]:
61
+        """转换为字典"""
62
+        return {
63
+            "id": self.id,
64
+            "device_sn": self.device_sn,
65
+            "device_type": self.device_type.value,
66
+            "name": self.name,
67
+            "description": self.description,
68
+            "area": self.area,
69
+            "position": self.position,
70
+            "geom": self.geom,
71
+            "manufacturer": self.manufacturer,
72
+            "model": self.model,
73
+            "firmware_version": self.firmware_version,
74
+            "hardware_version": self.hardware_version,
75
+            "status": self.status.value,
76
+            "last_seen": self.last_seen.isoformat() if self.last_seen else None,
77
+            "ip_address": self.ip_address,
78
+            "port": self.port,
79
+            "metadata": self.metadata,
80
+            "created_at": self.created_at.isoformat(),
81
+            "updated_at": self.updated_at.isoformat()
82
+        }
83
+
84
+
85
+@dataclass
86
+class DeviceShadow:
87
+    """设备影子模型"""
88
+    # 必需字段
89
+    device_sn: str                   # 设备序列号
90
+    
91
+    # 可选字段
92
+    state: Dict[str, Any] = field(default_factory=dict)      # 设备状态
93
+    desired_state: Dict[str, Any] = field(default_factory=dict)  # 期望状态
94
+    reported_state: Dict[str, Any] = field(default_factory=dict)  # 报告状态
95
+    timestamp: datetime = field(default_factory=datetime.now)  # 时间戳
96
+    
97
+    def to_dict(self) -> Dict[str, Any]:
98
+        """转换为字典"""
99
+        return {
100
+            "device_sn": self.device_sn,
101
+            "state": self.state,
102
+            "desired_state": self.desired_state,
103
+            "reported_state": self.reported_state,
104
+            "timestamp": self.timestamp.isoformat()
105
+        }
106
+
107
+
108
+@dataclass
109
+class OtaUpdate:
110
+    """OTA升级记录"""
111
+    # 必需字段
112
+    device_sn: str                   # 设备序列号
113
+    version: str                     # 目标版本
114
+    file_url: str                    # 固件文件URL
115
+    file_size: int                   # 文件大小
116
+    checksum: str                    # 文件校验和
117
+    
118
+    # 可选字段
119
+    id: str = field(default_factory=lambda: str(uuid.uuid4()))
120
+    status: str = "pending"          # 状态:pending/downloading/installed/failed
121
+    progress: int = 0                # 进度百分比
122
+    error_message: Optional[str] = None  # 错误信息
123
+    started_at: Optional[datetime] = None
124
+    completed_at: Optional[datetime] = None
125
+    
126
+    def to_dict(self) -> Dict[str, Any]:
127
+        """转换为字典"""
128
+        return {
129
+            "id": self.id,
130
+            "device_sn": self.device_sn,
131
+            "version": self.version,
132
+            "file_url": self.file_url,
133
+            "file_size": self.file_size,
134
+            "checksum": self.checksum,
135
+            "status": self.status,
136
+            "progress": self.progress,
137
+            "error_message": self.error_message,
138
+            "started_at": self.started_at.isoformat() if self.started_at else None,
139
+            "completed_at": self.completed_at.isoformat() if self.completed_at else None
140
+        }
141
+
142
+
143
+@dataclass
144
+class MqttMessage:
145
+    """MQTT消息模型"""
146
+    # 必需字段
147
+    topic: str                       # 主题
148
+    payload: Dict[str, Any]           # 消息内容
149
+    
150
+    # 可选字段
151
+    qos: int = 0                     # QoS等级
152
+    retain: bool = False             # 是否保留消息
153
+    timestamp: datetime = field(default_factory=datetime.now)  # 时间戳
154
+    
155
+    def to_dict(self) -> Dict[str, Any]:
156
+        """转换为字典"""
157
+        return {
158
+            "topic": self.topic,
159
+            "payload": self.payload,
160
+            "qos": self.qos,
161
+            "retain": self.retain,
162
+            "timestamp": self.timestamp.isoformat()
163
+        }

+ 352
- 0
src/iot/mqtt_adapter.py ファイルの表示

@@ -0,0 +1,352 @@
1
+"""
2
+MQTT 协议适配器
3
+负责MQTT连接管理、消息订阅/发布、消息解析等功能
4
+"""
5
+
6
+import json
7
+import logging
8
+import paho.mqtt.client as mqtt
9
+from datetime import datetime
10
+from typing import Dict, Any, Optional, Callable, List
11
+from .models import MqttMessage
12
+from threading import Lock
13
+
14
+
15
+class MqttAdapter:
16
+    """MQTT适配器"""
17
+    
18
+    def __init__(self, 
19
+                 broker_host: str = "localhost",
20
+                 broker_port: int = 1883,
21
+                 username: Optional[str] = None,
22
+                 password: Optional[str] = None,
23
+                 client_id: str = "water-management-system"):
24
+        """
25
+        初始化MQTT适配器
26
+        
27
+        Args:
28
+            broker_host: MQTT broker地址
29
+            broker_port: MQTT broker端口
30
+            username: 用户名
31
+            password: 密码
32
+            client_id: 客户端ID
33
+        """
34
+        self.broker_host = broker_host
35
+        self.broker_port = broker_port
36
+        self.username = username
37
+        self.password = password
38
+        self.client_id = client_id
39
+        
40
+        self.client = mqtt.Client(client_id=client_id)
41
+        self.message_handlers: Dict[str, Callable] = {}
42
+        self.connected = False
43
+        self.lock = Lock()
44
+        
45
+        # 配置MQTT客户端
46
+        if username and password:
47
+            self.client.username_pw_set(username, password)
48
+        
49
+        # 设置回调函数
50
+        self.client.on_connect = self._on_connect
51
+        self.client.on_disconnect = self._on_disconnect
52
+        self.client.on_message = self._on_message
53
+        self.client.on_publish = self._on_publish
54
+        self.client.on_subscribe = self._on_subscribe
55
+        
56
+        self.logger = logging.getLogger(__name__)
57
+    
58
+    def _on_connect(self, client, userdata, flags, rc):
59
+        """连接回调"""
60
+        if rc == 0:
61
+            self.connected = True
62
+            self.logger.info(f"Connected to MQTT broker at {self.broker_host}:{self.broker_port}")
63
+        else:
64
+            self.logger.error(f"Failed to connect to MQTT broker, return code {rc}")
65
+    
66
+    def _on_disconnect(self, client, userdata, rc):
67
+        """断开连接回调"""
68
+        self.connected = False
69
+        self.logger.warning(f"Disconnected from MQTT broker, return code {rc}")
70
+    
71
+    def _on_message(self, client, userdata, msg):
72
+        """消息接收回调"""
73
+        try:
74
+            # 解析消息
75
+            payload = json.loads(msg.payload.decode('utf-8')) if msg.payload else {}
76
+            
77
+            message = MqttMessage(
78
+                topic=msg.topic,
79
+                payload=payload,
80
+                qos=msg.qos,
81
+                retain=msg.retain
82
+            )
83
+            
84
+            self.logger.debug(f"Received message: {message.topic} - {message.payload}")
85
+            
86
+            # 查找对应的消息处理器
87
+            for topic_pattern, handler in self.message_handlers.items():
88
+                if self._topic_matches(msg.topic, topic_pattern):
89
+                    try:
90
+                        handler(message)
91
+                    except Exception as e:
92
+                        self.logger.error(f"Error in message handler for {msg.topic}: {e}")
93
+        
94
+        except json.JSONDecodeError as e:
95
+            self.logger.error(f"Failed to parse JSON message from {msg.topic}: {e}")
96
+        except Exception as e:
97
+            self.logger.error(f"Error processing message from {msg.topic}: {e}")
98
+    
99
+    def _on_publish(self, client, userdata, mid):
100
+        """发布消息回调"""
101
+        self.logger.debug(f"Message published with mid: {mid}")
102
+    
103
+    def _on_subscribe(self, client, userdata, mid, granted_qos):
104
+        """订阅回调"""
105
+        self.logger.debug(f"Subscribed with mid: {mid}, granted_qos: {granted_qos}")
106
+    
107
+    def _topic_matches(self, topic: str, pattern: str) -> bool:
108
+        """检查主题是否匹配模式"""
109
+        # 简单的通配符匹配实现
110
+        # 支持单层通配符 + 和多层通配符 #
111
+        pattern_parts = pattern.split('/')
112
+        topic_parts = topic.split('/')
113
+        
114
+        if len(pattern_parts) != len(topic_parts):
115
+            return False
116
+        
117
+        for p_part, t_part in zip(pattern_parts, topic_parts):
118
+            if p_part == '+' or p_part == '#':
119
+                continue
120
+            if p_part != t_part:
121
+                return False
122
+        
123
+        return True
124
+    
125
+    def connect(self) -> bool:
126
+        """
127
+        连接到MQTT broker
128
+        
129
+        Returns:
130
+            bool: 是否连接成功
131
+        """
132
+        try:
133
+            self.client.connect(self.broker_host, self.broker_port, 60)
134
+            self.client.loop_start()
135
+            return True
136
+        except Exception as e:
137
+            self.logger.error(f"Failed to connect to MQTT broker: {e}")
138
+            return False
139
+    
140
+    def disconnect(self):
141
+        """断开MQTT连接"""
142
+        if self.connected:
143
+            self.client.loop_stop()
144
+            self.client.disconnect()
145
+    
146
+    def is_connected(self) -> bool:
147
+        """
148
+        检查是否已连接
149
+        
150
+        Returns:
151
+            bool: 是否已连接
152
+        """
153
+        return self.connected
154
+    
155
+    def subscribe(self, topic: str, qos: int = 0) -> bool:
156
+        """
157
+        订阅主题
158
+        
159
+        Args:
160
+            topic: 主题
161
+            qos: QoS等级
162
+            
163
+        Returns:
164
+            bool: 是否订阅成功
165
+        """
166
+        try:
167
+            result = self.client.subscribe(topic, qos)
168
+            if result[0] == mqtt.MQTT_ERR_SUCCESS:
169
+                self.logger.info(f"Subscribed to topic: {topic}")
170
+                return True
171
+            else:
172
+                self.logger.error(f"Failed to subscribe to topic: {topic}")
173
+                return False
174
+        except Exception as e:
175
+            self.logger.error(f"Error subscribing to topic {topic}: {e}")
176
+            return False
177
+    
178
+    def unsubscribe(self, topic: str) -> bool:
179
+        """
180
+        取消订阅主题
181
+        
182
+        Args:
183
+            topic: 主题
184
+            
185
+        Returns:
186
+            bool: 是否取消订阅成功
187
+        """
188
+        try:
189
+            result = self.client.unsubscribe(topic)
190
+            if result[0] == mqtt.MQTT_ERR_SUCCESS:
191
+                self.logger.info(f"Unsubscribed from topic: {topic}")
192
+                return True
193
+            else:
194
+                self.logger.error(f"Failed to unsubscribe from topic: {topic}")
195
+                return False
196
+        except Exception as e:
197
+            self.logger.error(f"Error unsubscribing from topic {topic}: {e}")
198
+            return False
199
+    
200
+    def publish(self, topic: str, payload: Any, qos: int = 0, retain: bool = False) -> bool:
201
+        """
202
+        发布消息
203
+        
204
+        Args:
205
+            topic: 主题
206
+            payload: 消息内容
207
+            qos: QoS等级
208
+            retain: 是否保留消息
209
+            
210
+        Returns:
211
+            bool: 是否发布成功
212
+        """
213
+        try:
214
+            if isinstance(payload, dict):
215
+                payload = json.dumps(payload)
216
+            elif not isinstance(payload, str):
217
+                payload = str(payload)
218
+            
219
+            result = self.client.publish(topic, payload, qos, retain)
220
+            if result[0] == mqtt.MQTT_ERR_SUCCESS:
221
+                self.logger.debug(f"Published to topic: {topic}")
222
+                return True
223
+            else:
224
+                self.logger.error(f"Failed to publish to topic: {topic}")
225
+                return False
226
+        except Exception as e:
227
+            self.logger.error(f"Error publishing to topic {topic}: {e}")
228
+            return False
229
+    
230
+    def add_message_handler(self, topic_pattern: str, handler: Callable[[MqttMessage], None]):
231
+        """
232
+        添加消息处理器
233
+        
234
+        Args:
235
+            topic_pattern: 主题模式(支持通配符)
236
+            handler: 消息处理函数
237
+        """
238
+        with self.lock:
239
+            self.message_handlers[topic_pattern] = handler
240
+            self.logger.info(f"Added message handler for pattern: {topic_pattern}")
241
+    
242
+    def remove_message_handler(self, topic_pattern: str):
243
+        """
244
+        移除消息处理器
245
+        
246
+        Args:
247
+            topic_pattern: 主题模式
248
+        """
249
+        with self.lock:
250
+            if topic_pattern in self.message_handlers:
251
+                del self.message_handlers[topic_pattern]
252
+                self.logger.info(f"Removed message handler for pattern: {topic_pattern}")
253
+    
254
+    def subscribe_device_topics(self, device_manager):
255
+        """
256
+        订阅设备相关主题
257
+        
258
+        Args:
259
+            device_manager: 设备管理器实例
260
+        """
261
+        # 设备状态上报
262
+        self.add_message_handler("devices/+/status", self._handle_device_status)
263
+        
264
+        # 设备数据上报
265
+        self.add_message_handler("devices/+/data", self._handle_device_data)
266
+        
267
+        # 设备控制命令响应
268
+        self.add_message_handler("devices/+/command/response", self._handle_command_response)
269
+        
270
+        # 设备OTA状态
271
+        self.add_message_handler("devices/+/ota/status", self._handle_ota_status)
272
+    
273
+    def _handle_device_status(self, message: MqttMessage):
274
+        """处理设备状态消息"""
275
+        topic_parts = message.topic.split('/')
276
+        if len(topic_parts) >= 2:
277
+            device_sn = topic_parts[1]
278
+            status = message.payload.get('status', 'unknown')
279
+            
280
+            # 更新设备状态
281
+            device = device_manager.get_device(device_sn)
282
+            if device:
283
+                from .models import DeviceStatus
284
+                try:
285
+                    device.status = DeviceStatus(status)
286
+                    device.last_seen = datetime.now()
287
+                    device_manager.logger.info(f"Device {device_sn} status updated to {status}")
288
+                except ValueError:
289
+                    device_manager.logger.warning(f"Unknown status: {status}")
290
+    
291
+    def _handle_device_data(self, message: MqttMessage):
292
+        """处理设备数据消息"""
293
+        topic_parts = message.topic.split('/')
294
+        if len(topic_parts) >= 2:
295
+            device_sn = topic_parts[1]
296
+            data = message.payload
297
+            
298
+            # 更新设备影子
299
+            device_manager.update_device_shadow(device_sn, data)
300
+            device_manager.logger.debug(f"Device {device_sn} data updated")
301
+    
302
+    def _handle_command_response(self, message: MqttMessage):
303
+        """处理命令响应消息"""
304
+        topic_parts = message.topic.split('/')
305
+        if len(topic_parts) >= 2:
306
+            device_sn = topic_parts[1]
307
+            command_id = message.payload.get('command_id')
308
+            result = message.payload.get('result')
309
+            
310
+            device_manager.logger.info(f"Device {device_sn} command response: {command_id} -> {result}")
311
+    
312
+    def _handle_ota_status(self, message: MqttMessage):
313
+        """处理OTA状态消息"""
314
+        topic_parts = message.topic.split('/')
315
+        if len(topic_parts) >= 2:
316
+            device_sn = topic_parts[1]
317
+            status = message.payload.get('status')
318
+            progress = message.payload.get('progress', 0)
319
+            
320
+            device_manager.logger.info(f"Device {device_sn} OTA status: {status}, progress: {progress}%")
321
+    
322
+    def send_command(self, device_sn: str, command: Dict[str, Any]) -> bool:
323
+        """
324
+        发送设备控制命令
325
+        
326
+        Args:
327
+            device_sn: 设备序列号
328
+            command: 命令内容
329
+            
330
+        Returns:
331
+            bool: 是否发送成功
332
+        """
333
+        topic = f"devices/{device_sn}/command"
334
+        command['command_id'] = f"cmd_{datetime.now().timestamp()}"
335
+        command['timestamp'] = datetime.now().isoformat()
336
+        
337
+        return self.publish(topic, command, qos=1)
338
+    
339
+    def get_connection_status(self) -> Dict[str, Any]:
340
+        """
341
+        获取连接状态
342
+        
343
+        Returns:
344
+            Dict[str, Any]: 连接状态信息
345
+        """
346
+        return {
347
+            "connected": self.connected,
348
+            "broker_host": self.broker_host,
349
+            "broker_port": self.broker_port,
350
+            "client_id": self.client_id,
351
+            "message_handlers_count": len(self.message_handlers)
352
+        }

+ 214
- 0
src/iot/ota_controller.py ファイルの表示

@@ -0,0 +1,214 @@
1
+"""
2
+OTA固件升级控制器
3
+提供OTA升级相关的REST API接口
4
+"""
5
+
6
+import json
7
+import logging
8
+from datetime import datetime
9
+from typing import Dict, Any, List, Optional
10
+from flask import Blueprint, request, jsonify
11
+from .ota_manager import OtaManager
12
+from .models import OtaUpdate
13
+
14
+
15
+class OtaController:
16
+    """OTA控制器"""
17
+    
18
+    def __init__(self, ota_manager: OtaManager):
19
+        """
20
+        初始化OTA控制器
21
+        
22
+        Args:
23
+            ota_manager: OTA管理器
24
+        """
25
+        self.ota_manager = ota_manager
26
+        self.logger = logging.getLogger(__name__)
27
+        
28
+        # 创建Blueprint
29
+        self.blueprint = Blueprint('ota', __name__, url_prefix='/api/iot/ota')
30
+        
31
+        # 注册路由
32
+        self._register_routes()
33
+    
34
+    def _register_routes(self):
35
+        """注册API路由"""
36
+        
37
+        @self.blueprint.route('/updates', methods=['GET'])
38
+        def list_updates():
39
+            """获取OTA更新列表"""
40
+            try:
41
+                # 获取查询参数
42
+                device_sn = request.args.get('device_sn')
43
+                status = request.args.get('status')
44
+                page = int(request.args.get('page', 1))
45
+                per_page = int(request.args.get('per_page', 10))
46
+                
47
+                # 过滤条件
48
+                updates = list(self.ota_manager.updates.values())
49
+                if device_sn:
50
+                    updates = [u for u in updates if u.device_sn == device_sn]
51
+                if status:
52
+                    updates = [u for u in updates if u.status == status]
53
+                
54
+                # 分页
55
+                total = len(updates)
56
+                start = (page - 1) * per_page
57
+                end = start + per_page
58
+                paginated_updates = updates[start:end]
59
+                
60
+                # 转换为字典格式
61
+                update_list = [update.to_dict() for update in paginated_updates]
62
+                
63
+                return jsonify({
64
+                    "success": True,
65
+                    "data": update_list,
66
+                    "pagination": {
67
+                        "page": page,
68
+                        "per_page": per_page,
69
+                        "total": total,
70
+                        "pages": (total + per_page - 1) // per_page
71
+                    }
72
+                })
73
+            except Exception as e:
74
+                self.logger.error(f"Error listing OTA updates: {e}")
75
+                return jsonify({"success": False, "error": str(e)}), 500
76
+        
77
+        @self.blueprint.route('/updates/<update_id>', methods=['GET'])
78
+        def get_update(update_id):
79
+            """获取OTA更新详情"""
80
+            try:
81
+                update = self.ota_manager.get_update(update_id)
82
+                if not update:
83
+                    return jsonify({"success": False, "error": "Update not found"}), 404
84
+                
85
+                return jsonify({
86
+                    "success": True,
87
+                    "data": update.to_dict()
88
+                })
89
+            except Exception as e:
90
+                self.logger.error(f"Error getting OTA update {update_id}: {e}")
91
+                return jsonify({"success": False, "error": str(e)}), 500
92
+        
93
+        @self.blueprint.route('/updates', methods=['POST'])
94
+        def create_update():
95
+            """创建OTA更新任务"""
96
+            try:
97
+                update_data = request.get_json()
98
+                
99
+                # 验证必要字段
100
+                required_fields = ['device_sn', 'version', 'file_url', 'file_size', 'checksum']
101
+                for field in required_fields:
102
+                    if field not in update_data:
103
+                        return jsonify({"success": False, "error": f"Missing required field: {field}"}), 400
104
+                
105
+                # 创建更新
106
+                update = self.ota_manager.create_update(
107
+                    device_sn=update_data['device_sn'],
108
+                    version=update_data['version'],
109
+                    file_url=update_data['file_url'],
110
+                    file_size=update_data['file_size'],
111
+                    checksum=update_data['checksum']
112
+                )
113
+                
114
+                return jsonify({
115
+                    "success": True,
116
+                    "data": update.to_dict(),
117
+                    "message": "OTA update created successfully"
118
+                }), 201
119
+            except Exception as e:
120
+                self.logger.error(f"Error creating OTA update: {e}")
121
+                return jsonify({"success": False, "error": str(e)}), 500
122
+        
123
+        @self.blueprint.route('/updates/<update_id>/start', methods=['POST'])
124
+        def start_update(update_id):
125
+            """开始OTA更新"""
126
+            try:
127
+                success = self.ota_manager.start_update(update_id)
128
+                if not success:
129
+                    return jsonify({"success": False, "error": "Failed to start update"}), 400
130
+                
131
+                return jsonify({
132
+                    "success": True,
133
+                    "message": "OTA update started successfully",
134
+                    "update_id": update_id
135
+                })
136
+            except Exception as e:
137
+                self.logger.error(f"Error starting OTA update {update_id}: {e}")
138
+                return jsonify({"success": False, "error": str(e)}), 500
139
+        
140
+        @self.blueprint.route('/updates/<update_id>/progress', methods=['PUT'])
141
+        def update_progress(update_id):
142
+            """更新OTA进度"""
143
+            try:
144
+                progress_data = request.get_json()
145
+                
146
+                progress = progress_data.get('progress')
147
+                error_message = progress_data.get('error_message')
148
+                
149
+                if progress is None:
150
+                    return jsonify({"success": False, "error": "Progress is required"}), 400
151
+                
152
+                success = self.ota_manager.update_progress(update_id, progress, error_message)
153
+                if not success:
154
+                    return jsonify({"success": False, "error": "Update not found"}), 404
155
+                
156
+                return jsonify({
157
+                    "success": True,
158
+                    "message": "OTA progress updated successfully",
159
+                    "update_id": update_id,
160
+                    "progress": progress
161
+                })
162
+            except Exception as e:
163
+                self.logger.error(f"Error updating OTA progress {update_id}: {e}")
164
+                return jsonify({"success": False, "error": str(e)}), 500
165
+        
166
+        @self.blueprint.route('/updates/<update_id>/cancel', methods=['POST'])
167
+        def cancel_update(update_id):
168
+            """取消OTA更新"""
169
+            try:
170
+                success = self.ota_manager.cancel_update(update_id)
171
+                if not success:
172
+                    return jsonify({"success": False, "error": "Failed to cancel update"}), 400
173
+                
174
+                return jsonify({
175
+                    "success": True,
176
+                    "message": "OTA update cancelled successfully",
177
+                    "update_id": update_id
178
+                })
179
+            except Exception as e:
180
+                self.logger.error(f"Error cancelling OTA update {update_id}: {e}")
181
+                return jsonify({"success": False, "error": str(e)}), 500
182
+        
183
+        @self.blueprint.route('/updates/device/<device_sn>', methods=['GET'])
184
+        def get_device_updates(device_sn):
185
+            """获取设备的OTA更新记录"""
186
+            try:
187
+                updates = self.ota_manager.get_updates_by_device(device_sn)
188
+                
189
+                return jsonify({
190
+                    "success": True,
191
+                    "data": [update.to_dict() for update in updates],
192
+                    "device_sn": device_sn
193
+                })
194
+            except Exception as e:
195
+                self.logger.error(f"Error getting updates for device {device_sn}: {e}")
196
+                return jsonify({"success": False, "error": str(e)}), 500
197
+        
198
+        @self.blueprint.route('/statistics', methods=['GET'])
199
+        def get_statistics():
200
+            """获取OTA统计信息"""
201
+            try:
202
+                statistics = self.ota_manager.get_update_statistics()
203
+                
204
+                return jsonify({
205
+                    "success": True,
206
+                    "data": statistics
207
+                })
208
+            except Exception as e:
209
+                self.logger.error(f"Error getting OTA statistics: {e}")
210
+                return jsonify({"success": False, "error": str(e)}), 500
211
+    
212
+    def get_blueprint(self):
213
+        """获取Blueprint"""
214
+        return self.blueprint

+ 173
- 0
src/iot/ota_manager.py ファイルの表示

@@ -0,0 +1,173 @@
1
+"""
2
+OTA固件升级管理器
3
+负责设备OTA升级流程、版本管理、升级状态跟踪等功能
4
+"""
5
+
6
+import json
7
+import logging
8
+import hashlib
9
+from datetime import datetime
10
+from typing import Dict, Any, List, Optional
11
+from .models import OtaUpdate
12
+
13
+
14
+class OtaManager:
15
+    """OTA管理器"""
16
+    
17
+    def __init__(self):
18
+        self.updates: Dict[str, OtaUpdate] = {}  # update_id -> OtaUpdate
19
+        self.logger = logging.getLogger(__name__)
20
+    
21
+    def create_update(self, device_sn: str, version: str, file_url: str, 
22
+                     file_size: int, checksum: str) -> OtaUpdate:
23
+        """
24
+        创建OTA升级任务
25
+        
26
+        Args:
27
+            device_sn: 设备序列号
28
+            version: 目标版本
29
+            file_url: 固件文件URL
30
+            file_size: 文件大小
31
+            checksum: 文件校验和
32
+            
33
+        Returns:
34
+            OtaUpdate: OTA升级对象
35
+        """
36
+        update = OtaUpdate(
37
+            device_sn=device_sn,
38
+            version=version,
39
+            file_url=file_url,
40
+            file_size=file_size,
41
+            checksum=checksum
42
+        )
43
+        
44
+        self.updates[update.id] = update
45
+        self.logger.info(f"Created OTA update: {update.id} for device {device_sn}")
46
+        
47
+        return update
48
+    
49
+    def get_update(self, update_id: str) -> Optional[OtaUpdate]:
50
+        """
51
+        获取OTA升级信息
52
+        
53
+        Args:
54
+            update_id: 更新ID
55
+            
56
+        Returns:
57
+            OtaUpdate: OTA升级对象
58
+        """
59
+        return self.updates.get(update_id)
60
+    
61
+    def get_updates_by_device(self, device_sn: str) -> List[OtaUpdate]:
62
+        """
63
+        获取设备的OTA升级记录
64
+        
65
+        Args:
66
+            device_sn: 设备序列号
67
+            
68
+        Returns:
69
+            List[OtaUpdate]: OTA升级列表
70
+        """
71
+        return [update for update in self.updates.values() if update.device_sn == device_sn]
72
+    
73
+    def start_update(self, update_id: str) -> bool:
74
+        """
75
+        开始OTA升级
76
+        
77
+        Args:
78
+            update_id: 更新ID
79
+            
80
+        Returns:
81
+            bool: 是否开始成功
82
+        """
83
+        update = self.updates.get(update_id)
84
+        if not update:
85
+            return False
86
+        
87
+        if update.status != "pending":
88
+            self.logger.warning(f"Update {update_id} is not in pending state")
89
+            return False
90
+        
91
+        update.status = "downloading"
92
+        update.started_at = datetime.now()
93
+        self.logger.info(f"Started OTA update: {update_id}")
94
+        
95
+        return True
96
+    
97
+    def update_progress(self, update_id: str, progress: int, error_message: Optional[str] = None) -> bool:
98
+        """
99
+        更新OTA升级进度
100
+        
101
+        Args:
102
+            update_id: 更新ID
103
+            progress: 进度百分比
104
+            error_message: 错误信息
105
+            
106
+        Returns:
107
+            bool: 是否更新成功
108
+        """
109
+        update = self.updates.get(update_id)
110
+        if not update:
111
+            return False
112
+        
113
+        update.progress = progress
114
+        
115
+        if error_message:
116
+            update.error_message = error_message
117
+            update.status = "failed"
118
+            self.logger.error(f"OTA update {update_id} failed: {error_message}")
119
+        elif progress >= 100:
120
+            update.status = "installed"
121
+            update.completed_at = datetime.now()
122
+            self.logger.info(f"OTA update {update_id} completed successfully")
123
+        else:
124
+            # 保持下载中状态
125
+            pass
126
+        
127
+        return True
128
+    
129
+    def cancel_update(self, update_id: str) -> bool:
130
+        """
131
+        取消OTA升级
132
+        
133
+        Args:
134
+            update_id: 更新ID
135
+            
136
+        Returns:
137
+            bool: 是否取消成功
138
+        """
139
+        update = self.updates.get(update_id)
140
+        if not update:
141
+            return False
142
+        
143
+        if update.status in ["installed", "failed"]:
144
+            self.logger.warning(f"Cannot cancel completed update {update_id}")
145
+            return False
146
+        
147
+        update.status = "failed"
148
+        update.error_message = "Cancelled by user"
149
+        update.completed_at = datetime.now()
150
+        
151
+        self.logger.info(f"Cancelled OTA update: {update_id}")
152
+        return True
153
+    
154
+    def get_update_statistics(self) -> Dict[str, Any]:
155
+        """
156
+        获取OTA统计信息
157
+        
158
+        Returns:
159
+            Dict[str, Any]: 统计信息
160
+        """
161
+        total = len(self.updates)
162
+        pending = sum(1 for u in self.updates.values() if u.status == "pending")
163
+        downloading = sum(1 for u in self.updates.values() if u.status == "downloading")
164
+        installed = sum(1 for u in self.updates.values() if u.status == "installed")
165
+        failed = sum(1 for u in self.updates.values() if u.status == "failed")
166
+        
167
+        return {
168
+            "total_updates": total,
169
+            "pending": pending,
170
+            "downloading": downloading,
171
+            "installed": installed,
172
+            "failed": failed
173
+        }

+ 148
- 0
test_iot.py ファイルの表示

@@ -0,0 +1,148 @@
1
+"""
2
+IoT模块测试脚本
3
+用于验证MQTT适配器、设备管理器和API功能
4
+"""
5
+
6
+import asyncio
7
+import json
8
+import sys
9
+import os
10
+
11
+# 添加项目根目录到Python路径
12
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
13
+
14
+from src.iot.device_manager import DeviceManager
15
+from src.iot.mqtt_adapter import MqttAdapter
16
+from src.iot.device_controller import DeviceController
17
+from src.iot.models import DeviceType, DeviceStatus
18
+
19
+
20
+async def test_device_manager():
21
+    """测试设备管理器"""
22
+    print("=== 测试设备管理器 ===")
23
+    
24
+    device_manager = DeviceManager()
25
+    
26
+    # 注册设备
27
+    device_data = {
28
+        'device_sn': 'LL-001',
29
+        'device_type': 'flow_meter',
30
+        'name': '流量计-001',
31
+        'description': 'A区入口流量计',
32
+        'area': 'A区',
33
+        'position': '入口处',
34
+        'manufacturer': '华为',
35
+        'model': 'LL-100'
36
+    }
37
+    
38
+    device = device_manager.register_device(device_data)
39
+    print(f"注册设备: {device.device_sn} - {device.name}")
40
+    
41
+    # 获取设备
42
+    retrieved_device = device_manager.get_device('LL-001')
43
+    print(f"获取设备: {retrieved_device.name}")
44
+    
45
+    # 更新设备
46
+    updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
47
+    print(f"更新设备状态: {updated_device.status}")
48
+    
49
+    # 列出设备
50
+    devices = device_manager.list_devices()
51
+    print(f"设备列表: {len(devices)}个设备")
52
+    
53
+    # 更新设备影子
54
+    device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
55
+    shadow = device_manager.get_device_shadow('LL-001')
56
+    print(f"设备影子: {shadow.state}")
57
+    
58
+    # 获取统计信息
59
+    stats = device_manager.get_device_statistics()
60
+    print(f"设备统计: {stats}")
61
+    
62
+    print("设备管理器测试完成\n")
63
+
64
+
65
+async def test_mqtt_adapter():
66
+    """测试MQTT适配器"""
67
+    print("=== 测试MQTT适配器 ===")
68
+    
69
+    # 创建MQTT适配器(不实际连接)
70
+    mqtt_adapter = MqttAdapter(
71
+        broker_host="localhost",
72
+        broker_port=1883,
73
+        client_id="test-client"
74
+    )
75
+    
76
+    # 测试消息发布
77
+    test_payload = {"message": "Hello IoT", "timestamp": "2024-01-01T00:00:00"}
78
+    success = mqtt_adapter.publish("test/topic", test_payload)
79
+    print(f"消息发布测试: {'成功' if success else '失败'}")
80
+    
81
+    # 测试连接状态
82
+    status = mqtt_adapter.get_connection_status()
83
+    print(f"MQTT状态: {status}")
84
+    
85
+    print("MQTT适配器测试完成\n")
86
+
87
+
88
+async def test_device_controller():
89
+    """测试设备控制器"""
90
+    print("=== 测试设备控制器 ===")
91
+    
92
+    # 创建组件
93
+    device_manager = DeviceManager()
94
+    mqtt_adapter = MqttAdapter()
95
+    device_controller = DeviceController(device_manager, mqtt_adapter)
96
+    
97
+    # 注册一些测试设备
98
+    test_devices = [
99
+        {
100
+            'device_sn': 'LL-001',
101
+            'device_type': 'flow_meter',
102
+            'name': '流量计-001',
103
+            'area': 'A区',
104
+            'manufacturer': '华为'
105
+        },
106
+        {
107
+            'device_sn': 'YL-001',
108
+            'device_type': 'pressure_meter',
109
+            'name': '压力表-001',
110
+            'area': 'B区',
111
+            'manufacturer': '西门子'
112
+        }
113
+    ]
114
+    
115
+    for device_data in test_devices:
116
+        device_manager.register_device(device_data)
117
+    
118
+    # 模拟API请求
119
+    print("测试设备注册:")
120
+    print(f"已注册设备数量: {len(device_manager.devices)}")
121
+    
122
+    # 模拟设备发现
123
+    discovered = device_manager.discover_devices()
124
+    print(f"发现设备数量: {len(discovered)}")
125
+    
126
+    print("设备控制器测试完成\n")
127
+
128
+
129
+async def main():
130
+    """主测试函数"""
131
+    print("开始 IoT 模块测试...\n")
132
+    
133
+    try:
134
+        # 测试各个组件
135
+        await test_device_manager()
136
+        await test_mqtt_adapter()
137
+        await test_device_controller()
138
+        
139
+        print("✅ 所有测试完成!")
140
+        
141
+    except Exception as e:
142
+        print(f"❌ 测试失败: {e}")
143
+        import traceback
144
+        traceback.print_exc()
145
+
146
+
147
+if __name__ == "__main__":
148
+    asyncio.run(main())

+ 149
- 0
test_iot_simple.py ファイルの表示

@@ -0,0 +1,149 @@
1
+"""
2
+IoT模块简化测试脚本
3
+仅测试设备管理器功能,不依赖MQTT
4
+"""
5
+
6
+import asyncio
7
+import json
8
+import sys
9
+import os
10
+
11
+# 添加项目根目录到Python路径
12
+sys.path.append(os.path.dirname(os.path.abspath(__file__)))
13
+
14
+from src.iot.device_manager import DeviceManager
15
+from src.iot.models import DeviceType, DeviceStatus
16
+
17
+
18
+async def test_device_manager():
19
+    """测试设备管理器"""
20
+    print("=== 测试设备管理器 ===")
21
+    
22
+    device_manager = DeviceManager()
23
+    
24
+    # 注册设备
25
+    device_data = {
26
+        'device_sn': 'LL-001',
27
+        'device_type': 'flow_meter',
28
+        'name': '流量计-001',
29
+        'description': 'A区入口流量计',
30
+        'area': 'A区',
31
+        'position': '入口处',
32
+        'manufacturer': '华为',
33
+        'model': 'LL-100'
34
+    }
35
+    
36
+    device = device_manager.register_device(device_data)
37
+    print(f"注册设备: {device.device_sn} - {device.name}")
38
+    
39
+    # 获取设备
40
+    retrieved_device = device_manager.get_device('LL-001')
41
+    print(f"获取设备: {retrieved_device.name}")
42
+    
43
+    # 更新设备
44
+    updated_device = device_manager.update_device('LL-001', {'status': DeviceStatus.ONLINE})
45
+    print(f"更新设备状态: {updated_device.status}")
46
+    
47
+    # 列出设备
48
+    devices = device_manager.list_devices()
49
+    print(f"设备列表: {len(devices)}个设备")
50
+    
51
+    # 更新设备影子
52
+    device_manager.update_device_shadow('LL-001', {'temperature': 25.5, 'pressure': 0.8})
53
+    shadow = device_manager.get_device_shadow('LL-001')
54
+    print(f"设备影子: {shadow.state}")
55
+    
56
+    # 获取统计信息
57
+    stats = device_manager.get_device_statistics()
58
+    print(f"设备统计: {stats}")
59
+    
60
+    # 测试设备发现
61
+    discovered = device_manager.discover_devices()
62
+    print(f"发现设备: {len(discovered)}个设备")
63
+    
64
+    print("设备管理器测试完成\n")
65
+
66
+
67
+async def test_device_filtering():
68
+    """测试设备过滤功能"""
69
+    print("=== 测试设备过滤 ===")
70
+    
71
+    device_manager = DeviceManager()
72
+    
73
+    # 注册多个设备
74
+    test_devices = [
75
+        {'device_sn': 'LL-001', 'device_type': 'flow_meter', 'name': '流量计-001', 'area': 'A区'},
76
+        {'device_sn': 'YL-001', 'device_type': 'pressure_meter', 'name': '压力表-001', 'area': 'A区'},
77
+        {'device_sn': 'SW-001', 'device_type': 'level_meter', 'name': '水位计-001', 'area': 'B区'},
78
+        {'device_sn': 'LL-002', 'device_type': 'flow_meter', 'name': '流量计-002', 'area': 'B区'},
79
+    ]
80
+    
81
+    for device_data in test_devices:
82
+        device_manager.register_device(device_data)
83
+    
84
+    # 测试按类型过滤
85
+    flow_meters = device_manager.list_devices(device_type=DeviceType.FLOW_METER)
86
+    print(f"流量计数量: {len(flow_meters)}")
87
+    
88
+    # 测试按区域过滤
89
+    a_zone_devices = device_manager.list_devices(area='A区')
90
+    print(f"A区设备数量: {len(a_zone_devices)}")
91
+    
92
+    # 测试按状态过滤
93
+    online_devices = device_manager.list_devices(status=DeviceStatus.ONLINE)
94
+    print(f"在线设备数量: {len(online_devices)}")
95
+    
96
+    print("设备过滤测试完成\n")
97
+
98
+
99
+async def test_device_shadow():
100
+    """测试设备影子功能"""
101
+    print("=== 测试设备影子 ===")
102
+    
103
+    device_manager = DeviceManager()
104
+    
105
+    # 注册设备
106
+    device_data = {
107
+        'device_sn': 'LL-001',
108
+        'device_type': 'flow_meter',
109
+        'name': '流量计-001'
110
+    }
111
+    device_manager.register_device(device_data)
112
+    
113
+    # 更新设备影子
114
+    shadow_data = {
115
+        'temperature': 25.5,
116
+        'pressure': 0.8,
117
+        'flow_rate': 100.5
118
+    }
119
+    
120
+    success = device_manager.update_device_shadow('LL-001', shadow_data)
121
+    print(f"影子更新成功: {success}")
122
+    
123
+    # 获取设备影子
124
+    shadow = device_manager.get_device_shadow('LL-001')
125
+    print(f"设备影子状态: {shadow.state}")
126
+    
127
+    print("设备影子测试完成\n")
128
+
129
+
130
+async def main():
131
+    """主测试函数"""
132
+    print("开始 IoT 模块简化测试...\n")
133
+    
134
+    try:
135
+        # 测试各个组件
136
+        await test_device_manager()
137
+        await test_device_filtering()
138
+        await test_device_shadow()
139
+        
140
+        print("✅ 所有测试完成!")
141
+        
142
+    except Exception as e:
143
+        print(f"❌ 测试失败: {e}")
144
+        import traceback
145
+        traceback.print_exc()
146
+
147
+
148
+if __name__ == "__main__":
149
+    asyncio.run(main())