matterbridge 3.2.7-dev-20250908-3bb699e → 3.2.7-dev-20250913-9d0d095

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
@@ -8,7 +8,7 @@ If you like this project and find it useful, please consider giving it a star on
8
8
  <img src="bmc-button.svg" alt="Buy me a coffee" width="120">
9
9
  </a>
10
10
 
11
- ## [3.2.7] - 2025-09-??
11
+ ## [3.2.7] - 2025-09-12
12
12
 
13
13
  ### Breaking Changes
14
14
 
@@ -17,10 +17,16 @@ If you like this project and find it useful, please consider giving it a star on
17
17
 
18
18
  ### Added
19
19
 
20
+ - [jest]: Added Jest helpers module.
21
+ - [colorControl]: Added createEnhancedColorControlClusterServer (provisional to run compatibility tests on all controllers).
22
+ - [frontend]: Bumped `frontend` version to 2.7.6.
23
+ - [frontend]: Added api/view-diagnostic.
24
+ - [frontend]: Refactored the QRCode component for device with mode='server' (e.g. the Rvcs): added turn on and off pairing mode, resend mDns advertise, remove single fabrics, formatted manual paring code, copy to clipboard the manual pairing code and is fully web socket based. The main QRCode panel will have the same features (bridge mode and childbridge mode) in the next release.
25
+
20
26
  ### Changed
21
27
 
22
28
  - [package]: Updated dependencies.
23
- - [matterbridge.io]: Updated site.
29
+ - [matterbridge.io]: Updated web site [matterbridge.io](matterbridge.io).
24
30
 
25
31
  ### Fixed
26
32
 
package/dist/frontend.js CHANGED
@@ -2,15 +2,17 @@ import { createServer } from 'node:http';
2
2
  import https from 'node:https';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { existsSync, promises as fs } from 'node:fs';
5
+ import { existsSync, promises as fs, unlinkSync } from 'node:fs';
6
6
  import EventEmitter from 'node:events';
7
+ import { appendFile } from 'node:fs/promises';
7
8
  import express from 'express';
8
9
  import WebSocket, { WebSocketServer } from 'ws';
9
10
  import multer from 'multer';
10
11
  import { AnsiLogger, stringify, debugStringify, CYAN, db, er, nf, rs, UNDERLINE, UNDERLINEOFF, YELLOW, nt } from 'node-ansi-logger';
11
- import { Logger, LogLevel as MatterLogLevel, Lifecycle } from '@matter/main';
12
+ import { Logger, LogLevel as MatterLogLevel, LogFormat as MatterLogFormat, Lifecycle, LogDestination, Diagnostic, Time, FabricIndex } from '@matter/main';
12
13
  import { BridgedDeviceBasicInformation, PowerSource } from '@matter/main/clusters';
13
- import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter } from './utils/export.js';
14
+ import { DeviceAdvertiser, DeviceCommissioner, FabricManager } from '@matter/main/protocol';
15
+ import { createZip, isValidArray, isValidNumber, isValidObject, isValidString, isValidBoolean, withTimeout, hasParameter, wait, inspectError } from './utils/export.js';
14
16
  import { plg } from './matterbridgeTypes.js';
15
17
  import { capitalizeFirstLetter, getAttribute } from './matterbridgeEndpointHelpers.js';
16
18
  import { cliEmitter, lastCpuUsage } from './cliEmitter.js';
@@ -354,6 +356,52 @@ export class Frontend extends EventEmitter {
354
356
  res.status(500).send('Error reading matter log file. Please enable the matter log on file in the settings.');
355
357
  }
356
358
  });
