@matterbridge/core 3.10.10-dev-20260912-8fddbe4 → 3.10.10-dev-20260915-57739b4
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/behaviors/cameraAvStreamManagementServer.d.ts +13 -8
- package/dist/behaviors/cameraAvStreamManagementServer.js +57 -13
- package/dist/behaviors/snapshot.d.ts +1 -0
- package/dist/behaviors/snapshot.js +5 -0
- package/dist/behaviors/webRtcTransportProviderServer.js +6 -6
- package/dist/behaviors/weriftSession.d.ts +6 -3
- package/dist/behaviors/weriftSession.js +59 -28
- package/dist/chipTests.js +90 -1
- package/dist/demoDevices.js +118 -22
- package/dist/devices/camera/export.d.ts +9 -0
- package/dist/devices/camera/export.js +9 -0
- package/dist/devices/cameraController.d.ts +8 -0
- package/dist/devices/cameraController.js +35 -0
- package/dist/devices/floodlightCamera.d.ts +4 -2
- package/dist/devices/floodlightCamera.js +7 -6
- package/dist/devices/snapshotCamera.d.ts +3 -0
- package/dist/devices/snapshotCamera.js +3 -1
- package/dist/devices/videoDoorbell.d.ts +4 -2
- package/dist/devices/videoDoorbell.js +7 -6
- package/package.json +10 -6
|
@@ -3,12 +3,8 @@ import { StreamUsage } from '@matter/types';
|
|
|
3
3
|
import type { Viewport } from '@matter/types';
|
|
4
4
|
import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
|
|
5
5
|
import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
resolution: CameraAvStreamManagement.VideoResolution;
|
|
9
|
-
}
|
|
10
|
-
export declare function cameraColorTestJpegForResolution(requestedResolution: CameraAvStreamManagement.VideoResolution): CameraColorTestJpeg;
|
|
11
|
-
declare const MatterbridgeCameraAvStreamManagementServer_base: import("@matter/node").ClusterBehavior.Type<typeof CameraAvStreamManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvStreamManagement, {
|
|
6
|
+
export type SnapshotSource = 'test' | 'webcam' | 'rtsp';
|
|
7
|
+
declare const MatterbridgeCameraAvStreamManagementServerBase: import("@matter/node").ClusterBehavior.Type<typeof CameraAvStreamManagementServer, import("@matter/types").ClusterType.WithSupportedFeatures<CameraAvStreamManagement, {
|
|
12
8
|
audio: true;
|
|
13
9
|
highDynamicRange: false;
|
|
14
10
|
imageControl: true;
|
|
@@ -21,9 +17,10 @@ declare const MatterbridgeCameraAvStreamManagementServer_base: import("@matter/n
|
|
|
21
17
|
video: true;
|
|
22
18
|
watermark: false;
|
|
23
19
|
}>, import("@matter/types").ClusterType.Concrete, new () => {}, "cameraAvStreamManagement">;
|
|
24
|
-
export declare class MatterbridgeCameraAvStreamManagementServer extends
|
|
20
|
+
export declare class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCameraAvStreamManagementServerBase {
|
|
25
21
|
#private;
|
|
26
22
|
readonly endpoint: MatterbridgeEndpoint;
|
|
23
|
+
readonly state: MatterbridgeCameraAvStreamManagementServer.State;
|
|
27
24
|
initialize(): void;
|
|
28
25
|
setStreamPriorities(request: CameraAvStreamManagement.SetStreamPrioritiesRequest): void;
|
|
29
26
|
videoStreamAllocate(request: CameraAvStreamManagement.VideoStreamAllocateRequest): CameraAvStreamManagement.VideoStreamAllocateResponse;
|
|
@@ -32,7 +29,13 @@ export declare class MatterbridgeCameraAvStreamManagementServer extends Matterbr
|
|
|
32
29
|
audioStreamDeallocate(request: CameraAvStreamManagement.AudioStreamDeallocateRequest): void;
|
|
33
30
|
snapshotStreamAllocate(request: CameraAvStreamManagement.SnapshotStreamAllocateRequest): Promise<CameraAvStreamManagement.SnapshotStreamAllocateResponse>;
|
|
34
31
|
snapshotStreamDeallocate(request: CameraAvStreamManagement.SnapshotStreamDeallocateRequest): Promise<void>;
|
|
35
|
-
captureSnapshot(request: CameraAvStreamManagement.CaptureSnapshotRequest): CameraAvStreamManagement.CaptureSnapshotResponse
|
|
32
|
+
captureSnapshot(request: CameraAvStreamManagement.CaptureSnapshotRequest): Promise<CameraAvStreamManagement.CaptureSnapshotResponse>;
|
|
33
|
+
}
|
|
34
|
+
export declare namespace MatterbridgeCameraAvStreamManagementServer {
|
|
35
|
+
class State extends MatterbridgeCameraAvStreamManagementServerBase.State {
|
|
36
|
+
snapshotSource?: SnapshotSource;
|
|
37
|
+
snapshotSourceDevice?: string;
|
|
38
|
+
}
|
|
36
39
|
}
|
|
37
40
|
export interface CameraAvStreamManagementClusterOptions {
|
|
38
41
|
maxContentBufferSize: number;
|
|
@@ -76,6 +79,8 @@ export interface SnapshotCameraAvStreamManagementClusterOptions {
|
|
|
76
79
|
maxNetworkBandwidth: number;
|
|
77
80
|
supportedStreamUsages: StreamUsage[];
|
|
78
81
|
streamUsagePriorities: StreamUsage[];
|
|
82
|
+
snapshotSource?: SnapshotSource;
|
|
83
|
+
snapshotSourceDevice?: string;
|
|
79
84
|
}
|
|
80
85
|
export declare function createDefaultSnapshotCameraAvStreamManagementClusterServer(endpoint: MatterbridgeEndpoint, options: SnapshotCameraAvStreamManagementClusterOptions): MatterbridgeEndpoint;
|
|
81
86
|
export {};
|
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
1
|
import { CameraAvStreamManagementServer } from '@matter/node/behaviors/camera-av-stream-management';
|
|
2
|
+
import { WebRtcTransportProviderServer } from '@matter/node/behaviors/web-rtc-transport-provider';
|
|
3
3
|
import { Status, StatusResponseError, StreamUsage, ThreeLevelAuto } from '@matter/types';
|
|
4
4
|
import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
|
|
5
5
|
import { MatterbridgeServer } from './matterbridgeServer.js';
|
|
6
|
-
|
|
6
|
+
import { captureSnapshot, TEST_SNAPSHOT_SOURCE } from './snapshot.js';
|
|
7
7
|
const AUDIO_STREAM_BIT_DEPTHS = [8, 16, 24, 32];
|
|
8
8
|
function numericEnumValues(enumObject) {
|
|
9
9
|
return Object.values(enumObject).filter((value) => typeof value === 'number');
|
|
10
10
|
}
|
|
11
11
|
const AUDIO_CODECS = numericEnumValues(CameraAvStreamManagement.AudioCodec);
|
|
12
12
|
const VIDEO_CODECS = numericEnumValues(CameraAvStreamManagement.VideoCodec);
|
|
13
|
-
|
|
14
|
-
'
|
|
15
|
-
'1280x720': { data: readFileSync(new URL('../../assets/camera-color-test-1280-720.jpeg', import.meta.url)), resolution: { width: 1280, height: 720 } },
|
|
16
|
-
'1920x1080': { data: readFileSync(new URL('../../assets/camera-color-test-1920-1080.jpeg', import.meta.url)), resolution: { width: 1920, height: 1080 } },
|
|
17
|
-
};
|
|
18
|
-
export function cameraColorTestJpegForResolution(requestedResolution) {
|
|
19
|
-
return cameraColorTestJpegs[`${requestedResolution.width}x${requestedResolution.height}`] ?? cameraColorTestJpegs[DEFAULT_CAMERA_COLOR_TEST_RESOLUTION];
|
|
13
|
+
function hasWeriftOfferOptions(state) {
|
|
14
|
+
return 'weriftOfferOptions' in state && typeof state.weriftOfferOptions === 'object' && state.weriftOfferOptions !== null;
|
|
20
15
|
}
|
|
21
|
-
|
|
16
|
+
const MatterbridgeCameraAvStreamManagementServerBase = CameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl);
|
|
17
|
+
export class MatterbridgeCameraAvStreamManagementServer extends MatterbridgeCameraAvStreamManagementServerBase {
|
|
22
18
|
#isAutoAllocateSkipped() {
|
|
23
19
|
return process.env.MATTERBRIDGE_SKIP_AUTO_ALLOCATE_CAMERA_AV_STREAM_MANAGEMENT === '1';
|
|
24
20
|
}
|
|
@@ -328,7 +324,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
|
|
|
328
324
|
device.log.info(`MatterbridgeCameraAvStreamManagementServer: deallocated snapshot stream ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
|
|
329
325
|
this.endpoint.emitCommand(CameraAvStreamManagement, 'snapshotStreamDeallocate', request, this.context);
|
|
330
326
|
}
|
|
331
|
-
|
|
327
|
+
#resolveSnapshotSource() {
|
|
328
|
+
const endpoint = `(endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
|
|
329
|
+
const fail = (reason) => {
|
|
330
|
+
this.endpoint.stateOf(MatterbridgeServer).log.error(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`);
|
|
331
|
+
return new StatusResponseError(`MatterbridgeCameraAvStreamManagementServer: cannot capture snapshot: ${reason} ${endpoint}`, Status.Failure);
|
|
332
|
+
};
|
|
333
|
+
const sourceFor = (source, device, sourceOption, deviceOption) => {
|
|
334
|
+
if (source === 'test')
|
|
335
|
+
return TEST_SNAPSHOT_SOURCE;
|
|
336
|
+
if (device)
|
|
337
|
+
return device;
|
|
338
|
+
throw fail(`${sourceOption}=${source} requires ${deviceOption} to be set`);
|
|
339
|
+
};
|
|
340
|
+
const { snapshotSource, snapshotSourceDevice } = this.state;
|
|
341
|
+
if (snapshotSource !== undefined)
|
|
342
|
+
return sourceFor(snapshotSource, snapshotSourceDevice, 'snapshotSource', 'snapshotSourceDevice');
|
|
343
|
+
if (!this.endpoint.behaviors.has(WebRtcTransportProviderServer)) {
|
|
344
|
+
throw fail('no snapshotSource is configured and the endpoint has no WebRtcTransportProvider cluster to read the video source from');
|
|
345
|
+
}
|
|
346
|
+
const state = this.endpoint.stateOf(WebRtcTransportProviderServer);
|
|
347
|
+
if (!hasWeriftOfferOptions(state))
|
|
348
|
+
throw fail('the WebRtcTransportProvider cluster state has no weriftOfferOptions');
|
|
349
|
+
const { videoSource, videoSourceDevice } = state.weriftOfferOptions;
|
|
350
|
+
if (videoSource === 'none')
|
|
351
|
+
throw fail(`weriftOfferOptions.videoSource=${videoSource} provides no video source`);
|
|
352
|
+
return sourceFor(videoSource, videoSourceDevice, 'weriftOfferOptions.videoSource', 'videoSourceDevice');
|
|
353
|
+
}
|
|
354
|
+
async captureSnapshot(request) {
|
|
332
355
|
const device = this.endpoint.stateOf(MatterbridgeServer);
|
|
333
356
|
const { snapshotStreamId } = request;
|
|
334
357
|
const stream = this.state.allocatedSnapshotStreams.find((s) => snapshotStreamId === null || s.snapshotStreamId === snapshotStreamId);
|
|
@@ -337,15 +360,34 @@ export class MatterbridgeCameraAvStreamManagementServer extends CameraAvStreamMa
|
|
|
337
360
|
}
|
|
338
361
|
device.log.info(`MatterbridgeCameraAvStreamManagementServer: capturing snapshot ${snapshotStreamId ?? 'auto'} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
|
|
339
362
|
device.log.debug(`MatterbridgeCameraAvStreamManagementServer: captureSnapshot called with snapshotStreamId ${request.snapshotStreamId} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
|
|
340
|
-
const
|
|
363
|
+
const src = this.#resolveSnapshotSource();
|
|
364
|
+
const { width, height } = request.requestedResolution;
|
|
365
|
+
let captured;
|
|
366
|
+
try {
|
|
367
|
+
captured = await captureSnapshot({ src, width, height });
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
const message = `MatterbridgeCameraAvStreamManagementServer: snapshot capture failed: ${error instanceof Error ? error.message : String(error)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`;
|
|
371
|
+
device.log.error(message);
|
|
372
|
+
throw new StatusResponseError(message, Status.Failure);
|
|
373
|
+
}
|
|
374
|
+
const resolution = { width: captured.width, height: captured.height === -1 ? Math.round((captured.width * height) / width) : captured.height };
|
|
375
|
+
device.log.info(`MatterbridgeCameraAvStreamManagementServer: captured snapshot ${captured.byteLength}B at ${resolution.width}x${resolution.height}, quality ${captured.quality}${captured.downgraded ? ' (downgraded)' : ''} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
|
|
341
376
|
this.endpoint.emitCommand(CameraAvStreamManagement, 'captureSnapshot', request, this.context);
|
|
342
377
|
return {
|
|
343
|
-
data,
|
|
378
|
+
data: captured.data,
|
|
344
379
|
imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg,
|
|
345
380
|
resolution,
|
|
346
381
|
};
|
|
347
382
|
}
|
|
348
383
|
}
|
|
384
|
+
(function (MatterbridgeCameraAvStreamManagementServer) {
|
|
385
|
+
class State extends MatterbridgeCameraAvStreamManagementServerBase.State {
|
|
386
|
+
snapshotSource = undefined;
|
|
387
|
+
snapshotSourceDevice = undefined;
|
|
388
|
+
}
|
|
389
|
+
MatterbridgeCameraAvStreamManagementServer.State = State;
|
|
390
|
+
})(MatterbridgeCameraAvStreamManagementServer || (MatterbridgeCameraAvStreamManagementServer = {}));
|
|
349
391
|
export function createDefaultCameraAvStreamManagementClusterServer(endpoint, options) {
|
|
350
392
|
endpoint.behaviors.require(MatterbridgeCameraAvStreamManagementServer.with(CameraAvStreamManagement.Feature.Video, CameraAvStreamManagement.Feature.Audio, CameraAvStreamManagement.Feature.Snapshot, CameraAvStreamManagement.Feature.ImageControl), {
|
|
351
393
|
...options,
|
|
@@ -408,6 +450,8 @@ export function createDefaultSnapshotCameraAvStreamManagementClusterServer(endpo
|
|
|
408
450
|
maxEncodedPixelRate: options.maxEncodedPixelRate,
|
|
409
451
|
snapshotCapabilities: options.snapshotCapabilities,
|
|
410
452
|
allocatedSnapshotStreams: [],
|
|
453
|
+
snapshotSource: options.snapshotSource,
|
|
454
|
+
snapshotSourceDevice: options.snapshotSourceDevice,
|
|
411
455
|
});
|
|
412
456
|
return endpoint;
|
|
413
457
|
}
|
|
@@ -7,6 +7,7 @@ const MAX_BYTES = 64000;
|
|
|
7
7
|
const QUALITIES = [6, 10, 14, 18, 24, 31];
|
|
8
8
|
const FALLBACK_WIDTH = 640;
|
|
9
9
|
const WEBCAM_WARMUP_SECONDS = 1;
|
|
10
|
+
export const TEST_SNAPSHOT_SOURCE = 'test';
|
|
10
11
|
const CAPTURE_TIMEOUT_MS = 10_000;
|
|
11
12
|
const TERMINATION_GRACE_MS = 1_000;
|
|
12
13
|
const log = new AnsiLogger({ logName: 'Snapshot', logLevel: "debug", logNameColor: MAGENTA, logTimestampFormat: 4 });
|
|
@@ -56,6 +57,10 @@ export async function captureSnapshot(request) {
|
|
|
56
57
|
throw new Error(`Snapshot capture from ${redactedSrc} exceeds ${maxBytes}B at minimum quality/resolution (${last.length}B at quality ${lastQuality})`);
|
|
57
58
|
}
|
|
58
59
|
function buildInputArgs(src, width, height) {
|
|
60
|
+
if (src === TEST_SNAPSHOT_SOURCE) {
|
|
61
|
+
const size = height === -1 ? '' : `=size=${width}x${height}`;
|
|
62
|
+
return ['-f', 'lavfi', '-i', `testsrc${size}`];
|
|
63
|
+
}
|
|
59
64
|
if (src.startsWith('rtsp://')) {
|
|
60
65
|
return ['-rtsp_transport', 'tcp', '-i', src];
|
|
61
66
|
}
|
|
@@ -255,8 +255,8 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
|
|
|
255
255
|
const webRtcPeer = new WeriftWebRtcSession(webRtcSessionId, this.state.weriftOfferOptions);
|
|
256
256
|
this.internal.sessions.set(webRtcSessionId, webRtcPeer);
|
|
257
257
|
const sdp = await webRtcPeer.createOffer({
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
offerVideo: !!videoStreams?.length,
|
|
259
|
+
offerAudio: !!audioStreams?.length,
|
|
260
260
|
videoResolution: this.state.weriftOfferOptions.videoResolution ?? this.#resolveVideoResolution(videoStreams),
|
|
261
261
|
});
|
|
262
262
|
if (requestorEndpoint) {
|
|
@@ -324,8 +324,8 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
|
|
|
324
324
|
const webRtcPeer = this.internal.sessions.get(webRtcSessionId) ?? new WeriftWebRtcSession(webRtcSessionId, this.state.weriftOfferOptions);
|
|
325
325
|
this.internal.sessions.set(webRtcSessionId, webRtcPeer);
|
|
326
326
|
const sdp = await webRtcPeer.createAnswer(request.sdp, {
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
offerVideo: !!session.videoStreams?.length,
|
|
328
|
+
offerAudio: !!session.audioStreams?.length,
|
|
329
329
|
videoResolution: this.state.weriftOfferOptions.videoResolution ?? this.#resolveVideoResolution(session.videoStreams),
|
|
330
330
|
});
|
|
331
331
|
const requestorEndpoint = await this.#resolvePeerRequestorEndpoint(session.peerNodeId, session.fabricIndex, session.peerEndpointId);
|
|
@@ -407,7 +407,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
|
|
|
407
407
|
}
|
|
408
408
|
(function (MatterbridgeWebRtcTransportProviderServer) {
|
|
409
409
|
class State extends WebRtcTransportProviderServer.State {
|
|
410
|
-
weriftOfferOptions = {
|
|
410
|
+
weriftOfferOptions = { offerVideo: true, offerAudio: true, videoSource: 'none', audioSource: 'none' };
|
|
411
411
|
}
|
|
412
412
|
MatterbridgeWebRtcTransportProviderServer.State = State;
|
|
413
413
|
class Internal {
|
|
@@ -416,7 +416,7 @@ export class MatterbridgeWebRtcTransportProviderServer extends WebRtcTransportPr
|
|
|
416
416
|
}
|
|
417
417
|
MatterbridgeWebRtcTransportProviderServer.Internal = Internal;
|
|
418
418
|
})(MatterbridgeWebRtcTransportProviderServer || (MatterbridgeWebRtcTransportProviderServer = {}));
|
|
419
|
-
export function createDefaultWebRtcTransportProviderClusterServer(endpoint, weriftOfferOptions = {
|
|
419
|
+
export function createDefaultWebRtcTransportProviderClusterServer(endpoint, weriftOfferOptions = { offerVideo: true, offerAudio: true, videoSource: 'none', audioSource: 'none' }) {
|
|
420
420
|
endpoint.behaviors.require(MatterbridgeWebRtcTransportProviderServer, { currentSessions: [], weriftOfferOptions });
|
|
421
421
|
return endpoint;
|
|
422
422
|
}
|
|
@@ -2,8 +2,8 @@ import { RTCPeerConnection } from 'werift';
|
|
|
2
2
|
type VideoSource = 'none' | 'test' | 'webcam' | 'rtsp';
|
|
3
3
|
type AudioSource = 'none' | 'test' | 'microphone' | 'rtsp';
|
|
4
4
|
export interface WeriftOfferOptions {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
offerVideo: boolean;
|
|
6
|
+
offerAudio: boolean;
|
|
7
7
|
videoSource: VideoSource;
|
|
8
8
|
videoSourceDevice?: string;
|
|
9
9
|
videoResolution?: string;
|
|
@@ -29,6 +29,8 @@ export declare class WeriftWebRtcSession {
|
|
|
29
29
|
private static readonly DIAGNOSTICS_INTERVAL_MS;
|
|
30
30
|
private lastDiagnosticsCounters?;
|
|
31
31
|
private readonly dtlsTransportsWithStateLogging;
|
|
32
|
+
private static parseIcePortRange;
|
|
33
|
+
private static parseIceAdditionalHostAddresses;
|
|
32
34
|
constructor(webRtcSessionId: number, options: WeriftOfferOptions);
|
|
33
35
|
private summarizeSdp;
|
|
34
36
|
private getFreeUdpPort;
|
|
@@ -40,10 +42,11 @@ export declare class WeriftWebRtcSession {
|
|
|
40
42
|
private getConfiguredVideoResolution;
|
|
41
43
|
private getConfiguredVideoSource;
|
|
42
44
|
private buildFfmpegVideoInputArgs;
|
|
45
|
+
private logFfmpegStderr;
|
|
43
46
|
private generateVideoTrack;
|
|
44
47
|
private preferVideoCodecOnTransceivers;
|
|
45
48
|
private preferAudioCodecOnTransceivers;
|
|
46
|
-
private static readonly
|
|
49
|
+
private static readonly TEST_TONE_INPUT;
|
|
47
50
|
private getConfiguredAudioSource;
|
|
48
51
|
private buildFfmpegAudioInputArgs;
|
|
49
52
|
private generateAudioTrack;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createSocket } from 'node:dgram';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
2
|
import { fireAndForget, getErrorMessage } from '@matterbridge/utils';
|
|
4
3
|
import { AnsiLogger, MAGENTA } from 'node-ansi-logger';
|
|
5
4
|
import { RTCPeerConnection, RTCRtpCodecParameters, useH264, useOPUS, usePCMU, useVP8 } from 'werift';
|
|
@@ -22,11 +21,35 @@ export class WeriftWebRtcSession {
|
|
|
22
21
|
static DIAGNOSTICS_INTERVAL_MS = 10_000;
|
|
23
22
|
lastDiagnosticsCounters;
|
|
24
23
|
dtlsTransportsWithStateLogging = new Set();
|
|
24
|
+
static parseIcePortRange() {
|
|
25
|
+
const raw = process.env.MATTERBRIDGE_ICE_PORT_RANGE?.trim();
|
|
26
|
+
if (!raw)
|
|
27
|
+
return undefined;
|
|
28
|
+
const match = /^(\d{1,5})-(\d{1,5})$/.exec(raw);
|
|
29
|
+
if (!match)
|
|
30
|
+
return undefined;
|
|
31
|
+
const min = Number(match[1]);
|
|
32
|
+
const max = Number(match[2]);
|
|
33
|
+
if (min < 1024 || max > 65_535 || min >= max)
|
|
34
|
+
return undefined;
|
|
35
|
+
return [min, max];
|
|
36
|
+
}
|
|
37
|
+
static parseIceAdditionalHostAddresses() {
|
|
38
|
+
const addresses = process.env.MATTERBRIDGE_ICE_HOST_ADDRESSES?.split(',')
|
|
39
|
+
.map((address) => address.trim())
|
|
40
|
+
.filter((address) => address.length > 0) ?? [];
|
|
41
|
+
return addresses.length > 0 ? addresses : undefined;
|
|
42
|
+
}
|
|
25
43
|
constructor(webRtcSessionId, options) {
|
|
26
44
|
this.webRtcSessionId = webRtcSessionId;
|
|
27
45
|
this.options = { ...options };
|
|
28
|
-
|
|
46
|
+
const icePortRange = WeriftWebRtcSession.parseIcePortRange();
|
|
47
|
+
const iceAdditionalHostAddresses = WeriftWebRtcSession.parseIceAdditionalHostAddresses();
|
|
48
|
+
this.peerConnection = new RTCPeerConnection({ codecs: { audio: [useOPUS(), usePCMU()], video: [useH264(), useVP8()] }, icePortRange, iceAdditionalHostAddresses });
|
|
29
49
|
this.log = new AnsiLogger({ logName: `WebRTC session ${webRtcSessionId}`, logLevel: "debug", logNameColor: MAGENTA, logTimestampFormat: 4 });
|
|
50
|
+
if (icePortRange || iceAdditionalHostAddresses) {
|
|
51
|
+
this.log.debug(`ICE overrides: portRange=${icePortRange ? `${icePortRange[0]}-${icePortRange[1]}` : 'default'} additionalHostAddresses=${iceAdditionalHostAddresses?.join(', ') ?? 'none'}`);
|
|
52
|
+
}
|
|
30
53
|
this.peerConnection.iceGatheringStateChange.subscribe((state) => {
|
|
31
54
|
this.log.info(`ICE gathering state: ${state}`);
|
|
32
55
|
});
|
|
@@ -154,19 +177,19 @@ export class WeriftWebRtcSession {
|
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
buildFfmpegVideoInputArgs(videoSource) {
|
|
157
|
-
const testPatternInput = {
|
|
158
|
-
args: ['-re', '-f', 'lavfi', '-i', 'testsrc=size=640x480:rate=10'],
|
|
159
|
-
description: 'synthetic moving test pattern',
|
|
160
|
-
bitrateKbps: WeriftWebRtcSession.DEFAULT_BITRATE_KBPS,
|
|
161
|
-
};
|
|
162
180
|
if (videoSource === 'test') {
|
|
181
|
+
const testPatternInput = {
|
|
182
|
+
args: ['-re', '-f', 'lavfi', '-i', 'testsrc=size=640x480:rate=10'],
|
|
183
|
+
description: 'synthetic moving test pattern',
|
|
184
|
+
bitrateKbps: WeriftWebRtcSession.DEFAULT_BITRATE_KBPS,
|
|
185
|
+
};
|
|
163
186
|
this.log.debug(`Test pattern params: resolution=640x480, description="${testPatternInput.description}", bitrateKbps=${testPatternInput.bitrateKbps}`);
|
|
164
187
|
return testPatternInput;
|
|
165
188
|
}
|
|
166
189
|
const device = this.options.videoSourceDevice;
|
|
167
190
|
if (!device) {
|
|
168
|
-
this.log.
|
|
169
|
-
return
|
|
191
|
+
this.log.error(`options.videoSource=${videoSource} requires options.videoSourceDevice to be set; not injecting a video track`);
|
|
192
|
+
return undefined;
|
|
170
193
|
}
|
|
171
194
|
const resolution = this.getConfiguredVideoResolution();
|
|
172
195
|
const bitrateKbps = this.getConfiguredVideoBitrate();
|
|
@@ -185,10 +208,17 @@ export class WeriftWebRtcSession {
|
|
|
185
208
|
case 'win32':
|
|
186
209
|
return { args: ['-f', 'dshow', '-video_size', resolution, '-framerate', '30', '-i', `video=${device}`], description, bitrateKbps };
|
|
187
210
|
default:
|
|
188
|
-
this.log.
|
|
189
|
-
return
|
|
211
|
+
this.log.error(`Webcam capture via ffmpeg is not supported on platform "${process.platform}"; not injecting a video track`);
|
|
212
|
+
return undefined;
|
|
190
213
|
}
|
|
191
214
|
}
|
|
215
|
+
logFfmpegStderr(generator, kind) {
|
|
216
|
+
generator.stderr?.on('data', (chunk) => {
|
|
217
|
+
const message = chunk.toString().trim();
|
|
218
|
+
if (message)
|
|
219
|
+
this.log.debug(`Ffmpeg ${kind} generator: ${message}`);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
192
222
|
async generateVideoTrack(codec) {
|
|
193
223
|
if (this.testVideoAttached)
|
|
194
224
|
return;
|
|
@@ -198,6 +228,8 @@ export class WeriftWebRtcSession {
|
|
|
198
228
|
return;
|
|
199
229
|
}
|
|
200
230
|
const videoInput = this.buildFfmpegVideoInputArgs(videoSource);
|
|
231
|
+
if (!videoInput)
|
|
232
|
+
return;
|
|
201
233
|
this.log.debug(`Attempting to attach ${videoInput.description} video track at ${videoInput.bitrateKbps}kbps`);
|
|
202
234
|
if (!hasFfmpeg()) {
|
|
203
235
|
this.log.warn('Cannot inject video stream: missing dependency ffmpeg');
|
|
@@ -230,6 +262,7 @@ export class WeriftWebRtcSession {
|
|
|
230
262
|
`rtp://127.0.0.1:${udpPort}`,
|
|
231
263
|
];
|
|
232
264
|
const generator = runFfmpeg(ffmpegArgs);
|
|
265
|
+
this.logFfmpegStderr(generator, 'video');
|
|
233
266
|
generator.once('error', (error) => {
|
|
234
267
|
this.log.warn(`Video generator failed: ${getErrorMessage(error)}`);
|
|
235
268
|
});
|
|
@@ -272,7 +305,7 @@ export class WeriftWebRtcSession {
|
|
|
272
305
|
this.log.debug(`Preferred ${mimeType.toUpperCase()} codecs on ${adjustedTransceivers} audio transceiver(s)`);
|
|
273
306
|
}
|
|
274
307
|
}
|
|
275
|
-
static
|
|
308
|
+
static TEST_TONE_INPUT = 'sine=frequency=440:beep_factor=4:sample_rate=48000,volume=6dB';
|
|
276
309
|
getConfiguredAudioSource() {
|
|
277
310
|
const source = this.options.audioSource?.trim().toLowerCase() ?? 'none';
|
|
278
311
|
switch (source) {
|
|
@@ -287,17 +320,13 @@ export class WeriftWebRtcSession {
|
|
|
287
320
|
}
|
|
288
321
|
}
|
|
289
322
|
buildFfmpegAudioInputArgs(audioSource) {
|
|
290
|
-
|
|
291
|
-
args: ['-re', '-
|
|
292
|
-
|
|
293
|
-
volumeFilter: 'volume=6dB',
|
|
294
|
-
};
|
|
295
|
-
if (audioSource === 'test')
|
|
296
|
-
return testVoiceInput;
|
|
323
|
+
if (audioSource === 'test') {
|
|
324
|
+
return { args: ['-re', '-f', 'lavfi', '-i', WeriftWebRtcSession.TEST_TONE_INPUT], description: 'synthetic test tone' };
|
|
325
|
+
}
|
|
297
326
|
const device = this.options.audioSourceDevice;
|
|
298
327
|
if (!device) {
|
|
299
|
-
this.log.
|
|
300
|
-
return
|
|
328
|
+
this.log.error(`options.audioSource=${audioSource} requires options.audioSourceDevice to be set; not injecting an audio track`);
|
|
329
|
+
return undefined;
|
|
301
330
|
}
|
|
302
331
|
if (audioSource === 'rtsp') {
|
|
303
332
|
const description = `RTSP camera audio (${device})`;
|
|
@@ -314,8 +343,8 @@ export class WeriftWebRtcSession {
|
|
|
314
343
|
case 'win32':
|
|
315
344
|
return { args: ['-f', 'dshow', '-i', `audio=${device}`], description };
|
|
316
345
|
default:
|
|
317
|
-
this.log.
|
|
318
|
-
return
|
|
346
|
+
this.log.error(`Microphone capture via ffmpeg is not supported on platform "${process.platform}"; not injecting an audio track`);
|
|
347
|
+
return undefined;
|
|
319
348
|
}
|
|
320
349
|
}
|
|
321
350
|
async generateAudioTrack(codec) {
|
|
@@ -327,6 +356,8 @@ export class WeriftWebRtcSession {
|
|
|
327
356
|
return;
|
|
328
357
|
}
|
|
329
358
|
const audioInput = this.buildFfmpegAudioInputArgs(audioSource);
|
|
359
|
+
if (!audioInput)
|
|
360
|
+
return;
|
|
330
361
|
this.log.debug(`Attempting to attach ${audioInput.description} audio track`);
|
|
331
362
|
if (!hasFfmpeg()) {
|
|
332
363
|
this.log.warn('Cannot inject audio stream: missing dependency ffmpeg');
|
|
@@ -349,13 +380,12 @@ export class WeriftWebRtcSession {
|
|
|
349
380
|
'error',
|
|
350
381
|
...audioInput.args,
|
|
351
382
|
'-vn',
|
|
352
|
-
...(audioInput.volumeFilter ? ['-af', audioInput.volumeFilter] : []),
|
|
353
383
|
'-c:a',
|
|
354
384
|
'libopus',
|
|
355
385
|
'-b:a',
|
|
356
386
|
'32k',
|
|
357
387
|
'-ac',
|
|
358
|
-
|
|
388
|
+
'1',
|
|
359
389
|
'-ar',
|
|
360
390
|
String(clockRate),
|
|
361
391
|
'-f',
|
|
@@ -365,6 +395,7 @@ export class WeriftWebRtcSession {
|
|
|
365
395
|
`rtp://127.0.0.1:${udpPort}`,
|
|
366
396
|
];
|
|
367
397
|
const generator = runFfmpeg(ffmpegArgs);
|
|
398
|
+
this.logFfmpegStderr(generator, 'audio');
|
|
368
399
|
generator.once('error', (error) => {
|
|
369
400
|
this.log.warn(`Audio generator failed: ${getErrorMessage(error)}`);
|
|
370
401
|
});
|
|
@@ -403,8 +434,8 @@ export class WeriftWebRtcSession {
|
|
|
403
434
|
async createOffer(overrides) {
|
|
404
435
|
this.applyOptionOverrides(overrides);
|
|
405
436
|
const options = this.options;
|
|
406
|
-
this.log.debug(`CreateOffer requested (
|
|
407
|
-
if (options.
|
|
437
|
+
this.log.debug(`CreateOffer requested (offerVideo=${options.offerVideo}, offerAudio=${options.offerAudio}, videoResolution=${options.videoResolution ?? 'undefined'})`);
|
|
438
|
+
if (options.offerVideo) {
|
|
408
439
|
const preferredCodec = this.getPreferredInjectableVideoCodec();
|
|
409
440
|
if (preferredCodec) {
|
|
410
441
|
this.preferVideoCodecOnTransceivers(preferredCodec.mimeType.toLowerCase());
|
|
@@ -416,7 +447,7 @@ export class WeriftWebRtcSession {
|
|
|
416
447
|
if (!this.testVideoAttached)
|
|
417
448
|
this.peerConnection.addTransceiver('video', { direction: 'sendonly' });
|
|
418
449
|
}
|
|
419
|
-
if (options.
|
|
450
|
+
if (options.offerAudio)
|
|
420
451
|
this.peerConnection.addTransceiver('audio', { direction: 'sendonly' });
|
|
421
452
|
const offer = await this.peerConnection.createOffer();
|
|
422
453
|
await this.peerConnection.setLocalDescription(offer);
|
package/dist/chipTests.js
CHANGED
|
@@ -21,11 +21,14 @@ import { RefrigeratorAlarm } from '@matter/types/clusters/refrigerator-alarm';
|
|
|
21
21
|
import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
|
|
22
22
|
import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
|
|
23
23
|
import { SmokeCoAlarm } from '@matter/types/clusters/smoke-co-alarm';
|
|
24
|
+
import { Switch } from '@matter/types/clusters/switch';
|
|
24
25
|
import { TariffPriceType, TariffUnit } from '@matter/types/globals';
|
|
26
|
+
import { wait } from '@matterbridge/utils/wait';
|
|
25
27
|
import { MatterbridgeOccupancySensingServer } from './behaviors/occupancySensingServer.js';
|
|
26
28
|
import { cliEmitter } from './cliEmitter.js';
|
|
27
29
|
import { MatterbridgeRvcOperationalStateServer, MatterbridgeRvcRunModeServer } from './devices/roboticVacuumCleaner.js';
|
|
28
30
|
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
|
|
31
|
+
import { featuresFor } from './matterbridgeEndpointHelpers.js';
|
|
29
32
|
const chipTestAppPipePath = '/tmp/matterbridge-chip-test-app-pipe';
|
|
30
33
|
const chipTestValidEventTrigger = 0xfffffffffff10000n;
|
|
31
34
|
const smokeCoAlarmWarningSmokeAlarmTrigger = 0x005c000000000090n;
|
|
@@ -1097,10 +1100,87 @@ async function handleChipTestAppPipeCommand(matterbridge, command) {
|
|
|
1097
1100
|
}
|
|
1098
1101
|
matterbridge.log.info(`CHIP test app pipe OperationalStateChange(${command.Operation}${command.Param === undefined ? '' : `, ${command.Param}`}) applied on endpoint ${endpointId}`);
|
|
1099
1102
|
return;
|
|
1103
|
+
case 'SimulateSwitchIdle':
|
|
1104
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
|
|
1105
|
+
matterbridge.log.info(`CHIP test app pipe set Switch.CurrentPosition to 0 (idle) on endpoint ${endpointId}`);
|
|
1106
|
+
return;
|
|
1107
|
+
case 'SimulateLongPress':
|
|
1108
|
+
await simulateChipTestSwitchLongPress(matterbridge, endpoint, endpointId, command);
|
|
1109
|
+
return;
|
|
1110
|
+
case 'SimulateLatchPosition': {
|
|
1111
|
+
const newPosition = command.PositionId ?? 0;
|
|
1112
|
+
if (endpoint.getAttribute(Switch.id, 'currentPosition', matterbridge.log) === newPosition) {
|
|
1113
|
+
matterbridge.log.info(`CHIP test app pipe left Switch latched at position ${newPosition} on endpoint ${endpointId}`);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
|
|
1117
|
+
await endpoint.triggerEvent(Switch.id, 'switchLatched', { newPosition }, matterbridge.log);
|
|
1118
|
+
matterbridge.log.info(`CHIP test app pipe latched Switch to position ${newPosition} on endpoint ${endpointId}`);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
case 'SimulateMultiPress':
|
|
1122
|
+
await simulateChipTestSwitchMultiPress(matterbridge, endpoint, endpointId, command);
|
|
1123
|
+
return;
|
|
1100
1124
|
default:
|
|
1101
1125
|
matterbridge.log.warn(`Ignoring unsupported CHIP test app pipe command: ${JSON.stringify(command)}`);
|
|
1102
1126
|
}
|
|
1103
1127
|
}
|
|
1128
|
+
async function simulateChipTestSwitchLongPress(matterbridge, endpoint, endpointId, command) {
|
|
1129
|
+
if (!endpoint.hasClusterServer(Switch.id)) {
|
|
1130
|
+
matterbridge.log.warn(`Ignoring SimulateLongPress CHIP test app pipe command on endpoint ${endpointId} without a Switch cluster server`);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const features = featuresFor(endpoint, Switch.id);
|
|
1134
|
+
const newPosition = command.ButtonId ?? 1;
|
|
1135
|
+
const delayMs = command.LongPressDelayMillis ?? 0;
|
|
1136
|
+
const durationMs = command.LongPressDurationMillis ?? 0;
|
|
1137
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
|
|
1138
|
+
await endpoint.triggerEvent(Switch.id, 'initialPress', { newPosition }, matterbridge.log);
|
|
1139
|
+
matterbridge.log.info(`CHIP test app pipe pressed Switch position ${newPosition} on endpoint ${endpointId} for ${durationMs}ms`);
|
|
1140
|
+
if (features.momentarySwitchLongPress) {
|
|
1141
|
+
await wait(Math.min(delayMs, durationMs));
|
|
1142
|
+
await endpoint.triggerEvent(Switch.id, 'longPress', { newPosition }, matterbridge.log);
|
|
1143
|
+
await wait(Math.max(durationMs - delayMs, 0));
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
await wait(durationMs);
|
|
1147
|
+
}
|
|
1148
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
|
|
1149
|
+
if (features.momentarySwitchLongPress) {
|
|
1150
|
+
await endpoint.triggerEvent(Switch.id, 'longRelease', { previousPosition: newPosition }, matterbridge.log);
|
|
1151
|
+
}
|
|
1152
|
+
else if (features.momentarySwitchRelease) {
|
|
1153
|
+
await endpoint.triggerEvent(Switch.id, 'shortRelease', { previousPosition: newPosition }, matterbridge.log);
|
|
1154
|
+
}
|
|
1155
|
+
matterbridge.log.info(`CHIP test app pipe released Switch position ${newPosition} on endpoint ${endpointId}`);
|
|
1156
|
+
}
|
|
1157
|
+
async function simulateChipTestSwitchMultiPress(matterbridge, endpoint, endpointId, command) {
|
|
1158
|
+
if (!endpoint.hasClusterServer(Switch.id)) {
|
|
1159
|
+
matterbridge.log.warn(`Ignoring SimulateMultiPress CHIP test app pipe command on endpoint ${endpointId} without a Switch cluster server`);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const features = featuresFor(endpoint, Switch.id);
|
|
1163
|
+
const newPosition = command.ButtonId ?? 1;
|
|
1164
|
+
const presses = Math.max(command.MultiPressNumPresses ?? 1, 1);
|
|
1165
|
+
const pressedMs = command.MultiPressPressedTimeMillis ?? 0;
|
|
1166
|
+
const releasedMs = command.MultiPressReleasedTimeMillis ?? 0;
|
|
1167
|
+
const multiPressMax = endpoint.getAttribute(Switch.id, 'multiPressMax', matterbridge.log) ?? command.MultiPressMax ?? presses;
|
|
1168
|
+
for (let press = 1; press <= presses; press++) {
|
|
1169
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', newPosition, matterbridge.log);
|
|
1170
|
+
await endpoint.triggerEvent(Switch.id, 'initialPress', { newPosition }, matterbridge.log);
|
|
1171
|
+
if (press > 1) {
|
|
1172
|
+
await endpoint.triggerEvent(Switch.id, 'multiPressOngoing', { newPosition, currentNumberOfPressesCounted: Math.min(press, multiPressMax) }, matterbridge.log);
|
|
1173
|
+
}
|
|
1174
|
+
await wait(pressedMs);
|
|
1175
|
+
await endpoint.setAttribute(Switch.id, 'currentPosition', 0, matterbridge.log);
|
|
1176
|
+
if (features.momentarySwitchRelease) {
|
|
1177
|
+
await endpoint.triggerEvent(Switch.id, 'shortRelease', { previousPosition: newPosition }, matterbridge.log);
|
|
1178
|
+
}
|
|
1179
|
+
await wait(releasedMs);
|
|
1180
|
+
}
|
|
1181
|
+
await endpoint.triggerEvent(Switch.id, 'multiPressComplete', { previousPosition: newPosition, totalNumberOfPressesCounted: Math.min(presses, multiPressMax) }, matterbridge.log);
|
|
1182
|
+
matterbridge.log.info(`CHIP test app pipe completed ${presses} press(es) of Switch position ${newPosition} on endpoint ${endpointId}`);
|
|
1183
|
+
}
|
|
1104
1184
|
function getChipTestEndpoint(matterbridge, endpointId) {
|
|
1105
1185
|
const aggregatorEndpoint = matterbridge.aggregatorNode;
|
|
1106
1186
|
if (!aggregatorEndpoint)
|
|
@@ -1127,5 +1207,14 @@ function isChipTestAppPipeCommand(value) {
|
|
|
1127
1207
|
(!('SoilMoistureValue' in value) || typeof value.SoilMoistureValue === 'number') &&
|
|
1128
1208
|
(!('Device' in value) || typeof value.Device === 'string') &&
|
|
1129
1209
|
(!('Operation' in value) || typeof value.Operation === 'string') &&
|
|
1130
|
-
(!('Param' in value) || typeof value.Param === 'number')
|
|
1210
|
+
(!('Param' in value) || typeof value.Param === 'number') &&
|
|
1211
|
+
(!('ButtonId' in value) || typeof value.ButtonId === 'number') &&
|
|
1212
|
+
(!('LongPressDelayMillis' in value) || typeof value.LongPressDelayMillis === 'number') &&
|
|
1213
|
+
(!('LongPressDurationMillis' in value) || typeof value.LongPressDurationMillis === 'number') &&
|
|
1214
|
+
(!('FeatureMap' in value) || typeof value.FeatureMap === 'number') &&
|
|
1215
|
+
(!('PositionId' in value) || typeof value.PositionId === 'number') &&
|
|
1216
|
+
(!('MultiPressPressedTimeMillis' in value) || typeof value.MultiPressPressedTimeMillis === 'number') &&
|
|
1217
|
+
(!('MultiPressReleasedTimeMillis' in value) || typeof value.MultiPressReleasedTimeMillis === 'number') &&
|
|
1218
|
+
(!('MultiPressNumPresses' in value) || typeof value.MultiPressNumPresses === 'number') &&
|
|
1219
|
+
(!('MultiPressMax' in value) || typeof value.MultiPressMax === 'number'));
|
|
1131
1220
|
}
|
package/dist/demoDevices.js
CHANGED
|
@@ -5,6 +5,7 @@ import { AirQuality } from '@matter/types/clusters/air-quality';
|
|
|
5
5
|
import { ClosureDimension } from '@matter/types/clusters/closure-dimension';
|
|
6
6
|
import { DoorLock } from '@matter/types/clusters/door-lock';
|
|
7
7
|
import { FanControl } from '@matter/types/clusters/fan-control';
|
|
8
|
+
import { Identify } from '@matter/types/clusters/identify';
|
|
8
9
|
import { PowerSource } from '@matter/types/clusters/power-source';
|
|
9
10
|
import { PowerTopology } from '@matter/types/clusters/power-topology';
|
|
10
11
|
import { ResourceMonitoring } from '@matter/types/clusters/resource-monitoring';
|
|
@@ -14,18 +15,25 @@ import { Thermostat } from '@matter/types/clusters/thermostat';
|
|
|
14
15
|
import { EndpointNumber } from '@matter/types/datatype';
|
|
15
16
|
import { getErrorMessage } from '@matterbridge/utils/error';
|
|
16
17
|
import { AirConditioner } from './devices/airConditioner.js';
|
|
18
|
+
import { AudioDoorbell } from './devices/audioDoorbell.js';
|
|
17
19
|
import { BasicVideoPlayer } from './devices/basicVideoPlayer.js';
|
|
18
20
|
import { BatteryStorage } from './devices/batteryStorage.js';
|
|
21
|
+
import { Camera } from './devices/camera.js';
|
|
22
|
+
import { CameraController } from './devices/cameraController.js';
|
|
19
23
|
import { CastingVideoClient } from './devices/castingVideoClient.js';
|
|
20
24
|
import { CastingVideoPlayer } from './devices/castingVideoPlayer.js';
|
|
25
|
+
import { Chime } from './devices/chime.js';
|
|
21
26
|
import { Closure } from './devices/closure.js';
|
|
22
27
|
import { ContentApp } from './devices/contentApp.js';
|
|
23
28
|
import { Cooktop } from './devices/cooktop.js';
|
|
24
29
|
import { Dishwasher } from './devices/dishwasher.js';
|
|
30
|
+
import { Doorbell } from './devices/doorbell.js';
|
|
25
31
|
import { ElectricalUtilityMeter } from './devices/electricalUtilityMeter.js';
|
|
26
32
|
import { Evse } from './devices/evse.js';
|
|
27
33
|
import { ExtractorHood } from './devices/extractorHood.js';
|
|
34
|
+
import { FloodlightCamera } from './devices/floodlightCamera.js';
|
|
28
35
|
import { HeatPump } from './devices/heatPump.js';
|
|
36
|
+
import { Intercom } from './devices/intercom.js';
|
|
29
37
|
import { IrrigationSystem } from './devices/irrigationSystem.js';
|
|
30
38
|
import { LaundryDryer } from './devices/laundryDryer.js';
|
|
31
39
|
import { LaundryWasher } from './devices/laundryWasher.js';
|
|
@@ -33,8 +41,10 @@ import { MicrowaveOven } from './devices/microwaveOven.js';
|
|
|
33
41
|
import { Oven } from './devices/oven.js';
|
|
34
42
|
import { Refrigerator } from './devices/refrigerator.js';
|
|
35
43
|
import { RoboticVacuumCleaner } from './devices/roboticVacuumCleaner.js';
|
|
44
|
+
import { SnapshotCamera } from './devices/snapshotCamera.js';
|
|
36
45
|
import { SolarPower } from './devices/solarPower.js';
|
|
37
46
|
import { Speaker } from './devices/speaker.js';
|
|
47
|
+
import { VideoDoorbell } from './devices/videoDoorbell.js';
|
|
38
48
|
import { VideoRemoteControl } from './devices/videoRemoteControl.js';
|
|
39
49
|
import { WaterHeater } from './devices/waterHeater.js';
|
|
40
50
|
import { getSupportedDeviceType } from './matterbridgeDeviceTypes.js';
|
|
@@ -95,6 +105,15 @@ const demoPluginSchema = {
|
|
|
95
105
|
},
|
|
96
106
|
},
|
|
97
107
|
};
|
|
108
|
+
let demoPluginConfig = {
|
|
109
|
+
name: demoPluginName,
|
|
110
|
+
type: demoPluginType,
|
|
111
|
+
version: demoPluginVersion,
|
|
112
|
+
debug: false,
|
|
113
|
+
unregisterOnShutdown: false,
|
|
114
|
+
whiteList: [],
|
|
115
|
+
blackList: [],
|
|
116
|
+
};
|
|
98
117
|
export async function createDemoDevices(matterbridge) {
|
|
99
118
|
if (matterbridge.bridgeMode !== 'bridge') {
|
|
100
119
|
matterbridge.log.error('Demo devices can only be created in bridge mode');
|
|
@@ -108,20 +127,10 @@ export async function createDemoDevices(matterbridge) {
|
|
|
108
127
|
}
|
|
109
128
|
const configFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.config.json`);
|
|
110
129
|
const schemaFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.schema.json`);
|
|
111
|
-
const defaultConfig = {
|
|
112
|
-
name: demoPluginName,
|
|
113
|
-
type: demoPluginType,
|
|
114
|
-
version: demoPluginVersion,
|
|
115
|
-
debug: false,
|
|
116
|
-
unregisterOnShutdown: false,
|
|
117
|
-
whiteList: [],
|
|
118
|
-
blackList: [],
|
|
119
|
-
};
|
|
120
|
-
let config = defaultConfig;
|
|
121
130
|
try {
|
|
122
131
|
const storedConfig = JSON.parse(await readFile(configFile, 'utf8'));
|
|
123
|
-
|
|
124
|
-
...
|
|
132
|
+
demoPluginConfig = {
|
|
133
|
+
...demoPluginConfig,
|
|
125
134
|
...storedConfig,
|
|
126
135
|
name: demoPluginName,
|
|
127
136
|
type: demoPluginType,
|
|
@@ -135,7 +144,7 @@ export async function createDemoDevices(matterbridge) {
|
|
|
135
144
|
matterbridge.log.error(`Failed to read demo devices config ${configFile}: ${getErrorMessage(error)}`);
|
|
136
145
|
}
|
|
137
146
|
try {
|
|
138
|
-
await writeFile(configFile, JSON.stringify(
|
|
147
|
+
await writeFile(configFile, JSON.stringify(demoPluginConfig, null, 2), 'utf8');
|
|
139
148
|
await writeFile(schemaFile, JSON.stringify(demoPluginSchema, null, 2), 'utf8');
|
|
140
149
|
}
|
|
141
150
|
catch (error) {
|
|
@@ -152,14 +161,14 @@ export async function createDemoDevices(matterbridge) {
|
|
|
152
161
|
enabled: false,
|
|
153
162
|
private: true,
|
|
154
163
|
registeredDevices: 0,
|
|
155
|
-
configJson:
|
|
164
|
+
configJson: demoPluginConfig,
|
|
156
165
|
schemaJson: demoPluginSchema,
|
|
157
166
|
hasWhiteList: true,
|
|
158
167
|
hasBlackList: true,
|
|
159
168
|
});
|
|
160
169
|
const registerDevice = async (device, deviceName, serialNumber) => {
|
|
161
|
-
const whiteList =
|
|
162
|
-
const blackList =
|
|
170
|
+
const whiteList = demoPluginConfig.whiteList;
|
|
171
|
+
const blackList = demoPluginConfig.blackList;
|
|
163
172
|
if (blackList.includes(deviceName)) {
|
|
164
173
|
matterbridge.log.info(`Skipping demo device ${deviceName} because it is in the blacklist`);
|
|
165
174
|
return;
|
|
@@ -267,34 +276,42 @@ export async function createDemoDevices(matterbridge) {
|
|
|
267
276
|
ep = new MatterbridgeEndpoint([getSupportedDeviceType('PumpController'), bridgedNode, powerSource], { id: 'PumpController', number: EndpointNumber(6_05) });
|
|
268
277
|
ep.createDefaultPowerSourceWiredClusterServer();
|
|
269
278
|
await registerDevice(ep, 'Pump Controller', 'SWITCH-06-05');
|
|
270
|
-
ep = new MatterbridgeEndpoint([getSupportedDeviceType('
|
|
279
|
+
ep = new MatterbridgeEndpoint([getSupportedDeviceType('GenericSwitch'), bridgedNode, powerSource], { id: 'MomentarySwitch', number: EndpointNumber(6_06) });
|
|
280
|
+
ep.createDefaultPowerSourceBatteryClusterServer();
|
|
281
|
+
ep.createDefaultSwitchClusterServer();
|
|
282
|
+
await registerDevice(ep, 'Momentary Switch', 'SWITCH-06-06');
|
|
283
|
+
ep = new MatterbridgeEndpoint([getSupportedDeviceType('GenericSwitch'), bridgedNode, powerSource], { id: 'LatchingSwitch', number: EndpointNumber(6_06_1) });
|
|
284
|
+
ep.createDefaultPowerSourceBatteryClusterServer();
|
|
285
|
+
ep.createDefaultLatchingSwitchClusterServer();
|
|
286
|
+
await registerDevice(ep, 'Latching Switch', 'SWITCH-06-06-1');
|
|
287
|
+
ep = new MatterbridgeEndpoint([getSupportedDeviceType('Aggregator'), bridgedNode, powerSource], { id: 'GenericSwitch', number: EndpointNumber(6_06_2) });
|
|
271
288
|
ep.createDefaultPowerSourceBatteryClusterServer();
|
|
272
289
|
await ep.addFixedLabel('composed', 'GenericSwitch');
|
|
273
290
|
ep.addChildDeviceType('Button1', getSupportedDeviceType('GenericSwitch'), {
|
|
274
|
-
number: EndpointNumber(
|
|
291
|
+
number: EndpointNumber(6_06_3),
|
|
275
292
|
tagList: [getSemtag(SwitchesTag.On), getSemtag(CommonNumberTag.One)],
|
|
276
293
|
})
|
|
277
294
|
.createDefaultMomentarySwitchClusterServer()
|
|
278
295
|
.addRequiredClusters();
|
|
279
296
|
ep.addChildDeviceType('Button2', getSupportedDeviceType('GenericSwitch'), {
|
|
280
|
-
number: EndpointNumber(
|
|
297
|
+
number: EndpointNumber(6_06_4),
|
|
281
298
|
tagList: [getSemtag(SwitchesTag.Off), getSemtag(CommonNumberTag.Two)],
|
|
282
299
|
})
|
|
283
300
|
.createDefaultMomentarySwitchClusterServer()
|
|
284
301
|
.addRequiredClusters();
|
|
285
302
|
ep.addChildDeviceType('Button3', getSupportedDeviceType('GenericSwitch'), {
|
|
286
|
-
number: EndpointNumber(
|
|
303
|
+
number: EndpointNumber(6_06_5),
|
|
287
304
|
tagList: [getSemtag(SwitchesTag.Up), getSemtag(CommonNumberTag.Three)],
|
|
288
305
|
})
|
|
289
306
|
.createDefaultMomentarySwitchClusterServer()
|
|
290
307
|
.addRequiredClusters();
|
|
291
308
|
ep.addChildDeviceType('Button4', getSupportedDeviceType('GenericSwitch'), {
|
|
292
|
-
number: EndpointNumber(
|
|
309
|
+
number: EndpointNumber(6_06_6),
|
|
293
310
|
tagList: [getSemtag(SwitchesTag.Down), getSemtag(CommonNumberTag.Four)],
|
|
294
311
|
})
|
|
295
312
|
.createDefaultMomentarySwitchClusterServer()
|
|
296
313
|
.addRequiredClusters();
|
|
297
|
-
await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06');
|
|
314
|
+
await registerDevice(ep, 'Generic Switch', 'SWITCH-06-06-2');
|
|
298
315
|
ep = new MatterbridgeEndpoint([getSupportedDeviceType('ContactSensor'), bridgedNode, powerSource], { id: 'ContactSensor', number: EndpointNumber(7_01) });
|
|
299
316
|
ep.createDefaultPowerSourceBatteryClusterServer();
|
|
300
317
|
ep.createDefaultBooleanStateClusterServer();
|
|
@@ -823,4 +840,83 @@ export async function createDemoDevices(matterbridge) {
|
|
|
823
840
|
tagList: [getSemtag(ElectricalMeasurementTag.Ac), getSemtag(PowerSourceTag.Ev), getSemtag(CommodityTariffFlowTag.Import), getSemtag(CommodityTariffChronologyTag.Upcoming)],
|
|
824
841
|
});
|
|
825
842
|
await registerDevice(electricalUtilityMeterEvEndpoint, 'Electrical Utility Meter Ev', 'ENERGY-14-10');
|
|
843
|
+
ep = new Camera('Camera', 'CAMERA-16-01', {
|
|
844
|
+
id: 'Camera',
|
|
845
|
+
number: EndpointNumber(16_01),
|
|
846
|
+
weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
|
|
847
|
+
});
|
|
848
|
+
await registerDevice(ep, 'Camera', 'CAMERA-16-01');
|
|
849
|
+
ep = new Camera('Camera Ptz', 'CAMERA-16-01-1', {
|
|
850
|
+
id: 'CameraPtz',
|
|
851
|
+
number: EndpointNumber(16_01_1),
|
|
852
|
+
ptz: true,
|
|
853
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
854
|
+
weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
|
|
855
|
+
});
|
|
856
|
+
await registerDevice(ep, 'Camera Ptz', 'CAMERA-16-01-1');
|
|
857
|
+
ep = new FloodlightCamera('Floodlight Camera', 'CAMERA-16-02', {
|
|
858
|
+
id: 'FloodlightCamera',
|
|
859
|
+
number: EndpointNumber(16_02),
|
|
860
|
+
cameraOptions: {
|
|
861
|
+
number: EndpointNumber(16_02_1),
|
|
862
|
+
ptz: true,
|
|
863
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
864
|
+
weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
|
|
865
|
+
},
|
|
866
|
+
lightOptions: { number: EndpointNumber(16_02_2) },
|
|
867
|
+
});
|
|
868
|
+
await registerDevice(ep, 'Floodlight Camera', 'CAMERA-16-02');
|
|
869
|
+
ep = new VideoDoorbell('Video Doorbell', 'CAMERA-16-03', {
|
|
870
|
+
id: 'VideoDoorbell',
|
|
871
|
+
number: EndpointNumber(16_03),
|
|
872
|
+
cameraOptions: {
|
|
873
|
+
number: EndpointNumber(16_03_1),
|
|
874
|
+
ptz: true,
|
|
875
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
876
|
+
weriftOfferOptions: { offerVideo: true, offerAudio: true, videoSource: 'test', audioSource: 'test' },
|
|
877
|
+
},
|
|
878
|
+
doorbellOptions: { number: EndpointNumber(16_03_2) },
|
|
879
|
+
});
|
|
880
|
+
await registerDevice(ep, 'Video Doorbell', 'CAMERA-16-03');
|
|
881
|
+
ep = new Intercom('Intercom I', 'CAMERA-16-04', {
|
|
882
|
+
id: 'Intercom',
|
|
883
|
+
number: EndpointNumber(16_04),
|
|
884
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
885
|
+
weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
|
|
886
|
+
});
|
|
887
|
+
await registerDevice(ep, 'Intercom I', 'CAMERA-16-04');
|
|
888
|
+
ep = new Intercom('Intercom II', 'CAMERA-16-04-1', {
|
|
889
|
+
id: 'IntercomII',
|
|
890
|
+
number: EndpointNumber(16_04_1),
|
|
891
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
892
|
+
weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
|
|
893
|
+
});
|
|
894
|
+
await registerDevice(ep, 'Intercom II', 'CAMERA-16-04-1');
|
|
895
|
+
ep = new AudioDoorbell('Audio Doorbell', 'CAMERA-16-05', {
|
|
896
|
+
id: 'AudioDoorbell',
|
|
897
|
+
number: EndpointNumber(16_05),
|
|
898
|
+
weriftOfferOptions: { offerVideo: false, offerAudio: true, videoSource: 'none', audioSource: 'test' },
|
|
899
|
+
});
|
|
900
|
+
await registerDevice(ep, 'Audio Doorbell', 'CAMERA-16-05');
|
|
901
|
+
ep = new SnapshotCamera('Snapshot Camera', 'CAMERA-16-06', {
|
|
902
|
+
id: 'SnapshotCamera',
|
|
903
|
+
number: EndpointNumber(16_06),
|
|
904
|
+
identifyType: Identify.IdentifyType.VisibleIndicator,
|
|
905
|
+
snapshotSource: 'test',
|
|
906
|
+
});
|
|
907
|
+
await registerDevice(ep, 'Snapshot Camera', 'CAMERA-16-06');
|
|
908
|
+
ep = new Chime('Chime', 'CAMERA-16-07', {
|
|
909
|
+
id: 'Chime',
|
|
910
|
+
number: EndpointNumber(16_07),
|
|
911
|
+
identifyType: Identify.IdentifyType.AudibleBeep,
|
|
912
|
+
installedChimeSounds: [
|
|
913
|
+
{ chimeId: 0, name: 'Default Chime' },
|
|
914
|
+
{ chimeId: 1, name: 'Westminster' },
|
|
915
|
+
],
|
|
916
|
+
});
|
|
917
|
+
await registerDevice(ep, 'Chime', 'CAMERA-16-07');
|
|
918
|
+
ep = new CameraController('Camera Controller', 'CAMERA-16-08', { id: 'CameraController', number: EndpointNumber(16_08) });
|
|
919
|
+
await registerDevice(ep, 'Camera Controller', 'CAMERA-16-08');
|
|
920
|
+
ep = new Doorbell('Doorbell', 'CAMERA-16-09', { id: 'Doorbell', number: EndpointNumber(16_09) });
|
|
921
|
+
await registerDevice(ep, 'Doorbell', 'CAMERA-16-09');
|
|
826
922
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from '../audioDoorbell.js';
|
|
2
|
+
export * from '../camera.js';
|
|
3
|
+
export * from '../cameraController.js';
|
|
4
|
+
export * from '../chime.js';
|
|
5
|
+
export * from '../doorbell.js';
|
|
6
|
+
export * from '../floodlightCamera.js';
|
|
7
|
+
export * from '../intercom.js';
|
|
8
|
+
export * from '../snapshotCamera.js';
|
|
9
|
+
export * from '../videoDoorbell.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from '../audioDoorbell.js';
|
|
2
|
+
export * from '../camera.js';
|
|
3
|
+
export * from '../cameraController.js';
|
|
4
|
+
export * from '../chime.js';
|
|
5
|
+
export * from '../doorbell.js';
|
|
6
|
+
export * from '../floodlightCamera.js';
|
|
7
|
+
export * from '../intercom.js';
|
|
8
|
+
export * from '../snapshotCamera.js';
|
|
9
|
+
export * from '../videoDoorbell.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
2
|
+
import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
|
|
3
|
+
export interface CameraControllerOptions extends MatterbridgeEndpointOptions {
|
|
4
|
+
powerSourceType?: 'Rechargeable' | 'Replaceable' | 'Battery' | 'Wired' | 'None';
|
|
5
|
+
}
|
|
6
|
+
export declare class CameraController extends MatterbridgeEndpoint {
|
|
7
|
+
constructor(name: string, serial: string, options?: CameraControllerOptions);
|
|
8
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { addWebRtcTransportProviderClient } from '../behaviors/clients.js';
|
|
2
|
+
import { createDefaultWebRtcTransportRequestorClusterServer } from '../behaviors/webRtcTransportRequestorServer.js';
|
|
3
|
+
import { cameraController, powerSource } from '../matterbridgeDeviceTypes.js';
|
|
4
|
+
import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
5
|
+
export class CameraController extends MatterbridgeEndpoint {
|
|
6
|
+
constructor(name, serial, options = {}) {
|
|
7
|
+
const { powerSourceType = 'Wired', id, number, tagList, mode } = options;
|
|
8
|
+
super(powerSourceType === 'None' ? [cameraController] : [cameraController, powerSource], {
|
|
9
|
+
id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
|
|
10
|
+
number,
|
|
11
|
+
tagList,
|
|
12
|
+
mode,
|
|
13
|
+
});
|
|
14
|
+
this.createDefaultBasicInformationClusterServer(name, serial, 0xfff1, 'Matterbridge', 0x8000, 'Matterbridge Camera Controller');
|
|
15
|
+
switch (powerSourceType) {
|
|
16
|
+
case 'Rechargeable':
|
|
17
|
+
this.createDefaultPowerSourceRechargeableBatteryClusterServer();
|
|
18
|
+
break;
|
|
19
|
+
case 'Replaceable':
|
|
20
|
+
this.createDefaultPowerSourceReplaceableBatteryClusterServer();
|
|
21
|
+
break;
|
|
22
|
+
case 'Battery':
|
|
23
|
+
this.createDefaultPowerSourceBatteryClusterServer();
|
|
24
|
+
break;
|
|
25
|
+
case 'Wired':
|
|
26
|
+
this.createDefaultPowerSourceWiredClusterServer();
|
|
27
|
+
break;
|
|
28
|
+
case 'None':
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
createDefaultWebRtcTransportRequestorClusterServer(this);
|
|
32
|
+
addWebRtcTransportProviderClient(this);
|
|
33
|
+
this.addRequiredClusters();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Semtag } from '@matter/types';
|
|
2
|
+
import type { EndpointNumber } from '@matter/types/datatype';
|
|
2
3
|
import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
3
4
|
import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
|
|
4
5
|
import type { CameraOptions } from './camera.js';
|
|
5
|
-
export type FloodlightCameraCameraOptions = Omit<CameraOptions, 'id' | '
|
|
6
|
+
export type FloodlightCameraCameraOptions = Omit<CameraOptions, 'id' | 'mode' | 'powerSourceType'>;
|
|
6
7
|
export interface FloodlightCameraLightOptions {
|
|
7
8
|
name?: string;
|
|
8
9
|
tagList?: Semtag[];
|
|
10
|
+
number?: EndpointNumber;
|
|
9
11
|
onOff?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export interface FloodlightCameraOptions extends MatterbridgeEndpointOptions {
|
|
@@ -15,5 +17,5 @@ export interface FloodlightCameraOptions extends MatterbridgeEndpointOptions {
|
|
|
15
17
|
}
|
|
16
18
|
export declare class FloodlightCamera extends MatterbridgeEndpoint {
|
|
17
19
|
constructor(name: string, serial: string, options?: FloodlightCameraOptions);
|
|
18
|
-
addLight(name: string,
|
|
20
|
+
addLight(name: string, options?: FloodlightCameraLightOptions): MatterbridgeEndpoint;
|
|
19
21
|
}
|
|
@@ -43,8 +43,8 @@ export class FloodlightCamera extends MatterbridgeEndpoint {
|
|
|
43
43
|
{ resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
44
44
|
{ resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
45
45
|
{ resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
46
|
-
], weriftOfferOptions, } = cameraOptions;
|
|
47
|
-
const cameraChild = this.addChildDeviceType('Camera', camera, {});
|
|
46
|
+
], weriftOfferOptions, number: cameraNumber, tagList: cameraTagList = [], } = cameraOptions;
|
|
47
|
+
const cameraChild = this.addChildDeviceType('Camera', camera, { number: cameraNumber, ...(cameraTagList.length > 0 ? { tagList: cameraTagList } : {}) });
|
|
48
48
|
cameraChild.log.logName = 'Camera';
|
|
49
49
|
if (identifyType !== Identify.IdentifyType.None) {
|
|
50
50
|
cameraChild.createDefaultIdentifyClusterServer(identifyTime, identifyType);
|
|
@@ -67,11 +67,12 @@ export class FloodlightCamera extends MatterbridgeEndpoint {
|
|
|
67
67
|
createDefaultWebRtcTransportProviderClusterServer(cameraChild, weriftOfferOptions);
|
|
68
68
|
addWebRtcTransportRequestorClient(cameraChild);
|
|
69
69
|
cameraChild.addRequiredClusters();
|
|
70
|
-
const { name: lightName = 'Light', tagList: lightTagList = [], onOff: lightOnOff = false } = lightOptions;
|
|
71
|
-
this.addLight(lightName, lightTagList, lightOnOff);
|
|
70
|
+
const { name: lightName = 'Light', tagList: lightTagList = [], number: lightNumber, onOff: lightOnOff = false } = lightOptions;
|
|
71
|
+
this.addLight(lightName, { tagList: lightTagList, number: lightNumber, onOff: lightOnOff });
|
|
72
72
|
}
|
|
73
|
-
addLight(name,
|
|
74
|
-
const
|
|
73
|
+
addLight(name, options = {}) {
|
|
74
|
+
const { tagList = [], number, onOff = false } = options;
|
|
75
|
+
const light = this.addChildDeviceType(name, onOffLight, { number, ...(tagList.length > 0 ? { tagList } : {}) });
|
|
75
76
|
light.log.logName = name;
|
|
76
77
|
light.createDefaultIdentifyClusterServer();
|
|
77
78
|
light.createDefaultOnOffClusterServer(onOff);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StreamUsage } from '@matter/types';
|
|
2
2
|
import { CameraAvStreamManagement } from '@matter/types/clusters/camera-av-stream-management';
|
|
3
3
|
import { Identify } from '@matter/types/clusters/identify';
|
|
4
|
+
import { type SnapshotSource } from '../behaviors/cameraAvStreamManagementServer.js';
|
|
4
5
|
import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
5
6
|
import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
|
|
6
7
|
export interface SnapshotCameraOptions extends MatterbridgeEndpointOptions {
|
|
@@ -14,6 +15,8 @@ export interface SnapshotCameraOptions extends MatterbridgeEndpointOptions {
|
|
|
14
15
|
maxNetworkBandwidth?: number;
|
|
15
16
|
supportedStreamUsages?: StreamUsage[];
|
|
16
17
|
streamUsagePriorities?: StreamUsage[];
|
|
18
|
+
snapshotSource?: SnapshotSource;
|
|
19
|
+
snapshotSourceDevice?: string;
|
|
17
20
|
}
|
|
18
21
|
export declare class SnapshotCamera extends MatterbridgeEndpoint {
|
|
19
22
|
constructor(name: string, serial: string, options?: SnapshotCameraOptions);
|
|
@@ -10,7 +10,7 @@ export class SnapshotCamera extends MatterbridgeEndpoint {
|
|
|
10
10
|
{ resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
11
11
|
{ resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
12
12
|
{ resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
13
|
-
], maxNetworkBandwidth = 10000, supportedStreamUsages = [StreamUsage.Recording], streamUsagePriorities = [StreamUsage.Recording], id, number, tagList, mode, } = options;
|
|
13
|
+
], maxNetworkBandwidth = 10000, supportedStreamUsages = [StreamUsage.Recording], streamUsagePriorities = [StreamUsage.Recording], snapshotSource, snapshotSourceDevice, id, number, tagList, mode, } = options;
|
|
14
14
|
super(powerSourceType === 'None' ? [snapshotCamera] : [snapshotCamera, powerSource], {
|
|
15
15
|
id: id ?? `${name.replaceAll(' ', '')}-${serial.replaceAll(' ', '')}`,
|
|
16
16
|
number,
|
|
@@ -45,6 +45,8 @@ export class SnapshotCamera extends MatterbridgeEndpoint {
|
|
|
45
45
|
maxNetworkBandwidth,
|
|
46
46
|
supportedStreamUsages,
|
|
47
47
|
streamUsagePriorities,
|
|
48
|
+
snapshotSource,
|
|
49
|
+
snapshotSourceDevice,
|
|
48
50
|
});
|
|
49
51
|
this.addRequiredClusters();
|
|
50
52
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Semtag } from '@matter/types';
|
|
2
2
|
import { Identify } from '@matter/types/clusters/identify';
|
|
3
|
+
import type { EndpointNumber } from '@matter/types/datatype';
|
|
3
4
|
import { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
|
|
4
5
|
import type { MatterbridgeEndpointOptions } from '../matterbridgeEndpointTypes.js';
|
|
5
6
|
import type { CameraOptions } from './camera.js';
|
|
6
|
-
export type VideoDoorbellCameraOptions = Omit<CameraOptions, 'id' | '
|
|
7
|
+
export type VideoDoorbellCameraOptions = Omit<CameraOptions, 'id' | 'mode' | 'powerSourceType'>;
|
|
7
8
|
export interface VideoDoorbellDoorbellOptions {
|
|
8
9
|
name?: string;
|
|
9
10
|
tagList?: Semtag[];
|
|
11
|
+
number?: EndpointNumber;
|
|
10
12
|
identifyTime?: number;
|
|
11
13
|
identifyType?: Identify.IdentifyType;
|
|
12
14
|
}
|
|
@@ -17,5 +19,5 @@ export interface VideoDoorbellOptions extends MatterbridgeEndpointOptions {
|
|
|
17
19
|
}
|
|
18
20
|
export declare class VideoDoorbell extends MatterbridgeEndpoint {
|
|
19
21
|
constructor(name: string, serial: string, options?: VideoDoorbellOptions);
|
|
20
|
-
addDoorbell(name: string,
|
|
22
|
+
addDoorbell(name: string, options?: VideoDoorbellDoorbellOptions): MatterbridgeEndpoint;
|
|
21
23
|
}
|
|
@@ -43,8 +43,8 @@ export class VideoDoorbell extends MatterbridgeEndpoint {
|
|
|
43
43
|
{ resolution: { width: 640, height: 480 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
44
44
|
{ resolution: { width: 1280, height: 720 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
45
45
|
{ resolution: { width: 1920, height: 1080 }, maxFrameRate: 10, imageCodec: CameraAvStreamManagement.ImageCodec.Jpeg, requiresEncodedPixels: false },
|
|
46
|
-
], weriftOfferOptions, } = cameraOptions;
|
|
47
|
-
const cameraChild = this.addChildDeviceType('Camera', camera, {});
|
|
46
|
+
], weriftOfferOptions, number: cameraNumber, tagList: cameraTagList = [], } = cameraOptions;
|
|
47
|
+
const cameraChild = this.addChildDeviceType('Camera', camera, { number: cameraNumber, ...(cameraTagList.length > 0 ? { tagList: cameraTagList } : {}) });
|
|
48
48
|
cameraChild.log.logName = 'Camera';
|
|
49
49
|
if (cameraIdentifyType !== Identify.IdentifyType.None) {
|
|
50
50
|
cameraChild.createDefaultIdentifyClusterServer(cameraIdentifyTime, cameraIdentifyType);
|
|
@@ -67,11 +67,12 @@ export class VideoDoorbell extends MatterbridgeEndpoint {
|
|
|
67
67
|
createDefaultWebRtcTransportProviderClusterServer(cameraChild, weriftOfferOptions);
|
|
68
68
|
addWebRtcTransportRequestorClient(cameraChild);
|
|
69
69
|
cameraChild.addRequiredClusters();
|
|
70
|
-
const { name: doorbellName = 'Doorbell', tagList: doorbellTagList = [], identifyTime = 0, identifyType = Identify.IdentifyType.None } = doorbellOptions;
|
|
71
|
-
this.addDoorbell(doorbellName, doorbellTagList, identifyTime, identifyType);
|
|
70
|
+
const { name: doorbellName = 'Doorbell', tagList: doorbellTagList = [], number: doorbellNumber, identifyTime = 0, identifyType = Identify.IdentifyType.None } = doorbellOptions;
|
|
71
|
+
this.addDoorbell(doorbellName, { tagList: doorbellTagList, number: doorbellNumber, identifyTime, identifyType });
|
|
72
72
|
}
|
|
73
|
-
addDoorbell(name,
|
|
74
|
-
const
|
|
73
|
+
addDoorbell(name, options = {}) {
|
|
74
|
+
const { tagList = [], number, identifyTime = 0, identifyType = Identify.IdentifyType.None } = options;
|
|
75
|
+
const doorbellChild = this.addChildDeviceType(name, doorbell, { number, ...(tagList.length > 0 ? { tagList } : {}) });
|
|
75
76
|
doorbellChild.log.logName = name;
|
|
76
77
|
doorbellChild.createDefaultIdentifyClusterServer(identifyTime, identifyType);
|
|
77
78
|
doorbellChild.createDefaultMomentarySwitchClusterServer();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matterbridge/core",
|
|
3
|
-
"version": "3.10.10-dev-
|
|
3
|
+
"version": "3.10.10-dev-20260915-57739b4",
|
|
4
4
|
"description": "Matterbridge core library",
|
|
5
5
|
"author": "https://github.com/Luligu",
|
|
6
6
|
"homepage": "https://matterbridge.io/",
|
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
"types": "./dist/devices/export.d.ts",
|
|
55
55
|
"import": "./dist/devices/export.js"
|
|
56
56
|
},
|
|
57
|
+
"./devices/camera": {
|
|
58
|
+
"types": "./dist/devices/camera/export.d.ts",
|
|
59
|
+
"import": "./dist/devices/camera/export.js"
|
|
60
|
+
},
|
|
57
61
|
"./clusters": {
|
|
58
62
|
"types": "./dist/clusters/export.d.ts",
|
|
59
63
|
"import": "./dist/clusters/export.js"
|
|
@@ -132,14 +136,14 @@
|
|
|
132
136
|
},
|
|
133
137
|
"dependencies": {
|
|
134
138
|
"@matter/main": "0.17.9",
|
|
135
|
-
"@matterbridge/dgram": "3.10.10-dev-
|
|
136
|
-
"@matterbridge/thread": "3.10.10-dev-
|
|
137
|
-
"@matterbridge/types": "3.10.10-dev-
|
|
138
|
-
"@matterbridge/utils": "3.10.10-dev-
|
|
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
143
|
"escape-html": "1.0.3",
|
|
140
144
|
"express": "5.2.1",
|
|
141
145
|
"express-rate-limit": "8.7.0",
|
|
142
|
-
"multer": "2.
|
|
146
|
+
"multer": "2.4.0",
|
|
143
147
|
"node-ansi-logger": "3.3.1",
|
|
144
148
|
"node-persist-manager": "2.1.1",
|
|
145
149
|
"werift": "0.24.4",
|