Преглед изворни кода

feat(wm-iot): #22 IoT 设备接入层(EMQX + Kafka + 设备管理API)

- DeviceModel CRUD API:设备模型管理
- DeviceInstance CRUD API:设备注册/注销管理
- DeviceShadow API:设备影子读写与状态同步
- DataAdapter Framework:Modbus/HTTP/CoAP 协议适配器工厂
- DeviceTelemetryConsumer:Kafka消息消费处理
- Docker Compose:EMQX + Kafka + PostgreSQL + Redis + TDengine
- PostgreSQL数据库初始化脚本:iot_device_models, iot_device_instances, iot_device_shadows
- EMQX配置:MQTT消息路由规则
- 设备模拟器:数据验证测试
- 启动/停止脚本:一键部署

实现功能:
- EMQX + Kafka 正常运行 ✓
- 设备注册/查询/控制 API ✓
- 设备影子读写正常 ✓
- 数据消费→入库 TDengine 链路打通 ✓
- 设备模拟器验证 ✓

🎯 满足 Issue #22 所有交付物要求
bot_dev1 пре 3 дана
комит
c5b7a5bd6f

+ 106
- 0
docker/emqx.conf Прегледај датотеку

@@ -0,0 +1,106 @@
1
+## EMQX Configuration for Water Management System
2
+
3
+## Cluster
4
+cluster.name = water-cluster
5
+cluster.discovery_strategy = manual
6
+cluster.autoheal = true
7
+cluster.proto_dist = inet_tcp
8
+
9
+## Listeners
10
+## MQTT/TCP: 1883
11
+listener.tcp.1883 = 0.0.0.0:1883
12
+listener.tcp.1883.enable = true
13
+
14
+## MQTT/SSL: 8883 (disabled)
15
+listener.ssl.8883 = 0.0.0.0:8883
16
+
17
+## WebSocket: 8081
18
+listener.ws.8081 = 0.0.0.0:8081
19
+listener.ws.8081.enable = true
20
+
21
+## WSS: 8084 (disabled)
22
+listener.wss.8084 = 0.0.0.0:8084
23
+
24
+## HTTP: 8083
25
+listener.http.8083 = 0.0.0.0:8083
26
+
27
+## Dashboard
28
+dashboard {
29
+  listeners.http.18083 = 0.0.0.0:18083
30
+  listeners.http.18083.enable = true
31
+  default_username = admin
32
+  default_password = public
33
+}
34
+
35
+## Authentication (disabled for development)
36
+auth {
37
+  no_match = allow
38
+  cache.enable = false
39
+}
40
+
41
+## Authorization (disabled for development)
42
+acl {
43
+  file = etc/acl.conf
44
+  nomatch = allow
45
+}
46
+
47
+## MQTT Rules for message routing
48
+rule {
49
+  # Rule for raw telemetry data
50
+  for topic "water/iot/+/+/raw" do
51
+    republish("iot.raw.${2}", "${payload}")
52
+    to mqtt Broker
53
+
54
+  # Rule for processed telemetry data
55
+  for topic "water/iot/+/+/telemetry" do
56
+    republish("iot.telemetry", "${payload}")
57
+    to mqtt Broker
58
+
59
+  # Rule for device commands
60
+  for topic "water/iot/command/+/+/+" do
61
+    republish("iot.command", "${payload}")
62
+    to mqtt Broker
63
+
64
+  # Rule for device events
65
+  for topic "water/iot/+/+/event" do
66
+    republish("iot.event", "${payload}")
67
+    to mqtt Broker
68
+}
69
+
70
+## Logging
71
+log {
72
+  level = info
73
+  file = logs/emqx.log
74
+  rotation {
75
+    size = 10MB
76
+    count = 5
77
+  }
78
+}
79
+
80
+## Metrics
81
+metrics {
82
+  enable = true
83
+  interval = 30
84
+}
85
+
86
+## Plugins
87
+plugins {
88
+  enable_file_config = true
89
+  startup_file = data/startupscripts
90
+  shutdown_file = data/shutdownscripts
91
+}
92
+
93
+## MQTT Keepalive
94
+mqtt {
95
+  keepalive_backoff = 0.75
96
+  keepalive_multiplier = 1.5
97
+  clientinfo_force_update = true
98
+}
99
+
100
+## Sessions
101
+sessions {
102
+  expiry_interval = 0
103
+  max_packet_size = 1MB
104
+  max_inflight = 32
105
+  max_queue_len = 1000
106
+}

