matterbridge 3.3.8-dev-20251117-e3fb774 → 3.3.9-dev-20251118-930cfdb

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,24 @@ 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.9] - Not published
32
+
33
+ ### Development Breaking Changes
34
+
35
+ Removed 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
+ ### Changed
42
+
43
+ - [package]: Updated dependencies.
44
+
45
+ <a href="https://www.buymeacoffee.com/luligugithub">
46
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
47
+ </a>
48
+
31
49
  ## [3.3.8] - 2025-11-15
32
50
 
33
51
  ### Development Breaking Changes
package/README-DEV.md CHANGED
@@ -199,7 +199,7 @@ The plugin platform type.
199
199
  The plugin config (loaded before the platform constructor is called and saved after onShutdown() is called).
200
200
  Here you can store your plugin configuration (see matterbridge-zigbee2mqtt for example)
201
201
 
202
- ### constructor(matterbridge: Matterbridge, log: AnsiLogger, config: PlatformConfig)
202
+ ### constructor(matterbridge: PlatformMatterbridge, log: AnsiLogger, config: PlatformConfig)
203
203
 
204
204
  The contructor is called when is plugin is loaded.
205
205
 
@@ -282,7 +282,7 @@ You create a Matter device with a new instance of MatterbridgeEndpoint(definitio
282
282
 
283
283
  In the above example we create a contact sensor device type with also a power source device type feature replaceble battery.
284
284
 
285
- All device types are defined in src\matterbridgeDeviceTypes.ts and taken from the 'Matter-1.4-Device-Library-Specification.pdf'.
285
+ All device types are defined in src\matterbridgeDeviceTypes.ts and taken from the 'Matter-1.4.1-Device-Library-Specification.pdf'.
286
286
 
287
287
  All default cluster helpers are available as methods of MatterbridgeEndpoint.
288
288
 
@@ -293,12 +293,6 @@ All default cluster helpers are available as methods of MatterbridgeEndpoint.
293
293
  - @param {string} [id] - The unique storage key for the endpoint. If not provided, a default key will be used.
294
294
  - @param {EndpointNumber} [number] - The endpoint number for the endpoint. If not provided, the endpoint will be created with the next available endpoint number.
295
295
 
296
- ```typescript
297
- const robot = new RoboticVacuumCleaner('Robot Vacuum', 'RVC1238777820', 'server');
298
- ```
299
-
300
- In the above example we create a Rvc device type with its own server node.
301
-
302
296
  The mode=`server` property of MatterbridgeEndpointOptions, allows to create an independent (not bridged) Matter device with its server node. In this case the bridge mode is not relevant.
303
297
 
304
298
  The mode=`matter` property of MatterbridgeEndpointOptions, allows to create a (not bridged) Matter device that is added to the Matterbridge server node alongside the aggregator.
@@ -318,50 +312,50 @@ const robot = new RoboticVacuumCleaner('Robot Vacuum', 'RVC1238777820', 'server'
318
312
  ### Chapter 13. Appliances Device Types - Single class device types
319
313
 
320
314
  ```typescript
321
- this.laundryWasher = new LaundryWasher('Laundry Washer', 'LW1234567890');
315
+ const laundryWasher = new LaundryWasher('Laundry Washer', 'LW1234567890');
322
316
  ```
323
317
 
324
318
  ```typescript
325
- this.laundryDryer = new LaundryDryer('Laundry Dryer', 'LDW1235227890');
319
+ const laundryDryer = new LaundryDryer('Laundry Dryer', 'LDW1235227890');
326
320
  ```
327
321
 
328
322
  ```typescript
329
- this.dishwasher = new Dishwasher('Dishwasher', 'DW1234567890');
323
+ const dishwasher = new Dishwasher('Dishwasher', 'DW1234567890');
330
324
  ```
331
325
 
332
326
  ```typescript
333
- this.extractorHood = new ExtractorHood('Extractor Hood', 'EH1234567893');
327
+ const extractorHood = new ExtractorHood('Extractor Hood', 'EH1234567893');
334
328
  ```
335
329
 
336
330
  ```typescript
337
- this.microwaveOven = new MicrowaveOven('Microwave Oven', 'MO1234567893');
331
+ const microwaveOven = new MicrowaveOven('Microwave Oven', 'MO1234567893');
338
332
  ```
339
333
 
340
334
  The Oven is always a composed device. You create the Oven and add one or more cabinet.
341
335
 
342
336
  ```typescript
343
- this.oven = new Oven('Oven', 'OV1234567890');
344
- this.oven.addCabinet('Upper Cabinet', [{ mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label }]);
337
+ const oven = new Oven('Oven', 'OV1234567890');
338
+ oven.addCabinet('Upper Cabinet', [{ mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label }]);
345
339
  ```
346
340
 
347
341
  The Cooktop is always a composed device. You create the Cooktop and add one or more surface.
348
342
 
349
343
  ```typescript
350
- this.cooktop = new Cooktop('Cooktop', 'CT1234567890');
351
- this.cooktop.addSurface('Surface Top Left', [
352
- { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label },
353
- { mfgCode: null, namespaceId: PositionTag.Left.namespaceId, tag: PositionTag.Left.tag, label: PositionTag.Left.label },
354
- ]);
344
+ const cooktop = new Cooktop('Cooktop', 'CT1234567890');
345
+ cooktop.addSurface('Surface Top Left', [
346
+ { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: PositionTag.Top.label },
347
+ { mfgCode: null, namespaceId: PositionTag.Left.namespaceId, tag: PositionTag.Left.tag, label: PositionTag.Left.label },
348
+ ]);
355
349
  ```
356
350
 
357
351
  The Refrigerator is always a composed device. You create the Refrigerator and add one or more cabinet.
358
352
 
359
353
  ```typescript
360
- const refrigerator = new Refrigerator('Refrigerator', 'RE1234567890');
361
- refrigerator.addCabinet('Refrigerator Top', [
362
- { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: 'Refrigerator Top' },
363
- { mfgCode: null, namespaceId: RefrigeratorTag.Refrigerator.namespaceId, tag: RefrigeratorTag.Refrigerator.tag, label: RefrigeratorTag.Refrigerator.label },
364
- ]);
354
+ const refrigerator = new Refrigerator('Refrigerator', 'RE1234567890');
355
+ refrigerator.addCabinet('Refrigerator Top', [
356
+ { mfgCode: null, namespaceId: PositionTag.Top.namespaceId, tag: PositionTag.Top.tag, label: 'Refrigerator Top' },
357
+ { mfgCode: null, namespaceId: RefrigeratorTag.Refrigerator.namespaceId, tag: RefrigeratorTag.Refrigerator.tag, label: RefrigeratorTag.Refrigerator.label },
358
+ ]);
365
359
  ```
366
360
 
367
361
  ### Chapter 14. Energy Device Types - Single class device types
@@ -394,6 +388,7 @@ Each plugin has a minimal default config file injected by Matterbridge when it i
394
388
  {
395
389
  name: plugin.name, // i.e. matterbridge-test
396
390
  type: plugin.type, // i.e. AccessoryPlatform or DynamicPlatform (on the first run is AnyPlatform cause it is still unknown)
391
+ version: plugin.version,
397
392
  debug: false,
398
393
  unregisterOnShutdown: false
399
394
  }
package/dist/frontend.js CHANGED
@@ -214,6 +214,7 @@ export class Frontend extends EventEmitter {
214
214
  this.httpServer.on('upgrade', async (req, socket, head) => {
215
215
  try {
216
216
  if ((req.headers.upgrade || '').toLowerCase() !== 'websocket') {
217
+ this.log.error(`WebSocket upgrade error: Invalid upgrade header ${req.headers.upgrade}`);
217
218
  socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
218
219
  return socket.destroy();
219
220
  }
@@ -1140,6 +1141,9 @@ export class Frontend extends EventEmitter {
1140
1141
  }
1141
1142
  client.send(JSON.stringify(data));
1142
1143
  }
1144
+ else {
1145
+ this.log.error('Cannot send api response, client not connected');
1146
+ }
1143
1147
  };
1144
1148
  try {
1145
1149
  data = JSON.parse(message.toString());
@@ -1,15 +1,18 @@
1
1
  import { rmSync } from 'node:fs';
2
2
  import { inspect } from 'node:util';
3
3
  import path from 'node:path';
4
- import { AnsiLogger, er, rs } from 'node-ansi-logger';
4
+ import { AnsiLogger, er, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
5
5
  import { LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Environment, Lifecycle } from '@matter/general';
6
6
  import { Endpoint, ServerNode, ServerNodeStore } from '@matter/node';
7
7
  import { DeviceTypeId, VendorId } from '@matter/types/datatype';
8
8
  import { AggregatorEndpoint } from '@matter/node/endpoints';
9
9
  import { MdnsService } from '@matter/main/protocol';
10
+ import { NodeStorageManager } from 'node-persist-manager';
10
11
  import { Matterbridge } from '../matterbridge.js';
11
- import { MATTER_STORAGE_NAME } from '../matterbridgeTypes.js';
12
+ import { MATTER_STORAGE_NAME, NODE_STORAGE_DIR } from '../matterbridgeTypes.js';
12
13
  import { bridge } from '../matterbridgeDeviceTypes.js';
14
+ export const originalProcessArgv = Object.freeze([...process.argv]);
15
+ export const originalProcessEnv = Object.freeze({ ...process.env });
13
16
  export let loggerLogSpy;
14
17
  export let loggerDebugSpy;
15
18
  export let loggerInfoSpy;
@@ -25,7 +28,12 @@ export let consoleErrorSpy;
25
28
  export let addBridgedEndpointSpy;
26
29
  export let removeBridgedEndpointSpy;
27
30
  export let removeAllBridgedEndpointsSpy;
31
+ export let NAME;
32
+ export let HOMEDIR;
28
33
  export let matterbridge;
34
+ export let frontend;
35
+ export let plugins;
36
+ export let devices;
29
37
  export let environment;
30
38
  export let server;
31
39
  export let aggregator;
@@ -34,7 +42,9 @@ export async function setupTest(name, debug = false) {
34
42
  expect(name).toBeDefined();
35
43
  expect(typeof name).toBe('string');
36
44
  expect(name.length).toBeGreaterThanOrEqual(4);
37
- rmSync(path.join('jest', name), { recursive: true, force: true });
45
+ NAME = name;
46
+ HOMEDIR = path.join('jest', name);
47
+ rmSync(HOMEDIR, { recursive: true, force: true });
38
48
  const { jest } = await import('@jest/globals');
39
49
  loggerDebugSpy = jest.spyOn(AnsiLogger.prototype, 'debug');
40
50
  loggerInfoSpy = jest.spyOn(AnsiLogger.prototype, 'info');
@@ -87,6 +97,89 @@ export async function setDebug(debug) {
87
97
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
88
98
  }
89
99
  }
100
+ export async function startMatterbridge(bridgeMode = 'bridge', frontendPort = 8283, matterPort = 5540, passcode = 20252026, discriminator = 3840, pluginSize = 0, devicesSize = 0) {
101
+ process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS'] = '100';
102
+ process.env['MATTERBRIDGE_PAUSE_MATTER_INTERVAL_MS'] = '100';
103
+ process.argv.length = 0;
104
+ process.argv.push(...originalProcessArgv, '-novirtual', '-debug', '-verbose', '-logger', 'debug', '-matterlogger', 'debug', bridgeMode === '' ? '-test' : '-' + bridgeMode, '-homedir', HOMEDIR, '-frontend', frontendPort.toString(), '-port', matterPort.toString(), '-passcode', passcode.toString(), '-discriminator', discriminator.toString());
105
+ matterbridge = await Matterbridge.loadInstance(true);
106
+ expect(matterbridge).toBeDefined();
107
+ expect(matterbridge.profile).toBeUndefined();
108
+ expect(matterbridge.bridgeMode).toBe(bridgeMode);
109
+ frontend = matterbridge.frontend;
110
+ plugins = matterbridge.plugins;
111
+ devices = matterbridge.devices;
112
+ expect(matterbridge.initialized).toBeTruthy();
113
+ expect(matterbridge.log).toBeDefined();
114
+ expect(matterbridge.rootDirectory).toBe(path.resolve('./'));
115
+ expect(matterbridge.homeDirectory).toBe(path.join('jest', NAME));
116
+ expect(matterbridge.matterbridgeDirectory).toBe(path.join('jest', NAME, '.matterbridge'));
117
+ expect(matterbridge.matterbridgePluginDirectory).toBe(path.join('jest', NAME, 'Matterbridge'));
118
+ expect(matterbridge.matterbridgeCertDirectory).toBe(path.join('jest', NAME, '.mattercert'));
119
+ expect(plugins).toBeDefined();
120
+ expect(plugins.size).toBe(pluginSize);
121
+ expect(devices).toBeDefined();
122
+ expect(devices.size).toBe(devicesSize);
123
+ expect(frontend).toBeDefined();
124
+ expect(frontend.listening).toBeTruthy();
125
+ expect(frontend.httpServer).toBeDefined();
126
+ expect(frontend.httpsServer).toBeUndefined();
127
+ expect(frontend.expressApp).toBeDefined();
128
+ expect(frontend.webSocketServer).toBeDefined();
129
+ expect(matterbridge.nodeStorage).toBeDefined();
130
+ expect(matterbridge.nodeContext).toBeDefined();
131
+ expect(Environment.default.vars.get('path.root')).toBe(path.join(matterbridge.matterbridgeDirectory, MATTER_STORAGE_NAME));
132
+ expect(matterbridge.matterStorageService).toBeDefined();
133
+ expect(matterbridge.matterStorageManager).toBeDefined();
134
+ expect(matterbridge.matterbridgeContext).toBeDefined();
135
+ expect(matterbridge.controllerContext).toBeUndefined();
136
+ if (bridgeMode === 'bridge') {
137
+ expect(matterbridge.serverNode).toBeDefined();
138
+ expect(matterbridge.aggregatorNode).toBeDefined();
139
+ }
140
+ expect(matterbridge.mdnsInterface).toBe(undefined);
141
+ expect(matterbridge.port).toBe(matterPort + (bridgeMode === 'bridge' ? 1 : 0));
142
+ expect(matterbridge.passcode).toBe(passcode + (bridgeMode === 'bridge' ? 1 : 0));
143
+ expect(matterbridge.discriminator).toBe(discriminator + (bridgeMode === 'bridge' ? 1 : 0));
144
+ if (bridgeMode === 'bridge') {
145
+ const started = new Promise((resolve) => {
146
+ matterbridge.once('bridge_started', () => {
147
+ resolve();
148
+ });
149
+ });
150
+ const online = new Promise((resolve) => {
151
+ matterbridge.once('online', (name) => {
152
+ if (name === 'Matterbridge')
153
+ resolve();
154
+ });
155
+ });
156
+ await Promise.all([started, online]);
157
+ }
158
+ else if (bridgeMode === 'childbridge') {
159
+ await new Promise((resolve) => {
160
+ matterbridge.once('childbridge_started', () => {
161
+ resolve();
162
+ });
163
+ });
164
+ }
165
+ expect(loggerLogSpy).toHaveBeenCalledWith("info", `The frontend http server is listening on ${UNDERLINE}http://${matterbridge.systemInformation.ipv4Address}:${frontendPort}${UNDERLINEOFF}${rs}`);
166
+ if (bridgeMode === 'bridge') {
167
+ expect(loggerLogSpy).toHaveBeenCalledWith("notice", `Starting Matterbridge server node`);
168
+ expect(loggerLogSpy).toHaveBeenCalledWith("notice", `Server node for Matterbridge is online`);
169
+ expect(loggerLogSpy).toHaveBeenCalledWith("debug", `Starting start matter interval in bridge mode...`);
170
+ expect(loggerLogSpy).toHaveBeenCalledWith("debug", `Cleared startMatterInterval interval in bridge mode`);
171
+ expect(loggerLogSpy).toHaveBeenCalledWith("notice", `Matterbridge bridge started successfully`);
172
+ }
173
+ else if (bridgeMode === 'childbridge') {
174
+ expect(loggerLogSpy).toHaveBeenCalledWith("debug", `Starting start matter interval in childbridge mode...`);
175
+ expect(loggerLogSpy).toHaveBeenCalledWith("debug", `Cleared startMatterInterval interval in childbridge mode`);
176
+ expect(loggerLogSpy).toHaveBeenCalledWith("notice", `Matterbridge childbridge started successfully`);
177
+ }
178
+ return matterbridge;
179
+ }
180
+ export async function stopMatterbridge(cleanupPause = 10, destroyPause = 250) {
181
+ await destroyMatterbridgeEnvironment(cleanupPause, destroyPause);
182
+ }
90
183
  export async function createMatterbridgeEnvironment(name) {
91
184
  matterbridge = await Matterbridge.loadInstance(false);
92
185
  expect(matterbridge).toBeDefined();
@@ -106,6 +199,8 @@ export async function createMatterbridgeEnvironment(name) {
106
199
  return matterbridge;
107
200
  }
108
201
  export async function startMatterbridgeEnvironment(port = 5540) {
202
+ matterbridge.nodeStorage = new NodeStorageManager({ dir: path.join(matterbridge.matterbridgeDirectory, NODE_STORAGE_DIR), writeQueue: false, expiredInterval: undefined, logging: false });
203
+ matterbridge.nodeContext = await matterbridge.nodeStorage.createStorage('matterbridge');
109
204
  await matterbridge.startMatterStorage();
110
205
  expect(matterbridge.matterStorageService).toBeDefined();
111
206
  expect(matterbridge.matterStorageManager).toBeDefined();
@@ -180,6 +275,8 @@ export async function stopMatterbridgeEnvironment() {
180
275
  expect(matterbridge.matterStorageService).not.toBeDefined();
181
276
  expect(matterbridge.matterStorageManager).not.toBeDefined();
182
277
  expect(matterbridge.matterbridgeContext).not.toBeDefined();
278
+ await matterbridge.nodeContext?.close();
279
+ await matterbridge.nodeStorage?.close();
183
280
  await flushAsync();
184
281
  }
185
282
  export async function destroyMatterbridgeEnvironment(cleanupPause = 10, destroyPause = 250) {
@@ -1128,7 +1128,7 @@ export class Matterbridge extends EventEmitter {
1128
1128
  await this.serverNode.add(this.aggregatorNode);
1129
1129
  await addVirtualDevices(this, this.aggregatorNode);
1130
1130
  await this.startPlugins();
1131
- this.log.debug('Starting start matter interval in bridge mode');
1131
+ this.log.debug('Starting start matter interval in bridge mode...');
1132
1132
  let failCount = 0;
1133
1133
  this.startMatterInterval = setInterval(async () => {
1134
1134
  for (const plugin of this.plugins) {
@@ -1156,7 +1156,7 @@ export class Matterbridge extends EventEmitter {
1156
1156
  }
1157
1157
  clearInterval(this.startMatterInterval);
1158
1158
  this.startMatterInterval = undefined;
1159
- this.log.debug('Cleared startMatterInterval interval for Matterbridge');
1159
+ this.log.debug('Cleared startMatterInterval interval in bridge mode');
1160
1160
  this.startServerNode(this.serverNode);
1161
1161
  for (const device of this.devices.array()) {
1162
1162
  if (device.mode === 'server' && device.serverNode) {
@@ -1190,7 +1190,7 @@ export class Matterbridge extends EventEmitter {
1190
1190
  this.log.notice('Matterbridge bridge started successfully');
1191
1191
  this.frontend.wssSendRefreshRequired('settings');
1192
1192
  this.frontend.wssSendRefreshRequired('plugins');
1193
- }, this.startMatterIntervalMs);
1193
+ }, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
1194
1194
  }
1195
1195
  async startChildbridge(delay = 1000) {
1196
1196
  if (!this.matterStorageManager)
@@ -1236,7 +1236,7 @@ export class Matterbridge extends EventEmitter {
1236
1236
  clearInterval(this.startMatterInterval);
1237
1237
  this.startMatterInterval = undefined;
1238
1238
  if (delay > 0)
1239
- await wait(delay);
1239
+ await wait(Number(process.env['MATTERBRIDGE_PAUSE_MATTER_INTERVAL_MS']) || delay);
1240
1240
  this.log.debug('Cleared startMatterInterval interval in childbridge mode');
1241
1241
  this.configureTimeout = setTimeout(async () => {
1242
1242
  for (const plugin of this.plugins.array()) {
@@ -1291,7 +1291,7 @@ export class Matterbridge extends EventEmitter {
1291
1291
  this.log.notice('Matterbridge childbridge started successfully');
1292
1292
  this.frontend.wssSendRefreshRequired('settings');
1293
1293
  this.frontend.wssSendRefreshRequired('plugins');
1294
- }, this.startMatterIntervalMs);
1294
+ }, Number(process.env['MATTERBRIDGE_START_MATTER_INTERVAL_MS']) || this.startMatterIntervalMs);
1295
1295
  }
1296
1296
  async startController() {
1297
1297
  }
@@ -1373,6 +1373,7 @@ export class Matterbridge extends EventEmitter {
1373
1373
  this.log.debug(`- hardwareVersion: ${await storageContext.get('hardwareVersion')} hardwareVersionString: ${await storageContext.get('hardwareVersionString')}`);
1374
1374
  const serverNode = await ServerNode.create({
1375
1375
  id: storeId,
1376
+ environment: this.environment,
1376
1377
  network: {
1377
1378
  listeningAddressIpv4: this.ipv4Address,
1378
1379
  listeningAddressIpv6: this.ipv6Address,
@@ -63,6 +63,7 @@ import { ThermostatUserInterfaceConfigurationServer } from '@matter/node/behavio
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
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
+ import { inspectError } from './utils/error.js';
66
67
  export class MatterbridgeEndpoint extends Endpoint {
67
68
  static logLevel = "info";
68
69
  mode = undefined;
@@ -84,7 +85,6 @@ export class MatterbridgeEndpoint extends Endpoint {
84
85
  productUrl = 'https://www.npmjs.com/package/matterbridge';
85
86
  tagList = undefined;
86
87
  originalId = undefined;
87
- uniqueStorageKey = undefined;
88
88
  name = undefined;
89
89
  deviceType = undefined;
90
90
  deviceTypes = new Map();
@@ -122,20 +122,16 @@ export class MatterbridgeEndpoint extends Endpoint {
122
122
  behaviors: options.tagList ? SupportedBehaviors(DescriptorServer.with(Descriptor.Feature.TagList)) : SupportedBehaviors(DescriptorServer),
123
123
  };
124
124
  const endpointV8 = MutableEndpoint(deviceTypeDefinitionV8);
125
- if (options.uniqueStorageKey && checkNotLatinCharacters(options.uniqueStorageKey)) {
126
- options.uniqueStorageKey = generateUniqueId(options.uniqueStorageKey);
127
- }
128
125
  if (options.id && checkNotLatinCharacters(options.id)) {
129
126
  options.id = generateUniqueId(options.id);
130
127
  }
131
128
  const optionsV8 = {
132
- id: options.id?.replace(/[ .]/g, '') || options.uniqueStorageKey?.replace(/[ .]/g, ''),
133
- number: options.number || options.endpointId,
129
+ id: options.id?.replace(/[ .]/g, ''),
130
+ number: options.number,
134
131
  descriptor: options.tagList ? { tagList: options.tagList, deviceTypeList } : { deviceTypeList },
135
132
  };
136
133
  super(endpointV8, optionsV8);
137
134
  this.mode = options.mode;
138
- this.uniqueStorageKey = options.id ?? options.uniqueStorageKey;
139
135
  this.originalId = originalId;
140
136
  this.name = firstDefinition.name;
141
137
  this.deviceType = firstDefinition.code;
@@ -147,7 +143,7 @@ export class MatterbridgeEndpoint extends Endpoint {
147
143
  }
148
144
  else
149
145
  this.deviceTypes.set(firstDefinition.code, firstDefinition);
150
- this.log = new AnsiLogger({ logName: this.originalId ?? this.uniqueStorageKey ?? 'MatterbridgeEndpoint', logTimestampFormat: 4, logLevel: debug === true ? "debug" : MatterbridgeEndpoint.logLevel });
146
+ this.log = new AnsiLogger({ logName: this.originalId ?? 'MatterbridgeEndpoint', logTimestampFormat: 4, logLevel: debug === true ? "debug" : MatterbridgeEndpoint.logLevel });
151
147
  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}`);
152
148
  this.behaviors.require(MatterbridgeServer, { log: this.log, commandHandler: this.commandHandler });
153
149
  }
@@ -279,11 +275,21 @@ export class MatterbridgeEndpoint extends Endpoint {
279
275
  return child;
280
276
  if (this.lifecycle.isInstalled) {
281
277
  this.log.debug(`- with lifecycle installed`);
282
- this.add(child);
278
+ try {
279
+ this.add(child);
280
+ }
281
+ catch (error) {
282
+ inspectError(this.log, `addChildDeviceType: error adding (with lifecycle installed) child endpoint ${CYAN}${endpointName}${db}`, error);
283
+ }
283
284
  }
284
285
  else {
285
286
  this.log.debug(`- with lifecycle NOT installed`);
286
- this.parts.add(child);
287
+ try {
288
+ this.parts.add(child);
289
+ }
290
+ catch (error) {
291
+ inspectError(this.log, `addChildDeviceType: error adding (with lifecycle NOT installed) child endpoint ${CYAN}${endpointName}${db}`, error);
292
+ }
287
293
  }
288
294
  return child;
289
295
  }
@@ -341,11 +347,21 @@ export class MatterbridgeEndpoint extends Endpoint {
341
347
  return child;
342
348
  if (this.lifecycle.isInstalled) {
343
349
  this.log.debug(`- with lifecycle installed`);
344
- this.add(child);
350
+ try {
351
+ this.add(child);
352
+ }
353
+ catch (error) {
354
+ inspectError(this.log, `addChildDeviceType: error adding (with lifecycle installed) child endpoint ${CYAN}${endpointName}${db}`, error);
355
+ }
345
356
  }
346
357
  else {
347
358
  this.log.debug(`- with lifecycle NOT installed`);
348
- this.parts.add(child);
359
+ try {
360
+ this.parts.add(child);
361
+ }
362
+ catch (error) {
363
+ inspectError(this.log, `addChildDeviceType: error adding (with lifecycle NOT installed) child endpoint ${CYAN}${endpointName}${db}`, error);
364
+ }
349
365
  }
350
366
  return child;
351
367
  }
@@ -1007,7 +1023,7 @@ export class MatterbridgeEndpoint extends Endpoint {
1007
1023
  }
1008
1024
  createSmokeOnlySmokeCOAlarmClusterServer(smokeState = SmokeCoAlarm.AlarmState.Normal) {
1009
1025
  this.behaviors.require(MatterbridgeSmokeCoAlarmServer.with(SmokeCoAlarm.Feature.SmokeAlarm).enable({
1010
- events: { smokeAlarm: true, interconnectSmokeAlarm: false, coAlarm: false, interconnectCoAlarm: false, lowBattery: true, hardwareFault: true, endOfService: true, selfTestComplete: true, alarmMuted: true, muteEnded: true, allClear: true },
1026
+ events: { smokeAlarm: true, interconnectSmokeAlarm: false, lowBattery: true, hardwareFault: true, endOfService: true, selfTestComplete: true, alarmMuted: true, muteEnded: true, allClear: true },
1011
1027
  }), {
1012
1028
  smokeState,
1013
1029
  expressedState: SmokeCoAlarm.ExpressedState.Normal,
@@ -1021,7 +1037,7 @@ export class MatterbridgeEndpoint extends Endpoint {
1021
1037
  }
1022
1038
  createCoOnlySmokeCOAlarmClusterServer(coState = SmokeCoAlarm.AlarmState.Normal) {
1023
1039
  this.behaviors.require(MatterbridgeSmokeCoAlarmServer.with(SmokeCoAlarm.Feature.CoAlarm).enable({
1024
- events: { smokeAlarm: false, interconnectSmokeAlarm: false, coAlarm: true, interconnectCoAlarm: false, lowBattery: true, hardwareFault: true, endOfService: true, selfTestComplete: true, alarmMuted: true, muteEnded: true, allClear: true },
1040
+ events: { coAlarm: true, interconnectCoAlarm: false, lowBattery: true, hardwareFault: true, endOfService: true, selfTestComplete: true, alarmMuted: true, muteEnded: true, allClear: true },
1025
1041
  }), {
1026
1042
  coState,
1027
1043
  expressedState: SmokeCoAlarm.ExpressedState.Normal,
@@ -792,6 +792,7 @@ export function getDefaultOccupancySensingClusterServer(occupied = false, holdTi
792
792
  occupancySensorTypeBitmap: { pir: true, ultrasonic: false, physicalContact: false },
793
793
  pirOccupiedToUnoccupiedDelay: holdTime,
794
794
  pirUnoccupiedToOccupiedDelay: holdTime,
795
+ pirUnoccupiedToOccupiedThreshold: 1,
795
796
  holdTime,
796
797
  holdTimeLimits: { holdTimeMin, holdTimeMax, holdTimeDefault: holdTime },
797
798
  });
@@ -288,19 +288,19 @@ export class PluginManager extends EventEmitter {
288
288
  this.log.debug(`Saved ${BLUE}${plugins.length}${db} plugins to storage`);
289
289
  return plugins.length;
290
290
  }
291
- async resolve(pluginPath) {
291
+ async resolve(nameOrPath) {
292
292
  const { default: path } = await import('node:path');
293
293
  const { promises } = await import('node:fs');
294
- if (!pluginPath.endsWith('package.json'))
295
- pluginPath = path.join(pluginPath, 'package.json');
296
- let packageJsonPath = path.resolve(pluginPath);
294
+ if (!nameOrPath.endsWith('package.json'))
295
+ nameOrPath = path.join(nameOrPath, 'package.json');
296
+ let packageJsonPath = path.resolve(nameOrPath);
297
297
  this.log.debug(`Resolving plugin path ${plg}${packageJsonPath}${db}`);
298
298
  try {
299
299
  await promises.access(packageJsonPath);
300
300
  }
301
301
  catch {
302
302
  this.log.debug(`Package.json not found at ${plg}${packageJsonPath}${db}`);
303
- packageJsonPath = path.join(this.matterbridge.globalModulesDirectory, pluginPath);
303
+ packageJsonPath = path.join(this.matterbridge.globalModulesDirectory, nameOrPath);
304
304
  this.log.debug(`Trying at ${plg}${packageJsonPath}${db}`);
305
305
  }
306
306
  try {
@@ -359,11 +359,11 @@ export class PluginManager extends EventEmitter {
359
359
  this.log.error(`Please open an issue on the plugin repository to remove them.`);
360
360
  return null;
361
361
  }
362
- this.log.debug(`Resolved plugin path ${plg}${pluginPath}${db}: ${packageJsonPath}`);
362
+ this.log.debug(`Resolved plugin path ${plg}${nameOrPath}${db}: ${packageJsonPath}`);
363
363
  return packageJsonPath;
364
364
  }
365
365
  catch (err) {
366
- logError(this.log, `Failed to resolve plugin path ${plg}${pluginPath}${er}`, err);
366
+ logError(this.log, `Failed to resolve plugin path ${plg}${nameOrPath}${er}`, err);
367
367
  return null;
368
368
  }
369
369
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.3.8-dev-20251117-e3fb774",
3
+ "version": "3.3.9-dev-20251118-930cfdb",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge",
9
- "version": "3.3.8-dev-20251117-e3fb774",
9
+ "version": "3.3.9-dev-20251118-930cfdb",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@matter/main": "0.15.6",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge",
3
- "version": "3.3.8-dev-20251117-e3fb774",
3
+ "version": "3.3.9-dev-20251118-930cfdb",
4
4
  "description": "Matterbridge plugin manager for Matter",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",