@riddix/hamh 2.1.0-alpha.870 → 2.1.0-alpha.872

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.
@@ -110398,9 +110398,9 @@ function inspectEndpoint(endpoint, properties, behindAggregator = false) {
110398
110398
  properties.supportsEthernet = true;
110399
110399
  }
110400
110400
  }
110401
- const networkInterfaces6 = endpoint.maybeStateOf(GeneralDiagnosticsClient)?.networkInterfaces;
110402
- if (networkInterfaces6 !== void 0) {
110403
- for (const { type, isOperational } of networkInterfaces6) {
110401
+ const networkInterfaces5 = endpoint.maybeStateOf(GeneralDiagnosticsClient)?.networkInterfaces;
110402
+ if (networkInterfaces5 !== void 0) {
110403
+ for (const { type, isOperational } of networkInterfaces5) {
110404
110404
  if (type === GeneralDiagnostics3.InterfaceType.WiFi) {
110405
110405
  properties.wifiActive = (properties.wifiActive ?? false) || isOperational;
110406
110406
  } else if (type === GeneralDiagnostics3.InterfaceType.Ethernet) {
@@ -137901,8 +137901,8 @@ function systemApi(version2) {
137901
137901
  }
137902
137902
  function getNetworkInterfaces2() {
137903
137903
  const interfaces = [];
137904
- const networkInterfaces6 = os4.networkInterfaces();
137905
- for (const [name, ifaceList] of Object.entries(networkInterfaces6)) {
137904
+ const networkInterfaces5 = os4.networkInterfaces();
137905
+ for (const [name, ifaceList] of Object.entries(networkInterfaces5)) {
137906
137906
  if (!ifaceList) continue;
137907
137907
  for (const iface of ifaceList) {
137908
137908
  const family = String(iface.family);
@@ -157864,8 +157864,6 @@ var PluginManager = class {
157864
157864
 
157865
157865
  // src/services/bridges/bridge.ts
157866
157866
  init_dist();
157867
- init_esm7();
157868
- import * as os9 from "node:os";
157869
157867
  init_protocol3();
157870
157868
 
157871
157869
  // src/utils/json/create-bridge-server-config.ts
@@ -158178,6 +158176,41 @@ var BridgeServerNode = class extends ServerNode {
158178
158176
  }
158179
158177
  };
158180
158178
 
158179
+ // src/utils/ensure-commissioning-config.ts
158180
+ init_protocol3();
158181
+ var ServerNodeConfigProvider = class extends CommissioningConfigProvider {
158182
+ #node;
158183
+ constructor(node) {
158184
+ super();
158185
+ this.#node = node;
158186
+ }
158187
+ get values() {
158188
+ const { commissioning, productDescription, network } = this.#node.state;
158189
+ return {
158190
+ ...commissioning,
158191
+ productDescription,
158192
+ ble: !!network.ble
158193
+ };
158194
+ }
158195
+ };
158196
+ function ensureCommissioningConfig(server) {
158197
+ if (!server.env.has(CommissioningConfigProvider)) {
158198
+ server.env.set(
158199
+ CommissioningConfigProvider,
158200
+ new ServerNodeConfigProvider(server)
158201
+ );
158202
+ }
158203
+ }
158204
+
158205
+ // src/services/bridges/bridge.ts
158206
+ init_diagnostic_event_bus();
158207
+
158208
+ // src/services/bridges/session-supervisor.ts
158209
+ init_dist();
158210
+ init_esm7();
158211
+ import * as os9 from "node:os";
158212
+ init_protocol3();
158213
+
158181
158214
  // src/matter/mdns-address-watch.ts
158182
158215
  function collectAdvertisedAddresses(interfaces, netInterface, stripGlobalIpv6, ipv4Enabled = true) {
158183
158216
  const names = netInterface ? [netInterface] : Object.keys(interfaces);
@@ -158221,33 +158254,7 @@ async function runMdnsAddressWatchTick(deps) {
158221
158254
  return next;
158222
158255
  }
158223
158256
 
158224
- // src/utils/ensure-commissioning-config.ts
158225
- init_protocol3();
158226
- var ServerNodeConfigProvider = class extends CommissioningConfigProvider {
158227
- #node;
158228
- constructor(node) {
158229
- super();
158230
- this.#node = node;
158231
- }
158232
- get values() {
158233
- const { commissioning, productDescription, network } = this.#node.state;
158234
- return {
158235
- ...commissioning,
158236
- productDescription,
158237
- ble: !!network.ble
158238
- };
158239
- }
158240
- };
158241
- function ensureCommissioningConfig(server) {
158242
- if (!server.env.has(CommissioningConfigProvider)) {
158243
- server.env.set(
158244
- CommissioningConfigProvider,
158245
- new ServerNodeConfigProvider(server)
158246
- );
158247
- }
158248
- }
158249
-
158250
- // src/services/bridges/bridge.ts
158257
+ // src/services/bridges/session-supervisor.ts
158251
158258
  init_diagnostic_event_bus();
158252
158259
 
158253
158260
  // src/services/bridges/session-rotation.ts
@@ -158408,7 +158415,7 @@ function decideWedgeRotationV2(input) {
158408
158415
  return input.lastRotatedMsAgo == null || input.lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS;
158409
158416
  }
158410
158417
 
158411
- // src/services/bridges/bridge.ts
158418
+ // src/services/bridges/session-supervisor.ts
158412
158419
  var imWrapMarker = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
158413
158420
  var commandMessageTypes = [
158414
158421
  MessageType.ReadRequest,
@@ -158416,46 +158423,21 @@ var commandMessageTypes = [
158416
158423
  MessageType.InvokeRequest,
158417
158424
  MessageType.TimedRequest
158418
158425
  ];
158419
- var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
158420
158426
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
158421
158427
  var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
158422
- var Bridge = class {
158423
- constructor(env, logger255, dataProvider, endpointManager, serverOptions) {
158428
+ var SessionSupervisor = class {
158429
+ constructor(log, server, dataProvider, bridgeInfo, defaultMaxAgeHours) {
158430
+ this.log = log;
158431
+ this.server = server;
158424
158432
  this.dataProvider = dataProvider;
158425
- this.endpointManager = endpointManager;
158426
- this.serverOptions = serverOptions;
158427
- this.log = logger255.get(`Bridge / ${dataProvider.id}`);
158428
- this.server = new BridgeServerNode(
158429
- env,
158430
- this.dataProvider,
158431
- this.endpointManager.root,
158432
- this.serverOptions
158433
- );
158434
- const { basicInformation } = this.dataProvider;
158435
- this.log.debugCtx("Root bridge BasicInformation configured", {
158436
- vendorName: basicInformation.vendorName,
158437
- productName: basicInformation.productName,
158438
- productLabel: basicInformation.productLabel,
158439
- hardwareVersion: basicInformation.hardwareVersion,
158440
- hardwareVersionString: basicInformation.hardwareVersionString,
158441
- softwareVersion: basicInformation.softwareVersion,
158442
- softwareVersionString: basicInformation.softwareVersionString
158443
- });
158433
+ this.bridgeInfo = bridgeInfo;
158434
+ this.defaultMaxAgeHours = defaultMaxAgeHours;
158444
158435
  }
158445
- dataProvider;
158446
- endpointManager;
158447
- serverOptions;
158448
158436
  log;
158449
158437
  server;
158450
- status = {
158451
- code: BridgeStatus.Stopped,
158452
- reason: void 0
158453
- };
158454
- // Called whenever the bridge status changes. Set by BridgeService to
158455
- // broadcast updates via WebSocket so the frontend sees every transition
158456
- // (e.g. Stopped → Starting → Running).
158457
- onStatusChange;
158458
- autoForceSyncTimer = null;
158438
+ dataProvider;
158439
+ bridgeInfo;
158440
+ defaultMaxAgeHours;
158459
158441
  deadSessionTimer = null;
158460
158442
  staleSessionTimers = /* @__PURE__ */ new Map();
158461
158443
  // Age-based session rotation (#287): track when each session opened so an
@@ -158484,13 +158466,6 @@ var Bridge = class {
158484
158466
  // refreshOperationalAdvertisement can't overlap and re-announce off a stale
158485
158467
  // snapshot.
158486
158468
  mdnsCheckInFlight = false;
158487
- // Serialize concurrent lifecycle calls so auto-recovery and a manual
158488
- // restartBridge can't race past each other's Starting/Stopping states.
158489
- startInFlight;
158490
- stopInFlight;
158491
- // Tracks the last synced state JSON per entity to avoid pushing unchanged states.
158492
- // Key: entity_id, Value: JSON.stringify of entity.state
158493
- lastSyncedStates = /* @__PURE__ */ new Map();
158494
158469
  // Session lifecycle diagnostic handlers (non-destructive, logging only).
158495
158470
  // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
158496
158471
  sessionDiagHandler;
@@ -158501,17 +158476,24 @@ var Bridge = class {
158501
158476
  // Warns when Alexa pairs on a non-5540 port, the fabric rolls back later (#401).
158502
158477
  // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
158503
158478
  fabricAddedHandler;
158504
- get id() {
158505
- return this.dataProvider.id;
158479
+ // after server.start(); wireSessionDiagnostics self-unwires first because
158480
+ // factoryReset restarts without going through stop()
158481
+ start() {
158482
+ this.wireSessionDiagnostics();
158483
+ this.wireFabricWarnings();
158484
+ this.startSessionRotation();
158485
+ this.startWedgeWatchdog();
158486
+ this.startMdnsAddressWatch();
158506
158487
  }
158507
- get data() {
158508
- return this.dataProvider.withMetadata(
158509
- this.status,
158510
- this.server,
158511
- this.aggregator.parts.size,
158512
- this.endpointManager.failedEntities,
158513
- this.getExposedDeviceTypes()
158514
- );
158488
+ // before server.cancel(); unwireSessionDiagnostics also stops the timers
158489
+ stop() {
158490
+ this.unwireSessionDiagnostics();
158491
+ this.unwireFabricWarnings();
158492
+ }
158493
+ // bridge config changed while running
158494
+ reconfigure() {
158495
+ this.startSessionRotation();
158496
+ this.startWedgeWatchdog();
158515
158497
  }
158516
158498
  getSessionInfo() {
158517
158499
  try {
@@ -158590,218 +158572,6 @@ var Bridge = class {
158590
158572
  };
158591
158573
  }
158592
158574
  }
158593
- get aggregator() {
158594
- return this.endpointManager.root;
158595
- }
158596
- /**
158597
- * The entity id and numeric Matter device type of every exposed endpoint,
158598
- * walking composed sub-endpoints. Used to warn when a device type is not
158599
- * supported by a commissioned controller (#365 class).
158600
- */
158601
- getExposedDeviceTypes() {
158602
- const out = [];
158603
- const collect = (ep, inheritedEntityId) => {
158604
- const entityId = ep.entityId ?? inheritedEntityId;
158605
- const deviceTypeId = ep.type?.deviceType;
158606
- if (typeof deviceTypeId === "number" && entityId) {
158607
- out.push({ entityId, deviceTypeId });
158608
- }
158609
- for (const child of ep.parts) {
158610
- collect(child, entityId);
158611
- }
158612
- };
158613
- for (const ep of this.aggregator.parts) {
158614
- collect(ep);
158615
- }
158616
- return out;
158617
- }
158618
- get pluginInfo() {
158619
- return this.endpointManager.getPluginInfo();
158620
- }
158621
- async enablePlugin(pluginName) {
158622
- return await this.endpointManager.enablePlugin(pluginName);
158623
- }
158624
- async disablePlugin(pluginName) {
158625
- return await this.endpointManager.disablePlugin(pluginName);
158626
- }
158627
- resetPlugin(pluginName) {
158628
- this.endpointManager.resetPlugin(pluginName);
158629
- }
158630
- getPluginConfigSchema(pluginName) {
158631
- return this.endpointManager.getPluginConfigSchema(pluginName);
158632
- }
158633
- async updatePluginConfig(pluginName, config8) {
158634
- const result = await this.endpointManager.updatePluginConfig(
158635
- pluginName,
158636
- config8
158637
- );
158638
- if (result && pluginName === "camera") {
158639
- await this.applyCameraTcp(config8);
158640
- }
158641
- return result;
158642
- }
158643
- // #419: cameras need Matter over TCP because the WebRTC offer exceeds the UDP
158644
- // message size. Match the bridge listener to the saved camera list. Changing
158645
- // network config takes effect on (re)start, so bounce the bridge if running.
158646
- async applyCameraTcp(config8) {
158647
- const want = parseCameraList(config8.cameras).length > 0 || !!this.dataProvider.featureFlags?.enableMatterTcp;
158648
- const have = !!this.server.state.network.tcp;
158649
- if (want === have) {
158650
- return;
158651
- }
158652
- this.log.info(
158653
- want ? "cameras configured, enabling matter over tcp (#419)" : "no cameras left, disabling matter over tcp (#419)"
158654
- );
158655
- const running = this.status.code === BridgeStatus.Running;
158656
- if (running) {
158657
- await this.stop();
158658
- }
158659
- try {
158660
- await this.server.set({
158661
- network: { tcp: want ? CAMERA_TCP_CONFIG : void 0 }
158662
- });
158663
- } catch (e) {
158664
- this.log.warn(
158665
- "Could not apply tcp live, restart matterhub to apply it:",
158666
- e
158667
- );
158668
- }
158669
- if (running) {
158670
- await this.start();
158671
- }
158672
- }
158673
- async initialize() {
158674
- await this.server.construction.ready.then();
158675
- await this.refreshDevices();
158676
- }
158677
- async dispose() {
158678
- await this.stop();
158679
- }
158680
- async refreshDevices() {
158681
- await this.endpointManager.refreshDevices();
158682
- const currentEntityIds = new Set(
158683
- [...this.aggregator.parts].map(
158684
- (p) => p.entityId
158685
- )
158686
- );
158687
- for (const entityId of this.lastSyncedStates.keys()) {
158688
- if (!currentEntityIds.has(entityId)) {
158689
- this.lastSyncedStates.delete(entityId);
158690
- }
158691
- }
158692
- }
158693
- setStatus(status3) {
158694
- this.status = status3;
158695
- this.onStatusChange?.();
158696
- }
158697
- async start() {
158698
- if (this.status.code === BridgeStatus.Running) {
158699
- return;
158700
- }
158701
- if (this.startInFlight) {
158702
- return this.startInFlight;
158703
- }
158704
- this.startInFlight = this.runStart().finally(() => {
158705
- this.startInFlight = void 0;
158706
- });
158707
- return this.startInFlight;
158708
- }
158709
- async runStart() {
158710
- this.lastSyncedStates.clear();
158711
- try {
158712
- this.setStatus({
158713
- code: BridgeStatus.Starting,
158714
- reason: "The bridge is starting... Please wait."
158715
- });
158716
- await this.refreshDevices();
158717
- logMemoryUsage(
158718
- this.log,
158719
- `after refreshDevices (${this.aggregator.parts.size} endpoints)`
158720
- );
158721
- this.endpointManager.startObserving();
158722
- ensureCommissioningConfig(this.server);
158723
- applyLegacySpecSessionParameters(
158724
- this.server.env.get(SessionManager),
158725
- this.dataProvider.featureFlags
158726
- );
158727
- await this.server.setStateOf(
158728
- BasicInformationServer,
158729
- specVersionValues(this.dataProvider.featureFlags)
158730
- );
158731
- await applyTcpFlagBeforeStart(
158732
- this.server,
158733
- this.dataProvider.featureFlags
158734
- );
158735
- await this.server.start();
158736
- await this.endpointManager.startPlugins();
158737
- this.setStatus({ code: BridgeStatus.Running });
158738
- this.startAutoForceSyncIfEnabled();
158739
- if (this.dataProvider.featureFlags?.autoForceSync) {
158740
- this.forceSync().catch((e) => {
158741
- this.log.warn("Startup force sync failed:", e);
158742
- });
158743
- }
158744
- this.wireSessionDiagnostics();
158745
- this.wireFabricWarnings();
158746
- this.startSessionRotation();
158747
- this.startWedgeWatchdog();
158748
- this.startMdnsAddressWatch();
158749
- logMemoryUsage(this.log, "bridge running");
158750
- diagnosticEventBus.emit("bridge_started", `Bridge started`, {
158751
- bridgeId: this.id,
158752
- bridgeName: this.dataProvider.name,
158753
- details: { deviceCount: this.aggregator.parts.size }
158754
- });
158755
- } catch (e) {
158756
- const reason = "Failed to start bridge due to error:";
158757
- this.log.error(reason, e);
158758
- await this.stop(BridgeStatus.Failed, `${reason}
158759
- ${e?.toString()}`);
158760
- }
158761
- }
158762
- async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
158763
- if (this.stopInFlight) {
158764
- return this.stopInFlight;
158765
- }
158766
- this.stopInFlight = this.runStop(code, reason).finally(() => {
158767
- this.stopInFlight = void 0;
158768
- });
158769
- return this.stopInFlight;
158770
- }
158771
- async runStop(code, reason) {
158772
- this.unwireSessionDiagnostics();
158773
- this.unwireFabricWarnings();
158774
- this.stopAutoForceSync();
158775
- await this.closeActiveSessions();
158776
- await this.endpointManager.stopPlugins();
158777
- this.endpointManager.stopObserving();
158778
- try {
158779
- await this.server.cancel();
158780
- } catch (e) {
158781
- const errorMessage = e instanceof Error ? e.message : String(e);
158782
- if (!errorMessage.includes("mutex-closed")) {
158783
- this.log.warn("Error stopping bridge server:", e);
158784
- }
158785
- }
158786
- this.setStatus({ code, reason });
158787
- diagnosticEventBus.emit("bridge_stopped", `Bridge stopped: ${reason}`, {
158788
- bridgeId: this.id,
158789
- bridgeName: this.dataProvider.name
158790
- });
158791
- }
158792
- startAutoForceSyncIfEnabled() {
158793
- this.stopAutoForceSync();
158794
- const forceSyncEnabled = this.dataProvider.featureFlags?.autoForceSync ?? false;
158795
- if (!forceSyncEnabled) {
158796
- return;
158797
- }
158798
- this.autoForceSyncTimer = setInterval(() => {
158799
- this.forceSync().catch((e) => {
158800
- this.log.warn("Auto force sync failed:", e);
158801
- });
158802
- }, AUTO_FORCE_SYNC_INTERVAL_MS);
158803
- this.log.info(`Force sync: every ${AUTO_FORCE_SYNC_INTERVAL_MS / 1e3}s`);
158804
- }
158805
158575
  wireSessionDiagnostics() {
158806
158576
  this.unwireSessionDiagnostics();
158807
158577
  try {
@@ -158829,8 +158599,8 @@ ${e?.toString()}`);
158829
158599
  "subscription_changed",
158830
158600
  `Session ${session.id}: ${session.subscriptions.size} subs (total ${totalSubs})`,
158831
158601
  {
158832
- bridgeId: this.data.id,
158833
- bridgeName: this.data.name,
158602
+ bridgeId: this.bridgeInfo().id,
158603
+ bridgeName: this.bridgeInfo().name,
158834
158604
  details: {
158835
158605
  sessionId: session.id,
158836
158606
  sessionSubs: session.subscriptions.size,
@@ -158886,8 +158656,8 @@ ${e?.toString()}`);
158886
158656
  "session_opened",
158887
158657
  `Session ${newSession.id} opened (peer ${newSession.peerNodeId})`,
158888
158658
  {
158889
- bridgeId: this.data.id,
158890
- bridgeName: this.data.name,
158659
+ bridgeId: this.bridgeInfo().id,
158660
+ bridgeName: this.bridgeInfo().name,
158891
158661
  details: { sessionId: newSession.id }
158892
158662
  }
158893
158663
  );
@@ -158954,8 +158724,8 @@ ${e?.toString()}`);
158954
158724
  "session_closed",
158955
158725
  `Session ${session.id} closed (peer ${session.peerNodeId})`,
158956
158726
  {
158957
- bridgeId: this.data.id,
158958
- bridgeName: this.data.name,
158727
+ bridgeId: this.bridgeInfo().id,
158728
+ bridgeName: this.bridgeInfo().name,
158959
158729
  details: {
158960
158730
  sessionId: session.id,
158961
158731
  remainingSessions: sessions.length
@@ -159242,12 +159012,6 @@ ${e?.toString()}`);
159242
159012
  }
159243
159013
  this.fabricAddedHandler = void 0;
159244
159014
  }
159245
- stopAutoForceSync() {
159246
- if (this.autoForceSyncTimer) {
159247
- clearInterval(this.autoForceSyncTimer);
159248
- this.autoForceSyncTimer = null;
159249
- }
159250
- }
159251
159015
  // Start periodic age-based session rotation (#287). Aging out a controller's
159252
159016
  // session forces it to re-establish and re-subscribe, recovering a wedged
159253
159017
  // Alexa subscription that would otherwise stay stuck until a restart.
@@ -159429,10 +159193,9 @@ ${e?.toString()}`);
159429
159193
  }
159430
159194
  });
159431
159195
  }
159432
- // Resolve the rotation max age. Bridge config wins, then the env var. An
159433
- // aggregator bridge holds many devices on one controller session, so
159434
- // rotating it re-subscribes them all at once. Rotation is therefore opt-in
159435
- // here: it stays disabled unless set via config or the env var.
159196
+ // Env var unset or garbage falls back to defaultMaxAgeHours: 0 on the
159197
+ // aggregator bridge (rotation opt-in, one controller session holds many
159198
+ // devices), DEFAULT_SESSION_MAX_AGE_HOURS in server mode.
159436
159199
  readSessionMaxAgeHours() {
159437
159200
  const { min, max } = SESSION_MAX_AGE_HOURS_RANGE;
159438
159201
  const fromConfig = this.dataProvider.sessionMaxAgeHours;
@@ -159444,14 +159207,14 @@ ${e?.toString()}`);
159444
159207
  }
159445
159208
  const raw = process.env.HAMH_MATTER_SESSION_MAX_AGE_HOURS;
159446
159209
  if (raw == null || raw === "") {
159447
- return 0;
159210
+ return this.defaultMaxAgeHours;
159448
159211
  }
159449
159212
  const parsed = parseSessionMaxAgeHours(raw);
159450
159213
  if (parsed == null) {
159451
159214
  this.log.warn(
159452
- `Invalid HAMH_MATTER_SESSION_MAX_AGE_HOURS=${raw}, disabling session rotation`
159215
+ `Invalid HAMH_MATTER_SESSION_MAX_AGE_HOURS=${raw}, ${this.defaultMaxAgeHours === 0 ? "disabling session rotation" : `falling back to ${this.defaultMaxAgeHours}h`}`
159453
159216
  );
159454
- return 0;
159217
+ return this.defaultMaxAgeHours;
159455
159218
  }
159456
159219
  return parsed;
159457
159220
  }
@@ -159489,14 +159252,297 @@ ${e?.toString()}`);
159489
159252
  } catch {
159490
159253
  }
159491
159254
  }
159255
+ };
159256
+
159257
+ // src/services/bridges/bridge.ts
159258
+ var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
159259
+ var Bridge = class {
159260
+ constructor(env, logger255, dataProvider, endpointManager, serverOptions) {
159261
+ this.dataProvider = dataProvider;
159262
+ this.endpointManager = endpointManager;
159263
+ this.serverOptions = serverOptions;
159264
+ this.log = logger255.get(`Bridge / ${dataProvider.id}`);
159265
+ this.server = new BridgeServerNode(
159266
+ env,
159267
+ this.dataProvider,
159268
+ this.endpointManager.root,
159269
+ this.serverOptions
159270
+ );
159271
+ this.sessions = new SessionSupervisor(
159272
+ this.log,
159273
+ this.server,
159274
+ this.dataProvider,
159275
+ () => this.data,
159276
+ 0
159277
+ );
159278
+ const { basicInformation } = this.dataProvider;
159279
+ this.log.debugCtx("Root bridge BasicInformation configured", {
159280
+ vendorName: basicInformation.vendorName,
159281
+ productName: basicInformation.productName,
159282
+ productLabel: basicInformation.productLabel,
159283
+ hardwareVersion: basicInformation.hardwareVersion,
159284
+ hardwareVersionString: basicInformation.hardwareVersionString,
159285
+ softwareVersion: basicInformation.softwareVersion,
159286
+ softwareVersionString: basicInformation.softwareVersionString
159287
+ });
159288
+ }
159289
+ dataProvider;
159290
+ endpointManager;
159291
+ serverOptions;
159292
+ log;
159293
+ server;
159294
+ sessions;
159295
+ status = {
159296
+ code: BridgeStatus.Stopped,
159297
+ reason: void 0
159298
+ };
159299
+ // Called whenever the bridge status changes. Set by BridgeService to
159300
+ // broadcast updates via WebSocket so the frontend sees every transition
159301
+ // (e.g. Stopped → Starting → Running).
159302
+ onStatusChange;
159303
+ autoForceSyncTimer = null;
159304
+ // Serialize concurrent lifecycle calls so auto-recovery and a manual
159305
+ // restartBridge can't race past each other's Starting/Stopping states.
159306
+ startInFlight;
159307
+ stopInFlight;
159308
+ // Tracks the last synced state JSON per entity to avoid pushing unchanged states.
159309
+ // Key: entity_id, Value: JSON.stringify of entity.state
159310
+ lastSyncedStates = /* @__PURE__ */ new Map();
159311
+ get id() {
159312
+ return this.dataProvider.id;
159313
+ }
159314
+ get data() {
159315
+ return this.dataProvider.withMetadata(
159316
+ this.status,
159317
+ this.server,
159318
+ this.aggregator.parts.size,
159319
+ this.endpointManager.failedEntities,
159320
+ this.getExposedDeviceTypes()
159321
+ );
159322
+ }
159323
+ get aggregator() {
159324
+ return this.endpointManager.root;
159325
+ }
159326
+ /**
159327
+ * The entity id and numeric Matter device type of every exposed endpoint,
159328
+ * walking composed sub-endpoints. Used to warn when a device type is not
159329
+ * supported by a commissioned controller (#365 class).
159330
+ */
159331
+ getExposedDeviceTypes() {
159332
+ const out = [];
159333
+ const collect = (ep, inheritedEntityId) => {
159334
+ const entityId = ep.entityId ?? inheritedEntityId;
159335
+ const deviceTypeId = ep.type?.deviceType;
159336
+ if (typeof deviceTypeId === "number" && entityId) {
159337
+ out.push({ entityId, deviceTypeId });
159338
+ }
159339
+ for (const child of ep.parts) {
159340
+ collect(child, entityId);
159341
+ }
159342
+ };
159343
+ for (const ep of this.aggregator.parts) {
159344
+ collect(ep);
159345
+ }
159346
+ return out;
159347
+ }
159348
+ getSessionInfo() {
159349
+ return this.sessions.getSessionInfo();
159350
+ }
159351
+ get pluginInfo() {
159352
+ return this.endpointManager.getPluginInfo();
159353
+ }
159354
+ async enablePlugin(pluginName) {
159355
+ return await this.endpointManager.enablePlugin(pluginName);
159356
+ }
159357
+ async disablePlugin(pluginName) {
159358
+ return await this.endpointManager.disablePlugin(pluginName);
159359
+ }
159360
+ resetPlugin(pluginName) {
159361
+ this.endpointManager.resetPlugin(pluginName);
159362
+ }
159363
+ getPluginConfigSchema(pluginName) {
159364
+ return this.endpointManager.getPluginConfigSchema(pluginName);
159365
+ }
159366
+ async updatePluginConfig(pluginName, config8) {
159367
+ const result = await this.endpointManager.updatePluginConfig(
159368
+ pluginName,
159369
+ config8
159370
+ );
159371
+ if (result && pluginName === "camera") {
159372
+ await this.applyCameraTcp(config8);
159373
+ }
159374
+ return result;
159375
+ }
159376
+ // #419: cameras need Matter over TCP because the WebRTC offer exceeds the UDP
159377
+ // message size. Match the bridge listener to the saved camera list. Changing
159378
+ // network config takes effect on (re)start, so bounce the bridge if running.
159379
+ async applyCameraTcp(config8) {
159380
+ const want = parseCameraList(config8.cameras).length > 0 || !!this.dataProvider.featureFlags?.enableMatterTcp;
159381
+ const have = !!this.server.state.network.tcp;
159382
+ if (want === have) {
159383
+ return;
159384
+ }
159385
+ this.log.info(
159386
+ want ? "cameras configured, enabling matter over tcp (#419)" : "no cameras left, disabling matter over tcp (#419)"
159387
+ );
159388
+ const running = this.status.code === BridgeStatus.Running;
159389
+ if (running) {
159390
+ await this.stop();
159391
+ }
159392
+ try {
159393
+ await this.server.set({
159394
+ network: { tcp: want ? CAMERA_TCP_CONFIG : void 0 }
159395
+ });
159396
+ } catch (e) {
159397
+ this.log.warn(
159398
+ "Could not apply tcp live, restart matterhub to apply it:",
159399
+ e
159400
+ );
159401
+ }
159402
+ if (running) {
159403
+ await this.start();
159404
+ }
159405
+ }
159406
+ async initialize() {
159407
+ await this.server.construction.ready.then();
159408
+ await this.refreshDevices();
159409
+ }
159410
+ async dispose() {
159411
+ await this.stop();
159412
+ }
159413
+ async refreshDevices() {
159414
+ await this.endpointManager.refreshDevices();
159415
+ const currentEntityIds = new Set(
159416
+ [...this.aggregator.parts].map(
159417
+ (p) => p.entityId
159418
+ )
159419
+ );
159420
+ for (const entityId of this.lastSyncedStates.keys()) {
159421
+ if (!currentEntityIds.has(entityId)) {
159422
+ this.lastSyncedStates.delete(entityId);
159423
+ }
159424
+ }
159425
+ }
159426
+ setStatus(status3) {
159427
+ this.status = status3;
159428
+ this.onStatusChange?.();
159429
+ }
159430
+ async start() {
159431
+ if (this.status.code === BridgeStatus.Running) {
159432
+ return;
159433
+ }
159434
+ if (this.startInFlight) {
159435
+ return this.startInFlight;
159436
+ }
159437
+ this.startInFlight = this.runStart().finally(() => {
159438
+ this.startInFlight = void 0;
159439
+ });
159440
+ return this.startInFlight;
159441
+ }
159442
+ async runStart() {
159443
+ this.lastSyncedStates.clear();
159444
+ try {
159445
+ this.setStatus({
159446
+ code: BridgeStatus.Starting,
159447
+ reason: "The bridge is starting... Please wait."
159448
+ });
159449
+ await this.refreshDevices();
159450
+ logMemoryUsage(
159451
+ this.log,
159452
+ `after refreshDevices (${this.aggregator.parts.size} endpoints)`
159453
+ );
159454
+ this.endpointManager.startObserving();
159455
+ ensureCommissioningConfig(this.server);
159456
+ applyLegacySpecSessionParameters(
159457
+ this.server.env.get(SessionManager),
159458
+ this.dataProvider.featureFlags
159459
+ );
159460
+ await this.server.setStateOf(
159461
+ BasicInformationServer,
159462
+ specVersionValues(this.dataProvider.featureFlags)
159463
+ );
159464
+ await applyTcpFlagBeforeStart(
159465
+ this.server,
159466
+ this.dataProvider.featureFlags
159467
+ );
159468
+ await this.server.start();
159469
+ await this.endpointManager.startPlugins();
159470
+ this.setStatus({ code: BridgeStatus.Running });
159471
+ this.startAutoForceSyncIfEnabled();
159472
+ if (this.dataProvider.featureFlags?.autoForceSync) {
159473
+ this.forceSync().catch((e) => {
159474
+ this.log.warn("Startup force sync failed:", e);
159475
+ });
159476
+ }
159477
+ this.sessions.start();
159478
+ logMemoryUsage(this.log, "bridge running");
159479
+ diagnosticEventBus.emit("bridge_started", `Bridge started`, {
159480
+ bridgeId: this.id,
159481
+ bridgeName: this.dataProvider.name,
159482
+ details: { deviceCount: this.aggregator.parts.size }
159483
+ });
159484
+ } catch (e) {
159485
+ const reason = "Failed to start bridge due to error:";
159486
+ this.log.error(reason, e);
159487
+ await this.stop(BridgeStatus.Failed, `${reason}
159488
+ ${e?.toString()}`);
159489
+ }
159490
+ }
159491
+ async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
159492
+ if (this.stopInFlight) {
159493
+ return this.stopInFlight;
159494
+ }
159495
+ this.stopInFlight = this.runStop(code, reason).finally(() => {
159496
+ this.stopInFlight = void 0;
159497
+ });
159498
+ return this.stopInFlight;
159499
+ }
159500
+ async runStop(code, reason) {
159501
+ this.sessions.stop();
159502
+ this.stopAutoForceSync();
159503
+ await this.sessions.closeActiveSessions();
159504
+ await this.endpointManager.stopPlugins();
159505
+ this.endpointManager.stopObserving();
159506
+ try {
159507
+ await this.server.cancel();
159508
+ } catch (e) {
159509
+ const errorMessage = e instanceof Error ? e.message : String(e);
159510
+ if (!errorMessage.includes("mutex-closed")) {
159511
+ this.log.warn("Error stopping bridge server:", e);
159512
+ }
159513
+ }
159514
+ this.setStatus({ code, reason });
159515
+ diagnosticEventBus.emit("bridge_stopped", `Bridge stopped: ${reason}`, {
159516
+ bridgeId: this.id,
159517
+ bridgeName: this.dataProvider.name
159518
+ });
159519
+ }
159520
+ startAutoForceSyncIfEnabled() {
159521
+ this.stopAutoForceSync();
159522
+ const forceSyncEnabled = this.dataProvider.featureFlags?.autoForceSync ?? false;
159523
+ if (!forceSyncEnabled) {
159524
+ return;
159525
+ }
159526
+ this.autoForceSyncTimer = setInterval(() => {
159527
+ this.forceSync().catch((e) => {
159528
+ this.log.warn("Auto force sync failed:", e);
159529
+ });
159530
+ }, AUTO_FORCE_SYNC_INTERVAL_MS);
159531
+ this.log.info(`Force sync: every ${AUTO_FORCE_SYNC_INTERVAL_MS / 1e3}s`);
159532
+ }
159533
+ stopAutoForceSync() {
159534
+ if (this.autoForceSyncTimer) {
159535
+ clearInterval(this.autoForceSyncTimer);
159536
+ this.autoForceSyncTimer = null;
159537
+ }
159538
+ }
159492
159539
  async update(update) {
159493
159540
  try {
159494
159541
  this.dataProvider.update(update);
159495
159542
  await this.refreshDevices();
159496
159543
  if (this.status.code === BridgeStatus.Running) {
159497
159544
  this.startAutoForceSyncIfEnabled();
159498
- this.startSessionRotation();
159499
- this.startWedgeWatchdog();
159545
+ this.sessions.reconfigure();
159500
159546
  }
159501
159547
  } catch (e) {
159502
159548
  const reason = "Failed to update bridge due to error:";
@@ -162452,17 +162498,20 @@ var ThermostatServerBase = class extends FullFeaturedBase {
162452
162498
  controlSequence = this.clampControlSequence(controlSequence);
162453
162499
  this.internal.controlSequenceOfOperation = controlSequence;
162454
162500
  applyPatchState(this.state, {
162501
+ // abs limits BEFORE user limits: on a widening update the user limit
162502
+ // must never transiently sit outside the still-narrow abs limit, the
162503
+ // reactor asserts against abs on every write (#454 hardening)
162455
162504
  ...this.features.heating ? {
162456
- minHeatSetpointLimit: minHeatLimit,
162457
- maxHeatSetpointLimit: maxHeatLimit,
162458
162505
  absMinHeatSetpointLimit: minHeatLimit,
162459
- absMaxHeatSetpointLimit: maxHeatLimit
162506
+ absMaxHeatSetpointLimit: maxHeatLimit,
162507
+ minHeatSetpointLimit: minHeatLimit,
162508
+ maxHeatSetpointLimit: maxHeatLimit
162460
162509
  } : {},
162461
162510
  ...this.features.cooling ? {
162462
- minCoolSetpointLimit: minCoolLimit,
162463
- maxCoolSetpointLimit: maxCoolLimit,
162464
162511
  absMinCoolSetpointLimit: minCoolLimit,
162465
- absMaxCoolSetpointLimit: maxCoolLimit
162512
+ absMaxCoolSetpointLimit: maxCoolLimit,
162513
+ minCoolSetpointLimit: minCoolLimit,
162514
+ maxCoolSetpointLimit: maxCoolLimit
162466
162515
  } : {},
162467
162516
  localTemperature,
162468
162517
  controlSequenceOfOperation: controlSequence,
@@ -167470,14 +167519,12 @@ var DimmableLightWithBatteryType = DimmableLightDevice.with(
167470
167519
 
167471
167520
  // src/matter/endpoints/legacy/light/devices/extended-color-light.ts
167472
167521
  init_home_assistant_entity_behavior();
167473
- var ExtendedColorLightType = (supportsColorControl, supportsTemperature, hasBattery = false) => {
167522
+ var ExtendedColorLightType = (supportsColorControl, _supportsTemperature, hasBattery = false) => {
167474
167523
  const features = /* @__PURE__ */ new Set();
167524
+ features.add("Xy");
167525
+ features.add("ColorTemperature");
167475
167526
  if (supportsColorControl) {
167476
167527
  features.add("HueSaturation");
167477
- features.add("Xy");
167478
- }
167479
- if (supportsTemperature) {
167480
- features.add("ColorTemperature");
167481
167528
  }
167482
167529
  if (hasBattery) {
167483
167530
  return ExtendedColorLightDevice.with(
@@ -169352,6 +169399,99 @@ var CarbonMonoxideSensorType = AirQualitySensorDevice.with(
169352
169399
  CarbonMonoxideConcentrationMeasurementServer2
169353
169400
  );
169354
169401
 
169402
+ // src/matter/behaviors/carbon-dioxide-concentration-measurement-server.ts
169403
+ init_home_assistant_entity_behavior();
169404
+ var CarbonDioxideConcentrationMeasurementServerBase = CarbonDioxideConcentrationMeasurementServer.with(
169405
+ ConcentrationMeasurement3.Feature.NumericMeasurement
169406
+ );
169407
+ var CarbonDioxideConcentrationMeasurementServer2 = class extends CarbonDioxideConcentrationMeasurementServerBase {
169408
+ async initialize() {
169409
+ if (this.state.measuredValue === void 0) {
169410
+ this.state.measuredValue = null;
169411
+ }
169412
+ if (this.state.minMeasuredValue === void 0) {
169413
+ this.state.minMeasuredValue = null;
169414
+ }
169415
+ if (this.state.maxMeasuredValue === void 0) {
169416
+ this.state.maxMeasuredValue = null;
169417
+ }
169418
+ if (this.state.uncertainty === void 0) {
169419
+ this.state.uncertainty = 0;
169420
+ }
169421
+ if (this.state.measurementUnit === void 0) {
169422
+ this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ppm;
169423
+ }
169424
+ if (this.state.measurementMedium === void 0) {
169425
+ this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
169426
+ }
169427
+ await super.initialize();
169428
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
169429
+ this.update(homeAssistant.entity);
169430
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
169431
+ }
169432
+ update(entity) {
169433
+ if (!entity.state || !entity.state.attributes) {
169434
+ return;
169435
+ }
169436
+ const state = entity.state.state;
169437
+ let measuredValue = null;
169438
+ if (state != null && !Number.isNaN(+state)) {
169439
+ measuredValue = +state;
169440
+ }
169441
+ applyPatchState(this.state, {
169442
+ measuredValue
169443
+ });
169444
+ }
169445
+ };
169446
+
169447
+ // src/matter/endpoints/legacy/sensor/devices/co2-sensor.ts
169448
+ init_home_assistant_entity_behavior();
169449
+ var Co2AirQualityServerBase = AirQualityServer.with(
169450
+ AirQuality3.Feature.Fair,
169451
+ AirQuality3.Feature.Moderate,
169452
+ AirQuality3.Feature.VeryPoor,
169453
+ AirQuality3.Feature.ExtremelyPoor
169454
+ );
169455
+ var Co2AirQualityServer = class extends Co2AirQualityServerBase {
169456
+ async initialize() {
169457
+ if (this.state.airQuality === void 0) {
169458
+ this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
169459
+ }
169460
+ await super.initialize();
169461
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
169462
+ this.update(homeAssistant.entity);
169463
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
169464
+ }
169465
+ update(entity) {
169466
+ const state = entity.state.state;
169467
+ let airQuality = AirQuality3.AirQualityEnum.Unknown;
169468
+ if (state != null && !Number.isNaN(+state)) {
169469
+ const value = +state;
169470
+ if (value <= 800) {
169471
+ airQuality = AirQuality3.AirQualityEnum.Good;
169472
+ } else if (value <= 1e3) {
169473
+ airQuality = AirQuality3.AirQualityEnum.Fair;
169474
+ } else if (value <= 1400) {
169475
+ airQuality = AirQuality3.AirQualityEnum.Moderate;
169476
+ } else if (value <= 2e3) {
169477
+ airQuality = AirQuality3.AirQualityEnum.Poor;
169478
+ } else if (value <= 5e3) {
169479
+ airQuality = AirQuality3.AirQualityEnum.VeryPoor;
169480
+ } else {
169481
+ airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
169482
+ }
169483
+ }
169484
+ applyPatchState(this.state, { airQuality });
169485
+ }
169486
+ };
169487
+ var Co2SensorType = AirQualitySensorDevice.with(
169488
+ BasicInformationServer2,
169489
+ IdentifyServer2,
169490
+ HomeAssistantEntityBehavior,
169491
+ Co2AirQualityServer,
169492
+ CarbonDioxideConcentrationMeasurementServer2
169493
+ );
169494
+
169355
169495
  // src/matter/endpoints/legacy/sensor/devices/electrical-meter.ts
169356
169496
  init_home_assistant_entity_behavior();
169357
169497
 
@@ -170371,6 +170511,208 @@ var Pm1SensorType = AirQualitySensorDevice.with(
170371
170511
  Pm1ConcentrationMeasurementServer2
170372
170512
  );
170373
170513
 
170514
+ // src/matter/endpoints/legacy/sensor/devices/pm10-sensor.ts
170515
+ init_home_assistant_entity_behavior();
170516
+
170517
+ // src/matter/behaviors/pm10-concentration-measurement-server.ts
170518
+ init_home_assistant_entity_behavior();
170519
+ var Pm10ConcentrationMeasurementServerBase = Pm10ConcentrationMeasurementServer.with(
170520
+ ConcentrationMeasurement3.Feature.NumericMeasurement
170521
+ );
170522
+ var Pm10ConcentrationMeasurementServer2 = class extends Pm10ConcentrationMeasurementServerBase {
170523
+ async initialize() {
170524
+ if (this.state.measuredValue === void 0) {
170525
+ this.state.measuredValue = null;
170526
+ }
170527
+ if (this.state.minMeasuredValue === void 0) {
170528
+ this.state.minMeasuredValue = null;
170529
+ }
170530
+ if (this.state.maxMeasuredValue === void 0) {
170531
+ this.state.maxMeasuredValue = null;
170532
+ }
170533
+ if (this.state.uncertainty === void 0) {
170534
+ this.state.uncertainty = 0;
170535
+ }
170536
+ if (this.state.measurementUnit === void 0) {
170537
+ this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ugm3;
170538
+ }
170539
+ if (this.state.measurementMedium === void 0) {
170540
+ this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
170541
+ }
170542
+ await super.initialize();
170543
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170544
+ this.update(homeAssistant.entity);
170545
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170546
+ }
170547
+ update(entity) {
170548
+ if (!entity.state || !entity.state.attributes) {
170549
+ return;
170550
+ }
170551
+ const state = entity.state.state;
170552
+ let measuredValue = null;
170553
+ if (state != null && !Number.isNaN(+state)) {
170554
+ measuredValue = +state;
170555
+ }
170556
+ applyPatchState(this.state, {
170557
+ measuredValue
170558
+ });
170559
+ }
170560
+ };
170561
+
170562
+ // src/matter/endpoints/legacy/sensor/devices/pm10-sensor.ts
170563
+ var Pm10AirQualityServerBase = AirQualityServer.with(
170564
+ AirQuality3.Feature.Fair,
170565
+ AirQuality3.Feature.Moderate,
170566
+ AirQuality3.Feature.VeryPoor,
170567
+ AirQuality3.Feature.ExtremelyPoor
170568
+ );
170569
+ var Pm10AirQualityServer = class extends Pm10AirQualityServerBase {
170570
+ async initialize() {
170571
+ if (this.state.airQuality === void 0) {
170572
+ this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
170573
+ }
170574
+ await super.initialize();
170575
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170576
+ this.update(homeAssistant.entity);
170577
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170578
+ }
170579
+ update(entity) {
170580
+ const state = entity.state.state;
170581
+ let airQuality = AirQuality3.AirQualityEnum.Unknown;
170582
+ if (state != null && !Number.isNaN(+state)) {
170583
+ const value = +state;
170584
+ if (value <= 54) {
170585
+ airQuality = AirQuality3.AirQualityEnum.Good;
170586
+ } else if (value <= 154) {
170587
+ airQuality = AirQuality3.AirQualityEnum.Fair;
170588
+ } else if (value <= 254) {
170589
+ airQuality = AirQuality3.AirQualityEnum.Moderate;
170590
+ } else if (value <= 354) {
170591
+ airQuality = AirQuality3.AirQualityEnum.Poor;
170592
+ } else if (value <= 424) {
170593
+ airQuality = AirQuality3.AirQualityEnum.VeryPoor;
170594
+ } else {
170595
+ airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
170596
+ }
170597
+ }
170598
+ applyPatchState(this.state, { airQuality });
170599
+ }
170600
+ };
170601
+ var Pm10SensorType = AirQualitySensorDevice.with(
170602
+ BasicInformationServer2,
170603
+ IdentifyServer2,
170604
+ HomeAssistantEntityBehavior,
170605
+ Pm10AirQualityServer,
170606
+ Pm10ConcentrationMeasurementServer2
170607
+ );
170608
+
170609
+ // src/matter/endpoints/legacy/sensor/devices/pm25-sensor.ts
170610
+ init_esm();
170611
+ init_home_assistant_entity_behavior();
170612
+
170613
+ // src/matter/behaviors/pm25-concentration-measurement-server.ts
170614
+ init_esm();
170615
+ init_home_assistant_entity_behavior();
170616
+ var logger232 = Logger.get("Pm25ConcentrationMeasurementServer");
170617
+ var Pm25ConcentrationMeasurementServerBase = Pm25ConcentrationMeasurementServer.with(
170618
+ ConcentrationMeasurement3.Feature.NumericMeasurement
170619
+ );
170620
+ var Pm25ConcentrationMeasurementServer2 = class extends Pm25ConcentrationMeasurementServerBase {
170621
+ async initialize() {
170622
+ if (this.state.measuredValue === void 0) {
170623
+ this.state.measuredValue = null;
170624
+ }
170625
+ if (this.state.minMeasuredValue === void 0) {
170626
+ this.state.minMeasuredValue = null;
170627
+ }
170628
+ if (this.state.maxMeasuredValue === void 0) {
170629
+ this.state.maxMeasuredValue = null;
170630
+ }
170631
+ if (this.state.uncertainty === void 0) {
170632
+ this.state.uncertainty = 0;
170633
+ }
170634
+ if (this.state.measurementUnit === void 0) {
170635
+ this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ugm3;
170636
+ }
170637
+ if (this.state.measurementMedium === void 0) {
170638
+ this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
170639
+ }
170640
+ logger232.debug(
170641
+ "Pm25ConcentrationMeasurementServer: before super.initialize()"
170642
+ );
170643
+ await super.initialize();
170644
+ logger232.debug(
170645
+ "Pm25ConcentrationMeasurementServer: after super.initialize()"
170646
+ );
170647
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170648
+ this.update(homeAssistant.entity);
170649
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170650
+ }
170651
+ update(entity) {
170652
+ if (!entity.state || !entity.state.attributes) {
170653
+ return;
170654
+ }
170655
+ const state = entity.state.state;
170656
+ let measuredValue = null;
170657
+ if (state != null && !Number.isNaN(+state)) {
170658
+ measuredValue = +state;
170659
+ }
170660
+ applyPatchState(this.state, {
170661
+ measuredValue
170662
+ });
170663
+ }
170664
+ };
170665
+
170666
+ // src/matter/endpoints/legacy/sensor/devices/pm25-sensor.ts
170667
+ var logger233 = Logger.get("Pm25AirQualityServer");
170668
+ var Pm25AirQualityServerBase = AirQualityServer.with(
170669
+ AirQuality3.Feature.Fair,
170670
+ AirQuality3.Feature.Moderate,
170671
+ AirQuality3.Feature.VeryPoor,
170672
+ AirQuality3.Feature.ExtremelyPoor
170673
+ );
170674
+ var Pm25AirQualityServer = class extends Pm25AirQualityServerBase {
170675
+ async initialize() {
170676
+ if (this.state.airQuality === void 0) {
170677
+ this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
170678
+ }
170679
+ logger233.debug("Pm25AirQualityServer: before super.initialize()");
170680
+ await super.initialize();
170681
+ logger233.debug("Pm25AirQualityServer: after super.initialize()");
170682
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170683
+ this.update(homeAssistant.entity);
170684
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170685
+ }
170686
+ update(entity) {
170687
+ const state = entity.state.state;
170688
+ let airQuality = AirQuality3.AirQualityEnum.Unknown;
170689
+ if (state != null && !Number.isNaN(+state)) {
170690
+ const value = +state;
170691
+ if (value <= 12) {
170692
+ airQuality = AirQuality3.AirQualityEnum.Good;
170693
+ } else if (value <= 35) {
170694
+ airQuality = AirQuality3.AirQualityEnum.Fair;
170695
+ } else if (value <= 55) {
170696
+ airQuality = AirQuality3.AirQualityEnum.Moderate;
170697
+ } else if (value <= 150) {
170698
+ airQuality = AirQuality3.AirQualityEnum.Poor;
170699
+ } else if (value <= 250) {
170700
+ airQuality = AirQuality3.AirQualityEnum.VeryPoor;
170701
+ } else {
170702
+ airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
170703
+ }
170704
+ }
170705
+ applyPatchState(this.state, { airQuality });
170706
+ }
170707
+ };
170708
+ var Pm25SensorType = AirQualitySensorDevice.with(
170709
+ BasicInformationServer2,
170710
+ IdentifyServer2,
170711
+ HomeAssistantEntityBehavior,
170712
+ Pm25AirQualityServer,
170713
+ Pm25ConcentrationMeasurementServer2
170714
+ );
170715
+
170374
170716
  // src/matter/endpoints/legacy/sensor/devices/pressure-sensor.ts
170375
170717
  init_home_assistant_entity_behavior();
170376
170718
  var pressureSensorConfig = {
@@ -170589,7 +170931,7 @@ var TvocConcentrationMeasurementServer = class extends TvocConcentrationMeasurem
170589
170931
  };
170590
170932
 
170591
170933
  // src/matter/endpoints/legacy/sensor/devices/tvoc-sensor.ts
170592
- var logger232 = Logger.get("TvocSensor");
170934
+ var logger234 = Logger.get("TvocSensor");
170593
170935
  function airQualityFromUgm3(value) {
170594
170936
  if (value <= 300) return AirQuality3.AirQualityEnum.Good;
170595
170937
  if (value <= 1e3) return AirQuality3.AirQualityEnum.Fair;
@@ -170630,17 +170972,17 @@ var TvocAirQualityServer = class extends TvocAirQualityServerBase {
170630
170972
  const attributes9 = entity.state.attributes;
170631
170973
  const deviceClass = attributes9.device_class;
170632
170974
  let airQuality = AirQuality3.AirQualityEnum.Unknown;
170633
- logger232.debug(
170975
+ logger234.debug(
170634
170976
  `[${entity.entity_id}] TVOC update: state="${state}", device_class="${deviceClass}"`
170635
170977
  );
170636
170978
  if (state != null && !Number.isNaN(+state)) {
170637
170979
  const value = +state;
170638
170980
  airQuality = deviceClass === SensorDeviceClass.volatile_organic_compounds ? airQualityFromUgm3(value) : airQualityFromPpb(value);
170639
- logger232.debug(
170981
+ logger234.debug(
170640
170982
  `[${entity.entity_id}] TVOC value=${value} (${deviceClass}) -> airQuality=${AirQuality3.AirQualityEnum[airQuality]}`
170641
170983
  );
170642
170984
  } else {
170643
- logger232.warn(
170985
+ logger234.warn(
170644
170986
  `[${entity.entity_id}] TVOC state not a valid number: "${state}"`
170645
170987
  );
170646
170988
  }
@@ -170660,301 +171002,6 @@ init_dist();
170660
171002
  init_types2();
170661
171003
  init_diagnostic_event_bus();
170662
171004
 
170663
- // src/matter/behaviors/carbon-dioxide-concentration-measurement-server.ts
170664
- init_home_assistant_entity_behavior();
170665
- var CarbonDioxideConcentrationMeasurementServerBase = CarbonDioxideConcentrationMeasurementServer.with(
170666
- ConcentrationMeasurement3.Feature.NumericMeasurement
170667
- );
170668
- var CarbonDioxideConcentrationMeasurementServer2 = class extends CarbonDioxideConcentrationMeasurementServerBase {
170669
- async initialize() {
170670
- if (this.state.measuredValue === void 0) {
170671
- this.state.measuredValue = null;
170672
- }
170673
- if (this.state.minMeasuredValue === void 0) {
170674
- this.state.minMeasuredValue = null;
170675
- }
170676
- if (this.state.maxMeasuredValue === void 0) {
170677
- this.state.maxMeasuredValue = null;
170678
- }
170679
- if (this.state.uncertainty === void 0) {
170680
- this.state.uncertainty = 0;
170681
- }
170682
- if (this.state.measurementUnit === void 0) {
170683
- this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ppm;
170684
- }
170685
- if (this.state.measurementMedium === void 0) {
170686
- this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
170687
- }
170688
- await super.initialize();
170689
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170690
- this.update(homeAssistant.entity);
170691
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170692
- }
170693
- update(entity) {
170694
- if (!entity.state || !entity.state.attributes) {
170695
- return;
170696
- }
170697
- const state = entity.state.state;
170698
- let measuredValue = null;
170699
- if (state != null && !Number.isNaN(+state)) {
170700
- measuredValue = +state;
170701
- }
170702
- applyPatchState(this.state, {
170703
- measuredValue
170704
- });
170705
- }
170706
- };
170707
-
170708
- // src/matter/endpoints/legacy/sensor/devices/co2-sensor.ts
170709
- init_home_assistant_entity_behavior();
170710
- var Co2AirQualityServerBase = AirQualityServer.with(
170711
- AirQuality3.Feature.Fair,
170712
- AirQuality3.Feature.Moderate,
170713
- AirQuality3.Feature.VeryPoor,
170714
- AirQuality3.Feature.ExtremelyPoor
170715
- );
170716
- var Co2AirQualityServer = class extends Co2AirQualityServerBase {
170717
- async initialize() {
170718
- if (this.state.airQuality === void 0) {
170719
- this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
170720
- }
170721
- await super.initialize();
170722
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170723
- this.update(homeAssistant.entity);
170724
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170725
- }
170726
- update(entity) {
170727
- const state = entity.state.state;
170728
- let airQuality = AirQuality3.AirQualityEnum.Unknown;
170729
- if (state != null && !Number.isNaN(+state)) {
170730
- const value = +state;
170731
- if (value <= 800) {
170732
- airQuality = AirQuality3.AirQualityEnum.Good;
170733
- } else if (value <= 1e3) {
170734
- airQuality = AirQuality3.AirQualityEnum.Fair;
170735
- } else if (value <= 1400) {
170736
- airQuality = AirQuality3.AirQualityEnum.Moderate;
170737
- } else if (value <= 2e3) {
170738
- airQuality = AirQuality3.AirQualityEnum.Poor;
170739
- } else if (value <= 5e3) {
170740
- airQuality = AirQuality3.AirQualityEnum.VeryPoor;
170741
- } else {
170742
- airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
170743
- }
170744
- }
170745
- applyPatchState(this.state, { airQuality });
170746
- }
170747
- };
170748
- var Co2SensorType = AirQualitySensorDevice.with(
170749
- BasicInformationServer2,
170750
- IdentifyServer2,
170751
- HomeAssistantEntityBehavior,
170752
- Co2AirQualityServer,
170753
- CarbonDioxideConcentrationMeasurementServer2
170754
- );
170755
-
170756
- // src/matter/endpoints/legacy/sensor/devices/pm10-sensor.ts
170757
- init_home_assistant_entity_behavior();
170758
-
170759
- // src/matter/behaviors/pm10-concentration-measurement-server.ts
170760
- init_home_assistant_entity_behavior();
170761
- var Pm10ConcentrationMeasurementServerBase = Pm10ConcentrationMeasurementServer.with(
170762
- ConcentrationMeasurement3.Feature.NumericMeasurement
170763
- );
170764
- var Pm10ConcentrationMeasurementServer2 = class extends Pm10ConcentrationMeasurementServerBase {
170765
- async initialize() {
170766
- if (this.state.measuredValue === void 0) {
170767
- this.state.measuredValue = null;
170768
- }
170769
- if (this.state.minMeasuredValue === void 0) {
170770
- this.state.minMeasuredValue = null;
170771
- }
170772
- if (this.state.maxMeasuredValue === void 0) {
170773
- this.state.maxMeasuredValue = null;
170774
- }
170775
- if (this.state.uncertainty === void 0) {
170776
- this.state.uncertainty = 0;
170777
- }
170778
- if (this.state.measurementUnit === void 0) {
170779
- this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ugm3;
170780
- }
170781
- if (this.state.measurementMedium === void 0) {
170782
- this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
170783
- }
170784
- await super.initialize();
170785
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170786
- this.update(homeAssistant.entity);
170787
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170788
- }
170789
- update(entity) {
170790
- if (!entity.state || !entity.state.attributes) {
170791
- return;
170792
- }
170793
- const state = entity.state.state;
170794
- let measuredValue = null;
170795
- if (state != null && !Number.isNaN(+state)) {
170796
- measuredValue = +state;
170797
- }
170798
- applyPatchState(this.state, {
170799
- measuredValue
170800
- });
170801
- }
170802
- };
170803
-
170804
- // src/matter/endpoints/legacy/sensor/devices/pm10-sensor.ts
170805
- var Pm10AirQualityServerBase = AirQualityServer.with(
170806
- AirQuality3.Feature.Fair,
170807
- AirQuality3.Feature.Moderate,
170808
- AirQuality3.Feature.VeryPoor,
170809
- AirQuality3.Feature.ExtremelyPoor
170810
- );
170811
- var Pm10AirQualityServer = class extends Pm10AirQualityServerBase {
170812
- async initialize() {
170813
- if (this.state.airQuality === void 0) {
170814
- this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
170815
- }
170816
- await super.initialize();
170817
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170818
- this.update(homeAssistant.entity);
170819
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170820
- }
170821
- update(entity) {
170822
- const state = entity.state.state;
170823
- let airQuality = AirQuality3.AirQualityEnum.Unknown;
170824
- if (state != null && !Number.isNaN(+state)) {
170825
- const value = +state;
170826
- if (value <= 54) {
170827
- airQuality = AirQuality3.AirQualityEnum.Good;
170828
- } else if (value <= 154) {
170829
- airQuality = AirQuality3.AirQualityEnum.Fair;
170830
- } else if (value <= 254) {
170831
- airQuality = AirQuality3.AirQualityEnum.Moderate;
170832
- } else if (value <= 354) {
170833
- airQuality = AirQuality3.AirQualityEnum.Poor;
170834
- } else if (value <= 424) {
170835
- airQuality = AirQuality3.AirQualityEnum.VeryPoor;
170836
- } else {
170837
- airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
170838
- }
170839
- }
170840
- applyPatchState(this.state, { airQuality });
170841
- }
170842
- };
170843
- var Pm10SensorType = AirQualitySensorDevice.with(
170844
- BasicInformationServer2,
170845
- IdentifyServer2,
170846
- HomeAssistantEntityBehavior,
170847
- Pm10AirQualityServer,
170848
- Pm10ConcentrationMeasurementServer2
170849
- );
170850
-
170851
- // src/matter/endpoints/legacy/sensor/devices/pm25-sensor.ts
170852
- init_esm();
170853
- init_home_assistant_entity_behavior();
170854
-
170855
- // src/matter/behaviors/pm25-concentration-measurement-server.ts
170856
- init_esm();
170857
- init_home_assistant_entity_behavior();
170858
- var logger233 = Logger.get("Pm25ConcentrationMeasurementServer");
170859
- var Pm25ConcentrationMeasurementServerBase = Pm25ConcentrationMeasurementServer.with(
170860
- ConcentrationMeasurement3.Feature.NumericMeasurement
170861
- );
170862
- var Pm25ConcentrationMeasurementServer2 = class extends Pm25ConcentrationMeasurementServerBase {
170863
- async initialize() {
170864
- if (this.state.measuredValue === void 0) {
170865
- this.state.measuredValue = null;
170866
- }
170867
- if (this.state.minMeasuredValue === void 0) {
170868
- this.state.minMeasuredValue = null;
170869
- }
170870
- if (this.state.maxMeasuredValue === void 0) {
170871
- this.state.maxMeasuredValue = null;
170872
- }
170873
- if (this.state.uncertainty === void 0) {
170874
- this.state.uncertainty = 0;
170875
- }
170876
- if (this.state.measurementUnit === void 0) {
170877
- this.state.measurementUnit = ConcentrationMeasurement3.MeasurementUnit.Ugm3;
170878
- }
170879
- if (this.state.measurementMedium === void 0) {
170880
- this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
170881
- }
170882
- logger233.debug(
170883
- "Pm25ConcentrationMeasurementServer: before super.initialize()"
170884
- );
170885
- await super.initialize();
170886
- logger233.debug(
170887
- "Pm25ConcentrationMeasurementServer: after super.initialize()"
170888
- );
170889
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170890
- this.update(homeAssistant.entity);
170891
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170892
- }
170893
- update(entity) {
170894
- if (!entity.state || !entity.state.attributes) {
170895
- return;
170896
- }
170897
- const state = entity.state.state;
170898
- let measuredValue = null;
170899
- if (state != null && !Number.isNaN(+state)) {
170900
- measuredValue = +state;
170901
- }
170902
- applyPatchState(this.state, {
170903
- measuredValue
170904
- });
170905
- }
170906
- };
170907
-
170908
- // src/matter/endpoints/legacy/sensor/devices/pm25-sensor.ts
170909
- var logger234 = Logger.get("Pm25AirQualityServer");
170910
- var Pm25AirQualityServerBase = AirQualityServer.with(
170911
- AirQuality3.Feature.Fair,
170912
- AirQuality3.Feature.Moderate,
170913
- AirQuality3.Feature.VeryPoor,
170914
- AirQuality3.Feature.ExtremelyPoor
170915
- );
170916
- var Pm25AirQualityServer = class extends Pm25AirQualityServerBase {
170917
- async initialize() {
170918
- if (this.state.airQuality === void 0) {
170919
- this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
170920
- }
170921
- logger234.debug("Pm25AirQualityServer: before super.initialize()");
170922
- await super.initialize();
170923
- logger234.debug("Pm25AirQualityServer: after super.initialize()");
170924
- const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170925
- this.update(homeAssistant.entity);
170926
- this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170927
- }
170928
- update(entity) {
170929
- const state = entity.state.state;
170930
- let airQuality = AirQuality3.AirQualityEnum.Unknown;
170931
- if (state != null && !Number.isNaN(+state)) {
170932
- const value = +state;
170933
- if (value <= 12) {
170934
- airQuality = AirQuality3.AirQualityEnum.Good;
170935
- } else if (value <= 35) {
170936
- airQuality = AirQuality3.AirQualityEnum.Fair;
170937
- } else if (value <= 55) {
170938
- airQuality = AirQuality3.AirQualityEnum.Moderate;
170939
- } else if (value <= 150) {
170940
- airQuality = AirQuality3.AirQualityEnum.Poor;
170941
- } else if (value <= 250) {
170942
- airQuality = AirQuality3.AirQualityEnum.VeryPoor;
170943
- } else {
170944
- airQuality = AirQuality3.AirQualityEnum.ExtremelyPoor;
170945
- }
170946
- }
170947
- applyPatchState(this.state, { airQuality });
170948
- }
170949
- };
170950
- var Pm25SensorType = AirQualitySensorDevice.with(
170951
- BasicInformationServer2,
170952
- IdentifyServer2,
170953
- HomeAssistantEntityBehavior,
170954
- Pm25AirQualityServer,
170955
- Pm25ConcentrationMeasurementServer2
170956
- );
170957
-
170958
171005
  // src/matter/endpoints/legacy/sensor/devices/temperature-humidity-sensor.ts
170959
171006
  init_home_assistant_entity_behavior();
170960
171007
  var temperatureConfig3 = {
@@ -174409,6 +174456,9 @@ var matterDeviceTypeFactories = {
174409
174456
  formaldehyde_sensor: (ha) => FormaldehydeSensorType.set({ homeAssistantEntity: ha }),
174410
174457
  radon_sensor: (ha) => RadonSensorType.set({ homeAssistantEntity: ha }),
174411
174458
  pm1_sensor: (ha) => Pm1SensorType.set({ homeAssistantEntity: ha }),
174459
+ pm25_sensor: (ha) => Pm25SensorType.set({ homeAssistantEntity: ha }),
174460
+ pm10_sensor: (ha) => Pm10SensorType.set({ homeAssistantEntity: ha }),
174461
+ carbon_dioxide_sensor: (ha) => Co2SensorType.set({ homeAssistantEntity: ha }),
174412
174462
  electrical_meter: (ha) => ElectricalMeterType.set({ homeAssistantEntity: ha }),
174413
174463
  // Legacy SolarPower alias, kept for existing mappings.
174414
174464
  electrical_sensor: (ha) => ElectricalSensorType.set({ homeAssistantEntity: ha }),
@@ -179289,20 +179339,9 @@ var BridgeEndpointManager = class extends Service {
179289
179339
 
179290
179340
  // src/services/bridges/server-mode-bridge.ts
179291
179341
  init_dist();
179292
- init_esm7();
179293
- import * as os10 from "node:os";
179294
179342
  init_protocol3();
179295
179343
  init_diagnostic_event_bus();
179296
- var imWrapMarker2 = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
179297
- var commandMessageTypes2 = [
179298
- MessageType.ReadRequest,
179299
- MessageType.WriteRequest,
179300
- MessageType.InvokeRequest,
179301
- MessageType.TimedRequest
179302
- ];
179303
179344
  var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
179304
- var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2 = 2500;
179305
- var MDNS_ADDRESS_CHECK_INTERVAL_MS2 = 6e4;
179306
179345
  function makeWarmStartState(state, now = (/* @__PURE__ */ new Date()).toISOString()) {
179307
179346
  return { ...state, last_updated: now };
179308
179347
  }
@@ -179312,11 +179351,19 @@ var ServerModeBridge = class {
179312
179351
  this.endpointManager = endpointManager;
179313
179352
  this.server = server;
179314
179353
  this.log = logger255.get(`ServerModeBridge / ${dataProvider.id}`);
179354
+ this.sessions = new SessionSupervisor(
179355
+ this.log,
179356
+ this.server,
179357
+ this.dataProvider,
179358
+ () => this.data,
179359
+ DEFAULT_SESSION_MAX_AGE_HOURS
179360
+ );
179315
179361
  }
179316
179362
  dataProvider;
179317
179363
  endpointManager;
179318
179364
  server;
179319
179365
  log;
179366
+ sessions;
179320
179367
  status = {
179321
179368
  code: BridgeStatus.Stopped,
179322
179369
  reason: void 0
@@ -179325,46 +179372,9 @@ var ServerModeBridge = class {
179325
179372
  // broadcast updates via WebSocket so the frontend sees every transition.
179326
179373
  onStatusChange;
179327
179374
  autoForceSyncTimer = null;
179328
- deadSessionTimer = null;
179329
- staleSessionTimers = /* @__PURE__ */ new Map();
179330
179375
  warmStartTimer = null;
179331
- // Tracks when each session opened, used for age-based rotation (#287).
179332
- sessionStartedAt = /* @__PURE__ */ new Map();
179333
- rotationTimer = null;
179334
- maxSessionAgeMs = 0;
179335
- // Wedge watchdog: last inbound Interaction Model request time per session and
179336
- // last time the watchdog rotated it, both keyed by the long-lived session
179337
- // object so they clear when the session goes away.
179338
- lastImRequestAt = /* @__PURE__ */ new WeakMap();
179339
- wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
179340
- wedgeWatchdogTimer = null;
179341
- // V2 shadow signals (#365): last command-class request per session, plus
179342
- // rings of subscribe and delivery give-up times (newest WEDGE_RING_SIZE).
179343
- lastCommandImAt = /* @__PURE__ */ new WeakMap();
179344
- subscribeTimesMs = /* @__PURE__ */ new WeakMap();
179345
- giveUpTimesMs = /* @__PURE__ */ new WeakMap();
179346
- // Sessions whose subscriptions.deleted we already hooked for give-ups.
179347
- wedgeHookedSessions = /* @__PURE__ */ new WeakSet();
179348
- // Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
179349
- // change forces a fresh operational announcement (#415).
179350
- mdnsAddressTimer = null;
179351
- advertisedAddressSnapshot = [];
179352
- // Skip a tick while the previous one is still awaiting refresh, so a slow
179353
- // refreshOperationalAdvertisement can't overlap and re-announce off a stale
179354
- // snapshot.
179355
- mdnsCheckInFlight = false;
179356
179376
  // Tracks the last synced state JSON per entity to avoid pushing unchanged states.
179357
179377
  lastSyncedStates = /* @__PURE__ */ new Map();
179358
- // Session lifecycle diagnostic handlers (non-destructive, logging only).
179359
- // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
179360
- sessionDiagHandler;
179361
- // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
179362
- sessionAddedHandler;
179363
- // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
179364
- sessionDeletedHandler;
179365
- // Warns when Alexa pairs on a non-5540 port, the fabric rolls back later (#401).
179366
- // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
179367
- fabricAddedHandler;
179368
179378
  get id() {
179369
179379
  return this.dataProvider.id;
179370
179380
  }
@@ -179400,81 +179410,7 @@ var ServerModeBridge = class {
179400
179410
  return out;
179401
179411
  }
179402
179412
  getSessionInfo() {
179403
- try {
179404
- const sessionManager = this.server.env.get(SessionManager);
179405
- const sessions = [...sessionManager.sessions];
179406
- let totalSubscriptions = 0;
179407
- const fabricMap = /* @__PURE__ */ new Map();
179408
- const sessionList = sessions.map((s) => {
179409
- const subCount = s.subscriptions.size;
179410
- totalSubscriptions += subCount;
179411
- const subscriptions = summarizeSubscriptions(s.subscriptions);
179412
- const fi = typeof s.fabric?.fabricIndex === "number" ? s.fabric.fabricIndex : null;
179413
- if (fi !== null) {
179414
- const existing = fabricMap.get(fi) ?? {
179415
- sessions: 0,
179416
- subscriptions: 0
179417
- };
179418
- existing.sessions++;
179419
- existing.subscriptions += subCount;
179420
- fabricMap.set(fi, existing);
179421
- }
179422
- const nowMs = Date.now();
179423
- const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
179424
- const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
179425
- const lastImAt = this.lastImRequestAt.get(s);
179426
- const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
179427
- const startedAt = this.sessionStartedAt.get(s.id);
179428
- const lastCmdAt = this.lastCommandImAt.get(s);
179429
- const lastCommandImRequestMsAgo = lastCmdAt != null ? nowMs - lastCmdAt : null;
179430
- const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
179431
- const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
179432
- const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
179433
- const wedgeV2WouldRotate = decideWedgeRotationV2({
179434
- subscriptionCount: subCount,
179435
- sessionAgeMs: startedAt != null ? nowMs - startedAt : 0,
179436
- commandSilenceMs: lastCommandImRequestMsAgo,
179437
- subscribeTimesMs: subscribeTimes,
179438
- giveUpTimesMs: giveUpTimes,
179439
- nowMs,
179440
- lastRotatedMsAgo: lastRotatedAt != null ? nowMs - lastRotatedAt : null
179441
- });
179442
- return {
179443
- id: s.id,
179444
- peerNodeId: String(s.peerNodeId),
179445
- fabricIndex: fi,
179446
- subscriptionCount: subCount,
179447
- subscriptions,
179448
- lastActiveMsAgo,
179449
- lastAnyActivityMsAgo,
179450
- lastImRequestMsAgo,
179451
- lastCommandImRequestMsAgo,
179452
- subscribesLast30Min: countRecent(subscribeTimes, nowMs),
179453
- giveUpsLast30Min: countRecent(giveUpTimes, nowMs),
179454
- wedgeV2WouldRotate,
179455
- isPeerActive: Boolean(s.isPeerActive),
179456
- ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
179457
- };
179458
- });
179459
- const fabrics = [...fabricMap.entries()].map(([fabricIndex, data]) => ({
179460
- fabricIndex,
179461
- sessions: data.sessions,
179462
- subscriptions: data.subscriptions
179463
- }));
179464
- return {
179465
- sessions: sessionList,
179466
- totalSessions: sessions.length,
179467
- totalSubscriptions,
179468
- fabrics
179469
- };
179470
- } catch {
179471
- return {
179472
- sessions: [],
179473
- totalSessions: 0,
179474
- totalSubscriptions: 0,
179475
- fabrics: []
179476
- };
179477
- }
179413
+ return this.sessions.getSessionInfo();
179478
179414
  }
179479
179415
  async initialize() {
179480
179416
  await this.server.construction.ready.then();
@@ -179524,11 +179460,7 @@ var ServerModeBridge = class {
179524
179460
  this.log.warn("Startup force sync failed:", e);
179525
179461
  });
179526
179462
  }
179527
- this.wireSessionDiagnostics();
179528
- this.wireFabricWarnings();
179529
- this.startSessionRotation();
179530
- this.startWedgeWatchdog();
179531
- this.startMdnsAddressWatch();
179463
+ this.sessions.start();
179532
179464
  this.scheduleWarmStart();
179533
179465
  logMemoryUsage(this.log, "server mode bridge running");
179534
179466
  this.log.info("Server mode bridge started");
@@ -179544,14 +179476,10 @@ ${e?.toString()}`);
179544
179476
  }
179545
179477
  }
179546
179478
  async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
179547
- this.stopSessionRotation();
179548
- this.stopWedgeWatchdog();
179549
- this.stopMdnsAddressWatch();
179550
- this.unwireSessionDiagnostics();
179551
- this.unwireFabricWarnings();
179479
+ this.sessions.stop();
179552
179480
  this.cancelWarmStart();
179553
179481
  this.stopAutoForceSync();
179554
- await this.closeActiveSessions();
179482
+ await this.sessions.closeActiveSessions();
179555
179483
  this.endpointManager.stopObserving();
179556
179484
  try {
179557
179485
  await this.server.cancel();
@@ -179577,8 +179505,7 @@ ${e?.toString()}`);
179577
179505
  await this.refreshDevices();
179578
179506
  if (this.status.code === BridgeStatus.Running) {
179579
179507
  this.startAutoForceSyncIfEnabled();
179580
- this.startSessionRotation();
179581
- this.startWedgeWatchdog();
179508
+ this.sessions.reconfigure();
179582
179509
  }
179583
179510
  } catch (e) {
179584
179511
  const reason = "Failed to update server mode bridge due to error:";
@@ -179621,677 +179548,6 @@ ${e?.toString()}`);
179621
179548
  }, AUTO_FORCE_SYNC_INTERVAL_MS2);
179622
179549
  this.log.info(`Force sync: every ${AUTO_FORCE_SYNC_INTERVAL_MS2 / 1e3}s`);
179623
179550
  }
179624
- wireSessionDiagnostics() {
179625
- this.unwireSessionDiagnostics();
179626
- try {
179627
- const sessionManager = this.server.env.get(SessionManager);
179628
- this.sessionDiagHandler = (session) => {
179629
- const sessions = [...sessionManager.sessions];
179630
- let totalSubs = 0;
179631
- for (const s of sessions) {
179632
- totalSubs += s.subscriptions.size;
179633
- }
179634
- this.log.debug(
179635
- `Session ${session.id} (peer ${session.peerNodeId}): subscriptions=${session.subscriptions.size} | total: sessions=${sessions.length} subscriptions=${totalSubs}`
179636
- );
179637
- diagnosticEventBus.emit(
179638
- "subscription_changed",
179639
- `Session ${session.id}: ${session.subscriptions.size} subs (total ${totalSubs})`,
179640
- {
179641
- bridgeId: this.data.id,
179642
- bridgeName: this.data.name,
179643
- details: {
179644
- sessionId: session.id,
179645
- sessionSubs: session.subscriptions.size,
179646
- totalSessions: sessions.length,
179647
- totalSubs
179648
- }
179649
- }
179650
- );
179651
- if (totalSubs === 0 && sessions.length > 0) {
179652
- this.log.warn(
179653
- `All subscriptions lost, ${sessions.length} session(s) still active, waiting for controller to re-subscribe`
179654
- );
179655
- if (!this.deadSessionTimer) {
179656
- const timeoutMs = deadSessionTimeoutMs(
179657
- this.dataProvider.featureFlags
179658
- );
179659
- this.deadSessionTimer = setTimeout(() => {
179660
- this.deadSessionTimer = null;
179661
- this.closeDeadSessions();
179662
- }, timeoutMs);
179663
- this.log.info(
179664
- `Scheduled dead session cleanup in ${timeoutMs / 1e3}s`
179665
- );
179666
- }
179667
- } else if (totalSubs > 0 && this.deadSessionTimer) {
179668
- clearTimeout(this.deadSessionTimer);
179669
- this.deadSessionTimer = null;
179670
- this.log.info(
179671
- "Subscriptions recovered, canceled dead session cleanup"
179672
- );
179673
- }
179674
- if (session.subscriptions.size === 0 && !this.staleSessionTimers.has(session.id)) {
179675
- this.staleSessionTimers.set(
179676
- session.id,
179677
- setTimeout(() => {
179678
- this.staleSessionTimers.delete(session.id);
179679
- this.closeStaleSession(session.id);
179680
- }, deadSessionTimeoutMs(this.dataProvider.featureFlags))
179681
- );
179682
- } else if (session.subscriptions.size > 0 && this.staleSessionTimers.has(session.id)) {
179683
- clearTimeout(this.staleSessionTimers.get(session.id));
179684
- this.staleSessionTimers.delete(session.id);
179685
- }
179686
- };
179687
- sessionManager.subscriptionsChanged.on(this.sessionDiagHandler);
179688
- this.sessionAddedHandler = (newSession) => {
179689
- this.sessionStartedAt.set(newSession.id, Date.now());
179690
- hookGiveUpsWithPeers(newSession);
179691
- this.log.info(
179692
- `Session opened: id=${newSession.id} peer=${newSession.peerNodeId}`
179693
- );
179694
- diagnosticEventBus.emit(
179695
- "session_opened",
179696
- `Session ${newSession.id} opened (peer ${newSession.peerNodeId})`,
179697
- {
179698
- bridgeId: this.data.id,
179699
- bridgeName: this.data.name,
179700
- details: { sessionId: newSession.id }
179701
- }
179702
- );
179703
- for (const s of [...sessionManager.sessions]) {
179704
- if (s !== newSession && !s.isClosing && s.peerNodeId === newSession.peerNodeId && s.fabric?.fabricIndex === newSession.fabric?.fabricIndex && s.subscriptions.size === 0) {
179705
- this.log.info(
179706
- `Session ${s.id} (peer ${s.peerNodeId}, 0 subs) replaced by session ${newSession.id}, closing it once it goes quiet`
179707
- );
179708
- const staleId = s.id;
179709
- const armed = this.staleSessionTimers.get(staleId);
179710
- if (armed) clearTimeout(armed);
179711
- this.staleSessionTimers.set(
179712
- staleId,
179713
- setTimeout(() => {
179714
- this.staleSessionTimers.delete(staleId);
179715
- this.closeStaleSession(staleId, PRIMING_GRACE_MS);
179716
- }, replacedSessionTimeoutMs(this.dataProvider.featureFlags))
179717
- );
179718
- }
179719
- }
179720
- const quietWindowMs = staleSessionQuietWindowMs(
179721
- this.dataProvider.featureFlags
179722
- );
179723
- const now = Date.now();
179724
- const supersededIds = new Set(
179725
- selectSupersededSessions(
179726
- [...sessionManager.sessions],
179727
- newSession,
179728
- quietWindowMs,
179729
- now
179730
- )
179731
- );
179732
- const closes = [];
179733
- for (const s of [...sessionManager.sessions]) {
179734
- if (!supersededIds.has(s.id) || s.subscriptions.size === 0) continue;
179735
- const quietSec = Math.round((now - s.timestamp) / 1e3);
179736
- this.log.info(
179737
- `Closing superseded session ${s.id} (peer ${s.peerNodeId}, ${s.subscriptions.size} subs, quiet ${quietSec}s), replaced by session ${newSession.id}`
179738
- );
179739
- closes.push(
179740
- s.initiateClose().catch(
179741
- () => s.initiateForceClose({
179742
- cause: new Error("superseded session, forcing")
179743
- })
179744
- )
179745
- );
179746
- }
179747
- if (closes.length > 0) {
179748
- Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
179749
- }
179750
- };
179751
- this.sessionDeletedHandler = (session) => {
179752
- this.sessionStartedAt.delete(session.id);
179753
- const armed = this.staleSessionTimers.get(session.id);
179754
- if (armed) {
179755
- clearTimeout(armed);
179756
- this.staleSessionTimers.delete(session.id);
179757
- }
179758
- const sessions = [...sessionManager.sessions];
179759
- this.log.warn(
179760
- `Session closed: id=${session.id} peer=${session.peerNodeId} | remaining sessions=${sessions.length}`
179761
- );
179762
- diagnosticEventBus.emit(
179763
- "session_closed",
179764
- `Session ${session.id} closed (peer ${session.peerNodeId})`,
179765
- {
179766
- bridgeId: this.data.id,
179767
- bridgeName: this.data.name,
179768
- details: {
179769
- sessionId: session.id,
179770
- remainingSessions: sessions.length
179771
- }
179772
- }
179773
- );
179774
- };
179775
- sessionManager.sessions.added.on(this.sessionAddedHandler);
179776
- sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
179777
- seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
179778
- const hookGiveUpsWithPeers = (sess) => this.hookSubscriptionGiveUps(
179779
- sess,
179780
- () => [...sessionManager.sessions].filter(
179781
- (s) => s.peerNodeId === sess.peerNodeId && s.fabric?.fabricIndex === sess.fabric?.fabricIndex
179782
- )
179783
- );
179784
- for (const sess of sessionManager.sessions) {
179785
- hookGiveUpsWithPeers(sess);
179786
- }
179787
- this.wireImRequestTracking();
179788
- } catch {
179789
- }
179790
- }
179791
- // Stamp the time of every inbound Interaction Model request per session by
179792
- // wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
179793
- // tell a live-but-consuming controller from one that only keeps acking.
179794
- wireImRequestTracking() {
179795
- try {
179796
- const is = this.server.env.get(InteractionServer);
179797
- const marked = is;
179798
- if (marked[imWrapMarker2]) {
179799
- return;
179800
- }
179801
- const original = is.onNewExchange.bind(is);
179802
- is.onNewExchange = (exchange, message) => {
179803
- const session = exchange.session;
179804
- if (session) {
179805
- const now = Date.now();
179806
- this.lastImRequestAt.set(session, now);
179807
- const type = message?.payloadHeader?.messageType;
179808
- if (type === MessageType.SubscribeRequest) {
179809
- this.pushWedgeRing(this.subscribeTimesMs, session, now);
179810
- } else if (type != null && commandMessageTypes2.includes(type)) {
179811
- this.lastCommandImAt.set(session, now);
179812
- }
179813
- }
179814
- return original(exchange, message);
179815
- };
179816
- marked[imWrapMarker2] = true;
179817
- } catch {
179818
- }
179819
- }
179820
- // Keep only the newest WEDGE_RING_SIZE timestamps per session.
179821
- pushWedgeRing(ring, session, now) {
179822
- const times = ring.get(session) ?? [];
179823
- times.push(now);
179824
- if (times.length > WEDGE_RING_SIZE) {
179825
- times.splice(0, times.length - WEDGE_RING_SIZE);
179826
- }
179827
- ring.set(session, times);
179828
- }
179829
- // Watch this session's subscriptions for server-side delivery give-ups
179830
- // (#365 v2 shadow). isTerminated true fires both on a peer cancel and on
179831
- // the 3-strikes delivery give-up; only the give-up arrives without a
179832
- // coincident inbound IM request, so that is the discriminator.
179833
- hookSubscriptionGiveUps(session, peerSessions) {
179834
- const key = session;
179835
- if (this.wedgeHookedSessions.has(key)) {
179836
- return;
179837
- }
179838
- const deleted = session.subscriptions?.deleted;
179839
- if (typeof deleted?.on !== "function") {
179840
- return;
179841
- }
179842
- this.wedgeHookedSessions.add(key);
179843
- deleted.on((sub) => {
179844
- if (sub?.isTerminated !== true) {
179845
- return;
179846
- }
179847
- const now = Date.now();
179848
- const fresh = (s) => {
179849
- const at = this.lastImRequestAt.get(s);
179850
- return at != null && now - at <= WEDGE_GIVE_UP_QUIET_MS;
179851
- };
179852
- if (fresh(key)) {
179853
- return;
179854
- }
179855
- for (const s of peerSessions?.() ?? []) {
179856
- if (s !== key && fresh(s)) {
179857
- return;
179858
- }
179859
- }
179860
- this.pushWedgeRing(this.giveUpTimesMs, key, now);
179861
- this.log.debug(
179862
- `wedge v2 give-up recorded sub=${sub?.subscriptionId}`
179863
- );
179864
- });
179865
- }
179866
- closeStaleSession(sessionId, minQuietMs = 0) {
179867
- try {
179868
- const sessionManager = this.server.env.get(SessionManager);
179869
- for (const s of [...sessionManager.sessions]) {
179870
- if (s.id !== sessionId || s.isClosing || s.subscriptions.size > 0) {
179871
- continue;
179872
- }
179873
- const idleSec = Math.round((Date.now() - s.timestamp) / 1e3);
179874
- if (!staleSessionShouldClose(
179875
- s,
179876
- Math.max(
179877
- staleSessionQuietWindowMs(this.dataProvider.featureFlags),
179878
- minQuietMs
179879
- )
179880
- )) {
179881
- this.log.info(
179882
- `Keeping session ${s.id} (peer ${s.peerNodeId}, 0 subs, last traffic ${idleSec}s ago)`
179883
- );
179884
- this.staleSessionTimers.set(
179885
- sessionId,
179886
- setTimeout(() => {
179887
- this.staleSessionTimers.delete(sessionId);
179888
- this.closeStaleSession(sessionId, minQuietMs);
179889
- }, deadSessionTimeoutMs(this.dataProvider.featureFlags))
179890
- );
179891
- break;
179892
- }
179893
- this.log.warn(
179894
- `Closing stale session ${s.id} (peer ${s.peerNodeId}, no subscriptions for ${deadSessionTimeoutMs(this.dataProvider.featureFlags) / 1e3}s, no traffic for ${idleSec}s)`
179895
- );
179896
- s.initiateClose().catch(() => {
179897
- return s.initiateForceClose({
179898
- cause: new Error("graceful close failed, forcing")
179899
- });
179900
- }).catch(() => {
179901
- }).finally(() => this.triggerMdnsReAnnounce());
179902
- break;
179903
- }
179904
- } catch {
179905
- }
179906
- }
179907
- closeDeadSessions() {
179908
- try {
179909
- const sessionManager = this.server.env.get(SessionManager);
179910
- const sessions = [...sessionManager.sessions];
179911
- const closes = [];
179912
- let kept = 0;
179913
- for (const s of sessions) {
179914
- if (s.isClosing || s.subscriptions.size > 0) {
179915
- continue;
179916
- }
179917
- if (this.staleSessionTimers.has(s.id)) {
179918
- continue;
179919
- }
179920
- const idleSec = Math.round((Date.now() - s.timestamp) / 1e3);
179921
- if (!staleSessionShouldClose(
179922
- s,
179923
- staleSessionQuietWindowMs(this.dataProvider.featureFlags)
179924
- )) {
179925
- this.log.info(
179926
- `Keeping session ${s.id} (peer ${s.peerNodeId}, 0 subs, last traffic ${idleSec}s ago)`
179927
- );
179928
- kept++;
179929
- continue;
179930
- }
179931
- this.log.warn(
179932
- `Closing dead session ${s.id} (peer ${s.peerNodeId}, no subscriptions for ${deadSessionTimeoutMs(this.dataProvider.featureFlags) / 1e3}s, no traffic for ${idleSec}s)`
179933
- );
179934
- closes.push(
179935
- s.initiateClose().catch(() => {
179936
- return s.initiateForceClose({
179937
- cause: new Error("graceful close failed, forcing")
179938
- });
179939
- })
179940
- );
179941
- }
179942
- if (kept > 0 && !this.deadSessionTimer) {
179943
- this.deadSessionTimer = setTimeout(() => {
179944
- this.deadSessionTimer = null;
179945
- this.closeDeadSessions();
179946
- }, deadSessionTimeoutMs(this.dataProvider.featureFlags));
179947
- }
179948
- if (closes.length > 0) {
179949
- Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
179950
- }
179951
- } catch {
179952
- }
179953
- }
179954
- // Close every active session on shutdown so each controller is told to drop
179955
- // its CASE session instead of being left with a stale one. Mirrors the
179956
- // dead-session close, but covers all sessions and waits (capped) so the
179957
- // close actually reaches the peer before the server is canceled.
179958
- async closeActiveSessions() {
179959
- try {
179960
- const sessionManager = this.server.env.get(SessionManager);
179961
- const closes = [];
179962
- for (const s of [...sessionManager.sessions]) {
179963
- if (s.isClosing) {
179964
- continue;
179965
- }
179966
- closes.push(
179967
- s.initiateClose().catch(() => {
179968
- return s.initiateForceClose({
179969
- cause: new Error("graceful close failed, forcing")
179970
- });
179971
- })
179972
- );
179973
- }
179974
- if (closes.length === 0) {
179975
- return;
179976
- }
179977
- this.log.info(`Closing ${closes.length} active session(s) on shutdown`);
179978
- let timer;
179979
- const timeout = new Promise((resolve11) => {
179980
- timer = setTimeout(resolve11, SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2);
179981
- });
179982
- try {
179983
- await Promise.race([Promise.allSettled(closes), timeout]);
179984
- } finally {
179985
- clearTimeout(timer);
179986
- }
179987
- } catch {
179988
- }
179989
- }
179990
- /**
179991
- * Force a fresh mDNS operational advertisement after session cleanup.
179992
- * matter.js DeviceAdvertiser only re-announces when a subscription is
179993
- * canceled BY THE PEER. When the server cancels after 3 delivery
179994
- * timeouts, no re-announcement happens and the controller may not
179995
- * realize it should reconnect (#266).
179996
- */
179997
- triggerMdnsReAnnounce() {
179998
- try {
179999
- const advertiser = this.server.env.get(DeviceAdvertiser);
180000
- advertiser.restartAdvertisement();
180001
- this.log.info("Triggered mDNS re-announcement after session cleanup");
180002
- } catch {
180003
- }
180004
- }
180005
- unwireSessionDiagnostics() {
180006
- try {
180007
- const sessionManager = this.server.env.get(SessionManager);
180008
- if (this.sessionDiagHandler) {
180009
- sessionManager.subscriptionsChanged.off(this.sessionDiagHandler);
180010
- }
180011
- if (this.sessionAddedHandler) {
180012
- sessionManager.sessions.added.off(this.sessionAddedHandler);
180013
- }
180014
- if (this.sessionDeletedHandler) {
180015
- sessionManager.sessions.deleted.off(this.sessionDeletedHandler);
180016
- }
180017
- } catch {
180018
- }
180019
- this.sessionDiagHandler = void 0;
180020
- this.sessionAddedHandler = void 0;
180021
- this.sessionDeletedHandler = void 0;
180022
- if (this.deadSessionTimer) {
180023
- clearTimeout(this.deadSessionTimer);
180024
- this.deadSessionTimer = null;
180025
- }
180026
- for (const timer of this.staleSessionTimers.values()) {
180027
- clearTimeout(timer);
180028
- }
180029
- this.staleSessionTimers.clear();
180030
- this.sessionStartedAt.clear();
180031
- }
180032
- // Warn at the fabric-added event when Alexa pairs on a non-5540 port. Alexa
180033
- // completes AddNOC but never CASEs, so the fabric rolls back on failsafe
180034
- // expiry ~20s later and the committed-fabric warning path never sees it (#401).
180035
- wireFabricWarnings() {
180036
- this.unwireFabricWarnings();
180037
- try {
180038
- const fabrics = this.server.env.get(FabricManager);
180039
- this.fabricAddedHandler = (fabric) => {
180040
- const port = this.dataProvider.port;
180041
- if (alexaPairingPortProblem(fabric.rootVendorId, port)) {
180042
- this.log.warn(
180043
- `Fabric added by Amazon Alexa (vendor ${fabric.rootVendorId}) on port ${port}. Alexa only completes pairing on port 5540, this attempt will roll back about 20s after AddNOC. Recreate the bridge on port 5540 (#401)`
180044
- );
180045
- }
180046
- };
180047
- fabrics.events.added.on(this.fabricAddedHandler);
180048
- } catch {
180049
- }
180050
- }
180051
- unwireFabricWarnings() {
180052
- try {
180053
- const fabrics = this.server.env.get(FabricManager);
180054
- if (this.fabricAddedHandler) {
180055
- fabrics.events.added.off(this.fabricAddedHandler);
180056
- }
180057
- } catch {
180058
- }
180059
- this.fabricAddedHandler = void 0;
180060
- }
180061
- // Start the periodic age-based session rotation (#287).
180062
- startSessionRotation() {
180063
- this.stopSessionRotation();
180064
- const hours = this.readSessionMaxAgeHours();
180065
- if (hours === 0) {
180066
- this.log.info(
180067
- "Session rotation disabled (HAMH_MATTER_SESSION_MAX_AGE_HOURS=0)"
180068
- );
180069
- return;
180070
- }
180071
- this.maxSessionAgeMs = hours * 60 * 60 * 1e3;
180072
- this.rotationTimer = setInterval(
180073
- () => this.rotateAgedSessions(),
180074
- ROTATION_CHECK_INTERVAL_MS
180075
- );
180076
- this.log.info(
180077
- `Session rotation: max age ${hours}h, check every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
180078
- );
180079
- }
180080
- stopSessionRotation() {
180081
- if (this.rotationTimer) {
180082
- clearInterval(this.rotationTimer);
180083
- this.rotationTimer = null;
180084
- }
180085
- }
180086
- // Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
180087
- // the one session wedged on "Updating" and rotate just that one.
180088
- startWedgeWatchdog() {
180089
- this.stopWedgeWatchdog();
180090
- if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
180091
- return;
180092
- }
180093
- this.wedgeWatchdogTimer = setInterval(
180094
- () => this.runWedgeWatchdogCheck(),
180095
- ROTATION_CHECK_INTERVAL_MS
180096
- );
180097
- this.log.info(
180098
- `Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
180099
- );
180100
- }
180101
- stopWedgeWatchdog() {
180102
- if (this.wedgeWatchdogTimer) {
180103
- clearInterval(this.wedgeWatchdogTimer);
180104
- this.wedgeWatchdogTimer = null;
180105
- }
180106
- }
180107
- // Rotate exactly the sessions the pure rule flags as wedged. Closing is the
180108
- // same graceful-then-force path age rotation uses, so a false positive just
180109
- // re-CASEs the controller.
180110
- runWedgeWatchdogCheck() {
180111
- try {
180112
- const sessionManager = this.server.env.get(SessionManager);
180113
- const now = Date.now();
180114
- const closes = [];
180115
- for (const s of [...sessionManager.sessions]) {
180116
- if (s.isClosing) continue;
180117
- const startedAt = this.sessionStartedAt.get(s.id);
180118
- const sessionAgeMs = startedAt != null ? now - startedAt : 0;
180119
- const lastImAt = this.lastImRequestAt.get(s);
180120
- const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
180121
- const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
180122
- const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
180123
- const v1 = decideWedgeRotation({
180124
- subscriptionCount: s.subscriptions.size,
180125
- sessionAgeMs,
180126
- lastImRequestMsAgo,
180127
- lastRotatedMsAgo
180128
- });
180129
- const lastCmdAt = this.lastCommandImAt.get(s);
180130
- const commandSilenceMs = lastCmdAt != null ? now - lastCmdAt : null;
180131
- const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
180132
- const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
180133
- const v2 = decideWedgeRotationV2({
180134
- subscriptionCount: s.subscriptions.size,
180135
- sessionAgeMs,
180136
- commandSilenceMs,
180137
- subscribeTimesMs: subscribeTimes,
180138
- giveUpTimesMs: giveUpTimes,
180139
- nowMs: now,
180140
- lastRotatedMsAgo
180141
- });
180142
- const cmdSilenceMin = Math.round(
180143
- (commandSilenceMs ?? sessionAgeMs) / 6e4
180144
- );
180145
- const v2Stats = `cmdSilenceMin=${cmdSilenceMin} subscribes30m=${countRecent(subscribeTimes, now)} giveUps30m=${countRecent(giveUpTimes, now)}`;
180146
- if (v2 && !v1) {
180147
- this.log.info(`wedge v2 would rotate session ${s.id}: ${v2Stats}`);
180148
- }
180149
- if (!v1) {
180150
- continue;
180151
- }
180152
- this.log.info(
180153
- `wedge v2 session ${s.id}: v1 rotated, v2 agree=${v2} ${v2Stats}`
180154
- );
180155
- const silenceMin = Math.round(
180156
- (lastImRequestMsAgo ?? sessionAgeMs) / 6e4
180157
- );
180158
- this.log.info(
180159
- `Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
180160
- );
180161
- this.wedgeLastRotatedAt.set(s, now);
180162
- closes.push(
180163
- s.initiateClose().catch(() => {
180164
- return s.initiateForceClose({
180165
- cause: new Error("wedge watchdog, forcing")
180166
- });
180167
- })
180168
- );
180169
- }
180170
- if (closes.length > 0) {
180171
- Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
180172
- }
180173
- } catch {
180174
- }
180175
- }
180176
- // Poll the interface addresses mDNS advertises and re-announce when they
180177
- // change. matter.js caches the operational records at first announcement, so
180178
- // a dynamic ISP IPv6 prefix change keeps advertising the dead global address
180179
- // until a restart (#415). refreshOperationalAdvertisement re-runs the lookup.
180180
- startMdnsAddressWatch() {
180181
- this.stopMdnsAddressWatch();
180182
- const { netInterface, stripGlobalIpv6, ipv4Enabled } = this.readMdnsWatchSettings();
180183
- this.advertisedAddressSnapshot = collectAdvertisedAddresses(
180184
- os10.networkInterfaces(),
180185
- netInterface,
180186
- stripGlobalIpv6,
180187
- ipv4Enabled
180188
- );
180189
- this.mdnsAddressTimer = setInterval(() => {
180190
- if (this.mdnsCheckInFlight) return;
180191
- this.mdnsCheckInFlight = true;
180192
- this.checkAdvertisedAddresses().catch((e) => {
180193
- this.log.warn("mDNS address check failed:", e);
180194
- }).finally(() => {
180195
- this.mdnsCheckInFlight = false;
180196
- });
180197
- }, MDNS_ADDRESS_CHECK_INTERVAL_MS2);
180198
- }
180199
- stopMdnsAddressWatch() {
180200
- if (this.mdnsAddressTimer) {
180201
- clearInterval(this.mdnsAddressTimer);
180202
- this.mdnsAddressTimer = null;
180203
- }
180204
- }
180205
- // Read the mDNS settings the shared setup applied. MdnsService lives on the
180206
- // root env and stripGlobalIpv6 swaps in a FilteredNetwork, so both are
180207
- // visible from the bridge env.
180208
- readMdnsWatchSettings() {
180209
- try {
180210
- const mdns2 = this.server.env.get(MdnsService);
180211
- const stripGlobalIpv6 = this.server.env.get(Network) instanceof FilteredNetwork;
180212
- return {
180213
- netInterface: mdns2.limitedToNetInterface,
180214
- stripGlobalIpv6,
180215
- ipv4Enabled: mdns2.enableIpv4
180216
- };
180217
- } catch {
180218
- return { stripGlobalIpv6: false, ipv4Enabled: true };
180219
- }
180220
- }
180221
- async checkAdvertisedAddresses() {
180222
- const { netInterface, stripGlobalIpv6, ipv4Enabled } = this.readMdnsWatchSettings();
180223
- const advertiser = this.server.env.get(DeviceAdvertiser);
180224
- const fabrics = this.server.env.get(FabricManager);
180225
- this.advertisedAddressSnapshot = await runMdnsAddressWatchTick({
180226
- readInterfaces: () => os10.networkInterfaces(),
180227
- netInterface,
180228
- stripGlobalIpv6,
180229
- ipv4Enabled,
180230
- currentSnapshot: this.advertisedAddressSnapshot,
180231
- fabrics: () => fabrics.fabrics,
180232
- refresh: (fabric) => advertiser.refreshOperationalAdvertisement(fabric),
180233
- onChange: (prev, curr) => {
180234
- this.log.warn(
180235
- `Advertised address set changed (${prev.length} -> ${curr.length} addresses), re-announcing operational mDNS (#415)`
180236
- );
180237
- }
180238
- });
180239
- }
180240
- // Resolve session rotation max age. Bridge config wins, then env var,
180241
- // then built-in default. 0 disables, otherwise clamped to range.
180242
- readSessionMaxAgeHours() {
180243
- const { min, max } = SESSION_MAX_AGE_HOURS_RANGE;
180244
- const fromConfig = this.dataProvider.sessionMaxAgeHours;
180245
- if (fromConfig != null && Number.isFinite(fromConfig) && fromConfig >= 0) {
180246
- if (fromConfig === 0) return 0;
180247
- if (fromConfig < min) return min;
180248
- if (fromConfig > max) return max;
180249
- return fromConfig;
180250
- }
180251
- const raw = process.env.HAMH_MATTER_SESSION_MAX_AGE_HOURS;
180252
- const parsed = parseSessionMaxAgeHours(raw);
180253
- if (parsed == null) {
180254
- this.log.warn(
180255
- `Invalid HAMH_MATTER_SESSION_MAX_AGE_HOURS=${raw}, falling back to ${DEFAULT_SESSION_MAX_AGE_HOURS}h`
180256
- );
180257
- return DEFAULT_SESSION_MAX_AGE_HOURS;
180258
- }
180259
- return parsed;
180260
- }
180261
- // Gracefully close sessions older than maxSessionAgeMs so controllers
180262
- // re-establish CASE and re-subscribe. Stale (0-sub) sessions are handled
180263
- // by the existing dead-session path, so only rotate ones with subscriptions.
180264
- rotateAgedSessions() {
180265
- if (this.maxSessionAgeMs === 0) return;
180266
- try {
180267
- const sessionManager = this.server.env.get(SessionManager);
180268
- const now = Date.now();
180269
- const closes = [];
180270
- for (const s of [...sessionManager.sessions]) {
180271
- const startedAt = this.sessionStartedAt.get(s.id);
180272
- if (startedAt == null) continue;
180273
- const ageMs = now - startedAt;
180274
- if (ageMs < this.maxSessionAgeMs || s.isClosing || s.subscriptions.size === 0) {
180275
- continue;
180276
- }
180277
- const ageMin = Math.round(ageMs / 6e4);
180278
- this.log.info(
180279
- `Rotating session ${s.id} (peer ${s.peerNodeId}, age ${ageMin}min, subs ${s.subscriptions.size})`
180280
- );
180281
- closes.push(
180282
- s.initiateClose().catch(() => {
180283
- return s.initiateForceClose({
180284
- cause: new Error("session rotation, forcing")
180285
- });
180286
- })
180287
- );
180288
- }
180289
- if (closes.length > 0) {
180290
- Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
180291
- }
180292
- } catch {
180293
- }
180294
- }
180295
179551
  stopAutoForceSync() {
180296
179552
  if (this.autoForceSyncTimer) {
180297
179553
  clearInterval(this.autoForceSyncTimer);