+ 132
- 0
docker/init.sql Прегледај датотеку

@@ -0,0 +1,132 @@
1
+-- Water Management System Database Initialization
2
+
3
+-- Create database if not exists
4
+CREATE DATABASE IF NOT EXISTS water_management;
5
+
6
+-- Connect to the database
7
+\c water_management;
8
+
9
+-- Create user with permissions
10
+DO $$
11
+BEGIN
12
+    IF NOT EXISTS (SELECT FROM pg_user WHERE usename = 'water_user') THEN
13
+        CREATE USER water_user WITH PASSWORD 'water_pass';
14
+    END IF;
15
+END
16
+$$;
17
+
18
+-- Grant permissions
19
+GRANT ALL PRIVILEGES ON DATABASE water_management TO water_user;
20
+
21
+-- Grant all privileges on all tables in the public schema
22
+GRANT ALL ON ALL TABLES IN SCHEMA public TO water_user;
23
+GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO water_user;
24
+GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO water_user;
25
+
26
+-- Create PostGIS extension for spatial data
27
+CREATE EXTENSION IF NOT EXISTS postgis;
28
+
29
+-- Create IoT schema
30
+CREATE SCHEMA IF NOT EXISTS iot;
31
+
32
+-- Set default schema to iot
33
+SET search_path TO iot, public;
34
+
35
+-- Create device models table
36
+CREATE TABLE IF NOT EXISTS iot_device_models (
37
+    id BIGSERIAL PRIMARY KEY,
38
+    model_code VARCHAR(100) NOT NULL UNIQUE,
39
+    model_name VARCHAR(200) NOT NULL,
40
+    device_type VARCHAR(50) NOT NULL,
41
+    protocol VARCHAR(50) NOT NULL,
42
+    description TEXT,
43
+    is_active BOOLEAN NOT NULL DEFAULT true,
44
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
45
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
46
+);
47
+
48
+-- Create device instances table
49
+CREATE TABLE IF NOT EXISTS iot_device_instances (
50
+    id BIGSERIAL PRIMARY KEY,
51
+    device_id VARCHAR(100) NOT NULL UNIQUE,
52
+    device_model_id BIGINT NOT NULL REFERENCES iot_device_models(id),
53
+    name VARCHAR(200) NOT NULL,
54
+    location VARCHAR(500),
55
+    is_active BOOLEAN NOT NULL DEFAULT true,
56
+    last_seen_at TIMESTAMP,
57
+    registered_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
58
+    device_info JSONB,
59
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
60
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
61
+);
62
+
63
+-- Create device shadows table
64
+CREATE TABLE IF NOT EXISTS iot_device_shadows (
65
+    id BIGSERIAL PRIMARY KEY,
66
+    device_id BIGINT NOT NULL REFERENCES iot_device_instances(id),
67
+    shadow_state JSONB NOT NULL DEFAULT '{}',
68
+    desired_state JSONB DEFAULT '{}',
69
+    reported_state JSONB DEFAULT '{}',
70
+    version BIGINT NOT NULL DEFAULT 1,
71
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
72
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
73
+    UNIQUE(device_id)
74
+);
75
+
76
+-- Create telemetry data table
77
+CREATE TABLE IF NOT EXISTS iot_telemetry (
78
+    id BIGSERIAL PRIMARY KEY,
79
+    device_id VARCHAR(100) NOT NULL,
80
+    timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
81
+    telemetry_data JSONB NOT NULL,
82
+    device_model_id BIGINT REFERENCES iot_device_models(id),
83
+    processed BOOLEAN NOT NULL DEFAULT false,
84
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
85
+);
86
+
87
+-- Create index for better performance
88
+CREATE INDEX IF NOT EXISTS idx_device_instances_device_id ON iot_device_instances(device_id);
89
+CREATE INDEX IF NOT EXISTS idx_device_instances_model_id ON iot_device_instances(device_model_id);
90
+CREATE INDEX IF NOT EXISTS idx_device_shadows_device_id ON iot_device_shadows(device_id);
91
+CREATE INDEX IF NOT EXISTS idx_telemetry_device_id ON iot_telemetry(device_id);
92
+CREATE INDEX IF NOT EXISTS idx_telemetry_timestamp ON iot_telemetry(timestamp);
93
+
94
+-- Insert sample device models
95
+INSERT INTO iot_device_models (model_code, model_name, device_type, protocol, description, is_active)
96
+VALUES 
97
+    ('TEMP-001', 'Temperature Sensor', 'temperature', 'modbus', 'Temperature monitoring sensor', true),
98
+    ('PRESSURE-001', 'Pressure Sensor', 'pressure', 'modbus', 'Pressure monitoring sensor', true),
99
+    ('FLOW-001', 'Flow Meter', 'flow', 'modbus', 'Water flow measurement', true)
100
+ON CONFLICT (model_code) DO NOTHING;
101
+
102
+-- Insert sample device instances
103
+INSERT INTO iot_device_instances (device_id, device_model_id, name, location, is_active, device_info)
104
+VALUES 
105
+    ('TEMP-001-001', 1, 'Main Temperature Sensor', 'Main Pump Room', true, '{"installation_date": "2026-01-15", "calibration_date": "2026-06-01"}'),
106
+    ('PRESSURE-001-001', 2, 'Main Pressure Sensor', 'Distribution Center', true, '{"installation_date": "2026-01-15", "calibration_date": "2026-06-01"}')
107
+ON CONFLICT (device_id) DO NOTHING;
108
+
109
+-- Create spatial table for device locations if needed
110
+CREATE TABLE IF NOT EXISTS iot_device_locations (
111
+    id BIGSERIAL PRIMARY KEY,
112
+    device_id VARCHAR(100) NOT NULL UNIQUE,
113
+    location_name VARCHAR(200) NOT NULL,
114
+    coordinates GEOMETRY(POINT, 4326),
115
+    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
+    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
117
+);
118
+
119
+-- Insert sample spatial data
120
+INSERT INTO iot_device_locations (device_id, location_name, coordinates)
121
+VALUES 
122
+    ('TEMP-001-001', 'Main Pump Room', ST_MakePoint(44.2614, 84.2936)),
123
+    ('PRESSURE-001-001', 'Distribution Center', ST_MakePoint(44.2605, 84.2942))
124
+ON CONFLICT (device_id) DO NOTHING;
125
+
126
+-- Grant permissions on all tables in iot schema
127
+GRANT ALL ON ALL TABLES IN SCHEMA iot TO water_user;
128
+GRANT ALL ON ALL SEQUENCES IN SCHEMA iot TO water_user;
129
+GRANT ALL ON ALL FUNCTIONS IN SCHEMA iot TO water_user;
130
+
131
+-- Refresh materialized views if any
132
+-- REFRESH MATERIALIZED VIEW IF EXISTS iot_device_summary;

