iobroker.sonoff 3.3.0 → 4.0.0

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.
@@ -0,0 +1,1280 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FORBIDDEN_CHARS = void 0;
7
+ const datapoints_1 = __importDefault(require("./datapoints"));
8
+ const hueCalc = true;
9
+ exports.FORBIDDEN_CHARS = /[\]\\[*,;'"`<>\\?]/g;
10
+ const NO_PREFIX = '';
11
+ /*
12
+ * HSV to RGB color conversion
13
+ *
14
+ * H runs from 0 to 360 degrees
15
+ * S and V run from 0 to 100
16
+ *
17
+ * Ported from the excellent java algorithm by Eugene Vishnevsky at:
18
+ * http://www.cs.rit.edu/~ncs/color/t_convert.html
19
+ */
20
+ function hsvToRgb(h, s, v) {
21
+ let r;
22
+ let g;
23
+ let b;
24
+ // Make sure our arguments stay in-range
25
+ h = Math.max(0, Math.min(360, h));
26
+ s = Math.max(0, Math.min(100, s));
27
+ v = Math.max(0, Math.min(100, v));
28
+ // We accept saturation and value arguments from 0 to 100 because that's
29
+ // how Photoshop represents those values. Internally, however, the
30
+ // saturation and value are calculated from a range of 0 to 1. We make
31
+ // That conversion here.
32
+ s /= 100;
33
+ v /= 100;
34
+ if (s === 0) {
35
+ // Achromatic (grey)
36
+ r = g = b = v;
37
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
38
+ }
39
+ h /= 60; // sector 0 to 5
40
+ const i = Math.floor(h);
41
+ const f = h - i; // factorial part of h
42
+ const p = v * (1 - s);
43
+ const q = v * (1 - s * f);
44
+ const t = v * (1 - s * (1 - f));
45
+ switch (i) {
46
+ case 0:
47
+ r = v;
48
+ g = t;
49
+ b = p;
50
+ break;
51
+ case 1:
52
+ r = q;
53
+ g = v;
54
+ b = p;
55
+ break;
56
+ case 2:
57
+ r = p;
58
+ g = v;
59
+ b = t;
60
+ break;
61
+ case 3:
62
+ r = p;
63
+ g = q;
64
+ b = v;
65
+ break;
66
+ case 4:
67
+ r = t;
68
+ g = p;
69
+ b = v;
70
+ break;
71
+ default: // case 5:
72
+ r = v;
73
+ g = p;
74
+ b = q;
75
+ }
76
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
77
+ }
78
+ function componentToHex(c) {
79
+ const hex = c.toString(16);
80
+ return hex.length === 1 ? `0${hex}` : hex;
81
+ }
82
+ function toPaddedHexString(num, len) {
83
+ if (len === 2) {
84
+ if (num > 255) {
85
+ num = 255;
86
+ }
87
+ }
88
+ const str = num.toString(16);
89
+ if (str.length >= len) {
90
+ return str;
91
+ }
92
+ return '0'.repeat(len - str.length) + str;
93
+ }
94
+ class MQTTBase {
95
+ clients = {};
96
+ tasks = [];
97
+ taskCallbacks = [];
98
+ mappingClients = {};
99
+ adapter;
100
+ cacheAddedObjects = {};
101
+ cachedModeExor = {};
102
+ cachedReadColors = {};
103
+ cachePowerObjects = {};
104
+ specVars = [
105
+ 'Red',
106
+ 'Green',
107
+ 'Blue',
108
+ 'WW',
109
+ 'CW',
110
+ 'Color',
111
+ 'RGB_POWER',
112
+ 'WW_POWER',
113
+ 'CW_POWER',
114
+ 'Hue',
115
+ 'Saturation',
116
+ ];
117
+ config;
118
+ constructor(adapter) {
119
+ this.adapter = adapter;
120
+ this.config = adapter.config;
121
+ this.config.defaultQoS = parseInt(this.config.defaultQoS, 10) || 0;
122
+ }
123
+ setColor(channelId, val) {
124
+ const stateId = 'Color';
125
+ if (this.clients[channelId]?._map?.[stateId]) {
126
+ setImmediate(() => this.sendState2Client(this.clients[channelId], this.clients[channelId]._map[stateId] || 'cmnd/sonoff/Color', val, this.config.defaultQoS));
127
+ }
128
+ else if (this.clients[channelId]?._fallBackName) {
129
+ setImmediate(() => this.sendState2Client(this.clients[channelId], `cmnd/${this.clients[channelId]._fallBackName}/${stateId}`, val, this.config.defaultQoS));
130
+ }
131
+ else {
132
+ this.adapter.log.warn(`Unknown mapping for "${stateId}"`);
133
+ }
134
+ }
135
+ setPower(channelId, val) {
136
+ const stateId = 'POWER';
137
+ if (val === '' || val === null || val === undefined) {
138
+ return this.adapter.log.debug('Empty power was ignored');
139
+ }
140
+ if (this.clients[channelId]._map?.[stateId]) {
141
+ setImmediate(() => this.sendState2Client(this.clients[channelId], this.clients[channelId]._map[stateId] || 'cmnd/sonoff/POWER', val ? 'ON' : 'OFF', this.config.defaultQoS));
142
+ }
143
+ else if (this.clients[channelId]._fallBackName) {
144
+ setImmediate(() => this.sendState2Client(this.clients[channelId], `cmnd/${this.clients[channelId]._fallBackName}/${stateId}`, val, this.config.defaultQoS));
145
+ }
146
+ else {
147
+ this.adapter.log.warn(`Unknown mapping for "${stateId}"`);
148
+ }
149
+ }
150
+ setZbSendCommand(channelId, deviceId, attribute, val) {
151
+ // Send Zigbee command via ZbSend
152
+ const zbSendCommand = JSON.stringify({
153
+ device: deviceId,
154
+ send: {
155
+ [attribute]: val,
156
+ },
157
+ });
158
+ if (this.clients[channelId]?._fallBackName) {
159
+ setImmediate(() => this.sendState2Client(this.clients[channelId], `cmnd/${this.clients[channelId]._fallBackName}/ZbSend`, zbSendCommand, this.config.defaultQoS));
160
+ }
161
+ else {
162
+ this.adapter.log.warn(`Cannot send ZbSend command for device ${deviceId}: client not found`);
163
+ }
164
+ }
165
+ /**
166
+ * Transform shutter state names to correct Tasmota command format
167
+ * e.g., "Shutter1_Position" -> "ShutterPosition1"
168
+ * e.g., "Shutter1_Tilt" -> "ShutterTilt1"
169
+ *
170
+ * @param stateId - The original state ID to transform
171
+ * @returns The transformed state ID for Tasmota commands
172
+ */
173
+ transformShutterStateId(stateId) {
174
+ const shutterMatch = stateId.match(/^Shutter(\d+)_(Position|Direction|Target|Tilt)$/);
175
+ if (shutterMatch) {
176
+ const shutterNumber = shutterMatch[1];
177
+ const command = shutterMatch[2];
178
+ return `Shutter${command}${shutterNumber}`;
179
+ }
180
+ return stateId;
181
+ }
182
+ setStateImmediate(channelId, stateId, val) {
183
+ // Transform shutter state names to correct Tasmota command format
184
+ const transformedStateId = this.transformShutterStateId(stateId);
185
+ if (this.clients[channelId]?._map?.[stateId]) {
186
+ setImmediate(() => this.sendState2Client(this.clients[channelId], this.clients[channelId]._map[stateId] || `cmnd/sonoff/${transformedStateId}`, val, this.config.defaultQoS));
187
+ }
188
+ else if (this.clients[channelId]?._fallBackName) {
189
+ setImmediate(() => this.sendState2Client(this.clients[channelId], `cmnd/${this.clients[channelId]._fallBackName}/${transformedStateId}`, val, this.config.defaultQoS));
190
+ }
191
+ else {
192
+ this.adapter.log.warn(`Unknown mapping for "${stateId}"`);
193
+ }
194
+ }
195
+ async _setState(id, val) {
196
+ this.adapter.log.debug(`Set state after task: ${id}`);
197
+ await this.adapter.setForeignStateAsync(id, val, true);
198
+ }
199
+ updateState(task, val, callback) {
200
+ if (typeof val === 'function') {
201
+ callback = val;
202
+ val = undefined;
203
+ }
204
+ if (val !== undefined) {
205
+ task.setState = true;
206
+ task.setValue = val;
207
+ }
208
+ this.tasks.push(task);
209
+ if (callback) {
210
+ this.taskCallbacks.push(callback);
211
+ }
212
+ this.adapter.log.debug(`Update state ${task.id} - ${this.tasks.length}`);
213
+ if (this.tasks.length === 1) {
214
+ this.processTasks().catch(err => this.adapter.log.error(err));
215
+ }
216
+ }
217
+ async onStateChangedColors(id, state, channelId, stateId) {
218
+ if (!channelId) {
219
+ const parts = id.split('.');
220
+ stateId = parts.pop() || '';
221
+ if (stateId === 'level' ||
222
+ stateId === 'state' ||
223
+ stateId === 'red' ||
224
+ stateId === 'blue' ||
225
+ stateId === 'green') {
226
+ stateId = `${parts.pop()}.${stateId}`;
227
+ }
228
+ channelId = parts.splice(2, parts.length).join('.');
229
+ }
230
+ const ledModeIdExor = `${this.adapter.namespace}.${channelId}.modeLedExor`;
231
+ if (this.cachedModeExor[ledModeIdExor] === undefined) {
232
+ const _state = await this.adapter.getForeignStateAsync(ledModeIdExor);
233
+ this.cachedModeExor[ledModeIdExor] = _state ? _state.val || false : true;
234
+ setImmediate(() => this.onStateChangedColors(id, state, channelId, stateId));
235
+ return;
236
+ }
237
+ // ledstripe objects
238
+ const exorWhiteLeds = this.cachedModeExor[ledModeIdExor]; // exor for white leds and color leds => if white leds are switched on, color leds are switched off and vice versa (default on)
239
+ // now evaluate ledstripe vars
240
+ // adaptions for magichome tasmota
241
+ if (stateId?.match(/Color\d?/)) {
242
+ // id = sonoff.0.DVES_96ABFA.Color
243
+ // statid=Color
244
+ // state = {"val":"#faadcf","ack":false,"ts":1520146102580,"q":0,"from":"system.this.adapter.web.0","lc":1520146102580}
245
+ // set white to rgb or rgbww
246
+ const obj = await this.adapter.getObjectAsync(id);
247
+ if (!obj) {
248
+ this.adapter.log.warn(`Invalid rgbww obj for ${id}`);
249
+ }
250
+ else if (typeof state.val !== 'string') {
251
+ this.adapter.log.warn(`Invalid rgbww state value for ${id} : ${JSON.stringify(state.val)} needs to be a string`);
252
+ }
253
+ else {
254
+ const role = obj.common.role;
255
+ let color;
256
+ if (role === 'level.color.rgbww') {
257
+ // rgbww
258
+ if (state.val.toUpperCase() === '#FFFFFF') {
259
+ // transform white to WW
260
+ //color='000000FF';
261
+ color = `${state.val.substring(1)}00`;
262
+ }
263
+ else {
264
+ // strip # char and add ww
265
+ color = `${state.val.substring(1)}00`;
266
+ }
267
+ }
268
+ else if (role === 'level.color.rgbcwww') {
269
+ color = `${state.val.substring(1)}0000`;
270
+ }
271
+ else {
272
+ // rgb, strip # char
273
+ color = state.val.substring(1);
274
+ }
275
+ this.setColor(channelId, color);
276
+ // set rgb too
277
+ const hidE = id.split('.');
278
+ const deviceDesc = `${hidE[0]}.${hidE[1]}.${hidE[2]}`;
279
+ await this.adapter.setStateAsync(`${deviceDesc}.Red`, (100 * parseInt(color.substring(0, 2), 16)) / 255, true);
280
+ await this.adapter.setStateAsync(`${deviceDesc}.Green`, (100 * parseInt(color.substring(2, 4), 16)) / 255, true);
281
+ await this.adapter.setStateAsync(`${deviceDesc}.Blue`, (100 * parseInt(color.substring(4, 6), 16)) / 255, true);
282
+ }
283
+ return;
284
+ }
285
+ const hidE = id.split('.');
286
+ const deviceDesc = `${hidE[0]}.${hidE[1]}.${hidE[2]}`;
287
+ if (stateId?.match(/Red\d?/)) {
288
+ // set red component
289
+ if (state.val > 100) {
290
+ state.val = 100;
291
+ }
292
+ const red = toPaddedHexString(Math.floor((255 * state.val) / 100), 2);
293
+ const idAlive = `${deviceDesc}.Color`;
294
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
295
+ if (!_state) {
296
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
297
+ return;
298
+ }
299
+ const color = _state.val?.toString().substring(1) || '000000';
300
+ // replace red component
301
+ const out = red + color.substring(2, 10);
302
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
303
+ this.setColor(channelId, out);
304
+ }
305
+ else if (stateId?.match(/Green\d?/)) {
306
+ // set green component
307
+ if (state.val > 100) {
308
+ state.val = 100;
309
+ }
310
+ const green = toPaddedHexString(Math.floor((255 * state.val) / 100), 2);
311
+ const idAlive = `${deviceDesc}.Color`;
312
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
313
+ if (!_state) {
314
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
315
+ return;
316
+ }
317
+ const color = _state.val?.toString().substring(1) || '000000';
318
+ // replace green component
319
+ const out = color.substring(0, 2) + green + color.substring(4, 10);
320
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
321
+ this.setColor(channelId, out);
322
+ return;
323
+ }
324
+ if (stateId?.match(/Blue\d?/)) {
325
+ // set blue component
326
+ if (state.val > 100) {
327
+ state.val = 100;
328
+ }
329
+ const blue = toPaddedHexString(Math.floor((255 * state.val) / 100), 2);
330
+ const idAlive = `${deviceDesc}.Color`;
331
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
332
+ if (!_state) {
333
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
334
+ return;
335
+ }
336
+ const color = _state.val?.toString().substring(1) || '000000';
337
+ // replace blue component
338
+ const out = color.substring(0, 4) + blue + color.substring(6, 10);
339
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
340
+ this.setColor(channelId, out);
341
+ return;
342
+ }
343
+ if (stateId?.match(/RGB_POWER\d?/)) {
344
+ // set ww component
345
+ const rgbpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
346
+ const idAlive = `${deviceDesc}.Color`;
347
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
348
+ if (!_state || typeof _state.val !== 'string') {
349
+ this.adapter.log.info(`Invalid state color for ${idAlive}, correcting value to #000000`);
350
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
351
+ return;
352
+ }
353
+ const color = _state.val.substring(1);
354
+ let rgb = '000000';
355
+ if (rgbpow) {
356
+ rgb = 'FFFFFF';
357
+ }
358
+ // replace rgb component
359
+ let out = rgb + color.substring(6, 10);
360
+ if (rgbpow && exorWhiteLeds) {
361
+ out = `${rgb}0000`;
362
+ let idAlive = `${deviceDesc}.WW_POWER`;
363
+ await this.adapter.setStateAsync(idAlive, false, false);
364
+ idAlive = `${deviceDesc}.WW`;
365
+ await this.adapter.setStateAsync(idAlive, 0, false);
366
+ idAlive = `${deviceDesc}.CW_POWER`;
367
+ await this.adapter.setStateAsync(idAlive, false, false);
368
+ idAlive = `${deviceDesc}.CW`;
369
+ await this.adapter.setStateAsync(idAlive, 0, false);
370
+ }
371
+ this.setColor(channelId, out);
372
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
373
+ if (rgbpow) {
374
+ this.setPower(channelId, true);
375
+ }
376
+ // if led_mode&1, exor white leds
377
+ return;
378
+ }
379
+ if (hueCalc && stateId?.match(/Hue\d?/)) {
380
+ // calc hue + saturation params to rgb
381
+ let hue = state.val;
382
+ if (hue > 359) {
383
+ hue = 359;
384
+ }
385
+ // recalc color by hue
386
+ const idAlive = `${deviceDesc}.Dimmer`;
387
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
388
+ if (!_state) {
389
+ const dim = 100;
390
+ await this.adapter.setStateAsync(idAlive, dim, true);
391
+ }
392
+ else {
393
+ const dim = _state.val;
394
+ const idAlive = `${deviceDesc}.Saturation`;
395
+ const __state = await this.adapter.getForeignStateAsync(idAlive);
396
+ if (!__state) {
397
+ const sat = 100;
398
+ await this.adapter.setStateAsync(idAlive, sat, true);
399
+ }
400
+ else {
401
+ const sat = __state.val;
402
+ const rgb = hsvToRgb(hue, sat, dim);
403
+ const hexVal = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
404
+ const idAlive = `${deviceDesc}.Color`;
405
+ await this.adapter.setStateAsync(idAlive, `#${hexVal}`, false);
406
+ }
407
+ }
408
+ return;
409
+ }
410
+ if (hueCalc && stateId?.match(/Saturation\d?/)) {
411
+ let sat = state.val;
412
+ if (sat > 100) {
413
+ sat = 100;
414
+ }
415
+ // recalc color by saturation
416
+ const idAlive = `${deviceDesc}.Dimmer`;
417
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
418
+ if (!_state) {
419
+ const dim = 100;
420
+ await this.adapter.setStateAsync(idAlive, dim, true);
421
+ // this.adapter.log.warn('ill state Dimmer');
422
+ }
423
+ else {
424
+ const dim = _state.val;
425
+ const idAlive = `${deviceDesc}.Hue`;
426
+ const __state = await this.adapter.getForeignStateAsync(idAlive);
427
+ if (!__state) {
428
+ const hue = 100;
429
+ await this.adapter.setStateAsync(idAlive, hue, true);
430
+ }
431
+ else {
432
+ const hue = __state.val;
433
+ const rgb = hsvToRgb(hue, sat, dim);
434
+ const hexVal = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
435
+ const idAlive = `${deviceDesc}.Color`;
436
+ await this.adapter.setStateAsync(idAlive, `#${hexVal}`, false);
437
+ }
438
+ }
439
+ return;
440
+ }
441
+ // get color attributes to check other ledstripe vars
442
+ const idAlive = `${deviceDesc}.Color`;
443
+ const obj = await this.adapter.getForeignObjectAsync(idAlive);
444
+ if (!obj) {
445
+ // no color object
446
+ this.adapter.log.warn(`Unknown object: ${id}: ${JSON.stringify(state)}`);
447
+ }
448
+ else {
449
+ const role = obj.common.role;
450
+ //if (role='level.color.rgb') return;
451
+ let wwindex;
452
+ if (role === 'level.color.rgbww') {
453
+ wwindex = 6;
454
+ }
455
+ else {
456
+ wwindex = 8;
457
+ }
458
+ if (stateId?.match(/WW_POWER\d?/)) {
459
+ // set ww component
460
+ const wwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
461
+ let idAlive = `${deviceDesc}.Color`;
462
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
463
+ if (!_state) {
464
+ this.adapter.log.warn('ill state color');
465
+ return;
466
+ }
467
+ const color = _state.val?.toString().substring(1) || '000000';
468
+ let ww = '00';
469
+ if (wwpow) {
470
+ ww = 'FF';
471
+ }
472
+ // replace ww component
473
+ let out = color.substring(0, wwindex) + ww;
474
+ if (wwpow && exorWhiteLeds) {
475
+ out = `000000${ww}`;
476
+ const idAlive = `${deviceDesc}.RGB_POWER`;
477
+ await this.adapter.setStateAsync(idAlive, false, false);
478
+ }
479
+ idAlive = `${deviceDesc}.Color`;
480
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
481
+ this.setColor(channelId, out);
482
+ // set ww channel
483
+ idAlive = `${deviceDesc}.WW`;
484
+ await this.adapter.setStateAsync(idAlive, (100 * parseInt(out.substring(6, 8), 16)) / 255, true);
485
+ // in case POWER is off, switch it on
486
+ wwpow && this.setPower(channelId, true);
487
+ return;
488
+ }
489
+ if (stateId?.match(/CW_POWER\d?/)) {
490
+ // set ww component
491
+ const cwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
492
+ let idAlive = `${deviceDesc}.Color`;
493
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
494
+ if (!_state) {
495
+ this.adapter.log.warn('ill state color');
496
+ return;
497
+ }
498
+ const color = _state.val?.toString().substring(1) || '000000';
499
+ let cw = '00';
500
+ if (cwpow) {
501
+ cw = 'FF';
502
+ }
503
+ // replace cw component
504
+ let out = color.substring(0, 6) + cw + color.substring(8, 10);
505
+ if (cwpow && exorWhiteLeds) {
506
+ out = `000000${cw}${color.substring(8, 10)}`;
507
+ const idAlive = `${deviceDesc}.RGB_POWER`;
508
+ await this.adapter.setStateAsync(idAlive, false, false);
509
+ }
510
+ idAlive = `${deviceDesc}.Color`;
511
+ await this.adapter.setStateAsync(idAlive, `#${out}`, false);
512
+ this.setColor(channelId, out);
513
+ // set cw channel
514
+ idAlive = `${deviceDesc}.CW`;
515
+ await this.adapter.setStateAsync(idAlive, (100 * parseInt(out.substring(6, 8), 16)) / 255, true);
516
+ // in case POWER is off, switch it on
517
+ if (cwpow) {
518
+ const idAlive = `${deviceDesc}.POWER`;
519
+ await this.adapter.setStateAsync(idAlive, true, false);
520
+ }
521
+ return;
522
+ }
523
+ if (stateId?.match(/WW\d?/)) {
524
+ // set ww component
525
+ const ww = toPaddedHexString(Math.floor((255 * state.val) / 100), 2);
526
+ const idAlive = `${deviceDesc}.Color`;
527
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
528
+ if (!_state) {
529
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
530
+ return;
531
+ }
532
+ const color = _state.val?.toString().substring(1) || '000000';
533
+ // replace ww component
534
+ const out = color.substring(0, wwindex) + ww;
535
+ this.setColor(channelId, out);
536
+ return;
537
+ }
538
+ if (stateId?.match(/CW\d?/)) {
539
+ // set ww component
540
+ const cw = toPaddedHexString(Math.floor((255 * state.val) / 100), 2);
541
+ const idAlive = `${deviceDesc}.Color`;
542
+ const _state = await this.adapter.getForeignStateAsync(idAlive);
543
+ if (!_state) {
544
+ await this.adapter.setStateAsync(idAlive, '#000000', false);
545
+ return;
546
+ }
547
+ const color = _state.val?.toString().substring(1) || '000000';
548
+ // replace cw component
549
+ const out = color.substring(0, 6) + cw + color.substring(8, 10);
550
+ this.setColor(channelId, out);
551
+ }
552
+ }
553
+ }
554
+ async onStateChange(id, state) {
555
+ this.adapter.log.debug(`onStateChange ${id}: ${JSON.stringify(state)}`);
556
+ if (state && !state.ack) {
557
+ // find client.id
558
+ const parts = id.split('.');
559
+ const stateId = parts.pop() || '';
560
+ const channelId = parts.splice(2, parts.length).join('.');
561
+ // Check if this is a Zigbee device state change
562
+ // Pattern: ZbReceived_DEVICEID_ATTRIBUTE (e.g., ZbReceived_0x0856_Power)
563
+ const zbMatch = stateId.match(/^ZbReceived_([^_]+)_(Power|Dimmer)$/);
564
+ if (zbMatch && this.clients[this.mappingClients[channelId]]) {
565
+ const deviceId = zbMatch[1];
566
+ const attribute = zbMatch[2];
567
+ let zbValue;
568
+ // Convert values for Zigbee commands
569
+ if (attribute === 'Power') {
570
+ zbValue = state.val ? '1' : '0';
571
+ }
572
+ else if (attribute === 'Dimmer') {
573
+ zbValue = state.val;
574
+ }
575
+ this.adapter.log.debug(`Sending ZbSend command for device ${deviceId}: ${attribute}=${zbValue}`);
576
+ this.setZbSendCommand(this.mappingClients[channelId], deviceId, attribute, zbValue);
577
+ return;
578
+ }
579
+ if (this.clients[this.mappingClients[channelId]]) {
580
+ // check for special led-stripe vars
581
+ if (!this.specVars.includes(stateId)) {
582
+ // other objects
583
+ const obj = await this.adapter.getObjectAsync(id);
584
+ if (!obj) {
585
+ this.adapter.log.warn(`Invalid object ${id}`);
586
+ }
587
+ else {
588
+ const type = obj.common.type;
589
+ switch (type) {
590
+ case 'boolean':
591
+ this.setStateImmediate(this.mappingClients[channelId], stateId, state.val ? 'ON' : 'OFF');
592
+ break;
593
+ case 'number':
594
+ this.setStateImmediate(this.mappingClients[channelId], stateId, state.val === null ? '' : state.val.toString());
595
+ break;
596
+ case 'string':
597
+ this.setStateImmediate(this.mappingClients[channelId], stateId, state.val === null ? '' : state.val.toString());
598
+ break;
599
+ }
600
+ }
601
+ }
602
+ else if (state.val !== null) {
603
+ await this.onStateChangedColors(id, state, channelId, stateId);
604
+ }
605
+ }
606
+ else {
607
+ if (!this.config.ignoreNotConnectedWarnings) {
608
+ this.adapter.log.info(`Client "${channelId}" not connected`);
609
+ }
610
+ }
611
+ }
612
+ }
613
+ async processTasks(callback) {
614
+ if (callback) {
615
+ this.taskCallbacks.push(callback);
616
+ }
617
+ if (!this.tasks?.length) {
618
+ const doCallbacks = this.taskCallbacks;
619
+ this.taskCallbacks = [];
620
+ doCallbacks.forEach(cb => typeof cb === 'function' && cb());
621
+ return;
622
+ }
623
+ const task = this.tasks[0];
624
+ this.adapter.log.debug(`process task: ${JSON.stringify(task)}`);
625
+ if (!this.cacheAddedObjects[task.id]) {
626
+ this.cacheAddedObjects[task.id] = true;
627
+ const obj = await this.adapter.getForeignObjectAsync(task.id);
628
+ if (!obj?.common) {
629
+ try {
630
+ await this.adapter.setForeignObjectAsync(task.id, task.data);
631
+ this.adapter.log.info(`New object created: ${task.id}`);
632
+ }
633
+ catch (err) {
634
+ this.adapter.log.warn(`New object creation error: ${err.message}`);
635
+ }
636
+ }
637
+ else if (obj.common.type !== task.data.common.type ||
638
+ task.storeMap !== undefined) {
639
+ obj.common.type = task.data.common.type;
640
+ try {
641
+ await this.adapter.setForeignObjectAsync(task.id, obj);
642
+ this.adapter.log.info(`Object updated: ${task.id}`);
643
+ }
644
+ catch (err) {
645
+ this.adapter.log.warn(`Object update error: ${err.message}`);
646
+ }
647
+ }
648
+ }
649
+ if (task.setState) {
650
+ await this._setState(task.id, task.setValue);
651
+ }
652
+ this.tasks.shift();
653
+ setImmediate(() => this.processTasks());
654
+ }
655
+ createClient(client, callback) {
656
+ // mqtt.0.cmnd.sonoff.POWER
657
+ // mqtt.0.stat.sonoff.POWER
658
+ const isStart = !this.tasks.length;
659
+ const id = `${this.adapter.namespace}.${client.iobId}`;
660
+ const obj = {
661
+ _id: id,
662
+ common: {
663
+ name: client.id,
664
+ desc: '',
665
+ },
666
+ native: {
667
+ clientId: client.id,
668
+ },
669
+ type: 'channel',
670
+ };
671
+ this.tasks.push({ id: obj._id, data: obj });
672
+ const stateObj = {
673
+ _id: `${id}.alive`,
674
+ common: {
675
+ type: 'boolean',
676
+ role: 'indicator.reachable',
677
+ read: true,
678
+ write: false,
679
+ name: `${client.id} alive`,
680
+ },
681
+ native: {},
682
+ type: 'state',
683
+ };
684
+ this.tasks.push({ id: stateObj._id, data: stateObj });
685
+ if (isStart) {
686
+ this.processTasks(callback).catch(err => this.adapter.log.error(err));
687
+ }
688
+ else {
689
+ typeof callback === 'function' && callback();
690
+ }
691
+ }
692
+ async updateClients() {
693
+ const clientIds = [];
694
+ const clientNames = [];
695
+ if (this.clients) {
696
+ for (const id in this.clients) {
697
+ const oid = `info.clients.${id.replace(/[.\s]+/g, '_').replace(exports.FORBIDDEN_CHARS, '_')}`;
698
+ clientIds.push(oid);
699
+ clientNames.push(id);
700
+ const clientObj = await this.adapter.getObjectAsync(oid);
701
+ if (!clientObj?.native) {
702
+ await this.adapter.setObjectAsync(oid, {
703
+ type: 'state',
704
+ common: {
705
+ name: id,
706
+ role: 'indicator.reachable',
707
+ type: 'boolean',
708
+ read: true,
709
+ write: false,
710
+ },
711
+ native: {
712
+ ip: this.clients[id].stream.remoteAddress,
713
+ port: this.clients[id].stream.remotePort,
714
+ },
715
+ });
716
+ }
717
+ else {
718
+ if (this.clients[id] &&
719
+ (clientObj.native.port !== this.clients[id].stream.remotePort ||
720
+ clientObj.native.ip !== this.clients[id].stream.remoteAddress)) {
721
+ clientObj.native.port = this.clients[id].stream.remotePort;
722
+ clientObj.native.ip = this.clients[id].stream.remoteAddress;
723
+ await this.adapter.setObjectAsync(clientObj._id, clientObj);
724
+ }
725
+ }
726
+ await this.adapter.setStateAsync(oid, true, true);
727
+ }
728
+ }
729
+ // read all other states and set alive to false
730
+ const allStates = await this.adapter.getStatesAsync('info.clients.*');
731
+ for (const id in allStates) {
732
+ if (!clientIds.includes(id.replace(`${this.adapter.namespace}.`, ''))) {
733
+ await this.adapter.setStateAsync(id, { val: false, ack: true });
734
+ }
735
+ }
736
+ await this.updateConnectionState(clientNames);
737
+ }
738
+ /**
739
+ * Update info.connection: the server writes the list of the connected clients there,
740
+ * the bridge the URL of the external broker
741
+ */
742
+ async updateConnectionState(clientNames) {
743
+ await this.adapter.setStateAsync('info.connection', clientNames.join(','), true);
744
+ }
745
+ async updateAlive(client, alive) {
746
+ const idAlive = `${this.adapter.namespace}.${client.iobId}.alive`;
747
+ const state = await this.adapter.getForeignStateAsync(idAlive);
748
+ if (!state || state.val !== alive) {
749
+ await this.adapter.setForeignStateAsync(idAlive, alive, true);
750
+ }
751
+ }
752
+ addObject(typeKey, client, prefix, path) {
753
+ // Extract the actual attribute name for the state ID construction.
754
+ // The path is added to the ID anyway, so it must be removed from the key of the
755
+ // data point, but only if the key really starts with it: "VEML6075_UvIndex" in the
756
+ // path "VEML6075" is "UvIndex", but "Total_in" in the path "SML" stays "Total_in"
757
+ let attr = typeKey;
758
+ const pathPrefix = path.length ? `${path.join('_')}_` : '';
759
+ if (pathPrefix && typeKey.startsWith(pathPrefix)) {
760
+ attr = typeKey.substring(pathPrefix.length);
761
+ }
762
+ const replaceAttr = datapoints_1.default[typeKey].replace || attr;
763
+ const id = `${this.adapter.namespace}.${client.iobId}.${prefix ? `${prefix}.` : ''}${path.length ? `${path.join('_')}_` : ''}${replaceAttr.replace(exports.FORBIDDEN_CHARS, '_')}`;
764
+ return {
765
+ id,
766
+ data: {
767
+ _id: id,
768
+ common: {
769
+ name: `${client.id} ${prefix ? `${prefix} ` : ''}${path.length ? `${path.join(' ')} ` : ''} ${replaceAttr}`,
770
+ ...datapoints_1.default[typeKey],
771
+ },
772
+ native: {},
773
+ type: 'state',
774
+ },
775
+ };
776
+ }
777
+ async syncPowerState(powerId, val) {
778
+ if (!this.cachePowerObjects[powerId]) {
779
+ const exists = await this.adapter.getForeignObjectAsync(powerId);
780
+ if (exists) {
781
+ this.cachePowerObjects[powerId] = true;
782
+ }
783
+ }
784
+ if (this.cachePowerObjects[powerId]) {
785
+ const state = await this.adapter.getForeignStateAsync(powerId);
786
+ if (!state || state.val != val) {
787
+ this.adapter.log.debug(`Sync .STATE.POWER to .POWER: ${powerId}`);
788
+ this.adapter.setForeignState(powerId, val, true);
789
+ }
790
+ }
791
+ }
792
+ checkData(client, topic, prefix, data, unit, path) {
793
+ if (!data || typeof data !== 'object') {
794
+ return;
795
+ }
796
+ const ledModeReadColorsID = `${this.adapter.namespace}.${client.iobId}.modeReadColors`;
797
+ if (this.cachedReadColors[ledModeReadColorsID] === undefined) {
798
+ void this.adapter.getForeignState(ledModeReadColorsID, (err, state) => {
799
+ this.cachedReadColors[ledModeReadColorsID] = !!state?.val;
800
+ setImmediate(() => this.checkData(client, topic, prefix, data, unit, path));
801
+ });
802
+ return;
803
+ }
804
+ path ||= [];
805
+ prefix ||= '';
806
+ // Extract pressure and temperature units if available at this level for nested data
807
+ const pressureUnit = data.PressureUnit || (unit && typeof unit === 'object' ? unit.pressureUnit : undefined);
808
+ const tempUnit = data.TempUnit || (unit && typeof unit === 'object' ? unit.tempUnit : undefined);
809
+ for (const attr in data) {
810
+ if (!Object.prototype.hasOwnProperty.call(data, attr)) {
811
+ this.adapter.log.warn(`[${client.id}] attr error: ${attr}${data[attr]}`);
812
+ continue;
813
+ }
814
+ // Skip unit fields - they are used for dynamic unit override, not as states
815
+ if (attr === 'TempUnit' || attr === 'PressureUnit') {
816
+ continue;
817
+ }
818
+ if (typeof data[attr] === 'object') {
819
+ // check for arrays
820
+ if (datapoints_1.default[attr]) {
821
+ if (datapoints_1.default[attr].type === 'array') {
822
+ // transform to an array of attributes
823
+ for (let i = 1; i <= 10; i++) {
824
+ const val = data[attr][i - 1];
825
+ if (typeof val === 'undefined') {
826
+ break;
827
+ }
828
+ // define a new object
829
+ const replaceAttr = attr.replace(exports.FORBIDDEN_CHARS, '_') + i.toString();
830
+ const id = `${this.adapter.namespace}.${client.iobId}.${prefix ? `${prefix}.` : ''}${path.length ? `${path.join('_')}_` : ''}${replaceAttr}`;
831
+ const obj = {
832
+ id,
833
+ data: {
834
+ _id: id,
835
+ common: {
836
+ name: `${client.id} ${prefix ? `${prefix} ` : ''}${path.length ? `${path.join(' ')} ` : ''} ${replaceAttr}`,
837
+ ...datapoints_1.default[attr],
838
+ type: typeof val,
839
+ },
840
+ native: {},
841
+ type: 'state',
842
+ },
843
+ };
844
+ if (!['number', 'string', 'boolean'].includes(obj.data.common.type)) {
845
+ obj.data.common.type = 'string';
846
+ }
847
+ this.updateState(obj, val);
848
+ }
849
+ }
850
+ else {
851
+ let nPath = [...path];
852
+ nPath.push(attr.replace(exports.FORBIDDEN_CHARS, '_'));
853
+ const nestedUnits = pressureUnit || tempUnit ? { pressureUnit, tempUnit } : unit;
854
+ this.checkData(client, topic, prefix, data[attr], nestedUnits, nPath);
855
+ nPath = undefined;
856
+ }
857
+ }
858
+ else {
859
+ let nPath = [...path];
860
+ nPath.push(attr.replace(exports.FORBIDDEN_CHARS, '_'));
861
+ const nestedUnits = pressureUnit || tempUnit ? { pressureUnit, tempUnit } : unit;
862
+ if (this.config.OBJ_TREE) {
863
+ this.checkData(client, topic, `${prefix}.${nPath.join('.')}`, data[attr], nestedUnits);
864
+ }
865
+ else {
866
+ this.checkData(client, topic, prefix, data[attr], nestedUnits, nPath);
867
+ }
868
+ nPath = undefined;
869
+ }
870
+ }
871
+ else if (datapoints_1.default[attr] || (path.length > 0 && datapoints_1.default[`${path.join('_')}_${attr}`])) {
872
+ let allowReadColors;
873
+ // Check for path-based type definition first, then fallback to simple attr type
874
+ const typeKey = path.length > 0 && datapoints_1.default[`${path.join('_')}_${attr}`] ? `${path.join('_')}_${attr}` : attr;
875
+ // create object
876
+ const obj = this.addObject(typeKey, client, prefix, path);
877
+ const replaceAttr = datapoints_1.default[typeKey].replace || attr;
878
+ if (datapoints_1.default[typeKey].storeMap) {
879
+ client._map[replaceAttr] = topic.replace(/^\w+\//, 'cmnd/').replace(/\/\w+$/, `/${replaceAttr}`);
880
+ }
881
+ // handle dynamic pressure unit override
882
+ if (attr === 'Pressure' && pressureUnit && typeof pressureUnit === 'string') {
883
+ obj.data.common.unit = pressureUnit;
884
+ }
885
+ // handle dynamic temperature unit override
886
+ if (attr === 'Temperature' && tempUnit && typeof tempUnit === 'string') {
887
+ obj.data.common.unit = tempUnit;
888
+ }
889
+ // adaptions for magichome tasmota
890
+ if (attr === 'Color') {
891
+ // read vars
892
+ allowReadColors = this.cachedReadColors[ledModeReadColorsID]; // allow for color read from MQTT (default off)
893
+ // if ledFlags bit 2, read color from tasmota, else ignore
894
+ if (data[attr].length === 10) {
895
+ obj.data.common.role = 'level.color.rgbcwww'; // RGB + cold white + white???
896
+ }
897
+ else if (data[attr].length === 8) {
898
+ obj.data.common.role = 'level.color.rgbww'; // RGB + White
899
+ }
900
+ else {
901
+ obj.data.common.role = 'level.color.rgb';
902
+ }
903
+ if (hueCalc) {
904
+ // Create LEDs modes if required
905
+ let xObj = this.addObject('modeReadColors', client, prefix, path);
906
+ this.updateState(xObj);
907
+ xObj = this.addObject('modeLedExor', client, prefix, path);
908
+ this.updateState(xObj);
909
+ xObj = this.addObject('Hue', client, prefix, path);
910
+ this.updateState(xObj);
911
+ xObj = this.addObject('Saturation', client, prefix, path);
912
+ this.updateState(xObj);
913
+ xObj = this.addObject('Red', client, prefix, path);
914
+ xObj.data.common.read = allowReadColors;
915
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(0, 2), 16)) / 255 : undefined);
916
+ xObj = this.addObject('Green', client, prefix, path);
917
+ xObj.data.common.read = allowReadColors;
918
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(2, 4), 16)) / 255 : undefined);
919
+ xObj = this.addObject('Blue', client, prefix, path);
920
+ xObj.data.common.read = allowReadColors;
921
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(4, 6), 16)) / 255 : undefined);
922
+ xObj = this.addObject('RGB_POWER', client, prefix, path);
923
+ xObj.data.common.read = allowReadColors;
924
+ let val = parseInt(data[attr].substring(0, 6), 16);
925
+ this.updateState(xObj, allowReadColors ? val > 0 : undefined);
926
+ if (obj.data.common.role === 'level.color.rgbww') {
927
+ // rgbww
928
+ xObj = this.addObject('WW', client, prefix, path);
929
+ xObj.data.common.read = allowReadColors;
930
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(6, 8), 16)) / 255 : undefined);
931
+ xObj = this.addObject('WW_POWER', client, prefix, path);
932
+ xObj.data.common.read = allowReadColors;
933
+ this.updateState(xObj, allowReadColors ? val > 0 : undefined);
934
+ }
935
+ else if (obj.data.common.role === 'level.color.rgbcwww') {
936
+ // rgbcwww
937
+ xObj = this.addObject('CW', client, prefix, path);
938
+ xObj.data.common.read = allowReadColors;
939
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(6, 8), 16)) / 255 : undefined);
940
+ xObj = this.addObject('CW_POWER', client, prefix, path);
941
+ xObj.data.common.read = allowReadColors;
942
+ this.updateState(xObj, allowReadColors ? val > 0 : undefined);
943
+ xObj = this.addObject('WW', client, prefix, path);
944
+ xObj.data.common.read = allowReadColors;
945
+ this.updateState(xObj, allowReadColors ? (100 * parseInt(data[attr].substring(8, 10), 16)) / 255 : undefined);
946
+ xObj = this.addObject('WW_POWER', client, prefix, path);
947
+ xObj.data.common.read = allowReadColors;
948
+ val = parseInt(data[attr].substring(8, 10), 16);
949
+ this.updateState(xObj, allowReadColors ? val > 0 : undefined);
950
+ }
951
+ }
952
+ }
953
+ let val;
954
+ if (obj.data.common.type === 'number') {
955
+ val = parseFloat(data[attr]);
956
+ }
957
+ else if (obj.data.common.type === 'boolean') {
958
+ const _value = (data[attr] || '').toUpperCase();
959
+ val = _value === 'ON' || _value === 'TRUE' || _value === '1';
960
+ }
961
+ else {
962
+ if (attr === 'Color') {
963
+ // add # char
964
+ if (allowReadColors) {
965
+ val = `#${data[attr]}`;
966
+ }
967
+ }
968
+ else {
969
+ val = data[attr];
970
+ }
971
+ }
972
+ this.updateState(obj, val);
973
+ // Special handling for Zigbee devices: create writable control states for Power and Dimmer
974
+ if ((attr === 'Power' || attr === 'Dimmer') &&
975
+ prefix === 'SENSOR' &&
976
+ path.length >= 2 &&
977
+ path[0] === 'ZbReceived') {
978
+ // Extract device ID from path (e.g., ZbReceived_0x0856_Power -> 0x0856)
979
+ const deviceId = path[1];
980
+ // Create a writable control state for Zigbee devices
981
+ const controlObj = Object.assign({}, obj);
982
+ // Modify object properties for control state
983
+ if (attr === 'Power') {
984
+ // For Zigbee Power: boolean switch, not power consumption in watts
985
+ controlObj.data.common = {
986
+ type: 'boolean',
987
+ role: 'switch',
988
+ read: true,
989
+ write: true,
990
+ name: `${client.id} ${prefix ? `${prefix} ` : ''}${path.join(' ')} ${attr} Control`,
991
+ };
992
+ // Convert numeric power value (0/1) to boolean for control state
993
+ const controlVal = Boolean(val);
994
+ this.updateState(controlObj, controlVal);
995
+ }
996
+ else if (attr === 'Dimmer') {
997
+ // For Zigbee Dimmer: ensure it's writable
998
+ controlObj.data.common.write = true;
999
+ controlObj.data.common.name = `${client.id} ${prefix ? `${prefix} ` : ''}${path.join(' ')} ${attr} Control`;
1000
+ this.updateState(controlObj, val);
1001
+ }
1002
+ this.adapter.log.debug(`Created Zigbee control state for device ${deviceId}: ${controlObj.id}`);
1003
+ }
1004
+ // Sync .STATE.POWER -> .POWER
1005
+ if (obj.id.includes('.STATE.POWER')) {
1006
+ const powerId = obj.id.replace('.STATE.POWER', '.POWER');
1007
+ this.syncPowerState(powerId, val ?? null).catch(err => this.adapter.log.error(`Cannot sync power states: ${err}`));
1008
+ }
1009
+ }
1010
+ else {
1011
+ // not in list, auto insert
1012
+ // tele/sonoff/SENSOR tele/sonoff/STATE => read only
1013
+ // stat/sonoff/RESULT => read,write
1014
+ const parts = topic.split('/');
1015
+ // auto generate objects
1016
+ if ((parts[0] === 'tele' &&
1017
+ ((this.config.TELE_SENSOR && parts[2] === 'SENSOR') ||
1018
+ (this.config.STAT_RESULT && parts[2] === 'RESULT') ||
1019
+ (this.config.TELE_STATE && parts[2] === 'STATE') ||
1020
+ (this.config.TELE_MARGINS && parts[2] === 'MARGINS'))) ||
1021
+ (parts[0] === 'stat' && this.config.STAT_RESULT && parts[2] === 'RESULT')) {
1022
+ let val = data[attr];
1023
+ const replaceAttr = attr;
1024
+ const attributes = {
1025
+ role: 'value',
1026
+ read: true,
1027
+ write: false,
1028
+ type: typeof val,
1029
+ name: `${client.id} ${prefix ? `${prefix} ` : ''}${path.length ? `${path.join(' ')} ` : ''} ${replaceAttr}`,
1030
+ };
1031
+ if (parts[2] === 'RESULT') {
1032
+ // control
1033
+ attributes.write = true;
1034
+ attributes.role = 'level';
1035
+ }
1036
+ if (!['number', 'string', 'boolean'].includes(attributes.type)) {
1037
+ attributes.type = 'string';
1038
+ val = (val || '').toString();
1039
+ }
1040
+ const id = `${this.adapter.namespace}.${client.iobId}.${prefix ? `${prefix}.` : ''}${path.length ? `${path.join('_')}_` : ''}${replaceAttr}`;
1041
+ const obj = {
1042
+ id,
1043
+ data: {
1044
+ _id: id,
1045
+ common: attributes,
1046
+ native: {},
1047
+ type: 'state',
1048
+ },
1049
+ };
1050
+ obj.data.common.name =
1051
+ `${client.id} ${prefix ? `${prefix} ` : ''}${path.length ? `${path.join(' ')} ` : ''} ${replaceAttr}`;
1052
+ this.updateState(obj, val);
1053
+ // Sync .STATE.POWER -> .POWER
1054
+ if (obj.id.includes('.STATE.POWER')) {
1055
+ const powerId = obj.id.replace('.STATE.POWER', '.POWER');
1056
+ this.syncPowerState(powerId, val ?? null).catch(err => this.adapter.log.error(`Cannot sync power states: ${err}`));
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ async receivedTopic(packet, client) {
1063
+ if (!packet) {
1064
+ this.adapter.log.warn(`Empty packet received: ${JSON.stringify(packet)}`);
1065
+ return;
1066
+ }
1067
+ // Last will topic: "Online" is published by the device itself, "Offline" by the broker
1068
+ // if the device is gone. With the built-in broker the connection state is used instead,
1069
+ // but in bridge mode this is the only way to detect that a device disappeared.
1070
+ if (packet.topic.endsWith('/LWT')) {
1071
+ const lwt = packet.payload.toString('utf8').trim().toLowerCase();
1072
+ this.adapter.log.debug(`Client [${client.id}] received: ${packet.topic} = ${lwt}`);
1073
+ await this.updateAlive(client, lwt !== 'offline');
1074
+ return;
1075
+ }
1076
+ // update alive state
1077
+ await this.updateAlive(client, true);
1078
+ if (client._will?.topic && packet.topic === client._will.topic) {
1079
+ client._will.payload = packet.payload;
1080
+ return;
1081
+ }
1082
+ let val = packet.payload.toString('utf8');
1083
+ this.adapter.log.debug(`Client [${client.id}] received: ${packet.topic} = ${val}`);
1084
+ client._map ||= {};
1085
+ const parts = packet.topic.split('/');
1086
+ client._fallBackName ||= parts[1];
1087
+ let stateId = parts.pop() || '';
1088
+ // Handle OpenBeken topics with /get or /set suffix
1089
+ // e.g., devicename/led_enableAll/get -> stateId should be led_enableAll
1090
+ if ((stateId === 'get' || stateId === 'set') && parts.length > 0) {
1091
+ const possibleStateId = parts[parts.length - 1];
1092
+ if (possibleStateId && possibleStateId.startsWith('led_') && datapoints_1.default[possibleStateId]) {
1093
+ stateId = parts.pop() || ''; // Use led_enableAll instead of get/set
1094
+ }
1095
+ }
1096
+ if (val.includes('nan')) {
1097
+ val = val.replace(/:nan,/g, ':"NaN",').replace(/:nan}/g, ':"NaN"}').replace(/:nan]/g, ':"NaN"]');
1098
+ }
1099
+ if (stateId === 'RESULT') {
1100
+ // ignore: stat/Sonoff/RESULT = {"POWER":"ON"}
1101
+ // testserver.js reports error, so reject above cmd
1102
+ const str = val.replace(/\s+/g, '');
1103
+ if (str.startsWith('{"POWER":"ON"}')) {
1104
+ return;
1105
+ }
1106
+ if (str.startsWith('{"POWER":"OFF"}')) {
1107
+ return;
1108
+ }
1109
+ if (parts[0] === 'stat' || parts[0] === 'tele') {
1110
+ try {
1111
+ if (this.config.OBJ_TREE) {
1112
+ this.checkData(client, packet.topic, 'RESULT', JSON.parse(val));
1113
+ }
1114
+ else {
1115
+ this.checkData(client, packet.topic, NO_PREFIX, JSON.parse(val));
1116
+ }
1117
+ }
1118
+ catch (e) {
1119
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data "${stateId}": _${val}_ - ${e}`);
1120
+ }
1121
+ }
1122
+ return;
1123
+ }
1124
+ // tele/sonoff_4ch/STATE = {"Time":"2017-10-02T19:26:06", "Uptime":0, "Vcc":3.226, "POWER1":"OFF", "POWER2":"OFF", "POWER3":"OFF", "POWER4":"OFF", "Wifi":{"AP":1, "SSId":"AAA", "RSSI": 15}}
1125
+ // tele/sonoff/SENSOR = {"Time":"2017-10-05T17:43:19", "DS18x20":{"DS1":{"Type":"DS18B20", "Address":"28FF9A9876815022A", "Temperature":12.2}}, "TempUnit":"C"}
1126
+ // tele/sonoff5/SENSOR = {"Time":"2017-10-03T14:02:25", "AM2301-14":{"Temperature":21.6, "Humidity":54.7}, "TempUnit":"C"}
1127
+ // tele/sonoff/SENSOR = {"Time":"2018-02-23T17:36:59", "Analog0":298}
1128
+ if (parts[0] === 'tele' && stateId.match(/^(RESULT|STATE|SENSOR|WAKEUP)\d?$/)) {
1129
+ try {
1130
+ if (this.config.OBJ_TREE) {
1131
+ if (stateId.match(/^(STATE)\d?$/)) {
1132
+ this.checkData(client, packet.topic, 'STATE', JSON.parse(val));
1133
+ }
1134
+ else if (stateId.match(/^(SENSOR)\d?$/)) {
1135
+ this.checkData(client, packet.topic, 'SENSOR', JSON.parse(val));
1136
+ }
1137
+ else if (stateId.match(/^(WAKEUP)\d?$/)) {
1138
+ this.checkData(client, packet.topic, 'WAKEUP', JSON.parse(val));
1139
+ }
1140
+ else if (stateId.match(/^(RESULT)\d?$/)) {
1141
+ this.checkData(client, packet.topic, 'RESULT', JSON.parse(val));
1142
+ }
1143
+ }
1144
+ else {
1145
+ this.checkData(client, packet.topic, NO_PREFIX, JSON.parse(val));
1146
+ }
1147
+ }
1148
+ catch (e) {
1149
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data"${stateId}": _${val}_ - ${e}`);
1150
+ }
1151
+ }
1152
+ else if (parts[0] === 'tele' && stateId.match(/^INFO\d?$/)) {
1153
+ // tele/SonoffPOW/INFO1 = {"Module":"Sonoff Pow", "Version":"5.8.0", "FallbackTopic":"SonoffPOW", "GroupTopic":"sonoffs"}
1154
+ // tele/SonoffPOW/INFO2 = {"WebServerMode":"Admin", "Hostname":"Sonoffpow", "IPAddress":"192.168.2.182"}
1155
+ // tele/SonoffPOW/INFO3 = {"RestartReason":"Software/System restart"}
1156
+ try {
1157
+ this.checkData(client, packet.topic, 'INFO', JSON.parse(val));
1158
+ }
1159
+ catch (e) {
1160
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data"${stateId}": _${val}_ - ${e}`);
1161
+ }
1162
+ }
1163
+ else if (parts[0] === 'tele' && stateId.match(/^(ENERGY)\d?$/)) {
1164
+ // tele/sonoff_4ch/ENERGY = {"Time":"2017-10-02T19:24:32", "Total":1.753, "Yesterday":0.308, "Today":0.205, "Period":0, "Power":3, "Factor":0.12, "Voltage":221, "Current":0.097}
1165
+ try {
1166
+ this.checkData(client, packet.topic, 'ENERGY', JSON.parse(val));
1167
+ }
1168
+ catch (e) {
1169
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data"${stateId}": _${val}_ - ${e}`);
1170
+ }
1171
+ }
1172
+ else if (parts[0] === 'tele' && stateId.match(/^(MARGINS)\d?$/)) {
1173
+ // tele/sonoffPOW/MARGINS = {"Time":"2020-04-23T10:15:00","PowerLow":100,"PowerHigh":2000,"PowerDelta":50}
1174
+ try {
1175
+ this.checkData(client, packet.topic, 'MARGINS', JSON.parse(val));
1176
+ }
1177
+ catch (e) {
1178
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data"${stateId}": _${val}_ - ${e}`);
1179
+ }
1180
+ }
1181
+ else if (parts[0] === 'stat' && stateId === 'STATUS10') {
1182
+ // stat/device/STATUS10 = {"StatusSNS":{"Time":"...","Switch2":"ON","Switch3":"OFF",...}}
1183
+ try {
1184
+ const data = JSON.parse(val);
1185
+ if (data.StatusSNS && typeof data.StatusSNS === 'object') {
1186
+ this.checkData(client, packet.topic, NO_PREFIX, data.StatusSNS);
1187
+ }
1188
+ }
1189
+ catch (e) {
1190
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data "${stateId}": _${val}_ - ${e}`);
1191
+ }
1192
+ }
1193
+ else if (parts[0] === 'stat' && stateId === 'STATUS5') {
1194
+ // stat/device/STATUS5 = {"StatusNET":{"Hostname":"...","IPAddress":"...",...}}
1195
+ try {
1196
+ const data = JSON.parse(val);
1197
+ if (data.StatusNET && typeof data.StatusNET === 'object') {
1198
+ this.checkData(client, packet.topic, 'INFO', data.StatusNET);
1199
+ }
1200
+ }
1201
+ catch (e) {
1202
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data "${stateId}": _${val}_ - ${e}`);
1203
+ }
1204
+ }
1205
+ else if (parts[0] === 'stat' && stateId === 'STATUS2') {
1206
+ // stat/device/STATUS2 = {"StatusFWR":{"Version":"...","Hardware":"...",...}}
1207
+ try {
1208
+ const data = JSON.parse(val);
1209
+ if (data.StatusFWR && typeof data.StatusFWR === 'object') {
1210
+ this.checkData(client, packet.topic, 'INFO', data.StatusFWR);
1211
+ }
1212
+ }
1213
+ catch (e) {
1214
+ this.adapter.log.warn(`Client [${client.id}] cannot parse data "${stateId}": _${val}_ - ${e}`);
1215
+ }
1216
+ }
1217
+ else if (datapoints_1.default[stateId]) {
1218
+ // /ESP_BOX/BM280/Pressure = 1010.09
1219
+ // /ESP_BOX/BM280/Humidity = 42.39
1220
+ // /ESP_BOX/BM280/Temperature = 25.86
1221
+ // /ESP_BOX/BM280/Approx. Altitude = 24
1222
+ // cmnd/sonoff/POWER
1223
+ // stat/sonoff/POWER
1224
+ if (datapoints_1.default[stateId]) {
1225
+ const id = `${this.adapter.namespace}.${client.iobId}.${stateId.replace(/[-.+\s]+/g, '_').replace(exports.FORBIDDEN_CHARS, '_')}`;
1226
+ const obj = {
1227
+ id,
1228
+ data: {
1229
+ _id: id,
1230
+ common: {
1231
+ name: `${client.id} ${stateId}`,
1232
+ ...datapoints_1.default[stateId],
1233
+ },
1234
+ native: {},
1235
+ type: 'state',
1236
+ },
1237
+ };
1238
+ // push only new objects
1239
+ this.updateState(obj, async () => {
1240
+ if (obj.data.common.type === 'number') {
1241
+ await this.adapter.setStateAsync(id, parseFloat(val), true);
1242
+ }
1243
+ else if (obj.data.common.type === 'boolean') {
1244
+ if (val === 'ON' || val === '1' || val === 'true' || val === 'on') {
1245
+ await this.adapter.setStateAsync(id, true, true);
1246
+ }
1247
+ else if (val === 'OFF' || val === '0' || val === 'false' || val === 'off') {
1248
+ await this.adapter.setStateAsync(id, false, true);
1249
+ }
1250
+ }
1251
+ else {
1252
+ await this.adapter.setStateAsync(id, val, true);
1253
+ }
1254
+ // Store topic mapping for state changes
1255
+ let mappedTopic = packet.topic;
1256
+ if (mappedTopic.endsWith('/get')) {
1257
+ // OpenBeken: devicename/led_enableAll/get -> devicename/led_enableAll/set
1258
+ mappedTopic = mappedTopic.replace(/\/get$/, '/set');
1259
+ }
1260
+ else if (parts[0] === 'stat' || parts[0] === 'tele') {
1261
+ // Convert stat/tele topics to cmnd for outgoing commands.
1262
+ // Only the first and the last part are replaced, so nested full topics
1263
+ // like tele/house/floor/device/POWER stay intact
1264
+ mappedTopic = mappedTopic.replace(/^\w+\//, 'cmnd/').replace(/\/[^/]+$/, `/${stateId}`);
1265
+ }
1266
+ else if (stateId.startsWith('led_')) {
1267
+ // OpenBeken without suffix: devicename/led_enableAll -> devicename/led_enableAll/set
1268
+ mappedTopic = `${packet.topic}/set`;
1269
+ }
1270
+ client._map[stateId] = mappedTopic;
1271
+ });
1272
+ }
1273
+ else {
1274
+ this.adapter.log.debug(`Cannot process: ${packet.topic}`);
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1279
+ exports.default = MQTTBase;
1280
+ //# sourceMappingURL=mqttBase.js.map