@matterbridge/core 3.10.10-dev-20260915-57739b4 → 3.10.10-dev-20260917-79954cd

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,17 +1,36 @@
1
1
  import { CameraAvSettingsUserLevelManagementServer } from '@matter/node/behaviors/camera-av-settings-user-level-management';
2
+ import { type Viewport } from '@matter/types';
2
3
  import { CameraAvSettingsUserLevelManagement } from '@matter/types/clusters/camera-av-settings-user-level-management';
4
+ import type { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
3
5
  import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
4
6
  declare const MatterbridgeCameraAvSettingsUserLevelManagementServer_base: import("@matter/node").ClusterBehavior.Type<typeof CameraAvSettingsUserLevelManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvSettingsUserLevelManagement, {
5
- digitalPtz: false;
7
+ digitalPtz: true;
6
8
  mechanicalPan: true;
7
- mechanicalPresets: false;
9
+ mechanicalPresets: true;
8
10
  mechanicalTilt: true;
9
11
  mechanicalZoom: true;
10
12
  }>, import("@matter/types").ClusterType.Concrete, new () => {}, "cameraAvSettingsUserLevelManagement">;
11
13
  export declare class MatterbridgeCameraAvSettingsUserLevelManagementServer extends MatterbridgeCameraAvSettingsUserLevelManagementServer_base {
12
14
  readonly endpoint: MatterbridgeEndpoint;
15
+ protected internal: MatterbridgeCameraAvSettingsUserLevelManagementServer.Internal;
16
+ initialize(): void;
17
+ protected syncDptzStreams(allocatedVideoStreams: CameraAvStreamManagement.VideoStream[]): void;
18
+ protected syncDptzViewport(viewport: Viewport): void;
13
19
  mptzSetPosition(request: CameraAvSettingsUserLevelManagement.MptzSetPositionRequest): void;
14
20
  mptzRelativeMove(request: CameraAvSettingsUserLevelManagement.MptzRelativeMoveRequest): void;
21
+ mptzMoveToPreset(request: CameraAvSettingsUserLevelManagement.MptzMoveToPresetRequest): void;
22
+ mptzSavePreset(request: CameraAvSettingsUserLevelManagement.MptzSavePresetRequest): void;
23
+ mptzRemovePreset(request: CameraAvSettingsUserLevelManagement.MptzRemovePresetRequest): void;
24
+ private generatePresetId;
25
+ private dptzStreamEntry;
26
+ private applyDptzViewport;
27
+ dptzSetViewport(request: CameraAvSettingsUserLevelManagement.DptzSetViewportRequest): void;
28
+ dptzRelativeMove(request: CameraAvSettingsUserLevelManagement.DptzRelativeMoveRequest): void;
29
+ }
30
+ export declare namespace MatterbridgeCameraAvSettingsUserLevelManagementServer {
31
+ class Internal {
32
+ lastGeneratedPresetId: number;
33
+ }
15
34
  }
16
35
  export interface CameraAvSettingsUserLevelManagementClusterOptions {
17
36
  panMin: number;
@@ -20,6 +39,7 @@ export interface CameraAvSettingsUserLevelManagementClusterOptions {
20
39
  tiltMax: number;
21
40
  zoomMax: number;
22
41
  mptzPosition: CameraAvSettingsUserLevelManagement.Mptz;
42
+ maxPresets: number;
23
43
  }
24
44
  export declare function createDefaultCameraAvSettingsUserLevelManagementClusterServer(endpoint: MatterbridgeEndpoint, options: CameraAvSettingsUserLevelManagementClusterOptions): MatterbridgeEndpoint;
25
45
  export {};
@@ -1,11 +1,45 @@
1
1
  import { CameraAvSettingsUserLevelManagementServer } from '@matter/node/behaviors/camera-av-settings-user-level-management';
2
2
  import { Status, StatusResponseError } from '@matter/types';
3
3
  import { CameraAvSettingsUserLevelManagement } from '@matter/types/clusters/camera-av-settings-user-level-management';
4
+ import { MatterbridgeCameraAvStreamManagementServer } from './cameraAvStreamManagementServer.js';
4
5
  import { MatterbridgeServer } from './matterbridgeServer.js';
5
6
  function clamp(value, min, max) {
6
7
  return Math.min(Math.max(value, min), max);
7
8
  }
8
- export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends CameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom) {
9
+ function viewportWidth(viewport) {
10
+ return viewport.x2 - viewport.x1;
11
+ }
12
+ function viewportHeight(viewport) {
13
+ return viewport.y2 - viewport.y1;
14
+ }
15
+ function sameAspectRatio(width, height, referenceWidth, referenceHeight) {
16
+ return width * referenceHeight === referenceWidth * height;
17
+ }
18
+ function viewportAt(x1, y1, width, height, sensorWidth, sensorHeight) {
19
+ const left = clamp(x1, 0, sensorWidth - width);
20
+ const top = clamp(y1, 0, sensorHeight - height);
21
+ return { x1: left, y1: top, x2: left + width, y2: top + height };
22
+ }
23
+ export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends CameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom, CameraAvSettingsUserLevelManagement.Feature.MechanicalPresets, CameraAvSettingsUserLevelManagement.Feature.DigitalPtz) {
24
+ initialize() {
25
+ const streamManagement = this.agent.get(MatterbridgeCameraAvStreamManagementServer);
26
+ this.syncDptzStreams(streamManagement.state.allocatedVideoStreams);
27
+ this.reactTo(streamManagement.events.allocatedVideoStreams$Changed, this.syncDptzStreams);
28
+ this.reactTo(streamManagement.events.viewport$Changed, this.syncDptzViewport);
29
+ }
30
+ syncDptzStreams(allocatedVideoStreams) {
31
+ const allocatedIds = allocatedVideoStreams.map((stream) => stream.videoStreamId);
32
+ const currentIds = this.state.dptzStreams.map((entry) => entry.videoStreamId);
33
+ if (allocatedIds.length === currentIds.length && allocatedIds.every((id, index) => id === currentIds[index]))
34
+ return;
35
+ const viewport = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state.viewport;
36
+ this.state.dptzStreams = allocatedIds.map((videoStreamId) => this.state.dptzStreams.find((entry) => entry.videoStreamId === videoStreamId) ?? { videoStreamId, viewport });
37
+ }
38
+ syncDptzViewport(viewport) {
39
+ if (this.state.dptzStreams.length === 0)
40
+ return;
41
+ this.state.dptzStreams = this.state.dptzStreams.map((entry) => ({ videoStreamId: entry.videoStreamId, viewport }));
42
+ }
9
43
  mptzSetPosition(request) {
10
44
  const device = this.endpoint.stateOf(MatterbridgeServer);
11
45
  const { pan, tilt, zoom } = request;
@@ -42,11 +76,147 @@ export class MatterbridgeCameraAvSettingsUserLevelManagementServer extends Camer
42
76
  device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved mechanical PTZ position by pan ${request.panDelta ?? 0}°, tilt ${request.tiltDelta ?? 0}°, zoom ${request.zoomDelta ?? 0} to pan ${pan}°, tilt ${tilt}°, zoom ${zoom} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
43
77
  this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzRelativeMove', request, this.context);
44
78
  }
79
+ mptzMoveToPreset(request) {
80
+ const device = this.endpoint.stateOf(MatterbridgeServer);
81
+ const { presetId } = request;
82
+ if (presetId < 1 || presetId > this.state.maxPresets) {
83
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
84
+ }
85
+ const preset = this.state.mptzPresets.find((entry) => entry.presetId === presetId);
86
+ if (preset === undefined) {
87
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is not present in mptzPresets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
88
+ }
89
+ this.state.mptzPosition = preset.settings;
90
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved mechanical PTZ position to preset ${presetId} "${preset.name}": pan ${preset.settings.pan}°, tilt ${preset.settings.tilt}°, zoom ${preset.settings.zoom} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
91
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzMoveToPreset', request, this.context);
92
+ }
93
+ mptzSavePreset(request) {
94
+ const device = this.endpoint.stateOf(MatterbridgeServer);
95
+ const { presetId, name } = request;
96
+ if (name.length > 32) {
97
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: name "${name}" exceeds the maximum length of 32 characters (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
98
+ }
99
+ if (presetId !== undefined && (presetId < 1 || presetId > this.state.maxPresets)) {
100
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
101
+ }
102
+ if (presetId !== undefined && this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
103
+ this.state.mptzPresets = this.state.mptzPresets.map((entry) => (entry.presetId === presetId ? { presetId, name, settings: this.state.mptzPosition } : entry));
104
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: updated preset ${presetId} "${name}" with the current mechanical PTZ position (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
105
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzSavePreset', request, this.context);
106
+ return;
107
+ }
108
+ if (this.state.mptzPresets.length >= this.state.maxPresets) {
109
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: mptzPresets already holds the maximum of ${this.state.maxPresets} presets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ResourceExhausted);
110
+ }
111
+ const newPresetId = presetId ?? this.generatePresetId();
112
+ this.state.mptzPresets = [...this.state.mptzPresets, { presetId: newPresetId, name, settings: this.state.mptzPosition }];
113
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: saved preset ${newPresetId} "${name}" with the current mechanical PTZ position (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
114
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzSavePreset', request, this.context);
115
+ }
116
+ mptzRemovePreset(request) {
117
+ const device = this.endpoint.stateOf(MatterbridgeServer);
118
+ const { presetId } = request;
119
+ if (presetId < 1 || presetId > this.state.maxPresets) {
120
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is outside of the supported range [1, ${this.state.maxPresets}] (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
121
+ }
122
+ if (!this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
123
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: presetId ${presetId} is not present in mptzPresets (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
124
+ }
125
+ this.state.mptzPresets = this.state.mptzPresets.filter((entry) => entry.presetId !== presetId);
126
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: removed preset ${presetId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
127
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'mptzRemovePreset', request, this.context);
128
+ }
129
+ generatePresetId() {
130
+ let presetId = (this.internal.lastGeneratedPresetId % this.state.maxPresets) + 1;
131
+ while (this.state.mptzPresets.some((entry) => entry.presetId === presetId)) {
132
+ presetId = (presetId % this.state.maxPresets) + 1;
133
+ }
134
+ this.internal.lastGeneratedPresetId = presetId;
135
+ return presetId;
136
+ }
137
+ dptzStreamEntry(videoStreamId) {
138
+ const entry = this.state.dptzStreams.find((stream) => stream.videoStreamId === videoStreamId);
139
+ if (entry === undefined) {
140
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: video stream ${videoStreamId} is not present in dptzStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
141
+ }
142
+ return entry;
143
+ }
144
+ applyDptzViewport(videoStreamId, viewport) {
145
+ this.state.dptzStreams = this.state.dptzStreams.map((entry) => (entry.videoStreamId === videoStreamId ? { videoStreamId, viewport } : entry));
146
+ }
147
+ dptzSetViewport(request) {
148
+ const device = this.endpoint.stateOf(MatterbridgeServer);
149
+ const { videoStreamId, viewport } = request;
150
+ const entry = this.dptzStreamEntry(videoStreamId);
151
+ const { videoSensorParams, minViewportResolution } = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state;
152
+ const width = viewportWidth(viewport);
153
+ const height = viewportHeight(viewport);
154
+ if (width < minViewportResolution.width || height < minViewportResolution.height) {
155
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} is smaller than the minimum viewport resolution ${minViewportResolution.width}x${minViewportResolution.height} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
156
+ }
157
+ if (viewport.x2 > videoSensorParams.sensorWidth ||
158
+ viewport.y2 > videoSensorParams.sensorHeight ||
159
+ width > videoSensorParams.sensorWidth ||
160
+ height > videoSensorParams.sensorHeight) {
161
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} at (${viewport.x1}, ${viewport.y1}) does not fit the sensor ${videoSensorParams.sensorWidth}x${videoSensorParams.sensorHeight} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
162
+ }
163
+ if (!sameAspectRatio(width, height, viewportWidth(entry.viewport), viewportHeight(entry.viewport))) {
164
+ throw new StatusResponseError(`MatterbridgeCameraAvSettingsUserLevelManagementServer: viewport ${width}x${height} does not match the aspect ratio of video stream ${videoStreamId} (${viewportWidth(entry.viewport)}x${viewportHeight(entry.viewport)}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.DynamicConstraintError);
165
+ }
166
+ this.applyDptzViewport(videoStreamId, viewport);
167
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: set the viewport of video stream ${videoStreamId} to (${viewport.x1}, ${viewport.y1})-(${viewport.x2}, ${viewport.y2}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
168
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'dptzSetViewport', request, this.context);
169
+ }
170
+ dptzRelativeMove(request) {
171
+ const device = this.endpoint.stateOf(MatterbridgeServer);
172
+ const { videoStreamId, deltaX, deltaY, zoomDelta } = request;
173
+ const entry = this.dptzStreamEntry(videoStreamId);
174
+ const { videoSensorParams, minViewportResolution } = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state;
175
+ const { sensorWidth, sensorHeight } = videoSensorParams;
176
+ const current = entry.viewport;
177
+ let x1 = current.x1 + (deltaX ?? 0);
178
+ let y1 = current.y1 + (deltaY ?? 0);
179
+ let width = viewportWidth(current);
180
+ let height = viewportHeight(current);
181
+ if (zoomDelta !== undefined && zoomDelta !== 0) {
182
+ const scaledWidth = Math.round((width * (100 - zoomDelta)) / 100);
183
+ const scaledHeight = Math.round((height * (100 - zoomDelta)) / 100);
184
+ x1 += Math.round((width - scaledWidth) / 2);
185
+ y1 += Math.round((height - scaledHeight) / 2);
186
+ width = scaledWidth;
187
+ height = scaledHeight;
188
+ }
189
+ const streamWidth = viewportWidth(current);
190
+ const streamHeight = viewportHeight(current);
191
+ if (!sameAspectRatio(width, height, streamWidth, streamHeight)) {
192
+ const clippedHeight = Math.round((width * streamHeight) / streamWidth);
193
+ if (clippedHeight <= height)
194
+ height = clippedHeight;
195
+ else
196
+ width = Math.round((height * streamWidth) / streamHeight);
197
+ }
198
+ width = Math.max(width, minViewportResolution.width);
199
+ height = Math.max(height, minViewportResolution.height);
200
+ width = Math.min(width, sensorWidth);
201
+ height = Math.min(height, sensorHeight);
202
+ const viewport = viewportAt(x1, y1, width, height, sensorWidth, sensorHeight);
203
+ this.applyDptzViewport(videoStreamId, viewport);
204
+ device.log.info(`MatterbridgeCameraAvSettingsUserLevelManagementServer: moved the viewport of video stream ${videoStreamId} by deltaX ${deltaX ?? 0}, deltaY ${deltaY ?? 0}, zoomDelta ${zoomDelta ?? 0} to (${viewport.x1}, ${viewport.y1})-(${viewport.x2}, ${viewport.y2}) (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
205
+ this.endpoint.emitCommand(CameraAvSettingsUserLevelManagement, 'dptzRelativeMove', request, this.context);
206
+ }
45
207
  }
208
+ (function (MatterbridgeCameraAvSettingsUserLevelManagementServer) {
209
+ class Internal {
210
+ lastGeneratedPresetId = 0;
211
+ }
212
+ MatterbridgeCameraAvSettingsUserLevelManagementServer.Internal = Internal;
213
+ })(MatterbridgeCameraAvSettingsUserLevelManagementServer || (MatterbridgeCameraAvSettingsUserLevelManagementServer = {}));
46
214
  export function createDefaultCameraAvSettingsUserLevelManagementClusterServer(endpoint, options) {
47
- endpoint.behaviors.require(MatterbridgeCameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom), {
215
+ endpoint.behaviors.require(MatterbridgeCameraAvSettingsUserLevelManagementServer.with(CameraAvSettingsUserLevelManagement.Feature.MechanicalPan, CameraAvSettingsUserLevelManagement.Feature.MechanicalTilt, CameraAvSettingsUserLevelManagement.Feature.MechanicalZoom, CameraAvSettingsUserLevelManagement.Feature.MechanicalPresets, CameraAvSettingsUserLevelManagement.Feature.DigitalPtz), {
48
216
  ...options,
49
217
  movementState: CameraAvSettingsUserLevelManagement.PhysicalMovement.Idle,
218
+ mptzPresets: [],
219
+ dptzStreams: [],
50
220
  });
51
221
  return endpoint;
52
222
  }
@@ -185,6 +185,9 @@ export class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCame
185
185
  if (!this.state.allocatedVideoStreams.some((stream) => stream.videoStreamId === request.videoStreamId)) {
186
186
  throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: video stream ${request.videoStreamId} is not present in allocatedVideoStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
187
187
  }
188
+ if (this.state.allocatedVideoStreams.some((stream) => stream.videoStreamId === request.videoStreamId && stream.referenceCount > 0)) {
189
+ throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: video stream ${request.videoStreamId} is in use (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
190
+ }
188
191
  this.state.allocatedVideoStreams = this.state.allocatedVideoStreams.filter((stream) => stream.videoStreamId !== request.videoStreamId);
189
192
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated video stream ${request.videoStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
190
193
  this.endpoint.emitCommand(CameraAvStreamManagement, 'videoStreamDeallocate', request, this.context);
@@ -248,6 +251,9 @@ export class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCame
248
251
  if (!this.state.allocatedAudioStreams.some((stream) => stream.audioStreamId === request.audioStreamId)) {
249
252
  throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: audio stream ${request.audioStreamId} is not present in allocatedAudioStreams (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
250
253
  }
254
+ if (this.state.allocatedAudioStreams.some((stream) => stream.audioStreamId === request.audioStreamId && stream.referenceCount > 0)) {
255
+ throw new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: audio stream ${request.audioStreamId} is in use (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
256
+ }
251
257
  this.state.allocatedAudioStreams = this.state.allocatedAudioStreams.filter((stream) => stream.audioStreamId !== request.audioStreamId);
252
258
  device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated audio stream ${request.audioStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
253
259
  this.endpoint.emitCommand(CameraAvStreamManagement, 'audioStreamDeallocate', request, this.context);
@@ -1,11 +1,25 @@
1
1
  import { ColorControlServer } from '@matter/node/behaviors/color-control';
2
2
  import { ColorControl } from '@matter/types/clusters/color-control';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeColorControlServer extends ColorControlServer.with(ColorControl.Feature.HueSaturation, ColorControl.Feature.Xy, ColorControl.Feature.ColorTemperature, ColorControl.Feature.EnhancedHue) {
5
5
  initialize() {
6
6
  if (process.env.MATTERBRIDGE_CHIP_TEST)
7
7
  this.state.managedTransitionTimeHandling = true;
8
- return super.initialize();
8
+ const result = super.initialize();
9
+ if (this.features.colorTemperature && !isSoftwareUpdateBoot(this.env)) {
10
+ const startUpMireds = this.state.startUpColorTemperatureMireds ?? null;
11
+ if (startUpMireds !== null) {
12
+ const crop = (mireds) => Math.min(Math.max(mireds, this.minimumColorTemperatureMireds), this.maximumColorTemperatureMireds);
13
+ const currentMireds = crop(this.state.colorTemperatureMireds);
14
+ const targetMireds = crop(startUpMireds);
15
+ if (targetMireds !== currentMireds) {
16
+ this.state.colorMode = ColorControl.ColorMode.ColorTemperatureMireds;
17
+ this.state.enhancedColorMode = ColorControl.EnhancedColorMode.ColorTemperatureMireds;
18
+ this.state.colorTemperatureMireds = targetMireds;
19
+ }
20
+ }
21
+ }
22
+ return result;
9
23
  }
10
24
  async moveToHue(request) {
11
25
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
@@ -1,11 +1,20 @@
1
1
  import { LevelControlServer } from '@matter/node/behaviors/level-control';
2
2
  import { LevelControl } from '@matter/types/clusters/level-control';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeLevelControlServer extends LevelControlServer {
5
5
  initialize() {
6
6
  if (process.env.MATTERBRIDGE_CHIP_TEST)
7
7
  this.state.managedTransitionTimeHandling = true;
8
- return super.initialize();
8
+ const result = super.initialize();
9
+ if (this.features.lighting && !isSoftwareUpdateBoot(this.env)) {
10
+ const state = this.state;
11
+ const startUpCurrentLevel = state.startUpCurrentLevel ?? null;
12
+ const currentLevel = state.currentLevel;
13
+ const targetLevel = startUpCurrentLevel === 0 ? this.minLevel : (startUpCurrentLevel ?? currentLevel);
14
+ if (targetLevel !== currentLevel)
15
+ state.currentLevel = targetLevel;
16
+ }
17
+ return result;
9
18
  }
10
19
  async moveToLevel(request) {
11
20
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
@@ -1,6 +1,8 @@
1
+ import type { Environment } from '@matter/general';
1
2
  import { Behavior } from '@matter/node';
2
3
  import type { AnsiLogger } from 'node-ansi-logger';
3
4
  import type { CommandHandler } from '../matterbridgeEndpointCommandHandler.js';
5
+ export declare function isSoftwareUpdateBoot(env: Environment): boolean;
4
6
  export declare class MatterbridgeServer extends Behavior {
5
7
  static readonly id = "matterbridge";
6
8
  state: MatterbridgeServer.State;
@@ -1,4 +1,12 @@
1
- import { Behavior } from '@matter/node';
1
+ import { Behavior, ServerNode } from '@matter/node';
2
+ import { GeneralDiagnosticsBehavior } from '@matter/node/behaviors/general-diagnostics';
3
+ import { GeneralDiagnostics } from '@matter/types/clusters/general-diagnostics';
4
+ export function isSoftwareUpdateBoot(env) {
5
+ const rootEndpoint = env.get(ServerNode);
6
+ if (!rootEndpoint.behaviors.has(GeneralDiagnosticsBehavior))
7
+ return false;
8
+ return rootEndpoint.stateOf(GeneralDiagnosticsBehavior).bootReason === GeneralDiagnostics.BootReason.SoftwareUpdateCompleted;
9
+ }
2
10
  export class MatterbridgeServer extends Behavior {
3
11
  static id = 'matterbridge';
4
12
  initialize() {
@@ -1,3 +1,4 @@
1
+ import type { MaybePromise } from '@matter/general';
1
2
  import { OnOffServer } from '@matter/node/behaviors/on-off';
2
3
  import { OnOff } from '@matter/types/clusters/on-off';
3
4
  import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
@@ -16,6 +17,7 @@ declare const MatterbridgeOnOffServer_base: import("@matter/node").ClusterBehavi
16
17
  }>, import("@matter/types").ClusterType.Concrete, typeof import("@matter/node/behaviors/on-off").OnOffBaseServer.Internal, "onOff">;
17
18
  export declare class MatterbridgeOnOffServer extends MatterbridgeOnOffServer_base {
18
19
  readonly endpoint: MatterbridgeEndpoint;
20
+ initialize(): MaybePromise;
19
21
  on(): Promise<void>;
20
22
  off(): Promise<void>;
21
23
  toggle(): Promise<void>;
@@ -1,7 +1,20 @@
1
1
  import { OnOffServer } from '@matter/node/behaviors/on-off';
2
2
  import { OnOff } from '@matter/types/clusters/on-off';
3
- import { MatterbridgeServer } from './matterbridgeServer.js';
3
+ import { isSoftwareUpdateBoot, MatterbridgeServer } from './matterbridgeServer.js';
4
4
  export class MatterbridgeOnOffServer extends OnOffServer.with(OnOff.Feature.Lighting) {
5
+ initialize() {
6
+ const result = super.initialize();
7
+ if (this.features.lighting && !isSoftwareUpdateBoot(this.env)) {
8
+ const startUpOnOff = this.state.startUpOnOff ?? null;
9
+ if (startUpOnOff !== null) {
10
+ const currentOnOff = this.state.onOff;
11
+ const targetOnOff = startUpOnOff === OnOff.StartUpOnOff.Toggle ? !currentOnOff : startUpOnOff === OnOff.StartUpOnOff.On;
12
+ if (targetOnOff !== currentOnOff)
13
+ this.state.onOff = targetOnOff;
14
+ }
15
+ }
16
+ return result;
17
+ }
5
18
  async on() {
6
19
  if (process.env.MATTERBRIDGE_CHIP_TEST) {
7
20
  await super.on();
@@ -24,6 +24,7 @@ export declare namespace MatterbridgeWebRtcTransportProviderServer {
24
24
  class Internal {
25
25
  nextWebRtcSessionId: number;
26
26
  sessions: Map<number, WeriftWebRtcSession>;
27
+ trickledSessions: Set<number>;
27
28
  }
28
29
  }
29
30
  export declare function createDefaultWebRtcTransportProviderClusterServer(endpoint: MatterbridgeEndpoint, weriftOfferOptions?: WeriftOfferOptions): MatterbridgeEndpoint;
@@ -32,6 +32,15 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
32
32
  });
33
33
  }, DEFERRED_INVOKE_DELAY_MS);
34
34
  }
35
+ async #trickleLocalIceCandidates(requestorEndpoint, webRtcSessionId, webRtcPeer) {
36
+ const iceCandidates = webRtcPeer.localIceCandidates;
37
+ if (iceCandidates.length === 0)
38
+ return;
39
+ await requestorEndpoint.commandsOf(WebRtcTransportRequestorClient).iceCandidates({ webRtcSessionId, iceCandidates });
40
+ this.endpoint
41
+ .stateOf(MatterbridgeServer)
42
+ .log.info(`MatterbridgeWebRtcTransportProviderServer: invoked ICECandidates with ${iceCandidates.length} local candidate(s) for session ${webRtcSessionId} on the peer's WebRtcTransportRequestor`);
43
+ }
35
44
  async #resolvePeerRequestorEndpoint(peerNodeId, fabricIndex, peerEndpointId) {
36
45
  try {
37
46
  const serverNode = Node.forEndpoint(this.endpoint);
@@ -208,6 +217,19 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
208
217
  }
209
218
  return true;
210
219
  }
220
+ #updateStreamReferenceCounts(videoStreams, audioStreams, change) {
221
+ if (!this.endpoint.behaviors.has(MatterbridgeCameraAvStreamManagementServer))
222
+ return;
223
+ const state = this.agent.get(MatterbridgeCameraAvStreamManagementServer).state;
224
+ for (const stream of state.allocatedVideoStreams) {
225
+ if (videoStreams?.includes(stream.videoStreamId))
226
+ stream.referenceCount = Math.max(0, stream.referenceCount + change);
227
+ }
228
+ for (const stream of state.allocatedAudioStreams) {
229
+ if (audioStreams?.includes(stream.audioStreamId))
230
+ stream.referenceCount = Math.max(0, stream.referenceCount + change);
231
+ }
232
+ }
211
233
  async solicitOffer(request) {
212
234
  this.#validateNoConflictingStreamFields(request);
213
235
  let videoStreams;
@@ -252,6 +274,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
252
274
  this.endpoint.emitCommand(WebRtcTransportProvider, 'solicitOffer', request, this.context);
253
275
  return { webRtcSessionId, deferredOffer: false, ...this.#echoDeprecatedStreamIds(request, videoStreams, audioStreams) };
254
276
  }
277
+ this.#updateStreamReferenceCounts(videoStreams, audioStreams, 1);
255
278
  const webRtcPeer = new WeriftWebRtcSession(webRtcSessionId, this.state.weriftOfferOptions);
256
279
  this.internal.sessions.set(webRtcSessionId, webRtcPeer);
257
280
  const sdp = await webRtcPeer.createOffer({
@@ -309,11 +332,11 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
309
332
  fabricIndex,
310
333
  },
311
334
  ];
312
- const requestorEndpointForCapacityCheck = await this.#resolvePeerRequestorEndpoint(peerNodeId, fabricIndex, request.originatingEndpointId ?? EndpointNumber(0));
313
- if (this.#evictIfOverCapacity(webRtcSessionId, requestorEndpointForCapacityCheck)) {
314
- this.endpoint.emitCommand(WebRtcTransportProvider, 'provideOffer', request, this.context);
315
- return { webRtcSessionId, ...this.#echoDeprecatedStreamIds(request, videoStreams, audioStreams) };
335
+ if (this.state.currentSessions.length > MAX_CONCURRENT_SESSIONS) {
336
+ this.state.currentSessions = this.state.currentSessions.filter((session) => session.id !== webRtcSessionId);
337
+ throw new StatusResponseError(`MatterbridgeWebRtcTransportProviderServer.provideOffer: cannot sustain more than MAX_CONCURRENT_SESSIONS (${MAX_CONCURRENT_SESSIONS}) concurrent webRTC sessions (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ResourceExhausted);
316
338
  }
339
+ this.#updateStreamReferenceCounts(videoStreams, audioStreams, 1);
317
340
  }
318
341
  else if (!this.state.currentSessions.some((session) => session.id === webRtcSessionId)) {
319
342
  throw new StatusResponseError(`MatterbridgeWebRtcTransportProviderServer.provideOffer: webRTC session ${webRtcSessionId} is not present in currentSessions (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
@@ -387,15 +410,27 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
387
410
  `with ${String(error)} (endpoint ${endpointLabel})`);
388
411
  }
389
412
  }));
413
+ const session = this.state.currentSessions.find((candidateSession) => candidateSession.id === request.webRtcSessionId);
414
+ if (session && !this.internal.trickledSessions.has(request.webRtcSessionId)) {
415
+ this.internal.trickledSessions.add(request.webRtcSessionId);
416
+ this.#invokeDeferred(async () => {
417
+ const requestorEndpoint = await this.#resolvePeerRequestorEndpoint(session.peerNodeId, session.fabricIndex, session.peerEndpointId);
418
+ if (requestorEndpoint)
419
+ await this.#trickleLocalIceCandidates(requestorEndpoint, request.webRtcSessionId, webRtcPeer);
420
+ }, `ICECandidates for session ${request.webRtcSessionId}`);
421
+ }
390
422
  }
391
423
  this.endpoint.emitCommand(WebRtcTransportProvider, 'provideIceCandidates', request, this.context);
392
424
  }
393
425
  async endSession(request) {
394
426
  const device = this.endpoint.stateOf(MatterbridgeServer);
395
- if (!this.state.currentSessions.some((session) => session.id === request.webRtcSessionId)) {
427
+ const session = this.state.currentSessions.find((session) => session.id === request.webRtcSessionId);
428
+ if (!session) {
396
429
  throw new StatusResponseError(`MatterbridgeWebRtcTransportProviderServer.endSession: webRTC session ${request.webRtcSessionId} is not present in currentSessions (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.NotFound);
397
430
  }
431
+ this.#updateStreamReferenceCounts(session.videoStreams, session.audioStreams, -1);
398
432
  this.state.currentSessions = this.state.currentSessions.filter((session) => session.id !== request.webRtcSessionId);
433
+ this.internal.trickledSessions.delete(request.webRtcSessionId);
399
434
  const webRtcPeer = this.internal.sessions.get(request.webRtcSessionId);
400
435
  if (webRtcPeer) {
401
436
  this.internal.sessions.delete(request.webRtcSessionId);
@@ -413,6 +448,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
413
448
  class Internal {
414
449
  nextWebRtcSessionId = 0;
415
450
  sessions = new Map();
451
+ trickledSessions = new Set();
416
452
  }
417
453
  MatterbridgeWebRtcTransportProviderServer.Internal = Internal;
418
454
  })(MatterbridgeWebRtcTransportProviderServer || (MatterbridgeWebRtcTransportProviderServer = {}));
@@ -29,9 +29,15 @@ export declare class WeriftWebRtcSession {
29
29
  private static readonly DIAGNOSTICS_INTERVAL_MS;
30
30
  private lastDiagnosticsCounters?;
31
31
  private readonly dtlsTransportsWithStateLogging;
32
+ private readonly gatheredIceCandidates;
32
33
  private static parseIcePortRange;
33
34
  private static parseIceAdditionalHostAddresses;
34
35
  constructor(webRtcSessionId: number, options: WeriftOfferOptions);
36
+ get localIceCandidates(): {
37
+ candidate: string;
38
+ sdpMid: string | null;
39
+ sdpmLineIndex: number | null;
40
+ }[];
35
41
  private summarizeSdp;
36
42
  private getFreeUdpPort;
37
43
  private getPreferredInjectableVideoCodec;
@@ -21,6 +21,7 @@ export class WeriftWebRtcSession {
21
21
  static DIAGNOSTICS_INTERVAL_MS = 10_000;
22
22
  lastDiagnosticsCounters;
23
23
  dtlsTransportsWithStateLogging = new Set();
24
+ gatheredIceCandidates = [];
24
25
  static parseIcePortRange() {
25
26
  const raw = process.env.MATTERBRIDGE_ICE_PORT_RANGE?.trim();
26
27
  if (!raw)
@@ -55,6 +56,9 @@ export class WeriftWebRtcSession {
55
56
  });
56
57
  this.peerConnection.onIceCandidate.subscribe((candidate) => {
57
58
  this.log.debug(candidate ? `Gathered local ICE candidate: ${candidate.candidate}` : 'ICE candidate gathering completed');
59
+ if (candidate) {
60
+ this.gatheredIceCandidates.push({ candidate: candidate.candidate, sdpMid: candidate.sdpMid ?? null, sdpmLineIndex: candidate.sdpMLineIndex ?? null });
61
+ }
58
62
  });
59
63
  this.peerConnection.iceConnectionStateChange.subscribe((state) => {
60
64
  this.log.info(`ICE connection state: ${state}`);
@@ -89,6 +93,9 @@ export class WeriftWebRtcSession {
89
93
  });
90
94
  }
91
95
  }
96
+ get localIceCandidates() {
97
+ return [...this.gatheredIceCandidates];
98
+ }
92
99
  summarizeSdp(sdp) {
93
100
  const mediaKinds = sdp
94
101
  .split('\n')
@@ -7,4 +7,5 @@ export declare class MatterbridgeGeneralDiagnosticsServer extends GeneralDiagnos
7
7
  testEventTrigger({ eventTrigger, enableKey }: GeneralDiagnostics.TestEventTriggerRequest): Promise<void>;
8
8
  payloadTestRequest(request: GeneralDiagnostics.PayloadTestRequest): ReturnType<GeneralDiagnosticsServer['payloadTestRequest']>;
9
9
  }
10
+ export declare function createChipTestRestartFlag(matterbridge: Matterbridge): void;
10
11
  export declare function createChipTestAppPipe(matterbridge: Matterbridge): void;
package/dist/chipTests.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { closeSync, constants, existsSync, openSync, readSync, unlinkSync } from 'node:fs';
2
+ import { closeSync, constants, existsSync, openSync, readFileSync, readSync, unlinkSync } from 'node:fs';
3
3
  import { Seconds, Time } from '@matter/general';
4
4
  import { BasicInformationServer } from '@matter/node/behaviors/basic-information';
5
5
  import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
@@ -30,6 +30,8 @@ import { MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer } f
30
30
  import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
31
31
  import { featuresFor } from './matterbridgeEndpointHelpers.js';
32
32
  const chipTestAppPipePath = '/tmp/matterbridge-chip-test-app-pipe';
33
+ const chipTestRestartFlagPath = '/tmp/matterbridge-chip-test-restart-flag';
34
+ const chipTestRestartFlagText = 'restart';
33
35
  const chipTestValidEventTrigger = 0xfffffffffff10000n;
34
36
  const smokeCoAlarmWarningSmokeAlarmTrigger = 0x005c000000000090n;
35
37
  const smokeCoAlarmCriticalSmokeAlarmTrigger = 0x005c00000000009cn;
@@ -101,6 +103,9 @@ export const chipTestEnableKey = Uint8Array.from({ length: 16 }, (_, index) => i
101
103
  const smokeCoAlarmChipTestEnableKey = Uint8Array.from([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]);
102
104
  let chipTestMatterbridge;
103
105
  let closeChipTestAppPipe;
106
+ let chipTestRestartFlagTimer;
107
+ let chipTestRestartPending = false;
108
+ let chipTestRestartFlagWarned = false;
104
109
  let electricalPowerMeasurementFakeLoadTimer;
105
110
  let chipTestActiveEndpointId;
106
111
  export class MatterbridgeGeneralDiagnosticsServer extends GeneralDiagnosticsServer {
@@ -130,6 +135,65 @@ export class MatterbridgeGeneralDiagnosticsServer extends GeneralDiagnosticsServ
130
135
  return super.payloadTestRequest(request);
131
136
  }
132
137
  }
138
+ function removeChipTestRestartFlag(log) {
139
+ try {
140
+ if (existsSync(chipTestRestartFlagPath))
141
+ unlinkSync(chipTestRestartFlagPath);
142
+ }
143
+ catch (error) {
144
+ log.error(`Failed to remove CHIP test restart flag ${chipTestRestartFlagPath}: ${error instanceof Error ? error.message : String(error)}`);
145
+ }
146
+ }
147
+ export function createChipTestRestartFlag(matterbridge) {
148
+ if (chipTestRestartPending) {
149
+ const onOnline = (nodeId) => {
150
+ if (nodeId !== 'Matterbridge')
151
+ return;
152
+ matterbridge.off('online', onOnline);
153
+ removeChipTestRestartFlag(matterbridge.log);
154
+ chipTestRestartPending = false;
155
+ matterbridge.log.notice(`CHIP test restart completed, cleared restart flag ${chipTestRestartFlagPath}`);
156
+ };
157
+ matterbridge.on('online', onOnline);
158
+ }
159
+ if (chipTestRestartFlagTimer)
160
+ return;
161
+ matterbridge.log.notice(`CHIP test restart flag monitor watching ${chipTestRestartFlagPath}`);
162
+ chipTestRestartFlagTimer = setInterval(() => {
163
+ const instance = chipTestMatterbridge;
164
+ if (!instance || chipTestRestartPending || !existsSync(chipTestRestartFlagPath))
165
+ return;
166
+ let flagText;
167
+ try {
168
+ flagText = readFileSync(chipTestRestartFlagPath, 'utf8').trim();
169
+ }
170
+ catch (error) {
171
+ instance.log.error(`Failed to read CHIP test restart flag ${chipTestRestartFlagPath}: ${error instanceof Error ? error.message : String(error)}`);
172
+ return;
173
+ }
174
+ if (flagText !== chipTestRestartFlagText) {
175
+ if (!chipTestRestartFlagWarned) {
176
+ chipTestRestartFlagWarned = true;
177
+ instance.log.warn(`Ignoring unsupported CHIP test restart flag content "${flagText}": only "${chipTestRestartFlagText}" is implemented`);
178
+ }
179
+ return;
180
+ }
181
+ chipTestRestartFlagWarned = false;
182
+ chipTestRestartPending = true;
183
+ instance.log.notice(`CHIP test restart requested via ${chipTestRestartFlagPath}, restarting Matterbridge...`);
184
+ instance.restartProcess().catch((error) => {
185
+ chipTestRestartPending = false;
186
+ instance.log.error(`Failed to restart Matterbridge for the CHIP test restart flag: ${error instanceof Error ? error.message : String(error)}`);
187
+ });
188
+ }, 250);
189
+ chipTestRestartFlagTimer.unref();
190
+ cliEmitter.once('shutdown', () => {
191
+ if (chipTestRestartFlagTimer)
192
+ clearInterval(chipTestRestartFlagTimer);
193
+ chipTestRestartFlagTimer = undefined;
194
+ removeChipTestRestartFlag(matterbridge.log);
195
+ });
196
+ }
133
197
  function isChipTestEnableKey(keyData) {
134
198
  return equalsBytes(keyData, chipTestEnableKey) || equalsBytes(keyData, smokeCoAlarmChipTestEnableKey);
135
199
  }
package/dist/cli.js CHANGED
@@ -180,6 +180,7 @@ function help() {
180
180
  --fixed_delay [seconds]: set a fixed delay in seconds before starting Matterbridge (default 120)
181
181
  --no-ansi: disable ANSI color output in the logs
182
182
  --reset-sessions: reset sessions and resumption records on shutdown (use only if your controller has issue reconnecting on restart)
183
+ --configuration-version: increment by one the configuration version of all the server nodes when they go online (add a value to set it instead, i.e. --configuration-version 5)
183
184
  `);
184
185
  process.exit(0);
185
186
  }
@@ -160,6 +160,7 @@ export async function createDemoDevices(matterbridge) {
160
160
  author: 'https://github.com/Luligu',
161
161
  enabled: false,
162
162
  private: true,
163
+ configurationVersion: 1,
163
164
  registeredDevices: 0,
164
165
  configJson: demoPluginConfig,
165
166
  schemaJson: demoPluginSchema,
@@ -30,6 +30,7 @@ export interface CameraOptions extends MatterbridgeEndpointOptions {
30
30
  tiltMax?: number;
31
31
  zoomMax?: number;
32
32
  mptzPosition?: CameraAvSettingsUserLevelManagement.Mptz;
33
+ maxPresets?: number;
33
34
  weriftOfferOptions?: WeriftOfferOptions;
34
35
  }
35
36
  export declare class Camera extends MatterbridgeEndpoint {
@@ -17,7 +17,7 @@ export class Camera extends MatterbridgeEndpoint {
17
17
  { resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
18
18
  { resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
19
19
  { resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
20
- ], panMin = -170, panMax = 170, tiltMin = -20, tiltMax = 90, zoomMax = 10, mptzPosition = { pan: 0, tilt: 0, zoom: 1 }, weriftOfferOptions, id, number, tagList, mode, } = options;
20
+ ], panMin = -170, panMax = 170, tiltMin = -20, tiltMax = 90, zoomMax = 10, mptzPosition = { pan: 0, tilt: 0, zoom: 1 }, maxPresets = 5, weriftOfferOptions, id, number, tagList, mode, } = options;
21
21
  super(powerSourceType === 'None' ? [camera] : [camera, powerSource], { id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`, number, tagList, mode });
22
22
  if (identifyType !== Identify.IdentifyType.None) {
23
23
  this.createDefaultIdentifyClusterServer(identifyTime, identifyType);
@@ -56,7 +56,7 @@ export class Camera extends MatterbridgeEndpoint {
56
56
  snapshotCapabilities,
57
57
  });
58
58
  if (ptz)
59
- createDefaultCameraAvSettingsUserLevelManagementClusterServer(this, { panMin, panMax, tiltMin, tiltMax, zoomMax, mptzPosition });
59
+ createDefaultCameraAvSettingsUserLevelManagementClusterServer(this, { panMin, panMax, tiltMin, tiltMax, zoomMax, mptzPosition, maxPresets });
60
60
  createDefaultWebRtcTransportProviderClusterServer(this, weriftOfferOptions);
61
61
  addWebRtcTransportRequestorClient(this);
62
62
  this.addRequiredClusters();
package/dist/frontend.js CHANGED
@@ -1268,6 +1268,7 @@ export class Frontend extends EventEmitter {
1268
1268
  name: plugin.name,
1269
1269
  version: plugin.version,
1270
1270
  private: plugin.private,
1271
+ configurationVersion: plugin.configurationVersion,
1271
1272
  description: plugin.description,
1272
1273
  author: plugin.author,
1273
1274
  homepage: plugin.homepage,
@@ -10,7 +10,7 @@ import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/brid
10
10
  import { PowerSourceServer } from '@matter/node/behaviors/power-source';
11
11
  import { AggregatorEndpoint } from '@matter/node/endpoints/aggregator';
12
12
  import { MdnsService, PaseClient } from '@matter/protocol';
13
- import { DeviceTypeId, VendorId } from '@matter/types';
13
+ import { DeviceTypeId, EndpointNumber, VendorId } from '@matter/types';
14
14
  import { PowerSource } from '@matter/types/clusters/power-source';
15
15
  import { BroadcastServer } from '@matterbridge/thread/server';
16
16
  import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg } from '@matterbridge/types';
@@ -19,7 +19,7 @@ import { copyDirectory } from '@matterbridge/utils/copy-dir';
19
19
  import { getErrorMessage, inspectError } from '@matterbridge/utils/error';
20
20
  import { logModuleLoaded } from '@matterbridge/utils/loader';
21
21
  import { isValidInteger, isValidString, parseVersionString } from '@matterbridge/utils/validate';
22
- import { wait, withTimeout } from '@matterbridge/utils/wait';
22
+ import { fireAndForget, wait, withTimeout } from '@matterbridge/utils/wait';
23
23
  import { AnsiLogger, BLUE, CYAN, db, debugStringify, er, nf, or, zb } from 'node-ansi-logger';
24
24
  import { NodeStorageManager } from 'node-persist-manager';
25
25
  import { toBaseDevice } from './deviceManager.js';
@@ -471,7 +471,7 @@ export class MatterNode extends EventEmitter {
471
471
  status: PowerSource.PowerSourceStatus.Active,
472
472
  order: 0,
473
473
  description: 'AC Power',
474
- endpointList: [],
474
+ endpointList: [EndpointNumber(0)],
475
475
  wiredCurrentType: PowerSource.WiredCurrentType.Ac,
476
476
  },
477
477
  }
@@ -490,6 +490,13 @@ export class MatterNode extends EventEmitter {
490
490
  });
491
491
  serverNode.lifecycle.online.on(() => {
492
492
  this.log.notice(`Server node for ${storeId} is online`);
493
+ this.log.info(`Configuration version for server node ${storeId} is ${serverNode.state.basicInformation.configurationVersion}`);
494
+ if (hasParameter('configuration-version')) {
495
+ let configurationVersion = getIntParameter('configuration-version') ?? serverNode.state.basicInformation.configurationVersion + 1;
496
+ configurationVersion = Math.min(Math.max(configurationVersion, 1), UINT32_MAX);
497
+ fireAndForget(serverNode.setStateOf(BasicInformationServer, { configurationVersion }), this.log, `Failed to set configuration version for server node ${storeId}`);
498
+ this.log.notice(`Configuration version for server node ${storeId} is now ${configurationVersion}`);
499
+ }
493
500
  if (!serverNode.lifecycle.isCommissioned) {
494
501
  this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
495
502
  this.advertisingNodes.set(storeId, Date.now());
@@ -640,7 +647,7 @@ export class MatterNode extends EventEmitter {
640
647
  if (!plugin.locked) {
641
648
  plugin.locked = true;
642
649
  this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
643
- this.matterStorageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, undefined, undefined, parseVersionString(this.matterbridge.matterbridgeVersion), this.matterbridge.matterbridgeVersion, parseVersionString(this.matterbridge.systemInformation.osRelease), this.matterbridge.systemInformation.osRelease, plugin.description, plugin.homepage ?? 'https://matterbridge.io', 1);
650
+ this.matterStorageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, undefined, undefined, parseVersionString(this.matterbridge.matterbridgeVersion), this.matterbridge.matterbridgeVersion, parseVersionString(this.matterbridge.systemInformation.osRelease), this.matterbridge.systemInformation.osRelease, plugin.description, plugin.homepage ?? 'https://matterbridge.io', plugin.configurationVersion);
644
651
  this.serverNode = await this.createServerNode(this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
645
652
  this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
646
653
  this.aggregatorNode = await this.createAggregatorNode();
@@ -12,7 +12,7 @@ import { PowerSourceServer } from '@matter/node/behaviors/power-source';
12
12
  import { AggregatorEndpoint } from '@matter/node/endpoints/aggregator';
13
13
  import { PaseClient } from '@matter/protocol';
14
14
  import { PowerSource } from '@matter/types/clusters/power-source';
15
- import { DeviceTypeId, VendorId } from '@matter/types/datatype';
15
+ import { DeviceTypeId, EndpointNumber, VendorId } from '@matter/types/datatype';
16
16
  import { ManualPairingCodeCodec } from '@matter/types/schema';
17
17
  import { BroadcastServer } from '@matterbridge/thread/server';
18
18
  import { dev, MATTER_LOGGER_FILE, MATTER_STORAGE_DIR, MATTERBRIDGE_LOGGER_FILE, NODE_STORAGE_DIR, plg, typ } from '@matterbridge/types';
@@ -1248,6 +1248,10 @@ export class Matterbridge extends EventEmitter {
1248
1248
  this.log.info('Unregistering all devices and shutting down...');
1249
1249
  if (this.serverNode)
1250
1250
  await this.serverNode.setStateOf(BasicInformationServer, { configurationVersion: this.serverNode.state.basicInformation.configurationVersion + 1 });
1251
+ for (const device of this.devices.array()) {
1252
+ if (device.serverNode)
1253
+ await device.serverNode.setStateOf(BasicInformationServer, { configurationVersion: device.serverNode.state.basicInformation.configurationVersion + 1 });
1254
+ }
1251
1255
  for (const plugin of this.plugins.array()) {
1252
1256
  if (plugin.error || !plugin.enabled)
1253
1257
  continue;
@@ -1570,8 +1574,9 @@ export class Matterbridge extends EventEmitter {
1570
1574
  this.log.warn(' ***********************************************************************************');
1571
1575
  this.log.warn(' * MATTERBRIDGE_CHIP_TEST environment variable is set. Running chip test app pipe. *');
1572
1576
  this.log.warn(' ***********************************************************************************');
1573
- const { createChipTestAppPipe } = await import('./chipTests.js');
1577
+ const { createChipTestAppPipe, createChipTestRestartFlag } = await import('./chipTests.js');
1574
1578
  createChipTestAppPipe(this);
1579
+ createChipTestRestartFlag(this);
1575
1580
  }
1576
1581
  await this.startPlugins(false, true);
1577
1582
  this.log.debug('Starting start matter interval in bridge mode...');
@@ -1943,7 +1948,7 @@ export class Matterbridge extends EventEmitter {
1943
1948
  status: PowerSource.PowerSourceStatus.Active,
1944
1949
  order: 0,
1945
1950
  description: 'AC Power',
1946
- endpointList: [],
1951
+ endpointList: [EndpointNumber(0)],
1947
1952
  wiredCurrentType: PowerSource.WiredCurrentType.Ac,
1948
1953
  },
1949
1954
  }
@@ -1964,6 +1969,13 @@ export class Matterbridge extends EventEmitter {
1964
1969
  });
1965
1970
  serverNode.lifecycle.online.on(() => {
1966
1971
  this.log.notice(`Server node for ${storeId} is online`);
1972
+ this.log.info(`Configuration version for server node ${storeId} is ${serverNode.state.basicInformation.configurationVersion}`);
1973
+ if (hasParameter('configuration-version')) {
1974
+ let configurationVersion = getIntParameter('configuration-version') ?? serverNode.state.basicInformation.configurationVersion + 1;
1975
+ configurationVersion = Math.min(Math.max(configurationVersion, 1), UINT32_MAX);
1976
+ fireAndForget(serverNode.setStateOf(BasicInformationServer, { configurationVersion }), this.log, `Failed to set configuration version for server node ${storeId}`);
1977
+ this.log.notice(`Configuration version for server node ${storeId} is now ${configurationVersion}`);
1978
+ }
1967
1979
  if (serverNode.lifecycle.isCommissioned) {
1968
1980
  this.log.notice(`Server node for ${storeId} is already commissioned.`);
1969
1981
  this.advertisingNodes.delete(storeId);
@@ -2075,7 +2087,7 @@ export class Matterbridge extends EventEmitter {
2075
2087
  if (!plugin.locked) {
2076
2088
  plugin.locked = true;
2077
2089
  this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} server node...`);
2078
- plugin.storageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, undefined, undefined, parseVersionString(this.matterbridgeVersion), this.matterbridgeVersion, parseVersionString(this.systemInformation.osRelease), this.systemInformation.osRelease, plugin.description, plugin.homepage ?? 'https://matterbridge.io', 1);
2090
+ plugin.storageContext = await this.createServerNodeContext(plugin.name, plugin.description, this.aggregatorDeviceType, this.aggregatorVendorId, this.aggregatorVendorName, this.aggregatorProductId, this.aggregatorProductName, undefined, undefined, parseVersionString(this.matterbridgeVersion), this.matterbridgeVersion, parseVersionString(this.systemInformation.osRelease), this.systemInformation.osRelease, plugin.description, plugin.homepage ?? 'https://matterbridge.io', plugin.configurationVersion);
2079
2091
  plugin.serverNode = await this.createServerNode(plugin.storageContext, this.port ? this.port++ : undefined, this.passcode ? this.passcode++ : undefined, this.discriminator ? this.discriminator++ : undefined);
2080
2092
  this.log.debug(`Creating dynamic plugin ${plg}${plugin.name}${db} aggregator node...`);
2081
2093
  plugin.aggregatorNode = await this.createAggregatorNode(plugin.storageContext);
@@ -172,11 +172,11 @@ export declare class MatterbridgeEndpoint extends Endpoint {
172
172
  createOffOnlyOnOffClusterServer(onOff?: boolean): this;
173
173
  createDefaultLevelControlClusterServer(currentLevel?: number, minLevel?: number, maxLevel?: number, onLevel?: number | null, startUpCurrentLevel?: number | null): this;
174
174
  createLevelControlClusterServer(currentLevel?: number, onLevel?: number | null): this;
175
- createDefaultColorControlClusterServer(currentX?: number, currentY?: number, currentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number): this;
176
- createEnhancedColorControlClusterServer(currentX?: number, currentY?: number, enhancedCurrentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number): this;
177
- createXyColorControlClusterServer(currentX?: number, currentY?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number): this;
178
- createHsColorControlClusterServer(currentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number): this;
179
- createCtColorControlClusterServer(colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number): this;
175
+ createDefaultColorControlClusterServer(currentX?: number, currentY?: number, currentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number, startUpColorTemperatureMireds?: number | null): this;
176
+ createEnhancedColorControlClusterServer(currentX?: number, currentY?: number, enhancedCurrentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number, startUpColorTemperatureMireds?: number | null): this;
177
+ createXyColorControlClusterServer(currentX?: number, currentY?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number, startUpColorTemperatureMireds?: number | null): this;
178
+ createHsColorControlClusterServer(currentHue?: number, currentSaturation?: number, colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number, startUpColorTemperatureMireds?: number | null): this;
179
+ createCtColorControlClusterServer(colorTemperatureMireds?: number, colorTempPhysicalMinMireds?: number, colorTempPhysicalMaxMireds?: number, startUpColorTemperatureMireds?: number | null): this;
180
180
  configureColorControlMode(colorMode: ColorControl.ColorMode): Promise<void>;
181
181
  configureEnhancedColorControlMode(colorMode: ColorControl.EnhancedColorMode): Promise<void>;
182
182
  createDefaultWindowCoveringClusterServer(positionPercent100ths?: number | null, type?: WindowCovering.WindowCoveringType, endProductType?: WindowCovering.EndProductType, movementDuration?: number): this;
@@ -720,7 +720,7 @@ export class MatterbridgeEndpoint extends Endpoint {
720
720
  });
721
721
  return this;
722
722
  }
723
- createDefaultColorControlClusterServer(currentX = 0, currentY = 0, currentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
723
+ createDefaultColorControlClusterServer(currentX = 0, currentY = 0, currentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500, startUpColorTemperatureMireds = null) {
724
724
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.HueSaturation, ColorControl.Feature.ColorTemperature), {
725
725
  colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation,
726
726
  enhancedColorMode: ColorControl.EnhancedColorMode.CurrentHueAndCurrentSaturation,
@@ -737,12 +737,12 @@ export class MatterbridgeEndpoint extends Endpoint {
737
737
  colorTempPhysicalMinMireds,
738
738
  colorTempPhysicalMaxMireds,
739
739
  coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
740
- startUpColorTemperatureMireds: null,
740
+ startUpColorTemperatureMireds,
741
741
  remainingTime: 0,
742
742
  });
743
743
  return this;
744
744
  }
745
- createEnhancedColorControlClusterServer(currentX = 0, currentY = 0, enhancedCurrentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
745
+ createEnhancedColorControlClusterServer(currentX = 0, currentY = 0, enhancedCurrentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500, startUpColorTemperatureMireds = null) {
746
746
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.HueSaturation, ColorControl.Feature.EnhancedHue, ColorControl.Feature.ColorTemperature), {
747
747
  colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation,
748
748
  enhancedColorMode: ColorControl.EnhancedColorMode.EnhancedCurrentHueAndCurrentSaturation,
@@ -760,12 +760,12 @@ export class MatterbridgeEndpoint extends Endpoint {
760
760
  colorTempPhysicalMinMireds,
761
761
  colorTempPhysicalMaxMireds,
762
762
  coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
763
- startUpColorTemperatureMireds: null,
763
+ startUpColorTemperatureMireds,
764
764
  remainingTime: 0,
765
765
  });
766
766
  return this;
767
767
  }
768
- createXyColorControlClusterServer(currentX = 0, currentY = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
768
+ createXyColorControlClusterServer(currentX = 0, currentY = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500, startUpColorTemperatureMireds = null) {
769
769
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.ColorTemperature), {
770
770
  colorMode: ColorControl.ColorMode.CurrentXAndCurrentY,
771
771
  enhancedColorMode: ColorControl.EnhancedColorMode.CurrentXAndCurrentY,
@@ -780,12 +780,12 @@ export class MatterbridgeEndpoint extends Endpoint {
780
780
  colorTempPhysicalMinMireds,
781
781
  colorTempPhysicalMaxMireds,
782
782
  coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
783
- startUpColorTemperatureMireds: null,
783
+ startUpColorTemperatureMireds,
784
784
  remainingTime: 0,
785
785
  });
786
786
  return this;
787
787
  }
788
- createHsColorControlClusterServer(currentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
788
+ createHsColorControlClusterServer(currentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500, startUpColorTemperatureMireds = null) {
789
789
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.HueSaturation, ColorControl.Feature.ColorTemperature), {
790
790
  colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation,
791
791
  enhancedColorMode: ColorControl.EnhancedColorMode.CurrentHueAndCurrentSaturation,
@@ -800,12 +800,12 @@ export class MatterbridgeEndpoint extends Endpoint {
800
800
  colorTempPhysicalMinMireds,
801
801
  colorTempPhysicalMaxMireds,
802
802
  coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
803
- startUpColorTemperatureMireds: null,
803
+ startUpColorTemperatureMireds,
804
804
  remainingTime: 0,
805
805
  });
806
806
  return this;
807
807
  }
808
- createCtColorControlClusterServer(colorTemperatureMireds = 250, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
808
+ createCtColorControlClusterServer(colorTemperatureMireds = 250, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500, startUpColorTemperatureMireds = null) {
809
809
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.ColorTemperature), {
810
810
  colorMode: ColorControl.ColorMode.ColorTemperatureMireds,
811
811
  enhancedColorMode: ColorControl.EnhancedColorMode.ColorTemperatureMireds,
@@ -818,7 +818,7 @@ export class MatterbridgeEndpoint extends Endpoint {
818
818
  colorTempPhysicalMinMireds,
819
819
  colorTempPhysicalMaxMireds,
820
820
  coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
821
- startUpColorTemperatureMireds: null,
821
+ startUpColorTemperatureMireds,
822
822
  remainingTime: 0,
823
823
  });
824
824
  return this;
@@ -357,6 +357,7 @@ export class PluginManager extends EventEmitter {
357
357
  author: plugin.author,
358
358
  enabled: plugin.enabled,
359
359
  private: plugin.private,
360
+ configurationVersion: plugin.configurationVersion,
360
361
  };
361
362
  }
362
363
  toApiPlugin(plugin) {
@@ -368,6 +369,7 @@ export class PluginManager extends EventEmitter {
368
369
  path: plugin.path,
369
370
  type: plugin.type,
370
371
  private: plugin.private,
372
+ configurationVersion: plugin.configurationVersion,
371
373
  latestVersion: plugin.latestVersion,
372
374
  devVersion: plugin.devVersion,
373
375
  homepage: plugin.homepage,
@@ -432,7 +434,7 @@ export class PluginManager extends EventEmitter {
432
434
  }
433
435
  const pluginsArray = await this.matterbridge.nodeContext.get('plugins', []);
434
436
  for (const plugin of pluginsArray)
435
- this._plugins.set(plugin.name, { ...plugin, private: plugin.private ?? false });
437
+ this._plugins.set(plugin.name, { ...plugin, private: plugin.private ?? false, configurationVersion: plugin.configurationVersion ?? 1 });
436
438
  this.log.debug(`Loaded ${BLUE}${pluginsArray.length}${db} plugins from storage`);
437
439
  return pluginsArray;
438
440
  }
@@ -454,6 +456,7 @@ export class PluginManager extends EventEmitter {
454
456
  enabled: plugin.enabled,
455
457
  private: plugin.private,
456
458
  tarballPath: plugin.tarballPath,
459
+ configurationVersion: plugin.configurationVersion,
457
460
  });
458
461
  }
459
462
  await this.matterbridge.nodeContext.set('plugins', plugins);
@@ -823,6 +826,7 @@ export class PluginManager extends EventEmitter {
823
826
  type: 'AnyPlatform',
824
827
  version: packageJson.version,
825
828
  private: packageJson.private || false,
829
+ configurationVersion: 1,
826
830
  description: packageJson.description,
827
831
  author: this.getAuthor(packageJson),
828
832
  homepage: this.getHomepage(packageJson),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matterbridge/core",
3
- "version": "3.10.10-dev-20260915-57739b4",
3
+ "version": "3.10.10-dev-20260917-79954cd",
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.10-dev-20260915-57739b4",
140
- "@matterbridge/thread": "3.10.10-dev-20260915-57739b4",
141
- "@matterbridge/types": "3.10.10-dev-20260915-57739b4",
142
- "@matterbridge/utils": "3.10.10-dev-20260915-57739b4",
139
+ "@matterbridge/dgram": "3.10.10-dev-20260917-79954cd",
140
+ "@matterbridge/thread": "3.10.10-dev-20260917-79954cd",
141
+ "@matterbridge/types": "3.10.10-dev-20260917-79954cd",
142
+ "@matterbridge/utils": "3.10.10-dev-20260917-79954cd",
143
143
  "escape-html": "1.0.3",
144
144
  "express": "5.2.1",
145
145
  "express-rate-limit": "8.7.0",