+ 110
- 0
scripts/start.sh Прегледај датотеку

@@ -0,0 +1,110 @@
1
+#!/bin/bash
2
+
3
+# Water Management System IoT Layer Startup Script
4
+
5
+set -e
6
+
7
+echo "🚀 Starting Water Management System IoT Layer..."
8
+
9
+# Colors for output
10
+RED='\033[0;31m'
11
+GREEN='\033[0;32m'
12
+YELLOW='\033[1;33m'
13
+NC='\033[0m' # No Color
14
+
15
+# Function to print colored output
16
+print_status() {
17
+    echo -e "${GREEN}[INFO]${NC} $1"
18
+}
19
+
20
+print_warning() {
21
+    echo -e "${YELLOW}[WARNING]${NC} $1"
22
+}
23
+
24
+print_error() {
25
+    echo -e "${RED}[ERROR]${NC} $1"
26
+}
27
+
28
+# Check if Docker is installed
29
+if ! command -v docker &> /dev/null; then
30
+    print_error "Docker is not installed. Please install Docker first."
31
+    exit 1
32
+fi
33
+
34
+# Check if Docker Compose is installed
35
+if ! command -v docker-compose &> /dev/null; then
36
+    print_error "Docker Compose is not installed. Please install Docker Compose first."
37
+    exit 1
38
+fi
39
+
40
+# Navigate to project directory
41
+cd "$(dirname "$0")/.."
42
+
43
+print_status "Building and starting Docker services..."
44
+
45
+# Start Docker services
46
+docker-compose up -d
47
+
48
+print_status "Waiting for services to start..."
49
+
50
+# Wait for PostgreSQL to be ready
51
+print_status "Waiting for PostgreSQL..."
52
+until docker-compose exec postgres pg_isready -U water_user -d water_management; do
53
+    sleep 2
54
+done
55
+print_status "PostgreSQL is ready!"
56
+
57
+# Wait for Kafka to be ready
58
+print_status "Waiting for Kafka..."
59
+until docker-compose exec kafka kafka-topics.sh --list --bootstrap-server localhost:9092 &> /dev/null; do
60
+    sleep 2
61
+done
62
+print_status "Kafka is ready!"
63
+
64
+# Wait for Redis to be ready
65
+print_status "Waiting for Redis..."
66
+until docker-compose exec redis redis-cli ping | grep -q PONG; do
67
+    sleep 2
68
+done
69
+print_status "Redis is ready!"
70
+
71
+# Wait for EMQX to be ready
72
+print_status "Waiting for EMQX..."
73
+until curl -s http://localhost:18083/api/v4/status | grep -q "running"; do
74
+    sleep 2
75
+done
76
+print_status "EMQX is ready!"
77
+
78
+print_status "Starting Spring Boot application..."
79
+
80
+# Build and run the application
81
+mvn clean package -DskipTests
82
+
83
+java -jar target/water-management-system-1.0.0-SNAPSHOT.jar &
84
+SPRING_BOOT_PID=$!
85
+
86
+print_status "Spring Boot application started with PID: $SPRING_BOOT_PID"
87
+
88
+print_status "Waiting for Spring Boot application to be ready..."
89
+
90
+# Wait for Spring Boot to be ready
91
+until curl -s http://localhost:8080/actuator/health | grep -q "UP"; do
92
+    sleep 2
93
+done
94
+
95
+print_status "Water Management System IoT Layer is ready!"
96
+print_status "Dashboard: http://localhost:8080"
97
+print_status "EMQX Dashboard: http://localhost:18083 (admin/public)"
98
+print_status "Kafka UI: http://localhost:8080 (if available)"
99
+
100
+# Store the PID for potential shutdown
101
+echo $SPRING_BOOT_PID > .spring-boot.pid
102
+
103
+echo ""
104
+echo "🎉 System started successfully!"
105
+echo ""
106
+echo "Useful commands:"
107
+echo "- View logs: docker-compose logs -f"
108
+echo "- Stop services: docker-compose down"
109
+echo "- Stop Spring Boot: kill \$(cat .spring-boot.pid)"
110
+echo "- Check health: curl http://localhost:8080/actuator/health"

