iobroker.zigbee2mqtt 3.0.21 → 3.1.5
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 +23 -0
- package/admin/jsonConfig.json +37 -0
- package/io-package.json +68 -68
- package/lib/check.js +24 -14
- package/lib/colors.js +9 -9
- package/lib/deviceController.js +244 -157
- package/lib/exposes.js +1362 -1315
- package/lib/imageController.js +98 -66
- package/lib/messages.js +29 -19
- package/lib/mqttServerController.js +120 -23
- package/lib/nonGenericDevicesExtension.js +2 -3
- package/lib/rgb.js +52 -63
- package/lib/states.js +150 -33
- package/lib/statesController.js +152 -72
- package/lib/utils.js +37 -97
- package/lib/websocketController.js +187 -50
- package/lib/z2mController.js +63 -42
- package/main.js +467 -201
- package/package.json +13 -12
package/lib/deviceController.js
CHANGED
|
@@ -6,17 +6,32 @@ const rgb = require('./rgb.js');
|
|
|
6
6
|
const ImageController = require('./imageController').ImageController;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
+
* Prüft ob eine ieee_address in der kommaseparierten debugDevices-Liste enthalten ist.
|
|
10
|
+
* String.includes() würde Substring-Matches liefern (z.B. "0x12" trifft "0x1234").
|
|
9
11
|
*
|
|
12
|
+
* @param {string} debugDevices
|
|
13
|
+
* @param {string} address
|
|
14
|
+
* @returns {boolean}
|
|
15
|
+
*/
|
|
16
|
+
function isDebugDevice(debugDevices, address) {
|
|
17
|
+
if (!debugDevices || !address) { return false; }
|
|
18
|
+
return String(debugDevices).split(',').map((s) => s.trim()).includes(address);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Verwaltet das Erstellen und Aktualisieren von Geräte- und Gruppenobjekten in ioBroker
|
|
23
|
+
* basierend auf den Zigbee2MQTT-Exposes.
|
|
10
24
|
*/
|
|
11
25
|
class DeviceController {
|
|
12
26
|
/**
|
|
27
|
+
* Erstellt eine neue DeviceController-Instanz.
|
|
13
28
|
*
|
|
14
|
-
* @param adapter
|
|
15
|
-
* @param deviceCache
|
|
16
|
-
* @param groupCache
|
|
17
|
-
* @param config
|
|
18
|
-
* @param logCustomizations
|
|
19
|
-
* @param createCache
|
|
29
|
+
* @param {object} adapter Die ioBroker-Adapter-Instanz
|
|
30
|
+
* @param {Array} deviceCache Gemeinsamer Cache aller bekannten Geräte
|
|
31
|
+
* @param {Array} groupCache Gemeinsamer Cache aller bekannten Gruppen
|
|
32
|
+
* @param {object} config Adapter-Konfiguration
|
|
33
|
+
* @param {object} logCustomizations Debug/Filter-Einstellungen (debugDevices, logfilter)
|
|
34
|
+
* @param {object} createCache Cache bereits erstellter ioBroker-Objekte
|
|
20
35
|
*/
|
|
21
36
|
constructor(adapter, deviceCache, groupCache, config, logCustomizations, createCache) {
|
|
22
37
|
this.adapter = adapter;
|
|
@@ -29,29 +44,41 @@ class DeviceController {
|
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
/**
|
|
47
|
+
* Erstellt Geräte-Definitionen aus dem bridge/devices-Payload von Zigbee2MQTT
|
|
48
|
+
* und befüllt den deviceCache neu.
|
|
32
49
|
*
|
|
33
|
-
* @param devicesMessage
|
|
50
|
+
* @param {Array} devicesMessage Array mit Gerätedaten aus dem bridge/devices-Topic
|
|
34
51
|
*/
|
|
35
52
|
async createDeviceDefinitions(devicesMessage) {
|
|
53
|
+
// Fix 1: devicesMessage kann null/kein Array sein (z.B. leerer bridge/devices payload)
|
|
54
|
+
if (!Array.isArray(devicesMessage)) {
|
|
55
|
+
this.adapter.log.warn('createDeviceDefinitions: payload ist kein Array');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
36
58
|
utils.clearArray(this.deviceCache);
|
|
37
59
|
for (const devicesMessag of devicesMessage) {
|
|
38
|
-
if (
|
|
60
|
+
if (!devicesMessag || !devicesMessag.ieee_address) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isDebugDevice(this.logCustomizations.debugDevices, devicesMessag.ieee_address)) {
|
|
39
65
|
this.adapter.log.warn(
|
|
40
66
|
`--->>> fromZ2M -> ${devicesMessag.ieee_address} exposes: ${JSON.stringify(devicesMessag)}`
|
|
41
67
|
);
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
if (devicesMessag.definition != null) {
|
|
45
|
-
// if the device is already present in the cache, remove it
|
|
46
71
|
this.removeDeviceByIeee(this.deviceCache, devicesMessag.ieee_address);
|
|
47
72
|
|
|
48
|
-
|
|
73
|
+
// Fix 3: exposes muss auch ein Array sein
|
|
74
|
+
if (Array.isArray(devicesMessag.definition.exposes)) {
|
|
49
75
|
try {
|
|
50
76
|
const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
|
|
51
77
|
newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
|
|
52
78
|
this.deviceCache.push(newDevice);
|
|
53
79
|
} catch (err) {
|
|
54
|
-
|
|
80
|
+
// Fix 2: friendly_name im Fehlerlog verwenden
|
|
81
|
+
this.adapter.log.warn(`Cannot create Device from Exposes for ${devicesMessag.friendly_name || devicesMessag.ieee_address}!`);
|
|
55
82
|
this.adapter.log.debug(JSON.stringify(devicesMessag));
|
|
56
83
|
this.adapter.log.debug(err);
|
|
57
84
|
}
|
|
@@ -61,29 +88,29 @@ class DeviceController {
|
|
|
61
88
|
}
|
|
62
89
|
|
|
63
90
|
/**
|
|
91
|
+
* Erstellt die State-Definition für eine Zigbee2MQTT-Gruppe und legt sie im groupCache ab.
|
|
64
92
|
*
|
|
65
|
-
* @param groupID
|
|
66
|
-
* @param ieee_address
|
|
67
|
-
* @param scenes
|
|
93
|
+
* @param {string} groupID Friendly-Name der Gruppe (wird als ioBroker-ID verwendet)
|
|
94
|
+
* @param {string} ieee_address Interne Gruppen-ID (z.B. "group_1")
|
|
95
|
+
* @param {Array} scenes Liste der Szenen-Objekte ({id, name}) der Gruppe
|
|
68
96
|
*/
|
|
69
|
-
|
|
70
|
-
const brmPropName =
|
|
71
|
-
|
|
72
|
-
const brsPropName =
|
|
73
|
-
this.adapter.config.brightnessStepOnOff == true ? 'brightness_step_onoff' : 'brightness_step';
|
|
97
|
+
defineGroupDevice(groupID, ieee_address, scenes) {
|
|
98
|
+
const brmPropName = this.adapter.config.brightnessMoveOnOff === true ? 'brightness_move_onoff' : 'brightness_move';
|
|
99
|
+
const brsPropName = this.adapter.config.brightnessStepOnOff === true ? 'brightness_step_onoff' : 'brightness_step';
|
|
74
100
|
const newDevice = {
|
|
75
101
|
id: groupID,
|
|
76
102
|
ieee_address: ieee_address,
|
|
77
103
|
icon: undefined, // await imageController.getDeviceIcon(devicesMessag), device.definition.model
|
|
78
104
|
optionsValues: {},
|
|
79
105
|
states: [
|
|
80
|
-
statesDefs
|
|
81
|
-
statesDefs.
|
|
82
|
-
statesDefs.
|
|
83
|
-
statesDefs.
|
|
106
|
+
// Fix 1: Klone aller statesDefs-Objekte – sonst werden geteilte Referenzen mutiert
|
|
107
|
+
Object.assign({}, statesDefs.available),
|
|
108
|
+
Object.assign({}, statesDefs.brightness),
|
|
109
|
+
Object.assign({}, statesDefs.colortemp_move),
|
|
110
|
+
Object.assign({}, statesDefs.transition),
|
|
84
111
|
{
|
|
85
112
|
id: 'state',
|
|
86
|
-
|
|
113
|
+
prop: 'state',
|
|
87
114
|
name: 'Switch state',
|
|
88
115
|
options: ['transition'],
|
|
89
116
|
icon: undefined,
|
|
@@ -133,29 +160,28 @@ class DeviceController {
|
|
|
133
160
|
type: 'string',
|
|
134
161
|
def: '#ff00ff',
|
|
135
162
|
setter: (value) => {
|
|
136
|
-
|
|
163
|
+
// Fix 6: redundante [0,0]-Initialisierung entfernt
|
|
137
164
|
const rgbcolor = colors.ParseColor(value);
|
|
138
|
-
xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
165
|
+
const xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
|
|
139
166
|
return {
|
|
140
167
|
x: xy[0],
|
|
141
168
|
y: xy[1],
|
|
142
169
|
};
|
|
143
170
|
},
|
|
144
171
|
getter: (payload) => {
|
|
145
|
-
if (payload.color_mode
|
|
172
|
+
if (payload.color_mode !== 'xy' && !this.config.colorTempSyncColor) {
|
|
146
173
|
return undefined;
|
|
147
174
|
}
|
|
148
|
-
|
|
175
|
+
// Fix 2: x=0 / y=0 sind gültige CIE-Koordinaten → != null statt truthy
|
|
176
|
+
if (payload.color && payload.color.x != null && payload.color.y != null) {
|
|
149
177
|
const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
|
|
150
178
|
return (
|
|
151
|
-
`#${
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}${utils.decimalToHex(colorval[2])}`
|
|
179
|
+
`#${utils.decimalToHex(colorval[0])
|
|
180
|
+
}${utils.decimalToHex(colorval[1])
|
|
181
|
+
}${utils.decimalToHex(colorval[2])}`
|
|
155
182
|
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
159
185
|
},
|
|
160
186
|
},
|
|
161
187
|
{
|
|
@@ -168,22 +194,24 @@ class DeviceController {
|
|
|
168
194
|
write: true,
|
|
169
195
|
read: true,
|
|
170
196
|
type: 'number',
|
|
171
|
-
min: this.config.useKelvin
|
|
172
|
-
max: this.config.useKelvin
|
|
173
|
-
def: this.config.useKelvin
|
|
174
|
-
unit: this.config.useKelvin
|
|
197
|
+
min: this.config.useKelvin === true ? utils.miredKelvinConversion(550) : 150,
|
|
198
|
+
max: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
|
|
199
|
+
def: this.config.useKelvin === true ? utils.miredKelvinConversion(153) : 500,
|
|
200
|
+
unit: this.config.useKelvin === true ? 'K' : 'mired',
|
|
175
201
|
setter: (value) => {
|
|
176
202
|
return utils.toMired(value);
|
|
177
203
|
},
|
|
178
204
|
getter: (payload) => {
|
|
179
|
-
if (payload.color_mode
|
|
205
|
+
if (payload.color_mode !== 'color_temp') {
|
|
180
206
|
return undefined;
|
|
181
207
|
}
|
|
182
|
-
if (
|
|
208
|
+
if (payload.color_temp == null) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
if (this.config.useKelvin === true) {
|
|
183
212
|
return utils.miredKelvinConversion(payload.color_temp);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
213
|
+
}
|
|
214
|
+
return payload.color_temp;
|
|
187
215
|
},
|
|
188
216
|
},
|
|
189
217
|
{
|
|
@@ -226,7 +254,7 @@ class DeviceController {
|
|
|
226
254
|
const sceneSate = {
|
|
227
255
|
id: `scene_${scene.id}`,
|
|
228
256
|
prop: `scene_recall`,
|
|
229
|
-
name: scene.name
|
|
257
|
+
name: scene.name || `Scene ${scene.id}`,
|
|
230
258
|
icon: undefined,
|
|
231
259
|
role: 'button',
|
|
232
260
|
write: true,
|
|
@@ -244,148 +272,172 @@ class DeviceController {
|
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
/**
|
|
275
|
+
* Erstellt Gruppen-Definitionen aus dem bridge/groups-Payload von Zigbee2MQTT
|
|
276
|
+
* und befüllt den groupCache neu.
|
|
247
277
|
*
|
|
248
|
-
* @param groupsMessage
|
|
278
|
+
* @param {Array} groupsMessage Array mit Gruppendaten aus dem bridge/groups-Topic
|
|
249
279
|
*/
|
|
250
280
|
async createGroupDefinitions(groupsMessage) {
|
|
281
|
+
// Fix 4: groupsMessage kann null/kein Array sein
|
|
282
|
+
if (!Array.isArray(groupsMessage)) {
|
|
283
|
+
this.adapter.log.warn('createGroupDefinitions: payload ist kein Array');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
251
286
|
utils.clearArray(this.groupCache);
|
|
252
287
|
for (const groupMessage of groupsMessage) {
|
|
253
|
-
|
|
288
|
+
// Fix 5: fehlende Pflichtfelder abfangen
|
|
289
|
+
if (!groupMessage || groupMessage.id == null || !groupMessage.friendly_name) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (isDebugDevice(this.logCustomizations.debugDevices, String(groupMessage.id))) {
|
|
254
293
|
this.adapter.log.warn(`--->>> fromZ2M -> ${groupMessage.id} exposes: ${JSON.stringify(groupMessage)}`);
|
|
255
294
|
}
|
|
256
|
-
|
|
295
|
+
this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes || []);
|
|
257
296
|
}
|
|
258
297
|
}
|
|
259
298
|
|
|
260
299
|
/**
|
|
261
|
-
*
|
|
300
|
+
* Legt alle Geräte und Gruppen aus den Caches als ioBroker-Objekte an
|
|
301
|
+
* bzw. aktualisiert sie falls sich Name oder Beschreibung geändert haben.
|
|
262
302
|
*/
|
|
263
303
|
async createOrUpdateDevices() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
// Gruppen und Geräte getrennt verarbeiten
|
|
305
|
+
for (const device of this.groupCache) {
|
|
306
|
+
await this._createOrUpdateSingleDevice(device, true);
|
|
307
|
+
}
|
|
308
|
+
for (const device of this.deviceCache) {
|
|
309
|
+
await this._createOrUpdateSingleDevice(device, false);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
267
312
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Legt ein einzelnes Gerät oder eine Gruppe als ioBroker-Objekt an oder aktualisiert es.
|
|
315
|
+
*
|
|
316
|
+
* @param {object} device Das Gerät/Gruppen-Objekt aus dem Cache
|
|
317
|
+
* @param {boolean} isGroup true = Gruppe, false = echtes Gerät
|
|
318
|
+
*/
|
|
319
|
+
async _createOrUpdateSingleDevice(device, isGroup) {
|
|
320
|
+
let deviceName = this.getDeviceName(device);
|
|
321
|
+
let description = this.getDeviceDescription(device);
|
|
272
322
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
323
|
+
if (deviceName === '' && device.description) {
|
|
324
|
+
deviceName = device.description;
|
|
325
|
+
description = '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Disabled-Flag nur bei echten Geräten – Gruppen können nicht disabled sein
|
|
329
|
+
if (!isGroup && device.disabled === true) {
|
|
330
|
+
if (this.config.useEventInDesc === true) {
|
|
331
|
+
description = 'Device is disabled!';
|
|
332
|
+
} else {
|
|
333
|
+
// Fallback auf ieee_address wenn kein friendly_name vorhanden
|
|
334
|
+
const label = deviceName || device.ieee_address;
|
|
335
|
+
deviceName = `[Disabled] ${label}`;
|
|
280
336
|
}
|
|
337
|
+
}
|
|
281
338
|
|
|
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
|
-
};
|
|
339
|
+
if (!this.createCache[device.ieee_address] ||
|
|
340
|
+
this.createCache[device.ieee_address].name !== deviceName ||
|
|
341
|
+
this.createCache[device.ieee_address].description !== description) {
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
343
|
+
const deviceObj = {
|
|
344
|
+
type: 'device',
|
|
345
|
+
common: {
|
|
346
|
+
icon: device.icon,
|
|
347
|
+
name: deviceName,
|
|
348
|
+
desc: description,
|
|
349
|
+
statusStates: { onlineId: '' },
|
|
350
|
+
},
|
|
351
|
+
native: {
|
|
352
|
+
deviceRemoved: false,
|
|
353
|
+
groupDevice: isGroup,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (isGroup || device.disabled !== true) {
|
|
358
|
+
deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
|
|
312
359
|
}
|
|
313
360
|
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
}
|
|
361
|
+
await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
|
|
362
|
+
// Bestehende State-Einträge im Cache bewahren – nur name/description aktualisieren
|
|
363
|
+
if (!this.createCache[device.ieee_address]) {
|
|
364
|
+
this.createCache[device.ieee_address] = {};
|
|
323
365
|
}
|
|
366
|
+
this.createCache[device.ieee_address].name = deviceName;
|
|
367
|
+
this.createCache[device.ieee_address].description = description;
|
|
368
|
+
}
|
|
324
369
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const iobState = {
|
|
329
|
-
type: 'state',
|
|
330
|
-
common: await this.copyAndCleanStateObj(state),
|
|
331
|
-
native: {},
|
|
332
|
-
};
|
|
370
|
+
if (!Array.isArray(device.states)) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
333
373
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
374
|
+
// Veraltete scene_*-States bereinigen – getForeignObjectsAsync mit vollem Namespace-Pfad
|
|
375
|
+
const sceneObjects = await this.adapter.getForeignObjectsAsync(
|
|
376
|
+
`${this.adapter.namespace}.${device.ieee_address}.scene_*`
|
|
377
|
+
);
|
|
378
|
+
if (sceneObjects) {
|
|
379
|
+
for (const sceneObjId of Object.keys(sceneObjects)) {
|
|
380
|
+
const parts = sceneObjId.split('.');
|
|
381
|
+
const stateID = parts[parts.length - 1];
|
|
382
|
+
if (!device.states.find((x) => x.id === stateID)) {
|
|
383
|
+
await this.adapter.delForeignObjectAsync(sceneObjId);
|
|
337
384
|
}
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
|
-
}
|
|
341
387
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
388
|
+
for (const state of device.states) {
|
|
389
|
+
if (state && (!this.createCache[device.ieee_address][state.id] ||
|
|
390
|
+
this.createCache[device.ieee_address][state.id].name !== state.name)) {
|
|
391
|
+
|
|
392
|
+
const iobState = {
|
|
393
|
+
type: 'state',
|
|
394
|
+
common: this.copyAndCleanStateObj(state),
|
|
395
|
+
native: {},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
|
|
399
|
+
this.createCache[device.ieee_address][state.id] = { name: state.name, created: true };
|
|
400
|
+
}
|
|
352
401
|
}
|
|
353
402
|
}
|
|
354
403
|
|
|
355
404
|
/**
|
|
356
|
-
*
|
|
405
|
+
* Prüft alle in ioBroker vorhandenen Geräte-Objekte gegen den deviceCache und markiert
|
|
406
|
+
* nicht mehr vorhandene Geräte als "[Removed]" bzw. setzt available auf false.
|
|
357
407
|
*/
|
|
358
408
|
async checkAndProgressDeviceRemove() {
|
|
359
|
-
let description = '';
|
|
360
|
-
let deviceName = '';
|
|
361
409
|
let iobDevices = await this.adapter.getDevicesAsync();
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
410
|
+
if (!iobDevices) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// Nur nicht-entfernte, echte Geräte (keine Gruppen) in einem Schritt filtern
|
|
414
|
+
iobDevices = iobDevices.filter((x) =>
|
|
415
|
+
x.native && x.native.deviceRemoved === false && x.native.groupDevice === false
|
|
416
|
+
);
|
|
366
417
|
|
|
367
418
|
for (const iobDevice of iobDevices) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
419
|
+
const idParts = iobDevice._id.split('.');
|
|
420
|
+
if (idParts.length < 3) {continue;}
|
|
421
|
+
const ieee_address = idParts[2];
|
|
422
|
+
|
|
423
|
+
// Gruppen-Einträge im ioBroker-Baum explizit überspringen
|
|
424
|
+
if (ieee_address.startsWith('group_')) {continue;}
|
|
425
|
+
|
|
426
|
+
if (!this.deviceCache.find((x) => x.ieee_address === ieee_address)) {
|
|
427
|
+
let deviceName = iobDevice.common && iobDevice.common.name ? iobDevice.common.name : ieee_address;
|
|
428
|
+
let description = '';
|
|
372
429
|
|
|
373
|
-
if (this.config.useEventInDesc
|
|
430
|
+
if (this.config.useEventInDesc === true) {
|
|
374
431
|
description = 'Device was removed!';
|
|
375
432
|
} else {
|
|
376
433
|
deviceName = `[Removed] ${deviceName}`;
|
|
377
434
|
}
|
|
378
435
|
|
|
379
|
-
this.adapter.
|
|
380
|
-
common: {
|
|
381
|
-
|
|
382
|
-
desc: description,
|
|
383
|
-
},
|
|
384
|
-
native: {
|
|
385
|
-
deviceRemoved: true,
|
|
386
|
-
},
|
|
436
|
+
await this.adapter.extendObjectAsync(ieee_address, {
|
|
437
|
+
common: { name: deviceName, desc: description },
|
|
438
|
+
native: { deviceRemoved: true },
|
|
387
439
|
});
|
|
388
|
-
this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
|
|
440
|
+
await this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
|
|
389
441
|
|
|
390
442
|
delete this.createCache[ieee_address];
|
|
391
443
|
}
|
|
@@ -393,22 +445,46 @@ class DeviceController {
|
|
|
393
445
|
}
|
|
394
446
|
|
|
395
447
|
/**
|
|
448
|
+
* Aktualisiert die Geräte-ID im Cache wenn ein Gerät in Zigbee2MQTT umbenannt wurde.
|
|
449
|
+
*
|
|
450
|
+
* @param {{ payload: { data: { from: string, to: string } } }} messageObj Die Rename-Nachricht
|
|
451
|
+
*/
|
|
452
|
+
async renameDeviceInCache(messageObj) {
|
|
453
|
+
if (!messageObj.payload || !messageObj.payload.data) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!messageObj.payload.data.from || !messageObj.payload.data.to) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const renamedDevice = this.groupCache
|
|
460
|
+
.concat(this.deviceCache)
|
|
461
|
+
.find((x) => x.id === messageObj.payload.data.from);
|
|
462
|
+
if (renamedDevice) {
|
|
463
|
+
renamedDevice.id = messageObj.payload.data.to;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Entfernt ein Gerät anhand seiner IEEE-Adresse aus dem angegebenen Cache-Array.
|
|
396
469
|
*
|
|
397
|
-
* @param devices
|
|
398
|
-
* @param ieee_address
|
|
470
|
+
* @param {Array} devices Das Cache-Array (deviceCache oder groupCache)
|
|
471
|
+
* @param {string} ieee_address Die IEEE-Adresse des zu entfernenden Geräts
|
|
399
472
|
*/
|
|
400
473
|
removeDeviceByIeee(devices, ieee_address) {
|
|
401
|
-
const idx = devices.findIndex((x) => x.ieee_address
|
|
474
|
+
const idx = devices.findIndex((x) => x.ieee_address === ieee_address);
|
|
402
475
|
if (idx > -1) {
|
|
403
476
|
devices.splice(idx, 1);
|
|
404
477
|
}
|
|
405
478
|
}
|
|
406
479
|
|
|
407
480
|
/**
|
|
481
|
+
* Erstellt eine bereinigte Kopie eines State-Objekts ohne interne Laufzeit-Felder
|
|
482
|
+
* (getter, setter, prop, etc.) für die ioBroker-Objektdefinition.
|
|
408
483
|
*
|
|
409
|
-
* @param state
|
|
484
|
+
* @param {object} state Das vollständige State-Objekt mit Laufzeit-Feldern
|
|
485
|
+
* @returns {object} Das bereinigte State-Objekt für extendObjectAsync
|
|
410
486
|
*/
|
|
411
|
-
|
|
487
|
+
copyAndCleanStateObj(state) {
|
|
412
488
|
const iobState = { ...state };
|
|
413
489
|
const blacklistedKeys = [
|
|
414
490
|
'prop',
|
|
@@ -430,33 +506,44 @@ class DeviceController {
|
|
|
430
506
|
}
|
|
431
507
|
|
|
432
508
|
/**
|
|
509
|
+
* Gibt den Anzeigenamen eines Geräts zurück.
|
|
510
|
+
* Ist friendly_name gleich der IEEE-Adresse, wird ein leerer String zurückgegeben.
|
|
433
511
|
*
|
|
434
|
-
* @param device
|
|
512
|
+
* @param {object} device Das Geräteobjekt aus dem Cache
|
|
513
|
+
* @returns {string} Der Anzeigename oder ""
|
|
435
514
|
*/
|
|
436
515
|
getDeviceName(device) {
|
|
437
|
-
return device.id
|
|
516
|
+
return device.id === device.ieee_address ? '' : device.id;
|
|
438
517
|
}
|
|
439
518
|
|
|
440
519
|
/**
|
|
520
|
+
* Gibt die Beschreibung eines Geräts zurück (oder "" wenn keine vorhanden).
|
|
441
521
|
*
|
|
442
|
-
* @param device
|
|
522
|
+
* @param {object} device Das Geräteobjekt aus dem Cache
|
|
523
|
+
* @returns {string} Die Beschreibung oder ""
|
|
443
524
|
*/
|
|
444
525
|
getDeviceDescription(device) {
|
|
445
526
|
return device.description ? device.description : '';
|
|
446
527
|
}
|
|
447
528
|
|
|
448
529
|
/**
|
|
530
|
+
* Verarbeitet die Antwort eines Coordinator-Check-Requests von Zigbee2MQTT
|
|
531
|
+
* und schreibt fehlende Router in die info-States.
|
|
449
532
|
*
|
|
450
|
-
* @param payload
|
|
533
|
+
* @param {{ data: { missing_routers: Array } }} payload Der Antwort-Payload
|
|
451
534
|
*/
|
|
452
|
-
processCoordinatorCheck(payload) {
|
|
535
|
+
async processCoordinatorCheck(payload) {
|
|
453
536
|
if (payload && payload.data && payload.data.missing_routers) {
|
|
454
537
|
const missingRoutersCount = payload.data.missing_routers.length;
|
|
455
|
-
this.adapter.
|
|
456
|
-
this.adapter.
|
|
538
|
+
await this.adapter.setStateAsync('info.missing_routers', JSON.stringify(payload.data.missing_routers), true);
|
|
539
|
+
await this.adapter.setStateAsync('info.missing_routers_count', missingRoutersCount, true);
|
|
457
540
|
|
|
458
541
|
if (missingRoutersCount > 0) {
|
|
459
|
-
this.
|
|
542
|
+
const logLvl = this.config.coordinatorCheckLogLvl;
|
|
543
|
+
const logFn = (logLvl && typeof this.adapter.log[logLvl] === 'function')
|
|
544
|
+
? this.adapter.log[logLvl].bind(this.adapter.log)
|
|
545
|
+
: this.adapter.log.warn.bind(this.adapter.log);
|
|
546
|
+
logFn(
|
|
460
547
|
`Coordinator check: ${missingRoutersCount} missing routers were found, please check the data point 'zigbee2mqtt.x.info.missing_routers'!`
|
|
461
548
|
);
|
|
462
549
|
} else {
|