homebridge 2.0.0-beta.34 → 2.0.0-beta.36
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 +74 -32
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +48 -46
- package/dist/api.js.map +1 -1
- package/dist/api.spec.d.ts +2 -0
- package/dist/api.spec.d.ts.map +1 -0
- package/dist/api.spec.js +413 -0
- package/dist/api.spec.js.map +1 -0
- package/dist/childBridgeFork.d.ts +1 -18
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +21 -258
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/externalPortService.d.ts +2 -9
- package/dist/externalPortService.d.ts.map +1 -1
- package/dist/externalPortService.js +20 -40
- package/dist/externalPortService.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/logger.spec.d.ts +2 -0
- package/dist/logger.spec.d.ts.map +1 -0
- package/dist/logger.spec.js +95 -0
- package/dist/logger.spec.js.map +1 -0
- package/dist/matter/ChildBridgeMatterManager.d.ts +96 -0
- package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -0
- package/dist/matter/ChildBridgeMatterManager.js +397 -0
- package/dist/matter/ChildBridgeMatterManager.js.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts +48 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.js +73 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts +2 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.d.ts.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.js +293 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.spec.js.map +1 -0
- package/dist/matter/{matterServer.d.ts → MatterAPIImpl.d.ts} +203 -367
- package/dist/matter/MatterAPIImpl.d.ts.map +1 -0
- package/dist/matter/MatterAPIImpl.js +305 -0
- package/dist/matter/MatterAPIImpl.js.map +1 -0
- package/dist/matter/MatterBridgeManager.d.ts +91 -0
- package/dist/matter/MatterBridgeManager.d.ts.map +1 -0
- package/dist/matter/MatterBridgeManager.js +426 -0
- package/dist/matter/MatterBridgeManager.js.map +1 -0
- package/dist/matter/MatterConfigCollector.d.ts +26 -0
- package/dist/matter/MatterConfigCollector.d.ts.map +1 -0
- package/dist/matter/MatterConfigCollector.js +78 -0
- package/dist/matter/MatterConfigCollector.js.map +1 -0
- package/dist/matter/{matterAccessoryCache.d.ts → accessoryCache.d.ts} +12 -3
- package/dist/matter/accessoryCache.d.ts.map +1 -0
- package/dist/matter/{matterAccessoryCache.js → accessoryCache.js} +32 -10
- package/dist/matter/accessoryCache.js.map +1 -0
- package/dist/matter/accessoryCache.spec.d.ts +2 -0
- package/dist/matter/accessoryCache.spec.d.ts.map +1 -0
- package/dist/matter/accessoryCache.spec.js +452 -0
- package/dist/matter/accessoryCache.spec.js.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.d.ts +65 -0
- package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.js +139 -0
- package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts +2 -0
- package/dist/matter/behaviors/BehaviorRegistry.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.spec.js +307 -0
- package/dist/matter/behaviors/BehaviorRegistry.spec.js.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.d.ts +64 -0
- package/dist/matter/behaviors/ColorControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.js +160 -0
- package/dist/matter/behaviors/ColorControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/ColorControlBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.spec.js +29 -0
- package/dist/matter/behaviors/ColorControlBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.d.ts +21 -0
- package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.js +49 -0
- package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/DoorLockBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.spec.js +108 -0
- package/dist/matter/behaviors/DoorLockBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.d.ts +20 -0
- package/dist/matter/behaviors/FanControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.js +45 -0
- package/dist/matter/behaviors/FanControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/FanControlBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.spec.js +23 -0
- package/dist/matter/behaviors/FanControlBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.d.ts +21 -0
- package/dist/matter/behaviors/IdentifyBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.js +27 -0
- package/dist/matter/behaviors/IdentifyBehavior.js.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/IdentifyBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.spec.js +64 -0
- package/dist/matter/behaviors/IdentifyBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.d.ts +34 -0
- package/dist/matter/behaviors/LevelControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.js +75 -0
- package/dist/matter/behaviors/LevelControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/LevelControlBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.spec.js +140 -0
- package/dist/matter/behaviors/LevelControlBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.d.ts +28 -0
- package/dist/matter/behaviors/OnOffBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.js +63 -0
- package/dist/matter/behaviors/OnOffBehavior.js.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/OnOffBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.spec.js +116 -0
- package/dist/matter/behaviors/OnOffBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts +19 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.js +27 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.js.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js +56 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts +23 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js +49 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js +55 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts +19 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.js +27 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.js.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.js +56 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts +22 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.js +35 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.js.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.js +52 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.d.ts +23 -0
- package/dist/matter/behaviors/ThermostatBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.js +79 -0
- package/dist/matter/behaviors/ThermostatBehavior.js.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/ThermostatBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.spec.js +23 -0
- package/dist/matter/behaviors/ThermostatBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts +32 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.js +106 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.js.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts +2 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.d.ts.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.js +27 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.spec.js.map +1 -0
- package/dist/matter/behaviors/index.d.ts +20 -0
- package/dist/matter/behaviors/index.d.ts.map +1 -0
- package/dist/matter/behaviors/index.js +21 -0
- package/dist/matter/behaviors/index.js.map +1 -0
- package/dist/matter/{matterConfigValidator.d.ts → configValidator.d.ts} +1 -1
- package/dist/matter/configValidator.d.ts.map +1 -0
- package/dist/matter/{matterConfigValidator.js → configValidator.js} +1 -1
- package/dist/matter/configValidator.js.map +1 -0
- package/dist/matter/configValidator.spec.d.ts +2 -0
- package/dist/matter/configValidator.spec.d.ts.map +1 -0
- package/dist/matter/configValidator.spec.js +390 -0
- package/dist/matter/configValidator.spec.js.map +1 -0
- package/dist/matter/errorHandler.d.ts +33 -0
- package/dist/matter/errorHandler.d.ts.map +1 -0
- package/dist/matter/errorHandler.js +113 -0
- package/dist/matter/errorHandler.js.map +1 -0
- package/dist/matter/errorHandler.spec.d.ts +2 -0
- package/dist/matter/errorHandler.spec.d.ts.map +1 -0
- package/dist/matter/errorHandler.spec.js +159 -0
- package/dist/matter/errorHandler.spec.js.map +1 -0
- package/dist/matter/index.d.ts +7 -4
- package/dist/matter/index.d.ts.map +1 -1
- package/dist/matter/index.js +7 -4
- package/dist/matter/index.js.map +1 -1
- package/dist/matter/{matterLogFormatter.d.ts → logFormatter.d.ts} +1 -1
- package/dist/matter/logFormatter.d.ts.map +1 -0
- package/dist/matter/{matterLogFormatter.js → logFormatter.js} +33 -6
- package/dist/matter/logFormatter.js.map +1 -0
- package/dist/matter/logFormatter.spec.d.ts +2 -0
- package/dist/matter/logFormatter.spec.d.ts.map +1 -0
- package/dist/matter/logFormatter.spec.js +284 -0
- package/dist/matter/logFormatter.spec.js.map +1 -0
- package/dist/matter/server.d.ts +350 -0
- package/dist/matter/server.d.ts.map +1 -0
- package/dist/matter/{matterServer.js → server.js} +533 -352
- package/dist/matter/server.js.map +1 -0
- package/dist/matter/{matterServerHelpers.d.ts → serverHelpers.d.ts} +2 -2
- package/dist/matter/serverHelpers.d.ts.map +1 -0
- package/dist/matter/{matterServerHelpers.js → serverHelpers.js} +11 -8
- package/dist/matter/serverHelpers.js.map +1 -0
- package/dist/matter/serverHelpers.spec.d.ts +2 -0
- package/dist/matter/serverHelpers.spec.d.ts.map +1 -0
- package/dist/matter/serverHelpers.spec.js +521 -0
- package/dist/matter/serverHelpers.spec.js.map +1 -0
- package/dist/matter/{matterSharedTypes.d.ts → sharedTypes.d.ts} +4 -10
- package/dist/matter/sharedTypes.d.ts.map +1 -0
- package/dist/matter/{matterSharedTypes.js → sharedTypes.js} +4 -10
- package/dist/matter/sharedTypes.js.map +1 -0
- package/dist/matter/{matterStorage.d.ts → storage.d.ts} +9 -2
- package/dist/matter/storage.d.ts.map +1 -0
- package/dist/matter/{matterStorage.js → storage.js} +14 -5
- package/dist/matter/{matterStorage.js.map → storage.js.map} +1 -1
- package/dist/matter/storage.spec.d.ts +2 -0
- package/dist/matter/storage.spec.d.ts.map +1 -0
- package/dist/matter/storage.spec.js +570 -0
- package/dist/matter/storage.spec.js.map +1 -0
- package/dist/matter/typeHelpers.d.ts +45 -0
- package/dist/matter/typeHelpers.d.ts.map +1 -0
- package/dist/matter/typeHelpers.js +57 -0
- package/dist/matter/typeHelpers.js.map +1 -0
- package/dist/matter/typeHelpers.spec.d.ts +2 -0
- package/dist/matter/typeHelpers.spec.d.ts.map +1 -0
- package/dist/matter/typeHelpers.spec.js +127 -0
- package/dist/matter/typeHelpers.spec.js.map +1 -0
- package/dist/matter/{matterTypes.d.ts → types.d.ts} +2 -2
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/{matterTypes.js → types.js} +1 -2
- package/dist/matter/types.js.map +1 -0
- package/dist/platformAccessory.spec.d.ts +2 -0
- package/dist/platformAccessory.spec.d.ts.map +1 -0
- package/dist/platformAccessory.spec.js +126 -0
- package/dist/platformAccessory.spec.js.map +1 -0
- package/dist/pluginManager.spec.d.ts +2 -0
- package/dist/pluginManager.spec.d.ts.map +1 -0
- package/dist/pluginManager.spec.js +43 -0
- package/dist/pluginManager.spec.js.map +1 -0
- package/dist/server.d.ts +4 -13
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +45 -333
- package/dist/server.js.map +1 -1
- package/dist/server.spec.d.ts +2 -0
- package/dist/server.spec.d.ts.map +1 -0
- package/dist/server.spec.js +57 -0
- package/dist/server.spec.js.map +1 -0
- package/dist/user.spec.d.ts +2 -0
- package/dist/user.spec.d.ts.map +1 -0
- package/dist/user.spec.js +31 -0
- package/dist/user.spec.js.map +1 -0
- package/dist/util/mac.spec.d.ts +2 -0
- package/dist/util/mac.spec.d.ts.map +1 -0
- package/dist/util/mac.spec.js +36 -0
- package/dist/util/mac.spec.js.map +1 -0
- package/dist/version.spec.d.ts +2 -0
- package/dist/version.spec.d.ts.map +1 -0
- package/dist/version.spec.js +20 -0
- package/dist/version.spec.js.map +1 -0
- package/package.json +4 -4
- package/dist/matter/matterAccessoryCache.d.ts.map +0 -1
- package/dist/matter/matterAccessoryCache.js.map +0 -1
- package/dist/matter/matterBehaviors.d.ts +0 -194
- package/dist/matter/matterBehaviors.d.ts.map +0 -1
- package/dist/matter/matterBehaviors.js +0 -665
- package/dist/matter/matterBehaviors.js.map +0 -1
- package/dist/matter/matterConfigValidator.d.ts.map +0 -1
- package/dist/matter/matterConfigValidator.js.map +0 -1
- package/dist/matter/matterErrorHandler.d.ts +0 -106
- package/dist/matter/matterErrorHandler.d.ts.map +0 -1
- package/dist/matter/matterErrorHandler.js +0 -495
- package/dist/matter/matterErrorHandler.js.map +0 -1
- package/dist/matter/matterLogFormatter.d.ts.map +0 -1
- package/dist/matter/matterLogFormatter.js.map +0 -1
- package/dist/matter/matterNetworkMonitor.d.ts +0 -68
- package/dist/matter/matterNetworkMonitor.d.ts.map +0 -1
- package/dist/matter/matterNetworkMonitor.js +0 -249
- package/dist/matter/matterNetworkMonitor.js.map +0 -1
- package/dist/matter/matterServer.d.ts.map +0 -1
- package/dist/matter/matterServer.js.map +0 -1
- package/dist/matter/matterServerHelpers.d.ts.map +0 -1
- package/dist/matter/matterServerHelpers.js.map +0 -1
- package/dist/matter/matterSharedTypes.d.ts.map +0 -1
- package/dist/matter/matterSharedTypes.js.map +0 -1
- package/dist/matter/matterStorage.d.ts.map +0 -1
- package/dist/matter/matterTypes.d.ts.map +0 -1
- package/dist/matter/matterTypes.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomBytes } from 'node:crypto';
|
|
8
8
|
import { EventEmitter } from 'node:events';
|
|
9
|
-
import { constants,
|
|
9
|
+
import { constants, watch as fsWatch } from 'node:fs';
|
|
10
10
|
import { access, writeFile } from 'node:fs/promises';
|
|
11
11
|
import { homedir, release } from 'node:os';
|
|
12
12
|
import { join, normalize, resolve } from 'node:path';
|
|
@@ -21,15 +21,15 @@ import fse from 'fs-extra';
|
|
|
21
21
|
import QRCode from 'qrcode-terminal';
|
|
22
22
|
import { Logger } from '../logger.js';
|
|
23
23
|
import getVersion from '../version.js';
|
|
24
|
-
import { MatterAccessoryCache } from './
|
|
25
|
-
import { HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeFanControlServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeRvcCleanModeServer, HomebridgeRvcOperationalStateServer, HomebridgeRvcRunModeServer, HomebridgeServiceAreaServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer,
|
|
26
|
-
import { sanitizeUniqueId, truncateString, validatePort } from './
|
|
27
|
-
import { errorHandler } from './
|
|
28
|
-
import { createHomebridgeLogFormatter } from './
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { deviceTypes, MatterDeviceError, } from './
|
|
24
|
+
import { MatterAccessoryCache } from './accessoryCache.js';
|
|
25
|
+
import { BehaviorRegistry, HomebridgeColorControlServer, HomebridgeDoorLockServer, HomebridgeFanControlServer, HomebridgeIdentifyServer, HomebridgeLevelControlServer, HomebridgeOnOffServer, HomebridgeRvcCleanModeServer, HomebridgeRvcOperationalStateServer, HomebridgeRvcRunModeServer, HomebridgeServiceAreaServer, HomebridgeThermostatServer, HomebridgeWindowCoveringServer, } from './behaviors/index.js';
|
|
26
|
+
import { sanitizeUniqueId, truncateString, validatePort } from './configValidator.js';
|
|
27
|
+
import { errorHandler } from './errorHandler.js';
|
|
28
|
+
import { createHomebridgeLogFormatter } from './logFormatter.js';
|
|
29
|
+
import { applyWindowCoveringFeatures, CLUSTER_IDS, detectBehaviorFeatures, detectWindowCoveringFeatures, determineColorControlFeaturesFromHandlers, extractColorControlFeatures, extractThermostatFeatures, validateAccessoryRequiredFields, } from './serverHelpers.js';
|
|
30
|
+
import { MatterStorageManager } from './storage.js';
|
|
31
|
+
import { isDeviceType, withBehaviors, withFeatures } from './typeHelpers.js';
|
|
32
|
+
import { deviceTypes, MatterDeviceError, } from './types.js';
|
|
33
33
|
const log = Logger.withPrefix('Matter/Server');
|
|
34
34
|
/**
|
|
35
35
|
* Constants for Matter server configuration
|
|
@@ -54,9 +54,31 @@ export class MatterServer extends EventEmitter {
|
|
|
54
54
|
serverNode = null;
|
|
55
55
|
aggregator = null;
|
|
56
56
|
accessories = new Map();
|
|
57
|
+
behaviorRegistry;
|
|
57
58
|
isRunning = false;
|
|
58
59
|
MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
|
|
59
60
|
shutdownHandler = null;
|
|
61
|
+
// Map cluster names to custom behavior classes
|
|
62
|
+
// Only clusters with user-triggered commands need custom behaviors
|
|
63
|
+
static CLUSTER_BEHAVIOR_MAP = {
|
|
64
|
+
// Core controls
|
|
65
|
+
onOff: HomebridgeOnOffServer,
|
|
66
|
+
levelControl: HomebridgeLevelControlServer,
|
|
67
|
+
colorControl: HomebridgeColorControlServer,
|
|
68
|
+
// Coverings & locks
|
|
69
|
+
windowCovering: HomebridgeWindowCoveringServer,
|
|
70
|
+
doorLock: HomebridgeDoorLockServer,
|
|
71
|
+
// Climate control
|
|
72
|
+
fanControl: HomebridgeFanControlServer,
|
|
73
|
+
thermostat: HomebridgeThermostatServer,
|
|
74
|
+
// Robotic vacuum cleaners
|
|
75
|
+
rvcOperationalState: HomebridgeRvcOperationalStateServer,
|
|
76
|
+
rvcRunMode: HomebridgeRvcRunModeServer,
|
|
77
|
+
rvcCleanMode: HomebridgeRvcCleanModeServer,
|
|
78
|
+
serviceArea: HomebridgeServiceAreaServer,
|
|
79
|
+
// Identification
|
|
80
|
+
identify: HomebridgeIdentifyServer,
|
|
81
|
+
};
|
|
60
82
|
// Internal commissioning values (generated, not user-configurable)
|
|
61
83
|
passcode = 0;
|
|
62
84
|
discriminator = 0;
|
|
@@ -70,6 +92,7 @@ export class MatterServer extends EventEmitter {
|
|
|
70
92
|
accessoryCache = null;
|
|
71
93
|
// Fabric monitoring
|
|
72
94
|
fabricMonitorInterval = null;
|
|
95
|
+
fabricCheckTimeout = null;
|
|
73
96
|
previousFabrics = new Map();
|
|
74
97
|
constructor(config) {
|
|
75
98
|
super();
|
|
@@ -95,8 +118,21 @@ export class MatterServer extends EventEmitter {
|
|
|
95
118
|
// Initialize commissioning values (will be loaded from storage in start())
|
|
96
119
|
this.vendorId = DEFAULT_VENDOR_ID;
|
|
97
120
|
this.productId = DEFAULT_PRODUCT_ID;
|
|
98
|
-
//
|
|
99
|
-
|
|
121
|
+
// Create behavior registry and set it on all behavior classes
|
|
122
|
+
this.behaviorRegistry = new BehaviorRegistry(this.accessories);
|
|
123
|
+
// Set the registry on all custom behavior classes
|
|
124
|
+
HomebridgeOnOffServer.setRegistry(this.behaviorRegistry);
|
|
125
|
+
HomebridgeLevelControlServer.setRegistry(this.behaviorRegistry);
|
|
126
|
+
HomebridgeColorControlServer.setRegistry(this.behaviorRegistry);
|
|
127
|
+
HomebridgeWindowCoveringServer.setRegistry(this.behaviorRegistry);
|
|
128
|
+
HomebridgeDoorLockServer.setRegistry(this.behaviorRegistry);
|
|
129
|
+
HomebridgeFanControlServer.setRegistry(this.behaviorRegistry);
|
|
130
|
+
HomebridgeThermostatServer.setRegistry(this.behaviorRegistry);
|
|
131
|
+
HomebridgeIdentifyServer.setRegistry(this.behaviorRegistry);
|
|
132
|
+
HomebridgeRvcOperationalStateServer.setRegistry(this.behaviorRegistry);
|
|
133
|
+
HomebridgeRvcRunModeServer.setRegistry(this.behaviorRegistry);
|
|
134
|
+
HomebridgeRvcCleanModeServer.setRegistry(this.behaviorRegistry);
|
|
135
|
+
HomebridgeServiceAreaServer.setRegistry(this.behaviorRegistry);
|
|
100
136
|
}
|
|
101
137
|
/**
|
|
102
138
|
* Validate and sanitize Matter server configuration
|
|
@@ -239,7 +275,8 @@ export class MatterServer extends EventEmitter {
|
|
|
239
275
|
const digitCounts = new Map();
|
|
240
276
|
for (const digit of passcodeStr) {
|
|
241
277
|
digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
|
|
242
|
-
|
|
278
|
+
const count = digitCounts.get(digit);
|
|
279
|
+
if (count !== undefined && count > 3) {
|
|
243
280
|
return false;
|
|
244
281
|
}
|
|
245
282
|
}
|
|
@@ -285,10 +322,12 @@ export class MatterServer extends EventEmitter {
|
|
|
285
322
|
}
|
|
286
323
|
catch (error) {
|
|
287
324
|
// Check if this is a storage corruption error
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
||
|
|
325
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
326
|
+
const causeMessage = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
|
|
327
|
+
const isStorageError = errorMessage.includes('Invalid public key encoding')
|
|
328
|
+
|| errorMessage.includes('FabricManager unavailable')
|
|
329
|
+
|| errorMessage.includes('key-input')
|
|
330
|
+
|| causeMessage.includes('Invalid public key encoding');
|
|
292
331
|
if (!isStorageError) {
|
|
293
332
|
// Not a storage error, rethrow
|
|
294
333
|
throw error;
|
|
@@ -306,18 +345,32 @@ export class MatterServer extends EventEmitter {
|
|
|
306
345
|
const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
|
|
307
346
|
try {
|
|
308
347
|
let removedSomething = false;
|
|
309
|
-
// Delete the ServerNodeStore subdirectory
|
|
310
|
-
|
|
348
|
+
// Delete the ServerNodeStore subdirectory (async check and removal)
|
|
349
|
+
try {
|
|
350
|
+
await fse.stat(serverNodeStorePath);
|
|
311
351
|
log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
|
|
312
352
|
await fse.remove(serverNodeStorePath);
|
|
313
353
|
removedSomething = true;
|
|
314
354
|
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
357
|
+
if (code !== 'ENOENT') {
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
315
361
|
// Delete the ServerNodeStore JSON file (contains fabric data)
|
|
316
|
-
|
|
362
|
+
try {
|
|
363
|
+
await fse.stat(serverNodeStoreJsonFile);
|
|
317
364
|
log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
|
|
318
365
|
await fse.remove(serverNodeStoreJsonFile);
|
|
319
366
|
removedSomething = true;
|
|
320
367
|
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
370
|
+
if (code !== 'ENOENT') {
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
321
374
|
if (removedSomething) {
|
|
322
375
|
log.info('Corrupted storage removed, retrying ServerNode creation...');
|
|
323
376
|
}
|
|
@@ -353,9 +406,6 @@ export class MatterServer extends EventEmitter {
|
|
|
353
406
|
// Load or generate commissioning credentials
|
|
354
407
|
await this.loadOrGenerateCredentials();
|
|
355
408
|
log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
|
|
356
|
-
// Start network monitoring
|
|
357
|
-
networkMonitor.startMonitoring();
|
|
358
|
-
this.cleanupHandlers.push(() => networkMonitor.stopMonitoring());
|
|
359
409
|
// Configure network interfaces if specified in the config
|
|
360
410
|
if (this.config.networkInterfaces && this.config.networkInterfaces.length > 0) {
|
|
361
411
|
const environment = Environment.default;
|
|
@@ -474,7 +524,17 @@ export class MatterServer extends EventEmitter {
|
|
|
474
524
|
}
|
|
475
525
|
/**
|
|
476
526
|
* Run the server after devices have been added (for external accessory mode)
|
|
477
|
-
*
|
|
527
|
+
*
|
|
528
|
+
* This must be called after registerPlatformAccessories() when using externalAccessory mode.
|
|
529
|
+
* In bridge mode, the server starts automatically when accessories are registered.
|
|
530
|
+
*
|
|
531
|
+
* @throws {MatterDeviceError} If server node is not initialized or server is already running
|
|
532
|
+
* @example
|
|
533
|
+
* ```typescript
|
|
534
|
+
* await matterServer.start()
|
|
535
|
+
* await matterServer.registerPlatformAccessories('plugin', 'platform', accessories)
|
|
536
|
+
* await matterServer.runServer() // External accessory mode only
|
|
537
|
+
* ```
|
|
478
538
|
*/
|
|
479
539
|
async runServer() {
|
|
480
540
|
if (!this.serverNode) {
|
|
@@ -655,7 +715,8 @@ export class MatterServer extends EventEmitter {
|
|
|
655
715
|
log.debug(`Saved commissioning info to ${commissioningFilePath}`);
|
|
656
716
|
}
|
|
657
717
|
catch (error) {
|
|
658
|
-
|
|
718
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
719
|
+
log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
|
|
659
720
|
}
|
|
660
721
|
// Display commissioning information
|
|
661
722
|
log.info(`\n${'='.repeat(60)}`);
|
|
@@ -690,6 +751,7 @@ export class MatterServer extends EventEmitter {
|
|
|
690
751
|
}
|
|
691
752
|
/**
|
|
692
753
|
* Start monitoring fabric changes to emit commissioning events
|
|
754
|
+
* Uses file watching instead of polling for better performance
|
|
693
755
|
*/
|
|
694
756
|
startFabricMonitoring() {
|
|
695
757
|
// Stop any existing monitor
|
|
@@ -700,22 +762,63 @@ export class MatterServer extends EventEmitter {
|
|
|
700
762
|
this.previousFabrics.set(fabric.fabricIndex, fabric);
|
|
701
763
|
}
|
|
702
764
|
log.debug('Starting fabric monitoring for commissioning events');
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
765
|
+
// Watch the storage file for fabric changes instead of polling
|
|
766
|
+
// This is much more efficient - only checks when the file actually changes
|
|
767
|
+
if (this.storageManager && this.matterStoragePath) {
|
|
768
|
+
const fabricStorageFile = join(this.matterStoragePath, `${this.config.uniqueId}.json`);
|
|
769
|
+
try {
|
|
770
|
+
// Use fs.watch for efficient file monitoring
|
|
771
|
+
this.fabricMonitorInterval = fsWatch(fabricStorageFile, (eventType) => {
|
|
772
|
+
if (eventType === 'change') {
|
|
773
|
+
// Debounce rapid changes (file writes may trigger multiple events)
|
|
774
|
+
if (this.fabricCheckTimeout) {
|
|
775
|
+
clearTimeout(this.fabricCheckTimeout);
|
|
776
|
+
}
|
|
777
|
+
this.fabricCheckTimeout = setTimeout(() => {
|
|
778
|
+
this.checkFabricChanges();
|
|
779
|
+
}, 100); // 100ms debounce
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
log.debug(`Watching fabric storage file for changes: ${fabricStorageFile}`);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
// Fall back to polling if file watching fails
|
|
786
|
+
log.warn('Failed to set up file watcher for fabric monitoring, falling back to polling:', error);
|
|
787
|
+
this.fabricMonitorInterval = setInterval(() => {
|
|
788
|
+
this.checkFabricChanges();
|
|
789
|
+
}, FABRIC_MONITOR_INTERVAL_MS);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
// Fallback to polling if storage not available
|
|
794
|
+
this.fabricMonitorInterval = setInterval(() => {
|
|
795
|
+
this.checkFabricChanges();
|
|
796
|
+
}, FABRIC_MONITOR_INTERVAL_MS);
|
|
797
|
+
}
|
|
707
798
|
// Add to clean up handlers
|
|
708
799
|
this.cleanupHandlers.push(() => this.stopFabricMonitoring());
|
|
709
800
|
}
|
|
710
801
|
/**
|
|
711
|
-
* Stop fabric monitoring
|
|
802
|
+
* Stop fabric monitoring and clean up watchers/timers
|
|
712
803
|
*/
|
|
713
804
|
stopFabricMonitoring() {
|
|
714
805
|
if (this.fabricMonitorInterval) {
|
|
715
|
-
|
|
806
|
+
// Could be either a setInterval or a file watcher
|
|
807
|
+
if (typeof this.fabricMonitorInterval === 'object' && 'close' in this.fabricMonitorInterval) {
|
|
808
|
+
// It's a file watcher
|
|
809
|
+
this.fabricMonitorInterval.close();
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
// It's a setInterval
|
|
813
|
+
clearInterval(this.fabricMonitorInterval);
|
|
814
|
+
}
|
|
716
815
|
this.fabricMonitorInterval = null;
|
|
717
816
|
log.debug('Stopped fabric monitoring');
|
|
718
817
|
}
|
|
818
|
+
if (this.fabricCheckTimeout) {
|
|
819
|
+
clearTimeout(this.fabricCheckTimeout);
|
|
820
|
+
this.fabricCheckTimeout = null;
|
|
821
|
+
}
|
|
719
822
|
}
|
|
720
823
|
/**
|
|
721
824
|
* Check for fabric changes and emit appropriate events
|
|
@@ -794,11 +897,21 @@ export class MatterServer extends EventEmitter {
|
|
|
794
897
|
log.debug('Updated commissioning info file');
|
|
795
898
|
}
|
|
796
899
|
catch (error) {
|
|
797
|
-
|
|
900
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
901
|
+
log.debug(`Failed to update commissioning info file: ${errorMessage}`);
|
|
798
902
|
}
|
|
799
903
|
}
|
|
800
904
|
/**
|
|
801
905
|
* Register Matter platform accessories (Plugin API - matches HAP pattern)
|
|
906
|
+
*
|
|
907
|
+
* Registers Matter accessories from a dynamic platform plugin. Accessories are stored
|
|
908
|
+
* and automatically restored on server restart.
|
|
909
|
+
*
|
|
910
|
+
* @param pluginIdentifier - The plugin identifier (e.g., 'homebridge-example')
|
|
911
|
+
* @param platformName - The platform name from config.json
|
|
912
|
+
* @param accessories - Array of Matter accessories to register
|
|
913
|
+
* @throws {MatterDeviceError} If maximum device limit is reached or accessory is invalid
|
|
914
|
+
* @see {@link MatterAccessory} for accessory structure
|
|
802
915
|
*/
|
|
803
916
|
async registerPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
804
917
|
for (const accessory of accessories) {
|
|
@@ -813,6 +926,36 @@ export class MatterServer extends EventEmitter {
|
|
|
813
926
|
await this.unregisterAccessory(accessory.uuid);
|
|
814
927
|
}
|
|
815
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Update Matter platform accessories in the cache
|
|
931
|
+
* Similar to api.updatePlatformAccessories() for HAP accessories
|
|
932
|
+
*
|
|
933
|
+
* This updates the cached accessory information without unregistering and re-registering.
|
|
934
|
+
* Useful when device metadata changes (name, manufacturer, firmware version, etc.)
|
|
935
|
+
*/
|
|
936
|
+
async updatePlatformAccessories(accessories) {
|
|
937
|
+
if (!this.accessoryCache) {
|
|
938
|
+
log.warn('Cannot update Matter platform accessories - cache not initialized');
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
for (const accessory of accessories) {
|
|
942
|
+
const internal = accessory;
|
|
943
|
+
// Verify accessory exists in current session and cache
|
|
944
|
+
if (!this.accessories.has(accessory.uuid)) {
|
|
945
|
+
log.warn(`Cannot update Matter accessory ${accessory.uuid} - not registered in current session`);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!this.accessoryCache.hasCached(accessory.uuid)) {
|
|
949
|
+
log.warn(`Cannot update Matter accessory ${accessory.uuid} - not found in cache`);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
// Update the in-memory accessory
|
|
953
|
+
this.accessories.set(accessory.uuid, internal);
|
|
954
|
+
log.debug(`Updated Matter accessory ${accessory.uuid} (${accessory.displayName})`);
|
|
955
|
+
}
|
|
956
|
+
// Save updated accessories to cache
|
|
957
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
958
|
+
}
|
|
816
959
|
/**
|
|
817
960
|
* Register a single Matter accessory (internal method)
|
|
818
961
|
*/
|
|
@@ -832,6 +975,69 @@ export class MatterServer extends EventEmitter {
|
|
|
832
975
|
+ `New accessory: "${accessory.displayName}"\n`
|
|
833
976
|
+ 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
|
|
834
977
|
}
|
|
978
|
+
// Restore cached state if available
|
|
979
|
+
this.restoreCachedState(accessory);
|
|
980
|
+
// Check device limit
|
|
981
|
+
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
982
|
+
throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
|
|
983
|
+
+ `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
|
|
984
|
+
+ `Current registered devices: ${this.accessories.size}`);
|
|
985
|
+
}
|
|
986
|
+
try {
|
|
987
|
+
// Prepare device type with WindowCovering features
|
|
988
|
+
let deviceType = accessory.deviceType;
|
|
989
|
+
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
990
|
+
if (windowCoveringFeatures.length > 0) {
|
|
991
|
+
deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
|
|
992
|
+
}
|
|
993
|
+
// Detect cluster features for behavior configuration
|
|
994
|
+
const features = this.detectClusterFeatures(accessory, deviceType);
|
|
995
|
+
// Build and apply custom behaviors based on handlers
|
|
996
|
+
const customBehaviors = this.buildCustomBehaviors(accessory, deviceType, features);
|
|
997
|
+
if (customBehaviors.length > 0) {
|
|
998
|
+
deviceType = withBehaviors(deviceType, customBehaviors);
|
|
999
|
+
log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
|
|
1000
|
+
}
|
|
1001
|
+
// Add BridgedDeviceBasicInformationServer for bridged devices only
|
|
1002
|
+
// This is required by the Matter spec for devices behind an aggregator
|
|
1003
|
+
// External accessories should NOT have this cluster
|
|
1004
|
+
if (!this.config.externalAccessory) {
|
|
1005
|
+
deviceType = withBehaviors(deviceType, [BridgedDeviceBasicInformationServer]);
|
|
1006
|
+
log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
|
|
1007
|
+
}
|
|
1008
|
+
// Create endpoint with cluster states
|
|
1009
|
+
const endpointOptions = this.createEndpointOptions(accessory);
|
|
1010
|
+
const endpoint = new Endpoint(deviceType, endpointOptions);
|
|
1011
|
+
if (this.config.debugModeEnabled) {
|
|
1012
|
+
log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
|
|
1013
|
+
}
|
|
1014
|
+
// Add endpoint to aggregator or serverNode depending on mode
|
|
1015
|
+
if (this.config.externalAccessory) {
|
|
1016
|
+
await this.serverNode.add(endpoint);
|
|
1017
|
+
log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
await this.aggregator.add(endpoint);
|
|
1021
|
+
if (this.config.debugModeEnabled) {
|
|
1022
|
+
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Register command handlers
|
|
1026
|
+
this.registerAccessoryHandlers(accessory);
|
|
1027
|
+
// Create and register child endpoints (parts)
|
|
1028
|
+
const internalParts = await this.createAccessoryParts(accessory);
|
|
1029
|
+
// Finalize registration (store, emit events, save cache)
|
|
1030
|
+
await this.finalizeAccessoryRegistration(accessory, endpoint, internalParts);
|
|
1031
|
+
}
|
|
1032
|
+
catch (error) {
|
|
1033
|
+
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
1034
|
+
throw new MatterDeviceError(`Failed to register accessory: ${error}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Restore cached state for an accessory
|
|
1039
|
+
*/
|
|
1040
|
+
restoreCachedState(accessory) {
|
|
835
1041
|
// Check if there's a cached version - merge cached cluster states with new registration.
|
|
836
1042
|
// This ensures state persistence across Homebridge restarts.
|
|
837
1043
|
if (this.accessoryCache && this.accessoryCache.hasCached(accessory.uuid)) {
|
|
@@ -858,345 +1064,304 @@ export class MatterServer extends EventEmitter {
|
|
|
858
1064
|
log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
|
|
859
1065
|
}
|
|
860
1066
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Detect cluster features for an accessory
|
|
1070
|
+
* Returns an object containing detected features for various clusters
|
|
1071
|
+
*/
|
|
1072
|
+
detectClusterFeatures(accessory, deviceType) {
|
|
1073
|
+
// Detect WindowCovering features
|
|
1074
|
+
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
1075
|
+
// Detect ServiceArea features
|
|
1076
|
+
let serviceAreaFeatures = null;
|
|
1077
|
+
if (accessory.clusters?.serviceArea) {
|
|
1078
|
+
const features = [];
|
|
1079
|
+
// Check if Maps feature should be enabled (when supportedMaps is defined)
|
|
1080
|
+
if (accessory.clusters.serviceArea.supportedMaps) {
|
|
1081
|
+
features.push('Maps');
|
|
1082
|
+
}
|
|
1083
|
+
// Check if ProgressReporting feature should be enabled (when progress is defined)
|
|
1084
|
+
if (accessory.clusters.serviceArea.progress !== undefined) {
|
|
1085
|
+
features.push('ProgressReporting');
|
|
1086
|
+
}
|
|
1087
|
+
if (features.length > 0) {
|
|
1088
|
+
serviceAreaFeatures = features;
|
|
1089
|
+
log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
|
|
1090
|
+
}
|
|
866
1091
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
|
|
1092
|
+
// Detect ColorControl features
|
|
1093
|
+
let colorControlFeatures = null;
|
|
1094
|
+
if (accessory.handlers?.colorControl) {
|
|
1095
|
+
colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
|
|
1096
|
+
if (colorControlFeatures) {
|
|
1097
|
+
colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
|
|
874
1098
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1099
|
+
}
|
|
1100
|
+
// Detect Thermostat features
|
|
1101
|
+
let thermostatFeatures = null;
|
|
1102
|
+
if (accessory.handlers?.thermostat) {
|
|
1103
|
+
thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
windowCoveringFeatures,
|
|
1107
|
+
serviceAreaFeatures,
|
|
1108
|
+
colorControlFeatures,
|
|
1109
|
+
thermostatFeatures,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Build custom behaviors for an accessory based on handlers
|
|
1114
|
+
*/
|
|
1115
|
+
buildCustomBehaviors(accessory, deviceType, features) {
|
|
1116
|
+
const customBehaviors = [];
|
|
1117
|
+
if (!accessory.handlers) {
|
|
1118
|
+
return customBehaviors;
|
|
1119
|
+
}
|
|
1120
|
+
log.debug(`[${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
|
|
1121
|
+
// Use the static cluster behavior map
|
|
1122
|
+
const behaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
1123
|
+
// For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
|
|
1124
|
+
// These clusters need to be added to the device type even if there are no handlers
|
|
1125
|
+
if (isDeviceType(deviceType, devices.RoboticVacuumCleanerDevice)) {
|
|
1126
|
+
// Import RVC requirements
|
|
1127
|
+
const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
|
|
1128
|
+
// Add RvcCleanMode if defined in clusters
|
|
1129
|
+
if (accessory.clusters?.rvcCleanMode) {
|
|
1130
|
+
// Check if there's a custom behavior with handlers
|
|
1131
|
+
if (accessory.handlers?.rvcCleanMode) {
|
|
1132
|
+
const behaviorClass = HomebridgeRvcCleanModeServer;
|
|
1133
|
+
customBehaviors.push(behaviorClass);
|
|
1134
|
+
log.info('Adding custom RvcCleanMode behavior with handlers');
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
// No handlers, use base server
|
|
1138
|
+
customBehaviors.push(RvcCleanModeServer);
|
|
1139
|
+
log.info('Adding base RvcCleanMode server');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Add ServiceArea if defined in clusters
|
|
1143
|
+
if (accessory.clusters?.serviceArea) {
|
|
1144
|
+
// Check if there's a custom behavior with handlers
|
|
1145
|
+
if (accessory.handlers?.serviceArea) {
|
|
1146
|
+
let behaviorClass = HomebridgeServiceAreaServer;
|
|
1147
|
+
// Apply features if detected
|
|
1148
|
+
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1149
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
1150
|
+
log.info(`ServiceArea custom behavior will have features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
892
1151
|
}
|
|
1152
|
+
customBehaviors.push(behaviorClass);
|
|
1153
|
+
log.info('Adding custom ServiceArea behavior with handlers');
|
|
893
1154
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1155
|
+
else {
|
|
1156
|
+
// No handlers, use base server with features
|
|
1157
|
+
let behaviorClass = ServiceAreaServer;
|
|
1158
|
+
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1159
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
1160
|
+
log.info(`ServiceArea base server will have features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
900
1161
|
}
|
|
1162
|
+
customBehaviors.push(behaviorClass);
|
|
1163
|
+
log.info('Adding base ServiceArea server');
|
|
901
1164
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
for (const clusterName of Object.keys(accessory.handlers || {})) {
|
|
1168
|
+
// Skip windowCovering if we already applied features via base WindowCoveringServer
|
|
1169
|
+
const skipWindowCoveringBehavior = accessory.context?._skipWindowCoveringBehavior;
|
|
1170
|
+
if (clusterName === 'windowCovering' && skipWindowCoveringBehavior) {
|
|
1171
|
+
log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
// Skip RVC clusters - they're handled specially above for RoboticVacuumCleaner
|
|
1175
|
+
if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea') {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
let behaviorClass = behaviorMap[clusterName];
|
|
1179
|
+
// Apply ColorControl features if we detected them earlier
|
|
1180
|
+
if (clusterName === 'colorControl' && behaviorClass && features.colorControlFeatures && features.colorControlFeatures.length > 0) {
|
|
1181
|
+
behaviorClass = withFeatures(behaviorClass, features.colorControlFeatures);
|
|
1182
|
+
log.info(`ColorControl custom behavior will preserve features: ${features.colorControlFeatures.join(', ')}`);
|
|
1183
|
+
}
|
|
1184
|
+
// Apply Thermostat features if we detected them earlier
|
|
1185
|
+
if (clusterName === 'thermostat' && behaviorClass && features.thermostatFeatures && features.thermostatFeatures.length > 0) {
|
|
1186
|
+
behaviorClass = withFeatures(behaviorClass, features.thermostatFeatures);
|
|
1187
|
+
log.info(`Thermostat custom behavior will preserve features: ${features.thermostatFeatures.join(', ')}`);
|
|
1188
|
+
}
|
|
1189
|
+
// Apply ServiceArea features if we detected them earlier
|
|
1190
|
+
if (clusterName === 'serviceArea' && behaviorClass && features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
1191
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
1192
|
+
log.info(`ServiceArea custom behavior will preserve features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
1193
|
+
}
|
|
1194
|
+
// Apply WindowCovering features to custom behavior as well
|
|
1195
|
+
// (features were already applied to base device type, but custom behavior needs them too)
|
|
1196
|
+
if (clusterName === 'windowCovering') {
|
|
1197
|
+
log.debug(`WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${features.windowCoveringFeatures}, length=${features.windowCoveringFeatures?.length}`);
|
|
1198
|
+
if (behaviorClass && features.windowCoveringFeatures && features.windowCoveringFeatures.length > 0) {
|
|
1199
|
+
behaviorClass = withFeatures(behaviorClass, features.windowCoveringFeatures);
|
|
1200
|
+
log.debug(`WindowCovering custom behavior will have features: ${features.windowCoveringFeatures.join(', ')}`);
|
|
906
1201
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const behaviorMap = {
|
|
910
|
-
// Core controls
|
|
911
|
-
onOff: HomebridgeOnOffServer,
|
|
912
|
-
levelControl: HomebridgeLevelControlServer,
|
|
913
|
-
colorControl: HomebridgeColorControlServer,
|
|
914
|
-
// Coverings & locks
|
|
915
|
-
windowCovering: HomebridgeWindowCoveringServer,
|
|
916
|
-
doorLock: HomebridgeDoorLockServer,
|
|
917
|
-
// Climate control
|
|
918
|
-
fanControl: HomebridgeFanControlServer,
|
|
919
|
-
thermostat: HomebridgeThermostatServer,
|
|
920
|
-
// Robotic vacuum cleaners
|
|
921
|
-
rvcOperationalState: HomebridgeRvcOperationalStateServer,
|
|
922
|
-
rvcRunMode: HomebridgeRvcRunModeServer,
|
|
923
|
-
rvcCleanMode: HomebridgeRvcCleanModeServer,
|
|
924
|
-
serviceArea: HomebridgeServiceAreaServer,
|
|
925
|
-
// Identification
|
|
926
|
-
identify: HomebridgeIdentifyServer,
|
|
927
|
-
};
|
|
928
|
-
// Build array of custom behaviors to apply based on what handlers are defined
|
|
929
|
-
const customBehaviors = [];
|
|
930
|
-
// For RoboticVacuumCleaner, add optional clusters if they're defined in accessory.clusters
|
|
931
|
-
// These clusters need to be added to the device type even if there are no handlers
|
|
932
|
-
if (deviceType === devices.RoboticVacuumCleanerDevice || deviceType.name === 'RoboticVacuumCleaner') {
|
|
933
|
-
// Import RVC requirements
|
|
934
|
-
const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
|
|
935
|
-
// Add RvcCleanMode if defined in clusters
|
|
936
|
-
if (accessory.clusters?.rvcCleanMode) {
|
|
937
|
-
// Check if there's a custom behavior with handlers
|
|
938
|
-
if (accessory.handlers?.rvcCleanMode) {
|
|
939
|
-
const behaviorClass = HomebridgeRvcCleanModeServer;
|
|
940
|
-
customBehaviors.push(behaviorClass);
|
|
941
|
-
log.info('Adding custom RvcCleanMode behavior with handlers');
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
// No handlers, use base server
|
|
945
|
-
customBehaviors.push(RvcCleanModeServer);
|
|
946
|
-
log.info('Adding base RvcCleanMode server');
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Add ServiceArea if defined in clusters
|
|
950
|
-
if (accessory.clusters?.serviceArea) {
|
|
951
|
-
// Check if there's a custom behavior with handlers
|
|
952
|
-
if (accessory.handlers?.serviceArea) {
|
|
953
|
-
let behaviorClass = HomebridgeServiceAreaServer;
|
|
954
|
-
// Apply features if detected
|
|
955
|
-
if (serviceAreaFeatures && serviceAreaFeatures.length > 0) {
|
|
956
|
-
behaviorClass = behaviorClass.with(...serviceAreaFeatures);
|
|
957
|
-
log.info(`ServiceArea custom behavior will have features: ${serviceAreaFeatures.join(', ')}`);
|
|
958
|
-
}
|
|
959
|
-
customBehaviors.push(behaviorClass);
|
|
960
|
-
log.info('Adding custom ServiceArea behavior with handlers');
|
|
961
|
-
}
|
|
962
|
-
else {
|
|
963
|
-
// No handlers, use base server with features
|
|
964
|
-
let behaviorClass = ServiceAreaServer;
|
|
965
|
-
if (serviceAreaFeatures && serviceAreaFeatures.length > 0) {
|
|
966
|
-
behaviorClass = behaviorClass.with(...serviceAreaFeatures);
|
|
967
|
-
log.info(`ServiceArea base server will have features: ${serviceAreaFeatures.join(', ')}`);
|
|
968
|
-
}
|
|
969
|
-
customBehaviors.push(behaviorClass);
|
|
970
|
-
log.info('Adding base ServiceArea server');
|
|
971
|
-
}
|
|
972
|
-
}
|
|
1202
|
+
else {
|
|
1203
|
+
log.debug(`Skipping WindowCovering feature application: behaviorClass=${!!behaviorClass}, features=${features.windowCoveringFeatures}`);
|
|
973
1204
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1205
|
+
}
|
|
1206
|
+
if (behaviorClass) {
|
|
1207
|
+
customBehaviors.push(behaviorClass);
|
|
1208
|
+
log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
|
|
1209
|
+
}
|
|
1210
|
+
else {
|
|
1211
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return customBehaviors;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Create endpoint options for an accessory
|
|
1218
|
+
*/
|
|
1219
|
+
createEndpointOptions(accessory) {
|
|
1220
|
+
const endpointOptions = {
|
|
1221
|
+
id: accessory.uuid,
|
|
1222
|
+
...accessory.clusters, // Spread cluster states as initial values
|
|
1223
|
+
};
|
|
1224
|
+
// Add bridgedDeviceBasicInformation cluster only for bridged devices
|
|
1225
|
+
// For external accessories, use the root basicInformation instead
|
|
1226
|
+
if (!this.config.externalAccessory) {
|
|
1227
|
+
endpointOptions.bridgedDeviceBasicInformation = {
|
|
1228
|
+
vendorName: accessory.manufacturer,
|
|
1229
|
+
nodeLabel: accessory.displayName, // Main end user name for the device
|
|
1230
|
+
productName: accessory.model,
|
|
1231
|
+
productLabel: accessory.displayName,
|
|
1232
|
+
serialNumber: accessory.serialNumber,
|
|
1233
|
+
reachable: true,
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return endpointOptions;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Register command handlers for an accessory
|
|
1240
|
+
*/
|
|
1241
|
+
registerAccessoryHandlers(accessory) {
|
|
1242
|
+
if (!accessory.handlers) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
log.info(`Setting up handlers for accessory ${accessory.uuid}`);
|
|
1246
|
+
// Register handlers with the custom behavior classes
|
|
1247
|
+
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1248
|
+
log.info(` Processing cluster: ${clusterName}`);
|
|
1249
|
+
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1250
|
+
this.behaviorRegistry.registerHandler(accessory.uuid, clusterName, commandName, handler);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Create and register child endpoints (parts) for an accessory
|
|
1256
|
+
*/
|
|
1257
|
+
async createAccessoryParts(accessory) {
|
|
1258
|
+
const internalParts = [];
|
|
1259
|
+
if (!accessory.parts || accessory.parts.length === 0) {
|
|
1260
|
+
return internalParts;
|
|
1261
|
+
}
|
|
1262
|
+
log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
|
|
1263
|
+
for (const part of accessory.parts) {
|
|
1264
|
+
// Create unique endpoint ID for this part
|
|
1265
|
+
const partEndpointId = `${accessory.uuid}-part-${part.id}`;
|
|
1266
|
+
// Register the part endpoint mapping for handler context
|
|
1267
|
+
this.behaviorRegistry.registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
|
|
1268
|
+
// Apply custom behaviors to part based on its handlers (same logic as main accessory)
|
|
1269
|
+
let partDeviceType = part.deviceType;
|
|
1270
|
+
const partCustomBehaviors = [];
|
|
1271
|
+
if (part.handlers) {
|
|
1272
|
+
// Use the static cluster behavior map for parts as well
|
|
1273
|
+
const partBehaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
1274
|
+
for (const clusterName of Object.keys(part.handlers)) {
|
|
1275
|
+
const behaviorClass = partBehaviorMap[clusterName];
|
|
1012
1276
|
if (behaviorClass) {
|
|
1013
|
-
|
|
1014
|
-
log.info(`Will use ${behaviorClass.name} for ${
|
|
1277
|
+
partCustomBehaviors.push(behaviorClass);
|
|
1278
|
+
log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
|
|
1015
1279
|
}
|
|
1016
1280
|
else {
|
|
1017
|
-
log.warn(`No custom behavior class available for cluster '${clusterName}'
|
|
1281
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
|
|
1018
1282
|
}
|
|
1019
1283
|
}
|
|
1020
|
-
if (
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1023
|
-
log.info(`Applied ${
|
|
1284
|
+
if (partCustomBehaviors.length > 0) {
|
|
1285
|
+
// Add custom behaviors to part device type
|
|
1286
|
+
partDeviceType = withBehaviors(partDeviceType, partCustomBehaviors);
|
|
1287
|
+
log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
|
|
1024
1288
|
}
|
|
1025
1289
|
}
|
|
1026
|
-
// Add BridgedDeviceBasicInformationServer for bridged
|
|
1027
|
-
// This is required by the Matter spec for devices behind an aggregator
|
|
1028
|
-
// External accessories should NOT have this cluster
|
|
1290
|
+
// Add BridgedDeviceBasicInformationServer for bridged parts
|
|
1029
1291
|
if (!this.config.externalAccessory) {
|
|
1030
|
-
|
|
1031
|
-
log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
|
|
1292
|
+
partDeviceType = withBehaviors(partDeviceType, [BridgedDeviceBasicInformationServer]);
|
|
1032
1293
|
}
|
|
1033
|
-
// Create endpoint with
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
id: accessory.uuid,
|
|
1038
|
-
...accessory.clusters, // Spread cluster states as initial values
|
|
1294
|
+
// Create endpoint options with cluster states
|
|
1295
|
+
const partEndpointOptions = {
|
|
1296
|
+
id: partEndpointId,
|
|
1297
|
+
...part.clusters,
|
|
1039
1298
|
};
|
|
1040
|
-
// Add bridgedDeviceBasicInformation
|
|
1041
|
-
// For external accessories, use the root basicInformation instead
|
|
1299
|
+
// Add bridgedDeviceBasicInformation for the part
|
|
1042
1300
|
if (!this.config.externalAccessory) {
|
|
1043
|
-
|
|
1301
|
+
partEndpointOptions.bridgedDeviceBasicInformation = {
|
|
1044
1302
|
vendorName: accessory.manufacturer,
|
|
1045
|
-
nodeLabel:
|
|
1303
|
+
nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
|
|
1046
1304
|
productName: accessory.model,
|
|
1047
|
-
productLabel:
|
|
1048
|
-
serialNumber: accessory.serialNumber
|
|
1305
|
+
productLabel: part.displayName || part.id,
|
|
1306
|
+
serialNumber: `${accessory.serialNumber}-${part.id}`,
|
|
1049
1307
|
reachable: true,
|
|
1050
1308
|
};
|
|
1051
1309
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
// Add to aggregator or serverNode depending on mode
|
|
1057
|
-
// (validation happens here - cluster states must already be set)
|
|
1310
|
+
// Create the part endpoint
|
|
1311
|
+
const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
|
|
1312
|
+
// Add part endpoint to aggregator or serverNode
|
|
1058
1313
|
if (this.config.externalAccessory) {
|
|
1059
|
-
|
|
1060
|
-
await this.serverNode.add(endpoint);
|
|
1061
|
-
log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
|
|
1314
|
+
await this.serverNode.add(partEndpoint);
|
|
1062
1315
|
}
|
|
1063
1316
|
else {
|
|
1064
|
-
|
|
1065
|
-
await this.aggregator.add(endpoint);
|
|
1066
|
-
if (this.config.debugModeEnabled) {
|
|
1067
|
-
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
1068
|
-
}
|
|
1317
|
+
await this.aggregator.add(partEndpoint);
|
|
1069
1318
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
1075
|
-
log.info(` Processing cluster: ${clusterName}`);
|
|
1319
|
+
log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
|
|
1320
|
+
// Set up handlers for this part
|
|
1321
|
+
if (part.handlers) {
|
|
1322
|
+
for (const [clusterName, handlers] of Object.entries(part.handlers)) {
|
|
1076
1323
|
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1077
|
-
|
|
1324
|
+
// Register handler with the part's endpoint ID
|
|
1325
|
+
this.behaviorRegistry.registerHandler(partEndpointId, clusterName, commandName, handler);
|
|
1078
1326
|
}
|
|
1079
1327
|
}
|
|
1328
|
+
log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
|
|
1080
1329
|
}
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
// Create unique endpoint ID for this part
|
|
1087
|
-
const partEndpointId = `${accessory.uuid}-part-${part.id}`;
|
|
1088
|
-
// Register the part endpoint mapping for handler context
|
|
1089
|
-
registerPartEndpoint(partEndpointId, accessory.uuid, part.id);
|
|
1090
|
-
// Apply custom behaviors to part based on its handlers (same logic as main accessory)
|
|
1091
|
-
let partDeviceType = part.deviceType;
|
|
1092
|
-
const partCustomBehaviors = [];
|
|
1093
|
-
if (part.handlers) {
|
|
1094
|
-
// Map cluster names to custom behavior classes for parts
|
|
1095
|
-
const partBehaviorMap = {
|
|
1096
|
-
onOff: HomebridgeOnOffServer,
|
|
1097
|
-
levelControl: HomebridgeLevelControlServer,
|
|
1098
|
-
colorControl: HomebridgeColorControlServer,
|
|
1099
|
-
windowCovering: HomebridgeWindowCoveringServer,
|
|
1100
|
-
doorLock: HomebridgeDoorLockServer,
|
|
1101
|
-
fanControl: HomebridgeFanControlServer,
|
|
1102
|
-
thermostat: HomebridgeThermostatServer,
|
|
1103
|
-
identify: HomebridgeIdentifyServer,
|
|
1104
|
-
};
|
|
1105
|
-
for (const clusterName of Object.keys(part.handlers)) {
|
|
1106
|
-
const behaviorClass = partBehaviorMap[clusterName];
|
|
1107
|
-
if (behaviorClass) {
|
|
1108
|
-
partCustomBehaviors.push(behaviorClass);
|
|
1109
|
-
log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
|
|
1110
|
-
}
|
|
1111
|
-
else {
|
|
1112
|
-
log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
if (partCustomBehaviors.length > 0) {
|
|
1116
|
-
// Matter.js device types support .with() to add behaviors
|
|
1117
|
-
partDeviceType = partDeviceType.with(...partCustomBehaviors);
|
|
1118
|
-
log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
// Add BridgedDeviceBasicInformationServer for bridged parts
|
|
1122
|
-
if (!this.config.externalAccessory) {
|
|
1123
|
-
// Matter.js device types support .with() to add behaviors
|
|
1124
|
-
partDeviceType = partDeviceType.with(BridgedDeviceBasicInformationServer);
|
|
1125
|
-
}
|
|
1126
|
-
// Create endpoint options with cluster states
|
|
1127
|
-
const partEndpointOptions = {
|
|
1128
|
-
id: partEndpointId,
|
|
1129
|
-
...part.clusters,
|
|
1130
|
-
};
|
|
1131
|
-
// Add bridgedDeviceBasicInformation for the part
|
|
1132
|
-
if (!this.config.externalAccessory) {
|
|
1133
|
-
partEndpointOptions.bridgedDeviceBasicInformation = {
|
|
1134
|
-
vendorName: accessory.manufacturer,
|
|
1135
|
-
nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
|
|
1136
|
-
productName: accessory.model,
|
|
1137
|
-
productLabel: part.displayName || part.id,
|
|
1138
|
-
serialNumber: `${accessory.serialNumber}-${part.id}`,
|
|
1139
|
-
reachable: true,
|
|
1140
|
-
};
|
|
1141
|
-
}
|
|
1142
|
-
// Create the part endpoint
|
|
1143
|
-
const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
|
|
1144
|
-
// Add part endpoint to aggregator or serverNode
|
|
1145
|
-
if (this.config.externalAccessory) {
|
|
1146
|
-
await this.serverNode.add(partEndpoint);
|
|
1147
|
-
}
|
|
1148
|
-
else {
|
|
1149
|
-
await this.aggregator.add(partEndpoint);
|
|
1150
|
-
}
|
|
1151
|
-
log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
|
|
1152
|
-
// Set up handlers for this part
|
|
1153
|
-
if (part.handlers) {
|
|
1154
|
-
for (const [clusterName, handlers] of Object.entries(part.handlers)) {
|
|
1155
|
-
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
1156
|
-
// Register handler with the part's endpoint ID
|
|
1157
|
-
registerHandler(partEndpointId, clusterName, commandName, handler);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
|
|
1161
|
-
}
|
|
1162
|
-
// Store the internal part
|
|
1163
|
-
internalParts.push({
|
|
1164
|
-
...part,
|
|
1165
|
-
endpoint: partEndpoint,
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
// Store accessory with internal metadata and event emitter
|
|
1170
|
-
// The event emitter allows plugins to listen for lifecycle events (ready, commissioned, decommissioned)
|
|
1171
|
-
const internalAccessory = {
|
|
1172
|
-
...accessory,
|
|
1173
|
-
_associatedPlugin: pluginIdentifier,
|
|
1174
|
-
_associatedPlatform: platformName,
|
|
1175
|
-
endpoint,
|
|
1176
|
-
registered: true,
|
|
1177
|
-
_parts: internalParts.length > 0 ? internalParts : undefined,
|
|
1178
|
-
_eventEmitter: new EventEmitter(),
|
|
1179
|
-
};
|
|
1180
|
-
this.accessories.set(accessory.uuid, internalAccessory);
|
|
1181
|
-
log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
|
|
1182
|
-
if (this.config.debugModeEnabled) {
|
|
1183
|
-
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
1184
|
-
}
|
|
1185
|
-
// Emit accessory-registered event
|
|
1186
|
-
this.emit('accessory-registered', accessory);
|
|
1187
|
-
// Notify controllers about the new device (parts list changed)
|
|
1188
|
-
// This allows the Home app to discover new devices without re-pairing
|
|
1189
|
-
await this.notifyPartsListChanged();
|
|
1190
|
-
// Save to cache asynchronously (don't block registration)
|
|
1191
|
-
if (this.accessoryCache) {
|
|
1192
|
-
this.accessoryCache.save(this.accessories).catch((error) => {
|
|
1193
|
-
log.warn('Failed to save accessory cache:', error);
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1330
|
+
// Store the internal part
|
|
1331
|
+
internalParts.push({
|
|
1332
|
+
...part,
|
|
1333
|
+
endpoint: partEndpoint,
|
|
1334
|
+
});
|
|
1196
1335
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1336
|
+
return internalParts;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Finalize accessory registration (store, emit events, save cache)
|
|
1340
|
+
*/
|
|
1341
|
+
async finalizeAccessoryRegistration(accessory, endpoint, internalParts) {
|
|
1342
|
+
// Store accessory with internal metadata and event emitter
|
|
1343
|
+
// The event emitter allows plugins to listen for lifecycle events (ready, commissioned, decommissioned)
|
|
1344
|
+
// Note: _associatedPlugin and _associatedPlatform are already set by MatterAPIImpl
|
|
1345
|
+
const internalAccessory = {
|
|
1346
|
+
...accessory,
|
|
1347
|
+
endpoint,
|
|
1348
|
+
registered: true,
|
|
1349
|
+
_parts: internalParts.length > 0 ? internalParts : undefined,
|
|
1350
|
+
_eventEmitter: new EventEmitter(),
|
|
1351
|
+
};
|
|
1352
|
+
this.accessories.set(accessory.uuid, internalAccessory);
|
|
1353
|
+
log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.uuid})`);
|
|
1354
|
+
if (this.config.debugModeEnabled) {
|
|
1355
|
+
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
1356
|
+
}
|
|
1357
|
+
// Emit accessory-registered event
|
|
1358
|
+
this.emit('accessory-registered', accessory);
|
|
1359
|
+
// Notify controllers about the new device (parts list changed)
|
|
1360
|
+
// This allows the Home app to discover new devices without re-pairing
|
|
1361
|
+
await this.notifyPartsListChanged();
|
|
1362
|
+
// Request debounced save to cache (reduces disk I/O during rapid registration)
|
|
1363
|
+
if (this.accessoryCache) {
|
|
1364
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
1200
1365
|
}
|
|
1201
1366
|
}
|
|
1202
1367
|
/**
|
|
@@ -1211,9 +1376,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1211
1376
|
if (this.accessoryCache && this.accessoryCache.getCached(uuid)) {
|
|
1212
1377
|
log.debug(`Removing ${uuid} from cache`);
|
|
1213
1378
|
this.accessoryCache.removeCached(uuid);
|
|
1214
|
-
this.accessoryCache.
|
|
1215
|
-
log.warn('Failed to save accessory cache:', error);
|
|
1216
|
-
});
|
|
1379
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
1217
1380
|
}
|
|
1218
1381
|
return;
|
|
1219
1382
|
}
|
|
@@ -1231,9 +1394,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1231
1394
|
// Update cache (remove the accessory)
|
|
1232
1395
|
if (this.accessoryCache) {
|
|
1233
1396
|
this.accessoryCache.removeCached(uuid);
|
|
1234
|
-
this.accessoryCache.
|
|
1235
|
-
log.warn('Failed to save accessory cache:', error);
|
|
1236
|
-
});
|
|
1397
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
1237
1398
|
}
|
|
1238
1399
|
}
|
|
1239
1400
|
catch (error) {
|
|
@@ -1281,10 +1442,10 @@ export class MatterServer extends EventEmitter {
|
|
|
1281
1442
|
displayName = accessory.displayName;
|
|
1282
1443
|
}
|
|
1283
1444
|
// Defer the update to avoid "read-only transaction" errors when called from handlers
|
|
1284
|
-
// Matter.js uses transactions, and we need to
|
|
1285
|
-
//
|
|
1445
|
+
// Matter.js uses transactions, and we need to escape the current call stack
|
|
1446
|
+
// setImmediate ensures we're in a new event loop tick without arbitrary delays
|
|
1286
1447
|
return new Promise((resolve, reject) => {
|
|
1287
|
-
|
|
1448
|
+
setImmediate(async () => {
|
|
1288
1449
|
try {
|
|
1289
1450
|
// Construct the update object
|
|
1290
1451
|
const updateObject = { [cluster]: attributes };
|
|
@@ -1313,7 +1474,7 @@ export class MatterServer extends EventEmitter {
|
|
|
1313
1474
|
log.error(`Failed to update state for accessory ${uuid}${partInfo}:`, error);
|
|
1314
1475
|
reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
|
|
1315
1476
|
}
|
|
1316
|
-
}
|
|
1477
|
+
});
|
|
1317
1478
|
});
|
|
1318
1479
|
}
|
|
1319
1480
|
/**
|
|
@@ -1452,7 +1613,6 @@ export class MatterServer extends EventEmitter {
|
|
|
1452
1613
|
this.isRunning = false;
|
|
1453
1614
|
// Stop monitoring
|
|
1454
1615
|
this.stopFabricMonitoring();
|
|
1455
|
-
networkMonitor.stopMonitoring();
|
|
1456
1616
|
try {
|
|
1457
1617
|
// Save accessory cache before shutting down (BEFORE clearing accessories!)
|
|
1458
1618
|
if (this.accessoryCache && this.accessories.size > 0) {
|
|
@@ -1508,6 +1668,23 @@ export class MatterServer extends EventEmitter {
|
|
|
1508
1668
|
}
|
|
1509
1669
|
/**
|
|
1510
1670
|
* Get fabric information for commissioned controllers
|
|
1671
|
+
*
|
|
1672
|
+
* Returns information about each paired controller (fabric) including:
|
|
1673
|
+
* - fabricIndex: Unique identifier for the fabric
|
|
1674
|
+
* - fabricId: 64-bit fabric identifier
|
|
1675
|
+
* - nodeId: Node identifier within the fabric
|
|
1676
|
+
* - rootVendorId: Vendor ID of the root node
|
|
1677
|
+
* - label: Optional human-readable label
|
|
1678
|
+
*
|
|
1679
|
+
* @returns Array of fabric information objects, or empty array if no fabrics are commissioned
|
|
1680
|
+
* @example
|
|
1681
|
+
* ```typescript
|
|
1682
|
+
* const fabrics = matterServer.getFabricInfo()
|
|
1683
|
+
* console.log(`Commissioned to ${fabrics.length} controller(s)`)
|
|
1684
|
+
* fabrics.forEach(fabric => {
|
|
1685
|
+
* console.log(` Fabric ${fabric.fabricIndex}: ${fabric.label || 'Unnamed'}`)
|
|
1686
|
+
* })
|
|
1687
|
+
* ```
|
|
1511
1688
|
*/
|
|
1512
1689
|
getFabricInfo() {
|
|
1513
1690
|
try {
|
|
@@ -1641,8 +1818,11 @@ export class MatterServer extends EventEmitter {
|
|
|
1641
1818
|
// The fabric monitoring will detect this change and emit the appropriate events
|
|
1642
1819
|
}
|
|
1643
1820
|
catch (error) {
|
|
1821
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1644
1822
|
log.error(`Failed to remove fabric ${fabricIndex}:`, error);
|
|
1645
|
-
throw new MatterDeviceError(`Failed to remove fabric: ${
|
|
1823
|
+
throw new MatterDeviceError(`Failed to remove fabric: ${errorMessage}`, {
|
|
1824
|
+
originalError: error instanceof Error ? error : undefined,
|
|
1825
|
+
});
|
|
1646
1826
|
}
|
|
1647
1827
|
}
|
|
1648
1828
|
/**
|
|
@@ -1685,8 +1865,9 @@ export class MatterServer extends EventEmitter {
|
|
|
1685
1865
|
}
|
|
1686
1866
|
catch (error) {
|
|
1687
1867
|
// Non-fatal error - log but don't throw
|
|
1688
|
-
|
|
1868
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1869
|
+
log.warn(`Failed to notify controllers of parts list change: ${errorMessage}`);
|
|
1689
1870
|
}
|
|
1690
1871
|
}
|
|
1691
1872
|
}
|
|
1692
|
-
//# sourceMappingURL=
|
|
1873
|
+
//# sourceMappingURL=server.js.map
|