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 +30 -0
- package/dist/entity.js +42 -31
- package/dist/{platform.js → module.js} +11 -10
- package/dist/zigbee2mqtt.js +51 -278
- package/matterbridge-zigbee2mqtt.config.json +28 -0
- package/npm-shrinkwrap.json +16 -16
- package/package.json +3 -3
- package/dist/index.js +0 -4
- package/dist/jestHelpers.js +0 -231
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
330
|
-
this.
|
|
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(
|
|
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
|
-
|
|
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,
|
|
344
|
-
this.cachePublish('toggle', { ['state' + (
|
|
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' + (
|
|
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(
|
|
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
|
-
|
|
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(
|
|
366
|
-
this.cachePublish('moveToLevelWithOnOff', { ['state' + (
|
|
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(
|
|
371
|
-
this.cachePayload['state' + (
|
|
372
|
-
this.setCachePublishAttributes(data.endpoint,
|
|
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' + (
|
|
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' + (
|
|
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' + (
|
|
390
|
-
this.log.debug(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
43
|
-
throw new Error(`This plugin requires Matterbridge version >= "3.0
|
|
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,
|
|
100
|
-
this.z2m.setLogDebug(
|
|
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
|
});
|
package/dist/zigbee2mqtt.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
this.z2mPermitJoin =
|
|
381
|
-
this.z2mPermitJoinTimeout =
|
|
382
|
-
this.z2mVersion =
|
|
383
|
-
this.z2mIsAvailabilityEnabled =
|
|
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 => ${
|
|
388
|
-
this.log.debug(`Message bridge/info advanced.legacy_api => ${
|
|
389
|
-
this.log.debug(`Message bridge/info advanced.legacy_availability_payload => ${
|
|
390
|
-
if (
|
|
391
|
-
this.log.error(`Message bridge/info advanced.output must be 'json' or 'attribute_and_json'. Now is ${
|
|
392
|
-
if (
|
|
393
|
-
this.log.info(`Message bridge/info advanced.legacy_api is ${
|
|
394
|
-
if (
|
|
395
|
-
this.log.info(`Message bridge/info advanced.legacy_availability_payload is ${
|
|
396
|
-
this.emit('info', this.
|
|
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.
|
|
408
|
-
|
|
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.
|
|
501
|
-
|
|
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.
|
|
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.
|
|
521
|
+
fs.appendFileSync(filePath, `${new Date().toLocaleString()} - ` + JSON.stringify(logEntry).replaceAll('\\"', '"') + '\n');
|
|
522
|
+
this.loggedBridgePayloads++;
|
|
622
523
|
}
|
|
623
|
-
const foundDevice = this.z2mDevices.
|
|
624
|
-
if (foundDevice
|
|
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.
|
|
629
|
-
if (foundGroup
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "matterbridge-zigbee2mqtt",
|
|
3
|
-
"version": "
|
|
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": "
|
|
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
|
+
"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.
|
|
36
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.
|
|
37
|
-
"integrity": "sha512-
|
|
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.
|
|
40
|
+
"undici-types": "~7.16.0"
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"node_modules/@types/readable-stream": {
|
|
44
|
-
"version": "4.0.
|
|
45
|
-
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.
|
|
46
|
-
"integrity": "sha512
|
|
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.
|
|
95
|
-
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.
|
|
96
|
-
"integrity": "sha512-
|
|
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.
|
|
534
|
-
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.
|
|
535
|
-
"integrity": "sha512-
|
|
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": "
|
|
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/
|
|
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": ">=
|
|
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
package/dist/jestHelpers.js
DELETED
|
@@ -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
|
-
}
|