@matterbridge/core 3.10.14-dev-20261013-e87a0c8 → 3.10.14-dev-20261014-2811fdc

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/dist/backend.d.ts CHANGED
@@ -34,8 +34,8 @@ export declare class Backend extends EventEmitter<BackendEvents> {
34
34
  stop(): Promise<void>;
35
35
  getApiSettings(): Promise<ApiSettings>;
36
36
  getApiPlugins(): Promise<ApiPlugin[]>;
37
- getApiDevices(pluginName?: string): Promise<ApiDevice[]>;
38
- getApiCluster(pluginName: string, endpointNumber: number, serialNumber?: string, uniqueId?: string): Promise<ApiClusters | undefined>;
37
+ getApiDevices(pluginName?: string, extended?: boolean): Promise<ApiDevice[]>;
38
+ getApiCluster(pluginName: string, number: number, serialNumber: string, uniqueId: string): Promise<ApiClusters>;
39
39
  generateDiagnostic(): Promise<void>;
40
40
  getApiMatter(id: string): Promise<ApiMatter | undefined>;
41
41
  }
package/dist/backend.js CHANGED
@@ -341,19 +341,25 @@ export class Backend extends EventEmitter {
341
341
  writeDiagnostic('Backend', `getApiPlugins() took ${(performance.now() - start).toFixed(2)} ms`);
342
342
  return response.result.plugins;
343
343
  }
