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