@riddix/hamh 2.1.0-alpha.853 → 2.1.0-alpha.855

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,9 +19,9 @@
19
19
  ---
20
20
 
21
21
  > [!NOTE]
22
- > 🔀 **Community Fork** - This is a fork of the original [t0bst4r/home-assistant-matter-hub](https://github.com/t0bst4r/home-assistant-matter-hub), which was discontinued in January 2026. We continue active development with bug fixes, new features, and community support. Thank you **[@t0bst4r](https://github.com/t0bst4r)** for the original work! ❤️
22
+ > 🔀 **Community Fork** - This is the actively maintained fork and successor of the original [t0bst4r/home-assistant-matter-hub](https://github.com/t0bst4r/home-assistant-matter-hub), which was discontinued in January 2026 and has since been archived. We continue active development with bug fixes, new features, and community support. Thank you **[@t0bst4r](https://github.com/t0bst4r)** for the original work! ❤️
23
23
  >
24
- > **📦 Migrating?** See [Migration Guide](#migration-from-t0bst4r) - your paired devices will continue to work!
24
+ > **📦 Migrating?** See [Migration Guide](#migration-from-t0bst4r) - your paired devices will continue to work! For a side-by-side feature list, see the [fork comparison](https://riddix.github.io/home-assistant-matter-hub/getting-started/fork-comparison). The add-on slugs are `hamh` (stable), `hamh-alpha` and `hamh-testing`.
25
25
 
26
26
  ---
27
27
 
@@ -133493,6 +133493,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133493
133493
  lockPinMaxLength: config8.lockPinMaxLength,
133494
133494
  powerEntity: config8.powerEntity,
133495
133495
  energyEntity: config8.energyEntity,
133496
+ meterSerialNumber: config8.meterSerialNumber,
133497
+ pointOfDelivery: config8.pointOfDelivery,
133496
133498
  voltageEntity: config8.voltageEntity,
133497
133499
  currentEntity: config8.currentEntity,
133498
133500
  batteryPowerEntity: config8.batteryPowerEntity,
@@ -133657,6 +133659,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133657
133659
  lockPinMaxLength: config8.lockPinMaxLength,
133658
133660
  powerEntity: config8.powerEntity,
133659
133661
  energyEntity: config8.energyEntity,
133662
+ meterSerialNumber: config8.meterSerialNumber,
133663
+ pointOfDelivery: config8.pointOfDelivery,
133660
133664
  voltageEntity: config8.voltageEntity,
133661
133665
  currentEntity: config8.currentEntity,
133662
133666
  batteryPowerEntity: config8.batteryPowerEntity,
@@ -134512,6 +134516,8 @@ function entityMappingApi(mappingStorage, identityStorage) {
134512
134516
  lockPinMaxLength: body.lockPinMaxLength,
134513
134517
  powerEntity: body.powerEntity,
134514
134518
  energyEntity: body.energyEntity,
134519
+ meterSerialNumber: body.meterSerialNumber,
134520
+ pointOfDelivery: body.pointOfDelivery,
134515
134521
  voltageEntity: body.voltageEntity,
134516
134522
  currentEntity: body.currentEntity,
134517
134523
  batteryPowerEntity: body.batteryPowerEntity,
@@ -135016,6 +135022,8 @@ function configToProfileEntry(config8) {
135016
135022
  lockPinMaxLength: config8.lockPinMaxLength,
135017
135023
  powerEntity: config8.powerEntity,
135018
135024
  energyEntity: config8.energyEntity,
135025
+ meterSerialNumber: config8.meterSerialNumber,
135026
+ pointOfDelivery: config8.pointOfDelivery,
135019
135027
  voltageEntity: config8.voltageEntity,
135020
135028
  currentEntity: config8.currentEntity,
135021
135029
  batteryPowerEntity: config8.batteryPowerEntity,
@@ -135158,6 +135166,8 @@ function mappingProfileApi(mappingStorage) {
135158
135166
  lockPinMaxLength: entry.lockPinMaxLength,
135159
135167
  powerEntity: entry.powerEntity,
135160
135168
  energyEntity: entry.energyEntity,
135169
+ meterSerialNumber: entry.meterSerialNumber,
135170
+ pointOfDelivery: entry.pointOfDelivery,
135161
135171
  voltageEntity: entry.voltageEntity,
135162
135172
  currentEntity: entry.currentEntity,
135163
135173
  batteryPowerEntity: entry.batteryPowerEntity,
@@ -139936,6 +139946,8 @@ var EntityMappingStorage = class extends Service {
139936
139946
  lockPinMaxLength: pinLengths.max,
139937
139947
  powerEntity: request.powerEntity?.trim() || void 0,
139938
139948
  energyEntity: request.energyEntity?.trim() || void 0,
139949
+ meterSerialNumber: request.meterSerialNumber?.trim() || void 0,
139950
+ pointOfDelivery: request.pointOfDelivery?.trim() || void 0,
139939
139951
  voltageEntity: request.voltageEntity?.trim() || void 0,
139940
139952
  currentEntity: request.currentEntity?.trim() || void 0,
139941
139953
  batteryPowerEntity: request.batteryPowerEntity?.trim() || void 0,
@@ -139971,7 +139983,7 @@ var EntityMappingStorage = class extends Service {
139971
139983
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0,
139972
139984
  disableMomentaryFlip: request.disableMomentaryFlip || void 0
139973
139985
  };
139974
- if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
139986
+ if (!config8.matterDeviceType && !config8.customName && !config8.customProductName && !config8.customVendorName && !config8.customSerialNumber && config8.customVendorId === void 0 && config8.disabled !== true && !config8.filterLifeEntity && !config8.cleaningModeEntity && !config8.temperatureEntity && !config8.humidityEntity && !config8.batteryEntity && !config8.disableBatteryMapping && !config8.chargingStateEntity && !config8.roomEntities && !config8.disableLockPin && !config8.lockUsercodeService && config8.lockUsercodeSlot === void 0 && config8.lockPinMinLength === void 0 && config8.lockPinMaxLength === void 0 && !config8.powerEntity && !config8.energyEntity && !config8.meterSerialNumber && !config8.pointOfDelivery && !config8.voltageEntity && !config8.currentEntity && !config8.batteryPowerEntity && !config8.batteryEnergyEntity && !config8.chargingSwitchEntity && !config8.currentLimitEntity && !config8.pressureEntity && !config8.suctionLevelEntity && !config8.mopIntensityEntity && (!config8.customServiceAreas || config8.customServiceAreas.length === 0) && (!config8.customFanSpeedTags || Object.keys(config8.customFanSpeedTags).length === 0) && (!config8.fanWindPresets || (config8.fanWindPresets.natural?.length ?? 0) === 0 && (config8.fanWindPresets.sleep?.length ?? 0) === 0) && !config8.fanRestoreSpeedOnPowerOn && !config8.currentRoomEntity && !config8.cleanedAreaEntity && !config8.vacuumAscendingRoomOrder && !config8.disableCustomAreaRoomModes && !config8.valetudoIdentifier && !config8.coverSwapOpenClose && !config8.coverExposeAsDimmableLight && !config8.selectExposeAsSwitch && !config8.selectSwitchOnOption && !config8.selectSwitchOffOption && !config8.coverSliderDebounceMs && !config8.updateThrottleMs && !config8.disableClimateOnOff && !config8.disableClimateFanControl && !config8.climateKeepModeOnIdle && !config8.climateExposeFan && !config8.climateAutoMode && (!config8.composedEntities || config8.composedEntities.length === 0) && !config8.disableMomentaryFlip) {
139975
139987
  bridgeMap.delete(request.entityId);
139976
139988
  } else {
139977
139989
  bridgeMap.set(request.entityId, config8);
@@ -149611,6 +149623,7 @@ var DoorbellDeviceDefinition = MutableEndpoint({
149611
149623
  behaviors: SupportedBehaviors(DoorbellRequirements.server.mandatory.Identify)
149612
149624
  });
149613
149625
  Object.freeze(DoorbellDeviceDefinition);
149626
+ var DoorbellDevice = DoorbellDeviceDefinition;
149614
149627
 
149615
149628
  // ../../node_modules/.pnpm/@matter+types@0.17.9/node_modules/@matter/types/dist/esm/clusters/commodity-price.js
149616
149629
  init_ClusterType();
@@ -149834,6 +149847,7 @@ var ElectricalUtilityMeterDeviceDefinition = MutableEndpoint({
149834
149847
  behaviors: SupportedBehaviors(ElectricalUtilityMeterRequirements.server.mandatory.MeterIdentification)
149835
149848
  });
149836
149849
  Object.freeze(ElectricalUtilityMeterDeviceDefinition);
149850
+ var ElectricalUtilityMeterDevice = ElectricalUtilityMeterDeviceDefinition;
149837
149851
 
149838
149852
  // ../../node_modules/.pnpm/@matter+types@0.17.9/node_modules/@matter/types/dist/esm/clusters/energy-evse.js
149839
149853
  init_ClusterType();
@@ -157731,9 +157745,39 @@ function decideWedgeRotation(input) {
157731
157745
  const imSilenceMs = lastImRequestMsAgo == null ? sessionAgeMs : lastImRequestMsAgo;
157732
157746
  return subscriptionCount > 0 && sessionAgeMs > WEDGE_WARMUP_MS && imSilenceMs > WEDGE_IM_SILENCE_MS && (lastRotatedMsAgo == null || lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS);
157733
157747
  }
157748
+ var WEDGE_V2_COMMAND_SILENCE_MS = 45 * 60 * 1e3;
157749
+ var WEDGE_V2_WINDOW_MS = 30 * 60 * 1e3;
157750
+ var WEDGE_V2_MIN_SUBSCRIBES = 3;
157751
+ var WEDGE_V2_MIN_GIVE_UPS = 2;
157752
+ var WEDGE_RING_SIZE = 10;
157753
+ var WEDGE_GIVE_UP_QUIET_MS = 5e3;
157754
+ function inWindow(timesMs, nowMs) {
157755
+ return timesMs.filter((t) => nowMs - t <= WEDGE_V2_WINDOW_MS);
157756
+ }
157757
+ function countRecent(timesMs, nowMs) {
157758
+ return inWindow(timesMs, nowMs).length;
157759
+ }
157760
+ function decideWedgeRotationV2(input) {
157761
+ const silenceMs = input.commandSilenceMs ?? input.sessionAgeMs;
157762
+ if (input.subscriptionCount <= 0) return false;
157763
+ if (input.sessionAgeMs <= WEDGE_WARMUP_MS) return false;
157764
+ if (silenceMs <= WEDGE_V2_COMMAND_SILENCE_MS) return false;
157765
+ const subscribes = inWindow(input.subscribeTimesMs, input.nowMs);
157766
+ const giveUps = inWindow(input.giveUpTimesMs, input.nowMs);
157767
+ if (subscribes.length < WEDGE_V2_MIN_SUBSCRIBES) return false;
157768
+ if (giveUps.length < WEDGE_V2_MIN_GIVE_UPS) return false;
157769
+ if (Math.max(...giveUps) <= Math.min(...subscribes)) return false;
157770
+ return input.lastRotatedMsAgo == null || input.lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS;
157771
+ }
157734
157772
 
157735
157773
  // src/services/bridges/bridge.ts
157736
157774
  var imWrapMarker = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
157775
+ var commandMessageTypes = [
157776
+ MessageType.ReadRequest,
157777
+ MessageType.WriteRequest,
157778
+ MessageType.InvokeRequest,
157779
+ MessageType.TimedRequest
157780
+ ];
157737
157781
  var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
157738
157782
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
157739
157783
  var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
@@ -157787,6 +157831,13 @@ var Bridge = class {
157787
157831
  lastImRequestAt = /* @__PURE__ */ new WeakMap();
157788
157832
  wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
157789
157833
  wedgeWatchdogTimer = null;
157834
+ // V2 shadow signals (#365): last command-class request per session, plus
157835
+ // rings of subscribe and delivery give-up times (newest WEDGE_RING_SIZE).
157836
+ lastCommandImAt = /* @__PURE__ */ new WeakMap();
157837
+ subscribeTimesMs = /* @__PURE__ */ new WeakMap();
157838
+ giveUpTimesMs = /* @__PURE__ */ new WeakMap();
157839
+ // Sessions whose subscriptions.deleted we already hooked for give-ups.
157840
+ wedgeHookedSessions = /* @__PURE__ */ new WeakSet();
157790
157841
  // Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
157791
157842
  // change forces a fresh operational announcement (#415).
157792
157843
  mdnsAddressTimer = null;
@@ -157850,6 +157901,20 @@ var Bridge = class {
157850
157901
  const lastImAt = this.lastImRequestAt.get(s);
157851
157902
  const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
157852
157903
  const startedAt = this.sessionStartedAt.get(s.id);
157904
+ const lastCmdAt = this.lastCommandImAt.get(s);
157905
+ const lastCommandImRequestMsAgo = lastCmdAt != null ? nowMs - lastCmdAt : null;
157906
+ const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
157907
+ const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
157908
+ const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
157909
+ const wedgeV2WouldRotate = decideWedgeRotationV2({
157910
+ subscriptionCount: subCount,
157911
+ sessionAgeMs: startedAt != null ? nowMs - startedAt : 0,
157912
+ commandSilenceMs: lastCommandImRequestMsAgo,
157913
+ subscribeTimesMs: subscribeTimes,
157914
+ giveUpTimesMs: giveUpTimes,
157915
+ nowMs,
157916
+ lastRotatedMsAgo: lastRotatedAt != null ? nowMs - lastRotatedAt : null
157917
+ });
157853
157918
  return {
157854
157919
  id: s.id,
157855
157920
  peerNodeId: String(s.peerNodeId),
@@ -157859,6 +157924,10 @@ var Bridge = class {
157859
157924
  lastActiveMsAgo,
157860
157925
  lastAnyActivityMsAgo,
157861
157926
  lastImRequestMsAgo,
157927
+ lastCommandImRequestMsAgo,
157928
+ subscribesLast30Min: countRecent(subscribeTimes, nowMs),
157929
+ giveUpsLast30Min: countRecent(giveUpTimes, nowMs),
157930
+ wedgeV2WouldRotate,
157862
157931
  isPeerActive: Boolean(s.isPeerActive),
157863
157932
  ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
157864
157933
  };
@@ -158088,6 +158157,15 @@ ${e?.toString()}`);
158088
158157
  try {
158089
158158
  const sessionManager = this.server.env.get(SessionManager);
158090
158159
  seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
158160
+ const hookGiveUpsWithPeers = (sess) => this.hookSubscriptionGiveUps(
158161
+ sess,
158162
+ () => [...sessionManager.sessions].filter(
158163
+ (s) => s.peerNodeId === sess.peerNodeId && s.fabric?.fabricIndex === sess.fabric?.fabricIndex
158164
+ )
158165
+ );
158166
+ for (const sess of sessionManager.sessions) {
158167
+ hookGiveUpsWithPeers(sess);
158168
+ }
158091
158169
  this.sessionDiagHandler = (session) => {
158092
158170
  const sessions = [...sessionManager.sessions];
158093
158171
  let totalSubs = 0;
@@ -158150,6 +158228,7 @@ ${e?.toString()}`);
158150
158228
  sessionManager.subscriptionsChanged.on(this.sessionDiagHandler);
158151
158229
  this.sessionAddedHandler = (newSession) => {
158152
158230
  this.sessionStartedAt.set(newSession.id, Date.now());
158231
+ hookGiveUpsWithPeers(newSession);
158153
158232
  this.log.info(
158154
158233
  `Session opened: id=${newSession.id} peer=${newSession.peerNodeId}`
158155
158234
  );
@@ -158243,7 +158322,14 @@ ${e?.toString()}`);
158243
158322
  is.onNewExchange = (exchange, message) => {
158244
158323
  const session = exchange.session;
158245
158324
  if (session) {
158246
- this.lastImRequestAt.set(session, Date.now());
158325
+ const now = Date.now();
158326
+ this.lastImRequestAt.set(session, now);
158327
+ const type = message?.payloadHeader?.messageType;
158328
+ if (type === MessageType.SubscribeRequest) {
158329
+ this.pushWedgeRing(this.subscribeTimesMs, session, now);
158330
+ } else if (type != null && commandMessageTypes.includes(type)) {
158331
+ this.lastCommandImAt.set(session, now);
158332
+ }
158247
158333
  }
158248
158334
  return original(exchange, message);
158249
158335
  };
@@ -158251,6 +158337,52 @@ ${e?.toString()}`);
158251
158337
  } catch {
158252
158338
  }
158253
158339
  }
158340
+ // Keep only the newest WEDGE_RING_SIZE timestamps per session.
158341
+ pushWedgeRing(ring, session, now) {
158342
+ const times = ring.get(session) ?? [];
158343
+ times.push(now);
158344
+ if (times.length > WEDGE_RING_SIZE) {
158345
+ times.splice(0, times.length - WEDGE_RING_SIZE);
158346
+ }
158347
+ ring.set(session, times);
158348
+ }
158349
+ // Watch this session's subscriptions for server-side delivery give-ups
158350
+ // (#365 v2 shadow). isTerminated true fires both on a peer cancel and on
158351
+ // the 3-strikes delivery give-up; only the give-up arrives without a
158352
+ // coincident inbound IM request, so that is the discriminator.
158353
+ hookSubscriptionGiveUps(session, peerSessions) {
158354
+ const key = session;
158355
+ if (this.wedgeHookedSessions.has(key)) {
158356
+ return;
158357
+ }
158358
+ const deleted = session.subscriptions?.deleted;
158359
+ if (typeof deleted?.on !== "function") {
158360
+ return;
158361
+ }
158362
+ this.wedgeHookedSessions.add(key);
158363
+ deleted.on((sub) => {
158364
+ if (sub?.isTerminated !== true) {
158365
+ return;
158366
+ }
158367
+ const now = Date.now();
158368
+ const fresh = (s) => {
158369
+ const at = this.lastImRequestAt.get(s);
158370
+ return at != null && now - at <= WEDGE_GIVE_UP_QUIET_MS;
158371
+ };
158372
+ if (fresh(key)) {
158373
+ return;
158374
+ }
158375
+ for (const s of peerSessions?.() ?? []) {
158376
+ if (s !== key && fresh(s)) {
158377
+ return;
158378
+ }
158379
+ }
158380
+ this.pushWedgeRing(this.giveUpTimesMs, key, now);
158381
+ this.log.debug(
158382
+ `wedge v2 give-up recorded sub=${sub?.subscriptionId}`
158383
+ );
158384
+ });
158385
+ }
158254
158386
  closeStaleSession(sessionId) {
158255
158387
  try {
158256
158388
  const sessionManager = this.server.env.get(SessionManager);
@@ -158513,14 +158645,38 @@ ${e?.toString()}`);
158513
158645
  const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
158514
158646
  const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
158515
158647
  const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
158516
- if (!decideWedgeRotation({
158648
+ const v1 = decideWedgeRotation({
158517
158649
  subscriptionCount: s.subscriptions.size,
158518
158650
  sessionAgeMs,
158519
158651
  lastImRequestMsAgo,
158520
158652
  lastRotatedMsAgo
158521
- })) {
158653
+ });
158654
+ const lastCmdAt = this.lastCommandImAt.get(s);
158655
+ const commandSilenceMs = lastCmdAt != null ? now - lastCmdAt : null;
158656
+ const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
158657
+ const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
158658
+ const v2 = decideWedgeRotationV2({
158659
+ subscriptionCount: s.subscriptions.size,
158660
+ sessionAgeMs,
158661
+ commandSilenceMs,
158662
+ subscribeTimesMs: subscribeTimes,
158663
+ giveUpTimesMs: giveUpTimes,
158664
+ nowMs: now,
158665
+ lastRotatedMsAgo
158666
+ });
158667
+ const cmdSilenceMin = Math.round(
158668
+ (commandSilenceMs ?? sessionAgeMs) / 6e4
158669
+ );
158670
+ const v2Stats = `cmdSilenceMin=${cmdSilenceMin} subscribes30m=${countRecent(subscribeTimes, now)} giveUps30m=${countRecent(giveUpTimes, now)}`;
158671
+ if (v2 && !v1) {
158672
+ this.log.info(`wedge v2 would rotate session ${s.id}: ${v2Stats}`);
158673
+ }
158674
+ if (!v1) {
158522
158675
  continue;
158523
158676
  }
158677
+ this.log.info(
158678
+ `wedge v2 session ${s.id}: v1 rotated, v2 agree=${v2} ${v2Stats}`
158679
+ );
158524
158680
  const silenceMin = Math.round(
158525
158681
  (lastImRequestMsAgo ?? sessionAgeMs) / 6e4
158526
158682
  );
@@ -164991,6 +165147,9 @@ var HaGenericSwitchServer = HaGenericSwitchServerMultiBase.set({
164991
165147
  multiPressMax: 3
164992
165148
  });
164993
165149
 
165150
+ // src/matter/endpoints/legacy/event/doorbell.ts
165151
+ init_home_assistant_entity_behavior();
165152
+
164994
165153
  // src/matter/endpoints/legacy/event/index.ts
164995
165154
  init_home_assistant_entity_behavior();
164996
165155
  var EventEndpointTypeMulti = GenericSwitchDevice.with(
@@ -165033,6 +165192,33 @@ function EventDevice(homeAssistantEntity) {
165033
165192
  });
165034
165193
  }
165035
165194
 
165195
+ // src/matter/endpoints/legacy/event/doorbell.ts
165196
+ var DoorbellEndpointTypeMulti = DoorbellDevice.with(
165197
+ BasicInformationServer2,
165198
+ IdentifyServer2,
165199
+ HomeAssistantEntityBehavior,
165200
+ HaGenericSwitchServer
165201
+ );
165202
+ var DoorbellEndpointTypeSimple = DoorbellDevice.with(
165203
+ BasicInformationServer2,
165204
+ IdentifyServer2,
165205
+ HomeAssistantEntityBehavior,
165206
+ HaGenericSwitchServerSimple
165207
+ );
165208
+ function DoorbellDevice2(homeAssistantEntity) {
165209
+ const attrs = homeAssistantEntity.entity.state.attributes;
165210
+ const multiPressMax = detectMultiPressMax(attrs.event_types ?? []);
165211
+ if (multiPressMax >= 2) {
165212
+ return DoorbellEndpointTypeMulti.set({
165213
+ homeAssistantEntity,
165214
+ switch: { multiPressMax }
165215
+ });
165216
+ }
165217
+ return DoorbellEndpointTypeSimple.set({
165218
+ homeAssistantEntity
165219
+ });
165220
+ }
165221
+
165036
165222
  // src/matter/endpoints/legacy/fan/index.ts
165037
165223
  init_dist();
165038
165224
  init_home_assistant_entity_behavior();
@@ -168613,6 +168799,38 @@ var ElectricalMeterType = ElectricalMeterDevice.with(
168613
168799
  HaPowerTopologyServer
168614
168800
  );
168615
168801
 
168802
+ // src/matter/endpoints/legacy/sensor/devices/electrical-utility-meter.ts
168803
+ init_home_assistant_entity_behavior();
168804
+ var HaMeterIdentificationServer = class extends MeterIdentificationServer {
168805
+ async initialize() {
168806
+ await super.initialize();
168807
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168808
+ const mapping = homeAssistant.state.mapping;
168809
+ applyPatchState(this.state, {
168810
+ meterSerialNumber: mapping?.meterSerialNumber ?? null,
168811
+ pointOfDelivery: mapping?.pointOfDelivery ?? null
168812
+ });
168813
+ }
168814
+ };
168815
+ ((HaMeterIdentificationServer2) => {
168816
+ class State extends MeterIdentificationServer.State {
168817
+ }
168818
+ HaMeterIdentificationServer2.State = State;
168819
+ })(HaMeterIdentificationServer || (HaMeterIdentificationServer = {}));
168820
+ var ElectricalUtilityMeterType = ElectricalUtilityMeterDevice.with(
168821
+ BasicInformationServer2,
168822
+ IdentifyServer2,
168823
+ HomeAssistantEntityBehavior,
168824
+ HaMeterIdentificationServer.set({
168825
+ meterType: null,
168826
+ pointOfDelivery: null,
168827
+ meterSerialNumber: null
168828
+ }),
168829
+ PowerServer,
168830
+ EnergyServer,
168831
+ HaPowerTopologyServer
168832
+ );
168833
+
168616
168834
  // src/matter/endpoints/legacy/sensor/devices/energy-evse.ts
168617
168835
  init_esm();
168618
168836
  init_home_assistant_actions();
@@ -173433,6 +173651,8 @@ var matterDeviceTypeFactories = {
173433
173651
  // Legacy SolarPower alias, kept for existing mappings.
173434
173652
  electrical_sensor: (ha) => ElectricalSensorType.set({ homeAssistantEntity: ha }),
173435
173653
  solar_power: (ha) => ElectricalSensorType.set({ homeAssistantEntity: ha }),
173654
+ // Matter 1.4 utility meter (0x0511), opt-in so existing pairings keep 0x0514.
173655
+ electrical_utility_meter: (ha) => ElectricalUtilityMeterType.set({ homeAssistantEntity: ha }),
173436
173656
  evse: (ha) => energyEvseType().set({ homeAssistantEntity: ha }),
173437
173657
  contact_sensor: (ha) => ContactSensorType.set({ homeAssistantEntity: ha }),
173438
173658
  motion_sensor: (ha) => MotionSensorType.set({ homeAssistantEntity: ha }),
@@ -173444,6 +173664,7 @@ var matterDeviceTypeFactories = {
173444
173664
  water_heater: WaterHeaterDevice2,
173445
173665
  water_heater_management: WaterHeaterManagementDevice,
173446
173666
  generic_switch: EventDevice,
173667
+ doorbell: DoorbellDevice2,
173447
173668
  smoke_co_alarm: (ha) => SmokeAlarmType.set({ homeAssistantEntity: ha }),
173448
173669
  water_freeze_detector: (ha) => WaterFreezeDetectorType.set({ homeAssistantEntity: ha }),
173449
173670
  water_leak_detector: (ha) => WaterLeakDetectorType.set({ homeAssistantEntity: ha })
@@ -176895,6 +177116,12 @@ init_esm7();
176895
177116
  import * as os10 from "node:os";
176896
177117
  init_diagnostic_event_bus();
176897
177118
  var imWrapMarker2 = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
177119
+ var commandMessageTypes2 = [
177120
+ MessageType.ReadRequest,
177121
+ MessageType.WriteRequest,
177122
+ MessageType.InvokeRequest,
177123
+ MessageType.TimedRequest
177124
+ ];
176898
177125
  var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
176899
177126
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2 = 2500;
176900
177127
  var MDNS_ADDRESS_CHECK_INTERVAL_MS2 = 6e4;
@@ -176933,6 +177160,13 @@ var ServerModeBridge = class {
176933
177160
  lastImRequestAt = /* @__PURE__ */ new WeakMap();
176934
177161
  wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
176935
177162
  wedgeWatchdogTimer = null;
177163
+ // V2 shadow signals (#365): last command-class request per session, plus
177164
+ // rings of subscribe and delivery give-up times (newest WEDGE_RING_SIZE).
177165
+ lastCommandImAt = /* @__PURE__ */ new WeakMap();
177166
+ subscribeTimesMs = /* @__PURE__ */ new WeakMap();
177167
+ giveUpTimesMs = /* @__PURE__ */ new WeakMap();
177168
+ // Sessions whose subscriptions.deleted we already hooked for give-ups.
177169
+ wedgeHookedSessions = /* @__PURE__ */ new WeakSet();
176936
177170
  // Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
176937
177171
  // change forces a fresh operational announcement (#415).
176938
177172
  mdnsAddressTimer = null;
@@ -177013,6 +177247,20 @@ var ServerModeBridge = class {
177013
177247
  const lastImAt = this.lastImRequestAt.get(s);
177014
177248
  const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
177015
177249
  const startedAt = this.sessionStartedAt.get(s.id);
177250
+ const lastCmdAt = this.lastCommandImAt.get(s);
177251
+ const lastCommandImRequestMsAgo = lastCmdAt != null ? nowMs - lastCmdAt : null;
177252
+ const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
177253
+ const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
177254
+ const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
177255
+ const wedgeV2WouldRotate = decideWedgeRotationV2({
177256
+ subscriptionCount: subCount,
177257
+ sessionAgeMs: startedAt != null ? nowMs - startedAt : 0,
177258
+ commandSilenceMs: lastCommandImRequestMsAgo,
177259
+ subscribeTimesMs: subscribeTimes,
177260
+ giveUpTimesMs: giveUpTimes,
177261
+ nowMs,
177262
+ lastRotatedMsAgo: lastRotatedAt != null ? nowMs - lastRotatedAt : null
177263
+ });
177016
177264
  return {
177017
177265
  id: s.id,
177018
177266
  peerNodeId: String(s.peerNodeId),
@@ -177022,6 +177270,10 @@ var ServerModeBridge = class {
177022
177270
  lastActiveMsAgo,
177023
177271
  lastAnyActivityMsAgo,
177024
177272
  lastImRequestMsAgo,
177273
+ lastCommandImRequestMsAgo,
177274
+ subscribesLast30Min: countRecent(subscribeTimes, nowMs),
177275
+ giveUpsLast30Min: countRecent(giveUpTimes, nowMs),
177276
+ wedgeV2WouldRotate,
177025
177277
  isPeerActive: Boolean(s.isPeerActive),
177026
177278
  ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
177027
177279
  };
@@ -177245,6 +177497,7 @@ ${e?.toString()}`);
177245
177497
  sessionManager.subscriptionsChanged.on(this.sessionDiagHandler);
177246
177498
  this.sessionAddedHandler = (newSession) => {
177247
177499
  this.sessionStartedAt.set(newSession.id, Date.now());
177500
+ hookGiveUpsWithPeers(newSession);
177248
177501
  this.log.info(
177249
177502
  `Session opened: id=${newSession.id} peer=${newSession.peerNodeId}`
177250
177503
  );
@@ -177321,6 +177574,15 @@ ${e?.toString()}`);
177321
177574
  sessionManager.sessions.added.on(this.sessionAddedHandler);
177322
177575
  sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
177323
177576
  seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
177577
+ const hookGiveUpsWithPeers = (sess) => this.hookSubscriptionGiveUps(
177578
+ sess,
177579
+ () => [...sessionManager.sessions].filter(
177580
+ (s) => s.peerNodeId === sess.peerNodeId && s.fabric?.fabricIndex === sess.fabric?.fabricIndex
177581
+ )
177582
+ );
177583
+ for (const sess of sessionManager.sessions) {
177584
+ hookGiveUpsWithPeers(sess);
177585
+ }
177324
177586
  this.wireImRequestTracking();
177325
177587
  } catch {
177326
177588
  }
@@ -177339,7 +177601,14 @@ ${e?.toString()}`);
177339
177601
  is.onNewExchange = (exchange, message) => {
177340
177602
  const session = exchange.session;
177341
177603
  if (session) {
177342
- this.lastImRequestAt.set(session, Date.now());
177604
+ const now = Date.now();
177605
+ this.lastImRequestAt.set(session, now);
177606
+ const type = message?.payloadHeader?.messageType;
177607
+ if (type === MessageType.SubscribeRequest) {
177608
+ this.pushWedgeRing(this.subscribeTimesMs, session, now);
177609
+ } else if (type != null && commandMessageTypes2.includes(type)) {
177610
+ this.lastCommandImAt.set(session, now);
177611
+ }
177343
177612
  }
177344
177613
  return original(exchange, message);
177345
177614
  };
@@ -177347,6 +177616,52 @@ ${e?.toString()}`);
177347
177616
  } catch {
177348
177617
  }
177349
177618
  }
177619
+ // Keep only the newest WEDGE_RING_SIZE timestamps per session.
177620
+ pushWedgeRing(ring, session, now) {
177621
+ const times = ring.get(session) ?? [];
177622
+ times.push(now);
177623
+ if (times.length > WEDGE_RING_SIZE) {
177624
+ times.splice(0, times.length - WEDGE_RING_SIZE);
177625
+ }
177626
+ ring.set(session, times);
177627
+ }
177628
+ // Watch this session's subscriptions for server-side delivery give-ups
177629
+ // (#365 v2 shadow). isTerminated true fires both on a peer cancel and on
177630
+ // the 3-strikes delivery give-up; only the give-up arrives without a
177631
+ // coincident inbound IM request, so that is the discriminator.
177632
+ hookSubscriptionGiveUps(session, peerSessions) {
177633
+ const key = session;
177634
+ if (this.wedgeHookedSessions.has(key)) {
177635
+ return;
177636
+ }
177637
+ const deleted = session.subscriptions?.deleted;
177638
+ if (typeof deleted?.on !== "function") {
177639
+ return;
177640
+ }
177641
+ this.wedgeHookedSessions.add(key);
177642
+ deleted.on((sub) => {
177643
+ if (sub?.isTerminated !== true) {
177644
+ return;
177645
+ }
177646
+ const now = Date.now();
177647
+ const fresh = (s) => {
177648
+ const at = this.lastImRequestAt.get(s);
177649
+ return at != null && now - at <= WEDGE_GIVE_UP_QUIET_MS;
177650
+ };
177651
+ if (fresh(key)) {
177652
+ return;
177653
+ }
177654
+ for (const s of peerSessions?.() ?? []) {
177655
+ if (s !== key && fresh(s)) {
177656
+ return;
177657
+ }
177658
+ }
177659
+ this.pushWedgeRing(this.giveUpTimesMs, key, now);
177660
+ this.log.debug(
177661
+ `wedge v2 give-up recorded sub=${sub?.subscriptionId}`
177662
+ );
177663
+ });
177664
+ }
177350
177665
  closeStaleSession(sessionId) {
177351
177666
  try {
177352
177667
  const sessionManager = this.server.env.get(SessionManager);
@@ -177598,14 +177913,38 @@ ${e?.toString()}`);
177598
177913
  const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
177599
177914
  const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
177600
177915
  const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
177601
- if (!decideWedgeRotation({
177916
+ const v1 = decideWedgeRotation({
177602
177917
  subscriptionCount: s.subscriptions.size,
177603
177918
  sessionAgeMs,
177604
177919
  lastImRequestMsAgo,
177605
177920
  lastRotatedMsAgo
177606
- })) {
177921
+ });
177922
+ const lastCmdAt = this.lastCommandImAt.get(s);
177923
+ const commandSilenceMs = lastCmdAt != null ? now - lastCmdAt : null;
177924
+ const subscribeTimes = this.subscribeTimesMs.get(s) ?? [];
177925
+ const giveUpTimes = this.giveUpTimesMs.get(s) ?? [];
177926
+ const v2 = decideWedgeRotationV2({
177927
+ subscriptionCount: s.subscriptions.size,
177928
+ sessionAgeMs,
177929
+ commandSilenceMs,
177930
+ subscribeTimesMs: subscribeTimes,
177931
+ giveUpTimesMs: giveUpTimes,
177932
+ nowMs: now,
177933
+ lastRotatedMsAgo
177934
+ });
177935
+ const cmdSilenceMin = Math.round(
177936
+ (commandSilenceMs ?? sessionAgeMs) / 6e4
177937
+ );
177938
+ const v2Stats = `cmdSilenceMin=${cmdSilenceMin} subscribes30m=${countRecent(subscribeTimes, now)} giveUps30m=${countRecent(giveUpTimes, now)}`;
177939
+ if (v2 && !v1) {
177940
+ this.log.info(`wedge v2 would rotate session ${s.id}: ${v2Stats}`);
177941
+ }
177942
+ if (!v1) {
177607
177943
  continue;
177608
177944
  }
177945
+ this.log.info(
177946
+ `wedge v2 session ${s.id}: v1 rotated, v2 agree=${v2} ${v2Stats}`
177947
+ );
177609
177948
  const silenceMin = Math.round(
177610
177949
  (lastImRequestMsAgo ?? sessionAgeMs) / 6e4
177611
177950
  );