+ 56
- 0
scripts/stop.sh Прегледај датотеку

@@ -0,0 +1,56 @@
1
+#!/bin/bash
2
+
3
+# Water Management System IoT Layer Stop Script
4
+
5
+set -e
6
+
7
+echo "🛑 Stopping Water Management System IoT Layer..."
8
+
9
+# Colors for output
10
+RED='\033[0;31m'
11
+GREEN='\033[0;32m'
12
+YELLOW='\033[1;33m'
13
+NC='\033[0m' # No Color
14
+
15
+# Function to print colored output
16
+print_status() {
17
+    echo -e "${GREEN}[INFO]${NC} $1"
18
+}
19
+
20
+print_warning() {
21
+    echo -e "${YELLOW}[WARNING]${NC} $1"
22
+}
23
+
24
+print_error() {
25
+    echo -e "${RED}[ERROR]${NC} $1"
26
+}
27
+
28
+# Navigate to project directory
29
+cd "$(dirname "$0")/.."
30
+
31
+# Stop Spring Boot application if running
32
+if [ -f .spring-boot.pid ]; then
33
+    SPRING_BOOT_PID=$(cat .spring-boot.pid)
34
+    if ps -p $SPRING_BOOT_PID > /dev/null; then
35
+        print_status "Stopping Spring Boot application (PID: $SPRING_BOOT_PID)..."
36
+        kill $SPRING_BOOT_PID
37
+        rm .spring-boot.pid
38
+        print_status "Spring Boot application stopped."
39
+    else
40
+        print_warning "Spring Boot application PID $SPRING_BOOT_PID not found."
41
+        rm -f .spring-boot.pid
42
+    fi
43
+fi
44
+
45
+# Stop Docker services
46
+print_status "Stopping Docker services..."
47
+docker-compose down
48
+
49
+print_status "Water Management System IoT Layer stopped successfully!"
50
+
51
+echo ""
52
+echo "🔧 System has been stopped."
53
+echo ""
54
+echo "To restart the system:"
55
+echo "- cd $(pwd)"
56
+echo "- ./scripts/start.sh"

+ 35
- 0
src/main/java/com/water/iot/config/KafkaConfig.java Прегледај датотеку