344
- async getApiDevices(pluginName) {
344
+ async getApiDevices(pluginName, extended = false) {
345
345
  const start = performance.now();
346
- const response = await this.server.fetch({ type: 'devices_apidevicearray', src: 'frontend', dst: 'devices', params: { pluginName } });
346
+ const response = await this.server.fetch({ type: 'devices_apidevicearray', src: 'frontend', dst: 'devices', params: { pluginName, extended } });
347
347
  if (this.diagnostic)
348
348
  writeDiagnostic('Backend', `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
349
349
  return response.result.devices;
350
350
  }
351
- async getApiCluster(pluginName, endpointNumber, serialNumber, uniqueId) {
351
+ async getApiCluster(pluginName, number, serialNumber, uniqueId) {
352
352
  const start = performance.now();
353
- this.log.debug(`Retrieving API cluster for plugin: ${pluginName}, endpoint: ${endpointNumber}, serial: ${serialNumber}, uniqueId: ${uniqueId}`);
353
+ this.log.debug(`Retrieving API cluster for plugin: ${pluginName}, number: ${number}, serial: ${serialNumber}, uniqueId: ${uniqueId}`);
354
+ const response = await this.server.fetch({
355
+ type: 'platform_get_apiclusters',
356
+ src: 'frontend',
357
+ dst: 'platform',
358
+ params: { pluginName, number, serialNumber, uniqueId },
359
+ });
354
360
  if (this.diagnostic)
355
361
  writeDiagnostic('Backend', `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
356
- return undefined;
362
+ return response.result.clusters;
357
363
  }
358
364
  async generateDiagnostic() {
359
365
  const start = performance.now();
@@ -209,22 +209,30 @@ export class BackendWsServer {
209
209
  }
210
210
  else if (data.method === '/api/devices') {
211
211
  const start = performance.now();
212
- const devices = await this.backend.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
212
+ const devices = await this.backend.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined, data.params.extended);
213
213
  if (this.diagnostic)
214
214
  writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
215
215
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
216
216
  }
217
217
  else if (data.method === '/api/clusters') {
218
- if (!isValidString(data.params.plugin, 10)) {
219
- sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
218
+ if (!isValidString(data.params.pluginName, 10)) {
219
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/clusters' });
220
220
  return;
221
221
  }
222
- if (!isValidNumber(data.params.endpoint, 1)) {
223
- sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
222
+ if (!isValidNumber(data.params.number, 1)) {
223
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter number in /api/clusters' });
224
+ return;
225
+ }
226
+ if (!isValidString(data.params.serialNumber, 1)) {
227
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter serialNumber in /api/clusters' });
228
+ return;
229
+ }
230
+ if (!isValidString(data.params.uniqueId, 1)) {
231
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter uniqueId in /api/clusters' });
224
232
  return;
225
233
  }
226
234
  const start = performance.now();
227
- const clusters = await this.backend.getApiCluster(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
235
+ const clusters = await this.backend.getApiCluster(data.params.pluginName, data.params.number, data.params.serialNumber, data.params.uniqueId);
228
236
  if (this.diagnostic)
229
237
  writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
230
238
  if (clusters) {
@@ -44,6 +44,9 @@ export declare class WeriftWebRtcSession {
44
44
  private getPreferredInjectableAudioCodec;
45
45
  private static readonly SUPPORTED_VIDEO_RESOLUTIONS;
46
46
  private static readonly DEFAULT_BITRATE_KBPS;
47
+ private static readonly DEFAULT_TEST_VIDEO_FPS;
48
+ private static readonly MAX_TEST_VIDEO_FPS;
49
+ private getTestVideoFps;
47
50
  private getConfiguredVideoBitrate;
48
51
  private getConfiguredVideoResolution;
49
52
  private getConfiguredVideoSource;
@@ -146,6 +146,19 @@ export class WeriftWebRtcSession {
146
146
  }
147
147
  static SUPPORTED_VIDEO_RESOLUTIONS = ['640x480', '1280x720', '1920x1080'];
148
148
  static DEFAULT_BITRATE_KBPS = 1000;
149
+ static DEFAULT_TEST_VIDEO_FPS = 30;
150
+ static MAX_TEST_VIDEO_FPS = 60;
151
+ getTestVideoFps() {
152
+ const raw = process.env.MATTERBRIDGE_TEST_VIDEO_FPS?.trim();
153
+ if (!raw)
154
+ return WeriftWebRtcSession.DEFAULT_TEST_VIDEO_FPS;
155
+ const fps = Number(raw);
156
+ if (!/^\d+$/.test(raw) || fps < 1 || fps > WeriftWebRtcSession.MAX_TEST_VIDEO_FPS) {
157
+ this.log.warn(`Invalid MATTERBRIDGE_TEST_VIDEO_FPS "${raw}" (supported: 1-${WeriftWebRtcSession.MAX_TEST_VIDEO_FPS}); falling back to ${WeriftWebRtcSession.DEFAULT_TEST_VIDEO_FPS}`);
158
+ return WeriftWebRtcSession.DEFAULT_TEST_VIDEO_FPS;
159
+ }
160
+ return fps;
161
+ }
149
162
  getConfiguredVideoBitrate() {
150
163
  const configured = this.options.videoBitrate;
151
164
  if (configured === undefined)
@@ -185,13 +198,12 @@ export class WeriftWebRtcSession {
185
198
  }
186
199
  buildFfmpegVideoInputArgs(videoSource) {
187
200
  if (videoSource === 'test') {
188
- const testPatternInput = {
189
- args: ['-re', '-f', 'lavfi', '-i', 'testsrc=size=640x480:rate=10'],
190
- description: 'synthetic moving test pattern',
191
- bitrateKbps: WeriftWebRtcSession.DEFAULT_BITRATE_KBPS,
192
- };
193
- this.log.debug(`Test pattern params: resolution=640x480, description="${testPatternInput.description}", bitrateKbps=${testPatternInput.bitrateKbps}`);
194
- return testPatternInput;
201
+ const resolution = this.getConfiguredVideoResolution();
202
+ const bitrateKbps = this.getConfiguredVideoBitrate();
203
+ const fps = this.getTestVideoFps();
204
+ const description = `synthetic moving test pattern (${resolution}, ${fps}fps)`;
205
+ this.log.debug(`Test pattern params: resolution=${resolution}, fps=${fps}, description="${description}", bitrateKbps=${bitrateKbps}`);
206
+ return { args: ['-re', '-f', 'lavfi', '-i', `testsrc=size=${resolution}:rate=${fps}`], description, bitrateKbps, keyFrameInterval: fps * 2 };
195
207
  }
196
208
  const device = this.options.videoSourceDevice;
197
209
  if (!device) {
@@ -203,17 +215,17 @@ export class WeriftWebRtcSession {
203
215
  if (videoSource === 'rtsp') {
204
216
  const description = `RTSP camera (${device}, ${resolution})`;
205
217
  this.log.debug(`RTSP capture params: url=${device}, resolution=${resolution}, description="${description}", bitrateKbps=${bitrateKbps}`);
206
- return { args: ['-rtsp_transport', 'tcp', '-i', device, '-vf', `scale=${resolution.replace('x', ':')}`], description, bitrateKbps };
218
+ return { args: ['-rtsp_transport', 'tcp', '-i', device, '-vf', `scale=${resolution.replace('x', ':')}`], description, bitrateKbps, keyFrameInterval: 20 };
207
219
  }
208
220
  const description = `local webcam (${device}, ${resolution})`;
209
221
  this.log.debug(`Webcam capture params: device=${device}, resolution=${resolution}, description="${description}", bitrateKbps=${bitrateKbps}`);
210
222
  switch (process.platform) {
211
223
  case 'linux':
212
- return { args: ['-f', 'v4l2', '-video_size', resolution, '-framerate', '30', '-i', device], description, bitrateKbps };
224
+ return { args: ['-f', 'v4l2', '-video_size', resolution, '-framerate', '30', '-i', device], description, bitrateKbps, keyFrameInterval: 20 };
213
225
  case 'darwin':
214
- return { args: ['-f', 'avfoundation', '-video_size', resolution, '-framerate', '30', '-i', device], description, bitrateKbps };
226
+ return { args: ['-f', 'avfoundation', '-video_size', resolution, '-framerate', '30', '-i', device], description, bitrateKbps, keyFrameInterval: 20 };
215
227
  case 'win32':
216
- return { args: ['-f', 'dshow', '-video_size', resolution, '-framerate', '30', '-i', `video=${device}`], description, bitrateKbps };
228
+ return { args: ['-f', 'dshow', '-video_size', resolution, '-framerate', '30', '-i', `video=${device}`], description, bitrateKbps, keyFrameInterval: 20 };
217
229
  default:
218
230
  this.log.error(`Webcam capture via ffmpeg is not supported on platform "${process.platform}"; not injecting a video track`);
219
231
  return undefined;
@@ -252,9 +264,27 @@ export class WeriftWebRtcSession {
252
264
  });
253
265
  this.peerConnection.addTrack(track);
254
266
  const bitrate = `${videoInput.bitrateKbps}k`;
267
+ const keyFrameInterval = String(videoInput.keyFrameInterval);
255
268
  const encoderArgs = selectedMimeType === 'video/h264'
256
- ? ['-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-pix_fmt', 'yuv420p', '-g', '20', '-b:v', bitrate, '-maxrate', bitrate, '-bufsize', bitrate]
257
- : ['-c:v', 'libvpx', '-deadline', 'realtime', '-cpu-used', '4', '-pix_fmt', 'yuv420p', '-g', '20', '-b:v', bitrate, '-maxrate', bitrate, '-bufsize', bitrate];
269
+ ? [
270
+ '-c:v',
271
+ 'libx264',
272
+ '-preset',
273
+ 'ultrafast',
274
+ '-tune',
275
+ 'zerolatency',
276
+ '-pix_fmt',
277
+ 'yuv420p',
278
+ '-g',
279
+ keyFrameInterval,
280
+ '-b:v',
281
+ bitrate,
282
+ '-maxrate',
283
+ bitrate,
284
+ '-bufsize',
285
+ bitrate,
286
+ ]
287
+ : ['-c:v', 'libvpx', '-deadline', 'realtime', '-cpu-used', '4', '-pix_fmt', 'yuv420p', '-g', keyFrameInterval, '-b:v', bitrate, '-maxrate', bitrate, '-bufsize', bitrate];
258
288
  const ffmpegArgs = [
259
289
  '-hide_banner',
260
290
  '-loglevel',
@@ -883,6 +883,7 @@ export async function createDemoDevices(matterbridge) {
883
883
  id: 'Intercom',
884
884
  number: EndpointNumber(16_04),
885
885
  identifyType: Identify.IdentifyType.VisibleIndicator,
886
+ switchOptions: { number: EndpointNumber(16_04_2) },
886
887
  weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
887
888
  });
888
889
  await registerDevice(ep, 'Intercom I', 'CAMERA-16-04');
@@ -890,6 +891,7 @@ export async function createDemoDevices(matterbridge) {
890
891
  id: 'IntercomII',
891
892
  number: EndpointNumber(16_04_1),
892
893
  identifyType: Identify.IdentifyType.VisibleIndicator,
894
+ switchOptions: { number: EndpointNumber(16_04_1_2) },
893
895
  weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
894
896
  });
895
897
  await registerDevice(ep, 'Intercom II', 'CAMERA-16-04-1');
@@ -20,7 +20,7 @@ export declare class DeviceManager {
20
20
  private toBaseDevice;
21
21
  array(): MatterbridgeEndpoint[];
22
22
  baseArray(pluginName?: string): BaseDevice[];
23
- apiDeviceArray(pluginName?: string): ApiDevice[];
23
+ apiDeviceArray(pluginName?: string, extended?: boolean): ApiDevice[];
24
24
  [Symbol.iterator](): IterableIterator<MatterbridgeEndpoint>;
25
25
  forEach(callback: (device: MatterbridgeEndpoint) => Promise<void>): Promise<void>;
26
26
  set logLevel(logLevel: LogLevel);
@@ -71,7 +71,7 @@ export class DeviceManager {
71
71
  this.server.respond({ ...msg, result: { devices: this.baseArray(msg.params.pluginName) } });
72
72
  break;
73
73
  case 'devices_apidevicearray':
74
- this.server.respond({ ...msg, result: { devices: this.apiDeviceArray(msg.params.pluginName) } });
74
+ this.server.respond({ ...msg, result: { devices: this.apiDeviceArray(msg.params.pluginName, msg.params.extended) } });
75
75
  break;
76
76
  default:
77
77
  if (this.verbose)
@@ -124,12 +124,12 @@ export class DeviceManager {
124
124
  }
125
125
  return devices;
126
126
  }
127
- apiDeviceArray(pluginName) {
127
+ apiDeviceArray(pluginName, extended) {
128
128
  const devices = [];
129
129
  for (const device of this._devices.values()) {
130
130
  if (pluginName && pluginName !== device.plugin)
131
131
  continue;
132
- const apiDevice = toApiDevice(device);
132
+ const apiDevice = toApiDevice(device, extended);
133
133
  if (apiDevice)
134
134
  devices.push(apiDevice);
135
135
  }
@@ -9,6 +9,7 @@ export interface BatteryStorageOptions {
9
9
  tagList?: Semtag[];
10
10
  batPercentRemaining?: number;
11
11
  batChargeLevel?: PowerSource.BatChargeLevel;
12
+ batCapacity?: number;
12
13
  voltage?: number | bigint | null;
13
14
  current?: number | bigint | null;
14
15
  power?: number | bigint | null;
@@ -31,7 +31,7 @@ export class BatteryStorage extends MatterbridgeEndpoint {
31
31
  const battery = this.addChildDeviceType('Battery', [powerSource, electricalSensor], {
32
32
  tagList: [getSemtag(PowerSourceTag.Battery)],
33
33
  })
34
- .createDefaultPowerSourceRechargeableBatteryClusterServer(options.batPercentRemaining ?? 100, options.batChargeLevel ?? PowerSource.BatChargeLevel.Ok, 24_000)
34
+ .createDefaultPowerSourceRechargeableBatteryClusterServer(options.batPercentRemaining ?? 100, options.batChargeLevel ?? PowerSource.BatChargeLevel.Ok, 24_000, PowerSource.BatReplaceability.Unspecified, options.batCapacity ?? 200_000, null, null, [])
35
35
  .createDefaultPowerTopologyClusterServer()
36
36
  .createDefaultElectricalEnergyMeasurementClusterServer(options.energyImported ?? null, options.energyExported ?? null);
37
37
  battery.behaviors.require(ElectricalPowerMeasurementServer.with(ElectricalPowerMeasurement.Feature.DirectCurrent), getDefaultElectricalPowerMeasurementClusterServer(options.voltage ?? null, options.current ?? null, options.power ?? null));
@@ -1,9 +1,17 @@
1
1
  import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
2
2
  import { Identify } from '@matter/types/clusters/identify';
3
- import { StreamUsage } from '@matter/types/globals';
3
+ import type { EndpointNumber } from '@matter/types/datatype';
4
+ import { type Semtag, StreamUsage } from '@matter/types/globals';
4
5
  import type { WeriftOfferOptions } from '../behaviors/weriftSession.js';
5
6
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
6
7
  import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
8
+ export interface IntercomSwitchOptions {
9
+ name?: string;
10
+ tagList?: Semtag[];
11
+ number?: EndpointNumber;
12
+ identifyTime?: number;
13
+ identifyType?: Identify.IdentifyType;
14
+ }
7
15
  export interface IntercomOptions extends MatterbridgeEndpointOptions {
8
16
  identifyTime?: number;
9
17
  identifyType?: Identify.IdentifyType;
@@ -16,7 +24,9 @@ export interface IntercomOptions extends MatterbridgeEndpointOptions {
16
24
  speakerCapabilities?: CameraAvStreamManagement.AudioCapabilities;
17
25
  twoWayTalkSupport?: CameraAvStreamManagement.TwoWayTalkSupportType;
18
26
  weriftOfferOptions?: WeriftOfferOptions;
27
+ switchOptions?: IntercomSwitchOptions;
19
28
  }
20
29
  export declare class Intercom extends MatterbridgeEndpoint {
21
30
  constructor(name: string, serial: string, options?: IntercomOptions);
31
+ addSwitch(name: string, options?: IntercomSwitchOptions): MatterbridgeEndpoint;
22
32
  }
@@ -5,11 +5,11 @@ import { createDefaultIntercomCameraAvStreamManagementClusterServer } from '../b
5
5
  import { addChimeClient, addWebRtcTransportProviderClient, addWebRtcTransportRequestorClient } from '../behaviors/clients.js';
6
6
  import { createDefaultWebRtcTransportProviderClusterServer } from '../behaviors/webRtcTransportProviderServer.js';
7
7
  import { createDefaultWebRtcTransportRequestorClusterServer } from '../behaviors/webRtcTransportRequestorServer.js';
8
- import { intercom, powerSource } from '../matterbridgeDeviceTypes.js';
8
+ import { genericSwitch, intercom, powerSource } from '../matterbridgeDeviceTypes.js';
9
9
  import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
10
10
  export class Intercom extends MatterbridgeEndpoint {
11
11
  constructor(name, serial, options = {}) {
12
- const { identifyTime = 0, identifyType = Identify.IdentifyType.None, powerSourceType = 'Wired', maxContentBufferSize = 65_536, maxNetworkBandwidth = 128_000, supportedStreamUsages = [StreamUsage.LiveView], streamUsagePriorities = supportedStreamUsages, microphoneCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [CameraAvStreamManagement.AudioCodec.Opus], supportedSampleRates: [48000], supportedBitDepths: [16] }, speakerCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [CameraAvStreamManagement.AudioCodec.Opus], supportedSampleRates: [48000], supportedBitDepths: [16] }, twoWayTalkSupport = CameraAvStreamManagement.TwoWayTalkSupportType.FullDuplex, weriftOfferOptions, id, number, tagList, mode, } = options;
12
+ const { identifyTime = 0, identifyType = Identify.IdentifyType.None, powerSourceType = 'Wired', maxContentBufferSize = 65_536, maxNetworkBandwidth = 128_000, supportedStreamUsages = [StreamUsage.LiveView], streamUsagePriorities = supportedStreamUsages, microphoneCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [CameraAvStreamManagement.AudioCodec.Opus], supportedSampleRates: [48000], supportedBitDepths: [16] }, speakerCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [CameraAvStreamManagement.AudioCodec.Opus], supportedSampleRates: [48000], supportedBitDepths: [16] }, twoWayTalkSupport = CameraAvStreamManagement.TwoWayTalkSupportType.FullDuplex, weriftOfferOptions, switchOptions = {}, id, number, tagList, mode, } = options;
13
13
  super(powerSourceType === 'None' ? [intercom] : [intercom, powerSource], {
14
14
  id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
15
15
  number,
@@ -51,5 +51,16 @@ export class Intercom extends MatterbridgeEndpoint {
51
51
  addWebRtcTransportRequestorClient(this);
52
52
  addChimeClient(this);
53
53
  this.addRequiredClusters();
54
+ const { name: switchName = 'Switch', tagList: switchTagList = [], number: switchNumber, identifyTime: switchIdentifyTime = 0, identifyType: switchIdentifyType = Identify.IdentifyType.None, } = switchOptions;
55
+ this.addSwitch(switchName, { tagList: switchTagList, number: switchNumber, identifyTime: switchIdentifyTime, identifyType: switchIdentifyType });
56
+ }
57
+ addSwitch(name, options = {}) {
58
+ const { tagList = [], number, identifyTime = 0, identifyType = Identify.IdentifyType.None } = options;
59
+ const switchChild = this.addChildDeviceType(name, genericSwitch, { number, ...(tagList.length > 0 ? { tagList } : {}) });
60
+ switchChild.log.logName = name;
61
+ switchChild.createDefaultIdentifyClusterServer(identifyTime, identifyType);
62
+ switchChild.createDefaultMomentarySwitchClusterServer();
63
+ switchChild.addRequiredClusters();
64
+ return switchChild;
54
65
  }
55
66
  }
@@ -40,8 +40,8 @@ export declare class Frontend extends EventEmitter<FrontendEvents> {
40
40
  stop(): Promise<void>;
41
41
  getApiSettings(): ApiSettings;
42
42
  getApiPlugins(): ApiPlugin[];
43
- getApiDevices(pluginName?: string): ApiDevice[];
44
- getApiCluster(pluginName: string, endpointNumber: number, serialNumber?: string, uniqueId?: string): ApiClusters | undefined;
43
+ getApiDevices(pluginName?: string, extended?: boolean): ApiDevice[];
44
+ getApiCluster(pluginName: string, endpointNumber: number, serialNumber: string, uniqueId: string): ApiClusters | undefined;
45
45
  generateDiagnostic(): Promise<void>;
46
46
  private wsMessageHandler;
47
47
  wssSendLogMessage(level: string, time: string, name: string, message: string): void;
package/dist/frontend.js CHANGED
@@ -15,10 +15,9 @@ import { formatBytes, formatPercent, formatUptime } from '@matterbridge/utils/fo
15
15
  import { logModuleLoaded } from '@matterbridge/utils/loader';
16
16
  import { isValidArray, isValidBoolean, isValidNumber, isValidObject, isValidString } from '@matterbridge/utils/validate';
17
17
  import { fireAndForget, wait, withTimeout } from '@matterbridge/utils/wait';
18
- import { AnsiLogger, bgHex, CYAN, db, debugStringify, er, GREEN, nf, nt, rs, stringify, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
18
+ import { AnsiLogger, bgHex, CYAN, db, debugStringify, er, GREEN, nf, nt, rs, UNDERLINE, UNDERLINEOFF, wr, YELLOW } from 'node-ansi-logger';
19
19
  import { generateHistoryPage } from './cliHistory.js';
20
- import { capitalizeFirstLetter } from './matterbridgeEndpointHelpers.js';
21
- import { deleteAdvertisingNode, getBatteryLevel, getClusterTextFromDevice, getPowerSource, getReachability, getServerNodeData, setAdvertisingNode } from './matterNodeHelpers.js';
20
+ import { deleteAdvertisingNode, getBatteryLevel, getClusterTextFromDevice, getPowerSource, getReachability, getServerNodeData, setAdvertisingNode, toApiClusters, } from './matterNodeHelpers.js';
22
21
  logModuleLoaded('Frontend');
23
22
  function sendDownloadError(res, error, message) {
24
23
  if (error.code === 'ECONNABORTED' || res.headersSent)
@@ -40,10 +39,10 @@ export class Frontend extends EventEmitter {
40
39
  webSocketServer;
41
40
  server;
42
41
  serverFetchTimeout = 2000;
43
- debug = hasParameter('debug') || hasParameter('verbose');
44
- verbose = hasParameter('verbose');
45
- diagnostic = hasParameter('diagnostic');
46
- readOnly = hasParameter('readonly') || hasParameter('shelly');
42
+ debug = hasAnyParameter('debug', 'debug-frontend', 'verbose', 'verbose-frontend');
43
+ verbose = hasAnyParameter('verbose', 'verbose-frontend');
44
+ diagnostic = hasAnyParameter('diagnostic', 'diagnostic-frontend');
45
+ readOnly = hasParameter('readonly');
47
46
  restartRequired = false;
48
47
  fixedRestartRequired = false;
49
48
  updateRequired = false;
@@ -531,7 +530,7 @@ export class Frontend extends EventEmitter {
531
530
  this.log.debug('The frontend sent /api/devices');
532
531
  if (!this.validateReq(req, res))
533
532
  return;
534
- res.json(this.getApiDevices());
533
+ res.json(this.getApiDevices(undefined, true));
535
534
  });
536
535
  this.expressApp.get('/api/view-mblog', async (req, res) => {
537
536
  this.log.debug('The frontend sent /api/view-mblog');
@@ -1007,7 +1006,7 @@ export class Frontend extends EventEmitter {
1007
1006
  }
1008
1007
  return plugins;
1009
1008
  }
1010
- getApiDevices(pluginName) {
1009
+ getApiDevices(pluginName, extended = false) {
1011
1010
  if (this.matterbridge.hasCleanupStarted)
1012
1011
  return [];
1013
1012
  const devices = [];
@@ -1026,11 +1025,11 @@ export class Frontend extends EventEmitter {
1026
1025
  productUrl: device.productUrl,
1027
1026
  configUrl: device.configUrl,
1028
1027
  uniqueId: device.uniqueId,
1029
- reachable: getReachability(device),
1030
- powerSource: getPowerSource(device),
1031
- batteryLevel: getBatteryLevel(device),
1032
- matter: device.mode === 'server' && device.serverNode ? getServerNodeData(device.serverNode) : undefined,
1033
- cluster: getClusterTextFromDevice(device),
1028
+ reachable: extended ? getReachability(device) : true,
1029
+ powerSource: extended ? getPowerSource(device) : undefined,
1030
+ batteryLevel: extended ? getBatteryLevel(device) : undefined,
1031
+ matter: extended && device.mode === 'server' && device.serverNode ? getServerNodeData(device.serverNode) : undefined,
1032
+ cluster: extended ? getClusterTextFromDevice(device) : '',
1034
1033
  });
1035
1034
  }
1036
1035
  return devices;
@@ -1040,64 +1039,12 @@ export class Frontend extends EventEmitter {
1040
1039
  return;
1041
1040
  const endpoint = this.matterbridge.devices
1042
1041
  .array()
1043
- .find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber && (!serialNumber || d.serialNumber === serialNumber) && (!uniqueId || d.uniqueId === uniqueId));
1044
- if (!endpoint?.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber) {
1045
- this.log.error(`getApiCluster: no device found for plugin ${pluginName} and endpoint number ${endpointNumber} (serial: ${serialNumber ?? 'N/A'}, uniqueId: ${uniqueId ?? 'N/A'})`);
1042
+ .find((d) => d.plugin === pluginName && d.maybeNumber === endpointNumber && d.serialNumber === serialNumber && d.uniqueId === uniqueId);
1043
+ if (!endpoint?.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber || !endpoint.uniqueId) {
1044
+ this.log.error(`getApiCluster: no device found for plugin ${pluginName} and endpoint number ${endpointNumber} (serial: ${serialNumber}, uniqueId: ${uniqueId})`);
1046
1045
  return;
1047
1046
  }
1048
- const deviceTypes = [];
1049
- const clusters = [];
1050
- endpoint.state.descriptor.deviceTypeList.forEach((d) => {
1051
- deviceTypes.push(d.deviceType);
1052
- });
1053
- endpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1054
- if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1055
- return;
1056
- if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1057
- return;
1058
- clusters.push({
1059
- endpoint: endpoint.number.toString(),
1060
- number: endpoint.number,
1061
- id: 'main',
1062
- deviceTypes,
1063
- clusterName: capitalizeFirstLetter(clusterName),
1064
- clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
1065
- attributeName,
1066
- attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
1067
- attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
1068
- attributeLocalValue: attributeValue,
1069
- });
1070
- });
1071
- const childEndpoints = endpoint.getChildEndpoints();
1072
- childEndpoints.forEach((childEndpoint) => {
1073
- if (!childEndpoint.maybeId || !childEndpoint.maybeNumber) {
1074
- this.log.error(`getApiCluster: no child endpoint found for plugin ${pluginName} and endpoint number ${endpointNumber}`);
1075
- return;
1076
- }
1077
- const deviceTypes = [];
1078
- childEndpoint.state.descriptor.deviceTypeList.forEach((d) => {
1079
- deviceTypes.push(d.deviceType);
1080
- });
1081
- childEndpoint.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
1082
- if (typeof attributeValue === 'undefined' || attributeValue === undefined)
1083
- return;
1084
- if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
1085
- return;
1086
- clusters.push({
1087
- endpoint: childEndpoint.number.toString(),
1088
- number: childEndpoint.number,
1089
- id: childEndpoint.id,
1090
- deviceTypes,
1091
- clusterName: capitalizeFirstLetter(clusterName),
1092
- clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
1093
- attributeName,
1094
- attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
1095
- attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
1096
- attributeLocalValue: attributeValue,
1097
- });
1098
- });
1099
- });
1100
- return { plugin: endpoint.plugin, deviceName: endpoint.deviceName, serialNumber: endpoint.serialNumber, number: endpoint.number, id: endpoint.id, deviceTypes, clusters };
1047
+ return toApiClusters(endpoint, this.log);
1101
1048
  }
1102
1049
  async generateDiagnostic() {
1103
1050
  this.log.debug('Generating diagnostic...');
@@ -1493,24 +1440,32 @@ export class Frontend extends EventEmitter {
1493
1440
  }
1494
1441
  else if (data.method === '/api/devices') {
1495
1442
  const start = performance.now();
1496
- const devices = this.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined);
1443
+ const devices = this.getApiDevices(isValidString(data.params.pluginName) ? data.params.pluginName : undefined, data.params.extended);
1497
1444
  if (this.diagnostic)
1498
1445
  writeDiagnostic(`Frontend:${data.sender}`, `getApiDevices() took ${(performance.now() - start).toFixed(2)} ms`);
1499
1446
  sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true, response: devices });
1500
1447
  }
1501
1448
  else if (data.method === '/api/clusters') {
1502
- if (!isValidString(data.params.plugin, 10)) {
1503
- sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter plugin in /api/clusters' });
1449
+ if (!isValidString(data.params.pluginName, 10)) {
1450
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter pluginName in /api/clusters' });
1451
+ return;
1452
+ }
1453
+ if (!isValidNumber(data.params.number, 1)) {
1454
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter number in /api/clusters' });
1455
+ return;
1456
+ }
1457
+ if (!isValidString(data.params.serialNumber, 1)) {
1458
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter serialNumber in /api/clusters' });
1504
1459
  return;
1505
1460
  }
1506
- if (!isValidNumber(data.params.endpoint, 1)) {
1507
- sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter endpoint in /api/clusters' });
1461
+ if (!isValidString(data.params.uniqueId, 1)) {
1462
+ sendResponse({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter uniqueId in /api/clusters' });
1508
1463
  return;
1509
1464
  }
1510
1465
  const start = performance.now();
1511
- const clusters = this.getApiCluster(data.params.plugin, data.params.endpoint, data.params.serialNumber, data.params.uniqueId);
1466
+ const clusters = this.getApiCluster(data.params.pluginName, data.params.number, data.params.serialNumber, data.params.uniqueId);
1512
1467
  if (this.diagnostic)
1513
- writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster() took ${(performance.now() - start).toFixed(2)} ms`);
1468
+ writeDiagnostic(`Frontend:${data.sender}`, `getApiCluster(${data.params.pluginName}, ${data.params.number}, ${data.params.serialNumber}, ${data.params.uniqueId}) took ${(performance.now() - start).toFixed(2)} ms`);
1514
1469
  if (clusters) {
1515
1470
  sendResponse({
1516
1471
  id: data.id,
@@ -97,7 +97,7 @@ export class MatterNode extends EventEmitter {
97
97
  this.environment.vars.set('path.root', path.join(matterbridge.matterbridgeDirectory, MATTER_STORAGE_DIR));
98
98
  this.environment.vars.set('runtime.signals', false);
99
99
  this.environment.vars.set('runtime.exitcode', false);
100
- this.environment.vars.set('endpoint.validation', 'strict');
100
+ this.environment.vars.set('endpoint.validation', 'warn');
101
101
  if (this.mdnsInterface)
102
102
  this.environment.vars.set('mdns.networkInterface', this.mdnsInterface);
103
103
  if (this.verbose)
@@ -1,9 +1,11 @@
1
1
  import { ServerNode, type SessionsBehavior } from '@matter/node';
2
2
  import type { ExposedFabricInformation } from '@matter/protocol';
3
- import type { ApiDevice, ApiMatter, BaseDevice, SanitizedExposedFabricInformation, SanitizedSession } from '@matterbridge/types';
3
+ import type { ApiClusters, ApiDevice, ApiMatter, BaseDevice, SanitizedExposedFabricInformation, SanitizedSession } from '@matterbridge/types';
4
+ import { type AnsiLogger } from 'node-ansi-logger';
4
5
  import type { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
6
+ export declare function toApiClusters(endpoint: MatterbridgeEndpoint, log: AnsiLogger): ApiClusters | undefined;
5
7
  export declare function toBaseDevice(device: MatterbridgeEndpoint | BaseDevice): BaseDevice;
6
- export declare function toApiDevice(device: MatterbridgeEndpoint): ApiDevice | undefined;
8
+ export declare function toApiDevice(device: MatterbridgeEndpoint, extended?: boolean): ApiDevice | undefined;
7
9
  export declare function getReachability(device: MatterbridgeEndpoint): boolean;
8
10
  export declare function getPowerSource(endpoint: MatterbridgeEndpoint): 'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined;
9
11
  export declare function getBatteryLevel(endpoint: MatterbridgeEndpoint): number | undefined;
@@ -11,15 +11,55 @@ import { OperationalState } from '@matter/types/clusters/operational-state';
11
11
  import { PowerSource } from '@matter/types/clusters/power-source';
12
12
  import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
13
13
  import { SmokeCoAlarm } from '@matter/types/clusters/smoke-co-alarm';
14
- import { SoilMeasurement } from '@matter/types/clusters/soil-measurement';
15
14
  import { UserLabel } from '@matter/types/clusters/user-label';
16
15
  import { ValveConfigurationAndControl } from '@matter/types/clusters/valve-configuration-and-control';
17
16
  import { ThreeLevelAuto } from '@matter/types/globals';
18
17
  import { getEnumDescription } from '@matterbridge/utils/enum';
19
18
  import { isValidArray, isValidNumber, isValidObject } from '@matterbridge/utils/validate';
20
19
  import { stringify } from 'node-ansi-logger';
21
- import { featuresFor, getAttribute } from './matterbridgeEndpointHelpers.js';
20
+ import { capitalizeFirstLetter, featuresFor, getAttribute, getSnapshot } from './matterbridgeEndpointHelpers.js';
22
21
  const advertisingNodes = new Map();
22
+ export function toApiClusters(endpoint, log) {
23
+ if (!endpoint.plugin || !endpoint.maybeNumber || !endpoint.maybeId || !endpoint.deviceName || !endpoint.serialNumber || !endpoint.uniqueId)
24
+ return undefined;
25
+ const deviceTypes = getSnapshot(endpoint.state.descriptor.deviceTypeList).map((deviceType) => deviceType.deviceType);
26
+ const clusters = [];
27
+ for (const current of [endpoint, ...endpoint.getChildEndpoints()]) {
28
+ if (!current.maybeId || !current.maybeNumber) {
29
+ log.error(`getApiCluster: no child endpoint found for plugin ${endpoint.plugin} and endpoint number ${endpoint.number}`);
30
+ continue;
31
+ }
32
+ const currentDeviceTypes = current === endpoint ? deviceTypes : getSnapshot(current.state.descriptor.deviceTypeList).map((deviceType) => deviceType.deviceType);
33
+ current.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
34
+ if (attributeValue === undefined)
35
+ return;
36
+ if (clusterName === 'EveHistory' && ['configDataGet', 'configDataSet', 'historyStatus', 'historyEntries', 'historyRequest', 'historySetTime', 'rLoc'].includes(attributeName))
37
+ return;
38
+ clusters.push({
39
+ endpoint: current.number.toString(),
40
+ number: current.number,
41
+ id: current === endpoint ? 'main' : current.id,
42
+ deviceTypes: currentDeviceTypes,
43
+ clusterName: capitalizeFirstLetter(clusterName),
44
+ clusterId: '0x' + clusterId.toString(16).padStart(2, '0'),
45
+ attributeName,
46
+ attributeId: '0x' + attributeId.toString(16).padStart(2, '0'),
47
+ attributeValue: typeof attributeValue === 'object' ? stringify(attributeValue) : attributeValue.toString(),
48
+ attributeLocalValue: getSnapshot(attributeValue),
49
+ });
50
+ });
51
+ }
52
+ return {
53
+ plugin: endpoint.plugin,
54
+ deviceName: endpoint.deviceName,
55
+ serialNumber: endpoint.serialNumber,
56
+ uniqueId: endpoint.uniqueId,
57
+ number: endpoint.number,
58
+ id: endpoint.id,
59
+ deviceTypes,
60
+ clusters,
61
+ };
62
+ }
23
63
  export function toBaseDevice(device) {
24
64
  return {
25
65
  mode: device.mode,
@@ -48,7 +88,7 @@ export function toBaseDevice(device) {
48
88
  id: device.id,
49
89
  };
50
90
  }
51
- export function toApiDevice(device) {
91
+ export function toApiDevice(device, extended = false) {
52
92
  if (!device.plugin || !device.deviceType || !device.name || !device.deviceName || !device.serialNumber || !device.uniqueId || !device.lifecycle.isReady)
53
93
  return undefined;
54
94
  return {
@@ -61,11 +101,11 @@ export function toApiDevice(device) {
61
101
  productUrl: device.productUrl,
62
102
  configUrl: device.configUrl,
63
103
  uniqueId: device.uniqueId,
64
- reachable: getReachability(device),
65
- powerSource: getPowerSource(device),
66
- batteryLevel: getBatteryLevel(device),
67
- matter: device.mode === 'server' && device.serverNode ? getServerNodeData(device.serverNode) : undefined,
68
- cluster: getClusterTextFromDevice(device),
104
+ reachable: extended ? getReachability(device) : true,
105
+ powerSource: extended ? getPowerSource(device) : undefined,
106
+ batteryLevel: extended ? getBatteryLevel(device) : undefined,
107
+ matter: extended && device.mode === 'server' && device.serverNode ? getServerNodeData(device.serverNode) : undefined,
108
+ cluster: extended ? getClusterTextFromDevice(device) : '',
69
109
  };
70
110
  }
71
111
  export function getReachability(device) {
@@ -301,7 +341,7 @@ export function getClusterTextFromDevice(device) {
301
341
  appendMeasurement('Pressure', getMeasurementText(attributeValue, 1, ' hPa'));
302
342
  if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
303
343
  appendMeasurement('Flow', getMeasurementText(attributeValue, 10, ' m³/h'));
304
- if (clusterId === SoilMeasurement.id && attributeName === 'soilMoistureMeasuredValue')
344
+ if (clusterName === 'soilMeasurement' && attributeName === 'soilMoistureMeasuredValue')
305
345
  appendMeasurement('Soil moisture', getMeasurementText(attributeValue, 1, '%'));
306
346
  if (clusterName === 'electricalPowerMeasurement' && attributeName === 'voltage')
307
347
  appendMeasurement('Voltage', getMeasurementText(attributeValue, 1_000, 'V'));
@@ -460,7 +460,7 @@ export class Matterbridge extends EventEmitter {
460
460
  this.environment.vars.set('path.root', path.join(this.matterbridgeDirectory, MATTER_STORAGE_DIR));
461
461
  this.environment.vars.set('runtime.signals', false);
462
462
  this.environment.vars.set('runtime.exitcode', false);
463
- this.environment.vars.set('endpoint.validation', 'strict');
463
+ this.environment.vars.set('endpoint.validation', 'warn');
464
464
  this.registerProcessHandlers();
465
465
  try {
466
466
  this.log.debug(`Creating node storage manager: ${CYAN}${NODE_STORAGE_DIR}${db}`);
@@ -151,7 +151,7 @@ export declare class MatterbridgeEndpoint extends Endpoint {
151
151
  createDefaultPowerSourceWiredClusterServer(wiredCurrentType?: PowerSource.WiredCurrentType): this;
152
152
  createDefaultPowerSourceBatteryClusterServer(batPercentRemaining?: null | number, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: null | number, batReplaceability?: PowerSource.BatReplaceability): this;
153
153
  createDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining?: number | null, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: number | null, batReplacementDescription?: string, batQuantity?: number, batReplaceability?: PowerSource.BatReplaceability): this;
154
- createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining?: number | null, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: number | null, batReplaceability?: PowerSource.BatReplaceability): this;
154
+ createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining?: number | null, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: number | null, batReplaceability?: PowerSource.BatReplaceability, batCapacity?: number, batTimeToFullCharge?: number | null, batChargingCurrent?: number | null, activeBatChargeFaults?: PowerSource.BatChargeFault[]): this;
155
155
  createDefaultBasicInformationClusterServer(deviceName: string, serialNumber: string, vendorId?: number, vendorName?: string, productId?: number, productName?: string, softwareVersion?: number, softwareVersionString?: string, hardwareVersion?: number, hardwareVersionString?: string, productLabel?: string, productUrl?: string, configurationVersion?: number): this;
156
156
  createDefaultBridgedDeviceBasicInformationClusterServer(deviceName: string, serialNumber: string, vendorId?: number, vendorName?: string, productName?: string, softwareVersion?: number, softwareVersionString?: string, hardwareVersion?: number, hardwareVersionString?: string, productLabel?: string, productUrl?: string, configurationVersion?: number, productId?: number): this;
157
157
  createDefaultPowerTopologyClusterServer(feature?: PowerTopology.Feature, availableEndpoints?: EndpointNumber[], activeEndpoints?: EndpointNumber[]): this;
@@ -539,8 +539,8 @@ export class MatterbridgeEndpoint extends Endpoint {
539
539
  this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Replaceable), getDefaultPowerSourceReplaceableBatteryClusterServer(batPercentRemaining, batChargeLevel, batVoltage, batReplacementDescription, batQuantity, batReplaceability));
540
540
  return this;
541
541
  }
542
- createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplaceability = PowerSource.BatReplaceability.Unspecified) {
543
- this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Rechargeable), getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining, batChargeLevel, batVoltage, batReplaceability));
542
+ createDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplaceability = PowerSource.BatReplaceability.Unspecified, batCapacity, batTimeToFullCharge, batChargingCurrent, activeBatChargeFaults) {
543
+ this.behaviors.require(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Rechargeable), getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining, batChargeLevel, batVoltage, batReplaceability, batCapacity, batTimeToFullCharge, batChargingCurrent, activeBatChargeFaults));
544
544
  return this;
545
545
  }
546
546
  createDefaultBasicInformationClusterServer(deviceName, serialNumber, vendorId = 0xfff1, vendorName = 'Matterbridge', productId = 0x8000, productName = 'Matterbridge device', softwareVersion = 1, softwareVersionString = '1.0.0', hardwareVersion = 1, hardwareVersionString = '1.0.0', productLabel = 'Matter Endpoint', productUrl = 'https://matterbridge.io', configurationVersion = 1) {
@@ -81,7 +81,7 @@ export declare function getDefaultPowerSourceReplaceableBatteryClusterServer(bat
81
81
  replaceable: true;
82
82
  wired: false;
83
83
  }>, typeof MatterbridgePowerSourceServer>>;
84
- export declare function getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining?: number | null, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: number | null, batReplaceability?: PowerSource.BatReplaceability): Partial<import("@matter/node").ClusterState.Type<ClusterType.WithSupportedFeatures<PowerSource, {
84
+ export declare function getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining?: number | null, batChargeLevel?: PowerSource.BatChargeLevel, batVoltage?: number | null, batReplaceability?: PowerSource.BatReplaceability, batCapacity?: number, batTimeToFullCharge?: number | null, batChargingCurrent?: number | null, activeBatChargeFaults?: PowerSource.BatChargeFault[]): Partial<import("@matter/node").ClusterState.Type<ClusterType.WithSupportedFeatures<PowerSource, {
85
85
  battery: true;
86
86
  rechargeable: true;
87
87
  replaceable: false;
@@ -927,7 +927,7 @@ export function getDefaultPowerSourceReplaceableBatteryClusterServer(batPercentR
927
927
  batQuantity,
928
928
  });
929
929
  }
930
- export function getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplaceability = PowerSource.BatReplaceability.Unspecified) {
930
+ export function getDefaultPowerSourceRechargeableBatteryClusterServer(batPercentRemaining = 100, batChargeLevel = PowerSource.BatChargeLevel.Ok, batVoltage = 1500, batReplaceability = PowerSource.BatReplaceability.Unspecified, batCapacity, batTimeToFullCharge, batChargingCurrent, activeBatChargeFaults) {
931
931
  return optionsFor(MatterbridgePowerSourceServer.with(PowerSource.Feature.Battery, PowerSource.Feature.Rechargeable), {
932
932
  status: PowerSource.PowerSourceStatus.Active,
933
933
  order: 0,
@@ -943,6 +943,10 @@ export function getDefaultPowerSourceRechargeableBatteryClusterServer(batPercent
943
943
  activeBatFaults: [],
944
944
  batChargeState: PowerSource.BatChargeState.IsNotCharging,
945
945
  batFunctionalWhileCharging: true,
946
+ ...(batCapacity !== undefined ? { batCapacity } : {}),
947
+ ...(batTimeToFullCharge !== undefined ? { batTimeToFullCharge } : {}),
948
+ ...(batChargingCurrent !== undefined ? { batChargingCurrent } : {}),
949
+ ...(activeBatChargeFaults !== undefined ? { activeBatChargeFaults } : {}),
946
950
  });
947
951
  }
948
952
  export function getDefaultElectricalEnergyMeasurementClusterServer(energyImported = null, energyExported = null) {
@@ -11,7 +11,7 @@ import { NodeStorageManager } from 'node-persist-manager';
11
11
  import { bridgedNode } from './matterbridgeDeviceTypes.js';
12
12
  import { assertMatterbridgeEndpoint } from './matterbridgeEndpoint.js';
13
13
  import { checkNotLatinCharacters } from './matterbridgeEndpointHelpers.js';
14
- import { toApiDevice } from './matterNodeHelpers.js';
14
+ import { toApiClusters, toApiDevice } from './matterNodeHelpers.js';
15
15
  logModuleLoaded('MatterbridgePlatform');
16
16
  const MATTERBRIDGE_PLATFORM_BRAND = Symbol('MatterbridgePlatform.brand');
17
17
  export function isMatterbridgePlatform(value) {
@@ -130,26 +130,41 @@ export class MatterbridgePlatform {
130
130
  this.log.debug(`Received broadcast request ${CYAN}${msg.type}${db} from ${CYAN}${msg.src}${db}: ${debugStringify(msg)}${db}`);
131
131
  switch (msg.type) {
132
132
  case 'platform_get_apidevices':
133
- if (msg.params.plugin !== this.name)
133
+ if (msg.params.pluginName !== this.name)
134
134
  return;
135
135
  this.#server.respond({
136
136
  ...msg,
137
137
  result: {
138
138
  devices: this.getDevices()
139
- .map(toApiDevice)
139
+ .map((device) => toApiDevice(device, msg.params.extended))
140
140
  .filter((device) => device !== undefined),
141
141
  },
142
142
  });
143
143
  break;
144
+ case 'platform_get_apiclusters': {
145
+ if (msg.params.pluginName !== this.name)
146
+ return;
147
+ const { number, serialNumber, uniqueId } = msg.params;
148
+ const endpoint = this.getDevices().find((device) => device.maybeNumber === number && device.serialNumber === serialNumber && device.uniqueId === uniqueId);
149
+ const clusters = endpoint ? toApiClusters(endpoint, this.log) : undefined;
150
+ if (!clusters) {
151
+ const error = `getApiCluster: no device found for plugin ${this.name} and endpoint number ${number} (serial: ${serialNumber}, uniqueId: ${uniqueId})`;
152
+ this.log.error(error);
153
+ this.#server.respond({ ...msg, error });
154
+ break;
155
+ }
156
+ this.#server.respond({ ...msg, result: { clusters } });
157
+ break;
158
+ }
144
159
  case 'platform_get_apiselectdevices':
145
- if (msg.params.plugin !== this.name)
160
+ if (msg.params.pluginName !== this.name)
146
161
  return;
147
- this.#server.respond({ ...msg, result: { devices: this.getSelectDevices() } });
162
+ this.#server.respond({ ...msg, result: { devices: this.getSelectDevices().toSorted((keyA, keyB) => keyA.deviceName.localeCompare(keyB.deviceName)) } });
148
163
  break;
149
164
  case 'platform_get_apiselectentities':
150
- if (msg.params.plugin !== this.name)
165
+ if (msg.params.pluginName !== this.name)
151
166
  return;
152
- this.#server.respond({ ...msg, result: { entities: this.getSelectEntities() } });
167
+ this.#server.respond({ ...msg, result: { entities: this.getSelectEntities().toSorted((keyA, keyB) => keyA.name.localeCompare(keyB.name)) } });
153
168
  break;
154
169
  default:
155
170
  if (this.#verbose)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.10.14-dev-20261013-e87a0c8",
3
+ "version": "3.10.14-dev-20261014-2811fdc",
4
4
  "description": "Matterbridge core library",
5
5
  "author": "https://github.com/Luligu",
6
6
  "homepage": "https://matterbridge.io/",
@@ -136,10 +136,10 @@
136
136
  },
137
137
  "dependencies": {
138
138
  "@matter/main": "0.17.9",
139
- "@matterbridge/dgram": "3.10.14-dev-20261013-e87a0c8",
140
- "@matterbridge/thread": "3.10.14-dev-20261013-e87a0c8",
141
- "@matterbridge/types": "3.10.14-dev-20261013-e87a0c8",
142
- "@matterbridge/utils": "3.10.14-dev-20261013-e87a0c8",
139
+ "@matterbridge/dgram": "3.10.14-dev-20261014-2811fdc",
140
+ "@matterbridge/thread": "3.10.14-dev-20261014-2811fdc",
141
+ "@matterbridge/types": "3.10.14-dev-20261014-2811fdc",
142
+ "@matterbridge/utils": "3.10.14-dev-20261014-2811fdc",
143
143
  "escape-html": "1.0.3",
144
144
  "express": "5.2.1",
145
145
  "express-rate-limit": "8.7.1",