node-red-contrib-symi-mesh 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +592 -0
- package/examples/knx-sync-example.json +465 -0
- package/lib/device-manager.js +575 -0
- package/lib/mqtt-helper.js +659 -0
- package/lib/protocol.js +510 -0
- package/lib/serial-client.js +286 -0
- package/lib/tcp-client.js +262 -0
- package/nodes/symi-device.html +303 -0
- package/nodes/symi-device.js +344 -0
- package/nodes/symi-gateway.html +83 -0
- package/nodes/symi-gateway.js +450 -0
- package/nodes/symi-mqtt.html +94 -0
- package/nodes/symi-mqtt.js +1113 -0
- package/package.json +58 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Manager - Simplified version
|
|
3
|
+
* Manages device information and MAC/address mapping
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const EventEmitter = require('events');
|
|
7
|
+
|
|
8
|
+
const DEVICE_TYPE_NAMES = {
|
|
9
|
+
0: '未知设备', 1: '零火开关', 2: '单火开关', 3: '智能插座',
|
|
10
|
+
4: '双色调光灯', 5: '智能窗帘', 6: '情景面板', 7: '门磁传感器',
|
|
11
|
+
8: '人体感应', 9: '插卡取电', 10: '温控器', 11: '温湿度传感器',
|
|
12
|
+
12: '情景开关', 0x14: '透传丛机', 0x18: '五色调光灯', 0x94: '三合一面板'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class DeviceInfo {
|
|
16
|
+
constructor(data) {
|
|
17
|
+
this.macAddress = data.macAddress;
|
|
18
|
+
this.networkAddress = data.networkAddress;
|
|
19
|
+
this.deviceType = data.deviceType;
|
|
20
|
+
this.deviceSubType = data.deviceSubType || 1;
|
|
21
|
+
this.vendorId = data.vendorId || 0x01A8;
|
|
22
|
+
this.name = data.name || this.generateName();
|
|
23
|
+
this.channels = this.getChannelCount();
|
|
24
|
+
this.online = data.online !== undefined ? data.online : true;
|
|
25
|
+
this.state = {};
|
|
26
|
+
this.lastSeen = Date.now();
|
|
27
|
+
this.isThreeInOne = false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
generateName() {
|
|
31
|
+
const macClean = this.macAddress.replace(/:/g, '').toLowerCase();
|
|
32
|
+
const macSuffix = macClean.slice(-12);
|
|
33
|
+
|
|
34
|
+
// 三合一设备也使用温控器名称,保持设备标识不变
|
|
35
|
+
const typeName = DEVICE_TYPE_NAMES[this.deviceType] || 'Device';
|
|
36
|
+
return `${typeName}_${macSuffix}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getChannelCount() {
|
|
40
|
+
if ([1, 2, 3, 9, 12].includes(this.deviceType)) {
|
|
41
|
+
return this.deviceSubType;
|
|
42
|
+
}
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getEntityType() {
|
|
47
|
+
// 如果设备被标记为三合一,返回three_in_one类型
|
|
48
|
+
if (this.isThreeInOne) {
|
|
49
|
+
return 'three_in_one';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const typeMap = {
|
|
53
|
+
1: 'switch', 2: 'switch', 3: 'switch', 4: 'light',
|
|
54
|
+
5: 'cover', 6: 'scene', 7: 'binary_sensor', 8: 'binary_sensor',
|
|
55
|
+
9: 'switch', 10: 'climate', 11: 'sensor', 12: 'switch',
|
|
56
|
+
0x14: 'scene', 0x18: 'light'
|
|
57
|
+
};
|
|
58
|
+
return typeMap[this.deviceType] || 'switch';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getDisplayName() {
|
|
62
|
+
const entityType = this.getEntityType();
|
|
63
|
+
if (entityType === 'switch' && this.channels > 1) {
|
|
64
|
+
return `${this.name} (${this.channels}路)`;
|
|
65
|
+
}
|
|
66
|
+
return this.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
setState(key, value) {
|
|
70
|
+
this.state[key] = value;
|
|
71
|
+
this.lastSeen = Date.now();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
updateFromEvent(attrType, parameters) {
|
|
75
|
+
this.lastSeen = Date.now();
|
|
76
|
+
|
|
77
|
+
switch (attrType) {
|
|
78
|
+
case 0x02:
|
|
79
|
+
// TYPE_ON_OFF - 开关状态(仅用于开关、灯光、温控器)
|
|
80
|
+
// 窗帘(type=5)不使用0x02,忽略此消息避免误操作
|
|
81
|
+
if (this.deviceType !== 5) {
|
|
82
|
+
this.handleSwitchState(parameters);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 0x03:
|
|
86
|
+
if (parameters.length > 0) {
|
|
87
|
+
this.state.brightness = parameters[0];
|
|
88
|
+
this.state.switch = parameters[0] > 0;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case 0x04:
|
|
92
|
+
if (parameters.length > 0) this.state.colorTemp = parameters[0];
|
|
93
|
+
break;
|
|
94
|
+
case 0x05:
|
|
95
|
+
// CURT_RUN_STATUS - 窗帘运行状态
|
|
96
|
+
// 0=到头/停止, 1=打开中, 2=关闭中, 3=停止
|
|
97
|
+
if (parameters.length > 0) {
|
|
98
|
+
const newStatus = parameters[0];
|
|
99
|
+
// 如果已经到头(status=0),忽略后续的运行中状态(1或2)
|
|
100
|
+
// 直到收到新的位置(0x06)或控制命令
|
|
101
|
+
if (this.state.curtainStatus === 0 && (newStatus === 1 || newStatus === 2)) {
|
|
102
|
+
// 窗帘已到头,忽略设备继续发送的运行状态
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.state.curtainStatus = newStatus;
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case 0x06:
|
|
109
|
+
// CURT_RUN_PER_POS - 窗帘位置
|
|
110
|
+
if (parameters.length > 0) {
|
|
111
|
+
this.state.curtainPosition = parameters[0];
|
|
112
|
+
// 位置变化时清除到头标记,允许接收新的运行状态
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
case 0x0A:
|
|
116
|
+
if (parameters.length > 0) this.state.door = parameters[0] === 0x01;
|
|
117
|
+
break;
|
|
118
|
+
case 0x0C:
|
|
119
|
+
if (parameters.length > 0) this.state.motion = parameters[0] === 0x01;
|
|
120
|
+
break;
|
|
121
|
+
case 0x0E:
|
|
122
|
+
// CARD_DET_STATUS - 插卡状态(1字节)
|
|
123
|
+
if (parameters.length > 0) {
|
|
124
|
+
const cardValue = parameters[0];
|
|
125
|
+
this.state.cardInserted = (cardValue === 0x01 || cardValue === 0x1);
|
|
126
|
+
this.state.switch = this.state.cardInserted;
|
|
127
|
+
// 注意:0x80 06格式可能包含多个msg_type组合,这里只取第一个字节
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case 0x11:
|
|
131
|
+
if (parameters.length >= 5) {
|
|
132
|
+
this.state.rgb = {
|
|
133
|
+
r: parameters[0], g: parameters[1], b: parameters[2],
|
|
134
|
+
ww: parameters[3], cw: parameters[4]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case 0x16:
|
|
139
|
+
// SENSOR_TEMP - 温度传感器(2字节小端序,温度*100)
|
|
140
|
+
// 用于温湿度传感器和温控器当前温度
|
|
141
|
+
if (parameters.length >= 2) {
|
|
142
|
+
const tempRaw = parameters[0] | (parameters[1] << 8);
|
|
143
|
+
const tempValue = tempRaw / 100.0;
|
|
144
|
+
// 温控器使用currentTemp,温湿度传感器使用temperature
|
|
145
|
+
if (this.deviceType === 10) {
|
|
146
|
+
this.state.currentTemp = tempValue;
|
|
147
|
+
} else if (this.deviceType === 11) {
|
|
148
|
+
this.state.temperature = tempValue;
|
|
149
|
+
} else {
|
|
150
|
+
// 其他设备两个字段都设置
|
|
151
|
+
this.state.currentTemp = tempValue;
|
|
152
|
+
this.state.temperature = tempValue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 0x17:
|
|
157
|
+
// SENSOR_HUMI - 湿度传感器(1字节,0-100%)
|
|
158
|
+
if (parameters.length > 0) this.state.humidity = parameters[0];
|
|
159
|
+
break;
|
|
160
|
+
case 0x1B:
|
|
161
|
+
// 目标温度TMPC_TEMP:1字节,直接温度值(16-30°C)
|
|
162
|
+
if (parameters.length > 0) {
|
|
163
|
+
const temp = parameters[0];
|
|
164
|
+
if (temp >= 5 && temp <= 35) {
|
|
165
|
+
this.state.targetTemp = temp;
|
|
166
|
+
// 不推断开关状态,完全由0x02消息决定
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case 0x1C:
|
|
171
|
+
// 风速设置(温控器和三合一空调都使用)
|
|
172
|
+
if (parameters.length > 0) {
|
|
173
|
+
this.state.fanMode = parameters[0];
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case 0x1D:
|
|
177
|
+
// 模式设置(温控器和三合一空调都使用)
|
|
178
|
+
// 协议值:1=制冷, 2=制热, 3=送风, 4=除湿
|
|
179
|
+
if (parameters.length > 0) {
|
|
180
|
+
this.state.climateMode = parameters[0];
|
|
181
|
+
// 不推断开关状态,完全由0x02消息决定
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 0x68:
|
|
185
|
+
// 新风开关 (0x02=开, 0x01=关, 空参数=查询无结果)
|
|
186
|
+
// 收到0x68响应说明这是三合一面板(温控器不会响应0x68)
|
|
187
|
+
if (parameters.length > 0) {
|
|
188
|
+
// 首次收到0x68有效响应,标记为三合一
|
|
189
|
+
if (!this.isThreeInOne && this.deviceType === 10) {
|
|
190
|
+
this.isThreeInOne = true;
|
|
191
|
+
this.needsThreeInOneCheck = false;
|
|
192
|
+
}
|
|
193
|
+
this.state.freshAirSwitch = parameters[0] === 0x02;
|
|
194
|
+
} else if (this.isThreeInOne) {
|
|
195
|
+
this.state.freshAirSwitch = false;
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case 0x69:
|
|
199
|
+
// 新风模式 (0x00=送风, 0x01=排风)
|
|
200
|
+
if (this.isThreeInOne && parameters.length > 0) {
|
|
201
|
+
this.state.freshAirMode = parameters[0];
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case 0x6A:
|
|
205
|
+
// 新风风速 (1=高, 2=中, 3=低, 4=自动, 空参数=查询无结果)
|
|
206
|
+
// 收到0x6A响应也可以确认是三合一
|
|
207
|
+
if (parameters.length > 0) {
|
|
208
|
+
if (!this.isThreeInOne && this.deviceType === 10) {
|
|
209
|
+
this.isThreeInOne = true;
|
|
210
|
+
this.needsThreeInOneCheck = false;
|
|
211
|
+
}
|
|
212
|
+
this.state.freshAirSpeed = parameters[0];
|
|
213
|
+
} else if (this.isThreeInOne) {
|
|
214
|
+
this.state.freshAirSpeed = 4;
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
case 0x6B:
|
|
218
|
+
// 地暖开关 (0x02=开, 0x01=关, 空参数=查询无结果)
|
|
219
|
+
// 收到0x6B响应也可以确认是三合一
|
|
220
|
+
if (parameters.length > 0) {
|
|
221
|
+
if (!this.isThreeInOne && this.deviceType === 10) {
|
|
222
|
+
this.isThreeInOne = true;
|
|
223
|
+
this.needsThreeInOneCheck = false;
|
|
224
|
+
}
|
|
225
|
+
this.state.floorHeatingSwitch = parameters[0] === 0x02;
|
|
226
|
+
} else if (this.isThreeInOne) {
|
|
227
|
+
this.state.floorHeatingSwitch = false;
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
case 0x6C:
|
|
231
|
+
// 地暖温度 (1字节,18-32°C, 空参数=查询无结果)
|
|
232
|
+
if (this.isThreeInOne && parameters.length > 0) {
|
|
233
|
+
this.state.floorHeatingTemp = parameters[0];
|
|
234
|
+
this.state.floorHeatingSwitch = true;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case 0x94:
|
|
238
|
+
// 三合一设备状态(0x94是消息类型,不是设备类型)
|
|
239
|
+
// 收到0x94消息(带有效参数)说明这是三合一面板
|
|
240
|
+
if (parameters.length >= 8) {
|
|
241
|
+
if (!this.isThreeInOne && this.deviceType === 10) {
|
|
242
|
+
this.isThreeInOne = true;
|
|
243
|
+
this.needsThreeInOneCheck = false;
|
|
244
|
+
}
|
|
245
|
+
// 参数格式: [byte0, byte1, byte2(子类型标识), byte3, byte4, byte5(子类型标识2), ...]
|
|
246
|
+
// 新风: byte5=0x01
|
|
247
|
+
// 地暖: byte5=0x81
|
|
248
|
+
// 空调: byte2=0x81/0xC1
|
|
249
|
+
this.handleThreeInOneState(parameters);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
handleThreeInOneState(parameters) {
|
|
256
|
+
if (parameters.length < 8) return;
|
|
257
|
+
|
|
258
|
+
// 0x94消息结构(完整8字节)- 根据研发提供的结构体定义
|
|
259
|
+
// byte0: out_addr - 外机地址
|
|
260
|
+
// byte1: in_addr - 内机地址
|
|
261
|
+
// byte2: bit[0-2]=fan_level(空调风速), bit[3-5]=ctrl_mode(空调模式), bit6=resv, bit7=air_onoff(空调开关)
|
|
262
|
+
// byte3: air_temp - 空调温度 (16-30°C)
|
|
263
|
+
// byte4: room_temp - 室温
|
|
264
|
+
// byte5: bit0=floor_en(地暖存在), bit[1-6]=resv2, bit7=floor_onoff(地暖开关)
|
|
265
|
+
// byte6: floor_temp - 地暖温度 (18-32°C)
|
|
266
|
+
// byte7: bit[0-2]=fresh_fan(新风风速), bit[3-4]=fresh_mode(新风模式), bit5=fresh_en(新风存在), bit6=resv1, bit7=fresh_onoff(新风开关)
|
|
267
|
+
|
|
268
|
+
const byte0 = parameters[0]; // 外机地址
|
|
269
|
+
const byte1 = parameters[1]; // 内机地址
|
|
270
|
+
const byte2 = parameters[2]; // 空调状态
|
|
271
|
+
const byte3 = parameters[3]; // 空调温度
|
|
272
|
+
const byte4 = parameters[4]; // 室温
|
|
273
|
+
const byte5 = parameters[5]; // 地暖状态
|
|
274
|
+
const byte6 = parameters[6]; // 地暖温度
|
|
275
|
+
const byte7 = parameters[7]; // 新风状态
|
|
276
|
+
|
|
277
|
+
// ============ 空调状态解析 (byte2, byte3, byte4) ============
|
|
278
|
+
// byte2: bit7=空调开关
|
|
279
|
+
const acOn = (byte2 & 0x80) !== 0;
|
|
280
|
+
this.state.climateSwitch = acOn;
|
|
281
|
+
|
|
282
|
+
// byte2: bit[3-5]=空调模式 (0=制冷, 1=制热, 2=通风, 3=除湿, 4=自动)
|
|
283
|
+
const ctrlMode = (byte2 >> 3) & 0x07;
|
|
284
|
+
const modeMap = { 0: 1, 1: 2, 2: 3, 3: 4, 4: 1 }; // 0=冷→1(cool), 1=热→2(heat), 2=通风→3(fan_only), 3=除湿→4(dry), 4=自动→1(cool)
|
|
285
|
+
if (modeMap[ctrlMode] !== undefined) {
|
|
286
|
+
this.state.climateMode = modeMap[ctrlMode];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// byte2: bit[0-2]=空调风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
|
|
290
|
+
const fanLevel = byte2 & 0x07;
|
|
291
|
+
this.state.fanMode = fanLevel;
|
|
292
|
+
|
|
293
|
+
// byte3: 空调设定温度 (16-30°C)
|
|
294
|
+
if (byte3 >= 16 && byte3 <= 30) {
|
|
295
|
+
this.state.targetTemp = byte3;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// byte4: 室温
|
|
299
|
+
if (byte4 >= 10 && byte4 <= 50) {
|
|
300
|
+
this.state.currentTemp = byte4;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============ 地暖状态解析 (byte5, byte6) ============
|
|
304
|
+
// byte5: bit7=地暖开关, bit0=地暖存在
|
|
305
|
+
const floorHeatingOn = (byte5 & 0x80) !== 0;
|
|
306
|
+
const floorHeatingEn = (byte5 & 0x01) !== 0;
|
|
307
|
+
this.state.floorHeatingSwitch = floorHeatingOn;
|
|
308
|
+
|
|
309
|
+
// byte6: 地暖温度 (18-32°C)
|
|
310
|
+
if (byte6 >= 18 && byte6 <= 32) {
|
|
311
|
+
this.state.floorHeatingTemp = byte6;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============ 新风状态解析 (byte7) ============
|
|
315
|
+
// byte7: bit7=新风开关
|
|
316
|
+
const freshAirOn = (byte7 & 0x80) !== 0;
|
|
317
|
+
this.state.freshAirSwitch = freshAirOn;
|
|
318
|
+
|
|
319
|
+
// byte7: bit5=新风存在
|
|
320
|
+
const freshAirEn = (byte7 & 0x20) !== 0;
|
|
321
|
+
|
|
322
|
+
// byte7: bit4=新风模式 (0=送风forward, 1=排风reverse)
|
|
323
|
+
const freshMode = (byte7 >> 4) & 0x01;
|
|
324
|
+
this.state.freshAirMode = freshMode;
|
|
325
|
+
|
|
326
|
+
// byte7: bit[0-2]=新风风速 (协议值: 0=自动, 1=低, 2=中, 4=高)
|
|
327
|
+
const freshFan = byte7 & 0x07;
|
|
328
|
+
this.state.freshAirSpeed = freshFan;
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
parseClimateMode(modeValue) {
|
|
333
|
+
// 根据协议:1=制冷, 2=制热, 3=送风, 4=除湿
|
|
334
|
+
const modes = {
|
|
335
|
+
0x14: 'cool', 0x15: 'cool', 0x16: 'cool', 0x17: 'cool',
|
|
336
|
+
0x18: 'cool', 0x19: 'cool', 0x1B: 'heat', 0x1C: 'heat',
|
|
337
|
+
0x1D: 'fan_only', 0x1E: 'fan_only', 0x1F: 'fan_only',
|
|
338
|
+
0x20: 'dry'
|
|
339
|
+
};
|
|
340
|
+
return modes[modeValue] || 'off';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
handleSwitchState(parameters) {
|
|
344
|
+
if (parameters.length === 0) return;
|
|
345
|
+
|
|
346
|
+
// 根据协议:TYPE_ON_OFF,每2位表示1路开关,b01=关,b10=开
|
|
347
|
+
// 1-4路开关:1字节
|
|
348
|
+
// 5-6路开关:2字节,小端序
|
|
349
|
+
|
|
350
|
+
if (this.channels === 1) {
|
|
351
|
+
// 单路开关
|
|
352
|
+
const value = parameters[0];
|
|
353
|
+
this.state.switch = value === 0x02;
|
|
354
|
+
} else if (this.channels <= 4) {
|
|
355
|
+
// 1-4路开关:1字节,每2位表示1路
|
|
356
|
+
const value = parameters[0];
|
|
357
|
+
// 保存原始状态值供控制时使用
|
|
358
|
+
this.state.switchState = value;
|
|
359
|
+
for (let i = 0; i < this.channels; i++) {
|
|
360
|
+
const bitPos = i * 2;
|
|
361
|
+
const bitValue = (value >> bitPos) & 0x03;
|
|
362
|
+
if (bitValue === 0x01) {
|
|
363
|
+
this.state[`switch_${i + 1}`] = false;
|
|
364
|
+
} else if (bitValue === 0x02) {
|
|
365
|
+
this.state[`switch_${i + 1}`] = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} else if (this.channels === 6) {
|
|
369
|
+
// 6路开关:2字节,小端序
|
|
370
|
+
let value;
|
|
371
|
+
if (parameters.length >= 2) {
|
|
372
|
+
// 小端序:低字节在前
|
|
373
|
+
value = parameters[0] | (parameters[1] << 8);
|
|
374
|
+
} else {
|
|
375
|
+
// 兼容1字节情况,只处理前4路
|
|
376
|
+
value = parameters[0];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 保存原始状态值供控制时使用
|
|
380
|
+
this.state.switchState = value;
|
|
381
|
+
|
|
382
|
+
// 处理所有6路
|
|
383
|
+
for (let i = 0; i < 6; i++) {
|
|
384
|
+
const bitPos = i * 2;
|
|
385
|
+
const bitValue = (value >> bitPos) & 0x03;
|
|
386
|
+
if (bitValue === 0x01) {
|
|
387
|
+
this.state[`switch_${i + 1}`] = false;
|
|
388
|
+
} else if (bitValue === 0x02) {
|
|
389
|
+
this.state[`switch_${i + 1}`] = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 获取当前开关状态值(用于控制时组合状态)
|
|
396
|
+
getCurrentSwitchState() {
|
|
397
|
+
if (this.channels === 1) {
|
|
398
|
+
return this.state.switch ? 0x02 : 0x01;
|
|
399
|
+
} else if (this.channels <= 4) {
|
|
400
|
+
let stateValue = 0;
|
|
401
|
+
for (let i = 0; i < this.channels; i++) {
|
|
402
|
+
const bitPos = i * 2;
|
|
403
|
+
const channelState = this.state[`switch_${i + 1}`];
|
|
404
|
+
const bitValue = (channelState === true) ? 0x02 : 0x01;
|
|
405
|
+
stateValue |= (bitValue << bitPos);
|
|
406
|
+
}
|
|
407
|
+
return stateValue;
|
|
408
|
+
} else if (this.channels === 6) {
|
|
409
|
+
let stateValue = 0;
|
|
410
|
+
for (let i = 0; i < 6; i++) {
|
|
411
|
+
const bitPos = i * 2;
|
|
412
|
+
const channelState = this.state[`switch_${i + 1}`];
|
|
413
|
+
const bitValue = (channelState === true) ? 0x02 : 0x01;
|
|
414
|
+
stateValue |= (bitValue << bitPos);
|
|
415
|
+
}
|
|
416
|
+
return stateValue;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
class DeviceManager extends EventEmitter {
|
|
423
|
+
constructor(context, logger = console) {
|
|
424
|
+
super();
|
|
425
|
+
this.context = context;
|
|
426
|
+
this.logger = logger;
|
|
427
|
+
this.devices = new Map();
|
|
428
|
+
this.macToAddress = new Map();
|
|
429
|
+
this.addressToMac = new Map();
|
|
430
|
+
this.loadDevices();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
loadDevices() {
|
|
434
|
+
try {
|
|
435
|
+
const saved = this.context.get('symi_devices') || {};
|
|
436
|
+
const macMap = this.context.get('symi_mac_map') || {};
|
|
437
|
+
const addrMap = this.context.get('symi_addr_map') || {};
|
|
438
|
+
|
|
439
|
+
Object.entries(saved).forEach(([mac, data]) => {
|
|
440
|
+
const device = new DeviceInfo(data);
|
|
441
|
+
// 恢复三合一标记
|
|
442
|
+
if (data.isThreeInOne) {
|
|
443
|
+
device.isThreeInOne = true;
|
|
444
|
+
}
|
|
445
|
+
this.devices.set(mac, device);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
this.macToAddress = new Map(Object.entries(macMap));
|
|
449
|
+
this.addressToMac = new Map(Object.entries(addrMap).map(([k, v]) => [parseInt(k), v]));
|
|
450
|
+
|
|
451
|
+
this.logger.log(`Loaded ${this.devices.size} devices from storage`);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
this.logger.error('Error loading devices:', error);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
saveDevices() {
|
|
458
|
+
try {
|
|
459
|
+
const devicesObj = {};
|
|
460
|
+
this.devices.forEach((device, mac) => {
|
|
461
|
+
devicesObj[mac] = {
|
|
462
|
+
macAddress: device.macAddress,
|
|
463
|
+
networkAddress: device.networkAddress,
|
|
464
|
+
deviceType: device.deviceType,
|
|
465
|
+
deviceSubType: device.deviceSubType,
|
|
466
|
+
vendorId: device.vendorId,
|
|
467
|
+
name: device.name,
|
|
468
|
+
channels: device.channels,
|
|
469
|
+
online: device.online,
|
|
470
|
+
isThreeInOne: device.isThreeInOne || false
|
|
471
|
+
};
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const macMapObj = Object.fromEntries(this.macToAddress);
|
|
475
|
+
const addrMapObj = Object.fromEntries(
|
|
476
|
+
Array.from(this.addressToMac.entries()).map(([k, v]) => [k.toString(), v])
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
this.context.set('symi_devices', devicesObj);
|
|
480
|
+
this.context.set('symi_mac_map', macMapObj);
|
|
481
|
+
this.context.set('symi_addr_map', addrMapObj);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
this.logger.error('Error saving devices:', error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
addOrUpdateDevice(deviceData) {
|
|
488
|
+
const mac = deviceData.macAddress;
|
|
489
|
+
const addr = deviceData.networkAddress;
|
|
490
|
+
|
|
491
|
+
let device = this.devices.get(mac);
|
|
492
|
+
let isNew = false;
|
|
493
|
+
let nameChanged = false;
|
|
494
|
+
|
|
495
|
+
if (!device) {
|
|
496
|
+
device = new DeviceInfo(deviceData);
|
|
497
|
+
this.devices.set(mac, device);
|
|
498
|
+
isNew = true;
|
|
499
|
+
} else {
|
|
500
|
+
const oldName = device.name;
|
|
501
|
+
if (device.networkAddress !== addr) {
|
|
502
|
+
this.logger.log(`Device ${mac} address changed: 0x${device.networkAddress.toString(16)} -> 0x${addr.toString(16)}`);
|
|
503
|
+
device.networkAddress = addr;
|
|
504
|
+
}
|
|
505
|
+
device.online = deviceData.online !== undefined ? deviceData.online : true;
|
|
506
|
+
device.lastSeen = Date.now();
|
|
507
|
+
|
|
508
|
+
// 检查名称是否变化(如被标记为三合一)
|
|
509
|
+
if (device.name !== oldName) {
|
|
510
|
+
nameChanged = true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.macToAddress.set(mac, addr);
|
|
515
|
+
this.addressToMac.set(addr, mac);
|
|
516
|
+
|
|
517
|
+
this.saveDevices();
|
|
518
|
+
this.emit('device-updated', device, isNew || nameChanged);
|
|
519
|
+
|
|
520
|
+
return device;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
getDeviceByMac(mac) {
|
|
524
|
+
return this.devices.get(mac);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
getDeviceByAddress(addr) {
|
|
528
|
+
const mac = this.addressToMac.get(addr);
|
|
529
|
+
return mac ? this.devices.get(mac) : null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
getAllDevices() {
|
|
533
|
+
return Array.from(this.devices.values());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
getDeviceList() {
|
|
537
|
+
return this.getAllDevices().map(d => ({
|
|
538
|
+
mac: d.macAddress,
|
|
539
|
+
name: d.getDisplayName(),
|
|
540
|
+
type: d.getEntityType(),
|
|
541
|
+
channels: d.channels,
|
|
542
|
+
online: d.online,
|
|
543
|
+
deviceType: d.isThreeInOne ? 0x94 : d.deviceType,
|
|
544
|
+
isThreeInOne: d.isThreeInOne || false
|
|
545
|
+
}));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
updateDeviceState(addr, attrType, parameters) {
|
|
549
|
+
let device = this.getDeviceByAddress(addr);
|
|
550
|
+
|
|
551
|
+
if (device) {
|
|
552
|
+
device.updateFromEvent(attrType, parameters);
|
|
553
|
+
this.saveDevices();
|
|
554
|
+
|
|
555
|
+
// 所有消息都触发state-changed(包括0x94)
|
|
556
|
+
// 0x94用于面板物理操作的状态同步
|
|
557
|
+
this.emit('state-changed', device, attrType, parameters);
|
|
558
|
+
|
|
559
|
+
return device;
|
|
560
|
+
}
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
clearAll() {
|
|
565
|
+
this.devices.clear();
|
|
566
|
+
this.macToAddress.clear();
|
|
567
|
+
this.addressToMac.clear();
|
|
568
|
+
this.context.set('symi_devices', {});
|
|
569
|
+
this.context.set('symi_mac_map', {});
|
|
570
|
+
this.context.set('symi_addr_map', {});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
module.exports = { DeviceManager, DeviceInfo, DEVICE_TYPE_NAMES };
|
|
575
|
+
|