homebridge 2.0.3-beta.9 → 2.1.0
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/bridgeService.d.ts +78 -6
- package/dist/bridgeService.d.ts.map +1 -1
- package/dist/bridgeService.js +124 -30
- package/dist/bridgeService.js.map +1 -1
- package/dist/childBridgeFork.d.ts +12 -0
- package/dist/childBridgeFork.d.ts.map +1 -1
- package/dist/childBridgeFork.js +56 -25
- package/dist/childBridgeFork.js.map +1 -1
- package/dist/childBridgeService.d.ts +13 -7
- package/dist/childBridgeService.d.ts.map +1 -1
- package/dist/childBridgeService.js +75 -53
- package/dist/childBridgeService.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -4
- package/dist/cli.js.map +1 -1
- package/dist/ipcService.d.ts +6 -1
- package/dist/ipcService.d.ts.map +1 -1
- package/dist/matter/BaseMatterManager.d.ts +13 -0
- package/dist/matter/BaseMatterManager.d.ts.map +1 -1
- package/dist/matter/BaseMatterManager.js +43 -11
- package/dist/matter/BaseMatterManager.js.map +1 -1
- package/dist/matter/ChildBridgeMatterManager.d.ts +41 -2
- package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -1
- package/dist/matter/ChildBridgeMatterManager.js +103 -6
- package/dist/matter/ChildBridgeMatterManager.js.map +1 -1
- package/dist/matter/ChildBridgeMatterMessageHandler.d.ts.map +1 -1
- package/dist/matter/ChildBridgeMatterMessageHandler.js +13 -6
- package/dist/matter/ChildBridgeMatterMessageHandler.js.map +1 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.js +25 -1
- package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -1
- package/dist/matter/MatterAPIImpl.d.ts +17 -0
- package/dist/matter/MatterAPIImpl.d.ts.map +1 -1
- package/dist/matter/MatterAPIImpl.js +29 -0
- package/dist/matter/MatterAPIImpl.js.map +1 -1
- package/dist/matter/MatterBridgeManager.d.ts +38 -3
- package/dist/matter/MatterBridgeManager.d.ts.map +1 -1
- package/dist/matter/MatterBridgeManager.js +115 -12
- package/dist/matter/MatterBridgeManager.js.map +1 -1
- package/dist/matter/MatterError.d.ts +76 -0
- package/dist/matter/MatterError.d.ts.map +1 -0
- package/dist/matter/MatterError.js +90 -0
- package/dist/matter/MatterError.js.map +1 -0
- package/dist/matter/MatterPortAllocator.d.ts.map +1 -1
- package/dist/matter/MatterPortAllocator.js +7 -1
- package/dist/matter/MatterPortAllocator.js.map +1 -1
- package/dist/matter/accessoryCache.d.ts +12 -0
- package/dist/matter/accessoryCache.d.ts.map +1 -1
- package/dist/matter/accessoryCache.js +19 -0
- package/dist/matter/accessoryCache.js.map +1 -1
- package/dist/matter/behaviors/BehaviorRegistry.d.ts +3 -2
- package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -1
- package/dist/matter/behaviors/BehaviorRegistry.js +10 -1
- package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -1
- package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -1
- package/dist/matter/behaviors/DoorLockBehavior.js +10 -4
- package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -1
- package/dist/matter/config.d.ts +73 -1
- package/dist/matter/config.d.ts.map +1 -1
- package/dist/matter/config.js +138 -10
- package/dist/matter/config.js.map +1 -1
- package/dist/matter/configValidator.d.ts.map +1 -1
- package/dist/matter/configValidator.js +15 -0
- package/dist/matter/configValidator.js.map +1 -1
- package/dist/matter/ipc-types.d.ts +7 -0
- package/dist/matter/ipc-types.d.ts.map +1 -1
- package/dist/matter/logFormatter.d.ts.map +1 -1
- package/dist/matter/logFormatter.js +7 -60
- package/dist/matter/logFormatter.js.map +1 -1
- package/dist/matter/managerTypes.d.ts +4 -4
- package/dist/matter/managerTypes.d.ts.map +1 -1
- package/dist/matter/server/AccessoryManager.d.ts.map +1 -1
- package/dist/matter/server/AccessoryManager.js +9 -2
- package/dist/matter/server/AccessoryManager.js.map +1 -1
- package/dist/matter/server/ServerLifecycle.d.ts +23 -4
- package/dist/matter/server/ServerLifecycle.d.ts.map +1 -1
- package/dist/matter/server/ServerLifecycle.js +127 -22
- package/dist/matter/server/ServerLifecycle.js.map +1 -1
- package/dist/matter/server.js +1 -1
- package/dist/matter/server.js.map +1 -1
- package/dist/matter/types.d.ts +21 -57
- package/dist/matter/types.d.ts.map +1 -1
- package/dist/matter/types.js +6 -71
- package/dist/matter/types.js.map +1 -1
- package/dist/matter/utils.d.ts +2 -0
- package/dist/matter/utils.d.ts.map +1 -1
- package/dist/matter/utils.js +3 -1
- package/dist/matter/utils.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -14
- package/dist/plugin.js.map +1 -1
- package/dist/pluginManager.d.ts +11 -0
- package/dist/pluginManager.d.ts.map +1 -1
- package/dist/pluginManager.js +26 -14
- package/dist/pluginManager.js.map +1 -1
- package/dist/server.d.ts +29 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +141 -51
- package/dist/server.js.map +1 -1
- package/package.json +8 -7
package/dist/server.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class Server {
|
|
|
38
38
|
private readonly externalMatterBridgeRegistry;
|
|
39
39
|
private matterMonitoringActive;
|
|
40
40
|
private matterMonitoringClients;
|
|
41
|
+
private readonly pendingMatterAccessoryInfoLookups;
|
|
41
42
|
private serverStatus;
|
|
42
43
|
constructor(options?: HomebridgeOptions);
|
|
43
44
|
/**
|
|
@@ -55,12 +56,16 @@ export declare class Server {
|
|
|
55
56
|
private handleTriggerMatterCommand;
|
|
56
57
|
/**
|
|
57
58
|
* Whether HAP should be published for the given bridge configuration.
|
|
58
|
-
* HAP is on by default; users opt out via `bridge.hap: false`.
|
|
59
|
+
* HAP is on by default; users opt out via `bridge.hap.enabled: false`.
|
|
60
|
+
* In externalsOnly mode the bridge accessory itself is not published, so
|
|
61
|
+
* this returns false there too — externals are handled separately by
|
|
62
|
+
* BridgeService.
|
|
59
63
|
*/
|
|
60
64
|
static isHapEnabled(bridgeConfig: BridgeConfiguration): boolean;
|
|
61
65
|
/**
|
|
62
|
-
* Whether Matter is
|
|
63
|
-
* Matter is opt-in: a `bridge.matter` block must be present
|
|
66
|
+
* Whether Matter is enabled for the given bridge.
|
|
67
|
+
* Matter is opt-in: a `bridge.matter` block must be present and not
|
|
68
|
+
* explicitly disabled via `bridge.matter.enabled: false`.
|
|
64
69
|
*/
|
|
65
70
|
static isMatterEnabledForBridge(bridgeConfig: BridgeConfiguration): boolean;
|
|
66
71
|
private static loadConfig;
|
|
@@ -76,12 +81,20 @@ export declare class Server {
|
|
|
76
81
|
private initializeIpcEventHandlers;
|
|
77
82
|
/**
|
|
78
83
|
* Handle start Matter monitoring request from UI
|
|
79
|
-
* Only starts monitoring if this is the first client
|
|
84
|
+
* Only starts monitoring if this is the first client.
|
|
85
|
+
*
|
|
86
|
+
* The UI parks each `startMatterMonitoring` request under a `correlationId`
|
|
87
|
+
* so it can route the ack back to the matching waiter and gate its first
|
|
88
|
+
* `getMatterAccessories` on it; echo it on the reply so the UI's dispatcher
|
|
89
|
+
* (which drops events without a correlationId) can deliver it.
|
|
80
90
|
*/
|
|
81
91
|
private handleStartMatterMonitoring;
|
|
82
92
|
/**
|
|
83
93
|
* Handle stop Matter monitoring request from UI
|
|
84
|
-
* Only stops monitoring when no more clients
|
|
94
|
+
* Only stops monitoring when no more clients.
|
|
95
|
+
*
|
|
96
|
+
* Echo the request's `correlationId` for the same reason as
|
|
97
|
+
* `handleStartMatterMonitoring`.
|
|
85
98
|
*/
|
|
86
99
|
private handleStopMatterMonitoring;
|
|
87
100
|
/**
|
|
@@ -92,8 +105,17 @@ export declare class Server {
|
|
|
92
105
|
*/
|
|
93
106
|
registerExternalMatterBridge(externalBridgeUsername: string, ownerUsername: string): void;
|
|
94
107
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
108
|
+
* Cancel the pending fallback timer for a forwarded Matter accessory lookup.
|
|
109
|
+
* Called by ChildBridgeService when a child responds with accessoryInfoData
|
|
110
|
+
* so the 2s "Timed out" event isn't sent after a successful response.
|
|
111
|
+
*/
|
|
112
|
+
private cancelPendingMatterAccessoryInfoLookup;
|
|
113
|
+
/**
|
|
114
|
+
* Get Matter accessories for a specific bridge or all bridges.
|
|
115
|
+
*
|
|
116
|
+
* The UI parks each request under a `correlationId` and routes responses
|
|
117
|
+
* back to the matching waiter; events without the original correlationId
|
|
118
|
+
* are dropped, so every emitted `accessoriesData` event must echo it.
|
|
97
119
|
*/
|
|
98
120
|
private handleGetMatterAccessories;
|
|
99
121
|
/**
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAmC,MAAM,oBAAoB,CAAA;AAiB9F,OAAO,EAAsC,UAAU,EAAsB,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,mBAAmB,EAAmC,MAAM,oBAAoB,CAAA;AAiB9F,OAAO,EAAsC,UAAU,EAAsB,MAAM,iBAAiB,CAAA;AAUpG,MAAM,WAAW,iBAAiB;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAA;IACvC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,sBAAsB,CAAC,EAAE,OAAO,CAAA;CACjC;AAGD,0BAAkB,YAAY;IAC5B;;OAEG;IACH,OAAO,YAAY;IAEnB;;OAEG;IACH,EAAE,OAAO;IAET;;OAEG;IACH,IAAI,SAAS;CACd;AAED,qBAAa,MAAM;IAmCf,OAAO,CAAC,OAAO;IAlCjB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;IAE/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAIzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6C;IAI1E,OAAO,CAAC,aAAa,CAAC,CAAqB;IAK3C,OAAO,CAAC,QAAQ,CAAC,4BAA4B,CAAiC;IAG9E,OAAO,CAAC,sBAAsB,CAAQ;IACtC,OAAO,CAAC,uBAAuB,CAAI;IAKnC,OAAO,CAAC,QAAQ,CAAC,iCAAiC,CAAwD;IAG1G,OAAO,CAAC,YAAY,CAAqC;gBAG/C,OAAO,GAAE,iBAAsB;IA8DzC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAsBV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2FtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBtC,OAAO,CAAC,aAAa;IAKrB;;;OAGG;YACW,0BAA0B;IAOxC;;;;;;OAMG;WACW,YAAY,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO;IAItE;;;;OAIG;WACW,wBAAwB,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO;IAIlF,OAAO,CAAC,MAAM,CAAC,UAAU;IAoGzB,OAAO,CAAC,eAAe;IAyGvB,OAAO,CAAC,aAAa;IAiGrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IA4DjC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA4DlC;;;;;;;;OAQG;IACH,OAAO,CAAC,2BAA2B;IAiCnC;;;;;;OAMG;IACH,OAAO,CAAC,0BAA0B;IA8ClC;;;;;OAKG;IACI,4BAA4B,CAAC,sBAAsB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAOhG;;;;OAIG;IACH,OAAO,CAAC,sCAAsC;IAQ9C;;;;;;OAMG;YACW,0BAA0B;IAyExC;;OAEG;IACH,OAAO,CAAC,4BAA4B;IAuFpC;;OAEG;YACW,4BAA4B;IAwK1C,OAAO,CAAC,cAAc;CAqBvB"}
|
package/dist/server.js
CHANGED
|
@@ -3,19 +3,15 @@ import process from 'node:process';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import qrcode from 'qrcode-terminal';
|
|
5
5
|
import { HomebridgeAPI } from './api.js';
|
|
6
|
-
import { BridgeService } from './bridgeService.js';
|
|
6
|
+
import { BridgeService, isHapConfigEnabled, isHapExternalsOnly, validateHapConfig } from './bridgeService.js';
|
|
7
7
|
import { ChildBridgeService } from './childBridgeService.js';
|
|
8
8
|
import { ExternalPortService } from './externalPortService.js';
|
|
9
9
|
import { IpcService } from './ipcService.js';
|
|
10
10
|
import { Logger } from './logger.js';
|
|
11
|
-
import { MatterConfigCollector } from './matter/config.js';
|
|
11
|
+
import { isMatterActive, isMatterConfigEnabled, MatterConfigCollector } from './matter/config.js';
|
|
12
12
|
import { PluginManager } from './pluginManager.js';
|
|
13
13
|
import { User } from './user.js';
|
|
14
14
|
import { validMacAddress } from './util/mac.js';
|
|
15
|
-
// HAP specifies QR error-correction level M or higher for ECC. Set once at
|
|
16
|
-
// module load — qrcode-terminal stores this on the (process-global) module,
|
|
17
|
-
// so re-setting it on every printSetupInfo call was redundant.
|
|
18
|
-
qrcode.setErrorLevel('M');
|
|
19
15
|
const log = Logger.internal;
|
|
20
16
|
const matterLogger = Logger.withPrefix('Matter/MainManager');
|
|
21
17
|
// eslint-disable-next-line no-restricted-syntax
|
|
@@ -55,6 +51,10 @@ export class Server {
|
|
|
55
51
|
// Matter monitoring state (for UI accessories page)
|
|
56
52
|
matterMonitoringActive = false;
|
|
57
53
|
matterMonitoringClients = 0;
|
|
54
|
+
// Fallback timers for child-bridge Matter accessory lookups. Keyed by uuid
|
|
55
|
+
// so that a child's accessoryInfoData (success or error) can cancel the
|
|
56
|
+
// timer before it fires a spurious "Timed out" event at the UI.
|
|
57
|
+
pendingMatterAccessoryInfoLookups = new Map();
|
|
58
58
|
// current server status
|
|
59
59
|
serverStatus = "pending" /* ServerStatus.PENDING */;
|
|
60
60
|
constructor(options = {}) {
|
|
@@ -80,6 +80,7 @@ export class Server {
|
|
|
80
80
|
const bridgeConfig = {
|
|
81
81
|
cachedAccessoriesDir: User.cachedAccessoryPath(),
|
|
82
82
|
cachedAccessoriesItemName: 'cachedAccessories',
|
|
83
|
+
externalAccessoriesItemName: 'externalAccessories',
|
|
83
84
|
};
|
|
84
85
|
// shallow copy the homebridge options to the bridge options object
|
|
85
86
|
Object.assign(bridgeConfig, this.options);
|
|
@@ -178,9 +179,16 @@ export class Server {
|
|
|
178
179
|
this.publishBridge();
|
|
179
180
|
}
|
|
180
181
|
else {
|
|
181
|
-
// HAP is opted out
|
|
182
|
-
//
|
|
183
|
-
|
|
182
|
+
// HAP is opted out (or externalsOnly mode is set). The bridge ADVERTISED
|
|
183
|
+
// listener won't fire for the bridge itself, so move server status to OK
|
|
184
|
+
// explicitly. Matter may or may not be up — if both protocols are
|
|
185
|
+
// suppressed the bridge simply advertises nothing of its own.
|
|
186
|
+
if (isHapExternalsOnly(this.config.bridge.hap)) {
|
|
187
|
+
log.info('HAP externalsOnly mode for the main bridge; bridge accessory will not publish but external accessories will.');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
log.info('HAP is disabled for the main bridge (bridge.hap.enabled=false); skipping HAP publish.');
|
|
191
|
+
}
|
|
184
192
|
this.setServerStatus("ok" /* ServerStatus.OK */);
|
|
185
193
|
}
|
|
186
194
|
}
|
|
@@ -188,6 +196,14 @@ export class Server {
|
|
|
188
196
|
this.bridgeService.teardown();
|
|
189
197
|
// Teardown Matter servers (main bridge and external accessories)
|
|
190
198
|
await this.matterManager?.teardown();
|
|
199
|
+
// Cancel any in-flight Matter accessory info fallback timers so they
|
|
200
|
+
// don't fire `accessoryInfoData` events at the IPC channel after the
|
|
201
|
+
// service has stopped. The timers are already unref()'d so they don't
|
|
202
|
+
// hold the loop open — this is for tidiness, not a real leak.
|
|
203
|
+
for (const timer of this.pendingMatterAccessoryInfoLookups.values()) {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
}
|
|
206
|
+
this.pendingMatterAccessoryInfoLookups.clear();
|
|
191
207
|
this.ipcService.stop();
|
|
192
208
|
this.setServerStatus("down" /* ServerStatus.DOWN */);
|
|
193
209
|
}
|
|
@@ -207,17 +223,21 @@ export class Server {
|
|
|
207
223
|
}
|
|
208
224
|
/**
|
|
209
225
|
* Whether HAP should be published for the given bridge configuration.
|
|
210
|
-
* HAP is on by default; users opt out via `bridge.hap: false`.
|
|
226
|
+
* HAP is on by default; users opt out via `bridge.hap.enabled: false`.
|
|
227
|
+
* In externalsOnly mode the bridge accessory itself is not published, so
|
|
228
|
+
* this returns false there too — externals are handled separately by
|
|
229
|
+
* BridgeService.
|
|
211
230
|
*/
|
|
212
231
|
static isHapEnabled(bridgeConfig) {
|
|
213
|
-
return bridgeConfig.hap
|
|
232
|
+
return isHapConfigEnabled(bridgeConfig.hap) && !isHapExternalsOnly(bridgeConfig.hap);
|
|
214
233
|
}
|
|
215
234
|
/**
|
|
216
|
-
* Whether Matter is
|
|
217
|
-
* Matter is opt-in: a `bridge.matter` block must be present
|
|
235
|
+
* Whether Matter is enabled for the given bridge.
|
|
236
|
+
* Matter is opt-in: a `bridge.matter` block must be present and not
|
|
237
|
+
* explicitly disabled via `bridge.matter.enabled: false`.
|
|
218
238
|
*/
|
|
219
239
|
static isMatterEnabledForBridge(bridgeConfig) {
|
|
220
|
-
return
|
|
240
|
+
return isMatterConfigEnabled(bridgeConfig.matter);
|
|
221
241
|
}
|
|
222
242
|
static loadConfig() {
|
|
223
243
|
// Look for the configuration file
|
|
@@ -262,24 +282,24 @@ export class Server {
|
|
|
262
282
|
bridge.username = bridge.username || defaultBridge.username;
|
|
263
283
|
bridge.pin = bridge.pin || defaultBridge.pin;
|
|
264
284
|
config.bridge = bridge;
|
|
265
|
-
// Protocol-enablement validation: at least one of HAP or Matter must be on.
|
|
266
|
-
// HAP is enabled by default; users opt out via `bridge.hap: false`.
|
|
267
|
-
// Matter is enabled when `bridge.matter` is configured.
|
|
268
|
-
if (!Server.isHapEnabled(config.bridge) && !Server.isMatterEnabledForBridge(config.bridge)) {
|
|
269
|
-
throw new Error('At least one protocol (HAP or Matter) must be enabled. '
|
|
270
|
-
+ 'Set `bridge.hap` to true or add a `bridge.matter` configuration.');
|
|
271
|
-
}
|
|
272
285
|
// Validate Matter port pool configuration. Must run after bridge defaults
|
|
273
286
|
// are filled in, since the cast to HomebridgeConfig only becomes honest at
|
|
274
287
|
// that point.
|
|
275
288
|
MatterConfigCollector.validateMatterPortsPool(config);
|
|
276
289
|
// Normalise the main bridge username to uppercase so downstream comparisons
|
|
277
290
|
// (validMacAddress, registry lookups, child-bridge dedup) stay case-consistent.
|
|
278
|
-
|
|
291
|
+
// Guarded so a malformed (non-string) value falls through to `validMacAddress`
|
|
292
|
+
// below and produces the proper "Not a valid username" error rather than a
|
|
293
|
+
// raw TypeError from calling toUpperCase on a number/boolean.
|
|
294
|
+
if (typeof config.bridge.username === 'string') {
|
|
295
|
+
config.bridge.username = config.bridge.username.toUpperCase();
|
|
296
|
+
}
|
|
279
297
|
const username = config.bridge.username;
|
|
280
298
|
if (!validMacAddress(username)) {
|
|
281
299
|
throw new Error(`Not a valid username: ${username}. Must be 6 pairs of colon-separated hexadecimal chars (A-F 0-9), like a MAC address.`);
|
|
282
300
|
}
|
|
301
|
+
// Validate the main bridge HAP config (shape + externalsOnly/enabled coherence).
|
|
302
|
+
validateHapConfig(config.bridge, { bridgeLabel: 'main bridge' });
|
|
283
303
|
config.accessories = config.accessories || [];
|
|
284
304
|
config.platforms = config.platforms || [];
|
|
285
305
|
if (!Array.isArray(config.accessories)) {
|
|
@@ -365,6 +385,8 @@ export class Server {
|
|
|
365
385
|
childBridge = new ChildBridgeService("accessory" /* PluginType.ACCESSORY */, accessoryIdentifier, plugin, accessoryConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
366
386
|
// Set callback for external Matter bridge registration
|
|
367
387
|
childBridge.onExternalBridgeRegistered = this.registerExternalMatterBridge.bind(this);
|
|
388
|
+
// Cancel the parent-side fallback timer when this child answers a lookup
|
|
389
|
+
childBridge.onAccessoryInfoResponse = this.cancelPendingMatterAccessoryInfoLookup.bind(this);
|
|
368
390
|
this.childBridges.set(accessoryConfig._bridge.username, childBridge);
|
|
369
391
|
}
|
|
370
392
|
// add config to child bridge service
|
|
@@ -440,6 +462,8 @@ export class Server {
|
|
|
440
462
|
const childBridge = new ChildBridgeService("platform" /* PluginType.PLATFORM */, platformIdentifier, plugin, platformConfig._bridge, this.config, this.options, this.api, this.ipcService, this.externalPortService);
|
|
441
463
|
// Set callback for external Matter bridge registration
|
|
442
464
|
childBridge.onExternalBridgeRegistered = this.registerExternalMatterBridge.bind(this);
|
|
465
|
+
// Cancel the parent-side fallback timer when this child answers a lookup
|
|
466
|
+
childBridge.onAccessoryInfoResponse = this.cancelPendingMatterAccessoryInfoLookup.bind(this);
|
|
443
467
|
this.childBridges.set(platformConfig._bridge.username, childBridge);
|
|
444
468
|
// add config to child bridge service
|
|
445
469
|
childBridge.addConfig(platformConfig);
|
|
@@ -468,15 +492,13 @@ export class Server {
|
|
|
468
492
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
469
493
|
+ 'Missing required field "_bridge.username".');
|
|
470
494
|
}
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
// in
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
+ 'at least one protocol must be enabled on this child bridge. '
|
|
479
|
-
+ 'Set `_bridge.hap` to true or add a `_bridge.matter` configuration.');
|
|
495
|
+
// Normalise the child username to uppercase, mirroring the main bridge
|
|
496
|
+
// (loadConfig). validMacAddress only accepts A-F, so without this a lowercase
|
|
497
|
+
// MAC in _bridge.username would be rejected here even though the identical
|
|
498
|
+
// value is accepted on the main bridge. Guarded so a non-string value still
|
|
499
|
+
// falls through to the proper "not a valid username" error below.
|
|
500
|
+
if (typeof bridgeConfig.username === 'string') {
|
|
501
|
+
bridgeConfig.username = bridgeConfig.username.toUpperCase();
|
|
480
502
|
}
|
|
481
503
|
if (!validMacAddress(bridgeConfig.username)) {
|
|
482
504
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
@@ -495,10 +517,19 @@ export class Server {
|
|
|
495
517
|
+ `Duplicate username found in _bridge.username: "${bridgeConfig.username}". You can only group accessories of the same type in a child bridge.`);
|
|
496
518
|
}
|
|
497
519
|
}
|
|
498
|
-
|
|
520
|
+
// Both usernames are normalised to uppercase (main in loadConfig, child
|
|
521
|
+
// above), so a direct comparison is case-consistent.
|
|
522
|
+
if (bridgeConfig.username === this.config.bridge.username) {
|
|
499
523
|
throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
|
|
500
524
|
+ `Username found in _bridge.username: "${bridgeConfig.username}" is the same as the main bridge. Each child bridge platform/accessory must have it's own unique username.`);
|
|
501
525
|
}
|
|
526
|
+
// Validate the child bridge HAP config (shape + externalsOnly/enabled coherence).
|
|
527
|
+
// For accessory child bridges, `hap.externalsOnly` is stripped with a warning
|
|
528
|
+
// since externals are not supported via the accessory plugin API.
|
|
529
|
+
validateHapConfig(bridgeConfig, {
|
|
530
|
+
bridgeLabel: `${type} "${identifier}" child bridge`,
|
|
531
|
+
isAccessoryPlugin: type === "accessory" /* PluginType.ACCESSORY */,
|
|
532
|
+
});
|
|
502
533
|
}
|
|
503
534
|
/**
|
|
504
535
|
* Takes care of the IPC Events sent to Homebridge
|
|
@@ -534,14 +565,14 @@ export class Server {
|
|
|
534
565
|
this.ipcService.sendMessage("childBridgeMetadataResponse" /* IpcOutgoingEvent.CHILD_BRIDGE_METADATA_RESPONSE */, Array.from(this.childBridges.values(), x => x.getMetadata()));
|
|
535
566
|
});
|
|
536
567
|
// Matter monitoring lifecycle handlers
|
|
537
|
-
this.ipcService.on("startMatterMonitoring" /* IpcIncomingEvent.START_MATTER_MONITORING */, () => {
|
|
538
|
-
this.handleStartMatterMonitoring();
|
|
568
|
+
this.ipcService.on("startMatterMonitoring" /* IpcIncomingEvent.START_MATTER_MONITORING */, (data) => {
|
|
569
|
+
this.handleStartMatterMonitoring(data);
|
|
539
570
|
});
|
|
540
|
-
this.ipcService.on("stopMatterMonitoring" /* IpcIncomingEvent.STOP_MATTER_MONITORING */, () => {
|
|
541
|
-
this.handleStopMatterMonitoring();
|
|
571
|
+
this.ipcService.on("stopMatterMonitoring" /* IpcIncomingEvent.STOP_MATTER_MONITORING */, (data) => {
|
|
572
|
+
this.handleStopMatterMonitoring(data);
|
|
542
573
|
});
|
|
543
574
|
this.ipcService.on("getMatterAccessories" /* IpcIncomingEvent.GET_MATTER_ACCESSORIES */, (data) => {
|
|
544
|
-
void this.handleGetMatterAccessories(data
|
|
575
|
+
void this.handleGetMatterAccessories(data);
|
|
545
576
|
});
|
|
546
577
|
this.ipcService.on("getMatterAccessoryInfo" /* IpcIncomingEvent.GET_MATTER_ACCESSORY_INFO */, (data) => {
|
|
547
578
|
this.handleGetMatterAccessoryInfo(data?.uuid);
|
|
@@ -552,9 +583,15 @@ export class Server {
|
|
|
552
583
|
}
|
|
553
584
|
/**
|
|
554
585
|
* Handle start Matter monitoring request from UI
|
|
555
|
-
* Only starts monitoring if this is the first client
|
|
586
|
+
* Only starts monitoring if this is the first client.
|
|
587
|
+
*
|
|
588
|
+
* The UI parks each `startMatterMonitoring` request under a `correlationId`
|
|
589
|
+
* so it can route the ack back to the matching waiter and gate its first
|
|
590
|
+
* `getMatterAccessories` on it; echo it on the reply so the UI's dispatcher
|
|
591
|
+
* (which drops events without a correlationId) can deliver it.
|
|
556
592
|
*/
|
|
557
|
-
handleStartMatterMonitoring() {
|
|
593
|
+
handleStartMatterMonitoring(data) {
|
|
594
|
+
const correlationId = data?.correlationId;
|
|
558
595
|
this.matterMonitoringClients++;
|
|
559
596
|
// Only setup monitoring if this is the first client
|
|
560
597
|
if (this.matterMonitoringClients === 1) {
|
|
@@ -567,6 +604,7 @@ export class Server {
|
|
|
567
604
|
}
|
|
568
605
|
const event = {
|
|
569
606
|
type: 'monitoringStarted',
|
|
607
|
+
correlationId,
|
|
570
608
|
data: { success: true },
|
|
571
609
|
};
|
|
572
610
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
@@ -575,6 +613,7 @@ export class Server {
|
|
|
575
613
|
// Already monitoring, just acknowledge
|
|
576
614
|
const event = {
|
|
577
615
|
type: 'monitoringStarted',
|
|
616
|
+
correlationId,
|
|
578
617
|
data: { success: true, alreadyActive: true },
|
|
579
618
|
};
|
|
580
619
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
@@ -582,14 +621,19 @@ export class Server {
|
|
|
582
621
|
}
|
|
583
622
|
/**
|
|
584
623
|
* Handle stop Matter monitoring request from UI
|
|
585
|
-
* Only stops monitoring when no more clients
|
|
624
|
+
* Only stops monitoring when no more clients.
|
|
625
|
+
*
|
|
626
|
+
* Echo the request's `correlationId` for the same reason as
|
|
627
|
+
* `handleStartMatterMonitoring`.
|
|
586
628
|
*/
|
|
587
|
-
handleStopMatterMonitoring() {
|
|
629
|
+
handleStopMatterMonitoring(data) {
|
|
630
|
+
const correlationId = data?.correlationId;
|
|
588
631
|
if (this.matterMonitoringClients <= 0) {
|
|
589
632
|
// Nothing to do, but still acknowledge so the UI doesn't sit waiting
|
|
590
633
|
// for a confirmation event that never comes.
|
|
591
634
|
const event = {
|
|
592
635
|
type: 'monitoringStopped',
|
|
636
|
+
correlationId,
|
|
593
637
|
data: { success: true, alreadyStopped: true },
|
|
594
638
|
};
|
|
595
639
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
@@ -607,6 +651,7 @@ export class Server {
|
|
|
607
651
|
}
|
|
608
652
|
const event = {
|
|
609
653
|
type: 'monitoringStopped',
|
|
654
|
+
correlationId,
|
|
610
655
|
data: { success: true },
|
|
611
656
|
};
|
|
612
657
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
@@ -615,6 +660,7 @@ export class Server {
|
|
|
615
660
|
// Other clients still monitoring
|
|
616
661
|
const event = {
|
|
617
662
|
type: 'monitoringStopped',
|
|
663
|
+
correlationId,
|
|
618
664
|
data: { success: true, othersActive: true },
|
|
619
665
|
};
|
|
620
666
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
@@ -633,15 +679,33 @@ export class Server {
|
|
|
633
679
|
this.externalMatterBridgeRegistry.set(normalizedExternal, normalizedOwner);
|
|
634
680
|
}
|
|
635
681
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
682
|
+
* Cancel the pending fallback timer for a forwarded Matter accessory lookup.
|
|
683
|
+
* Called by ChildBridgeService when a child responds with accessoryInfoData
|
|
684
|
+
* so the 2s "Timed out" event isn't sent after a successful response.
|
|
685
|
+
*/
|
|
686
|
+
cancelPendingMatterAccessoryInfoLookup(uuid) {
|
|
687
|
+
const timer = this.pendingMatterAccessoryInfoLookups.get(uuid);
|
|
688
|
+
if (timer) {
|
|
689
|
+
clearTimeout(timer);
|
|
690
|
+
this.pendingMatterAccessoryInfoLookups.delete(uuid);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get Matter accessories for a specific bridge or all bridges.
|
|
695
|
+
*
|
|
696
|
+
* The UI parks each request under a `correlationId` and routes responses
|
|
697
|
+
* back to the matching waiter; events without the original correlationId
|
|
698
|
+
* are dropped, so every emitted `accessoriesData` event must echo it.
|
|
638
699
|
*/
|
|
639
|
-
async handleGetMatterAccessories(
|
|
700
|
+
async handleGetMatterAccessories(data) {
|
|
701
|
+
const bridgeUsername = data?.bridgeUsername;
|
|
702
|
+
const correlationId = data?.correlationId;
|
|
640
703
|
// Check if monitoring is active
|
|
641
704
|
if (!this.matterMonitoringActive) {
|
|
642
705
|
matterLogger.warn('Matter monitoring not active - cannot get accessories');
|
|
643
706
|
const event = {
|
|
644
707
|
type: 'accessoriesData',
|
|
708
|
+
correlationId,
|
|
645
709
|
data: {
|
|
646
710
|
bridgeUsername,
|
|
647
711
|
error: 'Matter monitoring not active',
|
|
@@ -654,6 +718,7 @@ export class Server {
|
|
|
654
718
|
if (!this.api.isMatterEnabled() && this.childBridges.size === 0) {
|
|
655
719
|
const event = {
|
|
656
720
|
type: 'accessoriesData',
|
|
721
|
+
correlationId,
|
|
657
722
|
data: {
|
|
658
723
|
bridgeUsername,
|
|
659
724
|
accessories: [],
|
|
@@ -676,6 +741,7 @@ export class Server {
|
|
|
676
741
|
}
|
|
677
742
|
const event = {
|
|
678
743
|
type: 'accessoriesData',
|
|
744
|
+
correlationId,
|
|
679
745
|
data: {
|
|
680
746
|
bridgeUsername: bridgeUsername || 'all',
|
|
681
747
|
accessories: allAccessories,
|
|
@@ -687,6 +753,7 @@ export class Server {
|
|
|
687
753
|
matterLogger.error('Failed to get Matter accessories:', error);
|
|
688
754
|
const event = {
|
|
689
755
|
type: 'accessoriesData',
|
|
756
|
+
correlationId,
|
|
690
757
|
data: {
|
|
691
758
|
bridgeUsername,
|
|
692
759
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
@@ -720,13 +787,19 @@ export class Server {
|
|
|
720
787
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, event);
|
|
721
788
|
return;
|
|
722
789
|
}
|
|
723
|
-
// If not found on main bridge, forward to child bridges
|
|
790
|
+
// If not found on main bridge, forward to child bridges whose Matter is
|
|
791
|
+
// actually active. A child with `matter: { enabled: false }` still carries
|
|
792
|
+
// a matterConfig block but never starts a Matter message handler, so it
|
|
793
|
+
// would never answer — forwarding to it would only make the UI wait out
|
|
794
|
+
// the 2s fallback instead of getting an immediate "not found". Gate on
|
|
795
|
+
// isMatterActive (enabled or externalsOnly), which mirrors the condition
|
|
796
|
+
// under which the child actually creates its Matter handler.
|
|
724
797
|
// The matching child responds directly to the UI via the existing
|
|
725
798
|
// MATTER_EVENT forwarding path; schedule a fallback error so the UI
|
|
726
799
|
// doesn't hang if no child knows the UUID either.
|
|
727
800
|
let forwardedToChildren = false;
|
|
728
801
|
for (const childBridge of this.childBridges.values()) {
|
|
729
|
-
if (childBridge.getMetadata().matterConfig) {
|
|
802
|
+
if (isMatterActive(childBridge.getMetadata().matterConfig)) {
|
|
730
803
|
childBridge.getMatterAccessoryInfo(uuid);
|
|
731
804
|
forwardedToChildren = true;
|
|
732
805
|
}
|
|
@@ -740,8 +813,17 @@ export class Server {
|
|
|
740
813
|
}
|
|
741
814
|
// 2s is comfortably longer than a healthy child response and short
|
|
742
815
|
// enough that the UI doesn't feel stuck. Use unref() so a late
|
|
743
|
-
// shutdown doesn't wait on this timer.
|
|
744
|
-
|
|
816
|
+
// shutdown doesn't wait on this timer. The timer is registered in
|
|
817
|
+
// pendingMatterAccessoryInfoLookups so a child's accessoryInfoData
|
|
818
|
+
// response (routed via ChildBridgeService.onAccessoryInfoResponse) can
|
|
819
|
+
// cancel it before it fires a spurious timed-out event. A second
|
|
820
|
+
// concurrent request for the same uuid replaces the existing timer.
|
|
821
|
+
const existing = this.pendingMatterAccessoryInfoLookups.get(uuid);
|
|
822
|
+
if (existing) {
|
|
823
|
+
clearTimeout(existing);
|
|
824
|
+
}
|
|
825
|
+
const fallback = setTimeout(() => {
|
|
826
|
+
this.pendingMatterAccessoryInfoLookups.delete(uuid);
|
|
745
827
|
this.ipcService.sendMessage("matterEvent" /* IpcOutgoingEvent.MATTER_EVENT */, {
|
|
746
828
|
type: 'accessoryInfoData',
|
|
747
829
|
data: {
|
|
@@ -750,7 +832,9 @@ export class Server {
|
|
|
750
832
|
timedOut: true,
|
|
751
833
|
},
|
|
752
834
|
});
|
|
753
|
-
}, 2000)
|
|
835
|
+
}, 2000);
|
|
836
|
+
fallback.unref();
|
|
837
|
+
this.pendingMatterAccessoryInfoLookups.set(uuid, fallback);
|
|
754
838
|
}
|
|
755
839
|
catch (error) {
|
|
756
840
|
matterLogger.error('Failed to get Matter accessory info:', error);
|
|
@@ -896,8 +980,13 @@ export class Server {
|
|
|
896
980
|
});
|
|
897
981
|
}
|
|
898
982
|
catch (error) {
|
|
899
|
-
// Main bridge doesn't have accessory - forward to child bridges
|
|
900
|
-
|
|
983
|
+
// Main bridge doesn't have accessory - forward to child bridges whose
|
|
984
|
+
// Matter is actually active. A child with `matter: { enabled: false }`
|
|
985
|
+
// still carries a matterConfig block but never starts a Matter handler,
|
|
986
|
+
// so forwarding a control request to it would just be dropped. Gate on
|
|
987
|
+
// isMatterActive (enabled or externalsOnly) — the same condition under
|
|
988
|
+
// which the child creates its Matter handler.
|
|
989
|
+
const matterChildBridges = [...this.childBridges.values()].filter(bridge => isMatterActive(bridge.getMetadata().matterConfig));
|
|
901
990
|
if (matterChildBridges.length > 0) {
|
|
902
991
|
matterLogger.debug(`Main bridge doesn't have accessory ${data.uuid}, forwarding to ${matterChildBridges.length} child bridge(s) with Matter enabled`);
|
|
903
992
|
for (const childBridge of matterChildBridges) {
|
|
@@ -923,6 +1012,7 @@ export class Server {
|
|
|
923
1012
|
console.log(this.bridgeService.bridge.setupURI());
|
|
924
1013
|
if (!this.options.hideQRCode) {
|
|
925
1014
|
console.log('Scan this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
1015
|
+
qrcode.setErrorLevel('M'); // HAP specifies level M or higher for ECC
|
|
926
1016
|
qrcode.generate(this.bridgeService.bridge.setupURI());
|
|
927
1017
|
console.log('Or enter this code with your HomeKit app on your iOS device to pair with Homebridge:');
|
|
928
1018
|
}
|