matterbridge-zigbee2mqtt 2.8.1 → 2.9.0-dev-20251101-be5ef3b

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,34 @@ 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
+ ## [2.9.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.1.0.0.
21
+ - [package]: Bumped package to automator v.2.0.10.
22
+ - [jest]: Bumped jestHelpers to v.1.0.11.
23
+ - [package]: Require matterbridge v.3.3.0.
24
+ - [package]: Added default config.
25
+ - [package]: Added typed ZigbeePlatformConfig.
26
+ - [platform]: Updated to new signature PlatformMatterbridge.
27
+ - [workflows]: Improved speed on Node CI.
28
+ - [devcontainer]: Added the plugin name to the container.
29
+ - [devcontainer]: Improved performance of first build with shallow clone.
30
+
31
+ ### Fixed
32
+
33
+ - [platform]: Fixed specific zbminir2 device case for all devices. Thanks subst4nc3 (https://github.com/Luligu/matterbridge-zigbee2mqtt/issues/126).
34
+
35
+ <a href="https://www.buymeacoffee.com/luligugithub">
36
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
37
+ </a>
38
+
11
39
  ## [2.8.1] - 2025-10-02
12
40
 
13
41
  ### Automations and scenes
package/dist/entity.js CHANGED
@@ -326,8 +326,9 @@ export class ZigbeeEntity extends EventEmitter {
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) {
@@ -335,16 +336,18 @@ export class ZigbeeEntity extends EventEmitter {
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) {
@@ -353,25 +356,27 @@ export class ZigbeeEntity extends EventEmitter {
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
370
  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);
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
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`);
371
- this.cachePayload['state' + (this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '')] = 'ON';
372
- this.setCachePublishAttributes(data.endpoint, this.hasEndpoints ? '_' + data.endpoint.uniqueStorageKey : '');
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,12 +386,13 @@ 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);
395
+ this.cachePublish('moveToColorTemperature', { ['color' + (isChildEndpoint ? '_' + data.endpoint.uniqueStorageKey : '')]: { r: rgb.r, g: rgb.g, b: rgb.b } }, data.request.transitionTime);
390
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
  }
@@ -397,7 +403,8 @@ export class ZigbeeEntity extends EventEmitter {
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'];
@@ -406,7 +413,8 @@ export class ZigbeeEntity extends EventEmitter {
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'];
@@ -415,7 +423,8 @@ export class ZigbeeEntity extends EventEmitter {
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'];
@@ -424,7 +433,8 @@ export class ZigbeeEntity extends EventEmitter {
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 = [];
@@ -39,8 +42,8 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
39
42
  availabilityTimer;
40
43
  constructor(matterbridge, log, config) {
41
44
  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."`);
45
+ if (this.verifyMatterbridgeVersion === undefined || typeof this.verifyMatterbridgeVersion !== 'function' || !this.verifyMatterbridgeVersion('3.3.0')) {
46
+ 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
47
  }
45
48
  this.debugEnabled = config.debug;
46
49
  this.shouldStart = false;
@@ -81,8 +84,8 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
81
84
  config.port = this.mqttPort;
82
85
  config.protocolVersion = this.mqttProtocol;
83
86
  config.topic = this.mqttTopic;
84
- config.username = this.mqttUsername;
85
- config.password = this.mqttPassword;
87
+ config.username = this.mqttUsername ?? '';
88
+ config.password = this.mqttPassword ?? '';
86
89
  config.postfix = this.postfix;
87
90
  if (config.postfixHostname !== undefined)
88
91
  delete config.postfixHostname;
@@ -96,6 +99,7 @@ export class ZigbeePlatform extends MatterbridgeDynamicPlatform {
96
99
  config.scenesPrefix = true;
97
100
  this.log.info(`Initializing platform: ${CYAN}${this.config.name}${nf} version: ${CYAN}${this.config.version}${rs}`);
98
101
  this.log.info(`Loaded zigbee2mqtt parameters from ${CYAN}${path.join(matterbridge.matterbridgeDirectory, 'matterbridge-zigbee2mqtt.config.json')}${rs}`);
102
+ this.log.debug(`Config:\n${rs}${JSON.stringify(config, null, 2)}${rs}`);
99
103
  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
104
  this.z2m.setLogDebug(this.debugEnabled);
101
105
  this.z2m.setDataPath(path.join(matterbridge.matterbridgePluginDirectory, 'matterbridge-zigbee2mqtt'));
@@ -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": "2.9.0-dev-20251101-be5ef3b",
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": "2.9.0-dev-20251101-be5ef3b",
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": "2.9.0-dev-20251101-be5ef3b",
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
- }