359
+ this.expressApp.get('/api/view-diagnostic', async (req, res) => {
360
+ this.log.debug('The frontend sent /api/view-diagnostic');
361
+ const serverNodes = [];
362
+ if (this.matterbridge.bridgeMode === 'bridge') {
363
+ if (this.matterbridge.serverNode)
364
+ serverNodes.push(this.matterbridge.serverNode);
365
+ }
366
+ else if (this.matterbridge.bridgeMode === 'childbridge') {
367
+ for (const plugin of this.matterbridge.getPlugins()) {
368
+ if (plugin.serverNode)
369
+ serverNodes.push(plugin.serverNode);
370
+ }
371
+ }
372
+ for (const device of this.matterbridge.getDevices()) {
373
+ if (device.serverNode)
374
+ serverNodes.push(device.serverNode);
375
+ }
376
+ if (existsSync(path.join(this.matterbridge.matterbridgeDirectory, 'diagnostic.log')))
377
+ unlinkSync(path.join(this.matterbridge.matterbridgeDirectory, 'diagnostic.log'));
378
+ const diagnosticDestination = LogDestination({ name: 'diagnostic', level: MatterLogLevel.INFO, format: MatterLogFormat.formats.plain });
379
+ diagnosticDestination.write = async (text, _message) => {
380
+ await appendFile(path.join(this.matterbridge.matterbridgeDirectory, 'diagnostic.log'), text + '\n', { encoding: 'utf8' });
381
+ };
382
+ Logger.destinations.diagnostic = diagnosticDestination;
383
+ if (!diagnosticDestination.context) {
384
+ diagnosticDestination.context = Diagnostic.Context();
385
+ }
386
+ diagnosticDestination.context.run(() => diagnosticDestination.add(Diagnostic.message({
387
+ now: Time.now(),
388
+ facility: 'Server nodes:',
389
+ level: MatterLogLevel.INFO,
390
+ prefix: Logger.nestingLevel ? '⎸'.padEnd(Logger.nestingLevel * 2) : '',
391
+ values: [...serverNodes],
392
+ })));
393
+ delete Logger.destinations.diagnostic;
394
+ await wait(500);
395
+ try {
396
+ const data = await fs.readFile(path.join(this.matterbridge.matterbridgeDirectory, 'diagnostic.log'), 'utf8');
397
+ res.type('text/plain');
398
+ res.send(data.slice(29));
399
+ }
400
+ catch (error) {
401
+ this.log.error(`Error reading diagnostic log file ${this.matterbridge.matterLoggerFile}: ${error instanceof Error ? error.message : error}`);
402
+ res.status(500).send('Error reading diagnostic log file.');
403
+ }
404
+ });
357
405
  this.expressApp.get('/api/shellyviewsystemlog', async (req, res) => {
358
406
  this.log.debug('The frontend sent /api/shellyviewsystemlog');
359
407
  try {
@@ -667,14 +715,7 @@ export class Frontend extends EventEmitter {
667
715
  }
668
716
  getMatterDataFromDevice(device) {
669
717
  if (device.mode === 'server' && device.serverNode) {
670
- return {
671
- commissioned: device.serverNode.state.commissioning.commissioned,
672
- qrPairingCode: device.serverNode.state.commissioning.pairingCodes.qrPairingCode,
673
- manualPairingCode: device.serverNode.state.commissioning.pairingCodes.manualPairingCode,
674
- fabricInformations: this.matterbridge.sanitizeFabricInformations(Object.values(device.serverNode.state.commissioning.fabrics)),
675
- sessionInformations: this.matterbridge.sanitizeSessionInformation(Object.values(device.serverNode.state.sessions.sessions)),
676
- serialNumber: device.serverNode.state.basicInformation.serialNumber,
677
- };
718
+ return this.matterbridge.getServerNodeData(device.serverNode);
678
719
  }
679
720
  }
680
721
  getClusterTextFromDevice(device) {
@@ -1267,6 +1308,50 @@ export class Frontend extends EventEmitter {
1267
1308
  this.wssSendSnackbarMessage(`Stopped fabrics share`, 0);
1268
1309
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
1269
1310
  }
1311
+ else if (data.method === '/api/matter') {
1312
+ if (!isValidString(data.params.id)) {
1313
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Wrong parameter id in /api/matter' }));
1314
+ return;
1315
+ }
1316
+ let serverNode;
1317
+ if (data.params.id === 'Matterbridge')
1318
+ serverNode = this.matterbridge.serverNode;
1319
+ else
1320
+ serverNode = this.matterbridge.getPlugins().find((p) => p.serverNode && p.serverNode.id === data.params.id)?.serverNode || this.matterbridge.getDevices().find((d) => d.serverNode && d.serverNode.id === data.params.id)?.serverNode;
1321
+ if (!serverNode) {
1322
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, error: 'Unknown server node id in /api/matter' }));
1323
+ return;
1324
+ }
1325
+ this.log.debug(`*Server node ${serverNode.id}: commissioned ${serverNode.state.commissioning.commissioned} upTime ${serverNode.state.generalDiagnostics.upTime}.`);
1326
+ if (data.params.server) {
1327
+ this.log.debug(`*Sending data for node ${data.params.id}`);
1328
+ this.wssSendRefreshRequired('matter', { matter: { ...this.matterbridge.getServerNodeData(serverNode) } });
1329
+ }
1330
+ if (data.params.commission) {
1331
+ await serverNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
1332
+ this.matterbridge.advertisingNodes.set(serverNode.id, Date.now());
1333
+ this.log.debug(`*Commissioning has been sent for node ${data.params.id}`);
1334
+ this.wssSendRefreshRequired('matter', { matter: { ...this.matterbridge.getServerNodeData(serverNode), advertising: true } });
1335
+ }
1336
+ if (data.params.stopCommission) {
1337
+ await serverNode.env.get(DeviceCommissioner)?.endCommissioning();
1338
+ this.matterbridge.advertisingNodes.delete(serverNode.id);
1339
+ this.log.debug(`*Stop commissioning has been sent for node ${data.params.id}`);
1340
+ this.wssSendRefreshRequired('matter', { matter: { ...this.matterbridge.getServerNodeData(serverNode), advertising: false } });
1341
+ }
1342
+ if (data.params.advertise) {
1343
+ await serverNode.env.get(DeviceAdvertiser)?.advertise(true);
1344
+ this.log.debug(`*Advertising has been sent for node ${data.params.id}`);
1345
+ this.wssSendRefreshRequired('matter', { matter: { ...this.matterbridge.getServerNodeData(serverNode), advertising: true } });
1346
+ }
1347
+ if (data.params.removeFabric) {
1348
+ if (serverNode.env.get(FabricManager).has(FabricIndex(data.params.removeFabric)))
1349
+ await serverNode.env.get(FabricManager).removeFabric(FabricIndex(data.params.removeFabric));
1350
+ this.log.debug(`*Removed fabric index ${data.params.removeFabric} for node ${data.params.id}`);
1351
+ this.wssSendRefreshRequired('matter', { matter: { ...this.matterbridge.getServerNodeData(serverNode) } });
1352
+ }
1353
+ client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, success: true }));
1354
+ }
1270
1355
  else if (data.method === '/api/settings') {
1271
1356
  client.send(JSON.stringify({ id: data.id, method: data.method, src: 'Matterbridge', dst: data.src, response: await this.getApiSettings() }));
1272
1357
  }
