matterbridge 3.3.7-dev-20251109-a306ab9 → 3.3.8-dev-20251114-9b65e59

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
@@ -28,6 +28,42 @@ Advantages:
28
28
  - individual plugin isolation in childbridge mode;
29
29
  - ability to update the plugin in childbridge mode without restarting matterbridge;
30
30
 
31
+ ## [3.3.8] - 2025-11-15
32
+
33
+ ### Development Breaking Changes
34
+
35
+ This will be the last release with the following long deprecated elements:
36
+
37
+ - [platform]: Matterbridge instead of PlatformMatterbridge in the platform constructor (deprecated since 3.0.0).
38
+ - [endpoint]: uniqueStorageKey instead of id in MatterbridgeEndpointOptions (deprecated since months).
39
+ - [endpoint]: endpointId instead of number in MatterbridgeEndpointOptions (deprecated since months).
40
+
41
+ So please update your plugin.
42
+
43
+ ### Added
44
+
45
+ - [endpoint]: Added matterbridgeEndpointTypes.
46
+ - [devices]: Added tests for device types and their revision changes.
47
+ - [clusters]: Added test for clusters and their revision changes.
48
+ - [chip]: Added fetch script to download zcl data from connectedhomeip.
49
+ - [endpoint]: Added createDefaultPowerSourceBatteryClusterServer(). Add Power Source cluster for a generic battery device.
50
+ - [platform]: Added setSchema() to temporarly set the schema for the config editor.
51
+ - [platform]: Added getSchema() to retrieve the schema from the Matterbridge plugin manager.
52
+ - [platform]: Added return value to registerVirtualDevice().
53
+
54
+ ### Changed
55
+
56
+ - [package]: Updated dependencies.
57
+ - [package]: Bumped jestHelpers v.1.0.12.
58
+ - [endpoint]: Changed long deprecated uniqueStorageKey with id in MatterbridgeEndpointOptions.
59
+ - [endpoint]: Changed long deprecated endpointId with number in MatterbridgeEndpointOptions.
60
+ - [endpoint]: Added property originalId in MatterbridgeEndpoint to store the original id passed in MatterbridgeEndpointOptions (since it can be changed for matter.js storage compatibility).
61
+ - [endpoint]: Changed logger level of single device classes.
62
+
63
+ <a href="https://www.buymeacoffee.com/luligugithub">
64
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
65
+ </a>
66
+
31
67
  ## [3.3.7] - 2025-11-08
32
68
 
33
69
  ### Breaking Changes
@@ -41,6 +77,7 @@ Advantages:
41
77
 
42
78
  ### Changed
43
79
 
80
+ - [package]: Updated dependencies.
44
81
  - [frontend]: Bumped `frontend` version to 3.3.1.
45
82
  - [PluginManager]: Bumped `PluginManager` version to 1.3.0.
46
83
  - [DeviceManager]: Bumped `DeviceManager` version to 1.1.0.
@@ -162,23 +162,23 @@ sudo systemctl stop matterbridge
162
162
  ### Show Matterbridge status
163
163
 
164
164
  ```bash
165
- sudo systemctl status matterbridge.service
165
+ sudo systemctl status matterbridge
166
166
  ```
167
167
 
168
168
  ### Enable Matterbridge to start automatically on boot
169
169
 
170
170
  ```bash
171
- sudo systemctl enable matterbridge.service
171
+ sudo systemctl enable matterbridge
172
172
  ```
173
173
 
174
174
  ### Disable Matterbridge from starting automatically on boot
175
175
 
176
176
  ```bash
177
- sudo systemctl disable matterbridge.service
177
+ sudo systemctl disable matterbridge
178
178
  ```
179
179
 
180
180
  ### View the log of Matterbridge in real time (this will show the log with colors)
181
181
 
182
182
  ```bash
183
- sudo journalctl -u matterbridge.service -n 1000 -f --output cat
183
+ sudo journalctl -u matterbridge -n 1000 -f --output cat
184
184
  ```
package/README.md CHANGED
@@ -354,6 +354,10 @@ The plugin uses local connection to Daikin Wifi modules. The plugin does not wor
354
354
 
355
355
  Matterbridge Roborock Platform Plugin is a dynamic platform plugin for Matterbridge that integrates Roborock vacuums into the Matter ecosystem, enabling control via Apple Home and other Matter-compatible apps.
356
356
 
357
+ ### [Wyze Robot Vacuum](https://github.com/RMCob/matterbridge-wyze-robovac)
358
+
359
+ Matterbridge plugin to control and report status of the Wyze S200 Robot Vacuum
360
+
357
361
  ## How to install and add a plugin with the frontend (best option)
358
362
 
359
363
  Just open the frontend on the link provided in the log, select a plugin and click install.