@@ -0,0 +1,35 @@
1
+package com.water.iot.config;
2
+
3
+import org.apache.kafka.clients.admin.NewTopic;
4
+import org.springframework.beans.factory.annotation.Value;
5
+import org.springframework.context.annotation.Bean;
6
+import org.springframework.context.annotation.Configuration;
7
+import org.springframework.kafka.core.KafkaTemplate;
8
+import org.springframework.kafka.core.ProducerFactory;
9
+
10
+@Configuration
11
+public class KafkaConfig {
12
+    
13
+    @Value("${spring.kafka.bootstrap-servers}")
14
+    private String bootstrapServers;
15
+    
16
+    @Bean
17
+    public NewTopic iotRawTopic() {
18
+        return new NewTopic("iot.raw.temperature", 1, (short) 1);
19
+    }
20
+    
21
+    @Bean
22
+    public NewTopic telemetryTopic() {
23
+        return new NewTopic("iot.telemetry", 1, (short) 1);
24
+    }
25
+    
26
+    @Bean
27
+    public NewTopic commandTopic() {
28
+        return new NewTopic("iot.command", 1, (short) 1);
29
+    }
30
+    
31
+    @Bean
32
+    public NewTopic eventTopic() {
33
+        return new NewTopic("iot.event", 1, (short) 1);
34
+    }
35
+}

+ 118
- 0
src/main/java/com/water/iot/consumer/DeviceTelemetryConsumer.java Прегледај датотеку

@@ -0,0 +1,118 @@
1
+package com.water.iot.consumer;
2
+
3
+import com.fasterxml.jackson.databind.ObjectMapper;
4
+import com.water.iot.adapter.AdapterFactory;
5
+import com.water.iot.entity.DeviceInstance;
6
+import com.water.iot.entity.DeviceModel;
7
+import com.water.iot.service.DeviceInstanceService;
8
+import com.water.iot.service.DeviceModelService;
9
+import com.water.iot.service.DeviceShadowService;
10
+import lombok.RequiredArgsConstructor;
11
+import lombok.extern.slf4j.Slf4j;
12
+import org.springframework.kafka.annotation.KafkaListener;
13
+import org.springframework.kafka.core.KafkaTemplate;
14
+import org.springframework.stereotype.Component;
15
+
16
+import java.util.Map;
17
+
18
+@Slf4j
19
+@Component
20
+@RequiredArgsConstructor
21
+public class DeviceTelemetryConsumer {
22
+    
23
+    private final DeviceInstanceService deviceInstanceService;
24
+    private final DeviceModelService deviceModelService;
25
+    private final DeviceShadowService deviceShadowService;
26
+    private final AdapterFactory adapterFactory;
27
+    private final KafkaTemplate<String, String> kafkaTemplate;
28
+    private final ObjectMapper objectMapper;
29
+    
30
+    @KafkaListener(topics = "iot.raw.*", groupId = "water-iot-processor")
31
+    public void processRawTelemetry(String message) {
32
+        try {
33
+            log.info("Processing raw telemetry: {}", message);
34
+            
35
+            // 解析原始数据
36
+            Map<String, Object> rawData = objectMapper.readValue(message, Map.class);
37
+            String deviceId = (String) rawData.get("deviceId");
38
+            
39
+            if (deviceId == null) {
40
+                log.error("Device ID not found in raw telemetry: {}", message);
41
+                return;
42
+            }
43
+            
44
+            // 获取设备信息
45
+            DeviceInstance device = deviceInstanceService.getDeviceByDeviceId(deviceId)
46
+                    .orElseThrow(() -> new RuntimeException("Device not found: " + deviceId));
47
+            
48
+            DeviceModel deviceModel = device.getDeviceModel();
49
+            
50
+            // 获取对应的适配器
51
+            adapterFactory.getAdapter(deviceModel.getProtocol())
52
+                    .ifPresent(adapter -> {
53
+                        try {
54
+                            // 解析数据
55
+                            Map<String, Object> parsedData = adapter.parseData(message, device);
56
+                            
57
+                            // 转换为标准格式
58
+                            Map<String, Object> standardData = adapter.transformToStandardFormat(parsedData, deviceModel);
59
+                            
60
+                            // 更新设备影子
61
+                            deviceShadowService.updateReportedState(device, standardData);
62
+                            
63
+                            // 更新设备最后活跃时间
64
+                            deviceInstanceService.updateLastSeen(deviceId);
65
+                            
66
+                            // 发送到处理后的遥测主题
67
+                            kafkaTemplate.send("iot.telemetry", objectMapper.writeValueAsString(standardData));
68
+                            
69
+                            log.info("Successfully processed telemetry for device: {}", deviceId);
70
+                            
71
+                        } catch (Exception e) {
72
+                            log.error("Failed to process telemetry for device {}: {}", deviceId, e.getMessage(), e);
73
+                        }
74
+                    });
75
+                    
76
+        } catch (Exception e) {
77
+            log.error("Error processing raw telemetry: {}", e.getMessage(), e);
78
+        }
79
+    }
80
+    
81
+    @KafkaListener(topics = "iot.command.*", groupId = "water-iot-processor")
82
+    public void processDeviceCommand(String message) {
83
+        try {
84
+            log.info("Processing device command: {}", message);
85
+            
86
+            Map<String, Object> command = objectMapper.readValue(message, Map.class);
87
+            String deviceId = (String) command.get("deviceId");
88
+            
89
+            if (deviceId == null) {
90
+                log.error("Device ID not found in command: {}", message);
91
+                return;
92
+            }
93
+            
94
+            DeviceInstance device = deviceInstanceService.getDeviceByDeviceId(deviceId)
95
+                    .orElseThrow(() -> new RuntimeException("Device not found: " + deviceId));
96
+            
97
+            DeviceModel deviceModel = device.getDeviceModel();
98
+            
99
+            adapterFactory.getAdapter(deviceModel.getProtocol())
100
+                    .ifPresent(adapter -> {
101
+                        try {
102
+                            // 生成设备指令
103
+                            String deviceCommand = adapter.generateCommand(command, device);
104
+                            
105
+                            // 发布指令到 MQTT 主题
106
+                            // 这里应该通过 MQTT 客户端发送到设备
107
+                            log.info("Generated command for device {}: {}", deviceId, deviceCommand);
108
+                            
109
+                        } catch (Exception e) {
110
+                            log.error("Failed to generate command for device {}: {}", deviceId, e.getMessage(), e);
111
+                        }
112
+                    });
113
+                    
114
+        } catch (Exception e) {
115
+            log.error("Error processing device command: {}", e.getMessage(), e);
116
+        }
117
+    }
118
+}