@@ -1651,7 +1736,7 @@ export class Frontend extends EventEmitter {
1651
1736
  }
1652
1737
  }
1653
1738
  catch (error) {
1654
- this.log.error(`Error parsing message "${message}" from websocket client:`, error instanceof Error ? error.message : error);
1739
+ inspectError(this.log, `Error processing message "${message}" from websocket client`, error);
1655
1740
  }
1656
1741
  }
1657
1742
  wssSendMessage(level, time, name, message) {
@@ -1681,11 +1766,11 @@ export class Frontend extends EventEmitter {
1681
1766
  }
1682
1767
  });
1683
1768
  }
1684
- wssSendRefreshRequired(changed = null) {
1769
+ wssSendRefreshRequired(changed = null, params = {}) {
1685
1770
  this.log.debug('Sending a refresh required message to all connected clients');
1686
1771
  this.webSocketServer?.clients.forEach((client) => {
1687
1772
  if (client.readyState === WebSocket.OPEN) {
1688
- client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed } }));
1773
+ client.send(JSON.stringify({ id: WS_ID_REFRESH_NEEDED, src: 'Matterbridge', dst: 'Frontend', method: 'refresh_required', params: { changed: changed, ...params } }));
1689
1774
  }
1690
1775
  });
1691
1776
  }
