@matterbridge/core 3.10.13-dev-20261004-0c9c4ee → 3.10.13-dev-20261007-320f739

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.
@@ -25,6 +25,7 @@ import { NodeStorageManager } from 'node-persist-manager';
25
25
  import { toBaseDevice } from './deviceManager.js';
26
26
  import { addVirtualDevice } from './helpers.js';
27
27
  import { bridge } from './matterbridgeDeviceTypes.js';
28
+ import { deleteAdvertisingNode, getServerNodeData, setAdvertisingNode } from './matterNodeHelpers.js';
28
29
  import { PluginManager } from './pluginManager.js';
29
30
  logModuleLoaded('MatterNode');
30
31
  export class MatterNode extends EventEmitter {
@@ -55,7 +56,6 @@ export class MatterNode extends EventEmitter {
55
56
  aggregatorDeviceType = DeviceTypeId(getIntParameter('deviceType') ?? bridge.code);
56
57
  aggregatorSerialNumber = getParameter('serialNumber');
57
58
  aggregatorUniqueId = getParameter('uniqueId');
58
- advertisingNodes = new Map();
59
59
  pluginManager;
60
60
  dependantMatterNodes = new Map();
61
61
  server;
@@ -479,13 +479,13 @@ export class MatterNode extends EventEmitter {
479
479
  });
480
480
  serverNode.lifecycle.commissioned.on(() => {
481
481
  this.log.notice(`Server node for ${storeId} was initially commissioned successfully!`);
482
- this.advertisingNodes.delete(storeId);
483
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
482
+ deleteAdvertisingNode(storeId);
483
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
484
484
  });
485
485
  serverNode.lifecycle.decommissioned.on(() => {
486
486
  this.log.notice(`Server node for ${storeId} was fully decommissioned successfully!`);
487
- this.advertisingNodes.delete(storeId);
488
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
487
+ deleteAdvertisingNode(storeId);
488
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
489
489
  this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
490
490
  });
491
491
  serverNode.lifecycle.online.on(() => {
@@ -499,23 +499,23 @@ export class MatterNode extends EventEmitter {
499
499
  }
500
500
  if (!serverNode.lifecycle.isCommissioned) {
501
501
  this.log.notice(`Server node for ${storeId} is not commissioned. Pair to commission ...`);
502
- this.advertisingNodes.set(storeId, Date.now());
502
+ setAdvertisingNode(storeId, Date.now());
503
503
  const { qrPairingCode, manualPairingCode } = serverNode.state.commissioning.pairingCodes;
504
504
  this.log.notice(`QR Code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=${qrPairingCode}`);
505
505
  this.log.notice(`Manual pairing code: ${manualPairingCode}`);
506
506
  }
507
507
  else {
508
508
  this.log.notice(`Server node for ${storeId} is already commissioned. Waiting for controllers to connect ...`);
509
- this.advertisingNodes.delete(storeId);
509
+ deleteAdvertisingNode(storeId);
510
510
  }
511
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
511
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
512
512
  this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is online`, timeout: 5, severity: 'success' } });
513
513
  this.emit('online', storeId);
514
514
  });
515
515
  serverNode.lifecycle.offline.on(() => {
516
516
  this.log.notice(`Server node for ${storeId} is offline`);
517
- this.advertisingNodes.delete(storeId);
518
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
517
+ deleteAdvertisingNode(storeId);
518
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
519
519
  this.server.request({ type: 'frontend_snackbarmessage', src: 'matter', dst: 'frontend', params: { message: `${storeId} is offline`, timeout: 5, severity: 'warning' } });
520
520
  this.emit('offline', storeId);
521
521
  });
@@ -523,7 +523,7 @@ export class MatterNode extends EventEmitter {
523
523
  let action = '';
524
524
  switch (fabricAction) {
525
525
  case 'added':
526
- this.advertisingNodes.delete(storeId);
526
+ deleteAdvertisingNode(storeId);
527
527
  action = 'added';
528
528
  break;
529
529
  case 'deleted':
@@ -534,40 +534,24 @@ export class MatterNode extends EventEmitter {
534
534
  break;
535
535
  }
536
536
  this.log.notice(`Commissioned fabric index ${fabricIndex} ${action} on server node for ${storeId}: ${debugStringify(serverNode.state.commissioning.fabrics[fabricIndex])}`);
537
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
537
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
538
538
  });
539
539
  serverNode.events.sessions.opened.on((session) => {
540
540
  this.log.notice(`Session opened on server node for ${storeId}: ${debugStringify(session)}`);
541
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
541
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
542
542
  });
543
543
  serverNode.events.sessions.closed.on((session) => {
544
544
  this.log.notice(`Session closed on server node for ${storeId}: ${debugStringify(session)}`);
545
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
545
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
546
546
  });
547
547
  serverNode.events.sessions.subscriptionsChanged.on((session) => {
548
548
  this.log.notice(`Session subscriptions changed on server node for ${storeId}: ${debugStringify(session)}`);
549
- this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...this.getServerNodeData(serverNode) } } });
549
+ this.server.request({ type: 'frontend_refreshrequired', src: 'matter', dst: 'frontend', params: { changed: 'matter', matter: { ...getServerNodeData(serverNode) } } });
550
550
  });
551
551
  this.storeId = storeId;
552
552
  this.log.info(`Created server node for ${this.storeId}`);
553
553
  return serverNode;
554
554
  }
555
- getServerNodeData(serverNode) {
556
- const advertiseTime = this.advertisingNodes.get(serverNode.id) || 0;
557
- return {
558
- id: serverNode.id,
559
- online: serverNode.lifecycle.isOnline,
560
- commissioned: serverNode.state.commissioning.commissioned,
561
- advertising: advertiseTime > Date.now() - 15 * 60 * 1000,
562
- advertiseTime,
563
- windowStatus: serverNode.state.administratorCommissioning.windowStatus,
564
- qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
565
- manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
566
- fabricInformations: this.sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
567
- sessionInformations: this.sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
568
- serialNumber: serverNode.state.basicInformation.serialNumber,
569
- };
570
- }
571
555
  async startServerNode(timeout = 30000) {
572
556
  if (!this.serverNode) {
573
557
  throw new Error('Matter server node not created yet. Call create() first.');
@@ -593,6 +577,7 @@ export class MatterNode extends EventEmitter {
593
577
  catch (error) {
594
578
  this.log.error(`Failed to close ${this.serverNode.id} server node: ${getErrorMessage(error)}`);
595
579
  }
580
+ deleteAdvertisingNode(this.serverNode.id);
596
581
  }
597
582
  async createAggregatorNode() {
598
583
  if (!this.matterStorageContext) {
@@ -773,7 +758,15 @@ export class MatterNode extends EventEmitter {
773
758
  const plugin = this.pluginManager.get(pluginName);
774
759
  if (!plugin)
775
760
  throw new Error(`Error removing bridged endpoint ${plg}${pluginName}${er}:${dev}${device.deviceName}${er} (${zb}${device.name}${er}): plugin not found`);
776
- if (device.serverNode) {
761
+ if (device.mode === 'server') {
762
+ const matterNode = this.dependantMatterNodes.get(device.id);
763
+ if (matterNode) {
764
+ this.log.debug(`Closing MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})...`);
765
+ await matterNode.stop();
766
+ await matterNode.destroy(false);
767
+ this.dependantMatterNodes.delete(device.id);
768
+ this.log.debug(`Closed MatterNode for device ${plg}${pluginName}${db}:${dev}${device.deviceName}${db} (${zb}${device.name}${db})`);
769
+ }
777
770
  }
778
771
  else if (this.matterbridge.bridgeMode === 'bridge') {
779
772
  if (!this.aggregatorNode)
@@ -963,45 +956,6 @@ export class MatterNode extends EventEmitter {
963
956
  }
964
957
  }
965
958
  }
966
- sanitizeFabricInformations(fabricInfo) {
967
- return fabricInfo.map((info) => {
968
- return {
969
- fabricIndex: info.fabricIndex,
970
- fabricId: info.fabricId.toString(),
971
- nodeId: info.nodeId.toString(),
972
- rootNodeId: info.rootNodeId.toString(),
973
- rootVendorId: info.rootVendorId,
974
- rootVendorName: this.getVendorIdName(info.rootVendorId),
975
- label: info.label,
976
- };
977
- });
978
- }
979
- sanitizeSessionInformation(sessions) {
980
- return sessions
981
- .filter((session) => session.isPeerActive)
982
- .map((session) => {
983
- return {
984
- name: session.name,
985
- nodeId: session.nodeId.toString(),
986
- peerNodeId: session.peerNodeId.toString(),
987
- fabric: session.fabric
988
- ? {
989
- fabricIndex: session.fabric.fabricIndex,
990
- fabricId: session.fabric.fabricId.toString(),
991
- nodeId: session.fabric.nodeId.toString(),
992
- rootNodeId: session.fabric.rootNodeId.toString(),
993
- rootVendorId: session.fabric.rootVendorId,
994
- rootVendorName: this.getVendorIdName(session.fabric.rootVendorId),
995
- label: session.fabric.label,
996
- }
997
- : undefined,
998
- isPeerActive: session.isPeerActive,
999
- lastInteractionTimestamp: session.lastInteractionTimestamp?.toString(),
1000
- lastActiveTimestamp: session.lastActiveTimestamp?.toString(),
1001
- numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
1002
- };
1003
- });
1004
- }
1005
959
  async setServerReachability(reachable) {
1006
960
  await this.serverNode?.setStateOf(BasicInformationServer, { reachable });
1007
961
  this.serverNode?.act((agent) => this.serverNode?.eventsOf(BasicInformationServer).reachableChanged?.emit({ reachableNewValue: reachable }, agent.context));
@@ -1013,50 +967,6 @@ export class MatterNode extends EventEmitter {
1013
967
  child.act((agent) => child.eventsOf(BridgedDeviceBasicInformationServer).reachableChanged.emit({ reachableNewValue: true }, agent.context));
1014
968
  }
1015
969
  }
1016
- getVendorIdName = (vendorId) => {
1017
- if (!vendorId)
1018
- return '';
1019
- let vendorName = '(Unknown vendorId)';
1020
- switch (vendorId) {
1021
- case 4937:
1022
- vendorName = '(AppleHome)';
1023
- break;
1024
- case 4996:
1025
- vendorName = '(AppleKeyChain)';
1026
- break;
1027
- case 4362:
1028
- vendorName = '(SmartThings)';
1029
- break;
1030
- case 4939:
1031
- vendorName = '(HomeAssistant)';
1032
- break;
1033
- case 24582:
1034
- vendorName = '(GoogleHome)';
1035
- break;
1036
- case 4631:
1037
- vendorName = '(Alexa)';
1038
- break;
1039
- case 4701:
1040
- vendorName = '(Tuya)';
1041
- break;
1042
- case 4718:
1043
- vendorName = '(Xiaomi)';
1044
- break;
1045
- case 4742:
1046
- vendorName = '(eWeLink)';
1047
- break;
1048
- case 5264:
1049
- vendorName = '(Shelly)';
1050
- break;
1051
- case 0x1488:
1052
- vendorName = '(ShortcutLabsFlic)';
1053
- break;
1054
- case 65521:
1055
- vendorName = '(MatterTest)';
1056
- break;
1057
- }
1058
- return vendorName;
1059
- };
1060
970
  async yieldToNode(timeout = 100) {
1061
971
  await Promise.resolve();
1062
972
  await new Promise((resolve) => {
@@ -0,0 +1,15 @@
1
+ import { ServerNode, type SessionsBehavior } from '@matter/node';
2
+ import type { ExposedFabricInformation } from '@matter/protocol';
3
+ import type { ApiMatter, SanitizedExposedFabricInformation, SanitizedSession } from '@matterbridge/types';
4
+ import type { MatterbridgeEndpoint } from './matterbridgeEndpoint.js';
5
+ export declare function getReachability(device: MatterbridgeEndpoint): boolean;
6
+ export declare function getPowerSource(endpoint: MatterbridgeEndpoint): 'ac' | 'dc' | 'ok' | 'warning' | 'critical' | undefined;
7
+ export declare function getBatteryLevel(endpoint: MatterbridgeEndpoint): number | undefined;
8
+ export declare function getClusterTextFromDevice(device: MatterbridgeEndpoint): string;
9
+ export declare function setAdvertisingNode(storeId: string, advertiseTime: number): void;
10
+ export declare function deleteAdvertisingNode(storeId: string): void;
11
+ export declare function clearAdvertisingNodes(): void;
12
+ export declare function getServerNodeData(serverNode: ServerNode): ApiMatter;
13
+ export declare function sanitizeFabricInformations(fabricInfo: ExposedFabricInformation[]): SanitizedExposedFabricInformation[];
14
+ export declare function sanitizeSessionInformation(sessions: SessionsBehavior.Session[]): SanitizedSession[];
15
+ export declare function getVendorIdName(vendorId: number | undefined): string;
@@ -0,0 +1,393 @@
1
+ import { Lifecycle } from '@matter/general';
2
+ import { ServerNode } from '@matter/node';
3
+ import { getClusterNameById } from '@matter/types/cluster';
4
+ import { AirQuality } from '@matter/types/clusters/air-quality';
5
+ import { BridgedDeviceBasicInformation } from '@matter/types/clusters/bridged-device-basic-information';
6
+ import { ClosureControl } from '@matter/types/clusters/closure-control';
7
+ import { DeviceEnergyManagement } from '@matter/types/clusters/device-energy-management';
8
+ import { FanControl } from '@matter/types/clusters/fan-control';
9
+ import { FixedLabel } from '@matter/types/clusters/fixed-label';
10
+ import { OperationalState } from '@matter/types/clusters/operational-state';
11
+ import { PowerSource } from '@matter/types/clusters/power-source';
12
+ import { RvcOperationalState } from '@matter/types/clusters/rvc-operational-state';
13
+ import { SmokeCoAlarm } from '@matter/types/clusters/smoke-co-alarm';
14
+ import { SoilMeasurement } from '@matter/types/clusters/soil-measurement';
15
+ import { UserLabel } from '@matter/types/clusters/user-label';
16
+ import { ValveConfigurationAndControl } from '@matter/types/clusters/valve-configuration-and-control';
17
+ import { ThreeLevelAuto } from '@matter/types/globals';
18
+ import { getEnumDescription } from '@matterbridge/utils/enum';
19
+ import { isValidArray, isValidNumber, isValidObject } from '@matterbridge/utils/validate';
20
+ import { stringify } from 'node-ansi-logger';
21
+ import { featuresFor, getAttribute } from './matterbridgeEndpointHelpers.js';
22
+ const advertisingNodes = new Map();
23
+ export function getReachability(device) {
24
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
25
+ return false;
26
+ if (device.hasClusterServer(BridgedDeviceBasicInformation.id))
27
+ return device.getAttribute(BridgedDeviceBasicInformation, 'reachable');
28
+ if (device.mode === 'server' && device.serverNode?.state.basicInformation.reachable !== undefined)
29
+ return device.serverNode.state.basicInformation.reachable;
30
+ const owner = device.ownerOfType(ServerNode.RootEndpoint);
31
+ if (owner?.state.basicInformation.reachable !== undefined)
32
+ return owner.state.basicInformation.reachable;
33
+ return false;
34
+ }
35
+ export function getPowerSource(endpoint) {
36
+ if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
37
+ return undefined;
38
+ const powerSource = (device) => {
39
+ const featureMap = featuresFor(device, PowerSource);
40
+ if (featureMap.wired) {
41
+ const wiredCurrentType = device.getAttribute(PowerSource.id, 'wiredCurrentType');
42
+ return ['ac', 'dc'][wiredCurrentType];
43
+ }
44
+ if (featureMap.battery) {
45
+ const batChargeLevel = device.getAttribute(PowerSource.id, 'batChargeLevel');
46
+ return ['ok', 'warning', 'critical'][batChargeLevel];
47
+ }
48
+ return undefined;
49
+ };
50
+ if (endpoint.hasClusterServer(PowerSource.id))
51
+ return powerSource(endpoint);
52
+ for (const child of endpoint.getChildEndpoints()) {
53
+ if (child.hasClusterServer(PowerSource.id))
54
+ return powerSource(child);
55
+ }
56
+ return undefined;
57
+ }
58
+ export function getBatteryLevel(endpoint) {
59
+ if (!endpoint.lifecycle.isReady || endpoint.construction.status !== Lifecycle.Status.Active)
60
+ return undefined;
61
+ const batteryLevel = (device) => {
62
+ const featureMap = featuresFor(device, PowerSource);
63
+ if (featureMap.battery) {
64
+ const batChargeLevel = device.getAttribute(PowerSource, 'batPercentRemaining');
65
+ return isValidNumber(batChargeLevel, 0, 200) ? batChargeLevel / 2 : undefined;
66
+ }
67
+ return undefined;
68
+ };
69
+ if (endpoint.hasClusterServer(PowerSource.id))
70
+ return batteryLevel(endpoint);
71
+ for (const child of endpoint.getChildEndpoints()) {
72
+ if (child.hasClusterServer(PowerSource.id))
73
+ return batteryLevel(child);
74
+ }
75
+ return undefined;
76
+ }
77
+ export function getClusterTextFromDevice(device) {
78
+ if (!device.lifecycle.isReady || device.construction.status !== Lifecycle.Status.Active)
79
+ return '';
80
+ let attributes = '';
81
+ const getUserLabel = (device) => {
82
+ const labelList = getAttribute(device, UserLabel, 'labelList');
83
+ if (labelList) {
84
+ const composed = labelList.find((entry) => entry.label === 'composed');
85
+ if (composed)
86
+ return 'Composed: ' + composed.value;
87
+ }
88
+ return '';
89
+ };
90
+ const getFixedLabel = (device) => {
91
+ const labelList = getAttribute(device, FixedLabel, 'labelList');
92
+ if (labelList) {
93
+ const composed = labelList.find((entry) => entry.label === 'composed');
94
+ if (composed)
95
+ return 'Composed: ' + composed.value;
96
+ }
97
+ return '';
98
+ };
99
+ const getMeasurementText = (value, scale, unit) => {
100
+ if (value === null)
101
+ return 'unknown';
102
+ if (isValidNumber(value) || typeof value === 'bigint')
103
+ return `${Number(value) / scale}${unit}`;
104
+ return '';
105
+ };
106
+ const getEnergyText = (value) => {
107
+ if (value === null)
108
+ return 'unknown';
109
+ if (isValidObject(value) && 'energy' in value && (isValidNumber(value.energy) || typeof value.energy === 'bigint'))
110
+ return `${Number(value.energy) / 1_000_000}kWh`;
111
+ return '';
112
+ };
113
+ const appendMeasurement = (label, value) => {
114
+ if (value)
115
+ attributes += `${label}: ${value} `;
116
+ };
117
+ device.forEachAttribute((clusterName, clusterId, attributeName, attributeId, attributeValue) => {
118
+ if (typeof attributeValue === 'undefined' || attributeValue === undefined)
119
+ return;
120
+ if (clusterName === 'descriptor' && attributeName === 'clientList' && isValidArray(attributeValue, 1))
121
+ attributes += `Client cluster(s): [${attributeValue.map((id) => getClusterNameById(id)).join(', ')}] `;
122
+ if (clusterName === 'binding' && attributeName === 'binding' && isValidArray(attributeValue)) {
123
+ if (attributeValue.length === 0)
124
+ attributes += `Bound cluster(s): none `;
125
+ else {
126
+ const targets = attributeValue;
127
+ const formatted = targets.map((target) => {
128
+ const parts = [];
129
+ if (target.node !== undefined)
130
+ parts.push(`node: ${target.node}`);
131
+ if (target.group !== undefined)
132
+ parts.push(`group: ${target.group}`);
133
+ if (target.endpoint !== undefined)
134
+ parts.push(`endpoint: ${target.endpoint}`);
135
+ if (target.cluster !== undefined)
136
+ parts.push(`cluster: ${getClusterNameById(target.cluster)}`);
137
+ if (target.fabricIndex !== undefined)
138
+ parts.push(`fabricIndex: ${target.fabricIndex}`);
139
+ return `[${parts.join(', ')}]`;
140
+ });
141
+ attributes += `Bound cluster(s): ${formatted.join(' ')} `;
142
+ }
143
+ }
144
+ if (clusterName === 'onOff' && attributeName === 'onOff')
145
+ attributes += `OnOff: ${attributeValue} `;
146
+ if (clusterName === 'switch' && attributeName === 'currentPosition')
147
+ attributes += `Position: ${attributeValue} `;
148
+ if (clusterName === 'windowCovering' && attributeName === 'currentPositionLiftPercent100ths' && isValidNumber(attributeValue, 0, 10000))
149
+ attributes += `Cover position: ${attributeValue / 100}% `;
150
+ if (clusterName === 'doorLock' && attributeName === 'lockState')
151
+ attributes += `State: ${attributeValue === 1 ? 'Locked' : 'Not locked'} `;
152
+ if (clusterName === 'closureControl' && attributeName === 'overallCurrentState' && attributeValue === null)
153
+ attributes += `Position: unknown Latch: unknown Speed: unknown SecureState: unknown `;
154
+ if (clusterName === 'closureControl' && attributeName === 'overallCurrentState' && isValidObject(attributeValue)) {
155
+ const overallCurrentState = attributeValue;
156
+ attributes += `Position: ${getEnumDescription(ClosureControl.CurrentPosition, overallCurrentState.position, { fallback: 'unknown' })} `;
157
+ if (overallCurrentState.latch !== undefined)
158
+ attributes += `Latch: ${overallCurrentState.latch} `;
159
+ if (overallCurrentState.speed !== undefined)
160
+ attributes += `Speed: ${getEnumDescription(ThreeLevelAuto, overallCurrentState.speed)} `;
161
+ attributes += `SecureState: ${overallCurrentState.secureState ?? 'unknown'} `;
162
+ }
163
+ if (clusterName === 'thermostat' && attributeName === 'localTemperature' && isValidNumber(attributeValue))
164
+ attributes += `Temperature: ${attributeValue / 100}°C `;
165
+ if (clusterName === 'thermostat' && attributeName === 'occupiedHeatingSetpoint' && isValidNumber(attributeValue))
166
+ attributes += `Heat to: ${attributeValue / 100}°C `;
167
+ if (clusterName === 'thermostat' && attributeName === 'occupiedCoolingSetpoint' && isValidNumber(attributeValue))
168
+ attributes += `Cool to: ${attributeValue / 100}°C `;
169
+ const modeClusters = new Set(['modeSelect', 'rvcRunMode', 'rvcCleanMode', 'laundryWasherMode', 'ovenMode', 'microwaveOvenMode', 'deviceEnergyManagementMode']);
170
+ if (modeClusters.has(clusterName) && attributeName === 'currentMode') {
171
+ const supportedModes = getAttribute(device, clusterName, 'supportedModes');
172
+ const supportedMode = supportedModes?.find((mode) => mode.mode === attributeValue);
173
+ if (supportedMode)
174
+ attributes += `Mode: ${supportedMode.label} `;
175
+ }
176
+ if (clusterName === 'operationalState' && attributeName === 'operationalState')
177
+ attributes += `OpState: ${getEnumDescription(OperationalState.OperationalStateEnum, attributeValue)} `;
178
+ if (clusterName === 'rvcOperationalState' && attributeName === 'operationalState')
179
+ attributes += `OpState: ${getEnumDescription(RvcOperationalState.OperationalState, attributeValue)} `;
180
+ if (clusterName === 'pumpConfigurationAndControl' && attributeName === 'operationMode')
181
+ attributes += `Mode: ${attributeValue} `;
182
+ if (clusterName === 'valveConfigurationAndControl' && attributeName === 'currentState')
183
+ attributes += `State: ${getEnumDescription(ValveConfigurationAndControl.ValveState, attributeValue, { fallback: 'unknown' })} `;
184
+ if (clusterName === 'levelControl' && attributeName === 'currentLevel')
185
+ attributes += `Level: ${attributeValue} `;
186
+ if (clusterName === 'applicationBasic' && attributeName === 'applicationName')
187
+ attributes += `App: ${attributeValue} `;
188
+ if (clusterName === 'colorControl' && attributeName === 'colorMode' && isValidNumber(attributeValue, 0, 2))
189
+ attributes += `Mode: ${['HS', 'XY', 'CT'][attributeValue]} `;
190
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentHue' && isValidNumber(attributeValue))
191
+ attributes += `Hue: ${Math.round(attributeValue)} `;
192
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 0 && attributeName === 'currentSaturation' && isValidNumber(attributeValue))
193
+ attributes += `Saturation: ${Math.round(attributeValue)} `;
194
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentX' && isValidNumber(attributeValue))
195
+ attributes += `X: ${Math.round(attributeValue / 655.36) / 100} `;
196
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 1 && attributeName === 'currentY' && isValidNumber(attributeValue))
197
+ attributes += `Y: ${Math.round(attributeValue / 655.36) / 100} `;
198
+ if (clusterName === 'colorControl' && getAttribute(device, 'colorControl', 'colorMode') === 2 && attributeName === 'colorTemperatureMireds' && isValidNumber(attributeValue))
199
+ attributes += `ColorTemp: ${Math.round(attributeValue)} `;
200
+ if (clusterName === 'booleanState' && attributeName === 'stateValue')
201
+ attributes += `Contact: ${attributeValue} `;
202
+ if (clusterName === 'booleanStateConfiguration' && attributeName === 'alarmsActive' && isValidObject(attributeValue))
203
+ attributes += `Active alarms: ${stringify(attributeValue)} `;
204
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'smokeState')
205
+ attributes += `Smoke: ${getEnumDescription(SmokeCoAlarm.AlarmState, attributeValue)} `;
206
+ if (clusterName === 'smokeCoAlarm' && attributeName === 'coState')
207
+ attributes += `Co: ${getEnumDescription(SmokeCoAlarm.AlarmState, attributeValue)} `;
208
+ if (clusterName === 'fanControl' && attributeName === 'fanMode')
209
+ attributes += `Mode: ${getEnumDescription(FanControl.FanMode, attributeValue)} `;
210
+ if (clusterName === 'fanControl' && attributeName === 'percentCurrent')
211
+ attributes += `Percent: ${attributeValue}% `;
212
+ if (clusterName === 'fanControl' && attributeName === 'speedCurrent')
213
+ attributes += `Speed: ${attributeValue} `;
214
+ if (clusterName === 'fanControl' && attributeName === 'rockSetting' && isValidObject(attributeValue))
215
+ attributes += `Rock: ${stringify(attributeValue)} `;
216
+ if (clusterName === 'fanControl' && attributeName === 'windSetting' && isValidObject(attributeValue))
217
+ attributes += `Wind: ${stringify(attributeValue)} `;
218
+ if (clusterName === 'fanControl' && attributeName === 'airflowDirection')
219
+ attributes += `Direction: ${getEnumDescription(FanControl.AirflowDirection, attributeValue)} `;
220
+ if (clusterName === 'hepaFilterMonitoring' && attributeName === 'condition')
221
+ attributes += `Hepa filter: ${attributeValue}% `;
222
+ if (clusterName === 'activatedCarbonFilterMonitoring' && attributeName === 'condition')
223
+ attributes += `Carbon filter: ${attributeValue}% `;
224
+ if (clusterName === 'waterTankLevelMonitoring' && attributeName === 'condition')
225
+ attributes += `Water tank: ${attributeValue}% `;
226
+ if (clusterName === 'temperatureAlarm' && attributeName === 'state' && isValidObject(attributeValue))
227
+ attributes += `Temp alarm: ${stringify(attributeValue)} `;
228
+ if (clusterName === 'occupancySensing' && attributeName === 'occupancy' && isValidObject(attributeValue, 1))
229
+ attributes += `Occupancy: ${attributeValue.occupied} `;
230
+ if (clusterName === 'illuminanceMeasurement' && attributeName === 'measuredValue') {
231
+ if (attributeValue === null)
232
+ attributes += `Illuminance: unknown `;
233
+ else if (isValidNumber(attributeValue))
234
+ attributes += `Illuminance: ${Math.round(Math.max(Math.pow(10, attributeValue / 10000), 0))} lx `;
235
+ }
236
+ if (clusterName === 'airQuality' && attributeName === 'airQuality')
237
+ attributes += `Air quality: ${getEnumDescription(AirQuality.AirQualityEnum, attributeValue)} `;
238
+ if (clusterName === 'totalVolatileOrganicCompoundsConcentrationMeasurement' && attributeName === 'measuredValue')
239
+ appendMeasurement('Voc', getMeasurementText(attributeValue, 1, ''));
240
+ if (clusterName === 'pm1ConcentrationMeasurement' && attributeName === 'measuredValue')
241
+ appendMeasurement('Pm1', getMeasurementText(attributeValue, 1, ''));
242
+ if (clusterName === 'pm25ConcentrationMeasurement' && attributeName === 'measuredValue')
243
+ appendMeasurement('Pm2.5', getMeasurementText(attributeValue, 1, ''));
244
+ if (clusterName === 'pm10ConcentrationMeasurement' && attributeName === 'measuredValue')
245
+ appendMeasurement('Pm10', getMeasurementText(attributeValue, 1, ''));
246
+ if (clusterName === 'formaldehydeConcentrationMeasurement' && attributeName === 'measuredValue')
247
+ appendMeasurement('CH₂O', getMeasurementText(attributeValue, 1, ''));
248
+ if (clusterName === 'temperatureMeasurement' && attributeName === 'measuredValue')
249
+ appendMeasurement('Temperature', getMeasurementText(attributeValue, 100, ' °C'));
250
+ if (clusterName === 'relativeHumidityMeasurement' && attributeName === 'measuredValue')
251
+ appendMeasurement('Humidity', getMeasurementText(attributeValue, 100, '%'));
252
+ if (clusterName === 'pressureMeasurement' && attributeName === 'measuredValue')
253
+ appendMeasurement('Pressure', getMeasurementText(attributeValue, 1, ' hPa'));
254
+ if (clusterName === 'flowMeasurement' && attributeName === 'measuredValue')
255
+ appendMeasurement('Flow', getMeasurementText(attributeValue, 10, ' m³/h'));
256
+ if (clusterId === SoilMeasurement.id && attributeName === 'soilMoistureMeasuredValue')
257
+ appendMeasurement('Soil moisture', getMeasurementText(attributeValue, 1, '%'));
258
+ if (clusterName === 'electricalPowerMeasurement' && attributeName === 'voltage')
259
+ appendMeasurement('Voltage', getMeasurementText(attributeValue, 1_000, 'V'));
260
+ if (clusterName === 'electricalPowerMeasurement' && attributeName === 'activeCurrent')
261
+ appendMeasurement('Current', getMeasurementText(attributeValue, 1_000, 'A'));
262
+ if (clusterName === 'electricalPowerMeasurement' && attributeName === 'activePower')
263
+ appendMeasurement('Power', getMeasurementText(attributeValue, 1_000_000, 'kW'));
264
+ if (clusterName === 'electricalPowerMeasurement' && attributeName === 'frequency')
265
+ appendMeasurement('Frequency', getMeasurementText(attributeValue, 1_000, 'Hz'));
266
+ if (clusterName === 'electricalEnergyMeasurement' && attributeName === 'cumulativeEnergyImported')
267
+ appendMeasurement('Imported', getEnergyText(attributeValue));
268
+ if (clusterName === 'electricalEnergyMeasurement' && attributeName === 'cumulativeEnergyExported')
269
+ appendMeasurement('Exported', getEnergyText(attributeValue));
270
+ if (clusterName === 'deviceEnergyManagement' && attributeName === 'esaCanGenerate')
271
+ attributes += `ESA can generate: ${attributeValue} `;
272
+ if (clusterName === 'deviceEnergyManagement' && attributeName === 'esaState')
273
+ attributes += `ESA state: ${DeviceEnergyManagement.EsaState[attributeValue]} `;
274
+ if (clusterName === 'fixedLabel' && attributeName === 'labelList')
275
+ attributes += `${getFixedLabel(device)} `;
276
+ if (clusterName === 'userLabel' && attributeName === 'labelList')
277
+ attributes += `${getUserLabel(device)} `;
278
+ });
279
+ return attributes.trimStart().trimEnd();
280
+ }
281
+ export function setAdvertisingNode(storeId, advertiseTime) {
282
+ advertisingNodes.set(storeId, advertiseTime);
283
+ }
284
+ export function deleteAdvertisingNode(storeId) {
285
+ advertisingNodes.delete(storeId);
286
+ }
287
+ export function clearAdvertisingNodes() {
288
+ advertisingNodes.clear();
289
+ }
290
+ export function getServerNodeData(serverNode) {
291
+ const advertiseTime = advertisingNodes.get(serverNode.id) ?? 0;
292
+ return {
293
+ id: serverNode.id,
294
+ online: serverNode.lifecycle.isOnline,
295
+ commissioned: serverNode.state.commissioning.commissioned,
296
+ advertising: advertiseTime > 0,
297
+ advertiseTime,
298
+ windowStatus: serverNode.state.administratorCommissioning.windowStatus,
299
+ qrPairingCode: serverNode.state.commissioning.pairingCodes.qrPairingCode,
300
+ manualPairingCode: serverNode.state.commissioning.pairingCodes.manualPairingCode,
301
+ fabricInformations: sanitizeFabricInformations(Object.values(serverNode.state.commissioning.fabrics)),
302
+ sessionInformations: sanitizeSessionInformation(Object.values(serverNode.state.sessions.sessions)),
303
+ serialNumber: serverNode.state.basicInformation.serialNumber,
304
+ };
305
+ }
306
+ export function sanitizeFabricInformations(fabricInfo) {
307
+ return fabricInfo.map((info) => {
308
+ return {
309
+ fabricIndex: info.fabricIndex,
310
+ fabricId: info.fabricId.toString(),
311
+ nodeId: info.nodeId.toString(),
312
+ rootNodeId: info.rootNodeId.toString(),
313
+ rootVendorId: info.rootVendorId,
314
+ rootVendorName: getVendorIdName(info.rootVendorId),
315
+ label: info.label,
316
+ };
317
+ });
318
+ }
319
+ function toIsoTimestamp(timestamp) {
320
+ if (!isValidNumber(timestamp, 1) || Number.isNaN(new Date(timestamp).getTime()))
321
+ return '';
322
+ return new Date(timestamp).toISOString();
323
+ }
324
+ export function sanitizeSessionInformation(sessions) {
325
+ return sessions
326
+ .filter((session) => session.isPeerActive)
327
+ .map((session) => {
328
+ return {
329
+ name: session.name,
330
+ nodeId: session.nodeId.toString(),
331
+ peerNodeId: session.peerNodeId.toString(),
332
+ fabric: session.fabric
333
+ ? {
334
+ fabricIndex: session.fabric.fabricIndex,
335
+ fabricId: session.fabric.fabricId.toString(),
336
+ nodeId: session.fabric.nodeId.toString(),
337
+ rootNodeId: session.fabric.rootNodeId.toString(),
338
+ rootVendorId: session.fabric.rootVendorId,
339
+ rootVendorName: getVendorIdName(session.fabric.rootVendorId),
340
+ label: session.fabric.label,
341
+ }
342
+ : undefined,
343
+ isPeerActive: session.isPeerActive,
344
+ lastInteractionTimestamp: toIsoTimestamp(session.lastInteractionTimestamp),
345
+ lastActiveTimestamp: toIsoTimestamp(session.lastActiveTimestamp),
346
+ numberOfActiveSubscriptions: session.numberOfActiveSubscriptions,
347
+ };
348
+ });
349
+ }
350
+ export function getVendorIdName(vendorId) {
351
+ if (!vendorId)
352
+ return '';
353
+ let vendorName = '(Unknown vendorId)';
354
+ switch (vendorId) {
355
+ case 4937:
356
+ vendorName = '(AppleHome)';
357
+ break;
358
+ case 4996:
359
+ vendorName = '(AppleKeyChain)';
360
+ break;
361
+ case 4362:
362
+ vendorName = '(SmartThings)';
363
+ break;
364
+ case 4939:
365
+ vendorName = '(HomeAssistant)';
366
+ break;
367
+ case 24582:
368
+ vendorName = '(GoogleHome)';
369
+ break;
370
+ case 4631:
371
+ vendorName = '(Alexa)';
372
+ break;
373
+ case 4701:
374
+ vendorName = '(Tuya)';
375
+ break;
376
+ case 4718:
377
+ vendorName = '(Xiaomi)';
378
+ break;
379
+ case 4742:
380
+ vendorName = '(eWeLink)';
381
+ break;
382
+ case 5264:
383
+ vendorName = '(Shelly)';
384
+ break;
385
+ case 0x1488:
386
+ vendorName = '(ShortcutLabsFlic)';
387
+ break;
388
+ case 65521:
389
+ vendorName = '(MatterTest)';
390
+ break;
391
+ }
392
+ return vendorName;
393
+ }