matterbridge-example-dynamic-platform 1.3.11 → 1.3.12

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/CHANGELOG.md CHANGED
@@ -23,12 +23,29 @@ If you like this project and find it useful, please consider giving it a star on
23
23
  <img src="bmc-button.svg" alt="Buy me a coffee" width="120">
24
24
  </a>
25
25
 
26
+ ## [1.3.12] - 2025-09-14
27
+
28
+ ### Added
29
+
30
+ - [jest]: Added jest helper module v. 1.0.5.
31
+ - [rvc]: Added DustBinFull to the Rvc animation (https://github.com/Luligu/matterbridge-example-dynamic-platform/pull/31).
32
+
33
+ ### Changed
34
+
35
+ - [package]: Updated dependencies.
36
+ - [package]: Updated to Automator v. 2.0.6.
37
+ - [workflows]: Ignore any .md anywhere.
38
+
39
+ <a href="https://www.buymeacoffee.com/luligugithub">
40
+ <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
41
+ </a>
42
+
26
43
  ## [1.3.11] - 2025-09-09
27
44
 
28
45
  ### Changed
29
46
 
30
47
  - [package]: Updated dependencies.
31
- - [platform]: Removed attributes setting when is managed by the Matterbridge servers. This add support for **Apple Home Adaptive Lighting**.
48
+ - [platform]: Removed attributes setting when is managed by the Matterbridge servers. This add support for **Apple Home Adaptive Lighting**. See https://github.com/Luligu/matterbridge/discussions/390.
32
49
 
33
50
  <a href="https://www.buymeacoffee.com/luligugithub">
34
51
  <img src="bmc-button.svg" alt="Buy me a coffee" width="80">
@@ -0,0 +1,231 @@
1
+ import { rmSync } from 'node:fs';
2
+ import { inspect } from 'node:util';
3
+ import path from 'node:path';
4
+ import { jest } from '@jest/globals';
5
+ import { DeviceTypeId, Endpoint, Environment, MdnsService, ServerNode, ServerNodeStore, VendorId, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel, Lifecycle, } from 'matterbridge/matter';
6
+ import { RootEndpoint, AggregatorEndpoint } from 'matterbridge/matter/endpoints';
7
+ import { AnsiLogger } from 'matterbridge/logger';
8
+ export let loggerLogSpy;
9
+ export let consoleLogSpy;
10
+ export let consoleDebugSpy;
11
+ export let consoleInfoSpy;
12
+ export let consoleWarnSpy;
13
+ export let consoleErrorSpy;
14
+ export function setupTest(name, debug = false) {
15
+ expect(name).toBeDefined();
16
+ expect(typeof name).toBe('string');
17
+ expect(name.length).toBeGreaterThanOrEqual(4);
18
+ rmSync(path.join('jest', name), { recursive: true, force: true });
19
+ if (debug) {
20
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
21
+ consoleLogSpy = jest.spyOn(console, 'log');
22
+ consoleDebugSpy = jest.spyOn(console, 'debug');
23
+ consoleInfoSpy = jest.spyOn(console, 'info');
24
+ consoleWarnSpy = jest.spyOn(console, 'warn');
25
+ consoleErrorSpy = jest.spyOn(console, 'error');
26
+ }
27
+ else {
28
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
29
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
30
+ consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
31
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
32
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
33
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
34
+ }
35
+ }
36
+ export function setDebug(debug) {
37
+ if (debug) {
38
+ loggerLogSpy.mockRestore();
39
+ consoleLogSpy.mockRestore();
40
+ consoleDebugSpy.mockRestore();
41
+ consoleInfoSpy.mockRestore();
42
+ consoleWarnSpy.mockRestore();
43
+ consoleErrorSpy.mockRestore();
44
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
45
+ consoleLogSpy = jest.spyOn(console, 'log');
46
+ consoleDebugSpy = jest.spyOn(console, 'debug');
47
+ consoleInfoSpy = jest.spyOn(console, 'info');
48
+ consoleWarnSpy = jest.spyOn(console, 'warn');
49
+ consoleErrorSpy = jest.spyOn(console, 'error');
50
+ }
51
+ else {
52
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
53
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
54
+ consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
55
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
56
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
57
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
58
+ }
59
+ }
60
+ export function createTestEnvironment(homeDir) {
61
+ expect(homeDir).toBeDefined();
62
+ expect(typeof homeDir).toBe('string');
63
+ expect(homeDir.length).toBeGreaterThanOrEqual(4);
64
+ rmSync(homeDir, { recursive: true, force: true });
65
+ const environment = Environment.default;
66
+ environment.vars.set('log.level', MatterLogLevel.DEBUG);
67
+ environment.vars.set('log.format', MatterLogFormat.ANSI);
68
+ environment.vars.set('path.root', homeDir);
69
+ environment.vars.set('runtime.signals', false);
70
+ environment.vars.set('runtime.exitcode', false);
71
+ return environment;
72
+ }
73
+ export async function flushAsync(ticks = 3, microTurns = 10, pause = 100) {
74
+ for (let i = 0; i < ticks; i++)
75
+ await new Promise((resolve) => setImmediate(resolve));
76
+ for (let i = 0; i < microTurns; i++)
77
+ await Promise.resolve();
78
+ if (pause)
79
+ await new Promise((resolve) => setTimeout(resolve, pause));
80
+ }
81
+ export async function flushAllEndpointNumberPersistence(targetServer, rounds = 2) {
82
+ const nodeStore = targetServer.env.get(ServerNodeStore);
83
+ for (let i = 0; i < rounds; i++) {
84
+ await new Promise((resolve) => setImmediate(resolve));
85
+ await nodeStore.endpointStores.close();
86
+ }
87
+ }
88
+ function collectAllEndpoints(root) {
89
+ const list = [];
90
+ const walk = (ep) => {
91
+ list.push(ep);
92
+ if (ep.parts) {
93
+ for (const child of ep.parts) {
94
+ walk(child);
95
+ }
96
+ }
97
+ };
98
+ walk(root);
99
+ return list;
100
+ }
101
+ export async function assertAllEndpointNumbersPersisted(targetServer) {
102
+ const nodeStore = targetServer.env.get(ServerNodeStore);
103
+ await nodeStore.endpointStores.close();
104
+ const all = collectAllEndpoints(targetServer);
105
+ for (const ep of all) {
106
+ const store = nodeStore.storeForEndpoint(ep);
107
+ if (ep.maybeNumber === 0) {
108
+ expect(store.number ?? 0).toBe(0);
109
+ }
110
+ else {
111
+ expect(store.number).toBeGreaterThan(0);
112
+ }
113
+ }
114
+ return all.length;
115
+ }
116
+ export async function startServerNode(name, port) {
117
+ const server = await ServerNode.create({
118
+ id: name + 'ServerNode',
119
+ productDescription: {
120
+ name: name + 'ServerNode',
121
+ deviceType: DeviceTypeId(RootEndpoint.deviceType),
122
+ vendorId: VendorId(0xfff1),
123
+ productId: 0x8000,
124
+ },
125
+ basicInformation: {
126
+ vendorId: VendorId(0xfff1),
127
+ vendorName: 'Matterbridge',
128
+ productId: 0x8000,
129
+ productName: 'Matterbridge ' + name,
130
+ nodeLabel: name + 'ServerNode',
131
+ hardwareVersion: 1,
132
+ softwareVersion: 1,
133
+ reachable: true,
134
+ },
135
+ network: {
136
+ port,
137
+ },
138
+ });
139
+ expect(server).toBeDefined();
140
+ expect(server.lifecycle.isReady).toBeTruthy();
141
+ const aggregator = new Endpoint(AggregatorEndpoint, {
142
+ id: name + 'AggregatorNode',
143
+ });
144
+ expect(aggregator).toBeDefined();
145
+ await server.add(aggregator);
146
+ expect(server.parts.has(aggregator.id)).toBeTruthy();
147
+ expect(server.parts.has(aggregator)).toBeTruthy();
148
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
149
+ expect(server.lifecycle.isOnline).toBeFalsy();
150
+ await new Promise((resolve) => {
151
+ server.lifecycle.online.on(async () => {
152
+ resolve();
153
+ });
154
+ server.start();
155
+ });
156
+ expect(server.lifecycle.isReady).toBeTruthy();
157
+ expect(server.lifecycle.isOnline).toBeTruthy();
158
+ expect(server.lifecycle.isCommissioned).toBeFalsy();
159
+ expect(server.lifecycle.isPartsReady).toBeTruthy();
160
+ expect(server.lifecycle.hasId).toBeTruthy();
161
+ expect(server.lifecycle.hasNumber).toBeTruthy();
162
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
163
+ expect(aggregator.lifecycle.isInstalled).toBeTruthy();
164
+ expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
165
+ expect(aggregator.lifecycle.hasId).toBeTruthy();
166
+ expect(aggregator.lifecycle.hasNumber).toBeTruthy();
167
+ await flushAsync();
168
+ return [server, aggregator];
169
+ }
170
+ export async function stopServerNode(server) {
171
+ await flushAllEndpointNumberPersistence(server);
172
+ await assertAllEndpointNumbersPersisted(server);
173
+ expect(server).toBeDefined();
174
+ expect(server.lifecycle.isReady).toBeTruthy();
175
+ expect(server.lifecycle.isOnline).toBeTruthy();
176
+ await server.close();
177
+ expect(server.lifecycle.isReady).toBeTruthy();
178
+ expect(server.lifecycle.isOnline).toBeFalsy();
179
+ await server.env.get(MdnsService)[Symbol.asyncDispose]();
180
+ await flushAsync();
181
+ }
182
+ export async function addDevice(owner, device, pause = 10) {
183
+ expect(owner).toBeDefined();
184
+ expect(device).toBeDefined();
185
+ expect(owner.lifecycle.isReady).toBeTruthy();
186
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
187
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
188
+ try {
189
+ await owner.add(device);
190
+ }
191
+ catch (error) {
192
+ const errorMessage = error instanceof Error ? error.message : error;
193
+ const errorInspect = inspect(error, { depth: 10 });
194
+ console.error(`Error adding device ${device.maybeId}.${device.maybeNumber}: ${errorMessage}\nstack: ${errorInspect}`);
195
+ return false;
196
+ }
197
+ expect(owner.parts.has(device)).toBeTruthy();
198
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
199
+ expect(device.lifecycle.isReady).toBeTruthy();
200
+ expect(device.lifecycle.isInstalled).toBeTruthy();
201
+ expect(device.lifecycle.hasId).toBeTruthy();
202
+ expect(device.lifecycle.hasNumber).toBeTruthy();
203
+ expect(device.construction.status).toBe(Lifecycle.Status.Active);
204
+ await flushAsync(1, 1, pause);
205
+ return true;
206
+ }
207
+ export async function deleteDevice(owner, device, pause = 10) {
208
+ expect(owner).toBeDefined();
209
+ expect(device).toBeDefined();
210
+ expect(owner.lifecycle.isReady).toBeTruthy();
211
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
212
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
213
+ try {
214
+ await device.delete();
215
+ }
216
+ catch (error) {
217
+ const errorMessage = error instanceof Error ? error.message : error;
218
+ const errorInspect = inspect(error, { depth: 10 });
219
+ console.error(`Error deleting device ${device.maybeId}.${device.maybeNumber}: ${errorMessage}\nstack: ${errorInspect}`);
220
+ return false;
221
+ }
222
+ expect(owner.parts.has(device)).toBeFalsy();
223
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
224
+ expect(device.lifecycle.isReady).toBeFalsy();
225
+ expect(device.lifecycle.isInstalled).toBeFalsy();
226
+ expect(device.lifecycle.hasId).toBeTruthy();
227
+ expect(device.lifecycle.hasNumber).toBeTruthy();
228
+ expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
229
+ await flushAsync(1, 1, pause);
230
+ return true;
231
+ }
package/dist/platform.js CHANGED
@@ -1195,6 +1195,7 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1195
1195
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1196
1196
  await this.roboticVacuum.setAttribute('RvcCleanMode', 'currentMode', 1, this.roboticVacuum.log);
1197
1197
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Docked, this.roboticVacuum.log);
1198
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1198
1199
  }