@@ -140,6 +140,7 @@ export class Matterbridge extends EventEmitter {
140
140
  aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
141
141
  aggregatorSerialNumber = getParameter('serialNumber');
142
142
  aggregatorUniqueId = getParameter('uniqueId');
143
+ advertisingNodes = new Map();
143
144
  static instance;
144
145
  constructor() {
145
146
  super();
@@ -1394,15 +1395,15 @@ export class Matterbridge extends EventEmitter {
1394
1395
  this.matterbridgeContext = undefined;
1395
1396
  this.log.info('Matter node storage closed');
1396
1397
  }
1397
- async createServerNodeContext(pluginName, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
1398
+ async createServerNodeContext(storeId, deviceName, deviceType, vendorId, vendorName, productId, productName, serialNumber, uniqueId) {
1398
1399
  const { randomBytes } = await import('node:crypto');
1399
1400
  if (!this.matterStorageService)
1400
1401
  throw new Error('No storage service initialized');
1401
- this.log.info(`Creating server node storage context "${pluginName}.persist" for ${pluginName}...`);
1402
- const storageManager = await this.matterStorageService.open(pluginName);
1402
+ this.log.info(`Creating server node storage context "${storeId}.persist" for ${storeId}...`);
1403
+ const storageManager = await this.matterStorageService.open(storeId);
1403
1404
  const storageContext = storageManager.createContext('persist');
1404
1405
  const random = randomBytes(8).toString('hex');
1405
- await storageContext.set('storeId', pluginName);
1406
+ await storageContext.set('storeId', storeId);
1406
1407
  await storageContext.set('deviceName', deviceName);
1407
1408
  await storageContext.set('deviceType', deviceType);
1408
1409
  await storageContext.set('vendorId', vendorId);
@@ -1417,10 +1418,16 @@ export class Matterbridge extends EventEmitter {
1417
1418
  await storageContext.set('softwareVersionString', isValidString(this.matterbridgeVersion, 5, 64) ? this.matterbridgeVersion : '1.0.0');
1418
1419
  await storageContext.set('hardwareVersion', isValidNumber(parseVersionString(this.systemInformation.osRelease), 0, UINT16_MAX) ? parseVersionString(this.systemInformation.osRelease) : 1);
1419
1420
  await storageContext.set('hardwareVersionString', isValidString(this.systemInformation.osRelease, 5, 64) ? this.systemInformation.osRelease : '1.0.0');
1420
- this.log.debug(`Created server node storage context "${pluginName}.persist" for ${pluginName}:`);
1421
+ this.log.debug(`Created server node storage context "${storeId}.persist" for ${storeId}:`);
1421
1422
  this.log.debug(`- storeId: ${await storageContext.get('storeId')}`);
1422
1423
  this.log.debug(`- deviceName: ${await storageContext.get('deviceName')}`);
1423
1424
  this.log.debug(`- deviceType: ${await storageContext.get('deviceType')}(0x${(await storageContext.get('deviceType'))?.toString(16).padStart(4, '0')})`);
1425
+ this.log.debug(`- vendorId: ${await storageContext.get('vendorId')}`);
1426
+ this.log.debug(`- vendorName: ${await storageContext.get('vendorName')}`);
1427
+ this.log.debug(`- productId: ${await storageContext.get('productId')}`);
1428
+ this.log.debug(`- productName: ${await storageContext.get('productName')}`);
1429
+ this.log.debug(`- nodeLabel: ${await storageContext.get('nodeLabel')}`);
1430
+ this.log.debug(`- productLabel: ${await storageContext.get('productLabel')}`);
1424
1431
  this.log.debug(`- serialNumber: ${await storageContext.get('serialNumber')}`);
1425
1432
  this.log.debug(`- uniqueId: ${await storageContext.get('uniqueId')}`);
1426
1433
  this.log.debug(`- softwareVersion: ${await storageContext.get('softwareVersion')} softwareVersionString: ${await storageContext.get('softwareVersionString')}`);
@@ -1486,12 +1493,15 @@ export class Matterbridge extends EventEmitter {
1486
1493
  this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
1487
1494
  this.log.notice(`Manual pairing code: ${manualPairingCode}`);
1488
1495
  this.startEndAdvertiseTimer(serverNode);
1496
+ this.advertisingNodes.set(storeId, Date.now());
1489
1497
  }
1490
1498
  else {
1491
1499
  this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
1500
+ this.advertisingNodes.delete(storeId);
1492
1501
  }
1493
1502
  this.frontend.wssSendRefreshRequired('plugins');
1494
1503
  this.frontend.wssSendRefreshRequired('settings');
1504
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1495
1505
  this.frontend.wssSendSnackbarMessage(`${storeId} is online`, 5, 'success');
1496
1506
  this.emit('online', storeId);
1497
1507
  });
@@ -1500,6 +1510,7 @@ export class Matterbridge extends EventEmitter {
1500
1510
  this.matterbridgeInformation.matterbridgeEndAdvertise = true;
1501
1511
  this.frontend.wssSendRefreshRequired('plugins');
1502
1512
  this.frontend.wssSendRefreshRequired('settings');
1513
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1503
1514
  this.frontend.wssSendSnackbarMessage(`${storeId} is offline`, 5, 'warning');
1504
1515
  this.emit('offline', storeId);
1505
1516
  });
@@ -1507,6 +1518,10 @@ export class Matterbridge extends EventEmitter {
1507
1518
  let action = '';
1508
1519
  switch (fabricAction) {
1509
1520
  case FabricAction.Added:
1521
+ this.advertisingNodes.delete(storeId);
1522
+ clearTimeout(this.endAdvertiseTimeout);
1523
+ this.endAdvertiseTimeout = undefined;
1524
+ this.matterbridgeInformation.matterbridgeEndAdvertise = true;
1510
1525
  action = 'added';
1511
1526
  break;
1512
1527
  case FabricAction.Removed:
@@ -1518,18 +1533,22 @@ export class Matterbridge extends EventEmitter {
1518
1533
  }
1519
1534
  this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
1520
1535
  this.frontend.wssSendRefreshRequired('fabrics');
1536
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1521
1537
  });
1522
1538
  serverNode.events.sessions.opened.on((session) => {
1523
1539
  this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
1524
1540
  this.frontend.wssSendRefreshRequired('sessions');
1541
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1525
1542
  });
1526
1543
  serverNode.events.sessions.closed.on((session) => {
1527
1544
  this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
1528
1545
  this.frontend.wssSendRefreshRequired('sessions');
1546
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1529
1547
  });
1530
1548
  serverNode.events.sessions.subscriptionsChanged.on((session) => {
1531
1549
  this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
1532
1550
  this.frontend.wssSendRefreshRequired('sessions');
1551
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(serverNode) } });
1533
1552
  });
1534
1553
  this.log.info(`Created server node for ${storeId}`);
1535
1554
  return serverNode;
@@ -1548,10 +1567,26 @@ export class Matterbridge extends EventEmitter {
1548
1567
  this.frontend.wssSendRefreshRequired('settings');
1549
1568
  this.frontend.wssSendRefreshRequired('fabrics');
1550
1569
  this.frontend.wssSendRefreshRequired('sessions');
1551
- this.frontend.wssSendSnackbarMessage(`Advertising stopped. Restart to commission again.`, 0);
1552
- this.log.notice(`Advertising stopped. Restart to commission again.`);
1570
+ this.frontend.wssSendRefreshRequired('matter', { matter: { ...this.getServerNodeData(matterServerNode) } });
1571
+ this.frontend.wssSendSnackbarMessage(`Advertising stopped.`, 0);
1572
+ this.log.notice(`Advertising stopped.`);
1553
1573
  }, 15 * 60 * 1000).unref();
1554
1574
  }
