homebridge 2.0.0-beta.44 → 2.0.0-beta.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +3 -365
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +13 -85
- package/dist/api.js.map +1 -1
- package/dist/bridgeService.d.ts +0 -25
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +32 -85
- package/dist/bridgeService.js.map +1 -1
- package/dist/childBridgeFork.d.ts +12 -15
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +126 -80
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +21 -105
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +57 -134
- package/dist/childBridgeService.js.map +1 -1
- package/dist/cli.js +0 -2
- package/dist/cli.js.map +1 -1
- package/dist/externalPortService.d.ts +0 -21
- package/dist/externalPortService.d.ts.map +1 -1
- package/dist/externalPortService.js +0 -28
- package/dist/externalPortService.js.map +1 -1
- package/dist/index.d.ts +0 -112
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -44
- package/dist/index.js.map +1 -1
- package/dist/ipcService.d.ts +23 -15
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/ipcService.js +6 -12
- package/dist/ipcService.js.map +1 -1
- package/dist/logger.d.ts +0 -46
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +13 -57
- package/dist/logger.js.map +1 -1
- package/dist/matter/BaseMatterManager.d.ts +19 -0
- package/dist/matter/BaseMatterManager.d.ts.map +1 -0
- package/dist/matter/BaseMatterManager.js +170 -0
- package/dist/matter/BaseMatterManager.js.map +1 -0
- package/dist/matter/ChildBridgeMatterManager.d.ts +5 -69
- package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -1
- package/dist/matter/ChildBridgeMatterManager.js +36 -227
- package/dist/matter/ChildBridgeMatterManager.js.map +1 -1
- package/dist/matter/ClusterCommandMapper.d.ts +5 -0
- package/dist/matter/ClusterCommandMapper.d.ts.map +1 -0
- package/dist/matter/ClusterCommandMapper.js +203 -0
- package/dist/matter/ClusterCommandMapper.js.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts +0 -27
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.js +2 -27
- package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -1
- package/dist/matter/MatterAPIImpl.d.ts +0 -68
- package/dist/matter/MatterAPIImpl.d.ts.map +1 -1
- package/dist/matter/MatterAPIImpl.js +6 -106
- package/dist/matter/MatterAPIImpl.js.map +1 -1
- package/dist/matter/MatterBridgeManager.d.ts +9 -60
- package/dist/matter/MatterBridgeManager.d.ts.map +1 -1
- package/dist/matter/MatterBridgeManager.js +139 -215
- package/dist/matter/MatterBridgeManager.js.map +1 -1
- package/dist/matter/MatterConfigCollector.d.ts +1 -20
- package/dist/matter/MatterConfigCollector.d.ts.map +1 -1
- package/dist/matter/MatterConfigCollector.js +14 -27
- package/dist/matter/MatterConfigCollector.js.map +1 -1
- package/dist/matter/accessoryCache.d.ts +0 -48
- package/dist/matter/accessoryCache.d.ts.map +1 -1
- package/dist/matter/accessoryCache.js +1 -60
- package/dist/matter/accessoryCache.js.map +1 -1
- package/dist/matter/behaviors/AirQualityBehavior.d.ts +0 -42
- package/dist/matter/behaviors/AirQualityBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/AirQualityBehavior.js +0 -44
- package/dist/matter/behaviors/AirQualityBehavior.js.map +1 -1
- package/dist/matter/behaviors/BehaviorRegistry.d.ts +4 -42
- package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -1
- package/dist/matter/behaviors/BehaviorRegistry.js +12 -42
- package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -1
- package/dist/matter/behaviors/ColorControlBehavior.d.ts +0 -49
- package/dist/matter/behaviors/ColorControlBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/ColorControlBehavior.js +0 -90
- package/dist/matter/behaviors/ColorControlBehavior.js.map +1 -1
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.d.ts +0 -91
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.js +0 -96
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.js.map +1 -1
- package/dist/matter/behaviors/DoorLockBehavior.d.ts +0 -11
- package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/DoorLockBehavior.js +0 -25
- package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -1
- package/dist/matter/behaviors/FanControlBehavior.d.ts +0 -11
- package/dist/matter/behaviors/FanControlBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/FanControlBehavior.js +0 -25
- package/dist/matter/behaviors/FanControlBehavior.js.map +1 -1
- package/dist/matter/behaviors/IdentifyBehavior.d.ts +0 -11
- package/dist/matter/behaviors/IdentifyBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/IdentifyBehavior.js +0 -17
- package/dist/matter/behaviors/IdentifyBehavior.js.map +1 -1
- package/dist/matter/behaviors/LevelControlBehavior.d.ts +0 -20
- package/dist/matter/behaviors/LevelControlBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/LevelControlBehavior.js +0 -52
- package/dist/matter/behaviors/LevelControlBehavior.js.map +1 -1
- package/dist/matter/behaviors/OnOffBehavior.d.ts +0 -17
- package/dist/matter/behaviors/OnOffBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/OnOffBehavior.js +0 -38
- package/dist/matter/behaviors/OnOffBehavior.js.map +1 -1
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts +0 -11
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/RvcCleanModeBehavior.js +0 -17
- package/dist/matter/behaviors/RvcCleanModeBehavior.js.map +1 -1
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts +0 -11
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js +0 -29
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js.map +1 -1
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts +0 -11
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/RvcRunModeBehavior.js +0 -17
- package/dist/matter/behaviors/RvcRunModeBehavior.js.map +1 -1
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts +0 -11
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/ServiceAreaBehavior.js +0 -23
- package/dist/matter/behaviors/ServiceAreaBehavior.js.map +1 -1
- package/dist/matter/behaviors/ThermostatBehavior.d.ts +0 -11
- package/dist/matter/behaviors/ThermostatBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/ThermostatBehavior.js +0 -39
- package/dist/matter/behaviors/ThermostatBehavior.js.map +1 -1
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts +0 -17
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/WindowCoveringBehavior.js +0 -56
- package/dist/matter/behaviors/WindowCoveringBehavior.js.map +1 -1
- package/dist/matter/behaviors/index.d.ts +0 -5
- package/dist/matter/behaviors/index.d.ts.map +1 -1
- package/dist/matter/behaviors/index.js +0 -5
- package/dist/matter/behaviors/index.js.map +1 -1
- package/dist/matter/configValidator.d.ts +0 -55
- package/dist/matter/configValidator.d.ts.map +1 -1
- package/dist/matter/configValidator.js +1 -68
- package/dist/matter/configValidator.js.map +1 -1
- package/dist/matter/errorHandler.d.ts +0 -22
- package/dist/matter/errorHandler.d.ts.map +1 -1
- package/dist/matter/errorHandler.js +0 -32
- package/dist/matter/errorHandler.js.map +1 -1
- package/dist/matter/errors.d.ts +0 -132
- package/dist/matter/errors.d.ts.map +1 -1
- package/dist/matter/errors.js +0 -132
- package/dist/matter/errors.js.map +1 -1
- package/dist/matter/index.d.ts +0 -30
- package/dist/matter/index.d.ts.map +1 -1
- package/dist/matter/index.js +0 -13
- package/dist/matter/index.js.map +1 -1
- package/dist/matter/logFormatter.d.ts +0 -17
- package/dist/matter/logFormatter.d.ts.map +1 -1
- package/dist/matter/logFormatter.js +5 -63
- package/dist/matter/logFormatter.js.map +1 -1
- package/dist/matter/server.d.ts +12 -236
- package/dist/matter/server.d.ts.map +1 -1
- package/dist/matter/server.js +177 -488
- package/dist/matter/server.js.map +1 -1
- package/dist/matter/serverHelpers.d.ts +0 -56
- package/dist/matter/serverHelpers.d.ts.map +1 -1
- package/dist/matter/serverHelpers.js +1 -66
- package/dist/matter/serverHelpers.js.map +1 -1
- package/dist/matter/sharedTypes.d.ts +0 -83
- package/dist/matter/sharedTypes.d.ts.map +1 -1
- package/dist/matter/sharedTypes.js +0 -26
- package/dist/matter/sharedTypes.js.map +1 -1
- package/dist/matter/storage.d.ts +0 -90
- package/dist/matter/storage.d.ts.map +1 -1
- package/dist/matter/storage.js +2 -130
- package/dist/matter/storage.js.map +1 -1
- package/dist/matter/typeHelpers.d.ts +0 -30
- package/dist/matter/typeHelpers.d.ts.map +1 -1
- package/dist/matter/typeHelpers.js +0 -24
- package/dist/matter/typeHelpers.js.map +1 -1
- package/dist/matter/types.d.ts +0 -273
- package/dist/matter/types.d.ts.map +1 -1
- package/dist/matter/types.js +0 -83
- package/dist/matter/types.js.map +1 -1
- package/dist/platformAccessory.d.ts +0 -15
- package/dist/platformAccessory.d.ts.map +1 -1
- package/dist/platformAccessory.js +6 -32
- package/dist/platformAccessory.js.map +1 -1
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -29
- package/dist/plugin.js.map +1 -1
- package/dist/pluginManager.d.ts +0 -22
- package/dist/pluginManager.d.ts.map +1 -1
- package/dist/pluginManager.js +18 -41
- package/dist/pluginManager.js.map +1 -1
- package/dist/server.d.ts +9 -29
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +304 -157
- package/dist/server.js.map +1 -1
- package/dist/user.d.ts +0 -3
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +2 -5
- package/dist/user.js.map +1 -1
- package/dist/util/mac.js +0 -1
- package/dist/util/mac.js.map +1 -1
- package/package.json +4 -4
- package/dist/api.spec.d.ts +0 -2
- package/dist/api.spec.d.ts.map +0 -1
- package/dist/api.spec.js +0 -413
- package/dist/api.spec.js.map +0 -1
- package/dist/logger.spec.d.ts +0 -2
- package/dist/logger.spec.d.ts.map +0 -1
- package/dist/logger.spec.js +0 -95
- package/dist/logger.spec.js.map +0 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts +0 -2
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts.map +0 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.js +0 -293
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.js.map +0 -1
- package/dist/matter/accessoryCache.spec.d.ts +0 -2
- package/dist/matter/accessoryCache.spec.d.ts.map +0 -1
- package/dist/matter/accessoryCache.spec.js +0 -452
- package/dist/matter/accessoryCache.spec.js.map +0 -1
- package/dist/matter/behaviors/AirQualityBehavior.spec.d.ts +0 -5
- package/dist/matter/behaviors/AirQualityBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/AirQualityBehavior.spec.js +0 -46
- package/dist/matter/behaviors/AirQualityBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts +0 -2
- package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/BehaviorRegistry.spec.js +0 -307
- package/dist/matter/behaviors/BehaviorRegistry.spec.js.map +0 -1
- package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/ColorControlBehavior.spec.js +0 -29
- package/dist/matter/behaviors/ColorControlBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.spec.d.ts +0 -5
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.spec.js +0 -95
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/DoorLockBehavior.spec.js +0 -120
- package/dist/matter/behaviors/DoorLockBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/FanControlBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/FanControlBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/FanControlBehavior.spec.js +0 -23
- package/dist/matter/behaviors/FanControlBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/IdentifyBehavior.spec.js +0 -64
- package/dist/matter/behaviors/IdentifyBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/LevelControlBehavior.spec.js +0 -145
- package/dist/matter/behaviors/LevelControlBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/OnOffBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/OnOffBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/OnOffBehavior.spec.js +0 -128
- package/dist/matter/behaviors/OnOffBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js +0 -57
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js +0 -55
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.js +0 -57
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.js +0 -53
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/ThermostatBehavior.spec.js +0 -23
- package/dist/matter/behaviors/ThermostatBehavior.spec.js.map +0 -1
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts +0 -2
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts.map +0 -1
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.js +0 -27
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.js.map +0 -1
- package/dist/matter/configValidator.spec.d.ts +0 -2
- package/dist/matter/configValidator.spec.d.ts.map +0 -1
- package/dist/matter/configValidator.spec.js +0 -390
- package/dist/matter/configValidator.spec.js.map +0 -1
- package/dist/matter/errorHandler.spec.d.ts +0 -2
- package/dist/matter/errorHandler.spec.d.ts.map +0 -1
- package/dist/matter/errorHandler.spec.js +0 -159
- package/dist/matter/errorHandler.spec.js.map +0 -1
- package/dist/matter/logFormatter.spec.d.ts +0 -2
- package/dist/matter/logFormatter.spec.d.ts.map +0 -1
- package/dist/matter/logFormatter.spec.js +0 -252
- package/dist/matter/logFormatter.spec.js.map +0 -1
- package/dist/matter/serverHelpers.spec.d.ts +0 -2
- package/dist/matter/serverHelpers.spec.d.ts.map +0 -1
- package/dist/matter/serverHelpers.spec.js +0 -527
- package/dist/matter/serverHelpers.spec.js.map +0 -1
- package/dist/matter/storage.spec.d.ts +0 -2
- package/dist/matter/storage.spec.d.ts.map +0 -1
- package/dist/matter/storage.spec.js +0 -570
- package/dist/matter/storage.spec.js.map +0 -1
- package/dist/matter/typeHelpers.spec.d.ts +0 -2
- package/dist/matter/typeHelpers.spec.d.ts.map +0 -1
- package/dist/matter/typeHelpers.spec.js +0 -127
- package/dist/matter/typeHelpers.spec.js.map +0 -1
- package/dist/platformAccessory.spec.d.ts +0 -2
- package/dist/platformAccessory.spec.d.ts.map +0 -1
- package/dist/platformAccessory.spec.js +0 -126
- package/dist/platformAccessory.spec.js.map +0 -1
- package/dist/pluginManager.spec.d.ts +0 -2
- package/dist/pluginManager.spec.d.ts.map +0 -1
- package/dist/pluginManager.spec.js +0 -43
- package/dist/pluginManager.spec.js.map +0 -1
- package/dist/server.spec.d.ts +0 -2
- package/dist/server.spec.d.ts.map +0 -1
- package/dist/server.spec.js +0 -57
- package/dist/server.spec.js.map +0 -1
- package/dist/user.spec.d.ts +0 -2
- package/dist/user.spec.d.ts.map +0 -1
- package/dist/user.spec.js +0 -31
- package/dist/user.spec.js.map +0 -1
- package/dist/util/mac.spec.d.ts +0 -2
- package/dist/util/mac.spec.d.ts.map +0 -1
- package/dist/util/mac.spec.js +0 -36
- package/dist/util/mac.spec.js.map +0 -1
- package/dist/version.spec.d.ts +0 -2
- package/dist/version.spec.d.ts.map +0 -1
- package/dist/version.spec.js +0 -20
- package/dist/version.spec.js.map +0 -1
package/dist/matter/server.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Matter.js Server Implementation for Homebridge Plugin API
|
|
3
|
-
*
|
|
4
|
-
* This provides a Matter bridge that plugins can use to register
|
|
5
|
-
* Matter accessories via the Homebridge API.
|
|
6
|
-
*/
|
|
7
1
|
import { randomBytes } from 'node:crypto';
|
|
8
2
|
import { EventEmitter } from 'node:events';
|
|
9
3
|
import { constants } from 'node:fs';
|
|
@@ -32,21 +26,14 @@ import { MatterStorageManager } from './storage.js';
|
|
|
32
26
|
import { isDeviceType, withBehaviors, withFeatures } from './typeHelpers.js';
|
|
33
27
|
import { deviceTypes, MatterDeviceError, } from './types.js';
|
|
34
28
|
const log = Logger.withPrefix('Matter/Server');
|
|
35
|
-
/**
|
|
36
|
-
* Constants for Matter server configuration
|
|
37
|
-
*/
|
|
38
29
|
const DEFAULT_MATTER_PORT = 5540;
|
|
39
|
-
const DEFAULT_VENDOR_ID = 0xFFF1;
|
|
40
|
-
const DEFAULT_PRODUCT_ID = 0x8001;
|
|
41
|
-
const MAX_DEVICES_PER_BRIDGE = 1000;
|
|
30
|
+
const DEFAULT_VENDOR_ID = 0xFFF1;
|
|
31
|
+
const DEFAULT_PRODUCT_ID = 0x8001;
|
|
32
|
+
const MAX_DEVICES_PER_BRIDGE = 1000;
|
|
42
33
|
const SERVER_READY_TIMEOUT_MS = 5000;
|
|
43
34
|
const SERVER_READY_POLL_INTERVAL_MS = 100;
|
|
44
35
|
const SERVER_INIT_DELAY_MS = 200;
|
|
45
36
|
const MAX_PASSCODE_ATTEMPTS = 100;
|
|
46
|
-
/**
|
|
47
|
-
* Matter Server for Homebridge Plugin API
|
|
48
|
-
* Allows plugins to register Matter accessories explicitly
|
|
49
|
-
*/
|
|
50
37
|
export class MatterServer extends EventEmitter {
|
|
51
38
|
config;
|
|
52
39
|
serverNode = null;
|
|
@@ -56,28 +43,20 @@ export class MatterServer extends EventEmitter {
|
|
|
56
43
|
isRunning = false;
|
|
57
44
|
MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
|
|
58
45
|
shutdownHandler = null;
|
|
59
|
-
// Map cluster names to custom behavior classes
|
|
60
|
-
// Only clusters with user-triggered commands need custom behaviors
|
|
61
46
|
static CLUSTER_BEHAVIOR_MAP = {
|
|
62
|
-
// Core controls
|
|
63
47
|
onOff: HomebridgeOnOffServer,
|
|
64
48
|
levelControl: HomebridgeLevelControlServer,
|
|
65
49
|
colorControl: HomebridgeColorControlServer,
|
|
66
|
-
// Coverings & locks
|
|
67
50
|
windowCovering: HomebridgeWindowCoveringServer,
|
|
68
51
|
doorLock: HomebridgeDoorLockServer,
|
|
69
|
-
// Climate control
|
|
70
52
|
fanControl: HomebridgeFanControlServer,
|
|
71
53
|
thermostat: HomebridgeThermostatServer,
|
|
72
|
-
// Robotic vacuum cleaners
|
|
73
54
|
rvcOperationalState: HomebridgeRvcOperationalStateServer,
|
|
74
55
|
rvcRunMode: HomebridgeRvcRunModeServer,
|
|
75
56
|
rvcCleanMode: HomebridgeRvcCleanModeServer,
|
|
76
57
|
serviceArea: HomebridgeServiceAreaServer,
|
|
77
|
-
// Identification
|
|
78
58
|
identify: HomebridgeIdentifyServer,
|
|
79
59
|
};
|
|
80
|
-
// Internal commissioning values (generated, not user-configurable)
|
|
81
60
|
passcode = 0;
|
|
82
61
|
discriminator = 0;
|
|
83
62
|
vendorId;
|
|
@@ -88,12 +67,15 @@ export class MatterServer extends EventEmitter {
|
|
|
88
67
|
storageManager = null;
|
|
89
68
|
matterStoragePath;
|
|
90
69
|
accessoryCache = null;
|
|
70
|
+
monitoringEnabled = false;
|
|
71
|
+
username;
|
|
72
|
+
bridgeName;
|
|
91
73
|
constructor(config) {
|
|
92
74
|
super();
|
|
93
|
-
// Store the validated config
|
|
94
75
|
this.config = this.validateAndSanitizeConfig(config);
|
|
95
|
-
|
|
96
|
-
|
|
76
|
+
const cleanId = this.config.uniqueId.replace(/[^A-F0-9]/gi, '');
|
|
77
|
+
this.username = cleanId.match(/.{1,2}/g)?.slice(0, 6).join(':').toUpperCase() || this.config.uniqueId;
|
|
78
|
+
this.bridgeName = this.config.serialNumber ? `Matter Bridge ${this.config.serialNumber}` : 'Matter Bridge';
|
|
97
79
|
if (this.config.debugModeEnabled) {
|
|
98
80
|
log.info('Matter debug mode enabled - verbose logging active');
|
|
99
81
|
MatterLogger.level = MatterLogLevel.DEBUG;
|
|
@@ -101,22 +83,15 @@ export class MatterServer extends EventEmitter {
|
|
|
101
83
|
else {
|
|
102
84
|
MatterLogger.level = MatterLogLevel.NOTICE;
|
|
103
85
|
}
|
|
104
|
-
// Set custom log format to match homebridge format
|
|
105
86
|
MatterLogger.format = createHomebridgeLogFormatter();
|
|
106
|
-
// Redirect all Matter.js logs to console.log to prevent console.debug() suppression.
|
|
107
|
-
// Matter.js uses console.debug() for DEBUG level logs, which is silently ignored in many Node.js environments.
|
|
108
87
|
MatterLogger.destinations.default.write = (text) => {
|
|
109
|
-
// Skip empty strings to avoid blank lines (from suppressed log facilities)
|
|
110
88
|
if (text.trim() !== '') {
|
|
111
|
-
console.log(text);
|
|
89
|
+
console.log(text);
|
|
112
90
|
}
|
|
113
91
|
};
|
|
114
|
-
// Initialize commissioning values (will be loaded from storage in start())
|
|
115
92
|
this.vendorId = DEFAULT_VENDOR_ID;
|
|
116
93
|
this.productId = DEFAULT_PRODUCT_ID;
|
|
117
|
-
|
|
118
|
-
this.behaviorRegistry = new BehaviorRegistry(this.accessories);
|
|
119
|
-
// Set the registry on all custom behavior classes
|
|
94
|
+
this.behaviorRegistry = new BehaviorRegistry(this.accessories, this);
|
|
120
95
|
HomebridgeAirQualityServer.setRegistry(this.behaviorRegistry);
|
|
121
96
|
HomebridgeCarbonMonoxideConcentrationMeasurementServer.setRegistry(this.behaviorRegistry);
|
|
122
97
|
HomebridgeColorControlServer.setRegistry(this.behaviorRegistry);
|
|
@@ -136,19 +111,13 @@ export class MatterServer extends EventEmitter {
|
|
|
136
111
|
HomebridgeThermostatServer.setRegistry(this.behaviorRegistry);
|
|
137
112
|
HomebridgeWindowCoveringServer.setRegistry(this.behaviorRegistry);
|
|
138
113
|
}
|
|
139
|
-
/**
|
|
140
|
-
* Validate and sanitize Matter server configuration
|
|
141
|
-
* Throws descriptive errors if configuration is invalid
|
|
142
|
-
*/
|
|
143
114
|
validateAndSanitizeConfig(config) {
|
|
144
115
|
const errors = [];
|
|
145
|
-
// Validate port
|
|
146
116
|
const port = config.port || DEFAULT_MATTER_PORT;
|
|
147
117
|
const portValidation = validatePort(port, false);
|
|
148
118
|
if (!portValidation.valid) {
|
|
149
119
|
errors.push(`Invalid port: ${portValidation.error}`);
|
|
150
120
|
}
|
|
151
|
-
// Validate and sanitize uniqueId (REQUIRED)
|
|
152
121
|
if (!config.uniqueId) {
|
|
153
122
|
errors.push('uniqueId is required for Matter server configuration');
|
|
154
123
|
}
|
|
@@ -158,36 +127,28 @@ export class MatterServer extends EventEmitter {
|
|
|
158
127
|
if (uniqueId.length === 0) {
|
|
159
128
|
errors.push('Invalid uniqueId: must be a non-empty string');
|
|
160
129
|
}
|
|
161
|
-
// Validate storagePath (if provided)
|
|
162
130
|
let storagePath = config.storagePath;
|
|
163
131
|
if (storagePath !== undefined) {
|
|
164
|
-
storagePath = resolve(storagePath);
|
|
132
|
+
storagePath = resolve(storagePath);
|
|
165
133
|
}
|
|
166
|
-
// Validate and sanitize manufacturer
|
|
167
134
|
let manufacturer = config.manufacturer;
|
|
168
135
|
if (manufacturer !== undefined) {
|
|
169
136
|
manufacturer = truncateString(manufacturer, 32, 'Manufacturer name').value;
|
|
170
137
|
}
|
|
171
|
-
// Validate and sanitize model
|
|
172
138
|
let model = config.model;
|
|
173
139
|
if (model !== undefined) {
|
|
174
140
|
model = truncateString(model, 32, 'Model name').value;
|
|
175
141
|
}
|
|
176
|
-
// Validate firmwareRevision
|
|
177
142
|
let firmwareRevision = config.firmwareRevision;
|
|
178
143
|
if (firmwareRevision !== undefined) {
|
|
179
144
|
firmwareRevision = truncateString(firmwareRevision, 64, 'Firmware revision').value;
|
|
180
145
|
}
|
|
181
|
-
// Validate serialNumber
|
|
182
146
|
let serialNumber = config.serialNumber;
|
|
183
147
|
if (serialNumber !== undefined) {
|
|
184
148
|
serialNumber = truncateString(serialNumber, 32, 'Serial number').value;
|
|
185
149
|
}
|
|
186
|
-
// Validate debugModeEnabled
|
|
187
150
|
const debugModeEnabled = config.debugModeEnabled || false;
|
|
188
|
-
// Validate externalAccessory
|
|
189
151
|
const externalAccessory = config.externalAccessory || false;
|
|
190
|
-
// Throw if there are validation errors
|
|
191
152
|
if (errors.length > 0) {
|
|
192
153
|
throw new MatterDeviceError(`Matter configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
193
154
|
}
|
|
@@ -203,13 +164,6 @@ export class MatterServer extends EventEmitter {
|
|
|
203
164
|
externalAccessory,
|
|
204
165
|
};
|
|
205
166
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Generate a secure random passcode
|
|
208
|
-
* According to Matter spec, passcode must be:
|
|
209
|
-
* - 8 digits (00000001 to 99999998)
|
|
210
|
-
* - Not in the invalid list
|
|
211
|
-
* - Not sequential or repeating patterns
|
|
212
|
-
*/
|
|
213
167
|
generateSecurePasscode() {
|
|
214
168
|
let passcode;
|
|
215
169
|
const maxAttempts = MAX_PASSCODE_ATTEMPTS;
|
|
@@ -229,9 +183,7 @@ export class MatterServer extends EventEmitter {
|
|
|
229
183
|
87654321,
|
|
230
184
|
];
|
|
231
185
|
do {
|
|
232
|
-
// Use cryptographically secure random number generation
|
|
233
186
|
const randomValue = randomBytes(4).readUInt32BE(0);
|
|
234
|
-
// Generate a value between 1 and 99999998
|
|
235
187
|
passcode = (randomValue % 99999998) + 1;
|
|
236
188
|
attempts++;
|
|
237
189
|
if (attempts > maxAttempts) {
|
|
@@ -241,17 +193,11 @@ export class MatterServer extends EventEmitter {
|
|
|
241
193
|
|| !this.isValidPasscode(passcode));
|
|
242
194
|
return passcode;
|
|
243
195
|
}
|
|
244
|
-
/**
|
|
245
|
-
* Validate a passcode according to Matter specifications
|
|
246
|
-
*/
|
|
247
196
|
isValidPasscode(passcode) {
|
|
248
|
-
// Must be between 1 and 99999998
|
|
249
197
|
if (passcode < 1 || passcode > 99999998) {
|
|
250
198
|
return false;
|
|
251
199
|
}
|
|
252
|
-
// Convert to 8-digit string
|
|
253
200
|
const passcodeStr = passcode.toString().padStart(8, '0');
|
|
254
|
-
// Check for sequential patterns (12345678, 23456789, etc.)
|
|
255
201
|
let isSequential = true;
|
|
256
202
|
for (let i = 1; i < passcodeStr.length; i++) {
|
|
257
203
|
if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
|
|
@@ -262,7 +208,6 @@ export class MatterServer extends EventEmitter {
|
|
|
262
208
|
if (isSequential) {
|
|
263
209
|
return false;
|
|
264
210
|
}
|
|
265
|
-
// Check for reverse sequential (87654321, 76543210, etc.)
|
|
266
211
|
let isReverseSequential = true;
|
|
267
212
|
for (let i = 1; i < passcodeStr.length; i++) {
|
|
268
213
|
if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
|
|
@@ -273,7 +218,6 @@ export class MatterServer extends EventEmitter {
|
|
|
273
218
|
if (isReverseSequential) {
|
|
274
219
|
return false;
|
|
275
220
|
}
|
|
276
|
-
// Check for too many repeating digits (more than 3 of same digit)
|
|
277
221
|
const digitCounts = new Map();
|
|
278
222
|
for (const digit of passcodeStr) {
|
|
279
223
|
digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
|
|
@@ -284,46 +228,18 @@ export class MatterServer extends EventEmitter {
|
|
|
284
228
|
}
|
|
285
229
|
return true;
|
|
286
230
|
}
|
|
287
|
-
/**
|
|
288
|
-
* Generate a random discriminator
|
|
289
|
-
* According to Matter spec, discriminator must be:
|
|
290
|
-
* - 12 bits (0-4095)
|
|
291
|
-
* - Should be random for security
|
|
292
|
-
*/
|
|
293
231
|
generateRandomDiscriminator() {
|
|
294
|
-
|
|
295
|
-
const discriminator = randomBytes(2).readUInt16BE(0) & 0x0FFF; // Mask to 12 bits
|
|
296
|
-
// Validate discriminator range
|
|
232
|
+
const discriminator = randomBytes(2).readUInt16BE(0) & 0x0FFF;
|
|
297
233
|
if (discriminator < 0 || discriminator > 4095) {
|
|
298
234
|
throw new Error(`Invalid discriminator generated: ${discriminator}`);
|
|
299
235
|
}
|
|
300
236
|
return discriminator;
|
|
301
237
|
}
|
|
302
|
-
/**
|
|
303
|
-
* Create ServerNode with automatic recovery from corrupted storage
|
|
304
|
-
*
|
|
305
|
-
* Matter.js can fail to start if fabric data is corrupted (common after
|
|
306
|
-
* hard shutdowns or disk errors). This method implements automatic recovery by:
|
|
307
|
-
*
|
|
308
|
-
* 1. Attempting normal ServerNode creation
|
|
309
|
-
* 2. If it fails with storage errors, identifying and removing corrupted files
|
|
310
|
-
* 3. Retrying ServerNode creation with fresh storage
|
|
311
|
-
*
|
|
312
|
-
* This prevents the need for manual intervention while preserving data
|
|
313
|
-
* safety by only removing storage on confirmed corruption errors.
|
|
314
|
-
*
|
|
315
|
-
* @param nodeOptions - Matter.js ServerNode configuration
|
|
316
|
-
* @param sanitizedId - Filesystem-safe bridge identifier
|
|
317
|
-
* @returns Initialized ServerNode instance
|
|
318
|
-
* @throws Error if recovery fails or error is not storage-related
|
|
319
|
-
*/
|
|
320
238
|
async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
|
|
321
239
|
try {
|
|
322
|
-
// First attempt to create ServerNode
|
|
323
240
|
return await ServerNode.create(nodeOptions);
|
|
324
241
|
}
|
|
325
242
|
catch (error) {
|
|
326
|
-
// Check if this is a storage corruption error
|
|
327
243
|
const errorMessage = error instanceof Error ? error.message : '';
|
|
328
244
|
const causeMessage = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
|
|
329
245
|
const isStorageError = errorMessage.includes('Invalid public key encoding')
|
|
@@ -331,12 +247,9 @@ export class MatterServer extends EventEmitter {
|
|
|
331
247
|
|| errorMessage.includes('key-input')
|
|
332
248
|
|| causeMessage.includes('Invalid public key encoding');
|
|
333
249
|
if (!isStorageError) {
|
|
334
|
-
// Not a storage error, rethrow
|
|
335
250
|
throw error;
|
|
336
251
|
}
|
|
337
|
-
// Storage is corrupted - clean up and retry
|
|
338
252
|
log.warn('Detected corrupted Matter storage, attempting automatic recovery...');
|
|
339
|
-
// The ServerNodeStore directory is inside our storage path with the same name as the bridge ID
|
|
340
253
|
const environment = Environment.default;
|
|
341
254
|
const storageService = environment.get(StorageService);
|
|
342
255
|
const storageLocation = storageService.location;
|
|
@@ -347,7 +260,6 @@ export class MatterServer extends EventEmitter {
|
|
|
347
260
|
const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
|
|
348
261
|
try {
|
|
349
262
|
let removedSomething = false;
|
|
350
|
-
// Delete the ServerNodeStore subdirectory (async check and removal)
|
|
351
263
|
try {
|
|
352
264
|
await fse.stat(serverNodeStorePath);
|
|
353
265
|
log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
|
|
@@ -360,7 +272,6 @@ export class MatterServer extends EventEmitter {
|
|
|
360
272
|
throw err;
|
|
361
273
|
}
|
|
362
274
|
}
|
|
363
|
-
// Delete the ServerNodeStore JSON file (contains fabric data)
|
|
364
275
|
try {
|
|
365
276
|
await fse.stat(serverNodeStoreJsonFile);
|
|
366
277
|
log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
|
|
@@ -379,7 +290,6 @@ export class MatterServer extends EventEmitter {
|
|
|
379
290
|
else {
|
|
380
291
|
log.warn('No corrupted storage files found, corruption may be elsewhere');
|
|
381
292
|
}
|
|
382
|
-
// Retry ServerNode creation
|
|
383
293
|
const serverNode = await ServerNode.create(nodeOptions);
|
|
384
294
|
log.info('Successfully recovered from corrupted Matter storage');
|
|
385
295
|
return serverNode;
|
|
@@ -392,9 +302,6 @@ export class MatterServer extends EventEmitter {
|
|
|
392
302
|
}
|
|
393
303
|
}
|
|
394
304
|
}
|
|
395
|
-
/**
|
|
396
|
-
* Start the Matter server
|
|
397
|
-
*/
|
|
398
305
|
async start() {
|
|
399
306
|
if (this.isRunning) {
|
|
400
307
|
log.warn('Matter server is already running');
|
|
@@ -402,20 +309,14 @@ export class MatterServer extends EventEmitter {
|
|
|
402
309
|
}
|
|
403
310
|
try {
|
|
404
311
|
log.info('Starting Matter.js server...');
|
|
405
|
-
// IMPORTANT: Storage must be configured BEFORE any Matter.js operations
|
|
406
|
-
// This ensures persistent fabric data across restarts
|
|
407
312
|
await this.setupStorage();
|
|
408
|
-
// Load or generate commissioning credentials
|
|
409
313
|
await this.loadOrGenerateCredentials();
|
|
410
314
|
log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
|
|
411
|
-
// Configure network interfaces if specified in the config
|
|
412
315
|
if (this.config.networkInterfaces && this.config.networkInterfaces.length > 0) {
|
|
413
316
|
const environment = Environment.default;
|
|
414
317
|
const interfaceConfig = {};
|
|
415
|
-
// Map each interface name to type 2 (Ethernet) as default
|
|
416
|
-
// Matter.js will use only these interfaces for the server
|
|
417
318
|
for (const interfaceName of this.config.networkInterfaces) {
|
|
418
|
-
interfaceConfig[interfaceName] = { type: 2 };
|
|
319
|
+
interfaceConfig[interfaceName] = { type: 2 };
|
|
419
320
|
}
|
|
420
321
|
environment.vars.set('network.interface', interfaceConfig);
|
|
421
322
|
log.info(`Configured Matter server to use network interfaces: ${this.config.networkInterfaces.join(', ')}`);
|
|
@@ -423,33 +324,29 @@ export class MatterServer extends EventEmitter {
|
|
|
423
324
|
else {
|
|
424
325
|
log.debug('No network interfaces specified, using all available interfaces');
|
|
425
326
|
}
|
|
426
|
-
// Create commissioning options
|
|
427
327
|
const commissioningOptions = {
|
|
428
328
|
passcode: this.passcode,
|
|
429
329
|
discriminator: this.discriminator,
|
|
430
330
|
};
|
|
431
331
|
log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
|
|
432
|
-
// Use different names based on mode
|
|
433
332
|
const displayName = this.config.externalAccessory
|
|
434
333
|
? (this.config.model || 'Matter Device')
|
|
435
334
|
: 'Homebridge Matter Bridge';
|
|
436
|
-
// uniqueId is already sanitized in validateAndSanitizeConfig()
|
|
437
335
|
const sanitizedId = this.config.uniqueId;
|
|
438
|
-
// Create node options with proper typing
|
|
439
336
|
const nodeOptions = {
|
|
440
337
|
id: sanitizedId,
|
|
441
338
|
network: {
|
|
442
339
|
port: this.config.port,
|
|
443
|
-
ipv4: true,
|
|
340
|
+
ipv4: true,
|
|
444
341
|
},
|
|
445
342
|
commissioning: commissioningOptions,
|
|
446
343
|
basicInformation: {
|
|
447
|
-
nodeLabel: displayName.slice(0, 32),
|
|
344
|
+
nodeLabel: displayName.slice(0, 32),
|
|
448
345
|
vendorId: VendorId(this.vendorId),
|
|
449
346
|
vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
|
|
450
347
|
productId: this.productId,
|
|
451
348
|
productName: displayName.slice(0, 32),
|
|
452
|
-
productLabel: displayName.slice(0, 64),
|
|
349
|
+
productLabel: displayName.slice(0, 64),
|
|
453
350
|
serialNumber: this.serialNumber = this.config.serialNumber || this.config.uniqueId,
|
|
454
351
|
hardwareVersion: 1,
|
|
455
352
|
hardwareVersionString: release(),
|
|
@@ -458,51 +355,39 @@ export class MatterServer extends EventEmitter {
|
|
|
458
355
|
reachable: true,
|
|
459
356
|
},
|
|
460
357
|
};
|
|
461
|
-
// Only add productDescription with bridge deviceType in bridge mode
|
|
462
358
|
if (!this.config.externalAccessory) {
|
|
463
359
|
nodeOptions.productDescription = {
|
|
464
360
|
name: displayName,
|
|
465
361
|
deviceType: AggregatorEndpoint.deviceType,
|
|
466
362
|
};
|
|
467
363
|
}
|
|
468
|
-
// Create server node with automatic recovery from corrupted storage
|
|
469
364
|
this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId);
|
|
470
|
-
// Set up commissioning event listeners
|
|
471
365
|
this.setupCommissioningEventListeners();
|
|
472
|
-
// Create aggregator endpoint for bridge pattern (skip for external accessories)
|
|
473
366
|
if (!this.config.externalAccessory) {
|
|
474
367
|
this.aggregator = new Endpoint(AggregatorEndpoint, {
|
|
475
368
|
id: 'homebridge-aggregator',
|
|
476
369
|
});
|
|
477
|
-
// Add aggregator to server
|
|
478
370
|
await this.serverNode.add(this.aggregator);
|
|
479
371
|
log.debug('Created aggregator endpoint for bridged mode');
|
|
480
372
|
}
|
|
481
373
|
else {
|
|
482
374
|
log.debug('External accessory mode - skipping aggregator creation');
|
|
483
375
|
}
|
|
484
|
-
// Generate and display commissioning information
|
|
485
376
|
await this.generateCommissioningInfo();
|
|
486
|
-
// Set up graceful shutdown handler
|
|
487
377
|
this.shutdownHandler = async () => {
|
|
488
378
|
log.info('Shutting down Matter server...');
|
|
489
379
|
await this.stop();
|
|
490
380
|
};
|
|
491
|
-
// Register shutdown handlers
|
|
492
381
|
process.on('SIGINT', this.shutdownHandler);
|
|
493
382
|
process.on('SIGTERM', this.shutdownHandler);
|
|
494
|
-
// If external accessory mode, skip running the server now (will be run later via runServer())
|
|
495
383
|
if (!this.config.externalAccessory) {
|
|
496
|
-
// Start the server in a non-blocking way
|
|
497
384
|
this.serverNode.run().then(() => {
|
|
498
385
|
log.info('Matter server stopped normally');
|
|
499
386
|
}, (error) => {
|
|
500
387
|
log.error('Matter server stopped with error:', error);
|
|
501
388
|
errorHandler.handleError(error, 'server-runtime');
|
|
502
389
|
});
|
|
503
|
-
// Wait for server to be ready
|
|
504
390
|
await this.waitForServerReady();
|
|
505
|
-
// Load cached accessories (don't restore them yet - wait for plugins to re-register)
|
|
506
391
|
if (this.accessoryCache) {
|
|
507
392
|
const loaded = await this.accessoryCache.load();
|
|
508
393
|
log.debug(`Matter cache loaded: ${loaded.size} accessories`);
|
|
@@ -510,7 +395,6 @@ export class MatterServer extends EventEmitter {
|
|
|
510
395
|
else {
|
|
511
396
|
log.debug('No accessory cache available');
|
|
512
397
|
}
|
|
513
|
-
// Update commissioning file to reflect current state
|
|
514
398
|
this.updateCommissioningFile().catch((error) => {
|
|
515
399
|
log.warn('Failed to update commissioning file on startup:', error);
|
|
516
400
|
});
|
|
@@ -528,20 +412,6 @@ export class MatterServer extends EventEmitter {
|
|
|
528
412
|
throw error;
|
|
529
413
|
}
|
|
530
414
|
}
|
|
531
|
-
/**
|
|
532
|
-
* Run the server after devices have been added (for external accessory mode)
|
|
533
|
-
*
|
|
534
|
-
* This must be called after registerPlatformAccessories() when using externalAccessory mode.
|
|
535
|
-
* In bridge mode, the server starts automatically when accessories are registered.
|
|
536
|
-
*
|
|
537
|
-
* @throws {MatterDeviceError} If server node is not initialized or server is already running
|
|
538
|
-
* @example
|
|
539
|
-
* ```typescript
|
|
540
|
-
* await matterServer.start()
|
|
541
|
-
* await matterServer.registerPlatformAccessories('plugin', 'platform', accessories)
|
|
542
|
-
* await matterServer.runServer() // External accessory mode only
|
|
543
|
-
* ```
|
|
544
|
-
*/
|
|
545
415
|
async runServer() {
|
|
546
416
|
if (!this.serverNode) {
|
|
547
417
|
throw new MatterDeviceError('Server node not initialized - call start() first');
|
|
@@ -554,16 +424,13 @@ export class MatterServer extends EventEmitter {
|
|
|
554
424
|
throw new MatterDeviceError('runServer() should only be called when externalAccessory mode is enabled');
|
|
555
425
|
}
|
|
556
426
|
log.debug('Running deferred server with device(s) already attached');
|
|
557
|
-
// Start the server in a non-blocking way
|
|
558
427
|
this.serverNode.run().then(() => {
|
|
559
428
|
log.info('Matter server stopped normally');
|
|
560
429
|
}, (error) => {
|
|
561
430
|
log.error('Matter server stopped with error:', error);
|
|
562
431
|
errorHandler.handleError(error, 'server-runtime');
|
|
563
432
|
});
|
|
564
|
-
// Wait for server to be ready
|
|
565
433
|
await this.waitForServerReady();
|
|
566
|
-
// Load cached accessories (don't restore them yet - wait for plugins to re-register)
|
|
567
434
|
if (this.accessoryCache) {
|
|
568
435
|
const loaded = await this.accessoryCache.load();
|
|
569
436
|
log.debug(`Matter cache loaded: ${loaded.size} accessories`);
|
|
@@ -571,34 +438,27 @@ export class MatterServer extends EventEmitter {
|
|
|
571
438
|
else {
|
|
572
439
|
log.debug('No accessory cache available');
|
|
573
440
|
}
|
|
574
|
-
// Update commissioning file to reflect current state
|
|
575
441
|
this.updateCommissioningFile().catch((error) => {
|
|
576
442
|
log.warn('Failed to update commissioning file on startup:', error);
|
|
577
443
|
});
|
|
578
444
|
this.isRunning = true;
|
|
579
445
|
log.info('Matter server is now running');
|
|
580
446
|
}
|
|
581
|
-
/**
|
|
582
|
-
* Set up and validate storage
|
|
583
|
-
*/
|
|
584
447
|
async setupStorage() {
|
|
585
448
|
if (!this.config.storagePath) {
|
|
586
449
|
throw new Error('Storage path is required for Matter server');
|
|
587
450
|
}
|
|
588
|
-
// Resolve to absolute path and validate
|
|
589
451
|
const storagePath = resolve(this.config.storagePath);
|
|
590
452
|
const normalizedPath = normalize(storagePath);
|
|
591
|
-
// Ensure path is within allowed directories
|
|
592
453
|
const allowedBasePaths = [
|
|
593
454
|
resolve(homedir(), '.homebridge'),
|
|
594
455
|
resolve(process.cwd()),
|
|
595
|
-
'/var/lib/homebridge',
|
|
456
|
+
'/var/lib/homebridge',
|
|
596
457
|
];
|
|
597
458
|
const isAllowed = allowedBasePaths.some(basePath => normalizedPath.startsWith(basePath));
|
|
598
459
|
if (!isAllowed || normalizedPath.includes('..')) {
|
|
599
460
|
throw new Error(`Storage path not allowed: ${normalizedPath}. Must be within homebridge directories.`);
|
|
600
461
|
}
|
|
601
|
-
// Ensure the storage directory exists with proper permissions
|
|
602
462
|
try {
|
|
603
463
|
await fse.ensureDir(normalizedPath);
|
|
604
464
|
await access(normalizedPath, constants.R_OK | constants.W_OK);
|
|
@@ -606,34 +466,24 @@ export class MatterServer extends EventEmitter {
|
|
|
606
466
|
catch (error) {
|
|
607
467
|
throw new Error(`Storage path not accessible: ${error}`);
|
|
608
468
|
}
|
|
609
|
-
// Create bridge-specific storage directory
|
|
610
|
-
// uniqueId is already sanitized in validateAndSanitizeConfig()
|
|
611
469
|
const bridgeId = this.config.uniqueId || 'default';
|
|
612
470
|
this.matterStoragePath = join(normalizedPath, bridgeId);
|
|
613
471
|
await fse.ensureDir(this.matterStoragePath);
|
|
614
|
-
// Create storage manager
|
|
615
472
|
this.storageManager = new MatterStorageManager(this.matterStoragePath);
|
|
616
|
-
// Create accessory cache
|
|
617
473
|
this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
|
|
618
|
-
// Configure environment to use our custom storage
|
|
619
474
|
const environment = Environment.default;
|
|
620
475
|
const storageService = environment.get(StorageService);
|
|
621
476
|
storageService.location = this.matterStoragePath;
|
|
622
|
-
// CRITICAL: Override storage factory with custom implementation
|
|
623
|
-
// This ensures fabric data is properly persisted
|
|
624
477
|
storageService.factory = (namespace) => {
|
|
625
478
|
if (!this.storageManager) {
|
|
626
479
|
throw new Error('Storage manager not initialized');
|
|
627
480
|
}
|
|
628
481
|
const storage = this.storageManager.getStorage(namespace);
|
|
629
|
-
// Initialize asynchronously - Matter.js handles async storage properly
|
|
630
482
|
storage.initialize().catch((error) => {
|
|
631
483
|
log.error(`Failed to initialize storage namespace ${namespace}:`, error);
|
|
632
484
|
});
|
|
633
|
-
// Note: Cast to unknown first to satisfy TypeScript - our storage implements the required interface
|
|
634
485
|
return storage;
|
|
635
486
|
};
|
|
636
|
-
// Add cleanup handler for storage
|
|
637
487
|
this.cleanupHandlers.push(async () => {
|
|
638
488
|
if (this.storageManager) {
|
|
639
489
|
await this.storageManager.closeAll();
|
|
@@ -641,71 +491,54 @@ export class MatterServer extends EventEmitter {
|
|
|
641
491
|
});
|
|
642
492
|
log.info(`Matter storage initialized at: ${this.matterStoragePath}`);
|
|
643
493
|
}
|
|
644
|
-
/**
|
|
645
|
-
* Load or generate commissioning credentials (passcode and discriminator)
|
|
646
|
-
* These must be persistent across restarts to maintain the same QR code
|
|
647
|
-
*/
|
|
648
494
|
async loadOrGenerateCredentials() {
|
|
649
495
|
if (!this.storageManager) {
|
|
650
496
|
throw new Error('Storage manager not initialized');
|
|
651
497
|
}
|
|
652
|
-
// Use 'credentials' namespace
|
|
653
498
|
const storage = this.storageManager.getStorage('credentials');
|
|
654
|
-
// CRITICAL: Initialize storage before reading to avoid race condition
|
|
655
499
|
await storage.initialize();
|
|
656
|
-
// Try to load existing credentials
|
|
657
500
|
const storedPasscode = storage.get([], 'passcode');
|
|
658
501
|
const storedDiscriminator = storage.get([], 'discriminator');
|
|
659
502
|
if (storedPasscode && storedDiscriminator) {
|
|
660
|
-
// Use stored credentials
|
|
661
503
|
log.info('Loading existing commissioning credentials from storage');
|
|
662
504
|
this.passcode = storedPasscode;
|
|
663
505
|
this.discriminator = storedDiscriminator;
|
|
664
506
|
}
|
|
665
507
|
else {
|
|
666
|
-
// Generate new credentials and store them
|
|
667
508
|
log.info('Generating new commissioning credentials');
|
|
668
509
|
this.passcode = this.generateSecurePasscode();
|
|
669
510
|
this.discriminator = this.generateRandomDiscriminator();
|
|
670
|
-
// Store for future use
|
|
671
511
|
storage.set([], 'passcode', this.passcode);
|
|
672
512
|
storage.set([], 'discriminator', this.discriminator);
|
|
673
513
|
log.info('Commissioning credentials saved to storage');
|
|
674
514
|
}
|
|
675
515
|
}
|
|
676
|
-
/**
|
|
677
|
-
* Generate and display commissioning information
|
|
678
|
-
*/
|
|
679
516
|
async generateCommissioningInfo() {
|
|
680
517
|
const passcode = this.passcode.toString().padStart(8, '0');
|
|
681
518
|
const discriminator = this.discriminator;
|
|
682
519
|
const vendorId = this.vendorId;
|
|
683
520
|
const productId = this.productId;
|
|
684
|
-
// Use Matter.js library to generate pairing codes properly
|
|
685
521
|
const manualCode = ManualPairingCodeCodec.encode({
|
|
686
522
|
discriminator,
|
|
687
523
|
passcode: this.passcode,
|
|
688
524
|
});
|
|
689
|
-
// Format as XXXX-XXX-XXXX for display
|
|
690
525
|
const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`;
|
|
691
526
|
log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`);
|
|
692
527
|
const qrCodePayload = QrPairingCodeCodec.encode([{
|
|
693
528
|
version: 0,
|
|
694
529
|
vendorId,
|
|
695
530
|
productId,
|
|
696
|
-
flowType: 0,
|
|
697
|
-
discoveryCapabilities: 4,
|
|
531
|
+
flowType: 0,
|
|
532
|
+
discoveryCapabilities: 4,
|
|
698
533
|
discriminator,
|
|
699
534
|
passcode: this.passcode,
|
|
700
535
|
}]);
|
|
701
536
|
log.info(`Generated QR code: ${qrCodePayload}`);
|
|
702
537
|
log.info(`Generated manual code: ${manualPairingCode}`);
|
|
703
|
-
// Store commissioning info
|
|
704
538
|
this.commissioningInfo = {
|
|
705
539
|
qrCode: qrCodePayload,
|
|
706
540
|
manualPairingCode,
|
|
707
541
|
};
|
|
708
|
-
// Save commissioning info to disk for UI access
|
|
709
542
|
try {
|
|
710
543
|
if (!this.matterStoragePath) {
|
|
711
544
|
throw new Error('Matter storage path not initialized');
|
|
@@ -726,7 +559,6 @@ export class MatterServer extends EventEmitter {
|
|
|
726
559
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
727
560
|
log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
|
|
728
561
|
}
|
|
729
|
-
// Display commissioning information
|
|
730
562
|
log.info(`${'='.repeat(60)}`);
|
|
731
563
|
log.info('📱 MATTER COMMISSIONING INFORMATION');
|
|
732
564
|
log.info('='.repeat(60));
|
|
@@ -734,33 +566,21 @@ export class MatterServer extends EventEmitter {
|
|
|
734
566
|
log.info(`Passcode: ${passcode}`);
|
|
735
567
|
log.info(`Discriminator: ${discriminator}`);
|
|
736
568
|
log.info('QR Code for commissioning:');
|
|
737
|
-
// Generate and display QR code in terminal
|
|
738
569
|
QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
|
|
739
|
-
// eslint-disable-next-line no-console
|
|
740
570
|
console.log(qrcode);
|
|
741
571
|
});
|
|
742
572
|
log.info(`${'='.repeat(60)}`);
|
|
743
573
|
}
|
|
744
|
-
/**
|
|
745
|
-
* Wait for the server to be ready
|
|
746
|
-
*/
|
|
747
574
|
async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS) {
|
|
748
575
|
const startTime = Date.now();
|
|
749
|
-
// In external accessory mode, only wait for serverNode (no aggregator)
|
|
750
|
-
// In bridge mode, wait for both serverNode and aggregator
|
|
751
576
|
while (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
|
|
752
577
|
if (Date.now() - startTime > maxWaitTime) {
|
|
753
578
|
throw new Error('Server failed to become ready within timeout');
|
|
754
579
|
}
|
|
755
580
|
await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS));
|
|
756
581
|
}
|
|
757
|
-
// Additional small delay to ensure everything is initialized
|
|
758
582
|
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
759
583
|
}
|
|
760
|
-
/**
|
|
761
|
-
* Set up Matter.js commissioning event listeners
|
|
762
|
-
* Uses native Matter.js events instead of file watching for reliability
|
|
763
|
-
*/
|
|
764
584
|
setupCommissioningEventListeners() {
|
|
765
585
|
if (!this.serverNode) {
|
|
766
586
|
log.warn('Cannot set up commissioning event listeners - serverNode not initialized');
|
|
@@ -768,37 +588,28 @@ export class MatterServer extends EventEmitter {
|
|
|
768
588
|
}
|
|
769
589
|
log.debug('Setting up commissioning event listeners');
|
|
770
590
|
try {
|
|
771
|
-
// Listen for fabric changes (add/remove/update)
|
|
772
591
|
this.serverNode.events.commissioning.fabricsChanged.on((fabricIndex, action) => {
|
|
773
592
|
log.info(`Fabric ${action}: index ${fabricIndex}`);
|
|
774
|
-
// Update commissioning file when fabrics change
|
|
775
593
|
this.updateCommissioningFile().catch((error) => {
|
|
776
594
|
log.warn('Failed to update commissioning file after fabric change:', error);
|
|
777
595
|
});
|
|
778
|
-
// Emit event for child bridge to update UI
|
|
779
596
|
const commissioned = this.isCommissioned();
|
|
780
597
|
const fabricCount = this.getCommissionedFabricCount();
|
|
781
598
|
this.emit('commissioning-status-changed', commissioned, fabricCount);
|
|
782
599
|
});
|
|
783
|
-
// Listen for commissioning (first fabric added)
|
|
784
600
|
this.serverNode.events.commissioning.commissioned.on(() => {
|
|
785
601
|
log.info('Bridge commissioned');
|
|
786
|
-
// Update commissioning file
|
|
787
602
|
this.updateCommissioningFile().catch((error) => {
|
|
788
603
|
log.warn('Failed to update commissioning file after commissioning:', error);
|
|
789
604
|
});
|
|
790
|
-
// Emit event for child bridge to update UI
|
|
791
605
|
const fabricCount = this.getCommissionedFabricCount();
|
|
792
606
|
this.emit('commissioning-status-changed', true, fabricCount);
|
|
793
607
|
});
|
|
794
|
-
// Listen for decommissioning (last fabric removed)
|
|
795
608
|
this.serverNode.events.commissioning.decommissioned.on(() => {
|
|
796
609
|
log.info('Bridge decommissioned');
|
|
797
|
-
// Update commissioning file
|
|
798
610
|
this.updateCommissioningFile().catch((error) => {
|
|
799
611
|
log.warn('Failed to update commissioning file after decommissioning:', error);
|
|
800
612
|
});
|
|
801
|
-
// Emit event for child bridge to update UI
|
|
802
613
|
this.emit('commissioning-status-changed', false, 0);
|
|
803
614
|
});
|
|
804
615
|
log.debug('Commissioning event listeners registered successfully');
|
|
@@ -807,9 +618,6 @@ export class MatterServer extends EventEmitter {
|
|
|
807
618
|
log.error('Failed to set up commissioning event listeners:', error);
|
|
808
619
|
}
|
|
809
620
|
}
|
|
810
|
-
/**
|
|
811
|
-
* Update commissioning info file when commissioning state changes
|
|
812
|
-
*/
|
|
813
621
|
async updateCommissioningFile() {
|
|
814
622
|
try {
|
|
815
623
|
if (!this.matterStoragePath) {
|
|
@@ -834,38 +642,16 @@ export class MatterServer extends EventEmitter {
|
|
|
834
642
|
log.debug(`Failed to update commissioning info file: ${errorMessage}`);
|
|
835
643
|
}
|
|
836
644
|
}
|
|
837
|
-
/**
|
|
838
|
-
* Register Matter platform accessories (Plugin API - matches HAP pattern)
|
|
839
|
-
*
|
|
840
|
-
* Registers Matter accessories from a dynamic platform plugin. Accessories are stored
|
|
841
|
-
* and automatically restored on server restart.
|
|
842
|
-
*
|
|
843
|
-
* @param pluginIdentifier - The plugin identifier (e.g., 'homebridge-example')
|
|
844
|
-
* @param platformName - The platform name from config.json
|
|
845
|
-
* @param accessories - Array of Matter accessories to register
|
|
846
|
-
* @throws {MatterDeviceError} If maximum device limit is reached or accessory is invalid
|
|
847
|
-
* @see {@link MatterAccessory} for accessory structure
|
|
848
|
-
*/
|
|
849
645
|
async registerPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
850
646
|
for (const accessory of accessories) {
|
|
851
647
|
await this.registerAccessory(pluginIdentifier, platformName, accessory);
|
|
852
648
|
}
|
|
853
649
|
}
|
|
854
|
-
/**
|
|
855
|
-
* Unregister Matter platform accessories (Plugin API - matches HAP pattern)
|
|
856
|
-
*/
|
|
857
650
|
async unregisterPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
858
651
|
for (const accessory of accessories) {
|
|
859
652
|
await this.unregisterAccessory(accessory.uuid);
|
|
860
653
|
}
|
|
861
654
|
}
|
|
862
|
-
/**
|
|
863
|
-
* Update Matter platform accessories in the cache
|
|
864
|
-
* Similar to api.updatePlatformAccessories() for HAP accessories
|
|
865
|
-
*
|
|
866
|
-
* This updates the cached accessory information without unregistering and re-registering.
|
|
867
|
-
* Useful when device metadata changes (name, manufacturer, firmware version, etc.)
|
|
868
|
-
*/
|
|
869
655
|
async updatePlatformAccessories(accessories) {
|
|
870
656
|
if (!this.accessoryCache) {
|
|
871
657
|
log.warn('Cannot update Matter platform accessories - cache not initialized');
|
|
@@ -873,7 +659,6 @@ export class MatterServer extends EventEmitter {
|
|
|
873
659
|
}
|
|
874
660
|
for (const accessory of accessories) {
|
|
875
661
|
const internal = accessory;
|
|
876
|
-
// Verify accessory exists in current session and cache
|
|
877
662
|
if (!this.accessories.has(accessory.uuid)) {
|
|
878
663
|
log.warn(`Cannot update Matter accessory ${accessory.uuid} - not registered in current session`);
|
|
879
664
|
continue;
|
|
@@ -882,25 +667,16 @@ export class MatterServer extends EventEmitter {
|
|
|
882
667
|
log.warn(`Cannot update Matter accessory ${accessory.uuid} - not found in cache`);
|
|
883
668
|
continue;
|
|
884
669
|
}
|
|
885
|
-
// Update the in-memory accessory
|
|
886
670
|
this.accessories.set(accessory.uuid, internal);
|
|
887
671
|
log.debug(`Updated Matter accessory ${accessory.uuid} (${accessory.displayName})`);
|
|
888
672
|
}
|
|
889
|
-
// Save updated accessories to cache
|
|
890
673
|
this.accessoryCache.requestSave(this.accessories);
|
|
891
674
|
}
|
|
892
|
-
/**
|
|
893
|
-
* Register a single Matter accessory (internal method)
|
|
894
|
-
*/
|
|
895
675
|
async registerAccessory(pluginIdentifier, platformName, accessory) {
|
|
896
|
-
// In external accessory mode, only check for serverNode (no aggregator).
|
|
897
|
-
// In bridge mode, check for both serverNode and aggregator.
|
|
898
676
|
if (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
|
|
899
677
|
throw new MatterDeviceError('Matter server not started');
|
|
900
678
|
}
|
|
901
|
-
// Validate required fields
|
|
902
679
|
validateAccessoryRequiredFields(accessory);
|
|
903
|
-
// Check if already registered (during this session)
|
|
904
680
|
if (this.accessories.has(accessory.uuid)) {
|
|
905
681
|
const existing = this.accessories.get(accessory.uuid);
|
|
906
682
|
throw new MatterDeviceError(`Matter accessory with UUID "${accessory.uuid}" is already registered.\n`
|
|
@@ -908,43 +684,33 @@ export class MatterServer extends EventEmitter {
|
|
|
908
684
|
+ `New accessory: "${accessory.displayName}"\n`
|
|
909
685
|
+ 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
|
|
910
686
|
}
|
|
911
|
-
// Restore cached state if available
|
|
912
687
|
this.restoreCachedState(accessory);
|
|
913
|
-
// Check device limit
|
|
914
688
|
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
915
689
|
throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
|
|
916
690
|
+ `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
|
|
917
691
|
+ `Current registered devices: ${this.accessories.size}`);
|
|
918
692
|
}
|
|
919
693
|
try {
|
|
920
|
-
// Prepare device type with WindowCovering features
|
|
921
694
|
let deviceType = accessory.deviceType;
|
|
922
695
|
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
923
696
|
if (windowCoveringFeatures.length > 0) {
|
|
924
697
|
deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
|
|
925
698
|
}
|
|
926
|
-
// Detect cluster features for behavior configuration
|
|
927
699
|
const features = this.detectClusterFeatures(accessory, deviceType);
|
|
928
|
-
// Build and apply custom behaviors based on handlers
|
|
929
700
|
const customBehaviors = await this.buildCustomBehaviors(accessory, deviceType, features);
|
|
930
701
|
if (customBehaviors.length > 0) {
|
|
931
702
|
deviceType = withBehaviors(deviceType, customBehaviors);
|
|
932
703
|
log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
|
|
933
704
|
}
|
|
934
|
-
// Add BridgedDeviceBasicInformationServer for bridged devices only
|
|
935
|
-
// This is required by the Matter spec for devices behind an aggregator
|
|
936
|
-
// External accessories should NOT have this cluster
|
|
937
705
|
if (!this.config.externalAccessory) {
|
|
938
706
|
deviceType = withBehaviors(deviceType, [BridgedDeviceBasicInformationServer]);
|
|
939
707
|
log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
|
|
940
708
|
}
|
|
941
|
-
// Create endpoint with cluster states
|
|
942
709
|
const endpointOptions = this.createEndpointOptions(accessory);
|
|
943
710
|
const endpoint = new Endpoint(deviceType, endpointOptions);
|
|
944
711
|
if (this.config.debugModeEnabled) {
|
|
945
712
|
log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
|
|
946
713
|
}
|
|
947
|
-
// Add endpoint to aggregator or serverNode depending on mode
|
|
948
714
|
if (this.config.externalAccessory) {
|
|
949
715
|
await this.serverNode.add(endpoint);
|
|
950
716
|
log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
|
|
@@ -955,11 +721,8 @@ export class MatterServer extends EventEmitter {
|
|
|
955
721
|
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
956
722
|
}
|
|
957
723
|
}
|
|
958
|
-
// Register command handlers
|
|
959
724
|
this.registerAccessoryHandlers(accessory);
|
|
960
|
-
// Create and register child endpoints (parts)
|
|
961
725
|
const internalParts = await this.createAccessoryParts(accessory);
|
|
962
|
-
// Finalize registration (store, emit events, save cache)
|
|
963
726
|
await this.finalizeAccessoryRegistration(accessory, endpoint, internalParts);
|
|
964
727
|
}
|
|
965
728
|
catch (error) {
|
|
@@ -967,30 +730,21 @@ export class MatterServer extends EventEmitter {
|
|
|
967
730
|
throw new MatterDeviceError(`Failed to register accessory: ${error}`);
|
|
968
731
|
}
|
|
969
732
|
}
|
|
970
|
-
/**
|
|
971
|
-
* Restore cached state for an accessory
|
|
972
|
-
*/
|
|
973
733
|
restoreCachedState(accessory) {
|
|
974
|
-
// Check if there's a cached version - merge cached cluster states with new registration.
|
|
975
|
-
// This ensures state persistence across Homebridge restarts.
|
|
976
734
|
if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
|
|
977
735
|
const cached = this.accessoryCache.getCached(accessory.uuid);
|
|
978
736
|
if (cached?.clusters && accessory.clusters) {
|
|
979
|
-
// Merge cached cluster states with new ones (prefer cached state to persist values across restarts)
|
|
980
737
|
for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
|
|
981
738
|
if (!accessory.clusters[clusterName]) {
|
|
982
|
-
// Cluster exists in cache but not in new registration - preserve it
|
|
983
739
|
accessory.clusters[clusterName] = cachedAttrs;
|
|
984
740
|
}
|
|
985
741
|
else {
|
|
986
|
-
// Cluster exists in both - merge (prefer cached state over initial values)
|
|
987
742
|
accessory.clusters[clusterName] = {
|
|
988
743
|
...accessory.clusters[clusterName],
|
|
989
744
|
...cachedAttrs,
|
|
990
745
|
};
|
|
991
746
|
}
|
|
992
747
|
}
|
|
993
|
-
// Restore context if available
|
|
994
748
|
if (cached.context) {
|
|
995
749
|
accessory.context = cached.context;
|
|
996
750
|
}
|
|
@@ -998,22 +752,14 @@ export class MatterServer extends EventEmitter {
|
|
|
998
752
|
}
|
|
999
753
|
}
|
|
1000
754
|
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Detect cluster features for an accessory
|
|
1003
|
-
* Returns an object containing detected features for various clusters
|
|
1004
|
-
*/
|
|
1005
755
|
detectClusterFeatures(accessory, deviceType) {
|
|
1006
|
-
// Detect WindowCovering features
|
|
1007
756
|
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
1008
|
-
// Detect ServiceArea features
|
|
1009
757
|
let serviceAreaFeatures = null;
|
|
1010
758
|
if (accessory.clusters?.serviceArea) {
|
|
1011
759
|
const features = [];
|
|
1012
|
-
// Check if Maps feature should be enabled (when supportedMaps is defined)
|
|
1013
760
|
if (accessory.clusters.serviceArea.supportedMaps) {
|
|
1014
761
|
features.push('Maps');
|
|
1015
762
|
}
|
|
1016
|
-
// Check if ProgressReporting feature should be enabled (when progress is defined)
|
|
1017
763
|
if (accessory.clusters.serviceArea.progress !== undefined) {
|
|
1018
764
|
features.push('ProgressReporting');
|
|
1019
765
|
}
|
|
@@ -1022,7 +768,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1022
768
|
log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
|
|
1023
769
|
}
|
|
1024
770
|
}
|
|
1025
|
-
// Detect ColorControl features
|
|
1026
771
|
let colorControlFeatures = null;
|
|
1027
772
|
if (accessory.handlers?.colorControl) {
|
|
1028
773
|
colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
|
|
@@ -1030,7 +775,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1030
775
|
colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
|
|
1031
776
|
}
|
|
1032
777
|
}
|
|
1033
|
-
// Detect Thermostat features
|
|
1034
778
|
let thermostatFeatures = null;
|
|
1035
779
|
if (accessory.handlers?.thermostat) {
|
|
1036
780
|
thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
|
|
@@ -1042,42 +786,29 @@ export class MatterServer extends EventEmitter {
|
|
|
1042
786
|
thermostatFeatures,
|
|
1043
787
|
};
|
|
1044
788
|
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Build custom behaviors for an accessory based on handlers
|
|
1047
|
-
*/
|
|
1048
789
|
async buildCustomBehaviors(accessory, deviceType, features) {
|
|
1049
790
|
const customBehaviors = [];
|
|
1050
791
|
if (!accessory.handlers) {
|
|
1051
792
|
return customBehaviors;
|
|
1052
793
|
}
|
|
1053
794
|
log.debug(`[${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
|
|
1054
|
-
// Use the static cluster behavior map
|
|
1055
795
|
const behaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
1056
|
-
// For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
|
|
1057
|
-
// These clusters need to be added to the device type even if there are no handlers
|
|
1058
796
|
if (isDeviceType(deviceType, devices.RoboticVacuumCleanerDevice)) {
|
|
1059
|
-
// Import RVC requirements
|
|
1060
797
|
const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
|
|
1061
|
-
// Add RvcCleanMode if defined in clusters
|
|
1062
798
|
if (accessory.clusters?.rvcCleanMode) {
|
|
1063
|
-
// Check if there's a custom behavior with handlers
|
|
1064
799
|
if (accessory.handlers?.rvcCleanMode) {
|
|
1065
800
|
const behaviorClass = HomebridgeRvcCleanModeServer;
|
|
1066
801
|
customBehaviors.push(behaviorClass);
|
|
1067
802
|
log.info('Adding custom RvcCleanMode behavior with handlers');
|
|
1068
803
|
}
|
|
1069
804
|
else {
|
|
1070
|
-
// No handlers, use base server
|
|
1071
805
|
customBehaviors.push(RvcCleanModeServer);
|
|
1072
806
|
log.info('Adding base RvcCleanMode server');
|
|
1073
807
|
}
|
|
1074
808
|
}
|
|
1075
|
-
// Add ServiceArea if defined in clusters
|
|
1076
809
|
if (accessory.clusters?.serviceArea) {
|
|
1077
|
-
// Check if there's a custom behavior with handlers
|
|
1078
810
|
if (accessory.handlers?.serviceArea) {
|
|
1079
811
|
let behaviorClass = HomebridgeServiceAreaServer;
|
|
1080
|
-
// Apply features if detected
|
|
1081
812
|
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1082
813
|
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
1083
814
|
log.info(`ServiceArea custom behavior will have features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
@@ -1086,7 +817,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1086
817
|
log.info('Adding custom ServiceArea behavior with handlers');
|
|
1087
818
|
}
|
|
1088
819
|
else {
|
|
1089
|
-
// No handlers, use base server with features
|
|
1090
820
|
let behaviorClass = ServiceAreaServer;
|
|
1091
821
|
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1092
822
|
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
@@ -1096,9 +826,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1096
826
|
log.info('Adding base ServiceArea server');
|
|
1097
827
|
}
|
|
1098
828
|
}
|
|
1099
|
-
// Add PowerSource if defined in clusters (for battery percentage)
|
|
1100
829
|
if (accessory.clusters?.powerSource) {
|
|
1101
|
-
// Detect Battery feature from cluster config
|
|
1102
830
|
const hasBattery = accessory.clusters.powerSource.batPercentRemaining !== undefined
|
|
1103
831
|
|| accessory.clusters.powerSource.batChargeLevel !== undefined;
|
|
1104
832
|
let powerSourceBehavior = PowerSourceServer;
|
|
@@ -1113,34 +841,27 @@ export class MatterServer extends EventEmitter {
|
|
|
1113
841
|
}
|
|
1114
842
|
}
|
|
1115
843
|
for (const clusterName of Object.keys(accessory.handlers || {})) {
|
|
1116
|
-
// Skip windowCovering if we already applied features via base WindowCoveringServer
|
|
1117
844
|
const skipWindowCoveringBehavior = accessory.context?._skipWindowCoveringBehavior;
|
|
1118
845
|
if (clusterName === 'windowCovering' && skipWindowCoveringBehavior) {
|
|
1119
846
|
log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
|
|
1120
847
|
continue;
|
|
1121
848
|
}
|
|
1122
|
-
// Skip RVC clusters - they're handled specially above for RoboticVacuumCleaner
|
|
1123
849
|
if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea' || clusterName === 'powerSource') {
|
|
1124
850
|
continue;
|
|
1125
851
|
}
|
|
1126
852
|
let behaviorClass = behaviorMap[clusterName];
|
|
1127
|
-
// Apply ColorControl features if we detected them earlier
|
|
1128
853
|
if (clusterName === 'colorControl' && behaviorClass && features.colorControlFeatures && features.colorControlFeatures.length > 0) {
|
|
1129
854
|
behaviorClass = withFeatures(behaviorClass, features.colorControlFeatures);
|
|
1130
855
|
log.info(`ColorControl custom behavior will preserve features: ${features.colorControlFeatures.join(', ')}`);
|
|
1131
856
|
}
|
|
1132
|
-
// Apply Thermostat features if we detected them earlier
|
|
1133
857
|
if (clusterName === 'thermostat' && behaviorClass && features.thermostatFeatures && features.thermostatFeatures.length > 0) {
|
|
1134
858
|
behaviorClass = withFeatures(behaviorClass, features.thermostatFeatures);
|
|
1135
859
|
log.info(`Thermostat custom behavior will preserve features: ${features.thermostatFeatures.join(', ')}`);
|
|
1136
860
|
}
|
|
1137
|
-
// Apply ServiceArea features if we detected them earlier
|
|
1138
861
|
if (clusterName === 'serviceArea' && behaviorClass && features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1139
862
|
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
1140
863
|
log.info(`ServiceArea custom behavior will preserve features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
1141
864
|
}
|
|
1142
|
-
// Apply WindowCovering features to custom behavior as well
|
|
1143
|
-
// (features were already applied to base device type, but custom behavior needs them too)
|
|
1144
865
|
if (clusterName === 'windowCovering') {
|
|
1145
866
|
log.debug(`WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${features.windowCoveringFeatures}, length=${features.windowCoveringFeatures?.length}`);
|
|
1146
867
|
if (behaviorClass && features.windowCoveringFeatures && features.windowCoveringFeatures.length > 0) {
|
|
@@ -1161,20 +882,15 @@ export class MatterServer extends EventEmitter {
|
|
|
1161
882
|
}
|
|
1162
883
|
return customBehaviors;
|
|
1163
884
|
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Create endpoint options for an accessory
|
|
1166
|
-
*/
|
|
1167
885
|
createEndpointOptions(accessory) {
|
|
1168
886
|
const endpointOptions = {
|
|
1169
887
|
id: accessory.uuid,
|
|
1170
|
-
...accessory.clusters,
|
|
888
|
+
...accessory.clusters,
|
|
1171
889
|
};
|
|
1172
|
-
// Add bridgedDeviceBasicInformation cluster only for bridged devices
|
|
1173
|
-
// For external accessories, use the root basicInformation instead
|
|
1174
890
|
if (!this.config.externalAccessory) {
|
|
1175
891
|
endpointOptions.bridgedDeviceBasicInformation = {
|
|
1176
892
|
vendorName: accessory.manufacturer,
|
|
1177
|
-
nodeLabel: accessory.displayName,
|
|
893
|
+
nodeLabel: accessory.displayName,
|
|
1178
894
|
productName: accessory.model,
|
|
1179
895
|
productLabel: accessory.displayName,
|
|
1180
896
|
serialNumber: accessory.serialNumber,
|
|
@@ -1183,15 +899,11 @@ export class MatterServer extends EventEmitter {
|
|
|
1183
899
|
}
|
|
1184
900
|
return endpointOptions;
|
|
1185
901
|
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Register command handlers for an accessory
|
|
1188
|
-
*/
|
|
1189
902
|
registerAccessoryHandlers(accessory) {
|
|
1190
903
|
if (!accessory.handlers) {
|
|
1191
904
|
return;
|
|
1192
905
|
}
|
|
1193
906
|
log.info(`Setting up handlers for accessory ${accessory.uuid}`);
|
|
1194
|
-
// Register handlers with the custom behavior classes
|
|
1195
907
|
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1196
908
|
log.info(` Processing cluster: ${clusterName}`);
|
|
1197
909
|
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
@@ -1199,9 +911,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1199
911
|
}
|
|
1200
912
|
}
|
|
1201
913
|
}
|
|
1202
|
-
/**
|
|
1203
|
-
* Create and register child endpoints (parts) for an accessory
|
|
1204
|
-
*/
|
|
1205
914
|
async createAccessoryParts(accessory) {
|
|
1206
915
|
const internalParts = [];
|
|
1207
916
|
if (!accessory.parts || accessory.parts.length === 0) {
|
|
@@ -1209,15 +918,11 @@ export class MatterServer extends EventEmitter {
|
|
|
1209
918
|
}
|
|
1210
919
|
log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
|
|
1211
920
|
for (const part of accessory.parts) {
|
|
1212
|
-
// Create unique endpoint ID for this part
|
|
1213
921
|
const partEndpointId = `${accessory.uuid}-part-${part.id}`;
|
|
1214
|
-
// Register the part endpoint mapping for handler context
|
|
1215
922
|
this.behaviorRegistry.registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
|
|
1216
|
-
// Apply custom behaviors to part based on its handlers (same logic as main accessory)
|
|
1217
923
|
let partDeviceType = part.deviceType;
|
|
1218
924
|
const partCustomBehaviors = [];
|
|
1219
925
|
if (part.handlers) {
|
|
1220
|
-
// Use the static cluster behavior map for parts as well
|
|
1221
926
|
const partBehaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
1222
927
|
for (const clusterName of Object.keys(part.handlers)) {
|
|
1223
928
|
const behaviorClass = partBehaviorMap[clusterName];
|
|
@@ -1230,21 +935,17 @@ export class MatterServer extends EventEmitter {
|
|
|
1230
935
|
}
|
|
1231
936
|
}
|
|
1232
937
|
if (partCustomBehaviors.length > 0) {
|
|
1233
|
-
// Add custom behaviors to part device type
|
|
1234
938
|
partDeviceType = withBehaviors(partDeviceType, partCustomBehaviors);
|
|
1235
939
|
log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
|
|
1236
940
|
}
|
|
1237
941
|
}
|
|
1238
|
-
// Add BridgedDeviceBasicInformationServer for bridged parts
|
|
1239
942
|
if (!this.config.externalAccessory) {
|
|
1240
943
|
partDeviceType = withBehaviors(partDeviceType, [BridgedDeviceBasicInformationServer]);
|
|
1241
944
|
}
|
|
1242
|
-
// Create endpoint options with cluster states
|
|
1243
945
|
const partEndpointOptions = {
|
|
1244
946
|
id: partEndpointId,
|
|
1245
947
|
...part.clusters,
|
|
1246
948
|
};
|
|
1247
|
-
// Add bridgedDeviceBasicInformation for the part
|
|
1248
949
|
if (!this.config.externalAccessory) {
|
|
1249
950
|
partEndpointOptions.bridgedDeviceBasicInformation = {
|
|
1250
951
|
vendorName: accessory.manufacturer,
|
|
@@ -1255,9 +956,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1255
956
|
reachable: true,
|
|
1256
957
|
};
|
|
1257
958
|
}
|
|
1258
|
-
// Create the part endpoint
|
|
1259
959
|
const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
|
|
1260
|
-
// Add part endpoint to aggregator or serverNode
|
|
1261
960
|
if (this.config.externalAccessory) {
|
|
1262
961
|
await this.serverNode.add(partEndpoint);
|
|
1263
962
|
}
|
|
@@ -1265,17 +964,14 @@ export class MatterServer extends EventEmitter {
|
|
|
1265
964
|
await this.aggregator.add(partEndpoint);
|
|
1266
965
|
}
|
|
1267
966
|
log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
|
|
1268
|
-
// Set up handlers for this part
|
|
1269
967
|
if (part.handlers) {
|
|
1270
968
|
for (const [clusterName, handlers] of Object.entries(part.handlers)) {
|
|
1271
969
|
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1272
|
-
// Register handler with the part's endpoint ID
|
|
1273
970
|
this.behaviorRegistry.registerHandler(partEndpointId, clusterName, commandName, handler);
|
|
1274
971
|
}
|
|
1275
972
|
}
|
|
1276
973
|
log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
|
|
1277
974
|
}
|
|
1278
|
-
// Store the internal part
|
|
1279
975
|
internalParts.push({
|
|
1280
976
|
...part,
|
|
1281
977
|
endpoint: partEndpoint,
|
|
@@ -1283,13 +979,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1283
979
|
}
|
|
1284
980
|
return internalParts;
|
|
1285
981
|
}
|
|
1286
|
-
/**
|
|
1287
|
-
* Finalize accessory registration (store, emit events, save cache)
|
|
1288
|
-
*/
|
|
1289
982
|
async finalizeAccessoryRegistration(accessory, endpoint, internalParts) {
|
|
1290
|
-
// Store accessory with internal metadata and event emitter
|
|
1291
|
-
// The event emitter allows plugins to listen for lifecycle events (currently only 'ready')
|
|
1292
|
-
// Note: _associatedPlugin and _associatedPlatform are already set by MatterAPIImpl
|
|
1293
983
|
const internalAccessory = {
|
|
1294
984
|
...accessory,
|
|
1295
985
|
endpoint,
|
|
@@ -1302,23 +992,25 @@ export class MatterServer extends EventEmitter {
|
|
|
1302
992
|
if (this.config.debugModeEnabled) {
|
|
1303
993
|
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
1304
994
|
}
|
|
1305
|
-
// Notify controllers about the new device (parts list changed)
|
|
1306
|
-
// This allows the Home app to discover new devices without re-pairing
|
|
1307
995
|
await this.notifyPartsListChanged();
|
|
1308
|
-
// Request debounced save to cache (reduces disk I/O during rapid registration)
|
|
1309
996
|
if (this.accessoryCache) {
|
|
1310
997
|
this.accessoryCache.requestSave(this.accessories);
|
|
1311
998
|
}
|
|
999
|
+
if (this.monitoringEnabled && process.send) {
|
|
1000
|
+
const event = {
|
|
1001
|
+
type: 'accessoryAdded',
|
|
1002
|
+
data: { uuid: accessory.uuid },
|
|
1003
|
+
};
|
|
1004
|
+
process.send({
|
|
1005
|
+
id: "matterEvent",
|
|
1006
|
+
data: event,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1312
1009
|
}
|
|
1313
|
-
/**
|
|
1314
|
-
* Unregister a Matter accessory (Plugin API)
|
|
1315
|
-
*/
|
|
1316
1010
|
async unregisterAccessory(uuid) {
|
|
1317
1011
|
const accessory = this.accessories.get(uuid);
|
|
1318
1012
|
if (!accessory) {
|
|
1319
|
-
// Accessory not in memory, but might be in cache - still remove from cache
|
|
1320
1013
|
log.debug(`Accessory ${uuid} not found or not registered`);
|
|
1321
|
-
// Check if it exists in cache and remove it
|
|
1322
1014
|
if (this.accessoryCache && this.accessoryCache.getCached(uuid)) {
|
|
1323
1015
|
log.debug(`Removing ${uuid} from cache`);
|
|
1324
1016
|
this.accessoryCache.removeCached(uuid);
|
|
@@ -1333,41 +1025,36 @@ export class MatterServer extends EventEmitter {
|
|
|
1333
1025
|
}
|
|
1334
1026
|
this.accessories.delete(uuid);
|
|
1335
1027
|
log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
|
|
1336
|
-
// Notify controllers about the removed device (parts list changed)
|
|
1337
1028
|
await this.notifyPartsListChanged();
|
|
1338
|
-
// Update cache (remove the accessory)
|
|
1339
1029
|
if (this.accessoryCache) {
|
|
1340
1030
|
this.accessoryCache.removeCached(uuid);
|
|
1341
1031
|
this.accessoryCache.requestSave(this.accessories);
|
|
1342
1032
|
}
|
|
1033
|
+
if (this.monitoringEnabled && process.send) {
|
|
1034
|
+
const event = {
|
|
1035
|
+
type: 'accessoryRemoved',
|
|
1036
|
+
data: { uuid },
|
|
1037
|
+
};
|
|
1038
|
+
process.send({
|
|
1039
|
+
id: "matterEvent",
|
|
1040
|
+
data: event,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1343
1043
|
}
|
|
1344
1044
|
catch (error) {
|
|
1345
1045
|
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
1346
1046
|
throw new MatterDeviceError(`Failed to unregister accessory: ${error}`);
|
|
1347
1047
|
}
|
|
1348
1048
|
}
|
|
1349
|
-
/**
|
|
1350
|
-
* Update a Matter accessory's state (Plugin API)
|
|
1351
|
-
*
|
|
1352
|
-
* This method can be called from anywhere, including from within handlers.
|
|
1353
|
-
* State updates are automatically deferred to avoid transaction conflicts.
|
|
1354
|
-
*
|
|
1355
|
-
* @param uuid - The UUID of the accessory
|
|
1356
|
-
* @param cluster - The cluster name
|
|
1357
|
-
* @param attributes - The attributes to update
|
|
1358
|
-
* @param partId - Optional: ID of the part to update (for composed devices)
|
|
1359
|
-
*/
|
|
1360
1049
|
async updateAccessoryState(uuid, cluster, attributes, partId) {
|
|
1361
1050
|
const accessory = this.accessories.get(uuid);
|
|
1362
1051
|
if (!accessory) {
|
|
1363
1052
|
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
1364
1053
|
}
|
|
1365
|
-
// Determine which endpoint to update
|
|
1366
1054
|
let targetEndpoint;
|
|
1367
1055
|
let targetClusters;
|
|
1368
1056
|
let displayName;
|
|
1369
1057
|
if (partId) {
|
|
1370
|
-
// Update a specific part
|
|
1371
1058
|
const part = accessory._parts?.find(p => p.id === partId);
|
|
1372
1059
|
if (!part || !part.endpoint) {
|
|
1373
1060
|
throw new MatterDeviceError(`Part ${partId} not found in accessory ${uuid}`);
|
|
@@ -1377,7 +1064,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1377
1064
|
displayName = part.displayName || `${accessory.displayName} - ${partId}`;
|
|
1378
1065
|
}
|
|
1379
1066
|
else {
|
|
1380
|
-
// Update the main accessory
|
|
1381
1067
|
if (!accessory.endpoint) {
|
|
1382
1068
|
throw new MatterDeviceError(`Accessory ${uuid} not registered or missing endpoint`);
|
|
1383
1069
|
}
|
|
@@ -1385,18 +1071,11 @@ export class MatterServer extends EventEmitter {
|
|
|
1385
1071
|
targetClusters = accessory.clusters;
|
|
1386
1072
|
displayName = accessory.displayName;
|
|
1387
1073
|
}
|
|
1388
|
-
// Defer the update to avoid "read-only transaction" errors when called from handlers
|
|
1389
|
-
// Matter.js uses transactions, and we need to escape the current call stack
|
|
1390
|
-
// setImmediate ensures we're in a new event loop tick without arbitrary delays
|
|
1391
1074
|
return new Promise((resolve, reject) => {
|
|
1392
1075
|
setImmediate(async () => {
|
|
1393
1076
|
try {
|
|
1394
|
-
// Construct the update object
|
|
1395
1077
|
const updateObject = { [cluster]: attributes };
|
|
1396
|
-
// Use endpoint.set() which properly handles state updates
|
|
1397
1078
|
await targetEndpoint.set(updateObject);
|
|
1398
|
-
// CRITICAL: Also update the cached clusters object so state persists across restarts
|
|
1399
|
-
// Merge the new attributes into the existing cluster state
|
|
1400
1079
|
if (!targetClusters) {
|
|
1401
1080
|
log.warn(`Target clusters undefined for ${displayName}, cannot cache state`);
|
|
1402
1081
|
}
|
|
@@ -1421,28 +1100,60 @@ export class MatterServer extends EventEmitter {
|
|
|
1421
1100
|
});
|
|
1422
1101
|
});
|
|
1423
1102
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1103
|
+
async triggerCommand(uuid, cluster, command, args, partId) {
|
|
1104
|
+
const accessory = this.accessories.get(uuid);
|
|
1105
|
+
if (!accessory) {
|
|
1106
|
+
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
1107
|
+
}
|
|
1108
|
+
let targetEndpoint;
|
|
1109
|
+
let displayName;
|
|
1110
|
+
if (partId) {
|
|
1111
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1112
|
+
if (!part || !part.endpoint) {
|
|
1113
|
+
throw new MatterDeviceError(`Part ${partId} not found in accessory ${uuid}`);
|
|
1114
|
+
}
|
|
1115
|
+
targetEndpoint = part.endpoint;
|
|
1116
|
+
displayName = part.displayName || `${accessory.displayName} - ${partId}`;
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
if (!accessory.endpoint) {
|
|
1120
|
+
throw new MatterDeviceError(`Accessory ${uuid} not registered or missing endpoint`);
|
|
1121
|
+
}
|
|
1122
|
+
targetEndpoint = accessory.endpoint;
|
|
1123
|
+
displayName = accessory.displayName;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
const partInfo = partId ? ` (part: ${partId})` : '';
|
|
1127
|
+
log.debug(`Triggering command ${cluster}.${command} for ${displayName}${partInfo}`, args);
|
|
1128
|
+
await targetEndpoint.act((agent) => {
|
|
1129
|
+
const clusterBehavior = agent[cluster];
|
|
1130
|
+
if (!clusterBehavior) {
|
|
1131
|
+
throw new Error(`Cluster '${cluster}' not found on endpoint`);
|
|
1132
|
+
}
|
|
1133
|
+
if (typeof clusterBehavior[command] !== 'function') {
|
|
1134
|
+
throw new TypeError(`Command '${command}' not found on cluster '${cluster}'`);
|
|
1135
|
+
}
|
|
1136
|
+
if (args && Object.keys(args).length > 0) {
|
|
1137
|
+
return clusterBehavior[command](args);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
return clusterBehavior[command]();
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
log.debug(`Command ${cluster}.${command} succeeded for ${displayName}${partInfo}`);
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
const partInfo = partId ? ` part ${partId}` : '';
|
|
1147
|
+
log.error(`Failed to trigger command for accessory ${uuid}${partInfo}:`, error);
|
|
1148
|
+
throw new MatterDeviceError(`Failed to trigger command: ${error}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1439
1151
|
getAccessoryState(uuid, cluster, partId) {
|
|
1440
1152
|
const accessory = this.accessories.get(uuid);
|
|
1441
1153
|
if (!accessory) {
|
|
1442
1154
|
log.debug(`Accessory ${uuid} not found`);
|
|
1443
1155
|
return undefined;
|
|
1444
1156
|
}
|
|
1445
|
-
// Determine which endpoint to read from
|
|
1446
1157
|
let targetEndpoint;
|
|
1447
1158
|
let displayName;
|
|
1448
1159
|
if (partId) {
|
|
@@ -1473,22 +1184,17 @@ export class MatterServer extends EventEmitter {
|
|
|
1473
1184
|
return undefined;
|
|
1474
1185
|
}
|
|
1475
1186
|
const clusterState = targetEndpoint.state[cluster];
|
|
1476
|
-
// Build result object by reading each property directly
|
|
1477
1187
|
const result = {};
|
|
1478
|
-
// Get list of properties to read - use both approaches for maximum compatibility
|
|
1479
1188
|
const allKeys = new Set([
|
|
1480
1189
|
...Object.keys(clusterState),
|
|
1481
1190
|
...Object.getOwnPropertyNames(clusterState),
|
|
1482
1191
|
]);
|
|
1483
1192
|
for (const key of allKeys) {
|
|
1484
1193
|
try {
|
|
1485
|
-
// Skip internal properties, methods, and symbols
|
|
1486
1194
|
if (key.startsWith('_') || key.startsWith('$')) {
|
|
1487
1195
|
continue;
|
|
1488
1196
|
}
|
|
1489
|
-
// Try to read the value directly
|
|
1490
1197
|
const value = clusterState[key];
|
|
1491
|
-
// Skip functions and undefined values
|
|
1492
1198
|
if (typeof value === 'function' || value === undefined) {
|
|
1493
1199
|
continue;
|
|
1494
1200
|
}
|
|
@@ -1509,10 +1215,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1509
1215
|
return undefined;
|
|
1510
1216
|
}
|
|
1511
1217
|
}
|
|
1512
|
-
/**
|
|
1513
|
-
* Get all cached accessories (Internal - for restore process)
|
|
1514
|
-
* @internal
|
|
1515
|
-
*/
|
|
1516
1218
|
getAllCachedAccessories() {
|
|
1517
1219
|
if (!this.accessoryCache) {
|
|
1518
1220
|
log.debug('getAllCachedAccessories: No cache available');
|
|
@@ -1522,33 +1224,20 @@ export class MatterServer extends EventEmitter {
|
|
|
1522
1224
|
log.debug(`getAllCachedAccessories: Returning ${cached.length} accessories`);
|
|
1523
1225
|
return cached;
|
|
1524
1226
|
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Get all registered accessories (Plugin API)
|
|
1527
|
-
*/
|
|
1528
1227
|
getAccessories() {
|
|
1529
1228
|
return Array.from(this.accessories.values()).map((acc) => {
|
|
1530
|
-
// Return copy without internal fields
|
|
1531
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
1532
1229
|
const { endpoint, registered, ...publicAccessory } = acc;
|
|
1533
1230
|
return publicAccessory;
|
|
1534
1231
|
});
|
|
1535
1232
|
}
|
|
1536
|
-
/**
|
|
1537
|
-
* Get a specific accessory by UUID (Plugin API)
|
|
1538
|
-
*/
|
|
1539
1233
|
getAccessory(uuid) {
|
|
1540
1234
|
const accessory = this.accessories.get(uuid);
|
|
1541
1235
|
if (!accessory) {
|
|
1542
1236
|
return undefined;
|
|
1543
1237
|
}
|
|
1544
|
-
// Return copy without internal fields
|
|
1545
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
1546
1238
|
const { endpoint, registered, ...publicAccessory } = accessory;
|
|
1547
1239
|
return publicAccessory;
|
|
1548
1240
|
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Stop the Matter server
|
|
1551
|
-
*/
|
|
1552
1241
|
async stop() {
|
|
1553
1242
|
if (!this.isRunning) {
|
|
1554
1243
|
log.debug('Matter server is not running');
|
|
@@ -1556,19 +1245,14 @@ export class MatterServer extends EventEmitter {
|
|
|
1556
1245
|
}
|
|
1557
1246
|
this.isRunning = false;
|
|
1558
1247
|
try {
|
|
1559
|
-
// Save accessory cache before shutting down (BEFORE clearing accessories!)
|
|
1560
1248
|
if (this.accessoryCache && this.accessories.size > 0) {
|
|
1561
1249
|
await this.accessoryCache.save(this.accessories);
|
|
1562
1250
|
log.debug('Saved accessory cache before shutdown');
|
|
1563
1251
|
}
|
|
1564
|
-
// Stop server (this will close all child endpoints automatically)
|
|
1565
|
-
// Note: We don't manually close endpoints here because they're part of the ServerNode
|
|
1566
|
-
// hierarchy and will be closed by serverNode.close()
|
|
1567
1252
|
if (this.serverNode) {
|
|
1568
1253
|
await this.serverNode.close();
|
|
1569
1254
|
log.debug('ServerNode closed (all endpoints cleaned up)');
|
|
1570
1255
|
}
|
|
1571
|
-
// Clear accessories map after server is stopped
|
|
1572
1256
|
this.accessories.clear();
|
|
1573
1257
|
await this.cleanup();
|
|
1574
1258
|
log.info('Matter server stopped');
|
|
@@ -1582,17 +1266,12 @@ export class MatterServer extends EventEmitter {
|
|
|
1582
1266
|
this.isRunning = false;
|
|
1583
1267
|
}
|
|
1584
1268
|
}
|
|
1585
|
-
/**
|
|
1586
|
-
* Cleanup resources
|
|
1587
|
-
*/
|
|
1588
1269
|
async cleanup() {
|
|
1589
|
-
// Remove signal handlers
|
|
1590
1270
|
if (this.shutdownHandler) {
|
|
1591
1271
|
process.off('SIGINT', this.shutdownHandler);
|
|
1592
1272
|
process.off('SIGTERM', this.shutdownHandler);
|
|
1593
1273
|
this.shutdownHandler = null;
|
|
1594
1274
|
}
|
|
1595
|
-
// Run all cleanup handlers
|
|
1596
1275
|
for (const handler of this.cleanupHandlers) {
|
|
1597
1276
|
try {
|
|
1598
1277
|
await handler();
|
|
@@ -1602,44 +1281,19 @@ export class MatterServer extends EventEmitter {
|
|
|
1602
1281
|
}
|
|
1603
1282
|
}
|
|
1604
1283
|
this.cleanupHandlers = [];
|
|
1605
|
-
// Clear references
|
|
1606
1284
|
this.serverNode = null;
|
|
1607
1285
|
this.aggregator = null;
|
|
1608
1286
|
this.isRunning = false;
|
|
1609
1287
|
this.commissioningInfo = {};
|
|
1610
1288
|
}
|
|
1611
|
-
/**
|
|
1612
|
-
* Get fabric information for commissioned controllers
|
|
1613
|
-
*
|
|
1614
|
-
* Returns information about each paired controller (fabric) including:
|
|
1615
|
-
* - fabricIndex: Unique identifier for the fabric
|
|
1616
|
-
* - fabricId: 64-bit fabric identifier
|
|
1617
|
-
* - nodeId: Node identifier within the fabric
|
|
1618
|
-
* - rootVendorId: Vendor ID of the root node
|
|
1619
|
-
* - label: Optional human-readable label
|
|
1620
|
-
*
|
|
1621
|
-
* @returns Array of fabric information objects, or empty array if no fabrics are commissioned
|
|
1622
|
-
* @example
|
|
1623
|
-
* ```typescript
|
|
1624
|
-
* const fabrics = matterServer.getFabricInfo()
|
|
1625
|
-
* console.log(`Commissioned to ${fabrics.length} controller(s)`)
|
|
1626
|
-
* fabrics.forEach(fabric => {
|
|
1627
|
-
* console.log(` Fabric ${fabric.fabricIndex}: ${fabric.label || 'Unnamed'}`)
|
|
1628
|
-
* })
|
|
1629
|
-
* ```
|
|
1630
|
-
*/
|
|
1631
1289
|
getFabricInfo() {
|
|
1632
1290
|
try {
|
|
1633
1291
|
if (!this.storageManager) {
|
|
1634
1292
|
return [];
|
|
1635
1293
|
}
|
|
1636
|
-
// Fabric data is stored in the main storage file (uniqueId namespace)
|
|
1637
|
-
// with the key "fabrics.fabrics"
|
|
1638
|
-
// Note: We can read this directly from storage even before serverNode is initialized
|
|
1639
1294
|
const storage = this.storageManager.getStorage(this.config.uniqueId);
|
|
1640
1295
|
const fabricsData = storage.get(['fabrics'], 'fabrics');
|
|
1641
1296
|
if (Array.isArray(fabricsData) && fabricsData.length > 0) {
|
|
1642
|
-
// Map the fabric data to our interface
|
|
1643
1297
|
return fabricsData.map(fabric => ({
|
|
1644
1298
|
fabricIndex: fabric.fabricIndex || 0,
|
|
1645
1299
|
fabricId: fabric.fabricId?.value?.toString() || '',
|
|
@@ -1655,22 +1309,16 @@ export class MatterServer extends EventEmitter {
|
|
|
1655
1309
|
return [];
|
|
1656
1310
|
}
|
|
1657
1311
|
}
|
|
1658
|
-
/**
|
|
1659
|
-
* Check if the server is commissioned
|
|
1660
|
-
*/
|
|
1661
1312
|
isCommissioned() {
|
|
1662
1313
|
try {
|
|
1663
1314
|
if (!this.storageManager) {
|
|
1664
1315
|
return false;
|
|
1665
1316
|
}
|
|
1666
|
-
// Commissioned status is stored in the main storage file (uniqueId namespace)
|
|
1667
|
-
// at key "root.commissioning.commissioned"
|
|
1668
1317
|
const storage = this.storageManager.getStorage(this.config.uniqueId);
|
|
1669
1318
|
const commissioned = storage.get(['root', 'commissioning'], 'commissioned');
|
|
1670
1319
|
if (commissioned === true) {
|
|
1671
1320
|
return true;
|
|
1672
1321
|
}
|
|
1673
|
-
// Fallback to checking fabric count if commissioned flag not found
|
|
1674
1322
|
const fabrics = this.getFabricInfo();
|
|
1675
1323
|
return fabrics.length > 0;
|
|
1676
1324
|
}
|
|
@@ -1679,15 +1327,9 @@ export class MatterServer extends EventEmitter {
|
|
|
1679
1327
|
return false;
|
|
1680
1328
|
}
|
|
1681
1329
|
}
|
|
1682
|
-
/**
|
|
1683
|
-
* Get the number of commissioned fabrics
|
|
1684
|
-
*/
|
|
1685
1330
|
getCommissionedFabricCount() {
|
|
1686
1331
|
return this.getFabricInfo().length;
|
|
1687
1332
|
}
|
|
1688
|
-
/**
|
|
1689
|
-
* Get server status information
|
|
1690
|
-
*/
|
|
1691
1333
|
getServerInfo() {
|
|
1692
1334
|
return {
|
|
1693
1335
|
running: this.isRunning,
|
|
@@ -1698,9 +1340,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1698
1340
|
serialNumber: this.serialNumber,
|
|
1699
1341
|
};
|
|
1700
1342
|
}
|
|
1701
|
-
/**
|
|
1702
|
-
* Get commissioning information
|
|
1703
|
-
*/
|
|
1704
1343
|
getCommissioningInfo() {
|
|
1705
1344
|
return {
|
|
1706
1345
|
...this.commissioningInfo,
|
|
@@ -1710,40 +1349,21 @@ export class MatterServer extends EventEmitter {
|
|
|
1710
1349
|
commissioned: this.isCommissioned(),
|
|
1711
1350
|
};
|
|
1712
1351
|
}
|
|
1713
|
-
/**
|
|
1714
|
-
* Get storage statistics
|
|
1715
|
-
*/
|
|
1716
1352
|
getStorageStats() {
|
|
1717
1353
|
if (!this.storageManager) {
|
|
1718
1354
|
return null;
|
|
1719
1355
|
}
|
|
1720
1356
|
return this.storageManager.getAllStats();
|
|
1721
1357
|
}
|
|
1722
|
-
/**
|
|
1723
|
-
* Check if server is running
|
|
1724
|
-
*/
|
|
1725
1358
|
isServerRunning() {
|
|
1726
1359
|
return this.isRunning;
|
|
1727
1360
|
}
|
|
1728
|
-
/**
|
|
1729
|
-
* Get Matter device types available for plugin use
|
|
1730
|
-
*/
|
|
1731
1361
|
getDeviceTypes() {
|
|
1732
1362
|
return deviceTypes;
|
|
1733
1363
|
}
|
|
1734
|
-
/**
|
|
1735
|
-
* Get Matter clusters available for plugin use
|
|
1736
|
-
*/
|
|
1737
1364
|
getClusters() {
|
|
1738
1365
|
return clusters;
|
|
1739
1366
|
}
|
|
1740
|
-
/**
|
|
1741
|
-
* Remove a specific fabric (controller) from the bridge
|
|
1742
|
-
* This decommissions a single controller while leaving others intact
|
|
1743
|
-
*
|
|
1744
|
-
* @param fabricIndex - The fabric index to remove
|
|
1745
|
-
* @returns Promise that resolves when the fabric is removed
|
|
1746
|
-
*/
|
|
1747
1367
|
async removeFabric(fabricIndex) {
|
|
1748
1368
|
if (!this.serverNode) {
|
|
1749
1369
|
throw new MatterDeviceError('Matter server not started');
|
|
@@ -1755,10 +1375,8 @@ export class MatterServer extends EventEmitter {
|
|
|
1755
1375
|
if (typeof removeFabric !== 'function') {
|
|
1756
1376
|
throw new MatterDeviceError('Fabric removal not supported by Matter.js version');
|
|
1757
1377
|
}
|
|
1758
|
-
// Remove the fabric
|
|
1759
1378
|
await removeFabric(fabricIndex);
|
|
1760
1379
|
log.info(`Fabric ${fabricIndex} removed successfully`);
|
|
1761
|
-
// The fabric monitoring will detect this change and emit the appropriate events
|
|
1762
1380
|
}
|
|
1763
1381
|
catch (error) {
|
|
1764
1382
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -1768,36 +1386,108 @@ export class MatterServer extends EventEmitter {
|
|
|
1768
1386
|
});
|
|
1769
1387
|
}
|
|
1770
1388
|
}
|
|
1771
|
-
/**
|
|
1772
|
-
* Check if a specific fabric exists
|
|
1773
|
-
*/
|
|
1774
1389
|
hasFabric(fabricIndex) {
|
|
1775
1390
|
const fabrics = this.getFabricInfo();
|
|
1776
1391
|
return fabrics.some(f => f.fabricIndex === fabricIndex);
|
|
1777
1392
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1393
|
+
enableStateMonitoring() {
|
|
1394
|
+
this.monitoringEnabled = true;
|
|
1395
|
+
log.debug('Matter state monitoring enabled');
|
|
1396
|
+
}
|
|
1397
|
+
disableStateMonitoring() {
|
|
1398
|
+
this.monitoringEnabled = false;
|
|
1399
|
+
log.debug('Matter state monitoring disabled');
|
|
1400
|
+
}
|
|
1401
|
+
isMonitoringEnabled() {
|
|
1402
|
+
return this.monitoringEnabled;
|
|
1403
|
+
}
|
|
1404
|
+
notifyStateChange(uuid, cluster, state, partId) {
|
|
1405
|
+
if (!this.monitoringEnabled) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (process.send) {
|
|
1409
|
+
const event = {
|
|
1410
|
+
type: 'accessoryUpdate',
|
|
1411
|
+
data: {
|
|
1412
|
+
uuid,
|
|
1413
|
+
cluster,
|
|
1414
|
+
state,
|
|
1415
|
+
partId,
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
process.send({
|
|
1419
|
+
id: "matterEvent",
|
|
1420
|
+
data: event,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
getDeviceTypeName(deviceType) {
|
|
1425
|
+
return deviceType.name || 'Unknown';
|
|
1426
|
+
}
|
|
1427
|
+
collectAccessories(bridgeUsername, bridgeType, bridgeName) {
|
|
1428
|
+
const accessories = [];
|
|
1429
|
+
for (const [uuid, accessory] of this.accessories.entries()) {
|
|
1430
|
+
const transformed = {
|
|
1431
|
+
uuid,
|
|
1432
|
+
displayName: accessory.displayName,
|
|
1433
|
+
deviceType: this.getDeviceTypeName(accessory.deviceType),
|
|
1434
|
+
clusters: this.getCurrentState(uuid),
|
|
1435
|
+
parts: accessory._parts?.map(part => ({
|
|
1436
|
+
id: part.id,
|
|
1437
|
+
displayName: part.displayName,
|
|
1438
|
+
deviceType: this.getDeviceTypeName(part.deviceType),
|
|
1439
|
+
clusters: this.getCurrentState(uuid, part.id),
|
|
1440
|
+
})),
|
|
1441
|
+
bridge: {
|
|
1442
|
+
username: bridgeUsername,
|
|
1443
|
+
type: bridgeType,
|
|
1444
|
+
name: bridgeName,
|
|
1445
|
+
},
|
|
1446
|
+
};
|
|
1447
|
+
accessories.push(transformed);
|
|
1448
|
+
}
|
|
1449
|
+
return accessories;
|
|
1450
|
+
}
|
|
1451
|
+
getAccessoryInfo(uuid) {
|
|
1452
|
+
const accessory = this.accessories.get(uuid);
|
|
1453
|
+
if (!accessory) {
|
|
1454
|
+
return undefined;
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
uuid,
|
|
1458
|
+
displayName: accessory.displayName,
|
|
1459
|
+
deviceType: this.getDeviceTypeName(accessory.deviceType),
|
|
1460
|
+
clusters: this.getCurrentState(uuid),
|
|
1461
|
+
parts: accessory._parts?.map(part => ({
|
|
1462
|
+
id: part.id,
|
|
1463
|
+
displayName: part.displayName,
|
|
1464
|
+
deviceType: this.getDeviceTypeName(part.deviceType),
|
|
1465
|
+
clusters: this.getCurrentState(uuid, part.id),
|
|
1466
|
+
})),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
getCurrentState(uuid, partId) {
|
|
1470
|
+
const accessory = this.accessories.get(uuid);
|
|
1471
|
+
if (!accessory) {
|
|
1472
|
+
return {};
|
|
1473
|
+
}
|
|
1474
|
+
if (partId) {
|
|
1475
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1476
|
+
return part?.clusters || {};
|
|
1477
|
+
}
|
|
1478
|
+
return accessory.clusters || {};
|
|
1479
|
+
}
|
|
1783
1480
|
async notifyPartsListChanged() {
|
|
1784
1481
|
if (!this.aggregator || !this.isCommissioned()) {
|
|
1785
|
-
// No controllers connected, skip notification
|
|
1786
1482
|
return;
|
|
1787
1483
|
}
|
|
1788
1484
|
try {
|
|
1789
|
-
// Access the aggregator's descriptor cluster state
|
|
1790
|
-
// The partsList is automatically updated by Matter.js when endpoints are added/removed
|
|
1791
|
-
// We just need to ensure controllers are notified of the change
|
|
1792
1485
|
const aggregatorState = this.aggregator;
|
|
1793
1486
|
if (aggregatorState.state?.descriptor) {
|
|
1794
|
-
// Get current parts list (endpoint numbers of all children)
|
|
1795
1487
|
const partsList = aggregatorState.state.descriptor.partsList || [];
|
|
1796
1488
|
if (this.config.debugModeEnabled) {
|
|
1797
1489
|
log.debug(`Parts list changed: ${partsList.length} devices (endpoints: ${partsList.join(', ')})`);
|
|
1798
1490
|
}
|
|
1799
|
-
// Trigger a state update event to notify subscribed controllers
|
|
1800
|
-
// By setting the partsList to itself, we trigger the change notification
|
|
1801
1491
|
await this.aggregator.set({
|
|
1802
1492
|
descriptor: {
|
|
1803
1493
|
partsList,
|
|
@@ -1807,7 +1497,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1807
1497
|
}
|
|
1808
1498
|
}
|
|
1809
1499
|
catch (error) {
|
|
1810
|
-
// Non-fatal error - log but don't throw
|
|
1811
1500
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1812
1501
|
log.warn(`Failed to notify controllers of parts list change: ${errorMessage}`);
|
|
1813
1502
|
}
|