iobroker.zigbee2mqtt 3.0.20 → 3.1.1
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/README.md +12 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +42 -55
- package/lib/check.js +19 -11
- package/lib/deviceController.js +167 -133
- package/lib/exposes.js +1350 -1315
- package/lib/imageController.js +49 -47
- package/lib/messages.js +14 -6
- package/lib/mqttServerController.js +113 -22
- package/lib/states.js +12 -5
- package/lib/statesController.js +68 -43
- package/lib/utils.js +1 -50
- package/lib/websocketController.js +151 -46
- package/lib/z2mController.js +38 -29
- package/main.js +398 -207
- package/package.json +13 -12
package/lib/deviceController.js
CHANGED
|
@@ -33,8 +33,17 @@ class DeviceController {
|
|
|
33
33
|
* @param devicesMessage
|
|
34
34
|
*/
|
|
35
35
|
async createDeviceDefinitions(devicesMessage) {
|
|
36
|
+
// Fix 1: devicesMessage kann null/kein Array sein (z.B. leerer bridge/devices payload)
|
|
37
|
+
if (!Array.isArray(devicesMessage)) {
|
|
38
|
+
this.adapter.log.warn('createDeviceDefinitions: payload ist kein Array');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
36
41
|
utils.clearArray(this.deviceCache);
|
|
37
42
|
for (const devicesMessag of devicesMessage) {
|
|
43
|
+
if (!devicesMessag || !devicesMessag.ieee_address) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
if (this.logCustomizations.debugDevices.includes(devicesMessag.ieee_address)) {
|
|
39
48
|
this.adapter.log.warn(
|
|
40
49
|
`--->>> fromZ2M -> ${devicesMessag.ieee_address} exposes: ${JSON.stringify(devicesMessag)}`
|
|
@@ -42,16 +51,17 @@ class DeviceController {
|
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
if (devicesMessag.definition != null) {
|
|
45
|
-
// if the device is already present in the cache, remove it
|
|
46
54
|
this.removeDeviceByIeee(this.deviceCache, devicesMessag.ieee_address);
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
// Fix 3: exposes muss auch ein Array sein
|
|
57
|
+
if (Array.isArray(devicesMessag.definition.exposes)) {
|
|
49
58
|
try {
|
|
50
59
|
const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
|
|
51
60
|
newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
|
|
52
61
|
this.deviceCache.push(newDevice);
|
|
53
62
|
} catch (err) {
|
|
54
|
-
|
|
63
|
+
// Fix 2: friendly_name im Fehlerlog verwenden
|
|
64
|
+
this.adapter.log.warn(`Cannot create Device from Exposes for ${devicesMessag.friendly_name || devicesMessag.ieee_address}!`);
|
|
55
65
|
this.adapter.log.debug(JSON.stringify(devicesMessag));
|
|
56
66
|
this.adapter.log.debug(err);
|
|
57
67
|
}
|
|
@@ -67,23 +77,22 @@ class DeviceController {
|
|
|
67
77
|
* @param scenes
|
|
68
78
|
*/
|
|
69
79
|
async defineGroupDevice(groupID, ieee_address, scenes) {
|
|
70
|
-
const brmPropName =
|
|
71
|
-
|
|
72
|
-
const brsPropName =
|
|
73
|
-
this.adapter.config.brightnessStepOnOff == true ? 'brightness_step_onoff' : 'brightness_step';
|
|
80
|
+
const brmPropName = this.adapter.config.brightnessMoveOnOff === true ? 'brightness_move_onoff' : 'brightness_move';
|
|
81
|
+
const brsPropName = this.adapter.config.brightnessStepOnOff === true ? 'brightness_step_onoff' : 'brightness_step';
|
|
74
82
|
const newDevice = {
|
|
75
83
|
id: groupID,
|
|
76
84
|
ieee_address: ieee_address,
|
|
77
85
|
icon: undefined, // await imageController.getDeviceIcon(devicesMessag), device.definition.model
|
|
78
86
|
optionsValues: {},
|
|
79
87
|
states: [
|
|
80
|
-
statesDefs
|
|
81
|
-
statesDefs.
|
|
82
|
-
statesDefs.
|
|
83
|
-
statesDefs.
|
|
88
|
+
// Fix 1: Klone aller statesDefs-Objekte – sonst werden geteilte Referenzen mutiert
|
|
89
|
+
Object.assign({}, statesDefs.available),
|
|
90
|
+
Object.assign({}, statesDefs.brightness),
|
|
91
|
+
Object.assign({}, statesDefs.colortemp_move),
|
|
92
|
+
Object.assign({}, statesDefs.transition),
|
|
84
93
|
{
|
|
85
94
|
id: 'state',
|
|
86
|
-
|
|
95
|
+
prop: 'state',
|
|
87
96
|
name: 'Switch state',
|
|
88
97
|
options: ['transition'],
|
|
89
98
|
icon: undefined,
|
|
@@ -133,29 +142,28 @@ class DeviceController {
|
|
|
133
142
|
type: 'string',
|
|
134
143
|
def: '#ff00ff',
|
|
135
144
|
setter: (value) => {
|
|
136
|
-
|
|
145
|
+
// Fix 6: redundante [0,0]-Initialisierung entfernt
|
|
137
146
|
const rgbcolor = colors.ParseColor(value);
|
|
138
|
-
xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
147
|
+
const xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
139
148
|
return {
|
|
140
149
|
x: xy[0],
|
|
141
150
|
y: xy[1],
|
|
142
151
|
};
|
|
143
152
|
},
|
|
144
153
|
getter: (payload) => {
|
|
145
|
-
if (payload.color_mode
|
|
154
|
+
if (payload.color_mode !== 'xy' && !this.config.colorTempSyncColor) {
|
|
146
155
|
return undefined;
|
|
147
156
|
}
|
|
148
|
-
|
|
157
|
+
// Fix 2: x=0 / y=0 sind gültige CIE-Koordinaten → != null statt truthy
|
|
158
|
+
if (payload.color && payload.color.x != null && payload.color.y != null) {
|
|
149
159
|
const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
|
|
150
160
|
return (
|
|
151
|
-
`#${
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}${utils.decimalToHex(colorval[2])}`
|
|
161
|
+
`#${utils.decimalToHex(colorval[0])
|
|
162
|
+
}${utils.decimalToHex(colorval[1])
|
|
163
|
+
}${utils.decimalToHex(colorval[2])}`
|
|
155
164
|
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
159
167
|
},
|
|
160
168
|
},
|
|
161
169
|
{
|
|
@@ -168,22 +176,21 @@ class DeviceController {
|
|
|
168
176
|
write: true,
|
|
169
177
|
read: true,
|
|
170
178
|
type: 'number',
|
|
171
|
-
min: this.config.useKelvin
|
|
172
|
-
max: this.config.useKelvin
|
|
173
|
-
def: this.config.useKelvin
|
|
174
|
-
unit: this.config.useKelvin
|
|
179
|
+
min: this.config.useKelvin === true ? utils.miredKelvinConversion(550) : 150,
|
|
180
|
+
max: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
|
|
181
|
+
def: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
|
|
182
|
+
unit: this.config.useKelvin === true ? 'K' : 'mired',
|
|
175
183
|
setter: (value) => {
|
|
176
184
|
return utils.toMired(value);
|
|
177
185
|
},
|
|
178
186
|
getter: (payload) => {
|
|
179
|
-
if (payload.color_mode
|
|
187
|
+
if (payload.color_mode !== 'color_temp') {
|
|
180
188
|
return undefined;
|
|
181
189
|
}
|
|
182
|
-
if (this.config.useKelvin
|
|
190
|
+
if (this.config.useKelvin === true) {
|
|
183
191
|
return utils.miredKelvinConversion(payload.color_temp);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
}
|
|
193
|
+
return payload.color_temp;
|
|
187
194
|
},
|
|
188
195
|
},
|
|
189
196
|
{
|
|
@@ -248,12 +255,21 @@ class DeviceController {
|
|
|
248
255
|
* @param groupsMessage
|
|
249
256
|
*/
|
|
250
257
|
async createGroupDefinitions(groupsMessage) {
|
|
258
|
+
// Fix 4: groupsMessage kann null/kein Array sein
|
|
259
|
+
if (!Array.isArray(groupsMessage)) {
|
|
260
|
+
this.adapter.log.warn('createGroupDefinitions: payload ist kein Array');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
251
263
|
utils.clearArray(this.groupCache);
|
|
252
264
|
for (const groupMessage of groupsMessage) {
|
|
253
|
-
|
|
265
|
+
// Fix 5: fehlende Pflichtfelder abfangen
|
|
266
|
+
if (!groupMessage || groupMessage.id == null || !groupMessage.friendly_name) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (this.logCustomizations.debugDevices.includes(String(groupMessage.id))) {
|
|
254
270
|
this.adapter.log.warn(`--->>> fromZ2M -> ${groupMessage.id} exposes: ${JSON.stringify(groupMessage)}`);
|
|
255
271
|
}
|
|
256
|
-
await this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes);
|
|
272
|
+
await this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes || []);
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
275
|
|
|
@@ -261,144 +277,158 @@ class DeviceController {
|
|
|
261
277
|
*
|
|
262
278
|
*/
|
|
263
279
|
async createOrUpdateDevices() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
280
|
+
// Gruppen und Geräte getrennt verarbeiten
|
|
281
|
+
for (const device of this.groupCache) {
|
|
282
|
+
await this._createOrUpdateSingleDevice(device, true);
|
|
283
|
+
}
|
|
284
|
+
for (const device of this.deviceCache) {
|
|
285
|
+
await this._createOrUpdateSingleDevice(device, false);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
267
288
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
/**
|
|
290
|
+
* @param device
|
|
291
|
+
* @param isGroup
|
|
292
|
+
*/
|
|
293
|
+
async _createOrUpdateSingleDevice(device, isGroup) {
|
|
294
|
+
let deviceName = await this.getDeviceName(device);
|
|
295
|
+
let description = await this.getDeviceDescription(device);
|
|
272
296
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
if (deviceName === '' && device.description) {
|
|
298
|
+
deviceName = device.description;
|
|
299
|
+
description = '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Disabled-Flag nur bei echten Geräten – Gruppen können nicht disabled sein
|
|
303
|
+
if (!isGroup && device.disabled === true) {
|
|
304
|
+
if (this.config.useEventInDesc === true) {
|
|
305
|
+
description = 'Device is disabled!';
|
|
306
|
+
} else {
|
|
307
|
+
deviceName = `[Disabled] ${deviceName}`;
|
|
280
308
|
}
|
|
309
|
+
}
|
|
281
310
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
common: {
|
|
286
|
-
icon: device.icon,
|
|
287
|
-
name: deviceName,
|
|
288
|
-
desc: description,
|
|
289
|
-
statusStates: { onlineId: '' },
|
|
290
|
-
},
|
|
291
|
-
native: {
|
|
292
|
-
deviceRemoved: false,
|
|
293
|
-
groupDevice: false,
|
|
294
|
-
},
|
|
295
|
-
};
|
|
311
|
+
if (!this.createCache[device.ieee_address] ||
|
|
312
|
+
this.createCache[device.ieee_address].name !== deviceName ||
|
|
313
|
+
this.createCache[device.ieee_address].description !== description) {
|
|
296
314
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
|
|
311
|
-
this.createCache[device.ieee_address] = { name: deviceName, description: description };
|
|
312
|
-
}
|
|
315
|
+
const deviceObj = {
|
|
316
|
+
type: 'device',
|
|
317
|
+
common: {
|
|
318
|
+
icon: device.icon,
|
|
319
|
+
name: deviceName,
|
|
320
|
+
desc: description,
|
|
321
|
+
statusStates: { onlineId: '' },
|
|
322
|
+
},
|
|
323
|
+
native: {
|
|
324
|
+
deviceRemoved: false,
|
|
325
|
+
groupDevice: isGroup,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
313
328
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const sceneStates = await this.adapter.getStatesAsync(`${device.ieee_address}.scene_*`);
|
|
317
|
-
const sceneIDs = Object.keys(sceneStates);
|
|
318
|
-
for (const sceneID of sceneIDs) {
|
|
319
|
-
const stateID = sceneID.split('.')[3];
|
|
320
|
-
if (device.states.find((x) => x.id == stateID) == null) {
|
|
321
|
-
this.adapter.delObject(sceneID);
|
|
322
|
-
}
|
|
329
|
+
if (isGroup || device.disabled !== true) {
|
|
330
|
+
deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
|
|
323
331
|
}
|
|
324
332
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const iobState = {
|
|
329
|
-
type: 'state',
|
|
330
|
-
common: await this.copyAndCleanStateObj(state),
|
|
331
|
-
native: {},
|
|
332
|
-
};
|
|
333
|
+
await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
|
|
334
|
+
this.createCache[device.ieee_address] = { name: deviceName, description: description };
|
|
335
|
+
}
|
|
333
336
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
if (!Array.isArray(device.states)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Veraltete scene_*-States bereinigen – getForeignObjectsAsync mit vollem Namespace-Pfad
|
|
342
|
+
const sceneObjects = await this.adapter.getForeignObjectsAsync(
|
|
343
|
+
`${this.adapter.namespace}.${device.ieee_address}.scene_*`
|
|
344
|
+
);
|
|
345
|
+
if (sceneObjects) {
|
|
346
|
+
for (const sceneObjId of Object.keys(sceneObjects)) {
|
|
347
|
+
const parts = sceneObjId.split('.');
|
|
348
|
+
const stateID = parts[parts.length - 1];
|
|
349
|
+
if (!device.states.find((x) => x.id === stateID)) {
|
|
350
|
+
await this.adapter.delForeignObjectAsync(sceneObjId);
|
|
337
351
|
}
|
|
338
352
|
}
|
|
339
353
|
}
|
|
340
|
-
}
|
|
341
354
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
355
|
+
for (const state of device.states) {
|
|
356
|
+
if (state && (!this.createCache[device.ieee_address][state.id] ||
|
|
357
|
+
this.createCache[device.ieee_address][state.id].name !== state.name)) {
|
|
358
|
+
|
|
359
|
+
const iobState = {
|
|
360
|
+
type: 'state',
|
|
361
|
+
common: await this.copyAndCleanStateObj(state),
|
|
362
|
+
native: {},
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
|
|
366
|
+
this.createCache[device.ieee_address][state.id] = { name: state.name, created: true };
|
|
367
|
+
}
|
|
352
368
|
}
|
|
353
369
|
}
|
|
354
370
|
|
|
355
371
|
/**
|
|
356
|
-
*
|
|
372
|
+
* Nur echte Geräte prüfen – Gruppen werden hier nie verarbeitet
|
|
357
373
|
*/
|
|
358
374
|
async checkAndProgressDeviceRemove() {
|
|
359
|
-
let description = '';
|
|
360
|
-
let deviceName = '';
|
|
361
375
|
let iobDevices = await this.adapter.getDevicesAsync();
|
|
362
|
-
//
|
|
363
|
-
iobDevices = iobDevices.filter((x) => x.native.deviceRemoved
|
|
364
|
-
|
|
365
|
-
iobDevices = iobDevices.filter((x) => x.native.groupDevice == false);
|
|
376
|
+
// Nur nicht-entfernte, echte Geräte (keine Gruppen)
|
|
377
|
+
iobDevices = iobDevices.filter((x) => x.native && x.native.deviceRemoved === false);
|
|
378
|
+
iobDevices = iobDevices.filter((x) => x.native && x.native.groupDevice === false);
|
|
366
379
|
|
|
367
380
|
for (const iobDevice of iobDevices) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
deviceName = iobDevice.common.name;
|
|
381
|
+
const idParts = iobDevice._id.split('.');
|
|
382
|
+
if (idParts.length < 3) {continue;}
|
|
383
|
+
const ieee_address = idParts[2];
|
|
372
384
|
|
|
373
|
-
|
|
385
|
+
// Gruppen-Einträge im ioBroker-Baum explizit überspringen
|
|
386
|
+
if (ieee_address.startsWith('group_')) {continue;}
|
|
387
|
+
|
|
388
|
+
if (!this.deviceCache.find((x) => x.ieee_address === ieee_address)) {
|
|
389
|
+
let deviceName = iobDevice.common && iobDevice.common.name ? iobDevice.common.name : ieee_address;
|
|
390
|
+
let description = '';
|
|
391
|
+
|
|
392
|
+
if (this.config.useEventInDesc === true) {
|
|
374
393
|
description = 'Device was removed!';
|
|
375
394
|
} else {
|
|
376
395
|
deviceName = `[Removed] ${deviceName}`;
|
|
377
396
|
}
|
|
378
397
|
|
|
379
|
-
this.adapter.
|
|
380
|
-
common: {
|
|
381
|
-
|
|
382
|
-
desc: description,
|
|
383
|
-
},
|
|
384
|
-
native: {
|
|
385
|
-
deviceRemoved: true,
|
|
386
|
-
},
|
|
398
|
+
await this.adapter.extendObjectAsync(ieee_address, {
|
|
399
|
+
common: { name: deviceName, desc: description },
|
|
400
|
+
native: { deviceRemoved: true },
|
|
387
401
|
});
|
|
388
|
-
this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
|
|
402
|
+
await this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
|
|
389
403
|
|
|
390
404
|
delete this.createCache[ieee_address];
|
|
391
405
|
}
|
|
392
406
|
}
|
|
393
407
|
}
|
|
394
408
|
|
|
409
|
+
/**
|
|
410
|
+
*
|
|
411
|
+
* @param messageObj
|
|
412
|
+
*/
|
|
413
|
+
async renameDeviceInCache(messageObj) {
|
|
414
|
+
if (!messageObj.payload || !messageObj.payload.data) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const renamedDevice = this.groupCache
|
|
418
|
+
.concat(this.deviceCache)
|
|
419
|
+
.find((x) => x.id === messageObj.payload.data.from);
|
|
420
|
+
if (renamedDevice) {
|
|
421
|
+
renamedDevice.id = messageObj.payload.data.to;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
395
425
|
/**
|
|
396
426
|
*
|
|
397
427
|
* @param devices
|
|
398
428
|
* @param ieee_address
|
|
399
429
|
*/
|
|
400
430
|
removeDeviceByIeee(devices, ieee_address) {
|
|
401
|
-
const idx = devices.findIndex((x) => x.ieee_address
|
|
431
|
+
const idx = devices.findIndex((x) => x.ieee_address === ieee_address);
|
|
402
432
|
if (idx > -1) {
|
|
403
433
|
devices.splice(idx, 1);
|
|
404
434
|
}
|
|
@@ -434,7 +464,7 @@ class DeviceController {
|
|
|
434
464
|
* @param device
|
|
435
465
|
*/
|
|
436
466
|
getDeviceName(device) {
|
|
437
|
-
return device.id
|
|
467
|
+
return device.id === device.ieee_address ? '' : device.id;
|
|
438
468
|
}
|
|
439
469
|
|
|
440
470
|
/**
|
|
@@ -456,7 +486,11 @@ class DeviceController {
|
|
|
456
486
|
this.adapter.setState('info.missing_routers_count', missingRoutersCount, true);
|
|
457
487
|
|
|
458
488
|
if (missingRoutersCount > 0) {
|
|
459
|
-
this.
|
|
489
|
+
const logLvl = this.config.coordinatorCheckLogLvl;
|
|
490
|
+
const logFn = (logLvl && typeof this.adapter.log[logLvl] === 'function')
|
|
491
|
+
? this.adapter.log[logLvl].bind(this.adapter.log)
|
|
492
|
+
: this.adapter.log.warn.bind(this.adapter.log);
|
|
493
|
+
logFn(
|
|
460
494
|
`Coordinator check: ${missingRoutersCount} missing routers were found, please check the data point 'zigbee2mqtt.x.info.missing_routers'!`
|
|
461
495
|
);
|
|
462
496
|
} else {
|