@@ -5,7 +5,7 @@ import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
5
5
  export class AirConditioner extends MatterbridgeEndpoint {
6
6
  constructor(name, serial, options = {}) {
7
7
  const { localTemperature = 23, occupiedHeatingSetpoint = 21, occupiedCoolingSetpoint = 25, minSetpointDeadBand = 1, minHeatSetpointLimit = 0, maxHeatSetpointLimit = 50, minCoolSetpointLimit = 0, maxCoolSetpointLimit = 50, temperatureDisplayMode = ThermostatUserInterfaceConfiguration.TemperatureDisplayMode.Celsius, keypadLockout = ThermostatUserInterfaceConfiguration.KeypadLockout.NoLockout, scheduleProgrammingVisibility = ThermostatUserInterfaceConfiguration.ScheduleProgrammingVisibility.ScheduleProgrammingPermitted, fanMode = FanControl.FanMode.Off, fanModeSequence = FanControl.FanModeSequence.OffLowMedHighAuto, percentSetting = 0, percentCurrent = 0, } = options;
8
- super([airConditioner, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
8
+ super([airConditioner, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
9
9
  this.createDefaultIdentifyClusterServer();
10
10
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Air Conditioner');
11
11
  this.createDefaultPowerSourceWiredClusterServer();
@@ -5,7 +5,7 @@ import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
5
5
  import { deviceEnergyManagement, electricalSensor, batteryStorage, powerSource } from '../matterbridgeDeviceTypes.js';
6
6
  export class BatteryStorage extends MatterbridgeEndpoint {
7
7
  constructor(name, serial, batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, voltage = null, current = null, power = null, energyImported = null, energyExported = null, absMinPower = 0, absMaxPower = 0) {
8
- super([batteryStorage, electricalSensor, deviceEnergyManagement], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
8
+ super([batteryStorage, electricalSensor, deviceEnergyManagement], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
9
9
  this.createDefaultIdentifyClusterServer()
10
10
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Solar Power')
11
11
  .createDefaultPowerTopologyClusterServer()
@@ -3,7 +3,7 @@ import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
3
3
  import { createLevelTemperatureControlClusterServer } from './temperatureControl.js';
4
4
  export class Cooktop extends MatterbridgeEndpoint {
5
5
  constructor(name, serial) {
6
- super([cooktop, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
6
+ super([cooktop, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
7
7
  this.createDefaultIdentifyClusterServer();
8
8
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Cooktop');
9
9
  this.createDefaultPowerSourceWiredClusterServer();
@@ -8,7 +8,7 @@ import { MatterbridgeOnOffServer, MatterbridgeServer } from '../matterbridgeBeha
8
8
  import { createLevelTemperatureControlClusterServer, createNumberTemperatureControlClusterServer } from './temperatureControl.js';
9
9
  export class Dishwasher extends MatterbridgeEndpoint {
10
10
  constructor(name, serial, currentMode, supportedModes, selectedTemperatureLevel, supportedTemperatureLevels, temperatureSetpoint, minTemperature, maxTemperature, step, operationalState) {
11
- super([dishwasher, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
11
+ super([dishwasher, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
12
12
  this.createDefaultIdentifyClusterServer();
13
13
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Dishwasher');
14
14
  this.createDefaultPowerSourceWiredClusterServer();
@@ -9,7 +9,7 @@ import { MatterbridgeServer } from '../matterbridgeBehaviors.js';
9
9
  import { deviceEnergyManagement, electricalSensor, evse, powerSource } from '../matterbridgeDeviceTypes.js';
10
10
  export class Evse extends MatterbridgeEndpoint {
11
11
  constructor(name, serial, currentMode, supportedModes, state, supplyState, faultState, voltage = null, current = null, power = null, energy = null, absMinPower, absMaxPower) {
12
- super([evse, powerSource, electricalSensor, deviceEnergyManagement], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
12
+ super([evse, powerSource, electricalSensor, deviceEnergyManagement], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
13
13
  this.createDefaultIdentifyClusterServer()
14
14
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Evse')
15
15
  .createDefaultPowerSourceWiredClusterServer()
@@ -3,7 +3,7 @@ import { extractorHood, powerSource } from '../matterbridgeDeviceTypes.js';
3
3
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
4
4
  export class ExtractorHood extends MatterbridgeEndpoint {
5
5
  constructor(name, serial, hepaCondition = 100, hepaChangeIndication = ResourceMonitoring.ChangeIndication.Ok, hepaInPlaceIndicator = true, hepaLastChangedTime = null, hepaReplacementProductList = [], activatedCarbonCondition = 100, activatedCarbonChangeIndication = ResourceMonitoring.ChangeIndication.Ok, activatedCarbonInPlaceIndicator = true, activatedCarbonLastChangedTime = null, activatedCarbonReplacementProductList = []) {
6
- super([extractorHood, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
6
+ super([extractorHood, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
7
7
  this.createDefaultIdentifyClusterServer();
8
8
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Extractor Hood');
9
9
  this.createDefaultPowerSourceWiredClusterServer();
@@ -7,7 +7,7 @@ export class HeatPump extends MatterbridgeEndpoint {
7
7
  super([heatPump, powerSource, electricalSensor, deviceEnergyManagement], {
8
8
  tagList: [{ mfgCode: null, namespaceId: PowerSourceTag.Grid.namespaceId, tag: PowerSourceTag.Grid.tag, label: null }],
9
9
  id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
10
- }, true);
10
+ });
11
11
  this.createDefaultIdentifyClusterServer()
12
12
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Heat Pump')
13
13
  .createDefaultPowerSourceWiredClusterServer()
@@ -7,7 +7,7 @@ import { MatterbridgeLaundryWasherModeServer } from './laundryWasher.js';
7
7
  import { createLevelTemperatureControlClusterServer, createNumberTemperatureControlClusterServer } from './temperatureControl.js';
8
8
  export class LaundryDryer extends MatterbridgeEndpoint {
9
9
  constructor(name, serial, currentMode, supportedModes, selectedTemperatureLevel, supportedTemperatureLevels, temperatureSetpoint, minTemperature, maxTemperature, step, operationalState) {
10
- super([laundryDryer, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
10
+ super([laundryDryer, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
11
11
  this.createDefaultIdentifyClusterServer();
12
12
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Laundry Dryer');
13
13
  this.createDefaultPowerSourceWiredClusterServer();
@@ -9,7 +9,7 @@ import { MatterbridgeOnOffServer, MatterbridgeServer } from '../matterbridgeBeha
9
9
  import { createLevelTemperatureControlClusterServer, createNumberTemperatureControlClusterServer } from './temperatureControl.js';
10
10
  export class LaundryWasher extends MatterbridgeEndpoint {
11
11
  constructor(name, serial, currentMode, supportedModes, spinSpeedCurrent, spinSpeeds, numberOfRinses, supportedRinses, selectedTemperatureLevel, supportedTemperatureLevels, temperatureSetpoint, minTemperature, maxTemperature, step, operationalState) {
12
- super([laundryWasher, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
12
+ super([laundryWasher, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
13
13
  this.createDefaultIdentifyClusterServer();
14
14
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Laundry Washer');
15
15
  this.createDefaultPowerSourceWiredClusterServer();
@@ -16,7 +16,7 @@ export class MicrowaveOven extends MatterbridgeEndpoint {
16
16
  { label: 'Normal', mode: 6, modeTags: [{ value: MicrowaveOvenMode.ModeTag.Normal }] },
17
17
  { label: 'Defrost', mode: 7, modeTags: [{ value: MicrowaveOvenMode.ModeTag.Defrost }] },
18
18
  ], selectedWattIndex = 5, supportedWatts = [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], cookTime = 60, maxCookTime = 3600) {
19
- super([microwaveOven, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
19
+ super([microwaveOven, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
20
20
  this.createDefaultIdentifyClusterServer();
21
21
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Microwave Oven');
22
22
  this.createDefaultPowerSourceWiredClusterServer();
@@ -9,7 +9,7 @@ import { MatterbridgeServer } from '../matterbridgeBehaviors.js';
9
9
  import { createLevelTemperatureControlClusterServer } from './temperatureControl.js';
10
10
  export class Oven extends MatterbridgeEndpoint {
11
11
  constructor(name, serial) {
12
- super([oven, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
12
+ super([oven, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
13
13
  this.createDefaultIdentifyClusterServer();
14
14
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Oven');
15
15
  this.createDefaultPowerSourceWiredClusterServer();
@@ -8,7 +8,7 @@ import { MatterbridgeServer } from '../matterbridgeBehaviors.js';
8
8
  import { createLevelTemperatureControlClusterServer } from './temperatureControl.js';
9
9
  export class Refrigerator extends MatterbridgeEndpoint {
10
10
  constructor(name, serial) {
11
- super([refrigerator, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
11
+ super([refrigerator, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
12
12
  this.createDefaultIdentifyClusterServer();
13
13
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Refrigerator');
14
14
  this.createDefaultPowerSourceWiredClusterServer();
@@ -19,7 +19,7 @@ export class Refrigerator extends MatterbridgeEndpoint {
19
19
  { label: 'RapidCool', mode: 2, modeTags: [{ value: RefrigeratorAndTemperatureControlledCabinetMode.ModeTag.RapidCool }] },
20
20
  { label: 'RapidFreeze', mode: 3, modeTags: [{ value: RefrigeratorAndTemperatureControlledCabinetMode.ModeTag.RapidFreeze }] },
21
21
  ], selectedTemperatureLevel = 2, supportedTemperatureLevels = ['Level 1', 'Level 2', 'Level 3', 'Level 4', 'Level 5'], currentTemperature = 1000) {
22
- const cabinet = this.addChildDeviceType(name, temperatureControlledCabinetCooler, { tagList }, true);
22
+ const cabinet = this.addChildDeviceType(name, temperatureControlledCabinetCooler, { tagList });
23
23
  cabinet.log.logName = name;
24
24
  cabinet.createDefaultIdentifyClusterServer();
25
25
  createLevelTemperatureControlClusterServer(cabinet, selectedTemperatureLevel, supportedTemperatureLevels);
@@ -14,7 +14,7 @@ import { powerSource, roboticVacuumCleaner } from '../matterbridgeDeviceTypes.js
14
14
  import { MatterbridgeServer, MatterbridgeServiceAreaServer } from '../matterbridgeBehaviors.js';
15
15
  export class RoboticVacuumCleaner extends MatterbridgeEndpoint {
16
16
  constructor(name, serial, mode = undefined, currentRunMode, supportedRunModes, currentCleanMode, supportedCleanModes, currentPhase = null, phaseList = null, operationalState, operationalStateList, supportedAreas, selectedAreas, currentArea, supportedMaps) {
17
- super([roboticVacuumCleaner, powerSource], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`, mode }, true);
17
+ super([roboticVacuumCleaner, powerSource], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`, mode });
18
18
  this.createDefaultIdentifyClusterServer()
19
19
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Robot Vacuum Cleaner')
20
20
  .createDefaultPowerSourceRechargeableBatteryClusterServer(80, PowerSource.BatChargeLevel.Ok, 5900)
@@ -7,7 +7,7 @@ export class SolarPower extends MatterbridgeEndpoint {
7
7
  super([solarPower, powerSource, electricalSensor, deviceEnergyManagement], {
8
8
  tagList: [{ mfgCode: null, namespaceId: PowerSourceTag.Solar.namespaceId, tag: PowerSourceTag.Solar.tag, label: null }],
9
9
  id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
10
- }, true);
10
+ });
11
11
  this.createDefaultIdentifyClusterServer()
12
12
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Solar Power')
13
13
  .createDefaultPowerSourceWiredClusterServer()
@@ -10,7 +10,7 @@ export class Speaker extends MatterbridgeEndpoint {
10
10
  volume = 1;
11
11
  if (volume > 254)
12
12
  volume = 254;
13
- super([speakerDevice], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
13
+ super([speakerDevice], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
14
14
  this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Speaker');
15
15
  this.createOnOffClusterServer(!muted);
16
16
  this.createLevelControlClusterServer(volume);
@@ -9,7 +9,7 @@ import { electricalSensor, powerSource, waterHeater } from '../matterbridgeDevic
9
9
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
10
10
  export class WaterHeater extends MatterbridgeEndpoint {
11
11
  constructor(name, serial, waterTemperature = 50, targetWaterTemperature = 55, minHeatSetpointLimit = 20, maxHeatSetpointLimit = 80, heaterTypes = { immersionElement1: true }, tankPercentage = 90, voltage = null, current = null, power = null, energy = null, absMinPower = 0, absMaxPower = 0) {
12
- super([waterHeater, powerSource, electricalSensor], { uniqueStorageKey: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` }, true);
12
+ super([waterHeater, powerSource, electricalSensor], { id: `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}` });
13
13
  this.createDefaultIdentifyClusterServer()
14
14
  .createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Water Heater')
15
15
  .createDefaultPowerSourceWiredClusterServer()
package/dist/frontend.js CHANGED
@@ -1453,7 +1453,11 @@ export class Frontend extends EventEmitter {
1453
1453
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertiseTime: 0, advertising: false } });
1454
1454
  }
1455
1455
  if (data.params.advertise) {
1456
- await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
1456
+ const advertiser = serverNode.env.get(DeviceAdvertiser);
1457
+ if (advertiser && advertiser.advertise && typeof advertiser.advertise === 'function')
1458
+ await advertiser.advertise(true);
1459
+ if (advertiser && advertiser.restartAdvertisement && typeof advertiser.restartAdvertisement === 'function')
1460
+ advertiser.restartAdvertisement();
1457
1461
  this.log.debug(`*Advertising has been sent for node ${data.params.id}`);
1458
1462
  this.wssSendRefreshRequired('matter', { matter: { ...matter, advertising: true } });
1459
1463
  }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './matterbridge.js';
4
4
  export * from './matterbridgeTypes.js';
5
5
  export * from './matterbridgeEndpoint.js';
6
6
  export * from './matterbridgeEndpointHelpers.js';
7
+ export * from './matterbridgeEndpointTypes.js';
7
8
  export * from './matterbridgeBehaviors.js';
8
9
  export * from './matterbridgeDeviceTypes.js';
9
10
  export * from './matterbridgePlatform.js';
@@ -0,0 +1 @@
1
+ export * from './jestHelpers.js';
@@ -1,26 +1,46 @@
1
1
  import { rmSync } from 'node:fs';
2
2
  import { inspect } from 'node:util';
3
3
  import path from 'node:path';
4
- import { jest } from '@jest/globals';
5
4
  import { AnsiLogger } from 'node-ansi-logger';
6
- import '@matter/nodejs';
7
- import { LogFormat as MatterLogFormat, LogLevel as MatterLogLevel, Environment, Lifecycle } from '@matter/general';
8
- import { DeviceTypeId, VendorId } from '@matter/types';
9
- import { MdnsService } from '@matter/protocol';
10
- import { ServerNode, Endpoint, ServerNodeStore } from '@matter/node';
11
- import { AggregatorEndpoint } from '@matter/node/endpoints/aggregator';
12
- import { RootEndpoint } from '@matter/node/endpoints/root';
5
+ import { LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Environment, Lifecycle } from '@matter/general';
6
+ import { Endpoint, ServerNode, ServerNodeStore } from '@matter/node';
7
+ import { DeviceTypeId, VendorId } from '@matter/types/datatype';
8
+ import { AggregatorEndpoint, RootEndpoint } from '@matter/node/endpoints';
9
+ import { MdnsService } from '@matter/main/protocol';
10
+ import { Matterbridge } from '../matterbridge.js';
11
+ import { MATTER_STORAGE_NAME } from '../matterbridgeTypes.js';
13
12
  export let loggerLogSpy;
13
+ export let loggerDebugSpy;
14
+ export let loggerInfoSpy;
15
+ export let loggerNoticeSpy;
16
+ export let loggerWarnSpy;
17
+ export let loggerErrorSpy;
18
+ export let loggerFatalSpy;
14
19
  export let consoleLogSpy;
15
20
  export let consoleDebugSpy;
16
21
  export let consoleInfoSpy;
17
22
  export let consoleWarnSpy;
18
23
  export let consoleErrorSpy;
19
- export function setupTest(name, debug = false) {
24
+ export let addBridgedEndpointSpy;
25
+ export let removeBridgedEndpointSpy;
26
+ export let removeAllBridgedEndpointsSpy;
27
+ export let matterbridge;
28
+ export let environment;
29
+ export let server;
30
+ export let aggregator;
31
+ export let log;
32
+ export async function setupTest(name, debug = false) {
20
33
  expect(name).toBeDefined();
21
34
  expect(typeof name).toBe('string');
22
35
  expect(name.length).toBeGreaterThanOrEqual(4);
23
36
  rmSync(path.join('jest', name), { recursive: true, force: true });
37
+ const { jest } = await import('@jest/globals');
38
+ loggerDebugSpy = jest.spyOn(AnsiLogger.prototype, 'debug');
39
+ loggerInfoSpy = jest.spyOn(AnsiLogger.prototype, 'info');
40
+ loggerNoticeSpy = jest.spyOn(AnsiLogger.prototype, 'notice');
41
+ loggerWarnSpy = jest.spyOn(AnsiLogger.prototype, 'warn');
42
+ loggerErrorSpy = jest.spyOn(AnsiLogger.prototype, 'error');
43
+ loggerFatalSpy = jest.spyOn(AnsiLogger.prototype, 'fatal');
24
44
  if (debug) {
25
45
  loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
26
46
  consoleLogSpy = jest.spyOn(console, 'log');
@@ -37,8 +57,12 @@ export function setupTest(name, debug = false) {
37
57
  consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
38
58
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
39
59
  }
60
+ addBridgedEndpointSpy = jest.spyOn(Matterbridge.prototype, 'addBridgedEndpoint');
61
+ removeBridgedEndpointSpy = jest.spyOn(Matterbridge.prototype, 'removeBridgedEndpoint');
62
+ removeAllBridgedEndpointsSpy = jest.spyOn(Matterbridge.prototype, 'removeAllBridgedEndpoints');
40
63
  }
41
- export function setDebug(debug) {
64
+ export async function setDebug(debug) {
65
+ const { jest } = await import('@jest/globals');
42
66
  if (debug) {
43
67
  loggerLogSpy.mockRestore();
44
68
  consoleLogSpy.mockRestore();
@@ -62,20 +86,133 @@ export function setDebug(debug) {
62
86
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
63
87
  }
64
88
  }
65
- export function createTestEnvironment(homeDir) {
66
- expect(homeDir).toBeDefined();
67
- expect(typeof homeDir).toBe('string');
68
- expect(homeDir.length).toBeGreaterThanOrEqual(4);
69
- rmSync(homeDir, { recursive: true, force: true });
70
- const environment = Environment.default;
89
+ export async function createMatterbridgeEnvironment(name) {
90
+ matterbridge = await Matterbridge.loadInstance(false);
91
+ expect(matterbridge).toBeDefined();
92
+ expect(matterbridge).toBeInstanceOf(Matterbridge);
93
+ matterbridge.matterbridgeVersion = '3.3.0';
94
+ matterbridge.bridgeMode = 'bridge';
95
+ matterbridge.rootDirectory = path.join('jest', name);
96
+ matterbridge.homeDirectory = path.join('jest', name);
97
+ matterbridge.matterbridgeDirectory = path.join('jest', name, '.matterbridge');
98
+ matterbridge.matterbridgePluginDirectory = path.join('jest', name, 'Matterbridge');
99
+ matterbridge.matterbridgeCertDirectory = path.join('jest', name, '.mattercert');
100
+ matterbridge.log.logLevel = "debug";
101
+ log = new AnsiLogger({ logName: 'Plugin platform', logTimestampFormat: 4, logLevel: "debug" });
102
+ matterbridge.environment = createTestEnvironment(name);
103
+ expect(matterbridge.environment).toBeDefined();
104
+ expect(matterbridge.environment).toBeInstanceOf(Environment);
105
+ return matterbridge;
106
+ }
107
+ export async function startMatterbridgeEnvironment(port = 5540) {
108
+ await matterbridge.startMatterStorage();
109
+ expect(matterbridge.matterStorageService).toBeDefined();
110
+ expect(matterbridge.matterStorageManager).toBeDefined();
111
+ expect(matterbridge.matterbridgeContext).toBeDefined();
112
+ server = await matterbridge.createServerNode(matterbridge.matterbridgeContext, port);
113
+ expect(server).toBeDefined();
114
+ expect(server).toBeDefined();
115
+ expect(server.lifecycle.isReady).toBeTruthy();
116
+ matterbridge.serverNode = server;
117
+ aggregator = await matterbridge.createAggregatorNode(matterbridge.matterbridgeContext);
118
+ expect(aggregator).toBeDefined();
119
+ matterbridge.aggregatorNode = aggregator;
120
+ expect(await server.add(aggregator)).toBeDefined();
121
+ expect(server.parts.has(aggregator.id)).toBeTruthy();
122
+ expect(server.parts.has(aggregator)).toBeTruthy();
123
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
124
+ expect(server.lifecycle.isOnline).toBeFalsy();
125
+ await new Promise((resolve) => {
126
+ server.lifecycle.online.on(async () => {
127
+ resolve();
128
+ });
129
+ server.start();
130
+ });
131
+ expect(server.lifecycle.isReady).toBeTruthy();
132
+ expect(server.lifecycle.isOnline).toBeTruthy();
133
+ expect(server.lifecycle.isCommissioned).toBeFalsy();
134
+ expect(server.lifecycle.isPartsReady).toBeTruthy();
135
+ expect(server.lifecycle.hasId).toBeTruthy();
136
+ expect(server.lifecycle.hasNumber).toBeTruthy();
137
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
138
+ expect(aggregator.lifecycle.isInstalled).toBeTruthy();
139
+ expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
140
+ expect(aggregator.lifecycle.hasId).toBeTruthy();
141
+ expect(aggregator.lifecycle.hasNumber).toBeTruthy();
142
+ await flushAsync();
143
+ return [server, aggregator];
144
+ }
145
+ export function addMatterbridgePlatform(platform, name) {
146
+ if (name)
147
+ platform.config.name = name;
148
+ expect(platform).toBeDefined();
149
+ expect(platform.config.name).toBeDefined();
150
+ expect(platform.config.type).toBeDefined();
151
+ expect(platform.type).toBeDefined();
152
+ expect(platform.config.version).toBeDefined();
153
+ expect(platform.version).toBeDefined();
154
+ expect(platform.config.debug).toBeDefined();
155
+ expect(platform.config.unregisterOnShutdown).toBeDefined();
156
+ matterbridge.plugins._plugins.set(platform.config.name, {
157
+ name: platform.config.name,
158
+ path: './',
159
+ type: platform.type,
160
+ version: platform.version,
161
+ description: 'Plugin ' + platform.config.name,
162
+ author: 'Unknown',
163
+ enabled: true,
164
+ });
165
+ platform['name'] = platform.config.name;
166
+ }
167
+ export async function stopMatterbridgeEnvironment() {
168
+ expect(matterbridge).toBeDefined();
169
+ expect(server).toBeDefined();
170
+ expect(aggregator).toBeDefined();
171
+ await flushAllEndpointNumberPersistence(server);
172
+ await assertAllEndpointNumbersPersisted(server);
173
+ expect(server.lifecycle.isReady).toBeTruthy();
174
+ expect(server.lifecycle.isOnline).toBeTruthy();
175
+ await server.close();
176
+ expect(server.lifecycle.isReady).toBeTruthy();
177
+ expect(server.lifecycle.isOnline).toBeFalsy();
178
+ await matterbridge.stopMatterStorage();
179
+ expect(matterbridge.matterStorageService).not.toBeDefined();
180
+ expect(matterbridge.matterStorageManager).not.toBeDefined();
181
+ expect(matterbridge.matterbridgeContext).not.toBeDefined();
182
+ await flushAsync();
183
+ }
184
+ export async function destroyMatterbridgeEnvironment(cleanupPause = 10, destroyPause = 250) {
185
+ await destroyInstance(matterbridge, cleanupPause, destroyPause);
186
+ await closeMdnsInstance(matterbridge);
187
+ Matterbridge.instance = undefined;
188
+ }
189
+ export async function destroyInstance(matterbridge, cleanupPause = 10, destroyPause = 250) {
190
+ await matterbridge.cleanup('destroying instance...', false, cleanupPause);
191
+ if (destroyPause > 0)
192
+ await flushAsync(undefined, undefined, destroyPause);
193
+ }
194
+ export async function closeMdnsInstance(matterbridge) {
195
+ const mdns = matterbridge.environment.get(MdnsService);
196
+ if (mdns && mdns[Symbol.asyncDispose] && typeof mdns[Symbol.asyncDispose] === 'function')
197
+ await mdns[Symbol.asyncDispose]();
198
+ if (mdns && mdns.close && typeof mdns.close === 'function')
199
+ await mdns.close();
200
+ }
201
+ export function createTestEnvironment(name) {
202
+ expect(name).toBeDefined();
203
+ expect(typeof name).toBe('string');
204
+ expect(name.length).toBeGreaterThanOrEqual(4);
205
+ rmSync(path.join('jest', name), { recursive: true, force: true });
206
+ environment = Environment.default;
71
207
  environment.vars.set('log.level', MatterLogLevel.DEBUG);
72
208
  environment.vars.set('log.format', MatterLogFormat.ANSI);
73
- environment.vars.set('path.root', homeDir);
209
+ environment.vars.set('path.root', path.join('jest', name, '.matterbridge', MATTER_STORAGE_NAME));
74
210
  environment.vars.set('runtime.signals', false);
75
211
  environment.vars.set('runtime.exitcode', false);
212
+ new MdnsService(environment);
76
213
  return environment;
77
214
  }
78
- export async function flushAsync(ticks = 3, microTurns = 10, pause = 100) {
215
+ export async function flushAsync(ticks = 3, microTurns = 10, pause = 250) {
79
216
  for (let i = 0; i < ticks; i++)
80
217
  await new Promise((resolve) => setImmediate(resolve));
81
218
  for (let i = 0; i < microTurns; i++)
@@ -119,8 +256,9 @@ export async function assertAllEndpointNumbersPersisted(targetServer) {
119
256
  return all.length;
120
257
  }
121
258
  export async function startServerNode(name, port) {
122
- const server = await ServerNode.create({
259
+ server = await ServerNode.create({
123
260
  id: name + 'ServerNode',
261
+ environment,
124
262
  productDescription: {
125
263
  name: name + 'ServerNode',
126
264
  deviceType: DeviceTypeId(RootEndpoint.deviceType),
@@ -143,7 +281,7 @@ export async function startServerNode(name, port) {
143
281
  });
144
282
  expect(server).toBeDefined();
145
283
  expect(server.lifecycle.isReady).toBeTruthy();
146
- const aggregator = new Endpoint(AggregatorEndpoint, {
284
+ aggregator = new Endpoint(AggregatorEndpoint, {
147
285
  id: name + 'AggregatorNode',
148
286
  });
149
287
  expect(aggregator).toBeDefined();
@@ -181,7 +319,11 @@ export async function stopServerNode(server) {
181
319
  await server.close();
182
320
  expect(server.lifecycle.isReady).toBeTruthy();
183
321
  expect(server.lifecycle.isOnline).toBeFalsy();
184
- await server.env.get(MdnsService)[Symbol.asyncDispose]();
322
+ const mdns = environment.get(MdnsService);
323
+ if (mdns && typeof mdns[Symbol.asyncDispose] === 'function')
324
+ await mdns[Symbol.asyncDispose]();
325
+ if (mdns && typeof mdns.close === 'function')
326
+ await mdns.close();
185
327
  await flushAsync();
186
328
  }
187
329
  export async function addDevice(owner, device, pause = 10) {
@@ -206,7 +348,7 @@ export async function addDevice(owner, device, pause = 10) {
206
348
  expect(device.lifecycle.hasId).toBeTruthy();
207
349
  expect(device.lifecycle.hasNumber).toBeTruthy();
208
350
  expect(device.construction.status).toBe(Lifecycle.Status.Active);
209
- await flushAsync(1, 1, pause);
351
+ await flushAsync(undefined, undefined, pause);
210
352
  return true;
211
353
  }
212
354
  export async function deleteDevice(owner, device, pause = 10) {
@@ -231,6 +373,6 @@ export async function deleteDevice(owner, device, pause = 10) {
231
373
  expect(device.lifecycle.hasId).toBeTruthy();
232
374
  expect(device.lifecycle.hasNumber).toBeTruthy();
233
375
  expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
234
- await flushAsync(1, 1, pause);
376
+ await flushAsync(undefined, undefined, pause);
235
377
  return true;
236
378
  }
@@ -171,12 +171,6 @@ export class Matterbridge extends EventEmitter {
171
171
  }
172
172
  return Matterbridge.instance;
173
173
  }
174
- async destroyInstance(timeout = 1000, pause = 250) {
175
- this.log.info(`Destroy instance...`);
176
- await this.cleanup('destroying instance...', false, timeout);
177
- if (pause)
178
- await wait(pause, 'destroyInstance stop', true);
179
- }
180
174
  async initialize() {
181
175
  this.emit('initialize_started');
182
176
  if (hasParameter('service'))
@@ -943,7 +937,7 @@ export class Matterbridge extends EventEmitter {
943
937
  async shutdownProcessAndFactoryReset() {
944
938
  await this.cleanup('shutting down with factory reset...', false);
945
939
  }
946
- async cleanup(message, restart = false, timeout = 1000) {
940
+ async cleanup(message, restart = false, pause = 1000) {
947
941
  if (this.initialized && !this.hasCleanupStarted) {
948
942
  this.emit('cleanup_started');
949
943
  this.hasCleanupStarted = true;
@@ -984,9 +978,9 @@ export class Matterbridge extends EventEmitter {
984
978
  }
985
979
  }
986
980
  this.log.notice(`Stopping matter server nodes in ${this.bridgeMode} mode...`);
987
- if (timeout > 0) {
988
- this.log.debug(`Waiting ${timeout}ms for the MessageExchange to finish...`);
989
- await wait(timeout, `Waiting ${timeout}ms for the MessageExchange to finish...`, true);
981
+ if (pause > 0) {
982
+ this.log.debug(`Waiting ${pause}ms for the MessageExchange to finish...`);
983
+ await wait(pause, `Waiting ${pause}ms for the MessageExchange to finish...`, false);
990
984
  }
991
985
  if (this.bridgeMode === 'bridge') {
992
986
  if (this.serverNode) {
@@ -85,6 +85,7 @@ import { WakeOnLan } from '@matter/types/clusters/wake-on-lan';
85
85
  import { WaterHeaterManagement } from '@matter/types/clusters/water-heater-management';
86
86
  import { WaterHeaterMode } from '@matter/types/clusters/water-heater-mode';
87
87
  import { WindowCovering } from '@matter/types/clusters/window-covering';
88
+ import { ScenesManagement } from '@matter/types/clusters/scenes-management';
88
89
  export var DeviceClasses;
89
90
  (function (DeviceClasses) {
90
91
  DeviceClasses["Node"] = "Node";
@@ -238,7 +239,7 @@ export const pumpDevice = DeviceTypeDefinition({
238
239
  deviceClass: DeviceClasses.Simple,
239
240
  revision: 3,
240
241
  requiredServerClusters: [OnOff.Cluster.id, PumpConfigurationAndControl.Cluster.id, Identify.Cluster.id],
241
- optionalServerClusters: [LevelControl.Cluster.id, Groups.Cluster.id, TemperatureMeasurement.Cluster.id, PressureMeasurement.Cluster.id, FlowMeasurement.Cluster.id],
242
+ optionalServerClusters: [LevelControl.Cluster.id, Groups.Cluster.id, ScenesManagement.Cluster.id, TemperatureMeasurement.Cluster.id, PressureMeasurement.Cluster.id, FlowMeasurement.Cluster.id],
242
243
  });
243
244
  export const waterValve = DeviceTypeDefinition({
244
245
  name: 'MA-waterValve',
@@ -254,7 +255,7 @@ export const onOffSwitch = DeviceTypeDefinition({
254
255
  deviceClass: DeviceClasses.Simple,
255
256
  revision: 3,
256
257
  requiredServerClusters: [Identify.Cluster.id, OnOff.Cluster.id],
257
- optionalServerClusters: [Groups.Cluster.id],
258
+ optionalServerClusters: [Groups.Cluster.id, ScenesManagement.Cluster.id],
258
259
  });
259
260
  export const dimmableSwitch = DeviceTypeDefinition({
260
261
  name: 'MA-dimmableswitch',
@@ -262,15 +263,15 @@ export const dimmableSwitch = DeviceTypeDefinition({
262
263
  deviceClass: DeviceClasses.Simple,
263
264
  revision: 3,
264
265
  requiredServerClusters: [Identify.Cluster.id, OnOff.Cluster.id, LevelControl.Cluster.id],
265
- optionalServerClusters: [Groups.Cluster.id],
266
+ optionalServerClusters: [Groups.Cluster.id, ScenesManagement.Cluster.id],
266
267
  });
267
268
  export const colorTemperatureSwitch = DeviceTypeDefinition({
268
269
  name: 'MA-colortemperatureswitch',
269
270
  code: 0x0105,
270
271
  deviceClass: DeviceClasses.Simple,
271
272
  revision: 3,
272
- requiredServerClusters: [Identify.Cluster.id, Groups.Cluster.id, OnOff.Cluster.id, LevelControl.Cluster.id, ColorControl.Cluster.id],
273
- optionalServerClusters: [Groups.Cluster.id],
273
+ requiredServerClusters: [Identify.Cluster.id, OnOff.Cluster.id, LevelControl.Cluster.id, ColorControl.Cluster.id],
274
+ optionalServerClusters: [Groups.Cluster.id, ScenesManagement.Cluster.id],
274
275
  });
275
276
  export const genericSwitch = DeviceTypeDefinition({
276
277
  name: 'MA-genericswitch',
@@ -390,7 +391,7 @@ export const doorLockDevice = DeviceTypeDefinition({
390
391
  deviceClass: DeviceClasses.Simple,
391
392
  revision: 3,
392
393
  requiredServerClusters: [Identify.Cluster.id, DoorLock.Cluster.id],
393
- optionalServerClusters: [],
394
+ optionalServerClusters: [ScenesManagement.Cluster.id, Groups.Cluster.id],
394
395
  });
395
396
  export const coverDevice = DeviceTypeDefinition({
396
397
  name: 'MA-windowCovering',
@@ -496,8 +497,8 @@ export const refrigerator = DeviceTypeDefinition({
496
497
  code: 0x70,
497
498
  deviceClass: DeviceClasses.Simple,
498
499
  revision: 2,
499
- requiredServerClusters: [Identify.Cluster.id, RefrigeratorAndTemperatureControlledCabinetMode.Cluster.id, RefrigeratorAlarm.Cluster.id],
500
- optionalServerClusters: [],
500
+ requiredServerClusters: [],
501
+ optionalServerClusters: [Identify.Cluster.id, RefrigeratorAndTemperatureControlledCabinetMode.Cluster.id, RefrigeratorAlarm.Cluster.id],
501
502
  });
502
503
  export const airConditioner = DeviceTypeDefinition({
503
504
  name: 'MA-airConditioner',
@@ -505,13 +506,7 @@ export const airConditioner = DeviceTypeDefinition({
505
506
  deviceClass: DeviceClasses.Simple,
506
507
  revision: 2,
507
508
  requiredServerClusters: [Identify.Cluster.id, OnOff.Cluster.id, Thermostat.Cluster.id],
508
- optionalServerClusters: [
509
- Groups.Cluster.id,
510
- FanControl.Cluster.id,
511
- ThermostatUserInterfaceConfiguration.Cluster.id,
512
- TemperatureMeasurement.Cluster.id,
513
- RelativeHumidityMeasurement.Cluster.id,
514
- ],
509
+ optionalServerClusters: [Groups.Cluster.id, ScenesManagement.Cluster.id, FanControl.Cluster.id, ThermostatUserInterfaceConfiguration.Cluster.id, TemperatureMeasurement.Cluster.id, RelativeHumidityMeasurement.Cluster.id],
515
510
  });
516
511
  export const temperatureControlledCabinetCooler = DeviceTypeDefinition({
517
512
  name: 'MA-temperaturecontrolledcabinetcooler',
@@ -550,8 +545,8 @@ export const cookSurface = DeviceTypeDefinition({
550
545
  code: 0x77,
551
546
  deviceClass: DeviceClasses.Simple,
552
547
  revision: 1,
553
- requiredServerClusters: [TemperatureControl.Cluster.id, TemperatureMeasurement.Cluster.id],
554
- optionalServerClusters: [OnOff.Cluster.id],
548
+ requiredServerClusters: [],
549
+ optionalServerClusters: [TemperatureControl.Cluster.id, TemperatureMeasurement.Cluster.id, OnOff.Cluster.id],
555
550
  });
556
551
  export const cooktop = DeviceTypeDefinition({
557
552
  name: 'MA-cooktop',
@@ -566,8 +561,8 @@ export const oven = DeviceTypeDefinition({
566
561
  code: 0x7b,
567
562
  deviceClass: DeviceClasses.Simple,
568
563
  revision: 2,
569
- requiredServerClusters: [Identify.Cluster.id],
570
- optionalServerClusters: [],
564
+ requiredServerClusters: [],
565
+ optionalServerClusters: [Identify.Cluster.id],
571
566
  });
572
567
  export const extractorHood = DeviceTypeDefinition({
573
568
  name: 'MA-extractorhood',
@@ -62,7 +62,7 @@ import { FanControlServer } from '@matter/node/behaviors/fan-control';
62
62
  import { ThermostatUserInterfaceConfigurationServer } from '@matter/node/behaviors/thermostat-user-interface-configuration';
63
63
  import { isValidNumber, isValidObject, isValidString } from './utils/isvalid.js';
64
64
  import { MatterbridgeServer, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeLiftTiltWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeActivatedCarbonFilterMonitoringServer, MatterbridgeHepaFilterMonitoringServer, MatterbridgeEnhancedColorControlServer, MatterbridgePowerSourceServer, } from './matterbridgeBehaviors.js';
65
- import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, getDefaultElectricalEnergyMeasurementClusterServer, getDefaultElectricalPowerMeasurementClusterServer, getApparentElectricalPowerMeasurementClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, featuresFor, getDefaultPowerSourceWiredClusterServer, getDefaultPowerSourceReplaceableBatteryClusterServer, getDefaultPowerSourceRechargeableBatteryClusterServer, getDefaultDeviceEnergyManagementClusterServer, getDefaultDeviceEnergyManagementModeClusterServer, } from './matterbridgeEndpointHelpers.js';
65
+ import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, getDefaultElectricalEnergyMeasurementClusterServer, getDefaultElectricalPowerMeasurementClusterServer, getApparentElectricalPowerMeasurementClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, featuresFor, getDefaultPowerSourceWiredClusterServer, getDefaultPowerSourceReplaceableBatteryClusterServer, getDefaultPowerSourceRechargeableBatteryClusterServer, getDefaultDeviceEnergyManagementClusterServer, getDefaultDeviceEnergyManagementModeClusterServer, getDefaultPowerSourceBatteryClusterServer, } from './matterbridgeEndpointHelpers.js';
66
66
  export class MatterbridgeEndpoint extends Endpoint {
67
67
  static logLevel = "info";
68
68
  mode = undefined;
@@ -82,14 +82,16 @@ export class MatterbridgeEndpoint extends Endpoint {
82
82
  hardwareVersion = undefined;
83
83
  hardwareVersionString = undefined;
84
84
  productUrl = 'https://www.npmjs.com/package/matterbridge';
85
+ tagList = undefined;
86
+ originalId = undefined;
87
+ uniqueStorageKey = undefined;
85
88
  name = undefined;
86
89
  deviceType = undefined;
87
- uniqueStorageKey = undefined;
88
- tagList = undefined;
89
90
  deviceTypes = new Map();
90
91
  commandHandler = new NamedHandler();
91
92
  constructor(definition, options = {}, debug = false) {
92
93
  let deviceTypeList = [];
94
+ const originalId = options.id;
93
95
  let firstDefinition;
94
96
  if (Array.isArray(definition)) {
95
97
  firstDefinition = definition[0];
@@ -127,19 +129,14 @@ export class MatterbridgeEndpoint extends Endpoint {
127
129
  options.id = generateUniqueId(options.id);
128
130
  }
129
131
  const optionsV8 = {
130
- id: options.uniqueStorageKey?.replace(/[ .]/g, ''),
131
- number: options.endpointId,
132
+ id: options.id?.replace(/[ .]/g, '') || options.uniqueStorageKey?.replace(/[ .]/g, ''),
133
+ number: options.number || options.endpointId,
132
134
  descriptor: options.tagList ? { tagList: options.tagList, deviceTypeList } : { deviceTypeList },
133
135
  };
134
- if (options.id !== undefined) {
135
- optionsV8.id = options.id.replace(/[ .]/g, '');
136
- }
137
- if (options.number !== undefined) {
138
- optionsV8.number = options.number;
139
- }
140
136
  super(endpointV8, optionsV8);
141
137
  this.mode = options.mode;
142
- this.uniqueStorageKey = options.id ? options.id : options.uniqueStorageKey;
138
+ this.uniqueStorageKey = options.id ?? options.uniqueStorageKey;
139
+ this.originalId = originalId;
143
140
  this.name = firstDefinition.name;
144
141
  this.deviceType = firstDefinition.code;
145
142
  this.tagList = options.tagList;
@@ -150,7 +147,7 @@ export class MatterbridgeEndpoint extends Endpoint {
150
147
  }
151
148
  else
152
149
  this.deviceTypes.set(firstDefinition.code, firstDefinition);
153
- this.log = new AnsiLogger({ logName: options.uniqueStorageKey ?? 'MatterbridgeEndpoint', logTimestampFormat: 4, logLevel: debug === true ? "debug" : MatterbridgeEndpoint.logLevel });
150
+ this.log = new AnsiLogger({ logName: this.originalId ?? this.uniqueStorageKey ?? 'MatterbridgeEndpoint', logTimestampFormat: 4, logLevel: debug === true ? "debug" : MatterbridgeEndpoint.logLevel });
154
151
  this.log.debug(`${YELLOW}new${db} MatterbridgeEndpoint: ${zb}${'0x' + firstDefinition.code.toString(16).padStart(4, '0')}${db}-${zb}${firstDefinition.name}${db} mode: ${CYAN}${this.mode}${db} id: ${CYAN}${optionsV8.id}${db} number: ${CYAN}${optionsV8.number}${db} taglist: ${CYAN}${options.tagList ? debugStringify(options.tagList) : 'undefined'}${db}`);
155
152
  this.behaviors.require(MatterbridgeServer, { log: this.log, commandHandler: this.commandHandler });
156
153
  }
@@ -264,10 +261,10 @@ export class MatterbridgeEndpoint extends Endpoint {
264
261
  for (const tag of options.tagList) {
265
262
  this.log.debug(`- with tagList: mfgCode ${CYAN}${tag.mfgCode}${db} namespaceId ${CYAN}${tag.namespaceId}${db} tag ${CYAN}${tag.tag}${db} label ${CYAN}${tag.label}${db}`);
266
263
  }
267
- child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId, tagList: options.tagList }, debug);
264
+ child = new MatterbridgeEndpoint(definition, { id: endpointName, number: options.number, tagList: options.tagList }, debug);
268
265
  }
269
266
  else {
270
- child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId }, debug);
267
+ child = new MatterbridgeEndpoint(definition, { id: endpointName, number: options.number }, debug);
271
268
  }
272
269
  }
273
270
  if (Array.isArray(definition)) {
@@ -303,10 +300,10 @@ export class MatterbridgeEndpoint extends Endpoint {
303
300
  for (const tag of options.tagList) {
304
301
  this.log.debug(`- with tagList: mfgCode ${CYAN}${tag.mfgCode}${db} namespaceId ${CYAN}${tag.namespaceId}${db} tag ${CYAN}${tag.tag}${db} label ${CYAN}${tag.label}${db}`);
305
302
  }
306
- child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId, tagList: options.tagList }, debug);
303
+ child = new MatterbridgeEndpoint(definition, { id: endpointName, number: options.number, tagList: options.tagList }, debug);
307
304
  }
308
305
  else {
309
- child = new MatterbridgeEndpoint(definition, { uniqueStorageKey: endpointName, endpointId: options.endpointId }, debug);
306
+ child = new MatterbridgeEndpoint(definition, { id: endpointName, number: options.number }, debug);
310
307
  }
311
308
  }
312
309
  if (Array.isArray(definition)) {
@@ -362,7 +359,7 @@ export class MatterbridgeEndpoint extends Endpoint {
362
359
  return Array.from(this.parts);
363
360
  }
364
361
  static serialize(device) {
365
- if (!device.serialNumber || !device.deviceName || !device.uniqueId)
362
+ if (!device.serialNumber || !device.deviceName || !device.uniqueId || !device.maybeId || !device.maybeNumber)
366
363
  return;
367
364
  const serialized = {
368
365
  pluginName: device.plugin ?? '',
@@ -374,8 +371,8 @@ export class MatterbridgeEndpoint extends Endpoint {
374
371
  vendorId: device.vendorId,
375
372
  vendorName: device.vendorName,
376
373
  deviceTypes: Array.from(device.deviceTypes.values()),
377
- endpoint: device.maybeNumber,
378
- endpointName: device.maybeId ?? device.deviceName,
374
+ id: device.id,
375
+ number: device.number,
379
376
  clusterServersId: [],
380
377
  };
381
378
  Object.keys(device.behaviors.supported).forEach((behaviorName) => {
@@ -387,7 +384,7 @@ export class MatterbridgeEndpoint extends Endpoint {
387
384
  return serialized;
388
385
  }
389
386
  static deserialize(serializedDevice) {
390
- const device = new MatterbridgeEndpoint(serializedDevice.deviceTypes, { uniqueStorageKey: serializedDevice.endpointName, endpointId: serializedDevice.endpoint }, false);
387
+ const device = new MatterbridgeEndpoint(serializedDevice.deviceTypes, { id: serializedDevice.id, number: serializedDevice.number }, false);
391
388
  device.plugin = serializedDevice.pluginName;
392
389
  device.deviceName = serializedDevice.deviceName;
393
390
  device.serialNumber = serializedDevice.serialNumber;
@@ -408,6 +405,10 @@ export class MatterbridgeEndpoint extends Endpoint {
408
405
  this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Wired), getDefaultPowerSourceWiredClusterServer(wiredCurrentType));
409
406
  return this;
410
407
  }
408
+ createDefaultPowerSourceBatteryClusterServer(batPercentRemaining = null, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = null, batReplaceability = PowerSource.BatReplaceability.Unspecified) {
409
+ this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery), getDefaultPowerSourceBatteryClusterServer(batPercentRemaining, batChargeLevel, batVoltage, batReplaceability));
410
+ return this;
411
+ }
411
412
  createDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplacementDescription = 'Battery type', batQuantity = 1, batReplaceability = PowerSource.BatReplaceability.UserReplaceable) {
412
413
  this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Replaceable), getDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining, batChargeLevel, batVoltage, batReplacementDescription, batQuantity, batReplaceability));
413
414
  return this;
@@ -946,7 +947,6 @@ export class MatterbridgeEndpoint extends Endpoint {
946
947
  actuatorEnabled: false,
947
948
  operatingMode: DoorLock.OperatingMode.Normal,
948
949
  supportedOperatingModes: { normal: false, vacation: true, privacy: true, noRemoteLockUnlock: true, passage: true, alwaysSet: 2047 },
949
- alarmMask: { lockJammed: false, lockFactoryReset: false, lockRadioPowerCycled: false, wrongCodeEntryLimit: false, frontEscutcheonRemoved: false, doorForcedOpen: false },
950
950
  });
951
951
  return this;
952
952
  }
@@ -78,11 +78,10 @@ import { Pm25ConcentrationMeasurementServer } from '@matter/node/behaviors/pm25-
78
78
  import { Pm10ConcentrationMeasurementServer } from '@matter/node/behaviors/pm10-concentration-measurement';
79
79
  import { RadonConcentrationMeasurementServer } from '@matter/node/behaviors/radon-concentration-measurement';
80
80
  import { TotalVolatileOrganicCompoundsConcentrationMeasurementServer } from '@matter/node/behaviors/total-volatile-organic-compounds-concentration-measurement';
81
- import { DeviceEnergyManagementServer } from '@matter/node/behaviors/device-energy-management';
82
81
  import { deepCopy } from './utils/deepCopy.js';
83
82
  import { deepEqual } from './utils/deepEqual.js';
84
83
  import { isValidArray } from './utils/isvalid.js';
85
- import { MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgePowerSourceServer, MatterbridgeDeviceEnergyManagementServer, } from './matterbridgeBehaviors.js';
84
+ import { MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeOperationalStateServer, MatterbridgePowerSourceServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeDeviceEnergyManagementModeServer, } from './matterbridgeBehaviors.js';
86
85
  export function capitalizeFirstLetter(name) {
87
86
  if (!name)
88
87
  return name;
@@ -216,7 +215,7 @@ export function getBehaviourTypeFromClusterServerId(clusterId) {
216
215
  if (clusterId === TotalVolatileOrganicCompoundsConcentrationMeasurement.Cluster.id)
217
216
  return TotalVolatileOrganicCompoundsConcentrationMeasurementServer.with('NumericMeasurement');
218
217
  if (clusterId === DeviceEnergyManagement.Cluster.id)
219
- return DeviceEnergyManagementServer.with('PowerForecastReporting');
218
+ return MatterbridgeDeviceEnergyManagementServer.with('PowerForecastReporting');
220
219
  if (clusterId === DeviceEnergyManagementMode.Cluster.id)
221
220
  return MatterbridgeDeviceEnergyManagementModeServer;
222
221
  return MatterbridgeIdentifyServer;
@@ -257,21 +256,21 @@ export async function invokeBehaviorCommand(endpoint, cluster, command, params)
257
256
  }
258
257
  export async function invokeSubscribeHandler(endpoint, cluster, attribute, newValue, oldValue) {
259
258
  const event = attribute + '$Changed';
260
- const clusterName = getBehavior(endpoint, cluster)?.id;
261
- if (!clusterName) {
259
+ const behaviorId = getBehavior(endpoint, cluster)?.id;
260
+ if (!behaviorId) {
262
261
  endpoint.log.error(`invokeSubscribeHandler ${hk}${event}${er} error: cluster not found on endpoint ${or}${endpoint.maybeId}${er}:${or}${endpoint.maybeNumber}${er}`);
263
262
  return false;
264
263
  }
265
264
  if (endpoint.construction.status !== Lifecycle.Status.Active) {
266
- endpoint.log.error(`invokeSubscribeHandler ${hk}${clusterName}.${event}${er} error: Endpoint ${or}${endpoint.maybeId}${er}:${or}${endpoint.maybeNumber}${er} is in the ${BLUE}${endpoint.construction.status}${er} state`);
265
+ endpoint.log.error(`invokeSubscribeHandler ${hk}${behaviorId}.${event}${er} error: Endpoint ${or}${endpoint.maybeId}${er}:${or}${endpoint.maybeNumber}${er} is in the ${BLUE}${endpoint.construction.status}${er} state`);
267
266
  return false;
268
267
  }
269
268
  const events = endpoint.events;
270
- if (!(clusterName in events) || !(event in events[clusterName])) {
271
- endpoint.log.error(`invokeSubscribeHandler ${hk}${event}${er} error: cluster ${clusterName} not found on endpoint ${or}${endpoint.id}${er}:${or}${endpoint.number}${er}`);
269
+ if (!(behaviorId in events) || !(event in events[behaviorId])) {
270
+ endpoint.log.error(`invokeSubscribeHandler ${hk}${event}${er} error: cluster ${behaviorId} not found on endpoint ${or}${endpoint.id}${er}:${or}${endpoint.number}${er}`);
272
271
  return false;
273
272
  }
274
- await endpoint.act((agent) => agent[clusterName].events[event].emit(newValue, oldValue, { ...agent.context, offline: false }));
273
+ await endpoint.act((agent) => agent[behaviorId].events[event].emit(newValue, oldValue, { ...agent.context, offline: false }));
275
274
  return true;
276
275
  }
277
276
  export function addRequiredClusterServers(endpoint) {
@@ -549,6 +548,19 @@ export function getDefaultPowerSourceWiredClusterServer(wiredCurrentType = Power
549
548
  wiredCurrentType,
550
549
  });
551
550
  }
551
+ export function getDefaultPowerSourceBatteryClusterServer(batPercentRemaining = null, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = null, batReplaceability = PowerSource.BatReplaceability.Unspecified) {
552
+ return optionsFor(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery), {
553
+ status: PowerSource.PowerSourceStatus.Active,
554
+ order: 0,
555
+ description: 'Primary battery',
556
+ endpointList: [],
557
+ batVoltage,
558
+ batPercentRemaining: batPercentRemaining !== null ? Math.min(Math.max(batPercentRemaining * 2, 0), 200) : null,
559
+ batChargeLevel,
560
+ batReplacementNeeded: false,
561
+ batReplaceability,
562
+ });
563
+ }
552
564
  export function getDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplacementDescription = 'Battery type', batQuantity = 1, batReplaceability = PowerSource.BatReplaceability.UserReplaceable) {
553
565
  return optionsFor(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Replaceable), {
554
566
  status: PowerSource.PowerSourceStatus.Active,
@@ -690,7 +702,7 @@ export function getDefaultDeviceEnergyManagementClusterServer(esaType = DeviceEn
690
702
  });
691
703
  }
692
704
  export function getDefaultDeviceEnergyManagementModeClusterServer(currentMode, supportedModes) {
693
- return optionsFor(MatterbridgeDeviceEnergyManagementModeServer, {
705
+ return optionsFor(MatterbridgeDeviceEnergyManagementModeServer.with(), {
694
706
  supportedModes: supportedModes ?? [
695
707
  { label: 'No Energy Management (Forecast reporting only)', mode: 1, modeTags: [{ value: DeviceEnergyManagementMode.ModeTag.NoOptimization }] },
696
708
  {
@@ -714,7 +726,7 @@ export function getDefaultDeviceEnergyManagementModeClusterServer(currentMode, s
714
726
  });
715
727
  }
716
728
  export function getDefaultOperationalStateClusterServer(operationalState = OperationalState.OperationalStateEnum.Stopped) {
717
- return optionsFor(MatterbridgeOperationalStateServer, {
729
+ return optionsFor(MatterbridgeOperationalStateServer.with(), {
718
730
  phaseList: [],
719
731
  currentPhase: null,
720
732
  countdownTime: null,
@@ -0,0 +1,3 @@
1
+ if (process.argv.includes('--loader') || process.argv.includes('-loader'))
2
+ console.log('\u001B[32mMatterbridgeEndpointTypes loaded.\u001B[40;0m');
3
+ export {};
@@ -104,6 +104,20 @@ export class MatterbridgePlatform {
104
104
  }
105
105
  this.matterbridge.plugins.saveConfigFromJson(plugin, config);
106
106
  }
107
+ getSchema() {
108
+ const plugin = this.matterbridge.plugins.get(this.name);
109
+ if (!plugin || !isValidObject(plugin.schemaJson)) {
110
+ throw new Error(`Plugin ${this.name} not found`);
111
+ }
112
+ return plugin.schemaJson;
113
+ }
114
+ setSchema(schema) {
115
+ const plugin = this.matterbridge.plugins.get(this.name);
116
+ if (!plugin) {
117
+ throw new Error(`Plugin ${this.name} not found`);
118
+ }
119
+ plugin.schemaJson = schema;
120
+ }
107
121
  wssSendRestartRequired(snackbar = true, fixed = false) {
108
122
  this.matterbridge.frontend.wssSendRestartRequired(snackbar, fixed);
109
123
  }
@@ -124,12 +138,15 @@ export class MatterbridgePlatform {
124
138
  if (aggregator) {
125
139
  if (aggregator.parts.has(name.replaceAll(' ', '') + ':' + type)) {
126
140
  this.log.warn(`Virtual device ${name} already registered. Please use a different name.`);
141
+ return false;
127
142
  }
128
143
  else {
129
144
  await addVirtualDevice(aggregator, name.slice(0, 32), type, callback);
130
145
  this.log.info(`Virtual device ${name} created.`);
146
+ return true;
131
147
  }
132
148
  }
149
+ return false;
133
150
  }
134
151
  async registerDevice(device) {
135
152
  device.plugin = this.name;
@@ -776,7 +776,7 @@ export class PluginManager extends EventEmitter {
776
776
  }
777
777
  }
778
778
  catch (err) {
779
- logError(this.log, `Failed to load plugin ${plg}${plugin.name}${er}`, err);
779
+ inspectError(this.log, `Failed to load plugin ${plg}${plugin.name}${er}`, err);
780
780
  plugin.error = true;
781
781
  }
782
782
  return undefined;
@@ -1,10 +1,10 @@
1
1
  import { inspect } from 'node:util';
2
2
  import { RESET } from 'node-ansi-logger';
3
3
  export function logError(log, message, error) {
4
- log.error(`${message}: ${error instanceof Error ? error.message + '\nStack:\n' + error.stack : error}`);
4
+ log.error(`${message}: ${error instanceof Error ? error.message + ' \nStack: \n' + error.stack : error}`);
5
5
  }
6
6
  export function inspectError(log, message, error) {
7
- const errorMessage = error instanceof Error ? `${error.message}\n` : '';
7
+ const errorMessage = error instanceof Error ? `${error.message} \n` : '';
8
8
  const inspectedError = inspect(error, { depth: 10, colors: true, showHidden: false });
9
9
  log.error(`${message}: ${errorMessage}${RESET}${inspectedError}`);
10
10
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.3.7-dev-20251109-a306ab9",
3
+ "version": "3.3.8-dev-20251114-9b65e59",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.3.7-dev-20251109-a306ab9",
9
+ "version": "3.3.8-dev-20251114-9b65e59",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.6",
@@ -367,9 +367,9 @@
367
367
  "license": "MIT"
368
368
  },
369
369
  "node_modules/bare-events": {
370
- "version": "2.8.1",
371
- "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz",
372
- "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==",
370
+ "version": "2.8.2",
371
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
372
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
373
373
  "license": "Apache-2.0",
374
374
  "peerDependencies": {
375
375
  "bare-abort-controller": "*"
@@ -1507,9 +1507,9 @@
1507
1507
  }
1508
1508
  },
1509
1509
  "node_modules/path-scurry": {
1510
- "version": "2.0.0",
1511
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
1512
- "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
1510
+ "version": "2.0.1",
1511
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
1512
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
1513
1513
  "license": "BlueOak-1.0.0",
1514
1514
  "dependencies": {
1515
1515
  "lru-cache": "^11.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.3.7-dev-20251109-a306ab9",
3
+ "version": "3.3.8-dev-20251114-9b65e59",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",
@@ -65,6 +65,10 @@
65
65
  "import": "./dist/utils/export.js",
66
66
  "types": "./dist/utils/export.d.ts"
67
67
  },
68
+ "./jestutils": {
69
+ "import": "./dist/jestutils/export.js",
70
+ "types": "./dist/jestutils/export.d.ts"
71
+ },
68
72
  "./logger": {
69
73
  "import": "./dist/logger/export.js",
70
74
  "types": "./dist/logger/export.d.ts"
@@ -0,0 +1,100 @@
1
+ /* eslint-disable */
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join as pathJoin, sep as pathSep } from 'node:path';
4
+ import https from 'node:https';
5
+
6
+ /**
7
+ * Fetch the canonical Matter ZCL JSON file from the connectedhomeip repository
8
+ * and store it locally for offline / deterministic revision checks.
9
+ *
10
+ * Environment overrides:
11
+ * - ZCL_OUT: output path for zcl.json (default: chip/zcl.json)
12
+ * - ZCL_BRANCH: connectedhomeip branch to fetch from (default: v1.4-branch)
13
+ */
14
+ const OUT_PATH = process.env.ZCL_OUT || 'chip/zcl.json';
15
+ // Single branch strategy from online only
16
+ const BRANCH = process.env.ZCL_BRANCH || 'v1.4-branch';
17
+ const ZCL_BASE = `https://raw.githubusercontent.com/project-chip/connectedhomeip/${BRANCH}/src/app/zap-templates/zcl/`;
18
+ const ZCL_JSON_URL = ZCL_BASE + 'zcl.json';
19
+ const XML_BASE = ZCL_BASE + 'data-model/chip/';
20
+ const MANUFACTURERS_URL = ZCL_BASE + 'data-model/manufacturers.xml';
21
+
22
+ function fetchUrl(url) {
23
+ return new Promise((resolve, reject) => {
24
+ https.get(url, (res) => {
25
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
26
+ // Follow redirect
27
+ return resolve(fetchUrl(res.headers.location));
28
+ }
29
+ if (res.statusCode !== 200) {
30
+ return reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`));
31
+ }
32
+ const chunks = [];
33
+ res.on('data', (c) => chunks.push(c));
34
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
35
+ }).on('error', reject);
36
+ });
37
+ }
38
+
39
+ async function main() {
40
+ await mkdir(dirname(OUT_PATH), { recursive: true });
41
+ // Always fetch online from the specified branch
42
+ const data = await fetchUrl(ZCL_JSON_URL);
43
+
44
+ // Structural sanity check: this is a ZAP ZCL properties file, expected to include xml roots and files
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(data);
48
+ } catch (e) {
49
+ throw new Error(`Downloaded content is not valid JSON: ${e.message}`);
50
+ }
51
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.xmlFile)) {
52
+ process.stderr.write('Warning: zcl.json does not contain expected ZAP ZCL properties (xmlFile). Saving anyway for manual inspection.\n');
53
+ }
54
+
55
+ await writeFile(OUT_PATH, JSON.stringify(parsed, null, 2));
56
+ process.stderr.write(`Saved ${OUT_PATH}.\n`);
57
+
58
+ // Also fetch manufacturers.xml to chip/manufacturers.xml from the same branch
59
+ try {
60
+ const outManu = 'chip/manufacturers.xml';
61
+ const manuContent = await fetchUrl(MANUFACTURERS_URL);
62
+ await writeFile(outManu, manuContent);
63
+ process.stderr.write('Saved chip/manufacturers.xml.\n');
64
+ } catch (e) {
65
+ process.stderr.write(`Warning: failed to fetch manufacturers.xml (${e.message}).\n`);
66
+ }
67
+
68
+ // Always fetch XMLs referenced by zcl.json from the same branch under data-model/chip
69
+ const xmlFiles = Array.isArray(parsed.xmlFile) ? parsed.xmlFile : [];
70
+ if (xmlFiles.length === 0) {
71
+ process.stderr.write('No xmlFile entries found; skipping XML fetch.\n');
72
+ } else {
73
+ const outXmlBase = 'chip/xml';
74
+ await mkdir(outXmlBase, { recursive: true });
75
+ let ok = 0;
76
+ let fail = 0;
77
+ for (const fileName of xmlFiles) {
78
+ const relative = fileName.trim();
79
+ try {
80
+ const url = XML_BASE + relative;
81
+ const content = await fetchUrl(url);
82
+ const outPath = pathJoin(outXmlBase, relative.replaceAll('/', pathSep));
83
+ await mkdir(dirname(outPath), { recursive: true });
84
+ await writeFile(outPath, content);
85
+ ok++;
86
+ } catch (e) {
87
+ fail++;
88
+ process.stderr.write(`Failed XML: ${relative} (${e.message})\n`);
89
+ }
90
+ }
91
+ process.stderr.write(`Fetched XML files: ${ok} ok, ${fail} failed. Output under ${outXmlBase}. Branch=${BRANCH}\n`);
92
+ }
93
+ // Ensure success exit unless an unhandled error occurred
94
+ process.exit(0);
95
+ }
96
+
97
+ main().catch((err) => {
98
+ process.stderr.write(`Error: ${err.message}\n`);
99
+ process.exitCode = 1;
100
+ });