homebridge 2.0.3-beta.8 → 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.
Files changed (100) hide show
  1. package/dist/bridgeService.d.ts +78 -6
  2. package/dist/bridgeService.d.ts.map +1 -1
  3. package/dist/bridgeService.js +124 -30
  4. package/dist/bridgeService.js.map +1 -1
  5. package/dist/childBridgeFork.d.ts +12 -0
  6. package/dist/childBridgeFork.d.ts.map +1 -1
  7. package/dist/childBridgeFork.js +56 -25
  8. package/dist/childBridgeFork.js.map +1 -1
  9. package/dist/childBridgeService.d.ts +13 -7
  10. package/dist/childBridgeService.d.ts.map +1 -1
  11. package/dist/childBridgeService.js +75 -53
  12. package/dist/childBridgeService.js.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +1 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/ipcService.d.ts +6 -1
  17. package/dist/ipcService.d.ts.map +1 -1
  18. package/dist/matter/BaseMatterManager.d.ts +13 -0
  19. package/dist/matter/BaseMatterManager.d.ts.map +1 -1
  20. package/dist/matter/BaseMatterManager.js +43 -11
  21. package/dist/matter/BaseMatterManager.js.map +1 -1
  22. package/dist/matter/ChildBridgeMatterManager.d.ts +41 -2
  23. package/dist/matter/ChildBridgeMatterManager.d.ts.map +1 -1
  24. package/dist/matter/ChildBridgeMatterManager.js +103 -6
  25. package/dist/matter/ChildBridgeMatterManager.js.map +1 -1
  26. package/dist/matter/ChildBridgeMatterMessageHandler.d.ts.map +1 -1
  27. package/dist/matter/ChildBridgeMatterMessageHandler.js +13 -6
  28. package/dist/matter/ChildBridgeMatterMessageHandler.js.map +1 -1
  29. package/dist/matter/ExternalMatterAccessoryPublisher.d.ts.map +1 -1
  30. package/dist/matter/ExternalMatterAccessoryPublisher.js +25 -1
  31. package/dist/matter/ExternalMatterAccessoryPublisher.js.map +1 -1
  32. package/dist/matter/MatterAPIImpl.d.ts +17 -0
  33. package/dist/matter/MatterAPIImpl.d.ts.map +1 -1
  34. package/dist/matter/MatterAPIImpl.js +29 -0
  35. package/dist/matter/MatterAPIImpl.js.map +1 -1
  36. package/dist/matter/MatterBridgeManager.d.ts +38 -3
  37. package/dist/matter/MatterBridgeManager.d.ts.map +1 -1
  38. package/dist/matter/MatterBridgeManager.js +115 -12
  39. package/dist/matter/MatterBridgeManager.js.map +1 -1
  40. package/dist/matter/MatterError.d.ts +76 -0
  41. package/dist/matter/MatterError.d.ts.map +1 -0
  42. package/dist/matter/MatterError.js +90 -0
  43. package/dist/matter/MatterError.js.map +1 -0
  44. package/dist/matter/MatterPortAllocator.d.ts.map +1 -1
  45. package/dist/matter/MatterPortAllocator.js +7 -1
  46. package/dist/matter/MatterPortAllocator.js.map +1 -1
  47. package/dist/matter/accessoryCache.d.ts +12 -0
  48. package/dist/matter/accessoryCache.d.ts.map +1 -1
  49. package/dist/matter/accessoryCache.js +19 -0
  50. package/dist/matter/accessoryCache.js.map +1 -1
  51. package/dist/matter/behaviors/BehaviorRegistry.d.ts +3 -2
  52. package/dist/matter/behaviors/BehaviorRegistry.d.ts.map +1 -1
  53. package/dist/matter/behaviors/BehaviorRegistry.js +10 -1
  54. package/dist/matter/behaviors/BehaviorRegistry.js.map +1 -1
  55. package/dist/matter/behaviors/DoorLockBehavior.d.ts.map +1 -1
  56. package/dist/matter/behaviors/DoorLockBehavior.js +10 -4
  57. package/dist/matter/behaviors/DoorLockBehavior.js.map +1 -1
  58. package/dist/matter/config.d.ts +73 -1
  59. package/dist/matter/config.d.ts.map +1 -1
  60. package/dist/matter/config.js +138 -10
  61. package/dist/matter/config.js.map +1 -1
  62. package/dist/matter/configValidator.d.ts.map +1 -1
  63. package/dist/matter/configValidator.js +15 -0
  64. package/dist/matter/configValidator.js.map +1 -1
  65. package/dist/matter/ipc-types.d.ts +7 -0
  66. package/dist/matter/ipc-types.d.ts.map +1 -1
  67. package/dist/matter/logFormatter.d.ts.map +1 -1
  68. package/dist/matter/logFormatter.js +7 -60
  69. package/dist/matter/logFormatter.js.map +1 -1
  70. package/dist/matter/managerTypes.d.ts +4 -4
  71. package/dist/matter/managerTypes.d.ts.map +1 -1
  72. package/dist/matter/server/AccessoryManager.d.ts.map +1 -1
  73. package/dist/matter/server/AccessoryManager.js +9 -2
  74. package/dist/matter/server/AccessoryManager.js.map +1 -1
  75. package/dist/matter/server/ServerLifecycle.d.ts +23 -4
  76. package/dist/matter/server/ServerLifecycle.d.ts.map +1 -1
  77. package/dist/matter/server/ServerLifecycle.js +127 -22
  78. package/dist/matter/server/ServerLifecycle.js.map +1 -1
  79. package/dist/matter/server.js +1 -1
  80. package/dist/matter/server.js.map +1 -1
  81. package/dist/matter/types.d.ts +21 -57
  82. package/dist/matter/types.d.ts.map +1 -1
  83. package/dist/matter/types.js +6 -71
  84. package/dist/matter/types.js.map +1 -1
  85. package/dist/matter/utils.d.ts +2 -0
  86. package/dist/matter/utils.d.ts.map +1 -1
  87. package/dist/matter/utils.js +2 -0
  88. package/dist/matter/utils.js.map +1 -1
  89. package/dist/plugin.d.ts.map +1 -1
  90. package/dist/plugin.js +2 -14
  91. package/dist/plugin.js.map +1 -1
  92. package/dist/pluginManager.d.ts +11 -0
  93. package/dist/pluginManager.d.ts.map +1 -1
  94. package/dist/pluginManager.js +26 -14
  95. package/dist/pluginManager.js.map +1 -1
  96. package/dist/server.d.ts +29 -7
  97. package/dist/server.d.ts.map +1 -1
  98. package/dist/server.js +141 -51
  99. package/dist/server.js.map +1 -1
  100. 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 configured for the given bridge.
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
- * Get Matter accessories for a specific bridge or all bridges
96
- * @param bridgeUsername - Optional: specific bridge username (MAC format)
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
  /**
@@ -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;AAepG,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;IA8Bf,OAAO,CAAC,OAAO;IA7BjB,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;IAGnC,OAAO,CAAC,YAAY,CAAqC;gBAG/C,OAAO,GAAE,iBAAsB;IA6DzC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAsBV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqFtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtC,OAAO,CAAC,aAAa;IAKrB;;;OAGG;YACW,0BAA0B;IAOxC;;;OAGG;WACW,YAAY,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO;IAItE;;;OAGG;WACW,wBAAwB,CAAC,YAAY,EAAE,mBAAmB,GAAG,OAAO;IAIlF,OAAO,CAAC,MAAM,CAAC,UAAU;IAsGzB,OAAO,CAAC,eAAe;IAuGvB,OAAO,CAAC,aAAa;IA+FrB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAsDjC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA4DlC;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IA8BnC;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAyClC;;;;;OAKG;IACI,4BAA4B,CAAC,sBAAsB,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAOhG;;;OAGG;YACW,0BAA0B;IAkExC;;OAEG;IACH,OAAO,CAAC,4BAA4B;IAsEpC;;OAEG;YACW,4BAA4B;IAmK1C,OAAO,CAAC,cAAc;CAoBvB"}
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. The bridge ADVERTISED listener won't fire, so move
182
- // server status to OK explicitly Matter is the only protocol up here.
183
- log.info('HAP is disabled for the main bridge (bridge.hap=false); skipping HAP publish.');
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 !== false;
232
+ return isHapConfigEnabled(bridgeConfig.hap) && !isHapExternalsOnly(bridgeConfig.hap);
214
233
  }
215
234
  /**
216
- * Whether Matter is configured for the given bridge.
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 !!bridgeConfig.matter;
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
- config.bridge.username = config.bridge.username.toUpperCase();
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
- // At least one of HAP or Matter must be enabled per child bridge.
472
- // Note: Matter is unsupported on accessory-style child bridges (warned about
473
- // in childBridgeFork.ts), so for ACCESSORY child bridges only HAP counts.
474
- const hapOk = Server.isHapEnabled(bridgeConfig);
475
- const matterOk = type === "platform" /* PluginType.PLATFORM */ && Server.isMatterEnabledForBridge(bridgeConfig);
476
- if (!hapOk && !matterOk) {
477
- throw new Error(`Error loading the ${type} "${identifier}" requested in your config.json - `
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
- if (bridgeConfig.username === this.config.bridge.username.toUpperCase()) {
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?.bridgeUsername);
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
- * Get Matter accessories for a specific bridge or all bridges
637
- * @param bridgeUsername - Optional: specific bridge username (MAC format)
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(bridgeUsername) {
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 with Matter enabled.
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
- setTimeout(() => {
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).unref();
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 with Matter enabled
900
- const matterChildBridges = [...this.childBridges.values()].filter(bridge => bridge.getMetadata().matterConfig);
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
  }