1199
1200
  if (this.phase === 1) {
1200
1201
  this.roboticVacuum.log.info(`RVC: start cleaning...`);
@@ -1202,6 +1203,7 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1202
1203
  await this.roboticVacuum.setAttribute('PowerSource', 'batVoltage', 5900, this.roboticVacuum.log);
1203
1204
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 2, this.roboticVacuum.log);
1204
1205
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Running, this.roboticVacuum.log);
1206
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1205
1207
  await this.roboticVacuum.setAttribute('ServiceArea', 'currentArea', 1, this.roboticVacuum.log);
1206
1208
  await this.roboticVacuum.setAttribute('ServiceArea', 'estimatedEndTime', Math.floor(Date.now() / 1000) + 300, this.roboticVacuum.log);
1207
1209
  }
@@ -1210,11 +1212,13 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1210
1212
  await this.roboticVacuum.setAttribute('PowerSource', 'batPercentRemaining', 180, this.roboticVacuum.log);
1211
1213
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 2, this.roboticVacuum.log);
1212
1214
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Paused, this.roboticVacuum.log);
1215
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1213
1216
  }
1214
1217
  if (this.phase === 3) {
1215
1218
  this.roboticVacuum.log.info(`RVC: resume cleaning...`);
1216
1219
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 2, this.roboticVacuum.log);
1217
1220
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Running, this.roboticVacuum.log);
1221
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1218
1222
  await this.roboticVacuum.setAttribute('ServiceArea', 'currentArea', 2, this.roboticVacuum.log);
