Skip to content

Commit c896ace

Browse files
committed
Update Demo Devices
1 parent 0b61adb commit c896ace

4 files changed

Lines changed: 138 additions & 16 deletions

File tree

packages/core/src/behaviors/booleanStateConfigurationServer.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type { MaybePromise } from '@matter/general';
2828
import { BooleanStateConfigurationServer } from '@matter/node/behaviors/boolean-state-configuration';
2929
import { Status, StatusResponseError } from '@matter/types';
3030
import { BooleanStateConfiguration } from '@matter/types/clusters/boolean-state-configuration';
31-
import { debugStringify } from 'node-ansi-logger';
31+
import { debugStringify, nf } from 'node-ansi-logger';
3232

3333
import type { MatterbridgeEndpoint } from '../matterbridgeEndpoint.js';
3434
import type { ClusterAttributeValues } from '../matterbridgeEndpointCommandHandler.js';
@@ -96,7 +96,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
9696

9797
#assertAlarmModesSupported(alarms: BooleanStateConfiguration.AlarmMode): void {
9898
if ([Boolean(alarms.visual && !this.state.alarmsSupported.visual), Boolean(alarms.audible && !this.state.alarmsSupported.audible)].some(Boolean)) {
99-
throw new StatusResponseError('Requested alarm mode is not supported', Status.ConstraintError);
99+
throw new StatusResponseError(`Requested alarm mode is not supported (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.ConstraintError);
100100
}
101101
}
102102

@@ -107,7 +107,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
107107
Boolean(alarmsToSuppress.audible && (!this.state.alarmsActive.audible || !this.state.alarmsEnabled?.audible)),
108108
].some(Boolean)
109109
) {
110-
throw new StatusResponseError('Requested alarm mode is not active', Status.InvalidInState);
110+
throw new StatusResponseError(`Requested alarm mode is not active (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`, Status.InvalidInState);
111111
}
112112
}
113113

@@ -118,7 +118,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
118118
*/
119119
override async suppressAlarm(request: BooleanStateConfiguration.SuppressAlarmRequest): Promise<void> {
120120
const device = this.endpoint.stateOf(MatterbridgeServer);
121-
device.log.info(`Suppressing alarm ${debugStringify(request.alarmsToSuppress)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
121+
device.log.info(`Suppressing alarm ${debugStringify(request.alarmsToSuppress)}${nf} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
122122
await device.commandHandler.executeHandler('BooleanStateConfiguration.suppressAlarm', {
123123
command: 'suppressAlarm',
124124
request,
@@ -130,7 +130,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
130130
this.#assertAlarmModesSupported(request.alarmsToSuppress);
131131
this.#assertSuppressAlarmAllowed(request.alarmsToSuppress);
132132
this.state.alarmsSuppressed = this.#mergeAlarmsSuppressed(request.alarmsToSuppress);
133-
device.log.debug(`MatterbridgeBooleanStateConfigurationServer: suppressAlarm called`);
133+
device.log.debug(`MatterbridgeBooleanStateConfigurationServer: suppressAlarm called (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
134134
}
135135

136136
/**
@@ -140,7 +140,7 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
140140
*/
141141
override async enableDisableAlarm(request: BooleanStateConfiguration.EnableDisableAlarmRequest): Promise<void> {
142142
const device = this.endpoint.stateOf(MatterbridgeServer);
143-
device.log.info(`Enabling/disabling alarm ${debugStringify(request.alarmsToEnableDisable)} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
143+
device.log.info(`Enabling/disabling alarm ${debugStringify(request.alarmsToEnableDisable)}${nf} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
144144
await device.commandHandler.executeHandler('BooleanStateConfiguration.enableDisableAlarm', {
145145
command: 'enableDisableAlarm',
146146
request,
@@ -151,6 +151,6 @@ export class MatterbridgeBooleanStateConfigurationServer extends BooleanStateCon
151151
});
152152
this.#assertAlarmModesSupported(request.alarmsToEnableDisable);
153153
this.#applyAlarmsEnabled(request.alarmsToEnableDisable);
154-
device.log.debug(`MatterbridgeBooleanStateConfigurationServer: enableDisableAlarm called`);
154+
device.log.debug(`MatterbridgeBooleanStateConfigurationServer: enableDisableAlarm called (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
155155
}
156156
}

packages/core/src/demoDevices.ts

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222
*/
2323

2424
/* v8 ignore start - No test cause is just a way to easily add new devices for testing purposes without using plugins */
25+
/* oxlint-disable max-lines-per-function */
2526
/* oxlint-disable typescript/no-non-null-assertion */
2627

28+
import { readFile, writeFile } from 'node:fs/promises';
29+
import path from 'node:path';
30+
2731
import { ClosureTag, ClosureWindowTag, CommonNumberTag, CommonPositionTag, RefrigeratorTag } from '@matter/node';
2832
import { AirQuality } from '@matter/types/clusters/air-quality';
2933
import { FanControl } from '@matter/types/clusters/fan-control';
@@ -32,6 +36,8 @@ import { ResourceMonitoring } from '@matter/types/clusters/resource-monitoring';
3236
import { RvcCleanMode } from '@matter/types/clusters/rvc-clean-mode';
3337
import { RvcRunMode } from '@matter/types/clusters/rvc-run-mode';
3438
import { EndpointNumber } from '@matter/types/datatype';
39+
import type { PlatformConfig, PlatformSchema } from '@matterbridge/types';
40+
import { getErrorMessage } from '@matterbridge/utils/error';
3541

3642
import { AirConditioner } from './devices/airConditioner.js';
3743
import { BasicVideoPlayer } from './devices/basicVideoPlayer.js';
@@ -56,32 +62,142 @@ import { getSupportedDeviceType } from './matterbridgeDeviceTypes.js';
5662
import { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
5763
import { getSemtag } from './matterbridgeEndpointHelpers.js';
5864

65+
const demoPluginName = 'matterbridge-demo-devices';
66+
const demoPluginType = 'DynamicPlatform';
67+
const demoPluginVersion = '1.0.0';
68+
69+
const demoPluginSchema: PlatformSchema = {
70+
title: 'Matterbridge Demo Devices',
71+
description: `${demoPluginName} v. ${demoPluginVersion} by Matterbridge`,
72+
type: 'object',
73+
properties: {
74+
name: {
75+
'description': 'Plugin name',
76+
'type': 'string',
77+
'readOnly': true,
78+
'ui:widget': 'hidden',
79+
},
80+
type: {
81+
'description': 'Plugin type',
82+
'type': 'string',
83+
'readOnly': true,
84+
'ui:widget': 'hidden',
85+
},
86+
version: {
87+
'description': 'Plugin version',
88+
'type': 'string',
89+
'readOnly': true,
90+
'default': demoPluginVersion,
91+
'ui:widget': 'hidden',
92+
},
93+
whiteList: {
94+
description: 'Only the devices in the list will be exposed. If the list is empty, all devices will be exposed.',
95+
type: 'array',
96+
items: { type: 'string' },
97+
default: [],
98+
uniqueItems: true,
99+
selectFrom: 'name',
100+
},
101+
blackList: {
102+
description: 'The devices in the list will not be exposed. If the list is empty, no devices will be excluded.',
103+
type: 'array',
104+
items: { type: 'string' },
105+
default: [],
106+
uniqueItems: true,
107+
selectFrom: 'name',
108+
},
109+
debug: {
110+
description: 'Enable debug logging for the plugin.',
111+
type: 'boolean',
112+
default: false,
113+
},
114+
unregisterOnShutdown: {
115+
description: 'Unregister all devices when the plugin is stopped.',
116+
type: 'boolean',
117+
default: false,
118+
},
119+
},
120+
};
121+
59122
export async function createDemoDevices(matterbridge: Matterbridge): Promise<void> {
60-
if (matterbridge.bridgeMode !== 'bridge' || !matterbridge.serverNode || !matterbridge.aggregatorNode) return;
123+
if (matterbridge.bridgeMode !== 'bridge') {
124+
matterbridge.log.error('Demo devices can only be created in bridge mode');
125+
return;
126+
}
61127
const serverNode = matterbridge.serverNode;
62128
const aggregator = matterbridge.aggregatorNode;
63129
if (!serverNode || !aggregator) {
64130
matterbridge.log.error('Demo devices can only be created when the server node and aggregator node are available');
65131
return;
66132
}
133+
const configFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.config.json`);
134+
const schemaFile = path.join(matterbridge.matterbridgeDirectory, `${demoPluginName}.schema.json`);
135+
const defaultConfig: PlatformConfig = {
136+
name: demoPluginName,
137+
type: demoPluginType,
138+
version: demoPluginVersion,
139+
debug: false,
140+
unregisterOnShutdown: false,
141+
whiteList: [],
142+
blackList: [],
143+
};
144+
let config = defaultConfig;
145+
try {
146+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
147+
const storedConfig = JSON.parse(await readFile(configFile, 'utf8')) as PlatformConfig;
148+
config = {
149+
...defaultConfig,
150+
...storedConfig,
151+
name: demoPluginName,
152+
type: demoPluginType,
153+
version: demoPluginVersion,
154+
whiteList: Array.isArray(storedConfig.whiteList) ? storedConfig.whiteList : [],
155+
blackList: Array.isArray(storedConfig.blackList) ? storedConfig.blackList : [],
156+
};
157+
} catch (error) {
158+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
159+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') matterbridge.log.error(`Failed to read demo devices config ${configFile}: ${getErrorMessage(error)}`);
160+
}
161+
try {
162+
await writeFile(configFile, JSON.stringify(config, null, 2), 'utf8');
163+
await writeFile(schemaFile, JSON.stringify(demoPluginSchema, null, 2), 'utf8');
164+
} catch (error) {
165+
matterbridge.log.error(`Failed to write demo devices config or schema: ${getErrorMessage(error)}`);
166+
}
67167
let ep: MatterbridgeEndpoint | undefined;
68168
matterbridge.plugins.set({
69-
name: 'matterbridge-demo-devices',
169+
name: demoPluginName,
70170
path: '',
71-
type: 'DynamicPlatform',
72-
version: '1.0.0',
171+
type: demoPluginType,
172+
version: demoPluginVersion,
73173
description: 'Matterbridge demo devices',
74174
author: 'Matterbridge',
75175
enabled: false,
76176
private: true,
77177
registeredDevices: 0,
178+
configJson: config,
179+
schemaJson: demoPluginSchema,
180+
hasWhiteList: true,
181+
hasBlackList: true,
78182
});
79183

80184
const registerDevice = async (device: MatterbridgeEndpoint, deviceName: string, serialNumber: string): Promise<void> => {
185+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
186+
const whiteList = config.whiteList as string[];
187+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
188+
const blackList = config.blackList as string[];
189+
if (blackList.includes(deviceName)) {
190+
matterbridge.log.info(`Skipping demo device ${deviceName} because it is in the blacklist`);
191+
return;
192+
}
193+
if (whiteList.length > 0 && !whiteList.includes(deviceName)) {
194+
matterbridge.log.info(`Skipping demo device ${deviceName} because it is not in the whitelist`);
195+
return;
196+
}
81197
device.createDefaultBridgedDeviceBasicInformationClusterServer(deviceName, serialNumber);
82198
device.addRequiredClusters();
83-
device.plugin = 'matterbridge-demo-devices';
84-
await matterbridge.addBridgedEndpoint('matterbridge-demo-devices', device);
199+
device.plugin = demoPluginName;
200+
await matterbridge.addBridgedEndpoint(demoPluginName, device);
85201
};
86202

87203
const bridgedNode = getSupportedDeviceType('BridgedNode')!;

packages/core/vitest/devices/oven.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,9 @@ describe('Matterbridge ' + NAME, () => {
506506
LogLevel.INFO,
507507
`MatterbridgeOvenCavityOperationalStateServer: stop (endpoint OvenTestCabinetTop.3) called setting operational state to Stopped and operational error to No error`,
508508
);
509+
510+
// A repeated Stop does not complete the same operation again.
511+
await cabinet1.invokeBehaviorCommand('ovenCavityOperationalState', 'stop', { newMode: 15 });
509512
} finally {
510513
vi.useRealTimers();
511514
}

packages/core/vitest/matterbridgeEndpoint-matterjs.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ import {
9393
} from '@matter/types/clusters';
9494
import { flushAsync, HOMEDIR, loggerLogSpy, setDebug, setupTest } from '@matterbridge/vitest-utils';
9595
import { createServerNode, createTestEnvironment, destroyTestEnvironment, flushServerNode, server, startServerNode, stopServerNode } from '@matterbridge/vitest-utils/matter';
96-
import { AnsiLogger, debugStringify, er, hk, LogLevel } from 'node-ansi-logger';
96+
import { AnsiLogger, debugStringify, er, hk, LogLevel, nf } from 'node-ansi-logger';
9797

9898
import { MatterbridgeBooleanStateConfigurationServer } from '../src/behaviors/booleanStateConfigurationServer.js';
9999
import { MatterbridgeColorControlServer } from '../src/behaviors/colorControlServer.js';
@@ -1253,13 +1253,16 @@ describe('Matterbridge ' + NAME, () => {
12531253
await leak.setAttribute('booleanStateConfiguration', 'alarmsActive', { audible: true, visual: true });
12541254
await leak.invokeBehaviorCommand('booleanStateConfiguration', 'suppressAlarm', { alarmsToSuppress: { audible: true, visual: true } });
12551255
expect(leak.getAttribute('booleanStateConfiguration', 'alarmsSuppressed')).toEqual({ audible: true, visual: true });
1256-
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Suppressing alarm ${debugStringify({ audible: true, visual: true })} (endpoint ${leak.id}.${leak.number})`);
1256+
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Suppressing alarm ${debugStringify({ audible: true, visual: true })}${nf} (endpoint ${leak.id}.${leak.number})`);
12571257
vi.clearAllMocks();
12581258
await leak.invokeBehaviorCommand('booleanStateConfiguration', 'enableDisableAlarm', { alarmsToEnableDisable: { audible: true, visual: false } });
12591259
expect(leak.getAttribute('booleanStateConfiguration', 'alarmsActive')).toEqual({ audible: true, visual: false });
12601260
expect(leak.getAttribute('booleanStateConfiguration', 'alarmsEnabled')).toEqual({ audible: true, visual: false });
12611261
expect(leak.getAttribute('booleanStateConfiguration', 'alarmsSuppressed')).toEqual({ audible: true, visual: false });
1262-
expect(loggerLogSpy).toHaveBeenCalledWith(LogLevel.INFO, `Enabling/disabling alarm ${debugStringify({ audible: true, visual: false })} (endpoint ${leak.id}.${leak.number})`);
1262+
expect(loggerLogSpy).toHaveBeenCalledWith(
1263+
LogLevel.INFO,
1264+
`Enabling/disabling alarm ${debugStringify({ audible: true, visual: false })}${nf} (endpoint ${leak.id}.${leak.number})`,
1265+
);
12631266
});
12641267

12651268
test('invoke MatterbridgeOperationalStateServer commands', async () => {

0 commit comments

Comments
 (0)