homebridge 2.0.0-beta.6 → 2.0.0-beta.60
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/README.md +16 -17
- package/bin/homebridge.js +22 -0
- package/config-sample.json +3 -3
- package/dist/api.d.ts +136 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +151 -0
- package/dist/api.js.map +1 -0
- package/{lib → dist}/bridgeService.d.ts +16 -32
- package/dist/bridgeService.d.ts.map +1 -0
- package/dist/bridgeService.js +341 -0
- package/dist/bridgeService.js.map +1 -0
- package/dist/childBridgeFork.d.ts +40 -0
- package/dist/childBridgeFork.d.ts.map +1 -0
- package/dist/childBridgeFork.js +354 -0
- package/dist/childBridgeFork.js.map +1 -0
- package/dist/childBridgeService.d.ts +146 -0
- package/dist/childBridgeService.d.ts.map +1 -0
- package/dist/childBridgeService.js +416 -0
- package/dist/childBridgeService.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +88 -0
- package/dist/cli.js.map +1 -0
- package/dist/externalPortService.d.ts +26 -0
- package/dist/externalPortService.d.ts.map +1 -0
- package/dist/externalPortService.js +77 -0
- package/dist/externalPortService.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/ipcService.d.ts +58 -0
- package/dist/ipcService.d.ts.map +1 -0
- package/dist/ipcService.js +43 -0
- package/dist/ipcService.js.map +1 -0
- package/dist/logger.d.ts +38 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +102 -0
- package/dist/logger.js.map +1 -0
- package/dist/matter/BaseMatterManager.d.ts +21 -0
- package/dist/matter/BaseMatterManager.d.ts.map +1 -0
- package/dist/matter/BaseMatterManager.js +197 -0
- package/dist/matter/BaseMatterManager.js.map +1 -0
- package/dist/matter/ChildBridgeMatterManager.d.ts +35 -0
- package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -0
- package/dist/matter/ChildBridgeMatterManager.js +222 -0
- package/dist/matter/ChildBridgeMatterManager.js.map +1 -0
- package/dist/matter/ClusterCommandMapper.d.ts +5 -0
- package/dist/matter/ClusterCommandMapper.d.ts.map +1 -0
- package/dist/matter/ClusterCommandMapper.js +222 -0
- package/dist/matter/ClusterCommandMapper.js.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts +22 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.js +49 -0
- package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -0
- package/dist/matter/MatterAPIImpl.d.ts +473 -0
- package/dist/matter/MatterAPIImpl.d.ts.map +1 -0
- package/dist/matter/MatterAPIImpl.js +210 -0
- package/dist/matter/MatterAPIImpl.js.map +1 -0
- package/dist/matter/MatterBridgeManager.d.ts +39 -0
- package/dist/matter/MatterBridgeManager.d.ts.map +1 -0
- package/dist/matter/MatterBridgeManager.js +323 -0
- package/dist/matter/MatterBridgeManager.js.map +1 -0
- package/dist/matter/accessoryCache.d.ts +57 -0
- package/dist/matter/accessoryCache.d.ts.map +1 -0
- package/dist/matter/accessoryCache.js +155 -0
- package/dist/matter/accessoryCache.js.map +1 -0
- package/dist/matter/behaviors/AirQualityBehavior.d.ts +8 -0
- package/dist/matter/behaviors/AirQualityBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/AirQualityBehavior.js +11 -0
- package/dist/matter/behaviors/AirQualityBehavior.js.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.d.ts +27 -0
- package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -0
- package/dist/matter/behaviors/BehaviorRegistry.js +110 -0
- package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.d.ts +12 -0
- package/dist/matter/behaviors/ColorControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ColorControlBehavior.js +139 -0
- package/dist/matter/behaviors/ColorControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.d.ts +32 -0
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.js +51 -0
- package/dist/matter/behaviors/ConcentrationMeasurementBehavior.js.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.d.ts +7 -0
- package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/DoorLockBehavior.js +48 -0
- package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.d.ts +7 -0
- package/dist/matter/behaviors/FanControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/FanControlBehavior.js +57 -0
- package/dist/matter/behaviors/FanControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.d.ts +7 -0
- package/dist/matter/behaviors/IdentifyBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/IdentifyBehavior.js +25 -0
- package/dist/matter/behaviors/IdentifyBehavior.js.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.d.ts +11 -0
- package/dist/matter/behaviors/LevelControlBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/LevelControlBehavior.js +100 -0
- package/dist/matter/behaviors/LevelControlBehavior.js.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.d.ts +8 -0
- package/dist/matter/behaviors/OnOffBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/OnOffBehavior.js +60 -0
- package/dist/matter/behaviors/OnOffBehavior.js.map +1 -0
- package/dist/matter/behaviors/RegistryManager.d.ts +15 -0
- package/dist/matter/behaviors/RegistryManager.d.ts.map +1 -0
- package/dist/matter/behaviors/RegistryManager.js +31 -0
- package/dist/matter/behaviors/RegistryManager.js.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts +14 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.js +29 -0
- package/dist/matter/behaviors/RvcCleanModeBehavior.js.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts +10 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js +81 -0
- package/dist/matter/behaviors/RvcOperationalStateBehavior.js.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts +14 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.js +29 -0
- package/dist/matter/behaviors/RvcRunModeBehavior.js.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts +9 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.js +62 -0
- package/dist/matter/behaviors/ServiceAreaBehavior.js.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.d.ts +9 -0
- package/dist/matter/behaviors/ThermostatBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/ThermostatBehavior.js +92 -0
- package/dist/matter/behaviors/ThermostatBehavior.js.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts +12 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.d.ts.map +1 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.js +108 -0
- package/dist/matter/behaviors/WindowCoveringBehavior.js.map +1 -0
- package/dist/matter/behaviors/index.d.ts +18 -0
- package/dist/matter/behaviors/index.d.ts.map +1 -0
- package/dist/matter/behaviors/index.js +17 -0
- package/dist/matter/behaviors/index.js.map +1 -0
- package/dist/matter/clusterTypes.d.ts +182 -0
- package/dist/matter/clusterTypes.d.ts.map +1 -0
- package/dist/matter/clusterTypes.js +2 -0
- package/dist/matter/clusterTypes.js.map +1 -0
- package/dist/matter/config.d.ts +9 -0
- package/dist/matter/config.d.ts.map +1 -0
- package/dist/matter/config.js +84 -0
- package/dist/matter/config.js.map +1 -0
- package/dist/matter/configValidator.d.ts +26 -0
- package/dist/matter/configValidator.d.ts.map +1 -0
- package/dist/matter/configValidator.js +173 -0
- package/dist/matter/configValidator.js.map +1 -0
- package/dist/matter/errorHandler.d.ts +11 -0
- package/dist/matter/errorHandler.d.ts.map +1 -0
- package/dist/matter/errorHandler.js +81 -0
- package/dist/matter/errorHandler.js.map +1 -0
- package/dist/matter/errors.d.ts +46 -0
- package/dist/matter/errors.d.ts.map +1 -0
- package/dist/matter/errors.js +68 -0
- package/dist/matter/errors.js.map +1 -0
- package/dist/matter/index.d.ts +97 -0
- package/dist/matter/index.d.ts.map +1 -0
- package/dist/matter/index.js +10 -0
- package/dist/matter/index.js.map +1 -0
- package/dist/matter/logFormatter.d.ts +2 -0
- package/dist/matter/logFormatter.d.ts.map +1 -0
- package/dist/matter/logFormatter.js +100 -0
- package/dist/matter/logFormatter.js.map +1 -0
- package/dist/matter/managerTypes.d.ts +57 -0
- package/dist/matter/managerTypes.d.ts.map +1 -0
- package/dist/matter/managerTypes.js +2 -0
- package/dist/matter/managerTypes.js.map +1 -0
- package/dist/matter/server.d.ts +112 -0
- package/dist/matter/server.d.ts.map +1 -0
- package/dist/matter/server.js +1508 -0
- package/dist/matter/server.js.map +1 -0
- package/dist/matter/serverHelpers.d.ts +31 -0
- package/dist/matter/serverHelpers.d.ts.map +1 -0
- package/dist/matter/serverHelpers.js +273 -0
- package/dist/matter/serverHelpers.js.map +1 -0
- package/dist/matter/sharedTypes.d.ts +93 -0
- package/dist/matter/sharedTypes.d.ts.map +1 -0
- package/dist/matter/sharedTypes.js +20 -0
- package/dist/matter/sharedTypes.js.map +1 -0
- package/dist/matter/storage.d.ts +45 -0
- package/dist/matter/storage.d.ts.map +1 -0
- package/dist/matter/storage.js +325 -0
- package/dist/matter/storage.js.map +1 -0
- package/dist/matter/typeHelpers.d.ts +15 -0
- package/dist/matter/typeHelpers.d.ts.map +1 -0
- package/dist/matter/typeHelpers.js +33 -0
- package/dist/matter/typeHelpers.js.map +1 -0
- package/dist/matter/types.d.ts +642 -0
- package/dist/matter/types.d.ts.map +1 -0
- package/dist/matter/types.js +208 -0
- package/dist/matter/types.js.map +1 -0
- package/dist/matter/utils.d.ts +13 -0
- package/dist/matter/utils.d.ts.map +1 -0
- package/dist/matter/utils.js +21 -0
- package/dist/matter/utils.js.map +1 -0
- package/{lib → dist}/platformAccessory.d.ts +8 -21
- package/dist/platformAccessory.d.ts.map +1 -0
- package/dist/platformAccessory.js +79 -0
- package/dist/platformAccessory.js.map +1 -0
- package/{lib → dist}/plugin.d.ts +2 -6
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +159 -0
- package/dist/plugin.js.map +1 -0
- package/{lib → dist}/pluginManager.d.ts +3 -25
- package/dist/pluginManager.d.ts.map +1 -0
- package/{lib → dist}/pluginManager.js +87 -115
- package/dist/pluginManager.js.map +1 -0
- package/{lib → dist}/server.d.ts +15 -20
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +727 -0
- package/dist/server.js.map +1 -0
- package/{lib → dist}/storageService.d.ts.map +1 -1
- package/dist/storageService.js +41 -0
- package/dist/storageService.js.map +1 -0
- package/{lib → dist}/user.d.ts +1 -3
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +29 -0
- package/dist/user.js.map +1 -0
- package/{lib → dist}/util/mac.d.ts +1 -0
- package/dist/util/mac.d.ts.map +1 -0
- package/dist/util/mac.js +13 -0
- package/dist/util/mac.js.map +1 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -50
- package/bin/homebridge +0 -17
- package/lib/api.d.ts +0 -210
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -155
- package/lib/api.js.map +0 -1
- package/lib/bridgeService.d.ts.map +0 -1
- package/lib/bridgeService.js +0 -426
- package/lib/bridgeService.js.map +0 -1
- package/lib/childBridgeFork.d.ts +0 -37
- package/lib/childBridgeFork.d.ts.map +0 -1
- package/lib/childBridgeFork.js +0 -244
- package/lib/childBridgeFork.js.map +0 -1
- package/lib/childBridgeService.d.ts +0 -199
- package/lib/childBridgeService.d.ts.map +0 -1
- package/lib/childBridgeService.js +0 -428
- package/lib/childBridgeService.js.map +0 -1
- package/lib/cli.d.ts +0 -4
- package/lib/cli.d.ts.map +0 -1
- package/lib/cli.js +0 -111
- package/lib/cli.js.map +0 -1
- package/lib/externalPortService.d.ts +0 -33
- package/lib/externalPortService.d.ts.map +0 -1
- package/lib/externalPortService.js +0 -64
- package/lib/externalPortService.js.map +0 -1
- package/lib/index.d.ts +0 -76
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -72
- package/lib/index.js.map +0 -1
- package/lib/ipcService.d.ts +0 -33
- package/lib/ipcService.d.ts.map +0 -1
- package/lib/ipcService.js +0 -49
- package/lib/ipcService.js.map +0 -1
- package/lib/logger.d.ts +0 -78
- package/lib/logger.d.ts.map +0 -1
- package/lib/logger.js +0 -147
- package/lib/logger.js.map +0 -1
- package/lib/platformAccessory.d.ts.map +0 -1
- package/lib/platformAccessory.js +0 -102
- package/lib/platformAccessory.js.map +0 -1
- package/lib/plugin.d.ts.map +0 -1
- package/lib/plugin.js +0 -194
- package/lib/plugin.js.map +0 -1
- package/lib/pluginManager.d.ts.map +0 -1
- package/lib/pluginManager.js.map +0 -1
- package/lib/server.d.ts.map +0 -1
- package/lib/server.js +0 -457
- package/lib/server.js.map +0 -1
- package/lib/storageService.js +0 -70
- package/lib/storageService.js.map +0 -1
- package/lib/user.d.ts.map +0 -1
- package/lib/user.js +0 -36
- package/lib/user.js.map +0 -1
- package/lib/util/mac.d.ts.map +0 -1
- package/lib/util/mac.js +0 -20
- package/lib/util/mac.js.map +0 -1
- package/lib/version.d.ts.map +0 -1
- package/lib/version.js +0 -21
- package/lib/version.js.map +0 -1
- /package/{lib → dist}/storageService.d.ts +0 -0
- /package/{lib → dist}/version.d.ts +0 -0
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { access, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir, release } from 'node:os';
|
|
6
|
+
import { join, normalize, resolve } from 'node:path';
|
|
7
|
+
import process from 'node:process';
|
|
8
|
+
import { Endpoint, Environment, Logger as MatterLogger, LogLevel as MatterLogLevel, ServerNode, StorageService, VendorId, } from '@matter/main';
|
|
9
|
+
import { BridgedDeviceBasicInformationServer } from '@matter/main/behaviors';
|
|
10
|
+
import * as clusters from '@matter/main/clusters';
|
|
11
|
+
import * as devices from '@matter/main/devices';
|
|
12
|
+
import { AggregatorEndpoint } from '@matter/main/endpoints';
|
|
13
|
+
import { PowerSourceServer } from '@matter/node/behaviors';
|
|
14
|
+
import { ManualPairingCodeCodec, QrPairingCodeCodec } from '@matter/types/schema';
|
|
15
|
+
import fse from 'fs-extra';
|
|
16
|
+
import QRCode from 'qrcode-terminal';
|
|
17
|
+
import { Logger } from '../logger.js';
|
|
18
|
+
import getVersion from '../version.js';
|
|
19
|
+
import { MatterAccessoryCache } from './accessoryCache.js';
|
|
20
|
+
import { BehaviorRegistry } from './behaviors/BehaviorRegistry.js';
|
|
21
|
+
import { HomebridgeColorControlServer } from './behaviors/ColorControlBehavior.js';
|
|
22
|
+
import { HomebridgeDoorLockServer } from './behaviors/DoorLockBehavior.js';
|
|
23
|
+
import { HomebridgeFanControlServer } from './behaviors/FanControlBehavior.js';
|
|
24
|
+
import { HomebridgeIdentifyServer } from './behaviors/IdentifyBehavior.js';
|
|
25
|
+
import { HomebridgeLevelControlServer } from './behaviors/LevelControlBehavior.js';
|
|
26
|
+
import { HomebridgeOnOffServer } from './behaviors/OnOffBehavior.js';
|
|
27
|
+
import { RegistryManager } from './behaviors/RegistryManager.js';
|
|
28
|
+
import { HomebridgeRvcCleanModeServer } from './behaviors/RvcCleanModeBehavior.js';
|
|
29
|
+
import { HomebridgeRvcOperationalStateServer } from './behaviors/RvcOperationalStateBehavior.js';
|
|
30
|
+
import { HomebridgeRvcRunModeServer } from './behaviors/RvcRunModeBehavior.js';
|
|
31
|
+
import { HomebridgeServiceAreaServer } from './behaviors/ServiceAreaBehavior.js';
|
|
32
|
+
import { HomebridgeThermostatServer } from './behaviors/ThermostatBehavior.js';
|
|
33
|
+
import { HomebridgeWindowCoveringServer } from './behaviors/WindowCoveringBehavior.js';
|
|
34
|
+
import { sanitizeUniqueId, truncateString, validatePort } from './configValidator.js';
|
|
35
|
+
import { errorHandler } from './errorHandler.js';
|
|
36
|
+
import { createHomebridgeLogFormatter } from './logFormatter.js';
|
|
37
|
+
import { applyWindowCoveringFeatures, CLUSTER_IDS, detectBehaviorFeatures, detectWindowCoveringFeatures, determineColorControlFeaturesFromHandlers, extractColorControlFeatures, extractThermostatFeatures, validateAccessoryRequiredFields, } from './serverHelpers.js';
|
|
38
|
+
import { MatterStorageManager } from './storage.js';
|
|
39
|
+
import { isDeviceType, withBehaviors, withFeatures } from './typeHelpers.js';
|
|
40
|
+
import { deviceTypes, MatterDeviceError, } from './types.js';
|
|
41
|
+
const log = Logger.withPrefix('Matter/Server');
|
|
42
|
+
const DEFAULT_MATTER_PORT = 5540;
|
|
43
|
+
const DEFAULT_VENDOR_ID = 0xFFF1;
|
|
44
|
+
const DEFAULT_PRODUCT_ID = 0x8001;
|
|
45
|
+
const MAX_DEVICES_PER_BRIDGE = 1000;
|
|
46
|
+
const SERVER_READY_TIMEOUT_MS = 5000;
|
|
47
|
+
const SERVER_READY_POLL_INTERVAL_MS = 100;
|
|
48
|
+
const SERVER_INIT_DELAY_MS = 200;
|
|
49
|
+
const MAX_PASSCODE_ATTEMPTS = 100;
|
|
50
|
+
export class MatterServer extends EventEmitter {
|
|
51
|
+
config;
|
|
52
|
+
serverNode = null;
|
|
53
|
+
aggregator = null;
|
|
54
|
+
accessories = new Map();
|
|
55
|
+
behaviorRegistry;
|
|
56
|
+
isRunning = false;
|
|
57
|
+
MAX_DEVICES = MAX_DEVICES_PER_BRIDGE;
|
|
58
|
+
shutdownHandler = null;
|
|
59
|
+
static CLUSTER_BEHAVIOR_MAP = {
|
|
60
|
+
onOff: HomebridgeOnOffServer,
|
|
61
|
+
levelControl: HomebridgeLevelControlServer,
|
|
62
|
+
colorControl: HomebridgeColorControlServer,
|
|
63
|
+
windowCovering: HomebridgeWindowCoveringServer,
|
|
64
|
+
doorLock: HomebridgeDoorLockServer,
|
|
65
|
+
fanControl: HomebridgeFanControlServer,
|
|
66
|
+
thermostat: HomebridgeThermostatServer,
|
|
67
|
+
rvcOperationalState: HomebridgeRvcOperationalStateServer,
|
|
68
|
+
rvcRunMode: HomebridgeRvcRunModeServer,
|
|
69
|
+
rvcCleanMode: HomebridgeRvcCleanModeServer,
|
|
70
|
+
serviceArea: HomebridgeServiceAreaServer,
|
|
71
|
+
identify: HomebridgeIdentifyServer,
|
|
72
|
+
};
|
|
73
|
+
passcode = 0;
|
|
74
|
+
discriminator = 0;
|
|
75
|
+
vendorId;
|
|
76
|
+
productId;
|
|
77
|
+
commissioningInfo = {};
|
|
78
|
+
serialNumber;
|
|
79
|
+
cleanupHandlers = [];
|
|
80
|
+
storageManager = null;
|
|
81
|
+
matterStoragePath;
|
|
82
|
+
accessoryCache = null;
|
|
83
|
+
monitoringEnabled = false;
|
|
84
|
+
username;
|
|
85
|
+
bridgeName;
|
|
86
|
+
constructor(config) {
|
|
87
|
+
super();
|
|
88
|
+
this.config = this.validateAndSanitizeConfig(config);
|
|
89
|
+
const cleanId = this.config.uniqueId.replace(/[^A-F0-9]/gi, '');
|
|
90
|
+
this.username = cleanId.match(/.{1,2}/g)?.slice(0, 6).join(':').toUpperCase() || this.config.uniqueId;
|
|
91
|
+
this.bridgeName = this.config.serialNumber ? `Matter Bridge ${this.config.serialNumber}` : 'Matter Bridge';
|
|
92
|
+
if (this.config.debugModeEnabled) {
|
|
93
|
+
log.info('Matter debug mode enabled - verbose logging active');
|
|
94
|
+
MatterLogger.level = MatterLogLevel.DEBUG;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
MatterLogger.level = MatterLogLevel.NOTICE;
|
|
98
|
+
}
|
|
99
|
+
MatterLogger.format = createHomebridgeLogFormatter();
|
|
100
|
+
MatterLogger.destinations.default.write = (text) => {
|
|
101
|
+
if (text.trim() !== '') {
|
|
102
|
+
console.log(text);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
this.vendorId = DEFAULT_VENDOR_ID;
|
|
106
|
+
this.productId = DEFAULT_PRODUCT_ID;
|
|
107
|
+
this.behaviorRegistry = new BehaviorRegistry(this.accessories, this);
|
|
108
|
+
}
|
|
109
|
+
validateAndSanitizeConfig(config) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
const port = config.port || DEFAULT_MATTER_PORT;
|
|
112
|
+
const portValidation = validatePort(port, false);
|
|
113
|
+
if (!portValidation.valid) {
|
|
114
|
+
errors.push(`Invalid port: ${portValidation.error}`);
|
|
115
|
+
}
|
|
116
|
+
if (!config.uniqueId) {
|
|
117
|
+
errors.push('uniqueId is required for Matter server configuration');
|
|
118
|
+
}
|
|
119
|
+
const rawUniqueId = config.uniqueId || '';
|
|
120
|
+
const uniqueIdResult = sanitizeUniqueId(rawUniqueId);
|
|
121
|
+
const uniqueId = uniqueIdResult.value;
|
|
122
|
+
if (uniqueId.length === 0) {
|
|
123
|
+
errors.push('Invalid uniqueId: must be a non-empty string');
|
|
124
|
+
}
|
|
125
|
+
let storagePath = config.storagePath;
|
|
126
|
+
if (storagePath !== undefined) {
|
|
127
|
+
storagePath = resolve(storagePath);
|
|
128
|
+
}
|
|
129
|
+
let manufacturer = config.manufacturer;
|
|
130
|
+
if (manufacturer !== undefined) {
|
|
131
|
+
manufacturer = truncateString(manufacturer, 32, 'Manufacturer name').value;
|
|
132
|
+
}
|
|
133
|
+
let model = config.model;
|
|
134
|
+
if (model !== undefined) {
|
|
135
|
+
model = truncateString(model, 32, 'Model name').value;
|
|
136
|
+
}
|
|
137
|
+
let firmwareRevision = config.firmwareRevision;
|
|
138
|
+
if (firmwareRevision !== undefined) {
|
|
139
|
+
firmwareRevision = truncateString(firmwareRevision, 64, 'Firmware revision').value;
|
|
140
|
+
}
|
|
141
|
+
let serialNumber = config.serialNumber;
|
|
142
|
+
if (serialNumber !== undefined) {
|
|
143
|
+
serialNumber = truncateString(serialNumber, 32, 'Serial number').value;
|
|
144
|
+
}
|
|
145
|
+
const debugModeEnabled = config.debugModeEnabled || false;
|
|
146
|
+
const externalAccessory = config.externalAccessory || false;
|
|
147
|
+
if (errors.length > 0) {
|
|
148
|
+
throw new MatterDeviceError(`Matter configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
port,
|
|
152
|
+
uniqueId,
|
|
153
|
+
storagePath,
|
|
154
|
+
manufacturer,
|
|
155
|
+
model,
|
|
156
|
+
firmwareRevision,
|
|
157
|
+
serialNumber,
|
|
158
|
+
debugModeEnabled,
|
|
159
|
+
externalAccessory,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
generateSecurePasscode() {
|
|
163
|
+
let passcode;
|
|
164
|
+
const maxAttempts = MAX_PASSCODE_ATTEMPTS;
|
|
165
|
+
let attempts = 0;
|
|
166
|
+
const invalidPasscodes = [
|
|
167
|
+
0,
|
|
168
|
+
11111111,
|
|
169
|
+
22222222,
|
|
170
|
+
33333333,
|
|
171
|
+
44444444,
|
|
172
|
+
55555555,
|
|
173
|
+
66666666,
|
|
174
|
+
77777777,
|
|
175
|
+
88888888,
|
|
176
|
+
99999999,
|
|
177
|
+
12345678,
|
|
178
|
+
87654321,
|
|
179
|
+
];
|
|
180
|
+
do {
|
|
181
|
+
const randomValue = randomBytes(4).readUInt32BE(0);
|
|
182
|
+
passcode = (randomValue % 99999998) + 1;
|
|
183
|
+
attempts++;
|
|
184
|
+
if (attempts > maxAttempts) {
|
|
185
|
+
throw new Error('Failed to generate secure passcode after maximum attempts');
|
|
186
|
+
}
|
|
187
|
+
} while (invalidPasscodes.includes(passcode)
|
|
188
|
+
|| !this.isValidPasscode(passcode));
|
|
189
|
+
return passcode;
|
|
190
|
+
}
|
|
191
|
+
isValidPasscode(passcode) {
|
|
192
|
+
if (passcode < 1 || passcode > 99999998) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
const passcodeStr = passcode.toString().padStart(8, '0');
|
|
196
|
+
let isSequential = true;
|
|
197
|
+
for (let i = 1; i < passcodeStr.length; i++) {
|
|
198
|
+
if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) + 1) {
|
|
199
|
+
isSequential = false;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (isSequential) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
let isReverseSequential = true;
|
|
207
|
+
for (let i = 1; i < passcodeStr.length; i++) {
|
|
208
|
+
if (Number.parseInt(passcodeStr[i]) !== Number.parseInt(passcodeStr[i - 1]) - 1) {
|
|
209
|
+
isReverseSequential = false;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (isReverseSequential) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const digitCounts = new Map();
|
|
217
|
+
for (const digit of passcodeStr) {
|
|
218
|
+
digitCounts.set(digit, (digitCounts.get(digit) || 0) + 1);
|
|
219
|
+
const count = digitCounts.get(digit);
|
|
220
|
+
if (count !== undefined && count > 3) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
generateRandomDiscriminator() {
|
|
227
|
+
const discriminator = randomBytes(2).readUInt16BE(0) & 0x0FFF;
|
|
228
|
+
if (discriminator < 0 || discriminator > 4095) {
|
|
229
|
+
throw new Error(`Invalid discriminator generated: ${discriminator}`);
|
|
230
|
+
}
|
|
231
|
+
return discriminator;
|
|
232
|
+
}
|
|
233
|
+
async createServerNodeWithRecovery(nodeOptions, sanitizedId) {
|
|
234
|
+
try {
|
|
235
|
+
return await ServerNode.create(nodeOptions);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const errorMessage = error instanceof Error ? error.message : '';
|
|
239
|
+
const causeMessage = error instanceof Error && error.cause instanceof Error ? error.cause.message : '';
|
|
240
|
+
const isStorageError = errorMessage.includes('Invalid public key encoding')
|
|
241
|
+
|| errorMessage.includes('FabricManager unavailable')
|
|
242
|
+
|| errorMessage.includes('key-input')
|
|
243
|
+
|| causeMessage.includes('Invalid public key encoding');
|
|
244
|
+
if (!isStorageError) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
log.warn('Detected corrupted Matter storage, attempting automatic recovery...');
|
|
248
|
+
const environment = Environment.default;
|
|
249
|
+
const storageService = environment.get(StorageService);
|
|
250
|
+
const storageLocation = storageService.location;
|
|
251
|
+
if (!storageLocation) {
|
|
252
|
+
throw new Error('Storage location not set, cannot recover from corrupted storage');
|
|
253
|
+
}
|
|
254
|
+
const serverNodeStorePath = join(storageLocation, sanitizedId);
|
|
255
|
+
const serverNodeStoreJsonFile = `${serverNodeStorePath}.json`;
|
|
256
|
+
try {
|
|
257
|
+
let removedSomething = false;
|
|
258
|
+
try {
|
|
259
|
+
await fse.stat(serverNodeStorePath);
|
|
260
|
+
log.info(`Removing corrupted ServerNodeStore directory: ${serverNodeStorePath}`);
|
|
261
|
+
await fse.remove(serverNodeStorePath);
|
|
262
|
+
removedSomething = true;
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
266
|
+
if (code !== 'ENOENT') {
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
await fse.stat(serverNodeStoreJsonFile);
|
|
272
|
+
log.info(`Removing corrupted ServerNodeStore JSON file: ${serverNodeStoreJsonFile}`);
|
|
273
|
+
await fse.remove(serverNodeStoreJsonFile);
|
|
274
|
+
removedSomething = true;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|
|
278
|
+
if (code !== 'ENOENT') {
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (removedSomething) {
|
|
283
|
+
log.info('Corrupted storage removed, retrying ServerNode creation...');
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
log.warn('No corrupted storage files found, corruption may be elsewhere');
|
|
287
|
+
}
|
|
288
|
+
const serverNode = await ServerNode.create(nodeOptions);
|
|
289
|
+
log.info('Successfully recovered from corrupted Matter storage');
|
|
290
|
+
return serverNode;
|
|
291
|
+
}
|
|
292
|
+
catch (retryError) {
|
|
293
|
+
log.error('Failed to recover from corrupted storage:', retryError);
|
|
294
|
+
log.error('Original error:', error);
|
|
295
|
+
throw new Error('Matter storage is corrupted and automatic recovery failed. '
|
|
296
|
+
+ `Please manually delete: ${serverNodeStorePath}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async start() {
|
|
301
|
+
if (this.isRunning) {
|
|
302
|
+
log.warn('Matter server is already running');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
log.info('Starting Matter.js server...');
|
|
307
|
+
await this.setupStorage();
|
|
308
|
+
await this.loadOrGenerateCredentials();
|
|
309
|
+
log.info(`Configuration: Port=${this.config.port}, Passcode=${this.passcode}, Discriminator=${this.discriminator}`);
|
|
310
|
+
if (this.config.networkInterfaces && this.config.networkInterfaces.length > 0) {
|
|
311
|
+
const environment = Environment.default;
|
|
312
|
+
const interfaceConfig = {};
|
|
313
|
+
for (const interfaceName of this.config.networkInterfaces) {
|
|
314
|
+
interfaceConfig[interfaceName] = { type: 2 };
|
|
315
|
+
}
|
|
316
|
+
environment.vars.set('network.interface', interfaceConfig);
|
|
317
|
+
log.info(`Configured Matter server to use network interfaces: ${this.config.networkInterfaces.join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
log.debug('No network interfaces specified, using all available interfaces');
|
|
321
|
+
}
|
|
322
|
+
const commissioningOptions = {
|
|
323
|
+
passcode: this.passcode,
|
|
324
|
+
discriminator: this.discriminator,
|
|
325
|
+
};
|
|
326
|
+
log.info(`Using commissioning credentials: passcode=${this.passcode}, discriminator=${this.discriminator}`);
|
|
327
|
+
const displayName = this.config.externalAccessory
|
|
328
|
+
? (this.config.model || 'Matter Device')
|
|
329
|
+
: 'Homebridge Matter Bridge';
|
|
330
|
+
const sanitizedId = this.config.uniqueId;
|
|
331
|
+
const nodeOptions = {
|
|
332
|
+
id: sanitizedId,
|
|
333
|
+
network: {
|
|
334
|
+
port: this.config.port,
|
|
335
|
+
ipv4: true,
|
|
336
|
+
},
|
|
337
|
+
commissioning: commissioningOptions,
|
|
338
|
+
basicInformation: {
|
|
339
|
+
nodeLabel: displayName.slice(0, 32),
|
|
340
|
+
vendorId: VendorId(this.vendorId),
|
|
341
|
+
vendorName: (this.config.manufacturer || 'Homebridge').slice(0, 32),
|
|
342
|
+
productId: this.productId,
|
|
343
|
+
productName: displayName.slice(0, 32),
|
|
344
|
+
productLabel: displayName.slice(0, 64),
|
|
345
|
+
serialNumber: this.serialNumber = this.config.serialNumber || this.config.uniqueId,
|
|
346
|
+
hardwareVersion: 1,
|
|
347
|
+
hardwareVersionString: release(),
|
|
348
|
+
softwareVersion: 1,
|
|
349
|
+
softwareVersionString: this.config.firmwareRevision || getVersion(),
|
|
350
|
+
reachable: true,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
if (!this.config.externalAccessory) {
|
|
354
|
+
nodeOptions.productDescription = {
|
|
355
|
+
name: displayName,
|
|
356
|
+
deviceType: AggregatorEndpoint.deviceType,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
this.serverNode = await this.createServerNodeWithRecovery(nodeOptions, sanitizedId);
|
|
360
|
+
this.setupCommissioningEventListeners();
|
|
361
|
+
if (!this.config.externalAccessory) {
|
|
362
|
+
this.aggregator = new Endpoint(AggregatorEndpoint, {
|
|
363
|
+
id: 'homebridge-aggregator',
|
|
364
|
+
});
|
|
365
|
+
await this.serverNode.add(this.aggregator);
|
|
366
|
+
log.debug('Created aggregator endpoint for bridged mode');
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
log.debug('External accessory mode - skipping aggregator creation');
|
|
370
|
+
}
|
|
371
|
+
await this.generateCommissioningInfo();
|
|
372
|
+
this.shutdownHandler = async () => {
|
|
373
|
+
log.info('Shutting down Matter server...');
|
|
374
|
+
await this.stop();
|
|
375
|
+
};
|
|
376
|
+
process.on('SIGINT', this.shutdownHandler);
|
|
377
|
+
process.on('SIGTERM', this.shutdownHandler);
|
|
378
|
+
if (!this.config.externalAccessory) {
|
|
379
|
+
this.serverNode.run().then(() => {
|
|
380
|
+
log.info('Matter server stopped normally');
|
|
381
|
+
}, (error) => {
|
|
382
|
+
log.error('Matter server stopped with error:', error);
|
|
383
|
+
errorHandler.handleError(error, 'server-runtime');
|
|
384
|
+
});
|
|
385
|
+
await this.waitForServerReady();
|
|
386
|
+
if (this.accessoryCache) {
|
|
387
|
+
const loaded = await this.accessoryCache.load();
|
|
388
|
+
log.debug(`Matter cache loaded: ${loaded.size} accessories`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
log.debug('No accessory cache available');
|
|
392
|
+
}
|
|
393
|
+
this.updateCommissioningFile().catch((error) => {
|
|
394
|
+
log.warn('Failed to update commissioning file on startup:', error);
|
|
395
|
+
});
|
|
396
|
+
this.isRunning = true;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
log.debug('Deferred start mode - server prepared but not running yet (will start after device registration)');
|
|
400
|
+
}
|
|
401
|
+
log.info(`Matter server started successfully on port ${this.config.port}`);
|
|
402
|
+
log.info('Plugins can now register Matter accessories via the API');
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
log.error('Failed to start Matter server:', error);
|
|
406
|
+
await this.cleanup();
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async runServer() {
|
|
411
|
+
if (!this.serverNode) {
|
|
412
|
+
throw new MatterDeviceError('Server node not initialized - call start() first');
|
|
413
|
+
}
|
|
414
|
+
if (this.isRunning) {
|
|
415
|
+
log.warn('Matter server is already running');
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (!this.config.externalAccessory) {
|
|
419
|
+
throw new MatterDeviceError('runServer() should only be called when externalAccessory mode is enabled');
|
|
420
|
+
}
|
|
421
|
+
log.debug('Running deferred server with device(s) already attached');
|
|
422
|
+
this.serverNode.run().then(() => {
|
|
423
|
+
log.info('Matter server stopped normally');
|
|
424
|
+
}, (error) => {
|
|
425
|
+
log.error('Matter server stopped with error:', error);
|
|
426
|
+
errorHandler.handleError(error, 'server-runtime');
|
|
427
|
+
});
|
|
428
|
+
await this.waitForServerReady();
|
|
429
|
+
if (this.accessoryCache) {
|
|
430
|
+
const loaded = await this.accessoryCache.load();
|
|
431
|
+
log.debug(`Matter cache loaded: ${loaded.size} accessories`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
log.debug('No accessory cache available');
|
|
435
|
+
}
|
|
436
|
+
this.updateCommissioningFile().catch((error) => {
|
|
437
|
+
log.warn('Failed to update commissioning file on startup:', error);
|
|
438
|
+
});
|
|
439
|
+
this.isRunning = true;
|
|
440
|
+
log.info('Matter server is now running');
|
|
441
|
+
}
|
|
442
|
+
async setupStorage() {
|
|
443
|
+
if (!this.config.storagePath) {
|
|
444
|
+
throw new Error('Storage path is required for Matter server');
|
|
445
|
+
}
|
|
446
|
+
const storagePath = resolve(this.config.storagePath);
|
|
447
|
+
const normalizedPath = normalize(storagePath);
|
|
448
|
+
const allowedBasePaths = [
|
|
449
|
+
resolve(homedir(), '.homebridge'),
|
|
450
|
+
resolve(process.cwd()),
|
|
451
|
+
'/var/lib/homebridge',
|
|
452
|
+
];
|
|
453
|
+
const isAllowed = allowedBasePaths.some(basePath => normalizedPath.startsWith(basePath));
|
|
454
|
+
if (!isAllowed || normalizedPath.includes('..')) {
|
|
455
|
+
throw new Error(`Storage path not allowed: ${normalizedPath}. Must be within homebridge directories.`);
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
await fse.ensureDir(normalizedPath);
|
|
459
|
+
await access(normalizedPath, constants.R_OK | constants.W_OK);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
throw new Error(`Storage path not accessible: ${error}`);
|
|
463
|
+
}
|
|
464
|
+
const bridgeId = this.config.uniqueId || 'default';
|
|
465
|
+
this.matterStoragePath = join(normalizedPath, bridgeId);
|
|
466
|
+
await fse.ensureDir(this.matterStoragePath);
|
|
467
|
+
this.storageManager = new MatterStorageManager(this.matterStoragePath);
|
|
468
|
+
this.accessoryCache = new MatterAccessoryCache(normalizedPath, bridgeId);
|
|
469
|
+
const environment = Environment.default;
|
|
470
|
+
const storageService = environment.get(StorageService);
|
|
471
|
+
storageService.location = this.matterStoragePath;
|
|
472
|
+
storageService.factory = (namespace) => {
|
|
473
|
+
if (!this.storageManager) {
|
|
474
|
+
throw new Error('Storage manager not initialized');
|
|
475
|
+
}
|
|
476
|
+
const storage = this.storageManager.getStorage(namespace);
|
|
477
|
+
storage.initialize().catch((error) => {
|
|
478
|
+
log.error(`Failed to initialize storage namespace ${namespace}:`, error);
|
|
479
|
+
});
|
|
480
|
+
return storage;
|
|
481
|
+
};
|
|
482
|
+
this.cleanupHandlers.push(async () => {
|
|
483
|
+
if (this.storageManager) {
|
|
484
|
+
await this.storageManager.closeAll();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
log.info(`Matter storage initialized at: ${this.matterStoragePath}`);
|
|
488
|
+
}
|
|
489
|
+
async loadOrGenerateCredentials() {
|
|
490
|
+
if (!this.storageManager) {
|
|
491
|
+
throw new Error('Storage manager not initialized');
|
|
492
|
+
}
|
|
493
|
+
const storage = this.storageManager.getStorage('credentials');
|
|
494
|
+
await storage.initialize();
|
|
495
|
+
const storedPasscode = storage.get([], 'passcode');
|
|
496
|
+
const storedDiscriminator = storage.get([], 'discriminator');
|
|
497
|
+
if (storedPasscode && storedDiscriminator) {
|
|
498
|
+
log.info('Loading existing commissioning credentials from storage');
|
|
499
|
+
this.passcode = storedPasscode;
|
|
500
|
+
this.discriminator = storedDiscriminator;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
log.info('Generating new commissioning credentials');
|
|
504
|
+
this.passcode = this.generateSecurePasscode();
|
|
505
|
+
this.discriminator = this.generateRandomDiscriminator();
|
|
506
|
+
storage.set([], 'passcode', this.passcode);
|
|
507
|
+
storage.set([], 'discriminator', this.discriminator);
|
|
508
|
+
log.info('Commissioning credentials saved to storage');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async generateCommissioningInfo() {
|
|
512
|
+
const passcode = this.passcode.toString().padStart(8, '0');
|
|
513
|
+
const discriminator = this.discriminator;
|
|
514
|
+
const vendorId = this.vendorId;
|
|
515
|
+
const productId = this.productId;
|
|
516
|
+
const manualCode = ManualPairingCodeCodec.encode({
|
|
517
|
+
discriminator,
|
|
518
|
+
passcode: this.passcode,
|
|
519
|
+
});
|
|
520
|
+
const manualPairingCode = `${manualCode.slice(0, 4)}-${manualCode.slice(4, 7)}-${manualCode.slice(7, 11)}`;
|
|
521
|
+
log.info(`Encoding QR code with: passcode=${this.passcode}, discriminator=${discriminator}, vendorId=${vendorId}, productId=${productId}`);
|
|
522
|
+
const qrCodePayload = QrPairingCodeCodec.encode([{
|
|
523
|
+
version: 0,
|
|
524
|
+
vendorId,
|
|
525
|
+
productId,
|
|
526
|
+
flowType: 0,
|
|
527
|
+
discoveryCapabilities: 4,
|
|
528
|
+
discriminator,
|
|
529
|
+
passcode: this.passcode,
|
|
530
|
+
}]);
|
|
531
|
+
log.info(`Generated QR code: ${qrCodePayload}`);
|
|
532
|
+
log.info(`Generated manual code: ${manualPairingCode}`);
|
|
533
|
+
this.commissioningInfo = {
|
|
534
|
+
qrCode: qrCodePayload,
|
|
535
|
+
manualPairingCode,
|
|
536
|
+
};
|
|
537
|
+
try {
|
|
538
|
+
if (!this.matterStoragePath) {
|
|
539
|
+
throw new Error('Matter storage path not initialized');
|
|
540
|
+
}
|
|
541
|
+
const commissioningFilePath = join(this.matterStoragePath, 'commissioning.json');
|
|
542
|
+
const commissioningData = {
|
|
543
|
+
qrCode: qrCodePayload,
|
|
544
|
+
manualPairingCode,
|
|
545
|
+
serialNumber: this.serialNumber,
|
|
546
|
+
passcode: this.passcode,
|
|
547
|
+
discriminator: this.discriminator,
|
|
548
|
+
commissioned: this.isCommissioned(),
|
|
549
|
+
};
|
|
550
|
+
await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
|
|
551
|
+
log.debug(`Saved commissioning info to ${commissioningFilePath}`);
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
555
|
+
log.warn(`Failed to save commissioning info to disk: ${errorMessage}`);
|
|
556
|
+
}
|
|
557
|
+
log.info(`${'='.repeat(60)}`);
|
|
558
|
+
log.info('📱 MATTER COMMISSIONING INFORMATION');
|
|
559
|
+
log.info('='.repeat(60));
|
|
560
|
+
log.info(`Manual Pairing Code: ${manualPairingCode}`);
|
|
561
|
+
log.info(`Passcode: ${passcode}`);
|
|
562
|
+
log.info(`Discriminator: ${discriminator}`);
|
|
563
|
+
log.info('QR Code for commissioning:');
|
|
564
|
+
QRCode.generate(qrCodePayload, { small: true }, (qrcode) => {
|
|
565
|
+
console.log(qrcode);
|
|
566
|
+
});
|
|
567
|
+
log.info(`${'='.repeat(60)}`);
|
|
568
|
+
}
|
|
569
|
+
async waitForServerReady(maxWaitTime = SERVER_READY_TIMEOUT_MS) {
|
|
570
|
+
const startTime = Date.now();
|
|
571
|
+
while (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
|
|
572
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
573
|
+
throw new Error('Server failed to become ready within timeout');
|
|
574
|
+
}
|
|
575
|
+
await new Promise(resolve => setTimeout(resolve, SERVER_READY_POLL_INTERVAL_MS));
|
|
576
|
+
}
|
|
577
|
+
await new Promise(resolve => setTimeout(resolve, SERVER_INIT_DELAY_MS));
|
|
578
|
+
}
|
|
579
|
+
setupCommissioningEventListeners() {
|
|
580
|
+
if (!this.serverNode) {
|
|
581
|
+
log.warn('Cannot set up commissioning event listeners - serverNode not initialized');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
log.debug('Setting up commissioning event listeners');
|
|
585
|
+
try {
|
|
586
|
+
this.serverNode.events.commissioning.fabricsChanged.on((fabricIndex, action) => {
|
|
587
|
+
log.info(`Fabric ${action}: index ${fabricIndex}`);
|
|
588
|
+
this.updateCommissioningFile().catch((error) => {
|
|
589
|
+
log.warn('Failed to update commissioning file after fabric change:', error);
|
|
590
|
+
});
|
|
591
|
+
const commissioned = this.isCommissioned();
|
|
592
|
+
const fabricCount = this.getCommissionedFabricCount();
|
|
593
|
+
this.emit('commissioning-status-changed', commissioned, fabricCount);
|
|
594
|
+
});
|
|
595
|
+
this.serverNode.events.commissioning.commissioned.on(() => {
|
|
596
|
+
log.info('Bridge commissioned');
|
|
597
|
+
this.updateCommissioningFile().catch((error) => {
|
|
598
|
+
log.warn('Failed to update commissioning file after commissioning:', error);
|
|
599
|
+
});
|
|
600
|
+
const fabricCount = this.getCommissionedFabricCount();
|
|
601
|
+
this.emit('commissioning-status-changed', true, fabricCount);
|
|
602
|
+
});
|
|
603
|
+
this.serverNode.events.commissioning.decommissioned.on(() => {
|
|
604
|
+
log.info('Bridge decommissioned');
|
|
605
|
+
this.updateCommissioningFile().catch((error) => {
|
|
606
|
+
log.warn('Failed to update commissioning file after decommissioning:', error);
|
|
607
|
+
});
|
|
608
|
+
this.emit('commissioning-status-changed', false, 0);
|
|
609
|
+
});
|
|
610
|
+
log.debug('Commissioning event listeners registered successfully');
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
log.error('Failed to set up commissioning event listeners:', error);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async updateCommissioningFile() {
|
|
617
|
+
try {
|
|
618
|
+
if (!this.matterStoragePath) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const commissioningFilePath = join(this.matterStoragePath, 'commissioning.json');
|
|
622
|
+
const commissioningData = {
|
|
623
|
+
qrCode: this.commissioningInfo.qrCode,
|
|
624
|
+
manualPairingCode: this.commissioningInfo.manualPairingCode,
|
|
625
|
+
serialNumber: this.serialNumber,
|
|
626
|
+
passcode: this.passcode,
|
|
627
|
+
discriminator: this.discriminator,
|
|
628
|
+
commissioned: this.isCommissioned(),
|
|
629
|
+
fabricCount: this.getCommissionedFabricCount(),
|
|
630
|
+
fabrics: this.getFabricInfo(),
|
|
631
|
+
};
|
|
632
|
+
await writeFile(commissioningFilePath, JSON.stringify(commissioningData, null, 2), 'utf-8');
|
|
633
|
+
log.debug('Updated commissioning info file');
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
637
|
+
log.debug(`Failed to update commissioning info file: ${errorMessage}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async registerPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
641
|
+
for (const accessory of accessories) {
|
|
642
|
+
await this.registerAccessory(pluginIdentifier, platformName, accessory);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async unregisterPlatformAccessories(pluginIdentifier, platformName, accessories) {
|
|
646
|
+
for (const accessory of accessories) {
|
|
647
|
+
await this.unregisterAccessory(accessory.UUID);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
async updatePlatformAccessories(accessories) {
|
|
651
|
+
if (!this.accessoryCache) {
|
|
652
|
+
log.warn('Cannot update Matter platform accessories - cache not initialized');
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
for (const accessory of accessories) {
|
|
656
|
+
const internal = accessory;
|
|
657
|
+
if (!this.accessories.has(accessory.UUID)) {
|
|
658
|
+
log.warn(`Cannot update Matter accessory ${accessory.UUID} - not registered in current session`);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (!this.accessoryCache.hasCached(accessory.UUID)) {
|
|
662
|
+
log.warn(`Cannot update Matter accessory ${accessory.UUID} - not found in cache`);
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
this.accessories.set(accessory.UUID, internal);
|
|
666
|
+
log.debug(`Updated Matter accessory ${accessory.UUID} (${accessory.displayName})`);
|
|
667
|
+
}
|
|
668
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
669
|
+
}
|
|
670
|
+
async registerAccessory(pluginIdentifier, platformName, accessory) {
|
|
671
|
+
if (!this.serverNode || (!this.config.externalAccessory && !this.aggregator)) {
|
|
672
|
+
throw new MatterDeviceError('Matter server not started');
|
|
673
|
+
}
|
|
674
|
+
validateAccessoryRequiredFields(accessory);
|
|
675
|
+
if (this.accessories.has(accessory.UUID)) {
|
|
676
|
+
const existing = this.accessories.get(accessory.UUID);
|
|
677
|
+
throw new MatterDeviceError(`Matter accessory with UUID "${accessory.UUID}" is already registered.\n`
|
|
678
|
+
+ `Existing accessory: "${existing?.displayName}"\n`
|
|
679
|
+
+ `New accessory: "${accessory.displayName}"\n`
|
|
680
|
+
+ 'Each accessory must have a unique UUID. Use api.hap.uuid.generate() with a unique string.');
|
|
681
|
+
}
|
|
682
|
+
this.restoreCachedState(accessory);
|
|
683
|
+
if (this.accessories.size >= this.MAX_DEVICES) {
|
|
684
|
+
throw new MatterDeviceError(`Cannot register Matter accessory "${accessory.displayName}": `
|
|
685
|
+
+ `Maximum device limit reached (${this.MAX_DEVICES} devices).\n`
|
|
686
|
+
+ `Current registered devices: ${this.accessories.size}`);
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
let deviceType = accessory.deviceType;
|
|
690
|
+
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
691
|
+
if (windowCoveringFeatures.length > 0) {
|
|
692
|
+
deviceType = applyWindowCoveringFeatures(deviceType, accessory, windowCoveringFeatures);
|
|
693
|
+
}
|
|
694
|
+
const features = this.detectClusterFeatures(accessory, deviceType);
|
|
695
|
+
const customBehaviors = await this.buildCustomBehaviors(accessory, deviceType, features);
|
|
696
|
+
if (customBehaviors.length > 0) {
|
|
697
|
+
deviceType = withBehaviors(deviceType, customBehaviors);
|
|
698
|
+
log.info(`Applied ${customBehaviors.length} custom behavior(s) to device type`);
|
|
699
|
+
}
|
|
700
|
+
if (!this.config.externalAccessory) {
|
|
701
|
+
deviceType = withBehaviors(deviceType, [BridgedDeviceBasicInformationServer]);
|
|
702
|
+
log.debug(`Added BridgedDeviceBasicInformationServer to ${accessory.displayName}`);
|
|
703
|
+
}
|
|
704
|
+
const endpointOptions = this.createEndpointOptions(accessory);
|
|
705
|
+
const endpoint = new Endpoint(deviceType, endpointOptions);
|
|
706
|
+
if (this.config.debugModeEnabled) {
|
|
707
|
+
log.debug(`Created endpoint for ${accessory.displayName} with initial cluster states`);
|
|
708
|
+
}
|
|
709
|
+
if (this.config.externalAccessory) {
|
|
710
|
+
await this.serverNode.add(endpoint);
|
|
711
|
+
log.debug(`Added ${accessory.displayName} as external accessory to ServerNode`);
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
await this.aggregator.add(endpoint);
|
|
715
|
+
if (this.config.debugModeEnabled) {
|
|
716
|
+
log.debug(`Added endpoint for ${accessory.displayName} to aggregator`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
this.registerAccessoryHandlers(accessory);
|
|
720
|
+
const internalParts = await this.createAccessoryParts(accessory);
|
|
721
|
+
await this.finalizeAccessoryRegistration(accessory, endpoint, internalParts);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
log.error(`Failed to register Matter accessory ${accessory.displayName}:`, error);
|
|
725
|
+
throw new MatterDeviceError(`Failed to register accessory: ${error}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
restoreCachedState(accessory) {
|
|
729
|
+
if (this.accessoryCache && this.accessoryCache.hasCached(accessory.UUID)) {
|
|
730
|
+
const cached = this.accessoryCache.getCached(accessory.UUID);
|
|
731
|
+
if (cached?.clusters && accessory.clusters) {
|
|
732
|
+
for (const [clusterName, cachedAttrs] of Object.entries(cached.clusters)) {
|
|
733
|
+
if (!accessory.clusters[clusterName]) {
|
|
734
|
+
accessory.clusters[clusterName] = cachedAttrs;
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
accessory.clusters[clusterName] = {
|
|
738
|
+
...accessory.clusters[clusterName],
|
|
739
|
+
...cachedAttrs,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (cached.context) {
|
|
744
|
+
accessory.context = cached.context;
|
|
745
|
+
}
|
|
746
|
+
log.info(`Restored cached state for Matter accessory: ${accessory.displayName}`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
detectClusterFeatures(accessory, deviceType) {
|
|
751
|
+
const windowCoveringFeatures = detectWindowCoveringFeatures(accessory);
|
|
752
|
+
let serviceAreaFeatures = null;
|
|
753
|
+
if (accessory.clusters?.serviceArea) {
|
|
754
|
+
const features = [];
|
|
755
|
+
if (accessory.clusters.serviceArea.supportedMaps) {
|
|
756
|
+
features.push('Maps');
|
|
757
|
+
}
|
|
758
|
+
if (accessory.clusters.serviceArea.progress !== undefined) {
|
|
759
|
+
features.push('ProgressReporting');
|
|
760
|
+
}
|
|
761
|
+
if (features.length > 0) {
|
|
762
|
+
serviceAreaFeatures = features;
|
|
763
|
+
log.info(`ServiceArea features will be enabled for ${accessory.displayName}: ${features.join(', ')}`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
let colorControlFeatures = null;
|
|
767
|
+
if (accessory.handlers?.colorControl) {
|
|
768
|
+
colorControlFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.COLOR_CONTROL, extractColorControlFeatures);
|
|
769
|
+
if (colorControlFeatures) {
|
|
770
|
+
colorControlFeatures = determineColorControlFeaturesFromHandlers(accessory.handlers.colorControl);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
let thermostatFeatures = null;
|
|
774
|
+
if (accessory.handlers?.thermostat) {
|
|
775
|
+
thermostatFeatures = detectBehaviorFeatures(deviceType, CLUSTER_IDS.THERMOSTAT, extractThermostatFeatures);
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
windowCoveringFeatures,
|
|
779
|
+
serviceAreaFeatures,
|
|
780
|
+
colorControlFeatures,
|
|
781
|
+
thermostatFeatures,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
async buildCustomBehaviors(accessory, deviceType, features) {
|
|
785
|
+
const customBehaviors = [];
|
|
786
|
+
if (!accessory.handlers) {
|
|
787
|
+
return customBehaviors;
|
|
788
|
+
}
|
|
789
|
+
log.debug(`[${accessory.displayName}] Has handlers: ${Object.keys(accessory.handlers).join(', ')}`);
|
|
790
|
+
const behaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
791
|
+
if (isDeviceType(deviceType, devices.RoboticVacuumCleanerDevice)) {
|
|
792
|
+
const { RvcCleanModeServer, ServiceAreaServer } = devices.RoboticVacuumCleanerRequirements;
|
|
793
|
+
if (accessory.clusters?.rvcCleanMode) {
|
|
794
|
+
if (accessory.handlers?.rvcCleanMode) {
|
|
795
|
+
const behaviorClass = HomebridgeRvcCleanModeServer;
|
|
796
|
+
customBehaviors.push(behaviorClass);
|
|
797
|
+
log.info('Adding custom RvcCleanMode behavior with handlers');
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
customBehaviors.push(RvcCleanModeServer);
|
|
801
|
+
log.info('Adding base RvcCleanMode server');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (accessory.clusters?.serviceArea) {
|
|
805
|
+
if (accessory.handlers?.serviceArea) {
|
|
806
|
+
let behaviorClass = HomebridgeServiceAreaServer;
|
|
807
|
+
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
808
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
809
|
+
log.info(`ServiceArea custom behavior will have features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
810
|
+
}
|
|
811
|
+
customBehaviors.push(behaviorClass);
|
|
812
|
+
log.info('Adding custom ServiceArea behavior with handlers');
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
let behaviorClass = ServiceAreaServer;
|
|
816
|
+
if (features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
817
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
818
|
+
log.info(`ServiceArea base server will have features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
819
|
+
}
|
|
820
|
+
customBehaviors.push(behaviorClass);
|
|
821
|
+
log.info('Adding base ServiceArea server');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (accessory.clusters?.powerSource) {
|
|
825
|
+
const hasBattery = accessory.clusters.powerSource.batPercentRemaining !== undefined
|
|
826
|
+
|| accessory.clusters.powerSource.batChargeLevel !== undefined;
|
|
827
|
+
let powerSourceBehavior = PowerSourceServer;
|
|
828
|
+
if (hasBattery) {
|
|
829
|
+
powerSourceBehavior = withFeatures(PowerSourceServer, ['Battery']);
|
|
830
|
+
log.debug('Adding PowerSource server with battery feature');
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
log.debug('Adding base PowerSource server');
|
|
834
|
+
}
|
|
835
|
+
customBehaviors.push(powerSourceBehavior);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const clusterName of Object.keys(accessory.handlers || {})) {
|
|
839
|
+
const skipWindowCoveringBehavior = accessory.context?._skipWindowCoveringBehavior;
|
|
840
|
+
if (clusterName === 'windowCovering' && skipWindowCoveringBehavior) {
|
|
841
|
+
log.debug('Skipping custom WindowCovering behavior (using base server with features instead)');
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (clusterName === 'rvcCleanMode' || clusterName === 'serviceArea' || clusterName === 'powerSource') {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
let behaviorClass = behaviorMap[clusterName];
|
|
848
|
+
if (clusterName === 'colorControl' && behaviorClass && features.colorControlFeatures && features.colorControlFeatures.length > 0) {
|
|
849
|
+
behaviorClass = withFeatures(behaviorClass, features.colorControlFeatures);
|
|
850
|
+
log.info(`ColorControl custom behavior will preserve features: ${features.colorControlFeatures.join(', ')}`);
|
|
851
|
+
}
|
|
852
|
+
if (clusterName === 'thermostat' && behaviorClass && features.thermostatFeatures && features.thermostatFeatures.length > 0) {
|
|
853
|
+
behaviorClass = withFeatures(behaviorClass, features.thermostatFeatures);
|
|
854
|
+
log.info(`Thermostat custom behavior will preserve features: ${features.thermostatFeatures.join(', ')}`);
|
|
855
|
+
}
|
|
856
|
+
if (clusterName === 'serviceArea' && behaviorClass && features.serviceAreaFeatures && features.serviceAreaFeatures.length > 0) {
|
|
857
|
+
behaviorClass = withFeatures(behaviorClass, features.serviceAreaFeatures);
|
|
858
|
+
log.info(`ServiceArea custom behavior will preserve features: ${features.serviceAreaFeatures.join(', ')}`);
|
|
859
|
+
}
|
|
860
|
+
if (clusterName === 'windowCovering') {
|
|
861
|
+
log.debug(`WindowCovering handler found: behaviorClass=${!!behaviorClass}, windowCoveringFeatures=${features.windowCoveringFeatures}, length=${features.windowCoveringFeatures?.length}`);
|
|
862
|
+
if (behaviorClass && features.windowCoveringFeatures && features.windowCoveringFeatures.length > 0) {
|
|
863
|
+
behaviorClass = withFeatures(behaviorClass, features.windowCoveringFeatures);
|
|
864
|
+
log.debug(`WindowCovering custom behavior will have features: ${features.windowCoveringFeatures.join(', ')}`);
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
log.debug(`Skipping WindowCovering feature application: behaviorClass=${!!behaviorClass}, features=${features.windowCoveringFeatures}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (behaviorClass) {
|
|
871
|
+
customBehaviors.push(behaviorClass);
|
|
872
|
+
log.info(`Will use ${behaviorClass.name} for ${accessory.displayName}`);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' - handlers will be registered but may not be called`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return customBehaviors;
|
|
879
|
+
}
|
|
880
|
+
createEndpointOptions(accessory) {
|
|
881
|
+
const endpointOptions = {
|
|
882
|
+
id: accessory.UUID,
|
|
883
|
+
...accessory.clusters,
|
|
884
|
+
};
|
|
885
|
+
if (!this.config.externalAccessory) {
|
|
886
|
+
endpointOptions.bridgedDeviceBasicInformation = {
|
|
887
|
+
vendorName: accessory.manufacturer,
|
|
888
|
+
nodeLabel: accessory.displayName,
|
|
889
|
+
productName: accessory.model,
|
|
890
|
+
productLabel: accessory.displayName,
|
|
891
|
+
serialNumber: accessory.serialNumber,
|
|
892
|
+
reachable: true,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return endpointOptions;
|
|
896
|
+
}
|
|
897
|
+
registerAccessoryHandlers(accessory) {
|
|
898
|
+
if (!accessory.handlers) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
log.info(`Setting up handlers for accessory ${accessory.UUID}`);
|
|
902
|
+
RegistryManager.registerEndpoint(accessory.UUID, this.behaviorRegistry);
|
|
903
|
+
for (const [clusterName, handlers] of Object.entries(accessory.handlers)) {
|
|
904
|
+
log.info(` Processing cluster: ${clusterName}`);
|
|
905
|
+
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
906
|
+
this.behaviorRegistry.registerHandler(accessory.UUID, clusterName, commandName, handler);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
async createAccessoryParts(accessory) {
|
|
911
|
+
const internalParts = [];
|
|
912
|
+
if (!accessory.parts || accessory.parts.length === 0) {
|
|
913
|
+
return internalParts;
|
|
914
|
+
}
|
|
915
|
+
log.info(`Creating ${accessory.parts.length} child endpoint(s) for ${accessory.displayName}`);
|
|
916
|
+
for (const part of accessory.parts) {
|
|
917
|
+
const partEndpointId = `${accessory.UUID}-part-${part.id}`;
|
|
918
|
+
this.behaviorRegistry.registerPartEndpoint(partEndpointId, accessory.UUID, part.id);
|
|
919
|
+
let partDeviceType = part.deviceType;
|
|
920
|
+
const partCustomBehaviors = [];
|
|
921
|
+
if (part.handlers) {
|
|
922
|
+
const partBehaviorMap = MatterServer.CLUSTER_BEHAVIOR_MAP;
|
|
923
|
+
for (const clusterName of Object.keys(part.handlers)) {
|
|
924
|
+
const behaviorClass = partBehaviorMap[clusterName];
|
|
925
|
+
if (behaviorClass) {
|
|
926
|
+
partCustomBehaviors.push(behaviorClass);
|
|
927
|
+
log.info(` Will use ${behaviorClass.name} for part ${part.id}`);
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
log.warn(`No custom behavior class available for cluster '${clusterName}' on part ${part.id}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (partCustomBehaviors.length > 0) {
|
|
934
|
+
partDeviceType = withBehaviors(partDeviceType, partCustomBehaviors);
|
|
935
|
+
log.info(` Applied ${partCustomBehaviors.length} custom behavior(s) to part ${part.id}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (!this.config.externalAccessory) {
|
|
939
|
+
partDeviceType = withBehaviors(partDeviceType, [BridgedDeviceBasicInformationServer]);
|
|
940
|
+
}
|
|
941
|
+
const partEndpointOptions = {
|
|
942
|
+
id: partEndpointId,
|
|
943
|
+
...part.clusters,
|
|
944
|
+
};
|
|
945
|
+
if (!this.config.externalAccessory) {
|
|
946
|
+
partEndpointOptions.bridgedDeviceBasicInformation = {
|
|
947
|
+
vendorName: accessory.manufacturer,
|
|
948
|
+
nodeLabel: part.displayName || `${accessory.displayName} - ${part.id}`,
|
|
949
|
+
productName: accessory.model,
|
|
950
|
+
productLabel: part.displayName || part.id,
|
|
951
|
+
serialNumber: `${accessory.serialNumber}-${part.id}`,
|
|
952
|
+
reachable: true,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const partEndpoint = new Endpoint(partDeviceType, partEndpointOptions);
|
|
956
|
+
if (this.config.externalAccessory) {
|
|
957
|
+
await this.serverNode.add(partEndpoint);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
await this.aggregator.add(partEndpoint);
|
|
961
|
+
}
|
|
962
|
+
log.info(` Created part endpoint: ${part.displayName || part.id} (${partEndpointId})`);
|
|
963
|
+
if (part.handlers) {
|
|
964
|
+
RegistryManager.registerEndpoint(partEndpointId, this.behaviorRegistry);
|
|
965
|
+
for (const [clusterName, handlers] of Object.entries(part.handlers)) {
|
|
966
|
+
for (const [commandName, handler] of Object.entries(handlers)) {
|
|
967
|
+
this.behaviorRegistry.registerHandler(partEndpointId, clusterName, commandName, handler);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
log.debug(` Registered ${Object.keys(part.handlers).length} handler(s) for part ${part.id}`);
|
|
971
|
+
}
|
|
972
|
+
internalParts.push({
|
|
973
|
+
...part,
|
|
974
|
+
endpoint: partEndpoint,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return internalParts;
|
|
978
|
+
}
|
|
979
|
+
async finalizeAccessoryRegistration(accessory, endpoint, internalParts) {
|
|
980
|
+
const internalAccessory = {
|
|
981
|
+
...accessory,
|
|
982
|
+
endpoint,
|
|
983
|
+
registered: true,
|
|
984
|
+
_parts: internalParts.length > 0 ? internalParts : undefined,
|
|
985
|
+
_eventEmitter: new EventEmitter(),
|
|
986
|
+
};
|
|
987
|
+
this.accessories.set(accessory.UUID, internalAccessory);
|
|
988
|
+
log.info(`Registered Matter accessory: ${accessory.displayName} (${accessory.UUID})`);
|
|
989
|
+
if (this.config.debugModeEnabled) {
|
|
990
|
+
log.debug(`Total registered accessories: ${this.accessories.size}/${this.MAX_DEVICES}`);
|
|
991
|
+
}
|
|
992
|
+
await this.notifyPartsListChanged();
|
|
993
|
+
if (this.accessoryCache) {
|
|
994
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
995
|
+
}
|
|
996
|
+
if (this.monitoringEnabled && process.send) {
|
|
997
|
+
const event = {
|
|
998
|
+
type: 'accessoryAdded',
|
|
999
|
+
data: { uuid: accessory.UUID },
|
|
1000
|
+
};
|
|
1001
|
+
process.send({
|
|
1002
|
+
id: "matterEvent",
|
|
1003
|
+
data: event,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async unregisterAccessory(uuid) {
|
|
1008
|
+
const accessory = this.accessories.get(uuid);
|
|
1009
|
+
if (!accessory) {
|
|
1010
|
+
log.debug(`Accessory ${uuid} not found or not registered`);
|
|
1011
|
+
if (this.accessoryCache && this.accessoryCache.getCached(uuid)) {
|
|
1012
|
+
log.debug(`Removing ${uuid} from cache`);
|
|
1013
|
+
this.accessoryCache.removeCached(uuid);
|
|
1014
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
if (accessory.endpoint && this.aggregator) {
|
|
1020
|
+
await accessory.endpoint.close();
|
|
1021
|
+
log.debug(`Removed endpoint for ${accessory.displayName}`);
|
|
1022
|
+
}
|
|
1023
|
+
this.accessories.delete(uuid);
|
|
1024
|
+
log.info(`Unregistered Matter accessory: ${accessory.displayName} (${uuid})`);
|
|
1025
|
+
await this.notifyPartsListChanged();
|
|
1026
|
+
if (this.accessoryCache) {
|
|
1027
|
+
this.accessoryCache.removeCached(uuid);
|
|
1028
|
+
this.accessoryCache.requestSave(this.accessories);
|
|
1029
|
+
}
|
|
1030
|
+
if (this.monitoringEnabled && process.send) {
|
|
1031
|
+
const event = {
|
|
1032
|
+
type: 'accessoryRemoved',
|
|
1033
|
+
data: { uuid },
|
|
1034
|
+
};
|
|
1035
|
+
process.send({
|
|
1036
|
+
id: "matterEvent",
|
|
1037
|
+
data: event,
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
log.error(`Failed to unregister Matter accessory ${uuid}:`, error);
|
|
1043
|
+
throw new MatterDeviceError(`Failed to unregister accessory: ${error}`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async updateAccessoryState(uuid, cluster, attributes, partId) {
|
|
1047
|
+
const accessory = this.accessories.get(uuid);
|
|
1048
|
+
if (!accessory) {
|
|
1049
|
+
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
1050
|
+
}
|
|
1051
|
+
let targetEndpoint;
|
|
1052
|
+
let targetClusters;
|
|
1053
|
+
let displayName;
|
|
1054
|
+
if (partId) {
|
|
1055
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1056
|
+
if (!part || !part.endpoint) {
|
|
1057
|
+
throw new MatterDeviceError(`Part ${partId} not found in accessory ${uuid}`);
|
|
1058
|
+
}
|
|
1059
|
+
targetEndpoint = part.endpoint;
|
|
1060
|
+
targetClusters = part.clusters;
|
|
1061
|
+
displayName = part.displayName || `${accessory.displayName} - ${partId}`;
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
if (!accessory.endpoint) {
|
|
1065
|
+
throw new MatterDeviceError(`Accessory ${uuid} not registered or missing endpoint`);
|
|
1066
|
+
}
|
|
1067
|
+
targetEndpoint = accessory.endpoint;
|
|
1068
|
+
targetClusters = accessory.clusters;
|
|
1069
|
+
displayName = accessory.displayName;
|
|
1070
|
+
}
|
|
1071
|
+
return new Promise((resolve, reject) => {
|
|
1072
|
+
setImmediate(async () => {
|
|
1073
|
+
try {
|
|
1074
|
+
const updateObject = { [cluster]: attributes };
|
|
1075
|
+
await targetEndpoint.set(updateObject);
|
|
1076
|
+
if (!targetClusters) {
|
|
1077
|
+
log.warn(`Target clusters undefined for ${displayName}, cannot cache state`);
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
if (!targetClusters[cluster]) {
|
|
1081
|
+
targetClusters[cluster] = {};
|
|
1082
|
+
}
|
|
1083
|
+
targetClusters[cluster] = {
|
|
1084
|
+
...targetClusters[cluster],
|
|
1085
|
+
...attributes,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
const partInfo = partId ? ` (part: ${partId})` : '';
|
|
1089
|
+
log.debug(`Updated ${cluster} state for ${displayName}${partInfo}:`, attributes);
|
|
1090
|
+
this.notifyStateChange(uuid, cluster, attributes, partId);
|
|
1091
|
+
resolve();
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
const partInfo = partId ? ` part ${partId}` : '';
|
|
1095
|
+
log.error(`Failed to update state for accessory ${uuid}${partInfo}:`, error);
|
|
1096
|
+
reject(new MatterDeviceError(`Failed to update accessory state: ${error}`));
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
async triggerCommand(uuid, cluster, command, args, partId) {
|
|
1102
|
+
const accessory = this.accessories.get(uuid);
|
|
1103
|
+
if (!accessory) {
|
|
1104
|
+
throw new MatterDeviceError(`Accessory ${uuid} not found or not registered`);
|
|
1105
|
+
}
|
|
1106
|
+
let targetEndpoint;
|
|
1107
|
+
let displayName;
|
|
1108
|
+
if (partId) {
|
|
1109
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1110
|
+
if (!part || !part.endpoint) {
|
|
1111
|
+
throw new MatterDeviceError(`Part ${partId} not found in accessory ${uuid}`);
|
|
1112
|
+
}
|
|
1113
|
+
targetEndpoint = part.endpoint;
|
|
1114
|
+
displayName = part.displayName || `${accessory.displayName} - ${partId}`;
|
|
1115
|
+
}
|
|
1116
|
+
else {
|
|
1117
|
+
if (!accessory.endpoint) {
|
|
1118
|
+
throw new MatterDeviceError(`Accessory ${uuid} not registered or missing endpoint`);
|
|
1119
|
+
}
|
|
1120
|
+
targetEndpoint = accessory.endpoint;
|
|
1121
|
+
displayName = accessory.displayName;
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const partInfo = partId ? ` (part: ${partId})` : '';
|
|
1125
|
+
log.debug(`Triggering command ${cluster}.${command} for ${displayName}${partInfo}`, args);
|
|
1126
|
+
await targetEndpoint.act((agent) => {
|
|
1127
|
+
const clusterBehavior = agent[cluster];
|
|
1128
|
+
if (!clusterBehavior) {
|
|
1129
|
+
throw new Error(`Cluster '${cluster}' not found on endpoint`);
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof clusterBehavior[command] !== 'function') {
|
|
1132
|
+
throw new TypeError(`Command '${command}' not found on cluster '${cluster}'`);
|
|
1133
|
+
}
|
|
1134
|
+
if (args && Object.keys(args).length > 0) {
|
|
1135
|
+
return clusterBehavior[command](args);
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
return clusterBehavior[command]();
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
log.debug(`Command ${cluster}.${command} succeeded for ${displayName}${partInfo}`);
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
const partInfo = partId ? ` part ${partId}` : '';
|
|
1145
|
+
log.error(`Failed to trigger command for accessory ${uuid}${partInfo}:`, error);
|
|
1146
|
+
throw new MatterDeviceError(`Failed to trigger command: ${error}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
getAccessoryState(uuid, cluster, partId) {
|
|
1150
|
+
const accessory = this.accessories.get(uuid);
|
|
1151
|
+
if (!accessory) {
|
|
1152
|
+
log.debug(`Accessory ${uuid} not found`);
|
|
1153
|
+
return undefined;
|
|
1154
|
+
}
|
|
1155
|
+
let targetEndpoint;
|
|
1156
|
+
let displayName;
|
|
1157
|
+
if (partId) {
|
|
1158
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1159
|
+
if (!part || !part.endpoint) {
|
|
1160
|
+
log.debug(`Part ${partId} not found in accessory ${uuid}`);
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
targetEndpoint = part.endpoint;
|
|
1164
|
+
displayName = part.displayName || `${accessory.displayName} - ${partId}`;
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
if (!accessory.endpoint) {
|
|
1168
|
+
log.debug(`Accessory ${uuid} not registered or missing endpoint`);
|
|
1169
|
+
return undefined;
|
|
1170
|
+
}
|
|
1171
|
+
targetEndpoint = accessory.endpoint;
|
|
1172
|
+
displayName = accessory.displayName;
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
if (!targetEndpoint.state) {
|
|
1176
|
+
log.debug(`endpoint.state is undefined for ${displayName}`);
|
|
1177
|
+
return undefined;
|
|
1178
|
+
}
|
|
1179
|
+
if (!targetEndpoint.state[cluster]) {
|
|
1180
|
+
const availableClusters = Object.keys(targetEndpoint.state || {});
|
|
1181
|
+
log.debug(`Cluster '${cluster}' not found on ${displayName}. Available: ${availableClusters.join(', ')}`);
|
|
1182
|
+
return undefined;
|
|
1183
|
+
}
|
|
1184
|
+
const clusterState = targetEndpoint.state[cluster];
|
|
1185
|
+
const result = {};
|
|
1186
|
+
const allKeys = new Set([
|
|
1187
|
+
...Object.keys(clusterState),
|
|
1188
|
+
...Object.getOwnPropertyNames(clusterState),
|
|
1189
|
+
]);
|
|
1190
|
+
for (const key of allKeys) {
|
|
1191
|
+
try {
|
|
1192
|
+
if (key.startsWith('_') || key.startsWith('$')) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const value = clusterState[key];
|
|
1196
|
+
if (typeof value === 'function' || value === undefined) {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
result[key] = value;
|
|
1200
|
+
}
|
|
1201
|
+
catch (propError) {
|
|
1202
|
+
log.debug(`Could not read property ${key} from ${cluster}:`, propError);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (Object.keys(result).length === 0) {
|
|
1206
|
+
log.debug(`Cluster ${cluster} found but no readable properties on accessory ${accessory.displayName}`);
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
catch (error) {
|
|
1212
|
+
log.error(`Failed to get state for accessory ${uuid}:`, error);
|
|
1213
|
+
return undefined;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
getAllCachedAccessories() {
|
|
1217
|
+
if (!this.accessoryCache) {
|
|
1218
|
+
log.debug('getAllCachedAccessories: No cache available');
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
const cached = Array.from(this.accessoryCache.getAllCached().values());
|
|
1222
|
+
log.debug(`getAllCachedAccessories: Returning ${cached.length} accessories`);
|
|
1223
|
+
return cached;
|
|
1224
|
+
}
|
|
1225
|
+
getAccessories() {
|
|
1226
|
+
return Array.from(this.accessories.values()).map((acc) => {
|
|
1227
|
+
const { endpoint, registered, ...publicAccessory } = acc;
|
|
1228
|
+
return publicAccessory;
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
getAccessory(uuid) {
|
|
1232
|
+
const accessory = this.accessories.get(uuid);
|
|
1233
|
+
if (!accessory) {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
const { endpoint, registered, ...publicAccessory } = accessory;
|
|
1237
|
+
return publicAccessory;
|
|
1238
|
+
}
|
|
1239
|
+
async stop() {
|
|
1240
|
+
if (!this.isRunning) {
|
|
1241
|
+
log.debug('Matter server is not running');
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
this.isRunning = false;
|
|
1245
|
+
try {
|
|
1246
|
+
if (this.accessoryCache && this.accessories.size > 0) {
|
|
1247
|
+
await this.accessoryCache.save(this.accessories);
|
|
1248
|
+
log.debug('Saved accessory cache before shutdown');
|
|
1249
|
+
}
|
|
1250
|
+
if (this.serverNode) {
|
|
1251
|
+
await this.serverNode.close();
|
|
1252
|
+
log.debug('ServerNode closed (all endpoints cleaned up)');
|
|
1253
|
+
}
|
|
1254
|
+
this.accessories.clear();
|
|
1255
|
+
await this.cleanup();
|
|
1256
|
+
log.info('Matter server stopped');
|
|
1257
|
+
}
|
|
1258
|
+
catch (error) {
|
|
1259
|
+
log.error('Error stopping Matter server:', error);
|
|
1260
|
+
await errorHandler.handleError(error, 'server-stop');
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
finally {
|
|
1264
|
+
this.isRunning = false;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
async cleanup() {
|
|
1268
|
+
if (this.shutdownHandler) {
|
|
1269
|
+
process.off('SIGINT', this.shutdownHandler);
|
|
1270
|
+
process.off('SIGTERM', this.shutdownHandler);
|
|
1271
|
+
this.shutdownHandler = null;
|
|
1272
|
+
}
|
|
1273
|
+
for (const handler of this.cleanupHandlers) {
|
|
1274
|
+
try {
|
|
1275
|
+
await handler();
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
log.debug('Error during cleanup handler:', error);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
this.cleanupHandlers = [];
|
|
1282
|
+
this.serverNode = null;
|
|
1283
|
+
this.aggregator = null;
|
|
1284
|
+
this.isRunning = false;
|
|
1285
|
+
this.commissioningInfo = {};
|
|
1286
|
+
}
|
|
1287
|
+
getFabricInfo() {
|
|
1288
|
+
try {
|
|
1289
|
+
if (!this.storageManager) {
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
const storage = this.storageManager.getStorage(this.config.uniqueId);
|
|
1293
|
+
const fabricsData = storage.get(['fabrics'], 'fabrics');
|
|
1294
|
+
if (Array.isArray(fabricsData) && fabricsData.length > 0) {
|
|
1295
|
+
return fabricsData.map(fabric => ({
|
|
1296
|
+
fabricIndex: fabric.fabricIndex || 0,
|
|
1297
|
+
fabricId: fabric.fabricId?.value?.toString() || '',
|
|
1298
|
+
nodeId: fabric.nodeId?.value?.toString() || '',
|
|
1299
|
+
rootVendorId: fabric.rootVendorId || 0,
|
|
1300
|
+
label: fabric.label || '',
|
|
1301
|
+
}));
|
|
1302
|
+
}
|
|
1303
|
+
return [];
|
|
1304
|
+
}
|
|
1305
|
+
catch (error) {
|
|
1306
|
+
log.debug('Failed to get fabric info from storage:', error);
|
|
1307
|
+
return [];
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
isCommissioned() {
|
|
1311
|
+
try {
|
|
1312
|
+
if (!this.storageManager) {
|
|
1313
|
+
return false;
|
|
1314
|
+
}
|
|
1315
|
+
const storage = this.storageManager.getStorage(this.config.uniqueId);
|
|
1316
|
+
const commissioned = storage.get(['root', 'commissioning'], 'commissioned');
|
|
1317
|
+
if (commissioned === true) {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
const fabrics = this.getFabricInfo();
|
|
1321
|
+
return fabrics.length > 0;
|
|
1322
|
+
}
|
|
1323
|
+
catch (error) {
|
|
1324
|
+
log.debug('Failed to check commissioned status from storage:', error);
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
getCommissionedFabricCount() {
|
|
1329
|
+
return this.getFabricInfo().length;
|
|
1330
|
+
}
|
|
1331
|
+
getServerInfo() {
|
|
1332
|
+
return {
|
|
1333
|
+
running: this.isRunning,
|
|
1334
|
+
port: this.config.port || 5540,
|
|
1335
|
+
deviceCount: this.accessories.size,
|
|
1336
|
+
commissioned: this.isCommissioned(),
|
|
1337
|
+
fabricCount: this.getCommissionedFabricCount(),
|
|
1338
|
+
serialNumber: this.serialNumber,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
getCommissioningInfo() {
|
|
1342
|
+
return {
|
|
1343
|
+
...this.commissioningInfo,
|
|
1344
|
+
serialNumber: this.serialNumber,
|
|
1345
|
+
passcode: this.passcode,
|
|
1346
|
+
discriminator: this.discriminator,
|
|
1347
|
+
commissioned: this.isCommissioned(),
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
getStorageStats() {
|
|
1351
|
+
if (!this.storageManager) {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
return this.storageManager.getAllStats();
|
|
1355
|
+
}
|
|
1356
|
+
isServerRunning() {
|
|
1357
|
+
return this.isRunning;
|
|
1358
|
+
}
|
|
1359
|
+
getDeviceTypes() {
|
|
1360
|
+
return deviceTypes;
|
|
1361
|
+
}
|
|
1362
|
+
getClusters() {
|
|
1363
|
+
return clusters;
|
|
1364
|
+
}
|
|
1365
|
+
async removeFabric(fabricIndex) {
|
|
1366
|
+
if (!this.serverNode) {
|
|
1367
|
+
throw new MatterDeviceError('Matter server not started');
|
|
1368
|
+
}
|
|
1369
|
+
try {
|
|
1370
|
+
log.info(`Removing fabric ${fabricIndex}...`);
|
|
1371
|
+
const serverState = this.serverNode;
|
|
1372
|
+
const removeFabric = serverState?.state?.commissioning?.removeFabric;
|
|
1373
|
+
if (typeof removeFabric !== 'function') {
|
|
1374
|
+
throw new MatterDeviceError('Fabric removal not supported by Matter.js version');
|
|
1375
|
+
}
|
|
1376
|
+
await removeFabric(fabricIndex);
|
|
1377
|
+
log.info(`Fabric ${fabricIndex} removed successfully`);
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1381
|
+
log.error(`Failed to remove fabric ${fabricIndex}:`, error);
|
|
1382
|
+
throw new MatterDeviceError(`Failed to remove fabric: ${errorMessage}`, {
|
|
1383
|
+
originalError: error instanceof Error ? error : undefined,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
hasFabric(fabricIndex) {
|
|
1388
|
+
const fabrics = this.getFabricInfo();
|
|
1389
|
+
return fabrics.some(f => f.fabricIndex === fabricIndex);
|
|
1390
|
+
}
|
|
1391
|
+
enableStateMonitoring() {
|
|
1392
|
+
this.monitoringEnabled = true;
|
|
1393
|
+
log.debug('Matter state monitoring enabled');
|
|
1394
|
+
}
|
|
1395
|
+
disableStateMonitoring() {
|
|
1396
|
+
this.monitoringEnabled = false;
|
|
1397
|
+
log.debug('Matter state monitoring disabled');
|
|
1398
|
+
}
|
|
1399
|
+
isMonitoringEnabled() {
|
|
1400
|
+
return this.monitoringEnabled;
|
|
1401
|
+
}
|
|
1402
|
+
notifyStateChange(uuid, cluster, state, partId) {
|
|
1403
|
+
if (!this.monitoringEnabled) {
|
|
1404
|
+
log.debug(`State change NOT sent - monitoring disabled for ${uuid}.${cluster}`);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
log.debug(`Sending state change IPC for ${uuid}.${cluster}:`, state);
|
|
1408
|
+
if (process.send) {
|
|
1409
|
+
const event = {
|
|
1410
|
+
type: 'accessoryUpdate',
|
|
1411
|
+
data: {
|
|
1412
|
+
uuid,
|
|
1413
|
+
cluster,
|
|
1414
|
+
state,
|
|
1415
|
+
partId,
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
process.send({
|
|
1419
|
+
id: "matterEvent",
|
|
1420
|
+
data: event,
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
log.warn('Cannot send state change - process.send not available');
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
getDeviceTypeName(deviceType) {
|
|
1428
|
+
return deviceType.name || 'Unknown';
|
|
1429
|
+
}
|
|
1430
|
+
collectAccessories(bridgeUsername, bridgeType, bridgeName) {
|
|
1431
|
+
const accessories = [];
|
|
1432
|
+
for (const [uuid, accessory] of this.accessories.entries()) {
|
|
1433
|
+
const transformed = {
|
|
1434
|
+
uuid,
|
|
1435
|
+
displayName: accessory.displayName,
|
|
1436
|
+
deviceType: this.getDeviceTypeName(accessory.deviceType),
|
|
1437
|
+
clusters: this.getCurrentState(uuid),
|
|
1438
|
+
parts: accessory._parts?.map(part => ({
|
|
1439
|
+
id: part.id,
|
|
1440
|
+
displayName: part.displayName,
|
|
1441
|
+
deviceType: this.getDeviceTypeName(part.deviceType),
|
|
1442
|
+
clusters: this.getCurrentState(uuid, part.id),
|
|
1443
|
+
})),
|
|
1444
|
+
bridge: {
|
|
1445
|
+
username: bridgeUsername,
|
|
1446
|
+
type: bridgeType,
|
|
1447
|
+
name: bridgeName,
|
|
1448
|
+
},
|
|
1449
|
+
};
|
|
1450
|
+
accessories.push(transformed);
|
|
1451
|
+
}
|
|
1452
|
+
return accessories;
|
|
1453
|
+
}
|
|
1454
|
+
getAccessoryInfo(uuid) {
|
|
1455
|
+
const accessory = this.accessories.get(uuid);
|
|
1456
|
+
if (!accessory) {
|
|
1457
|
+
return undefined;
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
uuid,
|
|
1461
|
+
displayName: accessory.displayName,
|
|
1462
|
+
deviceType: this.getDeviceTypeName(accessory.deviceType),
|
|
1463
|
+
clusters: this.getCurrentState(uuid),
|
|
1464
|
+
parts: accessory._parts?.map(part => ({
|
|
1465
|
+
id: part.id,
|
|
1466
|
+
displayName: part.displayName,
|
|
1467
|
+
deviceType: this.getDeviceTypeName(part.deviceType),
|
|
1468
|
+
clusters: this.getCurrentState(uuid, part.id),
|
|
1469
|
+
})),
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
getCurrentState(uuid, partId) {
|
|
1473
|
+
const accessory = this.accessories.get(uuid);
|
|
1474
|
+
if (!accessory) {
|
|
1475
|
+
return {};
|
|
1476
|
+
}
|
|
1477
|
+
if (partId) {
|
|
1478
|
+
const part = accessory._parts?.find(p => p.id === partId);
|
|
1479
|
+
return part?.clusters || {};
|
|
1480
|
+
}
|
|
1481
|
+
return accessory.clusters || {};
|
|
1482
|
+
}
|
|
1483
|
+
async notifyPartsListChanged() {
|
|
1484
|
+
if (!this.aggregator || !this.isCommissioned()) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
try {
|
|
1488
|
+
const aggregatorState = this.aggregator;
|
|
1489
|
+
if (aggregatorState.state?.descriptor) {
|
|
1490
|
+
const partsList = aggregatorState.state.descriptor.partsList || [];
|
|
1491
|
+
if (this.config.debugModeEnabled) {
|
|
1492
|
+
log.debug(`Parts list changed: ${partsList.length} devices (endpoints: ${partsList.join(', ')})`);
|
|
1493
|
+
}
|
|
1494
|
+
await this.aggregator.set({
|
|
1495
|
+
descriptor: {
|
|
1496
|
+
partsList,
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
log.info(`Notified controllers of parts list change (${this.accessories.size} devices)`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1504
|
+
log.warn(`Failed to notify controllers of parts list change: ${errorMessage}`);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
//# sourceMappingURL=server.js.map
|