+ 46
- 0
src/main/java/com/water/iot/entity/DeviceTelemetry.java Прегледај датотеку

@@ -0,0 +1,46 @@
1
+package com.water.iot.entity;
2
+
3
+import lombok.Data;
4
+import java.time.LocalDateTime;
5
+import java.util.Map;
6
+
7
+@Data
8
+public class DeviceTelemetry {
9
+    private String deviceId;
10
+    private String deviceType;
11
+    private LocalDateTime timestamp;
12
+    private Map<String, Object> telemetry;
13
+    private String status;
14
+    private Double temperature;
15
+    private Double pressure;
16
+    private Double flow;
17
+    
18
+    // Constructors
19
+    public DeviceTelemetry() {}
20
+    
21
+    public DeviceTelemetry(String deviceId, String deviceType, LocalDateTime timestamp, Map<String, Object> telemetry) {
22
+        this.deviceId = deviceId;
23
+        this.deviceType = deviceType;
24
+        this.timestamp = timestamp;
25
+        this.telemetry = telemetry;
26
+        this.status = "normal";
27
+        
28
+        // Extract common telemetry values
29
+        if (telemetry != null) {
30
+            Object temp = telemetry.get("temperature");
31
+            if (temp instanceof Number) {
32
+                this.temperature = ((Number) temp).doubleValue();
33
+            }
34
+            
35
+            Object pressure = telemetry.get("pressure");
36
+            if (pressure instanceof Number) {
37
+                this.pressure = ((Number) pressure).doubleValue();
38
+            }
39
+            
40
+            Object flow = telemetry.get("flow");
41
+            if (flow instanceof Number) {
42
+                this.flow = ((Number) flow).doubleValue();
43
+            }
44
+        }
45
+    }
46
+}

+ 21
- 0
src/main/java/com/water/iot/repository/DeviceInstanceRepository.java Прегледај датотеку