1219
1223
  await this.roboticVacuum.setAttribute('ServiceArea', 'estimatedEndTime', Math.floor(Date.now() / 1000) + 180, this.roboticVacuum.log);
1220
1224
  }
@@ -1223,12 +1227,14 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1223
1227
  await this.roboticVacuum.setAttribute('PowerSource', 'batPercentRemaining', 160, this.roboticVacuum.log);
1224
1228
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1225
1229
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Stopped, this.roboticVacuum.log);
1230
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1226
1231
  await this.roboticVacuum.setAttribute('ServiceArea', 'estimatedEndTime', 0, this.roboticVacuum.log);
1227
1232
  }
1228
1233
  if (this.phase === 5) {
1229
1234
  this.roboticVacuum.log.info(`RVC: going home...`);
1230
1235
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1231
1236
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.SeekingCharger, this.roboticVacuum.log);
1237
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1232
1238
  }
1233
1239
  if (this.phase === 6) {
1234
1240
  this.roboticVacuum.log.info(`RVC: charging...`);
@@ -1237,6 +1243,7 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1237
1243
  await this.roboticVacuum.setAttribute('PowerSource', 'batVoltage', 6100, this.roboticVacuum.log);
1238
1244
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1239
1245
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Charging, this.roboticVacuum.log);
1246
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1240
1247
  await this.roboticVacuum.setAttribute('ServiceArea', 'currentArea', 1, this.roboticVacuum.log);
