iobroker.sonoff 3.2.1 → 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.
- package/LICENSE +1 -1
- package/README.md +95 -149
- package/admin/i18n/de.json +89 -0
- package/admin/i18n/en.json +89 -0
- package/admin/i18n/es.json +89 -0
- package/admin/i18n/fr.json +89 -0
- package/admin/i18n/it.json +89 -0
- package/admin/i18n/nl.json +89 -0
- package/admin/i18n/pl.json +89 -0
- package/admin/i18n/pt.json +89 -0
- package/admin/i18n/ru.json +89 -0
- package/admin/i18n/uk.json +89 -0
- package/admin/i18n/zh-cn.json +89 -0
- package/admin/jsonConfig.json +394 -125
- package/build/lib/bridge.js +512 -0
- package/build/lib/bridge.js.map +1 -0
- package/build/lib/checkStates.js +68 -0
- package/build/lib/checkStates.js.map +1 -0
- package/build/lib/datapoints.js +430 -0
- package/build/lib/datapoints.js.map +1 -0
- package/build/lib/mqttBase.js +1280 -0
- package/build/lib/mqttBase.js.map +1 -0
- package/build/lib/server.js +485 -0
- package/build/lib/server.js.map +1 -0
- package/build/main.js +103 -0
- package/build/main.js.map +1 -0
- package/io-package.json +64 -43
- package/package.json +23 -19
- package/admin/i18n/de/translations.json +0 -54
- package/admin/i18n/en/translations.json +0 -54
- package/admin/i18n/es/translations.json +0 -54
- package/admin/i18n/fr/translations.json +0 -54
- package/admin/i18n/it/translations.json +0 -54
- package/admin/i18n/nl/translations.json +0 -54
- package/admin/i18n/pl/translations.json +0 -54
- package/admin/i18n/pt/translations.json +0 -54
- package/admin/i18n/ru/translations.json +0 -54
- package/admin/i18n/uk/translations.json +0 -54
- package/admin/i18n/zh-cn/translations.json +0 -54
- package/admin/index.html +0 -177
- package/admin/index_m.html +0 -218
- package/admin/words.js +0 -63
- package/lib/datapoints.js +0 -216
- package/lib/server.js +0 -1810
- package/main.js +0 -99
|
@@ -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
|