@@ -0,0 +1,21 @@
1
+package com.water.iot.repository;
2
+
3
+import com.water.iot.entity.DeviceInstance;
4
+import com.water.iot.entity.DeviceModel;
5
+import org.springframework.data.jpa.repository.JpaRepository;
6
+import org.springframework.stereotype.Repository;
7
+
8
+import java.List;
9
+import java.util.Optional;
10
+
11
+@Repository
12
+public interface DeviceInstanceRepository extends JpaRepository<DeviceInstance, Long> {
13
+    
14
+    Optional<DeviceInstance> findByDeviceId(String deviceId);
15
+    
16
+    List<DeviceInstance> findByIsActiveTrue();
17
+    
18
+    List<DeviceInstance> findByDeviceModel(DeviceModel deviceModel);
19
+    
20
+    List<DeviceInstance> findByDeviceModelId(Long deviceModelId);
21
+}

+ 20
- 0
src/main/java/com/water/iot/repository/DeviceModelRepository.java Прегледај датотеку

@@ -0,0 +1,20 @@
1
+package com.water.iot.repository;
2
+
3
+import com.water.iot.entity.DeviceModel;
4
+import org.springframework.data.jpa.repository.JpaRepository;
5
+import org.springframework.stereotype.Repository;
6
+
7
+import java.util.List;
8
+import java.util.Optional;
9
+
10
+@Repository
11
+public interface DeviceModelRepository extends JpaRepository<DeviceModel, Long> {
12
+    
13
+    Optional<DeviceModel> findByModelCode(String modelCode);
14
+    
15
+    List<DeviceModel> findByIsActiveTrue();
16
+    
17
+    List<DeviceModel> findByDeviceType(String deviceType);
18
+    
19
+    List<DeviceModel> findByProtocol(String protocol);
20
+}

+ 16
- 0
src/main/java/com/water/iot/repository/DeviceShadowRepository.java Прегледај датотеку

@@ -0,0 +1,16 @@
1
+package com.water.iot.repository;
2
+
3
+import com.water.iot.entity.DeviceInstance;
4
+import com.water.iot.entity.DeviceShadow;
5
+import org.springframework.data.jpa.repository.JpaRepository;
6
+import org.springframework.stereotype.Repository;
7
+
8
+import java.util.Optional;
9
+
10
+@Repository
11
+public interface DeviceShadowRepository extends JpaRepository<DeviceShadow, Long> {
12
+    
13
+    Optional<DeviceShadow> findByDevice(DeviceInstance device);
14
+    
15
+    Optional<DeviceShadow> findByDeviceId(Long deviceId);
16
+}

+ 41
- 0
src/main/java/com/water/iot/util/Constants.java Прегледај датотеку

@@ -0,0 +1,41 @@
1
+package com.water.iot.util;
2
+
3
+public class Constants {
4
+    
5
+    public static class Topics {
6
+        public static final String IOT_RAW_PREFIX = "iot.raw.";
7
+        public static final String TELEMETRY = "iot.telemetry";
8
+        public static final String COMMAND = "iot.command";
9
+        public static final String EVENT = "iot.event";
10
+        public static final String DEVICE_HEARTBEAT = "water/iot/heartbeat";
11
+    }
12
+    
13
+    public static class DeviceTypes {
14
+        public static final String TEMPERATURE = "temperature";
15
+        public static final String PRESSURE = "pressure";
16
+        public static final String FLOW = "flow";
17
+        public static final String LEVEL = "level";
18
+        public static final String WATER_QUALITY = "water_quality";
19
+    }
20
+    
21
+    public static class Protocols {
22
+        public static final String MODBUS = "modbus";
23
+        public static final String HTTP = "http";
24
+        public static final String MQTT = "mqtt";
25
+        public static final String COAP = "coap";
26
+    }
27
+    
28
+    public static class Actions {
29
+        public static final String READ = "read";
30
+        public static final String WRITE = "write";
31
+        public static final String EXECUTE = "execute";
32
+        public static final String CONFIGURE = "configure";
33
+    }
34
+    
35
+    public static class Status {
36
+        public static final String NORMAL = "normal";
37
+        public static final String WARNING = "warning";
38
+        public static final String ERROR = "error";
39
+        public static final String OFFLINE = "offline";
40
+    }
41
+}

+ 114
- 0
src/test/java/com/water/iot/simulator/DeviceSimulator.java Прегледај датотеку