1241
1248
  }
1242
1249
  if (this.phase === 7) {
@@ -1244,6 +1251,7 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1244
1251
  await this.roboticVacuum.setAttribute('PowerSource', 'batPercentRemaining', 190, this.roboticVacuum.log);
1245
1252
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1246
1253
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Charging, this.roboticVacuum.log);
1254
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1247
1255
  }
1248
1256
  if (this.phase === 8) {
1249
1257
  this.roboticVacuum.log.info(`RVC: docked...`);
@@ -1252,6 +1260,16 @@ export class ExampleMatterbridgeDynamicPlatform extends MatterbridgeDynamicPlatf
1252
1260
  await this.roboticVacuum.setAttribute('PowerSource', 'batVoltage', 6000, this.roboticVacuum.log);
1253
1261
  await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1254
1262
  await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Docked, this.roboticVacuum.log);
1263
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.NoError }, this.roboticVacuum.log);
1264
+ }
1265
+ if (this.phase === 9) {
1266
+ this.roboticVacuum.log.info(`RVC: error DustBinFull...`);
1267
+ await this.roboticVacuum.setAttribute('PowerSource', 'batPercentRemaining', 200, this.roboticVacuum.log);
1268
+ await this.roboticVacuum.setAttribute('PowerSource', 'batChargeState', PowerSource.BatChargeState.IsAtFullCharge, this.roboticVacuum.log);
1269
+ await this.roboticVacuum.setAttribute('PowerSource', 'batVoltage', 6000, this.roboticVacuum.log);
1270
+ await this.roboticVacuum.setAttribute('RvcRunMode', 'currentMode', 1, this.roboticVacuum.log);
1271
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalState', RvcOperationalState.OperationalState.Error, this.roboticVacuum.log);
1272
+ await this.roboticVacuum.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: RvcOperationalState.ErrorState.DustBinFull }, this.roboticVacuum.log);
1255
1273
  }
1256
1274
  }
1257
1275
  if (this.oven) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "matterbridge-example-dynamic-platform",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "matterbridge-example-dynamic-platform",
9
- "version": "1.3.11",
9
+ "version": "1.3.12",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "node-ansi-logger": "3.1.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "matterbridge-example-dynamic-platform",
3
- "version": "1.3.11",
3
+ "version": "1.3.12",
4
4
  "description": "Matterbridge dynamic plugin",
5
5
  "author": "https://github.com/Luligu",
6
6
  "license": "Apache-2.0",