1575
+ getServerNodeData(serverNode) {
1576
+ const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
1577
+ return {
1578
+ id: serverNode.id,
1579
+ commissioned: serverNode.state.commissioning.commissioned,
1580
+ advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
1581
+ advertiseTime,
1582
+ windowStatus: serverNode.state.administratorCommissioning.windowStatus,
1583
+ qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
1584
+ manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
1585
+ fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
1586
+ sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
1587
+ serialNumber: serverNode.state.basicInformation.serialNumber,
1588
+ };
1589
+ }
1555
1590
  async startServerNode(matterServerNode) {
1556
1591
  if (!matterServerNode)
1557
1592
  return;
@@ -1574,6 +1609,7 @@ export class Matterbridge extends EventEmitter {
1574
1609
  if (matterServerNode) {
1575
1610
  await matterServerNode.env.get(DeviceCommissioner)?.allowBasicCommissioning();
1576
1611
  const { qrPairingCode, manualPairingCode } = matterServerNode.state.commissioning.pairingCodes;
1612
+ this.advertisingNodes.set(matterServerNode.id, Date.now());
1577
1613
  this.log.notice(`Started advertising for ${matterServerNode.id} with the following pairing codes: qrPairingCode ${qrPairingCode}, manualPairingCode ${manualPairingCode}`);
1578
1614
  return { qrPairingCode, manualPairingCode };
1579
1615
  }
@@ -1581,6 +1617,7 @@ export class Matterbridge extends EventEmitter {
1581
1617
  async stopAdvertiseServerNode(matterServerNode) {
1582
1618
  if (matterServerNode && matterServerNode.lifecycle.isOnline) {
1583
1619
  await matterServerNode.env.get(DeviceCommissioner)?.endCommissioning();
1620
+ this.advertisingNodes.delete(matterServerNode.id);
1584
1621
  this.log.notice(`Stopped advertising for ${matterServerNode.id}`);
1585
1622
  }
1586
1623
  }
@@ -135,6 +135,57 @@ export class MatterbridgeColorControlServer extends ColorControlServer.with(Colo
135
135
  super.moveToColorTemperature(request);
136
136
  }
137
137
  }
138
+ export class MatterbridgeEnhancedColorControlServer extends ColorControlServer.with(ColorControl.Feature.HueSaturation, ColorControl.Feature.EnhancedHue, ColorControl.Feature.Xy, ColorControl.Feature.ColorTemperature) {
139
+ moveToHue(request) {
140
+ const device = this.endpoint.stateOf(MatterbridgeServer);
141
+ device.log.info(`Setting hue to ${request.hue} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
142
+ device.commandHandler.executeHandler('moveToHue', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
143
+ device.log.debug(`MatterbridgeColorControlServer: moveToHue called`);
144
+ super.moveToHue(request);
145
+ }
146
+ enhancedMoveToHue(request) {
147
+ const device = this.endpoint.stateOf(MatterbridgeServer);
148
+ device.log.info(`Setting enhanced hue to ${request.enhancedHue} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
149
+ device.commandHandler.executeHandler('enhancedMoveToHue', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
150
+ device.log.debug(`MatterbridgeColorControlServer: enhancedMoveToHue called`);
151
+ super.enhancedMoveToHue(request);
152
+ }
153
+ moveToSaturation(request) {
154
+ const device = this.endpoint.stateOf(MatterbridgeServer);
155
+ device.log.info(`Setting saturation to ${request.saturation} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
156
+ device.commandHandler.executeHandler('moveToSaturation', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
157
+ device.log.debug(`MatterbridgeColorControlServer: moveToSaturation called`);
158
+ super.moveToSaturation(request);
159
+ }
160
+ moveToHueAndSaturation(request) {
161
+ const device = this.endpoint.stateOf(MatterbridgeServer);
162
+ device.log.info(`Setting hue to ${request.hue} and saturation to ${request.saturation} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
163
+ device.commandHandler.executeHandler('moveToHueAndSaturation', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
164
+ device.log.debug(`MatterbridgeColorControlServer: moveToHueAndSaturation called`);
165
+ super.moveToHueAndSaturation(request);
166
+ }
167
+ enhancedMoveToHueAndSaturation(request) {
168
+ const device = this.endpoint.stateOf(MatterbridgeServer);
169
+ device.log.info(`Setting enhanced hue to ${request.enhancedHue} and saturation to ${request.saturation} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
170
+ device.commandHandler.executeHandler('enhancedMoveToHueAndSaturation', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
171
+ device.log.debug(`MatterbridgeColorControlServer: enhancedMoveToHueAndSaturation called`);
172
+ super.enhancedMoveToHueAndSaturation(request);
173
+ }
174
+ moveToColor(request) {
175
+ const device = this.endpoint.stateOf(MatterbridgeServer);
176
+ device.log.info(`Setting color to ${request.colorX}, ${request.colorY} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
177
+ device.commandHandler.executeHandler('moveToColor', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
178
+ device.log.debug(`MatterbridgeColorControlServer: moveToColor called`);
179
+ super.moveToColor(request);
180
+ }
181
+ moveToColorTemperature(request) {
182
+ const device = this.endpoint.stateOf(MatterbridgeServer);
183
+ device.log.info(`Setting color temperature to ${request.colorTemperatureMireds} with transitionTime ${request.transitionTime} (endpoint ${this.endpoint.maybeId}.${this.endpoint.maybeNumber})`);
184
+ device.commandHandler.executeHandler('moveToColorTemperature', { request, cluster: ColorControlServer.id, attributes: this.state, endpoint: this.endpoint });
185
+ device.log.debug(`MatterbridgeColorControlServer: moveToColorTemperature called`);
186
+ super.moveToColorTemperature(request);
187
+ }
188
+ }
138
189
  export class MatterbridgeLiftWindowCoveringServer extends WindowCoveringServer.with(WindowCovering.Feature.Lift, WindowCovering.Feature.PositionAwareLift) {
139
190
  upOrOpen() {
140
191
  const device = this.endpoint.stateOf(MatterbridgeServer);
@@ -59,7 +59,7 @@ import { ResourceMonitoring } from '@matter/main/clusters/resource-monitoring';
59
59
  import { ThermostatUserInterfaceConfigurationServer } from '@matter/main/behaviors/thermostat-user-interface-configuration';
60
60
  import { AnsiLogger, CYAN, YELLOW, db, debugStringify, hk, or, zb } from './logger/export.js';
61
61
  import { isValidNumber, isValidObject, isValidString } from './utils/export.js';
62
- import { MatterbridgeServer, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeLiftTiltWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeActivatedCarbonFilterMonitoringServer, MatterbridgeHepaFilterMonitoringServer, } from './matterbridgeBehaviors.js';
62
+ import { MatterbridgeServer, MatterbridgeIdentifyServer, MatterbridgeOnOffServer, MatterbridgeLevelControlServer, MatterbridgeColorControlServer, MatterbridgeLiftWindowCoveringServer, MatterbridgeLiftTiltWindowCoveringServer, MatterbridgeThermostatServer, MatterbridgeFanControlServer, MatterbridgeDoorLockServer, MatterbridgeModeSelectServer, MatterbridgeValveConfigurationAndControlServer, MatterbridgeSmokeCoAlarmServer, MatterbridgeBooleanStateConfigurationServer, MatterbridgeSwitchServer, MatterbridgeOperationalStateServer, MatterbridgeDeviceEnergyManagementModeServer, MatterbridgeDeviceEnergyManagementServer, MatterbridgeActivatedCarbonFilterMonitoringServer, MatterbridgeHepaFilterMonitoringServer, MatterbridgeEnhancedColorControlServer, } from './matterbridgeBehaviors.js';
63
63
  import { addClusterServers, addFixedLabel, addOptionalClusterServers, addRequiredClusterServers, addUserLabel, createUniqueId, getBehavior, getBehaviourTypesFromClusterClientIds, getBehaviourTypesFromClusterServerIds, getDefaultOperationalStateClusterServer, getDefaultFlowMeasurementClusterServer, getDefaultIlluminanceMeasurementClusterServer, getDefaultPressureMeasurementClusterServer, getDefaultRelativeHumidityMeasurementClusterServer, getDefaultTemperatureMeasurementClusterServer, getDefaultOccupancySensingClusterServer, lowercaseFirstLetter, updateAttribute, getClusterId, getAttributeId, setAttribute, getAttribute, checkNotLatinCharacters, generateUniqueId, subscribeAttribute, invokeBehaviorCommand, triggerEvent, featuresFor, } from './matterbridgeEndpointHelpers.js';
64
64
  export class MatterbridgeEndpoint extends Endpoint {
65
65
  static logLevel = "info";
@@ -586,6 +586,29 @@ export class MatterbridgeEndpoint extends Endpoint {
586
586
  });
587
587
  return this;
588
588
  }
589
+ createEnhancedColorControlClusterServer(currentX = 0, currentY = 0, enhancedCurrentHue = 0, currentSaturation = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
590
+ this.behaviors.require(MatterbridgeEnhancedColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.HueSaturation, ColorControl.Feature.EnhancedHue, ColorControl.Feature.ColorTemperature), {
591
+ colorMode: ColorControl.ColorMode.CurrentHueAndCurrentSaturation,
592
+ enhancedColorMode: ColorControl.EnhancedColorMode.EnhancedCurrentHueAndCurrentSaturation,
593
+ colorCapabilities: { xy: true, hueSaturation: true, colorLoop: false, enhancedHue: true, colorTemperature: true },
594
+ options: {
595
+ executeIfOff: false,
596
+ },
597
+ numberOfPrimaries: null,
598
+ currentX,
599
+ currentY,
600
+ currentHue: Math.round((enhancedCurrentHue / 65535) * 254),
601
+ enhancedCurrentHue,
602
+ currentSaturation,
603
+ colorTemperatureMireds,
604
+ colorTempPhysicalMinMireds,
605
+ colorTempPhysicalMaxMireds,
606
+ coupleColorTempToLevelMinMireds: colorTempPhysicalMinMireds,
607
+ startUpColorTemperatureMireds: null,
608
+ remainingTime: 0,
609
+ });
610
+ return this;
611
+ }
589
612
  createXyColorControlClusterServer(currentX = 0, currentY = 0, colorTemperatureMireds = 500, colorTempPhysicalMinMireds = 147, colorTempPhysicalMaxMireds = 500) {
590
613
  this.behaviors.require(MatterbridgeColorControlServer.with(ColorControl.Feature.Xy, ColorControl.Feature.ColorTemperature), {
591
614
  colorMode: ColorControl.ColorMode.CurrentXAndCurrentY,
@@ -645,8 +668,8 @@ export class MatterbridgeEndpoint extends Endpoint {
645
668
  return this;
646
669
  }
647
670
  async configureColorControlMode(colorMode) {
648
- if (isValidNumber(colorMode, ColorControl.ColorMode.CurrentHueAndCurrentSaturation, ColorControl.ColorMode.ColorTemperatureMireds)) {
649
- await this.setAttribute(ColorControl.Cluster.id, 'colorMode', colorMode, this.log);
671
+ if (isValidNumber(colorMode, ColorControl.EnhancedColorMode.CurrentHueAndCurrentSaturation, ColorControl.EnhancedColorMode.EnhancedCurrentHueAndCurrentSaturation)) {
672
+ await this.setAttribute(ColorControl.Cluster.id, 'colorMode', colorMode === ColorControl.EnhancedColorMode.EnhancedCurrentHueAndCurrentSaturation ? ColorControl.ColorMode.CurrentHueAndCurrentSaturation : colorMode, this.log);
650
673
  await this.setAttribute(ColorControl.Cluster.id, 'enhancedColorMode', colorMode, this.log);
651
674
  }
652
675
  }
@@ -1,8 +1,76 @@
1
1
  import { rmSync } from 'node:fs';
2
2
  import { inspect } from 'node:util';
3
+ import path from 'node:path';
4
+ import { jest } from '@jest/globals';
3
5
  import { DeviceTypeId, Endpoint, Environment, ServerNode, ServerNodeStore, VendorId, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel, Lifecycle } from '@matter/main';
4
6
  import { AggregatorEndpoint, RootEndpoint } from '@matter/main/endpoints';
5
7
  import { MdnsService } from '@matter/main/protocol';
8
+ import { AnsiLogger } from 'node-ansi-logger';
9
+ export let loggerLogSpy;
10
+ export let consoleLogSpy;
11
+ export let consoleDebugSpy;
12
+ export let consoleInfoSpy;
13
+ export let consoleWarnSpy;
14
+ export let consoleErrorSpy;
15
+ export function setupTest(name, debug = false) {
16
+ expect(name).toBeDefined();
17
+ expect(typeof name).toBe('string');
18
+ expect(name.length).toBeGreaterThanOrEqual(4);
19
+ rmSync(path.join('jest', name), { recursive: true, force: true });
20
+ if (debug) {
21
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
22
+ consoleLogSpy = jest.spyOn(console, 'log');
23
+ consoleDebugSpy = jest.spyOn(console, 'debug');
24
+ consoleInfoSpy = jest.spyOn(console, 'info');
25
+ consoleWarnSpy = jest.spyOn(console, 'warn');
26
+ consoleErrorSpy = jest.spyOn(console, 'error');
27
+ }
28
+ else {
29
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
30
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
31
+ consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
32
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
33
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
34
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
35
+ }
36
+ }
37
+ export function setDebug(debug) {
38
+ if (debug) {
39
+ loggerLogSpy.mockRestore();
40
+ consoleLogSpy.mockRestore();
41
+ consoleDebugSpy.mockRestore();
42
+ consoleInfoSpy.mockRestore();
43
+ consoleWarnSpy.mockRestore();
44
+ consoleErrorSpy.mockRestore();
45
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log');
46
+ consoleLogSpy = jest.spyOn(console, 'log');
47
+ consoleDebugSpy = jest.spyOn(console, 'debug');
48
+ consoleInfoSpy = jest.spyOn(console, 'info');
49
+ consoleWarnSpy = jest.spyOn(console, 'warn');
50
+ consoleErrorSpy = jest.spyOn(console, 'error');
51
+ }
52
+ else {
53
+ loggerLogSpy = jest.spyOn(AnsiLogger.prototype, 'log').mockImplementation(() => { });
54
+ consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
55
+ consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => { });
56
+ consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => { });
57
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
58
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
59
+ }
60
+ }
61
+ export function createTestEnvironment(homeDir) {
62
+ expect(homeDir).toBeDefined();
63
+ expect(typeof homeDir).toBe('string');
64
+ expect(homeDir.length).toBeGreaterThanOrEqual(4);
65
+ rmSync(homeDir, { recursive: true, force: true });
66
+ const environment = Environment.default;
67
+ environment.vars.set('log.level', MatterLogLevel.DEBUG);
68
+ environment.vars.set('log.format', MatterLogFormat.ANSI);
69
+ environment.vars.set('path.root', homeDir);
70
+ environment.vars.set('runtime.signals', false);
71
+ environment.vars.set('runtime.exitcode', false);
72
+ return environment;
73
+ }
6
74
  export async function flushAsync(ticks = 3, microTurns = 10, pause = 100) {
7
75
  for (let i = 0; i < ticks; i++)
8
76
  await new Promise((resolve) => setImmediate(resolve));
@@ -46,19 +114,6 @@ export async function assertAllEndpointNumbersPersisted(targetServer) {
46
114
  }
47
115
  return all.length;
48
116
  }
49
- export function createTestEnvironment(homeDir) {
50
- expect(homeDir).toBeDefined();
51
- expect(typeof homeDir).toBe('string');
52
- expect(homeDir.length).toBeGreaterThan(5);
53
- rmSync(homeDir, { recursive: true, force: true });
54
- const environment = Environment.default;
55
- environment.vars.set('log.level', MatterLogLevel.DEBUG);
56
- environment.vars.set('log.format', MatterLogFormat.ANSI);
57
- environment.vars.set('path.root', homeDir);
58
- environment.vars.set('runtime.signals', false);
59
- environment.vars.set('runtime.exitcode', false);
60
- return environment;
61
- }
62
117
  export async function startServerNode(name, port) {
63
118
  const server = await ServerNode.create({
64
119
  id: name + 'ServerNode',
@@ -110,6 +165,7 @@ export async function startServerNode(name, port) {
110
165
  expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
111
166
  expect(aggregator.lifecycle.hasId).toBeTruthy();
112
167
  expect(aggregator.lifecycle.hasNumber).toBeTruthy();
168
+ await flushAsync();
113
169
  return [server, aggregator];
114
170
  }
115
171
  export async function stopServerNode(server) {
@@ -124,7 +180,7 @@ export async function stopServerNode(server) {
124
180
  await server.env.get(MdnsService)[Symbol.asyncDispose]();
125
181
  await flushAsync();
126
182
  }
127
- export async function addDevice(owner, device) {
183
+ export async function addDevice(owner, device, pause = 10) {
128
184
  expect(owner).toBeDefined();
129
185
  expect(device).toBeDefined();
130
186
  expect(owner.lifecycle.isReady).toBeTruthy();
@@ -146,9 +202,10 @@ export async function addDevice(owner, device) {
146
202
  expect(device.lifecycle.hasId).toBeTruthy();
147
203
  expect(device.lifecycle.hasNumber).toBeTruthy();
148
204
  expect(device.construction.status).toBe(Lifecycle.Status.Active);
205
+ await flushAsync(1, 1, pause);
149
206
  return true;
150
207
  }
151
- export async function deleteDevice(owner, device) {
208
+ export async function deleteDevice(owner, device, pause = 10) {
152
209
  expect(owner).toBeDefined();
153
210
  expect(device).toBeDefined();
154
211
  expect(owner.lifecycle.isReady).toBeTruthy();
@@ -170,5 +227,6 @@ export async function deleteDevice(owner, device) {
170
227
  expect(device.lifecycle.hasId).toBeTruthy();
171
228
  expect(device.lifecycle.hasNumber).toBeTruthy();
172
229
  expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
230
+ await flushAsync(1, 1, pause);
173
231
  return true;
174
232
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "./static/css/main.a2f4846a.css",
4
- "main.js": "./static/js/main.e691e19f.js",
4
+ "main.js": "./static/js/main.ee68a4ae.js",
5
5
  "static/js/453.d855a71b.chunk.js": "./static/js/453.d855a71b.chunk.js",
6
6
  "static/media/roboto-latin-700-normal.woff2": "./static/media/roboto-latin-700-normal.c4d6cab43bec89049809.woff2",
7
7
  "static/media/roboto-latin-500-normal.woff2": "./static/media/roboto-latin-500-normal.599f66a60bdf974e578e.woff2",
@@ -77,11 +77,11 @@
77
77
  "static/media/roboto-greek-ext-300-normal.woff": "./static/media/roboto-greek-ext-300-normal.60729cafbded24073dfb.woff",
78
78
  "index.html": "./index.html",
79
79
  "main.a2f4846a.css.map": "./static/css/main.a2f4846a.css.map",
80
- "main.e691e19f.js.map": "./static/js/main.e691e19f.js.map",
80
+ "main.ee68a4ae.js.map": "./static/js/main.ee68a4ae.js.map",
81
81
  "453.d855a71b.chunk.js.map": "./static/js/453.d855a71b.chunk.js.map"
82
82
  },
83
83
  "entrypoints": [
84
84
  "static/css/main.a2f4846a.css",
85
- "static/js/main.e691e19f.js"
85
+ "static/js/main.ee68a4ae.js"
86
86
  ]
87
87
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="./"><link rel="icon" href="./matterbridge 32x32.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>Matterbridge</title><link rel="manifest" href="./manifest.json"/><script defer="defer" src="./static/js/main.e691e19f.js"></script><link href="./static/css/main.a2f4846a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><base href="./"><link rel="icon" href="./matterbridge 32x32.png"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>Matterbridge</title><link rel="manifest" href="./manifest.json"/><script defer="defer" src="./static/js/main.ee68a4ae.js"></script><link href="./static/css/main.a2f4846a.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>