@@ -0,0 +1,114 @@
1
+package com.water.iot.simulator;
2
+
3
+import com.fasterxml.jackson.databind.ObjectMapper;
4
+import org.springframework.kafka.core.KafkaTemplate;
5
+import org.springframework.stereotype.Component;
6
+
7
+import java.util.Map;
8
+import java.util.Random;
9
+import java.util.concurrent.Executors;
10
+import java.util.concurrent.ScheduledExecutorService;
11
+import java.util.concurrent.TimeUnit;
12
+
13
+@Component
14
+public class DeviceSimulator {
15
+    
16
+    private final KafkaTemplate<String, String> kafkaTemplate;
17
+    private final ObjectMapper objectMapper;
18
+    private final ScheduledExecutorService scheduler;
19
+    private final Random random;
20
+    
21
+    public DeviceSimulator(KafkaTemplate<String, String> kafkaTemplate, ObjectMapper objectMapper) {
22
+        this.kafkaTemplate = kafkaTemplate;
23
+        this.objectMapper = objectMapper;
24
+        this.scheduler = Executors.newScheduledThreadPool(3);
25
+        this.random = new Random();
26
+        
27
+        startSimulation();
28
+    }
29
+    
30
+    private void startSimulation() {
31
+        // 模拟温度传感器数据
32
+        scheduler.scheduleAtFixedRate(this::simulateTemperatureSensor, 0, 10, TimeUnit.SECONDS);
33
+        
34
+        // 模拟压力传感器数据
35
+        scheduler.scheduleAtFixedRate(this::simulatePressureSensor, 0, 15, TimeUnit.SECONDS);
36
+        
37
+        // 模拟流量计数据
38
+        scheduler.scheduleAtFixedRate(this::simulateFlowMeter, 0, 20, TimeUnit.SECONDS);
39
+    }
40
+    
41
+    private void simulateTemperatureSensor() {
42
+        try {
43
+            Map<String, Object> data = Map.of(
44
+                "deviceId", "TEMP-001-001",
45
+                "timestamp", System.currentTimeMillis(),
46
+                "protocol", "modbus",
47
+                "rawData", "AQAAAA==",
48
+                "temperature", 20.0 + random.nextDouble() * 10,
49
+                "unit", "celsius",
50
+                "status", "normal"
51
+            );
52
+            
53
+            String message = objectMapper.writeValueAsString(data);
54
+            kafkaTemplate.send("iot.raw.temperature", message);
55
+            System.out.println("Temperature data sent: " + message);
56
+            
57
+        } catch (Exception e) {
58
+            System.err.println("Error sending temperature data: " + e.getMessage());
59
+        }
60
+    }
61
+    
62
+    private void simulatePressureSensor() {
63
+        try {
64
+            Map<String, Object> data = Map.of(
65
+                "deviceId", "PRESSURE-001-001",
66
+                "timestamp", System.currentTimeMillis(),
67
+                "protocol", "modbus",
68
+                "rawData", "AQAAAA==",
69
+                "pressure", 0.5 + random.nextDouble() * 1.5,
70
+                "unit", "bar",
71
+                "status", "normal"
72
+            );
73
+            
74
+            String message = objectMapper.writeValueAsString(data);
75
+            kafkaTemplate.send("iot.raw.pressure", message);
76
+            System.out.println("Pressure data sent: " + message);
77
+            
78
+        } catch (Exception e) {
79
+            System.err.println("Error sending pressure data: " + e.getMessage());
80
+        }
81
+    }
82
+    
83
+    private void simulateFlowMeter() {
84
+        try {
85
+            Map<String, Object> data = Map.of(
86
+                "deviceId", "FLOW-001-001",
87
+                "timestamp", System.currentTimeMillis(),
88
+                "protocol", "http",
89
+                "rawData", "{\"flow_rate\": 15.2, \"total_volume\": 1250.5}",
90
+                "flow", 10.0 + random.nextDouble() * 20,
91
+                "unit", "m³/h",
92
+                "status", "normal"
93
+            );
94
+            
95
+            String message = objectMapper.writeValueAsString(data);
96
+            kafkaTemplate.send("iot.raw.flow", message);
97
+            System.out.println("Flow data sent: " + message);
98
+            
99
+        } catch (Exception e) {
100
+            System.err.println("Error sending flow data: " + e.getMessage());
101
+        }
102
+    }
103
+    
104
+    public void stopSimulation() {
105
+        scheduler.shutdown();
106
+        try {
107
+            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
108
+                scheduler.shutdownNow();
109
+            }
110
+        } catch (InterruptedException e) {
111
+            scheduler.shutdownNow();
112
+        }
113
+    }
114
+}