@matterbridge/core 3.8.0-dev-20260530-1971f9d → 3.8.0-dev-20260601-a1bf774

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.
@@ -1,13 +1,18 @@
1
- import { BindingBehavior } from '@matter/node/behaviors/binding';
1
+ import { Endpoint } from '@matter/main/node';
2
+ import { BindingBehavior, BindingServer } from '@matter/node/behaviors/binding';
2
3
  import { ClusterId } from '@matter/types';
3
- export declare class MatterbridgeBindingServer extends BindingBehavior {
4
+ import { Binding } from '@matter/types/clusters/binding';
5
+ export declare class MatterbridgeBindingServer extends BindingServer {
4
6
  protected internal: MatterbridgeBindingServer.Internal;
5
7
  state: MatterbridgeBindingServer.State;
6
8
  initialize(): Promise<void>;
9
+ [Symbol.asyncDispose](): Promise<void>;
10
+ getEndpoint(clusterId: ClusterId): Endpoint | undefined;
7
11
  }
8
12
  export declare namespace MatterbridgeBindingServer {
9
13
  class Internal {
10
14
  bound: boolean;
15
+ boundEndpoints: Map<Binding.Target, Endpoint>;
11
16
  }
12
17
  class State extends BindingBehavior.State {
13
18
  clientList: ClusterId[];
@@ -1,19 +1,53 @@
1
- import { BindingBehavior } from '@matter/node/behaviors/binding';
1
+ import { BindingBehavior, BindingServer } from '@matter/node/behaviors/binding';
2
+ import { DescriptorServer } from '@matter/node/behaviors/descriptor';
3
+ import { debugStringify, nt } from 'node-ansi-logger';
2
4
  import { MatterbridgeServer } from './matterbridgeServer.js';
3
- export class MatterbridgeBindingServer extends BindingBehavior {
5
+ export class MatterbridgeBindingServer extends BindingServer {
4
6
  async initialize() {
7
+ super.initialize();
5
8
  const device = this.endpoint.stateOf(MatterbridgeServer);
6
9
  device.log.info(`Initializing MatterbridgeBindingServer (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) with clientList: ${this.state.clientList.join(', ')}`);
10
+ const clientList = this.state.clientList;
11
+ this.endpoint.construction.onSuccess(async () => {
12
+ const currentClientList = this.endpoint.stateOf(DescriptorServer).clientList;
13
+ const toAddClientList = clientList.filter((id) => !currentClientList.includes(id));
14
+ if (toAddClientList.length > 0) {
15
+ await this.endpoint.setStateOf(DescriptorServer, { clientList: [...currentClientList, ...toAddClientList] });
16
+ device.log.info(`Adding client clusters to endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}: ${toAddClientList.join(', ')}`);
17
+ }
18
+ });
7
19
  this.reactTo(this.events.binding$Changed, (value) => {
8
20
  this.internal.bound = value.length > 0;
9
- device.log.notice(`MatterbridgeBindingServer (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) binding changed: ${value}, bound: ${this.internal.bound}`);
21
+ device.log.notice(`MatterbridgeBindingServer (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) binding changed: ${debugStringify(value)}${nt}, bound: ${this.internal.bound}`);
22
+ });
23
+ this.reactTo(this.events.established, async (resolution) => {
24
+ device.log.notice(`MatterbridgeBindingServer (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) binding established: kind ${resolution.kind} entry ${debugStringify(resolution.entry)}${nt}`);
25
+ this.internal.boundEndpoints.set(resolution.entry, resolution.endpoint);
26
+ if (resolution.kind !== 'client')
27
+ return;
28
+ await resolution.node.set({ network: { autoSubscribe: true } });
29
+ });
30
+ this.reactTo(this.events.removed, async (resolution) => {
31
+ device.log.notice(`MatterbridgeBindingServer (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber}) binding removed: kind ${resolution.kind} entry ${debugStringify(resolution.entry)}${nt}`);
32
+ this.internal.boundEndpoints.delete(resolution.entry);
10
33
  });
11
- await super.initialize();
34
+ }
35
+ async [Symbol.asyncDispose]() {
36
+ this.internal.boundEndpoints.clear();
37
+ await super[Symbol.asyncDispose]();
38
+ }
39
+ getEndpoint(clusterId) {
40
+ for (const [target, endpoint] of this.internal.boundEndpoints) {
41
+ if (target.cluster === clusterId)
42
+ return endpoint;
43
+ }
44
+ return undefined;
12
45
  }
13
46
  }
14
47
  (function (MatterbridgeBindingServer) {
15
48
  class Internal {
16
49
  bound = false;
50
+ boundEndpoints = new Map();
17
51
  }
18
52
  MatterbridgeBindingServer.Internal = Internal;
19
53
  class State extends BindingBehavior.State {
package/dist/frontend.js CHANGED
@@ -14,10 +14,11 @@ import { inspectError } from '@matterbridge/utils/error';
14
14
  import { formatBytes, formatPercent, formatUptime } from '@matterbridge/utils/format';
15
15
  import { isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString } from '@matterbridge/utils/validate';
16
16
  import { fireAndForget, wait, withTimeout } from '@matterbridge/utils/wait';
17
- import { AnsiLogger, CYAN, db, debugStringify, er, nf, nt, rs, stringify, UNDERLINE, UNDERLINEOFF, YELLOW } from 'node-ansi-logger';
17
+ import { AnsiLogger, bgHex, CYAN, db, debugStringify, er, GREEN, nf, nt, rs, stringify, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
18
18
  import { cliEmitter, lastOsCpuUsage, lastProcessCpuUsage } from './cliEmitter.js';
19
19
  import { generateHistoryPage } from './cliHistory.js';
20
20
  import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
21
+ import { logError } from './utils/export.js';
21
22
  if (hasParameter('loader'))
22
23
  console.log('\u001B[32mFrontend loaded.\u001B[40;0m');
23
24
  export class Frontend extends EventEmitter {
@@ -801,6 +802,50 @@ export class Frontend extends EventEmitter {
801
802
  res.status(500).send(`Error uploading or installing plugin package ${filename}`);
802
803
  }
803
804
  });
805
+ for (const plugin of this.matterbridge.plugins.array().filter((p) => p.enabled && !p.error)) {
806
+ const { existsSync } = await import('node:fs');
807
+ if (plugin.frontendPath && existsSync(plugin.frontendPath)) {
808
+ this.log.debug(`Registering frontend route for plugin ${plg}${plugin.name}${db} at ${GREEN}/plugins/${plugin.name}${db} with path ${CYAN}${plugin.frontendPath}${db}`);
809
+ this.expressApp.use(`/plugins/${plugin.name}`, express.static(path.dirname(plugin.frontendPath)));
810
+ const apiHandler = async (req, res) => {
811
+ const method = req.method.toUpperCase();
812
+ const path = Array.isArray(req.params.path) ? req.params.path[0] : req.params.path;
813
+ const query = req.query;
814
+ const body = req.body;
815
+ this.log.debug(`The frontend sent ${bgHex('#0ba7bc').bold.white ` ${method} `} ${bgHex('#0b6a2b').bold.italic.white ` /plugins/${plugin.name}/api/${path ?? ''} `}`);
816
+ if (!plugin.platform) {
817
+ this.log.error(`The platform for plugin ${plg}${plugin.name}${er} is not running`);
818
+ res.status(503).json({ error: `Plugin ${plugin.name} platform is not running` });
819
+ return;
820
+ }
821
+ try {
822
+ const value = await plugin.platform.onFetch(method, path, query, body);
823
+ if (value === undefined) {
824
+ this.log.warn(`MatterbridgePlatform.onFetch returned undefined for plugin ${plg}${plugin.name}${wr} method ${method} path ${path}`);
825
+ res.status(404).json({ error: `Not found in plugin ${plugin.name}` });
826
+ return;
827
+ }
828
+ if (method === 'DELETE')
829
+ res.status(204).send();
830
+ else
831
+ res.json(value);
832
+ }
833
+ catch (error) {
834
+ logError(this.log, `MatterbridgePlatform.onFetch threw an error for plugin ${plg}${plugin.name}${er}`, error);
835
+ res.status(500).json({ error: `Internal error in plugin ${plugin.name}` });
836
+ }
837
+ };
838
+ this.expressApp.get(`/plugins/${plugin.name}/api/:path`, apiHandler);
839
+ this.expressApp.post(`/plugins/${plugin.name}/api/:path`, apiHandler);
840
+ this.expressApp.put(`/plugins/${plugin.name}/api/:path`, apiHandler);
841
+ this.expressApp.patch(`/plugins/${plugin.name}/api/:path`, apiHandler);
842
+ this.expressApp.delete(`/plugins/${plugin.name}/api/:path`, apiHandler);
843
+ this.expressApp.get(`/plugins/${plugin.name}/{*splat}`, (_req, res) => {
844
+ this.log.warn(`The plugin frontend sent /plugins/${plugin.name}/index.html (plugin SPA fallback)`);
845
+ res.sendFile('index.html', { root: path.dirname(plugin.frontendPath) });
846
+ });
847
+ }
848
+ }
804
849
  this.expressApp.use((req, res) => {
805
850
  const filePath = path.resolve(this.matterbridge.rootDirectory, 'apps', 'frontend', 'build');
806
851
  this.log.debug(`The frontend sent ${req.url} method ${req.method}: sending index.html in ${filePath} as fallback`);
@@ -94,6 +94,8 @@ import { TemperatureControl } from '@matter/types/clusters/temperature-control';
94
94
  import { TemperatureMeasurement } from '@matter/types/clusters/temperature-measurement';
95
95
  import { Thermostat } from '@matter/types/clusters/thermostat';
96
96
  import { ThermostatUserInterfaceConfiguration } from '@matter/types/clusters/thermostat-user-interface-configuration';
97
+ import { TlsCertificateManagement } from '@matter/types/clusters/tls-certificate-management';
98
+ import { TlsClientManagement } from '@matter/types/clusters/tls-client-management';
97
99
  import { TotalVolatileOrganicCompoundsConcentrationMeasurement } from '@matter/types/clusters/total-volatile-organic-compounds-concentration-measurement';
98
100
  import { UserLabel } from '@matter/types/clusters/user-label';
99
101
  import { ValveConfigurationAndControl } from '@matter/types/clusters/valve-configuration-and-control';
@@ -169,8 +171,8 @@ export const OTAProvider = DeviceTypeDefinition({
169
171
  revision: 1,
170
172
  requiredServerClusters: [OtaSoftwareUpdateProvider.id],
171
173
  optionalServerClusters: [],
172
- requiredClientClusters: [OtaSoftwareUpdateRequestor.id],
173
- optionalClientClusters: [],
174
+ requiredClientClusters: [],
175
+ optionalClientClusters: [OtaSoftwareUpdateRequestor.id],
174
176
  });
175
177
  export const bridgedNode = DeviceTypeDefinition({
176
178
  name: 'MA-bridgedNode',
@@ -195,6 +197,8 @@ export const deviceEnergyManagement = DeviceTypeDefinition({
195
197
  revision: 3,
196
198
  requiredServerClusters: [DeviceEnergyManagement.id],
197
199
  optionalServerClusters: [DeviceEnergyManagementMode.id],
200
+ requiredClientClusters: [],
201
+ optionalClientClusters: [ElectricalGridConditions.id],
198
202
  });
199
203
  export const onOffLight = DeviceTypeDefinition({
200
204
  name: 'MA-onofflight',
@@ -203,6 +207,8 @@ export const onOffLight = DeviceTypeDefinition({
203
207
  revision: 3,
204
208
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id],
205
209
  optionalServerClusters: [LevelControl.id],
210
+ requiredClientClusters: [],
211
+ optionalClientClusters: [OccupancySensing.id],
206
212
  });
207
213
  export const dimmableLight = DeviceTypeDefinition({
208
214
  name: 'MA-dimmablelight',
@@ -211,6 +217,8 @@ export const dimmableLight = DeviceTypeDefinition({
211
217
  revision: 3,
212
218
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id, LevelControl.id],
213
219
  optionalServerClusters: [],
220
+ requiredClientClusters: [],
221
+ optionalClientClusters: [OccupancySensing.id],
214
222
  });
215
223
  export const colorTemperatureLight = DeviceTypeDefinition({
216
224
  name: 'MA-colortemperaturelight',
@@ -219,6 +227,8 @@ export const colorTemperatureLight = DeviceTypeDefinition({
219
227
  revision: 4,
220
228
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id, LevelControl.id, ColorControl.id],
221
229
  optionalServerClusters: [],
230
+ requiredClientClusters: [],
231
+ optionalClientClusters: [OccupancySensing.id],
222
232
  });
223
233
  export const extendedColorLight = DeviceTypeDefinition({
224
234
  name: 'MA-extendedcolorlight',
@@ -227,6 +237,8 @@ export const extendedColorLight = DeviceTypeDefinition({
227
237
  revision: 4,
228
238
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id, LevelControl.id, ColorControl.id],
229
239
  optionalServerClusters: [],
240
+ requiredClientClusters: [],
241
+ optionalClientClusters: [OccupancySensing.id],
230
242
  });
231
243
  export const onOffOutlet = DeviceTypeDefinition({
232
244
  name: 'MA-onoffpluginunit',
@@ -235,6 +247,8 @@ export const onOffOutlet = DeviceTypeDefinition({
235
247
  revision: 4,
236
248
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id],
237
249
  optionalServerClusters: [LevelControl.id],
250
+ requiredClientClusters: [],
251
+ optionalClientClusters: [OccupancySensing.id],
238
252
  });
239
253
  export const dimmableOutlet = DeviceTypeDefinition({
240
254
  name: 'MA-dimmablepluginunit',
@@ -243,6 +257,8 @@ export const dimmableOutlet = DeviceTypeDefinition({
243
257
  revision: 5,
244
258
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id, LevelControl.id],
245
259
  optionalServerClusters: [],
260
+ requiredClientClusters: [],
261
+ optionalClientClusters: [OccupancySensing.id],
246
262
  });
247
263
  export const onOffMountedSwitch = DeviceTypeDefinition({
248
264
  name: 'MA-onoffmountedswitch',
@@ -251,6 +267,8 @@ export const onOffMountedSwitch = DeviceTypeDefinition({
251
267
  revision: 2,
252
268
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id],
253
269
  optionalServerClusters: [LevelControl.id],
270
+ requiredClientClusters: [],
271
+ optionalClientClusters: [OccupancySensing.id],
254
272
  });
255
273
  export const dimmableMountedSwitch = DeviceTypeDefinition({
256
274
  name: 'MA-dimmablemountedswitch',
@@ -259,6 +277,8 @@ export const dimmableMountedSwitch = DeviceTypeDefinition({
259
277
  revision: 2,
260
278
  requiredServerClusters: [Identify.id, Groups.id, ScenesManagement.id, OnOff.id, LevelControl.id],
261
279
  optionalServerClusters: [],
280
+ requiredClientClusters: [],
281
+ optionalClientClusters: [OccupancySensing.id],
262
282
  });
263
283
  export const pumpDevice = DeviceTypeDefinition({
264
284
  name: 'MA-pump',
@@ -267,6 +287,8 @@ export const pumpDevice = DeviceTypeDefinition({
267
287
  revision: 3,
268
288
  requiredServerClusters: [OnOff.id, PumpConfigurationAndControl.id, Identify.id],
269
289
  optionalServerClusters: [LevelControl.id, Groups.id, ScenesManagement.id, TemperatureMeasurement.id, PressureMeasurement.id, FlowMeasurement.id],
290
+ requiredClientClusters: [],
291
+ optionalClientClusters: [TemperatureMeasurement.id, PressureMeasurement.id, FlowMeasurement.id, OccupancySensing.id],
270
292
  });
271
293
  export const waterValve = DeviceTypeDefinition({
272
294
  name: 'MA-waterValve',
@@ -275,6 +297,8 @@ export const waterValve = DeviceTypeDefinition({
275
297
  revision: 1,
276
298
  requiredServerClusters: [Identify.id, ValveConfigurationAndControl.id],
277
299
  optionalServerClusters: [FlowMeasurement.id],
300
+ requiredClientClusters: [],
301
+ optionalClientClusters: [FlowMeasurement.id],
278
302
  });
279
303
  export const onOffSwitch = DeviceTypeDefinition({
280
304
  name: 'MA-onoffswitch',
@@ -282,7 +306,9 @@ export const onOffSwitch = DeviceTypeDefinition({
282
306
  deviceClass: DeviceClasses.Simple,
283
307
  revision: 3,
284
308
  requiredServerClusters: [Identify.id, OnOff.id],
285
- optionalServerClusters: [Groups.id, ScenesManagement.id],
309
+ optionalServerClusters: [],
310
+ requiredClientClusters: [Identify.id, OnOff.id],
311
+ optionalClientClusters: [Groups.id, ScenesManagement.id],
286
312
  });
287
313
  export const dimmableSwitch = DeviceTypeDefinition({
288
314
  name: 'MA-dimmableswitch',
@@ -290,7 +316,9 @@ export const dimmableSwitch = DeviceTypeDefinition({
290
316
  deviceClass: DeviceClasses.Simple,
291
317
  revision: 3,
292
318
  requiredServerClusters: [Identify.id, OnOff.id, LevelControl.id],
293
- optionalServerClusters: [Groups.id, ScenesManagement.id],
319
+ optionalServerClusters: [],
320
+ requiredClientClusters: [Identify.id, OnOff.id, LevelControl.id],
321
+ optionalClientClusters: [Groups.id, ScenesManagement.id],
294
322
  });
295
323
  export const colorTemperatureSwitch = DeviceTypeDefinition({
296
324
  name: 'MA-colortemperatureswitch',
@@ -298,7 +326,9 @@ export const colorTemperatureSwitch = DeviceTypeDefinition({
298
326
  deviceClass: DeviceClasses.Simple,
299
327
  revision: 3,
300
328
  requiredServerClusters: [Identify.id, OnOff.id, LevelControl.id, ColorControl.id],
301
- optionalServerClusters: [Groups.id, ScenesManagement.id],
329
+ optionalServerClusters: [],
330
+ requiredClientClusters: [Identify.id, OnOff.id, LevelControl.id, ColorControl.id],
331
+ optionalClientClusters: [Groups.id, ScenesManagement.id],
302
332
  });
303
333
  export const genericSwitch = DeviceTypeDefinition({
304
334
  name: 'MA-genericswitch',
@@ -436,6 +466,8 @@ export const thermostatDevice = DeviceTypeDefinition({
436
466
  revision: 5,
437
467
  requiredServerClusters: [Identify.id, Thermostat.id],
438
468
  optionalServerClusters: [Groups.id, ThermostatUserInterfaceConfiguration.id, EnergyPreference.id],
469
+ requiredClientClusters: [],
470
+ optionalClientClusters: [FanControl.id, TemperatureMeasurement.id, RelativeHumidityMeasurement.id, OccupancySensing.id],
439
471
  });
440
472
  export const fanDevice = DeviceTypeDefinition({
441
473
  name: 'MA-fan',
@@ -655,7 +687,9 @@ export const heatPump = DeviceTypeDefinition({
655
687
  deviceClass: DeviceClasses.Simple,
656
688
  revision: 1,
657
689
  requiredServerClusters: [],
658
- optionalServerClusters: [Identify.id, Thermostat.id],
690
+ optionalServerClusters: [Identify.id],
691
+ requiredClientClusters: [],
692
+ optionalClientClusters: [Thermostat.id],
659
693
  });
660
694
  export const soilSensor = DeviceTypeDefinition({
661
695
  name: 'MA-soilSensor',
@@ -810,8 +844,9 @@ export const cameraController = DeviceTypeDefinition({
810
844
  ZoneManagement.id,
811
845
  CameraAvStreamManagement.id,
812
846
  CameraAvSettingsUserLevelManagement.id,
813
- WebRtcTransportRequestor.id,
814
847
  PushAvStreamTransport.id,
848
+ TlsCertificateManagement.id,
849
+ TlsClientManagement.id,
815
850
  ],
816
851
  });
817
852
  export const doorbell = DeviceTypeDefinition({
@@ -117,8 +117,12 @@ export declare class MatterbridgeEndpoint extends Endpoint {
117
117
  invokeBehaviorCommand<T extends Behavior.Type, C extends BehaviorCommandName<T>>(cluster: T, command: C, params?: BehaviorCommandParams<T, C>): Promise<void>;
118
118
  invokeBehaviorCommand<T extends ClusterType, C extends ClusterCommandName<T>>(cluster: T, command: C, params?: ClusterCommandParams<T, C>): Promise<void>;
119
119
  invokeBehaviorCommand(cluster: ClusterId | string, command: CommandHandlers, params?: Record<string, boolean | number | bigint | string | object | null>): Promise<void>;
120
+ addRequiredClusters(): MatterbridgeEndpoint;
120
121
  addRequiredClusterServers(): MatterbridgeEndpoint;
121
122
  addOptionalClusterServers(): MatterbridgeEndpoint;
123
+ addRequiredClusterClients(): MatterbridgeEndpoint;
124
+ addOptionalClusterClients(): MatterbridgeEndpoint;
125
+ addClusterClients(clientList: ClusterId[]): MatterbridgeEndpoint;
122
126
  getAllClusterServers(): Behavior.Type[];
123
127
  getAllClusterServerNames(): string[];
124
128
  forEachAttribute(callback: (clusterName: string, clusterId: number, attributeName: string, attributeId: number, attributeValue: boolean | number | bigint | string | object | null | undefined) => void): void;
@@ -143,6 +147,7 @@ export declare class MatterbridgeEndpoint extends Endpoint {
143
147
  createApparentElectricalPowerMeasurementClusterServer(voltage?: number | bigint | null, apparentCurrent?: number | bigint | null, apparentPower?: number | bigint | null, frequency?: number | bigint | null): this;
144
148
  createDefaultDeviceEnergyManagementClusterServer(esaType?: DeviceEnergyManagement.EsaType, esaCanGenerate?: boolean, esaState?: DeviceEnergyManagement.EsaState, absMinPower?: number, absMaxPower?: number): this;
145
149
  createDefaultDeviceEnergyManagementModeClusterServer(currentMode?: number, supportedModes?: DeviceEnergyManagementMode.ModeOption[]): this;
150
+ createDefaultBindingClusterServer(clientList?: ClusterId[]): this;
146
151
  createDefaultIdentifyClusterServer(identifyTime?: number, identifyType?: Identify.IdentifyType): this;
147
152
  createDefaultGroupsClusterServer(): this;
148
153
  createDefaultScenesManagementClusterServer(): this;
@@ -83,7 +83,7 @@ import { MatterbridgeThermostatServer } from './behaviors/thermostatServer.js';
83
83
  import { MatterbridgeValveConfigurationAndControlServer } from './behaviors/valveConfigurationAndControlServer.js';
84
84
  import { MatterbridgeWindowCoveringServer } from './behaviors/windowCoveringServer.js';
85
85
  import { CommandHandler } from './matterbridgeEndpointCommandHandler.js';
86
- import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, checkNotLatinCharacters, createUniqueId, defaultFor, featuresFor, generateUniqueId, getApparentElectricalPowerMeasurementClusterServer, getAttribute, getAttributeId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getCluster, getClusterId, getDefaultDeviceEnergyManagementClusterServer, getDefaultDeviceEnergyManagementModeClusterServer, getDefaultElectricalEnergyMeasurementClusterServer, getDefaultElectricalPowerMeasurementClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultOccupancySensingClusterServer, getDefaultOperationalStateClusterServer, getDefaultPowerSourceBatteryClusterServer, getDefaultPowerSourceRechargeableBatteryClusterServer, getDefaultPowerSourceReplaceableBatteryClusterServer, getDefaultPowerSourceWiredClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, invokeBehaviorCommand, lowercaseFirstLetter, setAttribute, setCluster, subscribeAttribute, triggerEvent, updateAttribute, } from './matterbridgeEndpointHelpers.js';
86
+ import { addClusterClients, addClusterServers, addFixedLabel, addOptionalClusterClients, addOptionalClusterServers, addRequiredClusterClients, addRequiredClusterServers, addUserLabel, checkNotLatinCharacters, createUniqueId, defaultFor, featuresFor, generateUniqueId, getApparentElectricalPowerMeasurementClusterServer, getAttribute, getAttributeId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getCluster, getClusterId, getDefaultDeviceEnergyManagementClusterServer, getDefaultDeviceEnergyManagementModeClusterServer, getDefaultElectricalEnergyMeasurementClusterServer, getDefaultElectricalPowerMeasurementClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultOccupancySensingClusterServer, getDefaultOperationalStateClusterServer, getDefaultPowerSourceBatteryClusterServer, getDefaultPowerSourceRechargeableBatteryClusterServer, getDefaultPowerSourceReplaceableBatteryClusterServer, getDefaultPowerSourceWiredClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, invokeBehaviorCommand, lowercaseFirstLetter, setAttribute, setCluster, subscribeAttribute, triggerEvent, updateAttribute, } from './matterbridgeEndpointHelpers.js';
87
87
  const MATTERBRIDGE_ENDPOINT_BRAND = Symbol('MatterbridgeEndpoint.brand');
88
88
  export function isMatterbridgeEndpoint(value) {
89
89
  if (!value || typeof value !== 'object')
@@ -268,6 +268,11 @@ export class MatterbridgeEndpoint extends Endpoint {
268
268
  async invokeBehaviorCommand(cluster, command, params) {
269
269
  await invokeBehaviorCommand(this, cluster, command, params);
270
270
  }
271
+ addRequiredClusters() {
272
+ addRequiredClusterServers(this);
273
+ addRequiredClusterClients(this);
274
+ return this;
275
+ }
271
276
  addRequiredClusterServers() {
272
277
  addRequiredClusterServers(this);
273
278
  return this;
@@ -276,6 +281,18 @@ export class MatterbridgeEndpoint extends Endpoint {
276
281
  addOptionalClusterServers(this);
277
282
  return this;
278
283
  }
284
+ addRequiredClusterClients() {
285
+ addRequiredClusterClients(this);
286
+ return this;
287
+ }
288
+ addOptionalClusterClients() {
289
+ addOptionalClusterClients(this);
290
+ return this;
291
+ }
292
+ addClusterClients(clientList) {
293
+ addClusterClients(this, clientList);
294
+ return this;
295
+ }
279
296
  getAllClusterServers() {
280
297
  return Object.values(this.behaviors.supported);
281
298
  }
@@ -560,6 +577,10 @@ export class MatterbridgeEndpoint extends Endpoint {
560
577
  this.behaviors.require(MatterbridgeDeviceEnergyManagementModeServer, getDefaultDeviceEnergyManagementModeClusterServer(currentMode, supportedModes));
561
578
  return this;
562
579
  }
580
+ createDefaultBindingClusterServer(clientList = []) {
581
+ addClusterClients(this, clientList);
582
+ return this;
583
+ }
563
584
  createDefaultIdentifyClusterServer(identifyTime = 0, identifyType = Identify.IdentifyType.None) {
564
585
  this.behaviors.require(MatterbridgeIdentifyServer, {
565
586
  identifyTime,
@@ -46,6 +46,9 @@ export declare function invokeSubscribeHandler(endpoint: MatterbridgeEndpoint, c
46
46
  export declare function addRequiredClusterServers(endpoint: MatterbridgeEndpoint): void;
47
47
  export declare function addOptionalClusterServers(endpoint: MatterbridgeEndpoint): void;
48
48
  export declare function addClusterServers(endpoint: MatterbridgeEndpoint, serverList: ClusterId[]): void;
49
+ export declare function addClusterClients(endpoint: MatterbridgeEndpoint, clientList: ClusterId[]): void;
50
+ export declare function addRequiredClusterClients(endpoint: MatterbridgeEndpoint): void;
51
+ export declare function addOptionalClusterClients(endpoint: MatterbridgeEndpoint): void;
49
52
  export declare function addFixedLabel(endpoint: MatterbridgeEndpoint, label: string, value: string): Promise<void>;
50
53
  export declare function addUserLabel(endpoint: MatterbridgeEndpoint, label: string, value: string): Promise<void>;
51
54
  export declare function getClusterId(endpoint: Endpoint, cluster: string): number | undefined;
@@ -84,6 +84,7 @@ import { MeasurementType } from '@matter/types/globals';
84
84
  import { deepEqual } from '@matterbridge/utils/deep-equal';
85
85
  import { isValidArray } from '@matterbridge/utils/validate';
86
86
  import { BLUE, CYAN, db, debugStringify, er, hk, or, YELLOW, zb } from 'node-ansi-logger';
87
+ import { MatterbridgeBindingServer } from './behaviors/bindingServer.js';
87
88
  import { MatterbridgeBooleanStateConfigurationServer } from './behaviors/booleanStateConfigurationServer.js';
88
89
  import { MatterbridgeColorControlServer } from './behaviors/colorControlServer.js';
89
90
  import { MatterbridgeDeviceEnergyManagementModeServer } from './behaviors/deviceEnergyManagementModeServer.js';
@@ -491,6 +492,44 @@ export function addClusterServers(endpoint, serverList) {
491
492
  if (serverList.includes(DeviceEnergyManagementMode.Cluster.id))
492
493
  endpoint.createDefaultDeviceEnergyManagementModeClusterServer();
493
494
  }
495
+ export function addClusterClients(endpoint, clientList) {
496
+ if (clientList.length === 0)
497
+ return;
498
+ if (!endpoint.behaviors.has(MatterbridgeBindingServer)) {
499
+ endpoint.behaviors.require(MatterbridgeBindingServer, { clientList });
500
+ return;
501
+ }
502
+ const existing = endpoint.behaviors.optionsFor(MatterbridgeBindingServer)?.clientList ?? [];
503
+ endpoint.behaviors.inject(MatterbridgeBindingServer, { clientList: [...new Set([...existing, ...clientList])] });
504
+ }
505
+ export function addRequiredClusterClients(endpoint) {
506
+ const requiredClientList = [];
507
+ endpoint.log.debug(`addRequiredClusterClients for ${CYAN}${endpoint.maybeId}${db}`);
508
+ Array.from(endpoint.deviceTypes.values()).forEach((deviceType) => {
509
+ endpoint.log.debug(`- for deviceType: ${zb}${'0x' + deviceType.code.toString(16).padStart(4, '0')}${db}-${zb}${deviceType.name}${db}`);
510
+ deviceType.requiredClientClusters.forEach((clusterId) => {
511
+ if (!requiredClientList.includes(clusterId)) {
512
+ requiredClientList.push(clusterId);
513
+ endpoint.log.debug(`- cluster: ${hk}${'0x' + clusterId.toString(16).padStart(4, '0')}${db}-${hk}${getClusterNameById(clusterId)}${db}`);
514
+ }
515
+ });
516
+ });
517
+ addClusterClients(endpoint, requiredClientList);
518
+ }
519
+ export function addOptionalClusterClients(endpoint) {
520
+ const optionalClientList = [];
521
+ endpoint.log.debug(`addOptionalClusterClients for ${CYAN}${endpoint.maybeId}${db}`);
522
+ Array.from(endpoint.deviceTypes.values()).forEach((deviceType) => {
523
+ endpoint.log.debug(`- for deviceType: ${zb}${'0x' + deviceType.code.toString(16).padStart(4, '0')}${db}-${zb}${deviceType.name}${db}`);
524
+ deviceType.optionalClientClusters.forEach((clusterId) => {
525
+ if (!optionalClientList.includes(clusterId)) {
526
+ optionalClientList.push(clusterId);
527
+ endpoint.log.debug(`- cluster: ${hk}${'0x' + clusterId.toString(16).padStart(4, '0')}${db}-${hk}${getClusterNameById(clusterId)}${db}`);
528
+ }
529
+ });
530
+ });
531
+ addClusterClients(endpoint, optionalClientList);
532
+ }
494
533
  export async function addFixedLabel(endpoint, label, value) {
495
534
  if (!endpoint.hasClusterServer(FixedLabel.Cluster.id)) {
496
535
  endpoint.log.debug(`addFixedLabel: add cluster ${hk}FixedLabel${db}:${hk}fixedLabel${db} with label ${CYAN}${label}${db} value ${CYAN}${value}${db}`);
@@ -29,6 +29,7 @@ export declare class MatterbridgePlatform {
29
29
  onShutdown(reason?: string): Promise<void>;
30
30
  onChangeLoggerLevel(logLevel: LogLevel): Promise<void>;
31
31
  onAction(action: string, value?: string, id?: string, formData?: PlatformConfig): Promise<void>;
32
+ onFetch(method: string, path?: string, query?: Record<string, unknown>, body?: unknown): Promise<unknown>;
32
33
  onConfigChanged(config: PlatformConfig): Promise<void>;
33
34
  saveConfig(config: PlatformConfig): void;
34
35
  getSchema(): Promise<PlatformSchema | undefined>;
@@ -156,6 +156,10 @@ export class MatterbridgePlatform {
156
156
  async onAction(action, value, id, formData) {
157
157
  this.log.debug(`The plugin ${CYAN}${this.name}${db} doesn't override onAction. Received action ${CYAN}${action}${db}${value ? ' with ' + CYAN + value + db : ''} ${id ? ' for schema ' + CYAN + id + db : ''}`, formData);
158
158
  }
159
+ async onFetch(method, path, query, body) {
160
+ this.log.debug(`onFetch called: method=${method} path=${path ?? 'none'} query=${query ? JSON.stringify(query) : 'none'} body=${body ? JSON.stringify(body) : 'none'}`);
161
+ return undefined;
162
+ }
159
163
  async onConfigChanged(config) {
160
164
  this.log.debug(`The plugin ${CYAN}${config.name}${db} doesn't override onConfigChanged. Received new config.`);
161
165
  }
@@ -1,6 +1,7 @@
1
1
  if (process.argv.includes('--loader') || process.argv.includes('-loader'))
2
2
  console.log('\u001B[32mPlugin Manager loaded.\u001B[40;0m');
3
3
  import EventEmitter from 'node:events';
4
+ import path from 'node:path';
4
5
  import { BroadcastServer } from '@matterbridge/thread/server';
5
6
  import { plg, typ } from '@matterbridge/types';
6
7
  import { hasParameter } from '@matterbridge/utils/cli';
@@ -657,6 +658,10 @@ export class PluginManager extends EventEmitter {
657
658
  plugin.funding = this.getFunding(packageJson);
658
659
  if (!plugin.type)
659
660
  this.log.warn(`Plugin ${plg}${plugin.name}${wr} has no type`);
661
+ const frontendPath = path.join(plugin.path.replace('package.json', ''), 'apps', 'frontend', 'build', 'index.html');
662
+ const { existsSync } = await import('node:fs');
663
+ if (existsSync(frontendPath))
664
+ plugin.frontendPath = frontendPath;
660
665
  const invalidDependencies = this.findInvalidDependencies(packageJson);
661
666
  if (invalidDependencies) {
662
667
  this.logInvalidDependencies(packageJson, invalidDependencies, plugin.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.8.0-dev-20260530-1971f9d",
3
+ "version": "3.8.0-dev-20260601-a1bf774",
4
4
  "description": "Matterbridge core library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -130,16 +130,16 @@
130
130
  ],
131
131
  "dependencies": {
132
132
  "@matter/main": "0.17.0",
133
- "@matterbridge/dgram": "3.8.0-dev-20260530-1971f9d",
134
- "@matterbridge/thread": "3.8.0-dev-20260530-1971f9d",
135
- "@matterbridge/types": "3.8.0-dev-20260530-1971f9d",
136
- "@matterbridge/utils": "3.8.0-dev-20260530-1971f9d",
133
+ "@matterbridge/dgram": "3.8.0-dev-20260601-a1bf774",
134
+ "@matterbridge/thread": "3.8.0-dev-20260601-a1bf774",
135
+ "@matterbridge/types": "3.8.0-dev-20260601-a1bf774",
136
+ "@matterbridge/utils": "3.8.0-dev-20260601-a1bf774",
137
137
  "escape-html": "1.0.3",
138
138
  "express": "5.2.1",
139
139
  "express-rate-limit": "8.5.2",
140
140
  "multer": "2.1.1",
141
141
  "node-ansi-logger": "3.3.0-dev-20260524-cac9dd5",
142
142
  "node-persist-manager": "2.1.0-dev-20260524-6a6019a",
143
- "ws": "8.20.1"
143
+ "ws": "8.21.0"
144
144
  }
145
145
  }