matterbridge-zigbee2mqtt 2.8.1 → 3.0.0-dev-20251102-bcdd456

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/CHANGELOG.md CHANGED
@@ -8,6 +8,36 @@ If you like this project and find it useful, please consider giving it a star on
8
8
  <img src="bmc-button.svg" alt="Buy me a coffee" width="120">
9
9
  </a>
10
10
 
11
+ ## [3.0.0] - 2025-11-02
12
+
13
+ ### Added
14
+
15
+ - [tvoc]: Added voc_index to the converter. Thanks Funca (https://github.com/Luligu/matterbridge-zigbee2mqtt/issues/129).
16
+
17
+ ### Changed
18
+
19
+ - [package]: Updated dependencies.
20
+ - [package]: Bumped platform to v.3.0.0.
21
+ - [package]: Bumped entity to v.3.3.0.
22
+ - [package]: Bumped zigbee to v.3.0.0.
23
+ - [package]: Bumped package to automator v.2.0.10.
24
+ - [jest]: Bumped jestHelpers to v.1.0.11.
25
+ - [package]: Require matterbridge v.3.3.0.
26
+ - [package]: Added default config.
27
+ - [package]: Added typed ZigbeePlatformConfig.
28
+ - [platform]: Updated to new signature PlatformMatterbridge.
29
+ - [workflows]: Improved speed on Node CI.
30
+ - [devcontainer]: Added the plugin name to the container.
31
+ - [devcontainer]: Improved performance of first build with shallow clone.
32
+
33
+ ### Fixed
34
+
35
+ - [platform]: Fixed specific zbminir2 device case for all devices. Thanks subst4nc3 (https://github.com/Luligu/matterbridge-zigbee2mqtt/issues/126).
36
+
37
+ <a href="https://www.buymeacoffee.com/luligugithub">
38
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
39
+ </a>
40
+
11
41
  ## [2.8.1] - 2025-10-02
12
42
 
13
43
  ### Automations and scenes
package/dist/entity.js CHANGED
@@ -58,7 +58,7 @@ export class ZigbeeEntity extends EventEmitter {
58
58
  this.log = new AnsiLogger({
59
59
  logName: this.entityName,
60
60
  logTimestampFormat: 4,
61
- logLevel: platform.debugEnabled ? "debug" : platform.log.logLevel,
61
+ logLevel: platform.config.debug ? "debug" : platform.log.logLevel,
62
62
  });
63
63
  this.log.debug(`Created MatterEntity: ${this.entityName}`);
64
64
  this.platform.z2m.on('MESSAGE-' + this.entityName, (payload) => {
@@ -256,12 +256,12 @@ export class ZigbeeEntity extends EventEmitter {
256
256
  this.publishCommand(command, (this.isGroup ? this.group?.friendly_name : this.device?.friendly_name), this.cachePayload);
257
257
  this.cachePayload = {};
258
258
  this.noUpdate = true;
259
- this.log.debug(`****No update for 2 seconds to allow the device ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} to update its state`);
259
+ this.log.debug(`No update for 2 seconds to allow the device ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} to update its state`);
260
260
  clearTimeout(this.noUpdateTimeout);
261
261
  this.noUpdateTimeout = setTimeout(() => {
262
262
  clearTimeout(this.noUpdateTimeout);
263
263
  this.noUpdateTimeout = undefined;
264
- this.log.debug(`****No update is now reset for the device ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db}`);
264
+ this.log.debug(`No update is now reset for the device ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db}`);
265
265
  this.noUpdate = false;
266
266
  }, this.noUpdateTimeoutTime).unref();
267
267
  }, this.cachePublishTimeoutTime).unref();
@@ -318,60 +318,65 @@ export class ZigbeeEntity extends EventEmitter {
318
318
  else if (endpoint.hasClusterServer(OnOff.Cluster.id)) {
319
319
  colorMode = 5;
320
320
  }
321
- this.log.debug(`***Set attributes called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} colorMode ${CYAN}${lookupColorMode[colorMode]}${db} payload ${debugStringify(this.cachePayload)}`);
321
+ this.log.debug(`Set attributes called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} colorMode ${CYAN}${lookupColorMode[colorMode]}${db} payload ${debugStringify(this.cachePayload)}`);
322
322
  }
323
323
  async onCommandHandler(data) {
324
324
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === true) {
325
- this.log.debug(`*Command on ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} already ON`);
325
+ this.log.debug(`Command on ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} already ON`);
326
326
  return;
327
327
  }
328
328
  this.log.debug(`Command on called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber}`);
329
- this.setCachePublishAttributes(data.endpoint, this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : undefined);
330
- this.cachePublish('on', { ['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: 'ON' });
329
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
330
+ this.setCachePublishAttributes(data.endpoint, isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : undefined);
331
+ this.cachePublish('on', { ['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: 'ON' });
331
332
  }
332
333
  async offCommandHandler(data) {
333
334
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false) {
334
- this.log.debug(`*Command off ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} already OFF`);
335
+ this.log.debug(`Command off ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} already OFF`);
335
336
  return;
336
337
  }
337
338
  this.log.debug(`Command off called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber}`);
338
- this.cachePublish('off', { ['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' });
339
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
340
+ this.cachePublish('off', { ['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' });
339
341
  }
340
342
  async toggleCommandHandler(data) {
341
343
  this.log.debug(`Command toggle called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber}`);
344
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
342
345
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false) {
343
- this.setCachePublishAttributes(data.endpoint, this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : undefined);
344
- this.cachePublish('toggle', { ['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: 'ON' });
346
+ this.setCachePublishAttributes(data.endpoint, isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : undefined);
347
+ this.cachePublish('toggle', { ['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: 'ON' });
345
348
  }
346
349
  else {
347
- this.cachePublish('toggle', { ['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' });
350
+ this.cachePublish('toggle', { ['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' });
348
351
  }
349
352
  }
350
353
  async moveToLevelCommandHandler(data) {
351
354
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false || data.endpoint.getAttribute(LevelControl.Cluster.id, 'currentLevel') === data.request.level) {
352
- this.log.debug(`*Command moveToLevel ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or level unchanged`);
355
+ this.log.debug(`Command moveToLevel ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or level unchanged`);
353
356
  return;
354
357
  }
355
358
  this.log.debug(`Command moveToLevel called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.level} transition: ${data.request.transitionTime}`);
356
- this.cachePublish('moveToLevel', { ['brightness' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.level }, data.request.transitionTime);
359
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
360
+ this.cachePublish('moveToLevel', { ['brightness' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.level }, data.request.transitionTime);
357
361
  }
358
362
  async moveToLevelWithOnOffCommandHandler(data) {
359
363
  this.log.debug(`Command moveToLevelWithOnOff called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.level} transition: ${data.request.transitionTime}`);
364
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
360
365
  if (data.request['level'] <= (data.endpoint.getAttribute(LevelControl.Cluster.id, 'minLevel') ?? 1)) {
361
366
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false) {
362
367
  this.log.debug(`*Command moveToLevelWithOnOff ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF`);
363
368
  return;
364
369
  }
365
- data.endpoint.log.debug(`***Command moveToLevelWithOnOff received with level <= minLevel(${data.endpoint.getAttribute(LevelControl.Cluster.id, 'minLevel')}) => turn off the light`);
366
- this.cachePublish('moveToLevelWithOnOff', { ['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' }, data.request.transitionTime);
370
+ data.endpoint.log.debug(`Command moveToLevelWithOnOff received with level <= minLevel(${data.endpoint.getAttribute(LevelControl.Cluster.id, 'minLevel')}) => turn off the light`);
371
+ this.cachePublish('moveToLevelWithOnOff', { ['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: 'OFF' }, data.request.transitionTime);
367
372
  }
368
373
  else {
369
374
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false) {
370
- data.endpoint.log.debug(`***Command moveToLevelWithOnOff received with level > minLevel(${data.endpoint.getAttribute(LevelControl.Cluster.id, 'minLevel')}) and light is off => turn on the light with attributes`);
371
- this.cachePayload['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')] = 'ON';
372
- this.setCachePublishAttributes(data.endpoint, this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '');
375
+ data.endpoint.log.debug(`Command moveToLevelWithOnOff received with level > minLevel(${data.endpoint.getAttribute(LevelControl.Cluster.id, 'minLevel')}) and light is off => turn on the light with attributes`);
376
+ this.cachePayload['state' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')] = 'ON';
377
+ this.setCachePublishAttributes(data.endpoint, isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '');
373
378
  }
374
- this.cachePublish('moveToLevelWithOnOff', { ['brightness' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.level }, data.request.transitionTime);
379
+ this.cachePublish('moveToLevelWithOnOff', { ['brightness' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.level }, data.request.transitionTime);
375
380
  }
376
381
  }
377
382
  async moveToColorTemperatureCommandHandler(data) {
@@ -381,50 +386,55 @@ export class ZigbeeEntity extends EventEmitter {
381
386
  return;
382
387
  }
383
388
  this.log.debug(`Command moveToColorTemperature called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.colorTemperatureMireds} transition: ${data.request.transitionTime}`);
389
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
384
390
  if (this.propertyMap.get('color_temp')) {
385
- this.cachePublish('moveToColorTemperature', { ['color_temp' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.colorTemperatureMireds }, data.request.transitionTime);
391
+ this.cachePublish('moveToColorTemperature', { ['color_temp' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: data.request.colorTemperatureMireds }, data.request.transitionTime);
386
392
  }
387
393
  else {
388
394
  const rgb = kelvinToRGB(miredToKelvin(data.request.colorTemperatureMireds));
389
- this.cachePublish('moveToColorTemperature', { ['color' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: { r: rgb.r, g: rgb.g, b: rgb.b } }, data.request.transitionTime);
390
- this.log.debug(`***Command moveToColorTemperature called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} but color_temp property is not available. Converting ${data.request.colorTemperatureMireds} to RGB ${debugStringify(rgb)}.`);
395
+ this.cachePublish('moveToColorTemperature', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { r: rgb.r, g: rgb.g, b: rgb.b } }, data.request.transitionTime);
396
+ this.log.debug(`Command moveToColorTemperature called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} but color_temp property is not available. Converting ${data.request.colorTemperatureMireds} to RGB ${debugStringify(rgb)}.`);
391
397
  }
392
398
  }
393
399
  async moveToColorCommandHandler(data) {
394
400
  delete this.cachePayload['color_temp'];
395
401
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false || (data.endpoint.getAttribute(ColorControl.Cluster.id, 'colorMode') === ColorControl.ColorMode.CurrentXAndCurrentY && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentX') === data.request.colorX && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentY') === data.request.colorY)) {
396
- this.log.debug(`*Command moveToColor ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or color unchanged`);
402
+ this.log.debug(`Command moveToColor ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or color unchanged`);
397
403
  return;
398
404
  }
399
405
  this.log.debug(`Command moveToColor called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: X: ${data.request.colorX} Y: ${data.request.colorY} transition: ${data.request.transitionTime}`);
400
- this.cachePublish('moveToColor', { ['color' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: { x: Math.round(data.request.colorX / 65536 * 10000) / 10000, y: Math.round(data.request.colorY / 65536 * 10000) / 10000 } }, data.request.transitionTime);
406
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
407
+ this.cachePublish('moveToColor', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { x: Math.round(data.request.colorX / 65536 * 10000) / 10000, y: Math.round(data.request.colorY / 65536 * 10000) / 10000 } }, data.request.transitionTime);
401
408
  }
402
409
  async moveToHueCommandHandler(data) {
403
410
  delete this.cachePayload['color_temp'];
404
411
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false || (data.endpoint.getAttribute(ColorControl.Cluster.id, 'colorMode') === ColorControl.ColorMode.CurrentHueAndCurrentSaturation && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentHue') === data.request.hue)) {
405
- this.log.debug(`*Command moveToHue ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or hue unchanged`);
412
+ this.log.debug(`Command moveToHue ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or hue unchanged`);
406
413
  return;
407
414
  }
408
415
  this.log.debug(`Command moveToHue called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.hue} transition: ${data.request.transitionTime}`);
409
- this.cachePublish('moveToHue', { ['color' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.request.hue / 254 * 360), s: Math.round(data.endpoint.getAttribute(ColorControlCluster.id, 'currentSaturation') / 254 * 100) } }, data.request.transitionTime);
416
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
417
+ this.cachePublish('moveToHue', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.request.hue / 254 * 360), s: Math.round(data.endpoint.getAttribute(ColorControlCluster.id, 'currentSaturation') / 254 * 100) } }, data.request.transitionTime);
410
418
  }
411
419
  async moveToSaturationCommandHandler(data) {
412
420
  delete this.cachePayload['color_temp'];
413
421
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false || (data.endpoint.getAttribute(ColorControl.Cluster.id, 'colorMode') === ColorControl.ColorMode.CurrentHueAndCurrentSaturation && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentSaturation') === data.request.saturation)) {
414
- this.log.debug(`*Command moveToSaturation ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or saturation unchanged`);
422
+ this.log.debug(`Command moveToSaturation ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or saturation unchanged`);
415
423
  return;
416
424
  }
417
425
  this.log.debug(`Command moveToSaturation called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.saturation} transition: ${data.request.transitionTime}`);
418
- this.cachePublish('moveToSaturation', { ['color' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.endpoint.getAttribute(ColorControlCluster.id, 'currentHue') / 254 * 360), s: Math.round(data.request.saturation / 254 * 100) } }, data.request.transitionTime);
426
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
427
+ this.cachePublish('moveToSaturation', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.endpoint.getAttribute(ColorControlCluster.id, 'currentHue') / 254 * 360), s: Math.round(data.request.saturation / 254 * 100) } }, data.request.transitionTime);
419
428
  }
420
429
  async moveToHueAndSaturationCommandHandler(data) {
421
430
  delete this.cachePayload['color_temp'];
422
431
  if (data.endpoint.getAttribute(OnOff.Cluster.id, 'onOff') === false || (data.endpoint.getAttribute(ColorControl.Cluster.id, 'colorMode') === ColorControl.ColorMode.CurrentHueAndCurrentSaturation && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentHue') === data.request.hue && data.endpoint.getAttribute(ColorControl.Cluster.id, 'currentSaturation') === data.request.saturation)) {
423
- this.log.debug(`*Command moveToHueAndSaturation ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or hue/saturation unchanged`);
432
+ this.log.debug(`Command moveToHueAndSaturation ignored for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} light OFF or hue/saturation unchanged`);
424
433
  return;
425
434
  }
426
435
  this.log.debug(`Command moveToHueAndSaturation called for ${this.ien}${this.isGroup ? this.group?.friendly_name : this.device?.friendly_name}${rs}${db} endpoint: ${data.endpoint?.maybeId}:${data.endpoint?.maybeNumber} request: ${data.request.hue} - ${data.request.saturation} transition: ${data.request.transitionTime}`);
427
- this.cachePublish('moveToHueAndSaturation', { ['color' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.request.hue / 254 * 360), s: Math.round(data.request.saturation / 254 * 100) } }, data.request.transitionTime);
436
+ const isChildEndpoint = data.endpoint.deviceName !== this.entityName;
437
+ this.cachePublish('moveToHueAndSaturation', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { h: Math.round(data.request.hue / 254 * 360), s: Math.round(data.request.saturation / 254 * 100) } }, data.request.transitionTime);
428
438
  }
429
439
  addBridgedDeviceBasicInformation() {
430
440
  if (!this.bridgedDevice)
@@ -839,6 +849,7 @@ const z2ms = [
839
849
  { type: '', name: 'pressure', property: 'pressure', deviceType: pressureSensor, cluster: PressureMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return value; } },
840
850
  { type: '', name: 'air_quality', property: 'air_quality', deviceType: airQualitySensor, cluster: AirQuality.Cluster.id, attribute: 'airQuality', valueLookup: ['unknown', 'excellent', 'good', 'moderate', 'poor', 'unhealthy', 'out_of_range'] },
841
851
  { type: '', name: 'voc', property: 'voc', deviceType: airQualitySensor, cluster: TotalVolatileOrganicCompoundsConcentrationMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.min(65535, value); } },
852
+ { type: '', name: 'voc_index', property: 'voc_index', deviceType: airQualitySensor, cluster: TotalVolatileOrganicCompoundsConcentrationMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.min(65535, value); } },
842
853
  { type: '', name: 'co', property: 'co', deviceType: airQualitySensor, cluster: CarbonMonoxideConcentrationMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(value); } },
843
854
  { type: '', name: 'co2', property: 'co2', deviceType: airQualitySensor, cluster: CarbonDioxideConcentrationMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(value); } },
844
855
  { type: '', name: 'formaldehyd', property: 'formaldehyd', deviceType: airQualitySensor, cluster: FormaldehydeConcentrationMeasurement.Cluster.id, attribute: 'measuredValue', converter: (value) => { return Math.round(value); } },
@@ -5,6 +5,9 @@ import { isValidNumber, isValidString, waiter } from 'matterbridge/utils';
5
5
  import { BridgedDeviceBasicInformation, DoorLock } from 'matterbridge/matter/clusters';
6
6
  import { ZigbeeDevice, ZigbeeGroup } from './entity.js';
7
7
  import { Zigbee2MQTT } from './zigbee2mqtt.js';
8
+ export default function initializePlugin(matterbridge, log, config) {
9
+ return new ZigbeePlatform(matterbridge, log, config);
10
+ }
8
11
  export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
9
12
  bridgedDevices = [];
10
13
  zigbeeEntities = [];
@@ -24,7 +27,6 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
24
27
  featureBlackList = [];
25
28
  deviceFeatureBlackList = {};
26
29
  postfix = '';
27
- debugEnabled;
28
30
  shouldStart;
29
31
  shouldConfigure;
30
32
  z2m;
@@ -39,10 +41,9 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
39
41
  availabilityTimer;
40
42
  constructor(matterbridge, log, config) {
41
43
  super(matterbridge, log, config);
42
- if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.0.4')) {
43
- throw new Error(`This plugin requires Matterbridge version >= "3.0.4". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`);
44
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.3.0')) {
45
+ throw new Error(`This plugin requires Matterbridge version >= "3.3.0". Please update Matterbridge from ${this.matterbridge.matterbridgeVersion} to the latest version in the frontend."`);
44
46
  }
45
- this.debugEnabled = config.debug;
46
47
  this.shouldStart = false;
47
48
  this.shouldConfigure = false;
48
49
  if (config.host && typeof config.host === 'string') {
@@ -81,8 +82,8 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
81
82
  config.port = this.mqttPort;
82
83
  config.protocolVersion = this.mqttProtocol;
83
84
  config.topic = this.mqttTopic;
84
- config.username = this.mqttUsername;
85
- config.password = this.mqttPassword;
85
+ config.username = this.mqttUsername ?? '';
86
+ config.password = this.mqttPassword ?? '';
86
87
  config.postfix = this.postfix;
87
88
  if (config.postfixHostname !== undefined)
88
89
  delete config.postfixHostname;
@@ -96,8 +97,8 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
96
97
  config.scenesPrefix = true;
97
98
  this.log.info(`Initializing platform: ${CYAN}${this.config.name}${nf} version: ${CYAN}${this.config.version}${rs}`);
98
99
  this.log.info(`Loaded zigbee2mqtt parameters from ${CYAN}${path.join(matterbridge.matterbridgeDirectory, 'matterbridge-zigbee2mqtt.config.json')}${rs}`);
99
- this.z2m = new Zigbee2MQTT(this.mqttHost, this.mqttPort, this.mqttTopic, this.mqttUsername, this.mqttPassword, this.mqttProtocol, this.config.ca, this.config.rejectUnauthorized, this.config.cert, this.config.key, this.debugEnabled);
100
- this.z2m.setLogDebug(this.debugEnabled);
100
+ this.z2m = new Zigbee2MQTT(this.mqttHost, this.mqttPort, this.mqttTopic, this.mqttUsername, this.mqttPassword, this.mqttProtocol, this.config.ca, this.config.rejectUnauthorized, this.config.cert, this.config.key, config.debug);
101
+ this.z2m.setLogDebug(config.debug);
101
102
  this.z2m.setDataPath(path.join(matterbridge.matterbridgePluginDirectory, 'matterbridge-zigbee2mqtt'));
102
103
  if (isValidString(this.mqttHost) && isValidNumber(this.mqttPort, 1, 65535)) {
103
104
  this.log.info(`Connecting to MQTT broker: ${this.mqttHost + ':' + this.mqttPort.toString()}`);
@@ -169,7 +170,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
169
170
  for (const bridgedEntity of this.zigbeeEntities) {
170
171
  if (bridgedEntity.isDevice && bridgedEntity.device)
171
172
  await this.requestDeviceUpdate(bridgedEntity.device);
172
- bridgedEntity.configure();
173
+ await bridgedEntity.configure();
173
174
  }
174
175
  }
175
176
  });
@@ -191,7 +192,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
191
192
  for (const bridgedEntity of this.zigbeeEntities) {
192
193
  if (bridgedEntity.isGroup && bridgedEntity.group)
193
194
  await this.requestGroupUpdate(bridgedEntity.group);
194
- bridgedEntity.configure();
195
+ await bridgedEntity.configure();
195
196
  }
196
197
  }
197
198
  });
@@ -1,12 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import * as util from 'node:util';
4
- import * as crypto from 'node:crypto';
3
+ import crypto from 'node:crypto';
5
4
  import { EventEmitter } from 'node:events';
6
- import { mkdir } from 'node:fs/promises';
7
5
  import { connectAsync } from 'mqtt';
8
6
  import { AnsiLogger, rs, db, dn, gn, er, zb, hk, id, idn, ign, REVERSE, REVERSEOFF } from 'node-ansi-logger';
9
- const writeFile = util.promisify(fs.writeFile);
10
7
  export class Zigbee2MQTT extends EventEmitter {
11
8
  log;
12
9
  mqttHost;
@@ -28,9 +25,11 @@ export class Zigbee2MQTT extends EventEmitter {
28
25
  z2mPermitJoin;
29
26
  z2mPermitJoinTimeout;
30
27
  z2mVersion;
28
+ z2mBridge;
31
29
  z2mDevices;
32
30
  z2mGroups;
33
- loggedEntries = 0;
31
+ loggedBridgePayloads = 0;
32
+ loggedPublishPayloads = 0;
34
33
  options = {
35
34
  clientId: 'matterbridge_' + crypto.randomBytes(8).toString('hex'),
36
35
  keepalive: 60,
@@ -106,6 +105,7 @@ export class Zigbee2MQTT extends EventEmitter {
106
105
  this.z2mPermitJoin = false;
107
106
  this.z2mPermitJoinTimeout = 0;
108
107
  this.z2mVersion = '';
108
+ this.z2mBridge = {};
109
109
  this.z2mDevices = [];
110
110
  this.z2mGroups = [];
111
111
  this.log.debug(`Created new instance with host: ${mqttHost} port: ${mqttPort} protocol ${protocolVersion} topic: ${mqttTopic} username: ${mqttUsername !== undefined && mqttUsername !== '' ? mqttUsername : 'undefined'} password: ${mqttPassword !== undefined && mqttPassword !== '' ? '*****' : 'undefined'}`);
@@ -118,7 +118,7 @@ export class Zigbee2MQTT extends EventEmitter {
118
118
  }
119
119
  async setDataPath(dataPath) {
120
120
  try {
121
- await mkdir(dataPath, { recursive: true });
121
+ await fs.promises.mkdir(dataPath, { recursive: true });
122
122
  this.mqttDataPath = dataPath;
123
123
  this.log.debug(`Data directory ${this.mqttDataPath} created successfully.`);
124
124
  }
@@ -138,6 +138,13 @@ export class Zigbee2MQTT extends EventEmitter {
138
138
  catch (error) {
139
139
  this.log.debug(`Error deleting bridge-payloads.txt: ${error}`);
140
140
  }
141
+ try {
142
+ const filePath = path.join(this.mqttDataPath, 'bridge-publish-payloads.txt');
143
+ fs.unlinkSync(filePath);
144
+ }
145
+ catch (error) {
146
+ this.log.debug(`Error deleting bridge-publish-payloads.txt: ${error}`);
147
+ }
141
148
  }
142
149
  getUrl() {
143
150
  return this.mqttHost + ':' + this.mqttPort.toString();
@@ -305,6 +312,11 @@ export class Zigbee2MQTT extends EventEmitter {
305
312
  this.log.debug(`Publish ${REVERSE}[${this.mqttPublishInflights}]${REVERSEOFF} success on topic: ${topic} message: ${message}`);
306
313
  this.emit('mqtt_published');
307
314
  this.mqttPublishInflights--;
315
+ if (this.log.logLevel === "debug" && this.loggedPublishPayloads < 10000) {
316
+ const filePath = path.join(this.mqttDataPath, 'bridge-publish-payloads.txt');
317
+ fs.appendFileSync(filePath, `${new Date().toLocaleString()} - ` + JSON.stringify({ topic, message }).replaceAll('\\"', '"') + '\n');
318
+ this.loggedPublishPayloads++;
319
+ }
308
320
  }
309
321
  catch (error) {
310
322
  this.mqttPublishInflights--;
@@ -325,7 +337,8 @@ export class Zigbee2MQTT extends EventEmitter {
325
337
  this.log.error('writeBufferJSON: parsing error:', error);
326
338
  return;
327
339
  }
328
- writeFile(`${filePath}.json`, JSON.stringify(jsonData, null, 2))
340
+ fs.promises
341
+ .writeFile(`${filePath}.json`, JSON.stringify(jsonData, null, 2))
329
342
  .then(() => {
330
343
  this.log.debug(`Successfully wrote to ${filePath}.json`);
331
344
  return;
@@ -336,7 +349,8 @@ export class Zigbee2MQTT extends EventEmitter {
336
349
  }
337
350
  async writeFile(file, data) {
338
351
  const filePath = path.join(this.mqttDataPath, file);
339
- writeFile(`${filePath}`, data)
352
+ fs.promises
353
+ .writeFile(`${filePath}`, data)
340
354
  .then(() => {
341
355
  this.log.debug(`Successfully wrote to ${filePath}`);
342
356
  return;
@@ -376,151 +390,38 @@ export class Zigbee2MQTT extends EventEmitter {
376
390
  this.log.debug(`Message bridge/state online => ${this.z2mIsOnline}`);
377
391
  }
378
392
  else if (topic.startsWith(this.mqttTopic + '/bridge/info')) {
379
- const data = this.tryJsonParse(payload.toString());
380
- this.z2mPermitJoin = data.permit_join ? data.permit_join : false;
381
- this.z2mPermitJoinTimeout = data.permit_join_timeout ? data.permit_join_timeout : 0;
382
- this.z2mVersion = data.version ? data.version : '';
383
- this.z2mIsAvailabilityEnabled = data.config.availability ? true : false;
393
+ this.z2mBridge = this.tryJsonParse(payload.toString());
394
+ this.z2mPermitJoin = this.z2mBridge.permit_join;
395
+ this.z2mPermitJoinTimeout = this.z2mBridge.permit_join_timeout;
396
+ this.z2mVersion = this.z2mBridge.version;
397
+ this.z2mIsAvailabilityEnabled = this.z2mBridge.config.availability !== undefined;
384
398
  this.log.debug(`Message bridge/info availability => ${this.z2mIsAvailabilityEnabled}`);
385
399
  this.log.debug(`Message bridge/info version => ${this.z2mVersion}`);
386
400
  this.log.debug(`Message bridge/info permit_join => ${this.z2mPermitJoin} timeout => ${this.z2mPermitJoinTimeout}`);
387
- this.log.debug(`Message bridge/info advanced.output => ${data.config.advanced.output}`);
388
- this.log.debug(`Message bridge/info advanced.legacy_api => ${data.config.advanced.legacy_api}`);
389
- this.log.debug(`Message bridge/info advanced.legacy_availability_payload => ${data.config.advanced.legacy_availability_payload}`);
390
- if (data.config.advanced.output === 'attribute')
391
- this.log.error(`Message bridge/info advanced.output must be 'json' or 'attribute_and_json'. Now is ${data.config.advanced.output}`);
392
- if (data.config.advanced.legacy_api === true)
393
- this.log.info(`Message bridge/info advanced.legacy_api is ${data.config.advanced.legacy_api}`);
394
- if (data.config.advanced.legacy_availability_payload === true)
395
- this.log.info(`Message bridge/info advanced.legacy_availability_payload is ${data.config.advanced.legacy_availability_payload}`);
396
- this.emit('info', this.z2mVersion, this.z2mIsAvailabilityEnabled, this.z2mPermitJoin, this.z2mPermitJoinTimeout);
397
- this.emit('bridge-info', data);
401
+ this.log.debug(`Message bridge/info advanced.output => ${this.z2mBridge.config.advanced.output}`);
402
+ this.log.debug(`Message bridge/info advanced.legacy_api => ${this.z2mBridge.config.advanced.legacy_api}`);
403
+ this.log.debug(`Message bridge/info advanced.legacy_availability_payload => ${this.z2mBridge.config.advanced.legacy_availability_payload}`);
404
+ if (this.z2mBridge.config.advanced.output === 'attribute')
405
+ this.log.error(`Message bridge/info advanced.output must be 'json' or 'attribute_and_json'. Now is ${this.z2mBridge.config.advanced.output}`);
406
+ if (this.z2mBridge.config.advanced.legacy_api === true)
407
+ this.log.info(`Message bridge/info advanced.legacy_api is ${this.z2mBridge.config.advanced.legacy_api}`);
408
+ if (this.z2mBridge.config.advanced.legacy_availability_payload === true)
409
+ this.log.info(`Message bridge/info advanced.legacy_availability_payload is ${this.z2mBridge.config.advanced.legacy_availability_payload}`);
410
+ this.emit('bridge-info', this.z2mBridge);
398
411
  if (this.log.logLevel === "debug")
399
412
  this.writeBufferJSON('bridge-info', payload);
400
413
  }
401
414
  else if (topic.startsWith(this.mqttTopic + '/bridge/devices')) {
402
- this.z2mDevices.splice(0, this.z2mDevices.length);
403
- const devices = this.tryJsonParse(payload.toString());
404
- const data = this.tryJsonParse(payload.toString());
405
415
  if (this.log.logLevel === "debug")
406
416
  this.writeBufferJSON('bridge-devices', payload);
407
- this.emit('bridge-devices', data);
408
- let index = 1;
409
- for (const device of devices) {
410
- if (device.type === 'Coordinator' && device.supported === true && device.disabled === false && device.interview_completed === true && device.interviewing === false) {
411
- const z2m = {
412
- logName: 'Coordinator',
413
- index: 0,
414
- ieee_address: device.ieee_address,
415
- friendly_name: device.friendly_name,
416
- getPayload: undefined,
417
- description: '',
418
- manufacturer: '',
419
- model_id: '',
420
- vendor: 'zigbee2MQTT',
421
- model: 'coordinator',
422
- date_code: '',
423
- software_build_id: '',
424
- power_source: 'Mains (single phase)',
425
- isAvailabilityEnabled: false,
426
- isOnline: false,
427
- category: '',
428
- hasEndpoints: false,
429
- exposes: [],
430
- options: [],
431
- endpoints: [],
432
- };
433
- this.z2mDevices.push(z2m);
434
- }
435
- if (device.type !== 'Coordinator' && device.supported === true && device.disabled === false && device.interview_completed === true && device.interviewing === false) {
436
- const z2m = {
437
- logName: 'Dev#' + index.toString().padStart(2, '0'),
438
- index: index++,
439
- ieee_address: device.ieee_address,
440
- friendly_name: device.friendly_name,
441
- getPayload: undefined,
442
- description: device.definition.description || '',
443
- manufacturer: device.manufacturer || '',
444
- model_id: device.model_id || '',
445
- vendor: device.definition.vendor || '',
446
- model: device.definition.model || '',
447
- date_code: device.date_code || '',
448
- software_build_id: device.software_build_id || '',
449
- power_source: device.power_source,
450
- isAvailabilityEnabled: false,
451
- isOnline: false,
452
- category: '',
453
- hasEndpoints: false,
454
- exposes: [],
455
- options: [],
456
- endpoints: [],
457
- };
458
- for (const expose of device.definition.exposes) {
459
- if (!expose.property && !expose.name && expose.features && expose.type) {
460
- if (z2m.category === '') {
461
- z2m.category = expose.type;
462
- }
463
- for (const feature of expose.features) {
464
- feature.category = expose.type;
465
- z2m.exposes.push(feature);
466
- if (feature.endpoint) {
467
- z2m.hasEndpoints = true;
468
- }
469
- }
470
- }
471
- else {
472
- expose.category = '';
473
- z2m.exposes.push(expose);
474
- }
475
- }
476
- for (const option of device.definition.options) {
477
- const feature = option;
478
- z2m.options.push(feature);
479
- }
480
- for (const key in device.endpoints) {
481
- const endpoint = device.endpoints[key];
482
- const endpointWithKey = {
483
- ...endpoint,
484
- endpoint: key,
485
- };
486
- z2m.endpoints.push(endpointWithKey);
487
- }
488
- this.z2mDevices.push(z2m);
489
- }
490
- }
491
- this.log.debug(`Received ${this.z2mDevices.length} devices`);
492
- this.emit('devices');
417
+ this.z2mDevices = this.tryJsonParse(payload.toString());
418
+ this.emit('bridge-devices', this.z2mDevices);
493
419
  }
494
420
  else if (topic.startsWith(this.mqttTopic + '/bridge/groups')) {
495
- this.z2mGroups.splice(0, this.z2mGroups.length);
496
- const groups = this.tryJsonParse(payload.toString());
497
- const data = this.tryJsonParse(payload.toString());
498
421
  if (this.log.logLevel === "debug")
499
422
  this.writeBufferJSON('bridge-groups', payload);
500
- this.emit('bridge-groups', data);
501
- let index = 1;
502
- for (const group of groups) {
503
- const z2m = {
504
- logName: 'Grp#' + index.toString().padStart(2, '0'),
505
- index: index++,
506
- id: group.id,
507
- friendly_name: group.friendly_name,
508
- getPayload: undefined,
509
- isAvailabilityEnabled: false,
510
- isOnline: false,
511
- members: [],
512
- scenes: [],
513
- };
514
- for (const member of group.members) {
515
- z2m.members.push(member);
516
- }
517
- for (const scene of group.scenes) {
518
- z2m.scenes.push(scene);
519
- }
520
- this.z2mGroups.push(z2m);
521
- }
522
- this.log.debug(`Received ${this.z2mGroups.length} groups`);
523
- this.emit('groups');
423
+ this.z2mGroups = this.tryJsonParse(payload.toString());
424
+ this.emit('bridge-groups', this.z2mGroups);
524
425
  }
525
426
  else if (topic.startsWith(this.mqttTopic + '/bridge/extensions')) {
526
427
  const extensions = this.tryJsonParse(payload.toString());
@@ -610,43 +511,32 @@ export class Zigbee2MQTT extends EventEmitter {
610
511
  }
611
512
  return;
612
513
  }
613
- if (this.log.logLevel === "debug" && this.loggedEntries < 1000) {
514
+ if (this.log.logLevel === "debug" && this.loggedBridgePayloads < 10000) {
614
515
  const logEntry = {
615
516
  entity,
616
517
  service,
617
518
  payload: payload.toString(),
618
519
  };
619
520
  const filePath = path.join(this.mqttDataPath, 'bridge-payloads.txt');
620
- fs.appendFileSync(filePath, JSON.stringify(logEntry) + '\n');
621
- this.loggedEntries++;
521
+ fs.appendFileSync(filePath, `${new Date().toLocaleString()} - ` + JSON.stringify(logEntry).replaceAll('\\"', '"') + '\n');
522
+ this.loggedBridgePayloads++;
622
523
  }
623
- const foundDevice = this.z2mDevices.findIndex((device) => device.ieee_address === entity || device.friendly_name === entity);
624
- if (foundDevice !== -1) {
524
+ const foundDevice = this.z2mDevices.find((device) => device.ieee_address === entity || device.friendly_name === entity);
525
+ if (foundDevice) {
625
526
  this.handleDeviceMessage(foundDevice, entity, service, payload);
626
527
  }
627
528
  else {
628
- const foundGroup = this.z2mGroups.findIndex((group) => group.friendly_name === entity);
629
- if (foundGroup !== -1) {
529
+ const foundGroup = this.z2mGroups.find((group) => group.friendly_name === entity);
530
+ if (foundGroup) {
630
531
  this.handleGroupMessage(foundGroup, entity, service, payload);
631
532
  }
632
533
  else {
633
- try {
634
- this.log.debug('Message for ***unknown*** entity:', entity, 'service:', service, 'payload:', payload);
635
- }
636
- catch {
637
- this.log.debug('Message for ***unknown*** entity:', entity, 'service:', service, 'payload: error');
638
- }
534
+ this.log.debug('Message for ***unknown*** entity:', entity, 'service:', service, 'payload:', payload);
639
535
  }
640
536
  }
641
537
  }
642
538
  }
643
- getDevice(name) {
644
- return this.z2mDevices.find((device) => device.ieee_address === name || device.friendly_name === name);
645
- }
646
- getGroup(name) {
647
- return this.z2mGroups.find((group) => group.friendly_name === name);
648
- }
649
- handleDeviceMessage(deviceIndex, entity, service, payload) {
539
+ handleDeviceMessage(device, entity, service, payload) {
650
540
  if (payload.length === 0 || payload === null) {
651
541
  return;
652
542
  }
@@ -660,13 +550,10 @@ export class Zigbee2MQTT extends EventEmitter {
660
550
  }
661
551
  if (service === 'availability') {
662
552
  if (data.state === 'online') {
663
- this.z2mDevices[deviceIndex].isAvailabilityEnabled = true;
664
- this.z2mDevices[deviceIndex].isOnline = true;
665
553
  this.emit('availability', entity, true);
666
554
  this.emit('ONLINE-' + entity);
667
555
  }
668
556
  else if (data.state === 'offline') {
669
- this.z2mDevices[deviceIndex].isOnline = false;
670
557
  this.emit('availability', entity, false);
671
558
  this.emit('OFFLINE-' + entity);
672
559
  }
@@ -682,7 +569,7 @@ export class Zigbee2MQTT extends EventEmitter {
682
569
  else {
683
570
  }
684
571
  }
685
- handleGroupMessage(groupIndex, entity, service, payload) {
572
+ handleGroupMessage(group, entity, service, payload) {
686
573
  if (payload.length === 0 || payload === null) {
687
574
  return;
688
575
  }
@@ -697,13 +584,10 @@ export class Zigbee2MQTT extends EventEmitter {
697
584
  data['last_seen'] = new Date().toISOString();
698
585
  if (service === 'availability') {
699
586
  if (data.state === 'online') {
700
- this.z2mGroups[groupIndex].isAvailabilityEnabled = true;
701
- this.z2mGroups[groupIndex].isOnline = true;
702
587
  this.emit('availability', entity, true);
703
588
  this.emit('ONLINE-' + entity);
704
589
  }
705
590
  else if (data.state === 'offline') {
706
- this.z2mGroups[groupIndex].isOnline = false;
707
591
  this.emit('availability', entity, false);
708
592
  this.emit('OFFLINE-' + entity);
709
593
  }
@@ -921,115 +805,4 @@ export class Zigbee2MQTT extends EventEmitter {
921
805
  emitPayload(entity, data) {
922
806
  this.emit('MESSAGE-' + entity, data);
923
807
  }
924
- printDevice(device) {
925
- this.log.debug(`Device - ${dn}${device.friendly_name}${rs}`);
926
- this.log.debug(`IEEE Address: ${device.ieee_address}`);
927
- this.log.debug(`Description: ${device.description}`);
928
- this.log.debug(`Manufacturer: ${device.manufacturer}`);
929
- this.log.debug(`Model ID: ${device.model_id}`);
930
- this.log.debug(`Date Code: ${device.date_code}`);
931
- this.log.debug(`Software Build ID: ${device.software_build_id}`);
932
- this.log.debug(`Power Source: ${device.power_source}`);
933
- this.log.debug(`Availability Enabled: ${device.isAvailabilityEnabled}`);
934
- this.log.debug(`Online: ${device.isOnline}`);
935
- this.log.debug(`Type: ${device.category}`);
936
- const printFeatures = (features, featureType) => {
937
- this.log.debug(`${featureType}:`);
938
- features.forEach((feature) => {
939
- this.log.debug(` Name: ${zb}${feature.name}${rs}`);
940
- this.log.debug(` Description: ${feature.description}`);
941
- this.log.debug(` Property: ${zb}${feature.property}${rs}`);
942
- this.log.debug(` Type: ${feature.type}`);
943
- this.log.debug(` Access: ${feature.access}`);
944
- if (feature.endpoint) {
945
- this.log.debug(` Endpoint: ${feature.endpoint}`);
946
- }
947
- if (feature.unit) {
948
- this.log.debug(` Unit: ${feature.unit}`);
949
- }
950
- if (feature.value_max) {
951
- this.log.debug(` Value Max: ${feature.value_max}`);
952
- }
953
- if (feature.value_min) {
954
- this.log.debug(` Value Min: ${feature.value_min}`);
955
- }
956
- if (feature.value_step) {
957
- this.log.debug(` Value Step: ${feature.value_step}`);
958
- }
959
- if (feature.value_on) {
960
- this.log.debug(` Value On: ${feature.value_on}`);
961
- }
962
- if (feature.value_off) {
963
- this.log.debug(` Value Off: ${feature.value_off}`);
964
- }
965
- if (feature.value_toggle) {
966
- this.log.debug(` Value Toggle: ${feature.value_toggle}`);
967
- }
968
- if (feature.values) {
969
- this.log.debug(` Values: ${feature.values.join(', ')}`);
970
- }
971
- if (feature.presets) {
972
- this.log.debug(` Presets: ${feature.presets.join(', ')}`);
973
- }
974
- this.log.debug('');
975
- });
976
- };
977
- const printEndpoints = (endpoints) => {
978
- endpoints.forEach((endpoint) => {
979
- this.log.debug(`--Endpoint ${endpoint.endpoint}`);
980
- endpoint.bindings.forEach((binding) => {
981
- this.log.debug(`----Bindings: ${binding.cluster}`, binding.target);
982
- });
983
- endpoint.clusters.input.forEach((input) => {
984
- this.log.debug(`----Clusters input: ${input}`);
985
- });
986
- endpoint.clusters.output.forEach((output) => {
987
- this.log.debug(`----Clusters output: ${output}`);
988
- });
989
- endpoint.configured_reportings.forEach((reporting) => {
990
- this.log.debug(`----Reportings: ${reporting.attribute} ${reporting.cluster} ${reporting.minimum_report_interval} ${reporting.maximum_report_interval} ${reporting.reportable_change}`);
991
- });
992
- endpoint.scenes.forEach((scene) => {
993
- this.log.debug(`----Scenes: ID ${scene.id} Name ${scene.name}`);
994
- });
995
- this.log.debug('');
996
- });
997
- };
998
- printFeatures(device.exposes, 'Exposes');
999
- printFeatures(device.options, 'Options');
1000
- printEndpoints(device.endpoints);
1001
- this.log.debug('');
1002
- }
1003
- printDevices() {
1004
- this.z2mDevices.forEach((device) => {
1005
- this.printDevice(device);
1006
- });
1007
- }
1008
- printGroup(group) {
1009
- this.log.debug(`Group - ${dn}${group.friendly_name}${rs}`);
1010
- this.log.debug(`ID: ${group.id}`);
1011
- const printMembers = (members) => {
1012
- this.log.debug('Members:');
1013
- members.forEach((member) => {
1014
- this.log.debug(`--Endpoint ${member.endpoint}`);
1015
- this.log.debug(`--IEEE Address ${member.ieee_address}`);
1016
- });
1017
- };
1018
- printMembers(group.members);
1019
- const printScenes = (scenes) => {
1020
- this.log.debug('Scenes:');
1021
- scenes.forEach((scene) => {
1022
- this.log.debug(`--ID ${scene.id}`);
1023
- this.log.debug(`--Name ${scene.name}`);
1024
- });
1025
- };
1026
- printScenes(group.scenes);
1027
- this.log.debug(`Availability Enabled: ${group.isAvailabilityEnabled}`);
1028
- this.log.debug(`Online: ${group.isOnline}`);
1029
- }
1030
- printGroups() {
1031
- this.z2mGroups.forEach((group) => {
1032
- this.printGroup(group);
1033
- });
1034
- }
1035
808
  }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "matterbridge-zigbee2mqtt",
3
+ "type": "DynamicPlatform",
4
+ "version": "2.9.0",
5
+ "host": "mqtt://localhost",
6
+ "port": 1883,
7
+ "protocolVersion": 5,
8
+ "username": "",
9
+ "password": "",
10
+ "ca": "",
11
+ "rejectUnauthorized": true,
12
+ "cert": "",
13
+ "key": "",
14
+ "topic": "zigbee2mqtt",
15
+ "zigbeeFrontend": "http://localhost:8080",
16
+ "whiteList": [],
17
+ "blackList": [],
18
+ "switchList": [],
19
+ "lightList": [],
20
+ "outletList": [],
21
+ "featureBlackList": [],
22
+ "deviceFeatureBlackList": {},
23
+ "scenesType": "outlet",
24
+ "scenesPrefix": true,
25
+ "postfix": "",
26
+ "debug": false,
27
+ "unregisterOnShutdown": false
28
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-zigbee2mqtt",
3
- "version": "2.8.1",
3
+ "version": "3.0.0-dev-20251102-bcdd456",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge-zigbee2mqtt",
9
- "version": "2.8.1",
9
+ "version": "3.0.0-dev-20251102-bcdd456",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "moment": "2.30.1",
@@ -15,7 +15,7 @@
15
15
  "node-persist-manager": "2.0.0"
16
16
  },
17
17
  "engines": {
18
- "node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0 || >=24.0.0 <25.0.0"
18
+ "node": ">=20.0.0 <21.0.0 || >=22.0.0 <23.0.0 || >=24.0.0 <25.0.0"
19
19
  },
20
20
  "funding": {
21
21
  "type": "buymeacoffee",
@@ -32,18 +32,18 @@
32
32
  }
33
33
  },
34
34
  "node_modules/@types/node": {
35
- "version": "24.6.1",
36
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.1.tgz",
37
- "integrity": "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==",
35
+ "version": "24.9.2",
36
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
37
+ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
38
38
  "license": "MIT",
39
39
  "dependencies": {
40
- "undici-types": "~7.13.0"
40
+ "undici-types": "~7.16.0"
41
41
  }
42
42
  },
43
43
  "node_modules/@types/readable-stream": {
44
- "version": "4.0.21",
45
- "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.21.tgz",
46
- "integrity": "sha512-19eKVv9tugr03IgfXlA9UVUVRbW6IuqRO5B92Dl4a6pT7K8uaGrNS0GkxiZD0BOk6PLuXl5FhWl//eX/pzYdTQ==",
44
+ "version": "4.0.22",
45
+ "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.22.tgz",
46
+ "integrity": "sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==",
47
47
  "license": "MIT",
48
48
  "dependencies": {
49
49
  "@types/node": "*"
@@ -91,9 +91,9 @@
91
91
  "license": "MIT"
92
92
  },
93
93
  "node_modules/bl": {
94
- "version": "6.1.3",
95
- "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.3.tgz",
96
- "integrity": "sha512-nHB8B5roHlGX5TFsWeiQJijdddZIOHuv1eL2cM2kHnG3qR91CYLsysGe+CvxQfEd23EKD0eJf4lto0frTbddKA==",
94
+ "version": "6.1.4",
95
+ "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.4.tgz",
96
+ "integrity": "sha512-ZV/9asSuknOExbM/zPPA8z00lc1ihPKWaStHkkQrxHNeYx+yY+TmF+v80dpv2G0mv3HVXBu7ryoAsxbFFhf4eg==",
97
97
  "license": "MIT",
98
98
  "dependencies": {
99
99
  "@types/readable-stream": "^4.0.0",
@@ -530,9 +530,9 @@
530
530
  "license": "MIT"
531
531
  },
532
532
  "node_modules/undici-types": {
533
- "version": "7.13.0",
534
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz",
535
- "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==",
533
+ "version": "7.16.0",
534
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
535
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
536
536
  "license": "MIT"
537
537
  },
538
538
  "node_modules/util-deprecate": {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "matterbridge-zigbee2mqtt",
3
- "version": "2.8.1",
3
+ "version": "3.0.0-dev-20251102-bcdd456",
4
4
  "description": "Matterbridge zigbee2mqtt plugin",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",
7
7
  "type": "module",
8
- "main": "dist/index.js",
8
+ "main": "dist/module.js",
9
9
  "homepage": "https://www.npmjs.com/package/matterbridge-zigbee2mqtt",
10
10
  "repository": {
11
11
  "type": "git",
@@ -38,7 +38,7 @@
38
38
  "zigbee2mqtt"
39
39
  ],
40
40
  "engines": {
41
- "node": ">=18.0.0 <19.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0 || >=24.0.0 <25.0.0"
41
+ "node": ">=20.0.0 <21.0.0 || >=22.0.0 <23.0.0 || >=24.0.0 <25.0.0"
42
42
  },
43
43
  "dependencies": {
44
44
  "moment": "2.30.1",
package/dist/index.js DELETED
@@ -1,4 +0,0 @@
1
- import { ZigbeePlatform } from './platform.js';
2
- export default function initializePlugin(matterbridge, log, config) {
3
- return new ZigbeePlatform(matterbridge, log, config);
4
- }
@@ -1,231 +0,0 @@
1
- import { rmSync } from 'node:fs';
2
- import { inspect } from 'node:util';
3
- import path from 'node:path';
4
- import { jest } from '@jest/globals';
5
- import { DeviceTypeId, Endpoint, Environment, MdnsService, ServerNode, ServerNodeStore, VendorId, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel, Lifecycle, } from 'matterbridge/matter';
6
- import { RootEndpoint, AggregatorEndpoint } from 'matterbridge/matter/endpoints';
7
- import { AnsiLogger } from 'matterbridge/logger';
8
- export let loggerLogSpy;
9
- export let consoleLogSpy;
10
- export let consoleDebugSpy;
11
- export let consoleInfoSpy;
12
- export let consoleWarnSpy;
13
- export let consoleErrorSpy;
14
- export function setupTest(name, debug = false) {
15
- expect(name).toBeDefined();
16
- expect(typeof name).toBe('string');
17
- expect(name.length).toBeGreaterThanOrEqual(4);
18
- rmSync(path.join('jest', name), { recursive: true, force: true });
19
- if (debug) {
20
- loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
21
- consoleLogSpy = jest.spyOn(console, 'log');
22
- consoleDebugSpy = jest.spyOn(console, 'debug');
23
- consoleInfoSpy = jest.spyOn(console, 'info');
24
- consoleWarnSpy = jest.spyOn(console, 'warn');
25
- consoleErrorSpy = jest.spyOn(console, 'error');
26
- }
27
- else {
28
- loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
29
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
30
- consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
31
- consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
32
- consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
33
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
34
- }
35
- }
36
- export function setDebug(debug) {
37
- if (debug) {
38
- loggerLogSpy.mockRestore();
39
- consoleLogSpy.mockRestore();
40
- consoleDebugSpy.mockRestore();
41
- consoleInfoSpy.mockRestore();
42
- consoleWarnSpy.mockRestore();
43
- consoleErrorSpy.mockRestore();
44
- loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
45
- consoleLogSpy = jest.spyOn(console, 'log');
46
- consoleDebugSpy = jest.spyOn(console, 'debug');
47
- consoleInfoSpy = jest.spyOn(console, 'info');
48
- consoleWarnSpy = jest.spyOn(console, 'warn');
49
- consoleErrorSpy = jest.spyOn(console, 'error');
50
- }
51
- else {
52
- loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
53
- consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
54
- consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
55
- consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
56
- consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
57
- consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
58
- }
59
- }
60
- export function createTestEnvironment(homeDir) {
61
- expect(homeDir).toBeDefined();
62
- expect(typeof homeDir).toBe('string');
63
- expect(homeDir.length).toBeGreaterThanOrEqual(4);
64
- rmSync(homeDir, { recursive: true, force: true });
65
- const environment = Environment.default;
66
- environment.vars.set('log.level', MatterLogLevel.DEBUG);
67
- environment.vars.set('log.format', MatterLogFormat.ANSI);
68
- environment.vars.set('path.root', homeDir);
69
- environment.vars.set('runtime.signals', false);
70
- environment.vars.set('runtime.exitcode', false);
71
- return environment;
72
- }
73
- export async function flushAsync(ticks = 3, microTurns = 10, pause = 100) {
74
- for (let i = 0; i < ticks; i++)
75
- await new Promise((resolve) => setImmediate(resolve));
76
- for (let i = 0; i < microTurns; i++)
77
- await Promise.resolve();
78
- if (pause)
79
- await new Promise((resolve) => setTimeout(resolve, pause));
80
- }
81
- export async function flushAllEndpointNumberPersistence(targetServer, rounds = 2) {
82
- const nodeStore = targetServer.env.get(ServerNodeStore);
83
- for (let i = 0; i < rounds; i++) {
84
- await new Promise((resolve) => setImmediate(resolve));
85
- await nodeStore.endpointStores.close();
86
- }
87
- }
88
- function collectAllEndpoints(root) {
89
- const list = [];
90
- const walk = (ep) => {
91
- list.push(ep);
92
- if (ep.parts) {
93
- for (const child of ep.parts) {
94
- walk(child);
95
- }
96
- }
97
- };
98
- walk(root);
99
- return list;
100
- }
101
- export async function assertAllEndpointNumbersPersisted(targetServer) {
102
- const nodeStore = targetServer.env.get(ServerNodeStore);
103
- await nodeStore.endpointStores.close();
104
- const all = collectAllEndpoints(targetServer);
105
- for (const ep of all) {
106
- const store = nodeStore.storeForEndpoint(ep);
107
- if (ep.maybeNumber === 0) {
108
- expect(store.number ?? 0).toBe(0);
109
- }
110
- else {
111
- expect(store.number).toBeGreaterThan(0);
112
- }
113
- }
114
- return all.length;
115
- }
116
- export async function startServerNode(name, port) {
117
- const server = await ServerNode.create({
118
- id: name + 'ServerNode',
119
- productDescription: {
120
- name: name + 'ServerNode',
121
- deviceType: DeviceTypeId(RootEndpoint.deviceType),
122
- vendorId: VendorId(0xfff1),
123
- productId: 0x8000,
124
- },
125
- basicInformation: {
126
- vendorId: VendorId(0xfff1),
127
- vendorName: 'Matterbridge',
128
- productId: 0x8000,
129
- productName: 'Matterbridge ' + name,
130
- nodeLabel: name + 'ServerNode',
131
- hardwareVersion: 1,
132
- softwareVersion: 1,
133
- reachable: true,
134
- },
135
- network: {
136
- port,
137
- },
138
- });
139
- expect(server).toBeDefined();
140
- expect(server.lifecycle.isReady).toBeTruthy();
141
- const aggregator = new Endpoint(AggregatorEndpoint, {
142
- id: name + 'AggregatorNode',
143
- });
144
- expect(aggregator).toBeDefined();
145
- await server.add(aggregator);
146
- expect(server.parts.has(aggregator.id)).toBeTruthy();
147
- expect(server.parts.has(aggregator)).toBeTruthy();
148
- expect(aggregator.lifecycle.isReady).toBeTruthy();
149
- expect(server.lifecycle.isOnline).toBeFalsy();
150
- await new Promise((resolve) => {
151
- server.lifecycle.online.on(async () => {
152
- resolve();
153
- });
154
- server.start();
155
- });
156
- expect(server.lifecycle.isReady).toBeTruthy();
157
- expect(server.lifecycle.isOnline).toBeTruthy();
158
- expect(server.lifecycle.isCommissioned).toBeFalsy();
159
- expect(server.lifecycle.isPartsReady).toBeTruthy();
160
- expect(server.lifecycle.hasId).toBeTruthy();
161
- expect(server.lifecycle.hasNumber).toBeTruthy();
162
- expect(aggregator.lifecycle.isReady).toBeTruthy();
163
- expect(aggregator.lifecycle.isInstalled).toBeTruthy();
164
- expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
165
- expect(aggregator.lifecycle.hasId).toBeTruthy();
166
- expect(aggregator.lifecycle.hasNumber).toBeTruthy();
167
- await flushAsync(undefined, undefined, 200);
168
- return [server, aggregator];
169
- }
170
- export async function stopServerNode(server) {
171
- await flushAllEndpointNumberPersistence(server);
172
- await assertAllEndpointNumbersPersisted(server);
173
- expect(server).toBeDefined();
174
- expect(server.lifecycle.isReady).toBeTruthy();
175
- expect(server.lifecycle.isOnline).toBeTruthy();
176
- await server.close();
177
- expect(server.lifecycle.isReady).toBeTruthy();
178
- expect(server.lifecycle.isOnline).toBeFalsy();
179
- await server.env.get(MdnsService)[Symbol.asyncDispose]();
180
- await flushAsync(undefined, undefined, 200);
181
- }
182
- export async function addDevice(owner, device, pause = 10) {
183
- expect(owner).toBeDefined();
184
- expect(device).toBeDefined();
185
- expect(owner.lifecycle.isReady).toBeTruthy();
186
- expect(owner.construction.status).toBe(Lifecycle.Status.Active);
187
- expect(owner.lifecycle.isPartsReady).toBeTruthy();
188
- try {
189
- await owner.add(device);
190
- }
191
- catch (error) {
192
- const errorMessage = error instanceof Error ? error.message : error;
193
- const errorInspect = inspect(error, { depth: 10 });
194
- console.error(`Error adding device ${device.maybeId}.${device.maybeNumber}: ${errorMessage}\nstack: ${errorInspect}`);
195
- return false;
196
- }
197
- expect(owner.parts.has(device)).toBeTruthy();
198
- expect(owner.lifecycle.isPartsReady).toBeTruthy();
199
- expect(device.lifecycle.isReady).toBeTruthy();
200
- expect(device.lifecycle.isInstalled).toBeTruthy();
201
- expect(device.lifecycle.hasId).toBeTruthy();
202
- expect(device.lifecycle.hasNumber).toBeTruthy();
203
- expect(device.construction.status).toBe(Lifecycle.Status.Active);
204
- await flushAsync(1, 1, pause);
205
- return true;
206
- }
207
- export async function deleteDevice(owner, device, pause = 10) {
208
- expect(owner).toBeDefined();
209
- expect(device).toBeDefined();
210
- expect(owner.lifecycle.isReady).toBeTruthy();
211
- expect(owner.construction.status).toBe(Lifecycle.Status.Active);
212
- expect(owner.lifecycle.isPartsReady).toBeTruthy();
213
- try {
214
- await device.delete();
215
- }
216
- catch (error) {
217
- const errorMessage = error instanceof Error ? error.message : error;
218
- const errorInspect = inspect(error, { depth: 10 });
219
- console.error(`Error deleting device ${device.maybeId}.${device.maybeNumber}: ${errorMessage}\nstack: ${errorInspect}`);
220
- return false;
221
- }
222
- expect(owner.parts.has(device)).toBeFalsy();
223
- expect(owner.lifecycle.isPartsReady).toBeTruthy();
224
- expect(device.lifecycle.isReady).toBeFalsy();
225
- expect(device.lifecycle.isInstalled).toBeFalsy();
226
- expect(device.lifecycle.hasId).toBeTruthy();
227
- expect(device.lifecycle.hasNumber).toBeTruthy();
228
- expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
229
- await flushAsync(1, 1, pause);
230
- return true;
231
- }