@riddix/hamh 2.0.50 → 2.0.52

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.
@@ -1767,8 +1767,8 @@ var init_Cancelable = __esm({
1767
1767
  };
1768
1768
  return result;
1769
1769
  }
1770
- static set logger(logger247) {
1771
- this.#logger = logger247;
1770
+ static set logger(logger251) {
1771
+ this.#logger = logger251;
1772
1772
  }
1773
1773
  static get logger() {
1774
1774
  return this.#logger;
@@ -3475,13 +3475,13 @@ var init_Logger = __esm({
3475
3475
  *
3476
3476
  * @deprecated use {@link destinations}
3477
3477
  */
3478
- static addLogger(identifier, logger247, options) {
3478
+ static addLogger(identifier, logger251, options) {
3479
3479
  if (identifier in this.destinations) {
3480
3480
  throw new ImplementationError(`Logger "${identifier}" already exists`);
3481
3481
  }
3482
3482
  const dest = LogDestination({ name: identifier });
3483
3483
  const legacy = adaptDestinationToLegacy(dest);
3484
- legacy.log = logger247;
3484
+ legacy.log = logger251;
3485
3485
  if (options?.defaultLogLevel !== void 0) {
3486
3486
  legacy.defaultLogLevel = options.defaultLogLevel;
3487
3487
  }
@@ -5687,8 +5687,8 @@ var init_Construction = __esm({
5687
5687
  }
5688
5688
  }
5689
5689
  #unhandledError(...args) {
5690
- const logger247 = Logger.get(this.#subject.constructor.name);
5691
- logger247.error(...args);
5690
+ const logger251 = Logger.get(this.#subject.constructor.name);
5691
+ logger251.error(...args);
5692
5692
  }
5693
5693
  #createErrorHandler(name) {
5694
5694
  return (e) => {
@@ -11712,18 +11712,18 @@ var init_FileHandleTracker = __esm({
11712
11712
  logger12 = Logger.get("FileHandleTracker");
11713
11713
  ((FileHandleTracker2) => {
11714
11714
  const openHandles = /* @__PURE__ */ new Set();
11715
- const registry2 = new FinalizationRegistry((info) => {
11715
+ const registry3 = new FinalizationRegistry((info) => {
11716
11716
  logger12.warn(`File handle GC'd without close: "${info.purpose}" for ${info.path}`);
11717
11717
  });
11718
11718
  function register(handle) {
11719
11719
  const info = { purpose: handle.purpose, path: handle.path };
11720
- registry2.register(handle, info, handle);
11720
+ registry3.register(handle, info, handle);
11721
11721
  const ref = new WeakRef(handle);
11722
11722
  openHandles.add(ref);
11723
11723
  }
11724
11724
  FileHandleTracker2.register = register;
11725
11725
  function unregister(handle) {
11726
- registry2.unregister(handle);
11726
+ registry3.unregister(handle);
11727
11727
  for (const ref of openHandles) {
11728
11728
  if (ref.deref() === handle) {
11729
11729
  openHandles.delete(ref);
@@ -64070,8 +64070,8 @@ var init_NodeSession = __esm({
64070
64070
  return session?.type === SessionType.Unicast;
64071
64071
  }
64072
64072
  NodeSession2.is = is;
64073
- function logNew(logger247, operation, session, messenger, fabric, peerNodeId) {
64074
- logger247.info(
64073
+ function logNew(logger251, operation, session, messenger, fabric, peerNodeId) {
64074
+ logger251.info(
64075
64075
  session.via,
64076
64076
  `${operation} session with`,
64077
64077
  Diagnostic.strong(PeerAddress({ fabricIndex: fabric.fabricIndex, nodeId: peerNodeId }).toString()),
@@ -117714,11 +117714,11 @@ var init_Api = __esm({
117714
117714
  }
117715
117715
  Api2.resourceFor = resourceFor;
117716
117716
  function log(level, facility, id, ...message) {
117717
- let logger247 = loggers.get(facility);
117718
- if (!logger247) {
117719
- loggers.set(facility, logger247 = Logger.get(facility));
117717
+ let logger251 = loggers.get(facility);
117718
+ if (!logger251) {
117719
+ loggers.set(facility, logger251 = Logger.get(facility));
117720
117720
  }
117721
- logger247[level](Diagnostic.via(id || "(anon)"), message);
117721
+ logger251[level](Diagnostic.via(id || "(anon)"), message);
117722
117722
  }
117723
117723
  Api2.log = log;
117724
117724
  function logRequest(facility, id, method, target) {
@@ -131221,6 +131221,13 @@ var init_controller_compat = __esm({
131221
131221
  aqara: "yes",
131222
131222
  note: "Aqara lists battery storage; others show battery inside a device."
131223
131223
  },
131224
+ 1292: {
131225
+ apple: "no",
131226
+ google: "no",
131227
+ alexa: "no",
131228
+ aqara: "yes",
131229
+ note: "HA and Aqara render EnergyEvse; SmartThings announced support, unconfirmed. Bridged EVSE can break Alexa device recognition, keep it off Alexa bridges."
131230
+ },
131224
131231
  39: {
131225
131232
  apple: "no",
131226
131233
  google: "no",
@@ -132058,6 +132065,12 @@ var init_bridge_config_schema = __esm({
132058
132065
  description: "Anchor each device to its Home Assistant entity registry id instead of the entity id, so renaming an entity no longer re-adds it in your controller (Alexa, Google Home, Apple Home) and keeps groups and automations. Records are seeded from the start, so enabling this later is safe and never re-adds existing devices. Default off.",
132059
132066
  type: "boolean",
132060
132067
  default: false
132068
+ },
132069
+ wedgeWatchdog: {
132070
+ title: "Wedge Watchdog (Apple 'Updating' workaround)",
132071
+ description: "Rotate the one session that looks wedged, subscriptions still alive but no inbound request from the controller for about 45 minutes, earlier than the blind session rotation. Targets Apple Home tiles stuck on 'Updating' where the controller keeps acking but stops consuming data. A false positive only triggers a transparent reconnect, the same as normal rotation. Default off.",
132072
+ type: "boolean",
132073
+ default: false
132061
132074
  }
132062
132075
  }
132063
132076
  };
@@ -132618,10 +132631,10 @@ var init_home_assistant_actions = __esm({
132618
132631
  circuitBreakerResetMs: 3e4
132619
132632
  };
132620
132633
  HomeAssistantActions = class extends Service {
132621
- constructor(logger247, client, config11) {
132634
+ constructor(logger251, client, config11) {
132622
132635
  super("HomeAssistantActions");
132623
132636
  this.client = client;
132624
- this.log = logger247.get(this);
132637
+ this.log = logger251.get(this);
132625
132638
  this.config = { ...defaultConfig, ...config11 };
132626
132639
  this.circuitBreaker = new CircuitBreaker(
132627
132640
  this.config.circuitBreakerThreshold,
@@ -132967,10 +132980,10 @@ var DiagnosticService = class {
132967
132980
  };
132968
132981
 
132969
132982
  // src/api/access-log.ts
132970
- function accessLogger(logger247) {
132983
+ function accessLogger(logger251) {
132971
132984
  return (req, res, next) => {
132972
132985
  res.on("finish", () => {
132973
- logger247.debug(
132986
+ logger251.debug(
132974
132987
  `${req.method} ${decodeURI(req.originalUrl)} ${res.statusCode} ${res.statusMessage} from ${req.socket.remoteAddress}`
132975
132988
  );
132976
132989
  });
@@ -133155,6 +133168,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133155
133168
  currentEntity: config11.currentEntity,
133156
133169
  batteryPowerEntity: config11.batteryPowerEntity,
133157
133170
  batteryEnergyEntity: config11.batteryEnergyEntity,
133171
+ chargingSwitchEntity: config11.chargingSwitchEntity,
133172
+ currentLimitEntity: config11.currentLimitEntity,
133158
133173
  suctionLevelEntity: config11.suctionLevelEntity,
133159
133174
  mopIntensityEntity: config11.mopIntensityEntity,
133160
133175
  valetudoIdentifier: config11.valetudoIdentifier,
@@ -133316,6 +133331,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133316
133331
  currentEntity: config11.currentEntity,
133317
133332
  batteryPowerEntity: config11.batteryPowerEntity,
133318
133333
  batteryEnergyEntity: config11.batteryEnergyEntity,
133334
+ chargingSwitchEntity: config11.chargingSwitchEntity,
133335
+ currentLimitEntity: config11.currentLimitEntity,
133319
133336
  suctionLevelEntity: config11.suctionLevelEntity,
133320
133337
  mopIntensityEntity: config11.mopIntensityEntity,
133321
133338
  temperatureEntity: config11.temperatureEntity,
@@ -134168,6 +134185,8 @@ function entityMappingApi(mappingStorage, identityStorage) {
134168
134185
  currentEntity: body.currentEntity,
134169
134186
  batteryPowerEntity: body.batteryPowerEntity,
134170
134187
  batteryEnergyEntity: body.batteryEnergyEntity,
134188
+ chargingSwitchEntity: body.chargingSwitchEntity,
134189
+ currentLimitEntity: body.currentLimitEntity,
134171
134190
  suctionLevelEntity: body.suctionLevelEntity,
134172
134191
  mopIntensityEntity: body.mopIntensityEntity,
134173
134192
  customServiceAreas: body.customServiceAreas,
@@ -134669,6 +134688,8 @@ function configToProfileEntry(config11) {
134669
134688
  currentEntity: config11.currentEntity,
134670
134689
  batteryPowerEntity: config11.batteryPowerEntity,
134671
134690
  batteryEnergyEntity: config11.batteryEnergyEntity,
134691
+ chargingSwitchEntity: config11.chargingSwitchEntity,
134692
+ currentLimitEntity: config11.currentLimitEntity,
134672
134693
  suctionLevelEntity: config11.suctionLevelEntity,
134673
134694
  mopIntensityEntity: config11.mopIntensityEntity,
134674
134695
  customServiceAreas: config11.customServiceAreas,
@@ -134808,6 +134829,8 @@ function mappingProfileApi(mappingStorage) {
134808
134829
  currentEntity: entry.currentEntity,
134809
134830
  batteryPowerEntity: entry.batteryPowerEntity,
134810
134831
  batteryEnergyEntity: entry.batteryEnergyEntity,
134832
+ chargingSwitchEntity: entry.chargingSwitchEntity,
134833
+ currentLimitEntity: entry.currentLimitEntity,
134811
134834
  suctionLevelEntity: entry.suctionLevelEntity,
134812
134835
  mopIntensityEntity: entry.mopIntensityEntity,
134813
134836
  customServiceAreas: entry.customServiceAreas,
@@ -135034,6 +135057,300 @@ function resolveLabelValue(value, labels) {
135034
135057
  return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
135035
135058
  }
135036
135059
 
135060
+ // src/matter/endpoints/entity-endpoint.ts
135061
+ init_esm7();
135062
+ var EntityEndpoint = class extends Endpoint {
135063
+ constructor(type, entityId, customName, mappedEntityIds, endpointId) {
135064
+ super(type, { id: endpointId ?? createEndpointId(entityId, customName) });
135065
+ this.entityId = entityId;
135066
+ this.mappedEntityIds = mappedEntityIds ?? [];
135067
+ }
135068
+ entityId;
135069
+ mappedEntityIds;
135070
+ lastMappedStates = {};
135071
+ hasMappedEntityChanged(states) {
135072
+ let changed = false;
135073
+ for (const mappedId of this.mappedEntityIds) {
135074
+ const mappedState = states[mappedId];
135075
+ if (!mappedState) continue;
135076
+ const fp = mappedState.state;
135077
+ if (fp !== this.lastMappedStates[mappedId]) {
135078
+ this.lastMappedStates[mappedId] = fp;
135079
+ changed = true;
135080
+ }
135081
+ }
135082
+ return changed;
135083
+ }
135084
+ };
135085
+ function createEndpointId(entityId, customName) {
135086
+ const baseName = customName || entityId;
135087
+ return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
135088
+ }
135089
+ function getMappedEntityIds(mapping) {
135090
+ if (!mapping) return [];
135091
+ const ids = [];
135092
+ if (mapping.batteryEntity && !mapping.disableBatteryMapping) {
135093
+ ids.push(mapping.batteryEntity);
135094
+ }
135095
+ if (mapping.faultEntity) ids.push(mapping.faultEntity);
135096
+ if (mapping.chargingStateEntity) ids.push(mapping.chargingStateEntity);
135097
+ if (mapping.temperatureEntity) ids.push(mapping.temperatureEntity);
135098
+ if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
135099
+ if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
135100
+ if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
135101
+ if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
135102
+ if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
135103
+ if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
135104
+ if (mapping.powerEntity) ids.push(mapping.powerEntity);
135105
+ if (mapping.energyEntity) ids.push(mapping.energyEntity);
135106
+ if (mapping.voltageEntity) ids.push(mapping.voltageEntity);
135107
+ if (mapping.currentEntity) ids.push(mapping.currentEntity);
135108
+ if (mapping.batteryPowerEntity) ids.push(mapping.batteryPowerEntity);
135109
+ if (mapping.batteryEnergyEntity) ids.push(mapping.batteryEnergyEntity);
135110
+ if (mapping.chargingSwitchEntity) ids.push(mapping.chargingSwitchEntity);
135111
+ if (mapping.currentLimitEntity) ids.push(mapping.currentLimitEntity);
135112
+ if (mapping.currentRoomEntity) ids.push(mapping.currentRoomEntity);
135113
+ if (mapping.cleanedAreaEntity) ids.push(mapping.cleanedAreaEntity);
135114
+ if (mapping.composedEntities) {
135115
+ for (const sub of mapping.composedEntities) {
135116
+ if (sub.entityId) ids.push(sub.entityId);
135117
+ }
135118
+ }
135119
+ return ids;
135120
+ }
135121
+
135122
+ // src/services/bridges/identity-resolver.ts
135123
+ var SEP = "\0";
135124
+ function identityKey(entity) {
135125
+ const uniqueId = entity.registry?.unique_id;
135126
+ const platform = entity.registry?.platform;
135127
+ if (!uniqueId || !platform) {
135128
+ return null;
135129
+ }
135130
+ const domain = entity.entity_id.split(".")[0];
135131
+ return platform + SEP + domain + SEP + uniqueId;
135132
+ }
135133
+ var IdentityResolver = class {
135134
+ constructor(identityStorage, mappingStorage) {
135135
+ this.identityStorage = identityStorage;
135136
+ this.mappingStorage = mappingStorage;
135137
+ }
135138
+ identityStorage;
135139
+ mappingStorage;
135140
+ async resolveIdentity(bridgeId, entity, mapping, opts) {
135141
+ const entityId = entity.entity_id;
135142
+ const key = identityKey(entity);
135143
+ if (key == null) {
135144
+ return {
135145
+ endpointId: createEndpointId(entityId, mapping?.customName),
135146
+ anchorEntityId: entityId,
135147
+ protected: false
135148
+ };
135149
+ }
135150
+ const existing = this.identityStorage.getIdentity(bridgeId, key);
135151
+ if (existing) {
135152
+ const renamedFrom = existing.lastEntityId !== entityId ? existing.lastEntityId ?? existing.anchorEntityId : void 0;
135153
+ if (opts.stableIdentity) {
135154
+ if (renamedFrom) {
135155
+ this.identityStorage.setIdentity(bridgeId, key, {
135156
+ ...existing,
135157
+ lastEntityId: entityId
135158
+ });
135159
+ await this.rekeyMapping(bridgeId, renamedFrom, entityId);
135160
+ }
135161
+ return {
135162
+ endpointId: existing.endpointId,
135163
+ anchorEntityId: existing.anchorEntityId,
135164
+ protected: true,
135165
+ renamedFrom
135166
+ };
135167
+ }
135168
+ const liveEndpointId = createEndpointId(entityId, mapping?.customName);
135169
+ if (existing.endpointId !== liveEndpointId || existing.anchorEntityId !== entityId || existing.lastEntityId !== entityId) {
135170
+ this.identityStorage.setIdentity(bridgeId, key, {
135171
+ ...existing,
135172
+ endpointId: liveEndpointId,
135173
+ anchorEntityId: entityId,
135174
+ lastEntityId: entityId
135175
+ });
135176
+ }
135177
+ return {
135178
+ endpointId: liveEndpointId,
135179
+ anchorEntityId: entityId,
135180
+ protected: false,
135181
+ renamedFrom
135182
+ };
135183
+ }
135184
+ const desired = createEndpointId(entityId, mapping?.customName);
135185
+ let endpointId = desired;
135186
+ if (opts.stableIdentity) {
135187
+ let n = 2;
135188
+ while (opts.isEndpointIdTaken(endpointId, key)) {
135189
+ endpointId = `${desired}_${n++}`;
135190
+ }
135191
+ }
135192
+ const record = {
135193
+ endpointId,
135194
+ anchorEntityId: entityId,
135195
+ lastEntityId: entityId,
135196
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
135197
+ };
135198
+ this.identityStorage.setIdentity(bridgeId, key, record);
135199
+ if (opts.stableIdentity) {
135200
+ return {
135201
+ endpointId: record.endpointId,
135202
+ anchorEntityId: record.anchorEntityId,
135203
+ protected: true
135204
+ };
135205
+ }
135206
+ return {
135207
+ endpointId: desired,
135208
+ anchorEntityId: entityId,
135209
+ protected: false
135210
+ };
135211
+ }
135212
+ // Carry a mapping from the old entity_id to the new one on rename. anchorEntityId
135213
+ // never changes, only the mapping key. Skips when no old mapping exists or a new
135214
+ // one already does, so a manual mapping under the new id is never clobbered.
135215
+ async rekeyMapping(bridgeId, oldEntityId, newEntityId) {
135216
+ if (oldEntityId === newEntityId) {
135217
+ return;
135218
+ }
135219
+ const old = this.mappingStorage.getMapping(bridgeId, oldEntityId);
135220
+ if (!old) {
135221
+ return;
135222
+ }
135223
+ if (this.mappingStorage.getMapping(bridgeId, newEntityId)) {
135224
+ return;
135225
+ }
135226
+ await this.mappingStorage.setMapping({
135227
+ ...old,
135228
+ bridgeId,
135229
+ entityId: newEntityId
135230
+ });
135231
+ await this.mappingStorage.deleteMapping(bridgeId, oldEntityId);
135232
+ }
135233
+ };
135234
+
135235
+ // src/services/storage/orphan-cleanup.ts
135236
+ var ORPHAN_TOMBSTONE_MS = 7 * 24 * 60 * 60 * 1e3;
135237
+ function buildPresentIdentityKeys(entities) {
135238
+ const keys3 = /* @__PURE__ */ new Set();
135239
+ for (const entity of Object.values(entities ?? {})) {
135240
+ if (!entity?.entity_id) continue;
135241
+ const key = identityKey({ entity_id: entity.entity_id, registry: entity });
135242
+ if (key != null) keys3.add(key);
135243
+ }
135244
+ return keys3;
135245
+ }
135246
+ function buildPresentEntityIds(entities) {
135247
+ const ids = /* @__PURE__ */ new Set();
135248
+ for (const entity of Object.values(entities ?? {})) {
135249
+ if (entity?.entity_id) ids.add(entity.entity_id);
135250
+ }
135251
+ return ids;
135252
+ }
135253
+ function stampIdentityPresence(store, bridgeId, presentKeys, now = Date.now()) {
135254
+ const nowIso = new Date(now).toISOString();
135255
+ for (const key of [...store.getBridgeIdentities(bridgeId).keys()]) {
135256
+ if (presentKeys.has(key)) {
135257
+ store.clearIdentityMissing(bridgeId, key);
135258
+ } else {
135259
+ store.markIdentityMissing(bridgeId, key, nowIso);
135260
+ }
135261
+ }
135262
+ }
135263
+ function stampMappingPresence(store, bridgeId, presentEntityIds, now = Date.now()) {
135264
+ const nowIso = new Date(now).toISOString();
135265
+ const entityIds = store.getMappingsForBridge(bridgeId).map((m) => m.entityId);
135266
+ for (const entityId of entityIds) {
135267
+ if (presentEntityIds.has(entityId)) {
135268
+ store.clearMappingMissing(bridgeId, entityId);
135269
+ } else {
135270
+ store.markMappingMissing(bridgeId, entityId, nowIso);
135271
+ }
135272
+ }
135273
+ }
135274
+ function computeOrphanCandidates(input) {
135275
+ const now = input.now ?? Date.now();
135276
+ const candidates = [];
135277
+ for (const [key, record] of input.identities) {
135278
+ if (!record.missingSince) continue;
135279
+ const missingFor = now - Date.parse(record.missingSince);
135280
+ if (!(missingFor >= ORPHAN_TOMBSTONE_MS)) continue;
135281
+ if (input.presentKeys.has(key)) continue;
135282
+ const lastEntityId = record.lastEntityId ?? record.anchorEntityId;
135283
+ candidates.push({
135284
+ identityKey: key,
135285
+ lastEntityId,
135286
+ missingSince: record.missingSince,
135287
+ // Only a true orphan mapping, not one a live entity holds under a reused
135288
+ // entity_id (a different identity key). Guards the sweep from clobbering
135289
+ // that live entity's custom mapping.
135290
+ hasMapping: input.hasMapping(lastEntityId) && !input.presentEntityIds.has(lastEntityId),
135291
+ kind: "identity"
135292
+ });
135293
+ }
135294
+ if (input.mappings) {
135295
+ const linkedByIdentity = /* @__PURE__ */ new Set();
135296
+ for (const candidate of candidates) {
135297
+ if (candidate.hasMapping) linkedByIdentity.add(candidate.lastEntityId);
135298
+ }
135299
+ for (const mapping of input.mappings) {
135300
+ if (!mapping.missingSince) continue;
135301
+ const missingFor = now - Date.parse(mapping.missingSince);
135302
+ if (!(missingFor >= ORPHAN_TOMBSTONE_MS)) continue;
135303
+ if (input.presentEntityIds.has(mapping.entityId)) continue;
135304
+ if (linkedByIdentity.has(mapping.entityId)) continue;
135305
+ candidates.push({
135306
+ identityKey: mapping.entityId,
135307
+ lastEntityId: mapping.entityId,
135308
+ missingSince: mapping.missingSince,
135309
+ hasMapping: true,
135310
+ kind: "mapping"
135311
+ });
135312
+ }
135313
+ }
135314
+ return candidates;
135315
+ }
135316
+ async function executeOrphanCleanup(input) {
135317
+ const candidates = computeOrphanCandidates({
135318
+ bridgeId: input.bridgeId,
135319
+ identities: input.identities,
135320
+ presentKeys: input.presentKeys,
135321
+ presentEntityIds: input.presentEntityIds,
135322
+ hasMapping: (entityId) => input.getMapping(input.bridgeId, entityId) != null,
135323
+ mappings: input.mappings,
135324
+ now: input.now
135325
+ });
135326
+ const byKey = new Map(candidates.map((c) => [c.identityKey, c]));
135327
+ const results = [];
135328
+ for (const key of input.requestedKeys) {
135329
+ const candidate = byKey.get(key);
135330
+ if (!candidate) {
135331
+ results.push({
135332
+ identityKey: key,
135333
+ deleted: false,
135334
+ reason: "not a current orphan candidate"
135335
+ });
135336
+ continue;
135337
+ }
135338
+ if (candidate.kind === "mapping") {
135339
+ if (!input.presentEntityIds.has(candidate.lastEntityId)) {
135340
+ await input.deleteMapping(input.bridgeId, candidate.lastEntityId);
135341
+ }
135342
+ results.push({ identityKey: key, deleted: true });
135343
+ continue;
135344
+ }
135345
+ if (candidate.hasMapping && !input.presentEntityIds.has(candidate.lastEntityId)) {
135346
+ await input.deleteMapping(input.bridgeId, candidate.lastEntityId);
135347
+ }
135348
+ await input.deleteIdentity(input.bridgeId, key);
135349
+ results.push({ identityKey: key, deleted: true });
135350
+ }
135351
+ return results;
135352
+ }
135353
+
135037
135354
  // src/utils/json/endpoint-to-json.ts
135038
135355
  function safeClone(value, depth = 0) {
135039
135356
  if (depth > 20 || value === null || value === void 0) return value;
@@ -135079,7 +135396,7 @@ function endpointToJson(endpoint, parentId) {
135079
135396
 
135080
135397
  // src/api/matter-api.ts
135081
135398
  var ajv = new Ajv();
135082
- function matterApi(bridgeService, haRegistry, identityStorage) {
135399
+ function matterApi(bridgeService, haRegistry, identityStorage, mappingStorage) {
135083
135400
  const router = express11.Router();
135084
135401
  router.get("/", (_, res) => {
135085
135402
  res.status(200).json({});
@@ -135182,6 +135499,64 @@ function matterApi(bridgeService, haRegistry, identityStorage) {
135182
135499
  });
135183
135500
  }
135184
135501
  });
135502
+ router.get("/bridges/:bridgeId/orphans", async (req, res) => {
135503
+ const bridgeId = req.params.bridgeId;
135504
+ const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
135505
+ if (!bridge) {
135506
+ res.status(404).send("Not Found");
135507
+ return;
135508
+ }
135509
+ if (!haRegistry || !identityStorage) {
135510
+ res.status(503).json({ error: "Orphan cleanup is not available" });
135511
+ return;
135512
+ }
135513
+ const presentKeys = buildPresentIdentityKeys(haRegistry.entities);
135514
+ const presentEntityIds = buildPresentEntityIds(haRegistry.entities);
135515
+ const candidates = computeOrphanCandidates({
135516
+ bridgeId,
135517
+ identities: identityStorage.getBridgeIdentities(bridgeId),
135518
+ presentKeys,
135519
+ presentEntityIds,
135520
+ hasMapping: (entityId) => mappingStorage?.getMapping(bridgeId, entityId) != null,
135521
+ mappings: mappingStorage?.getMappingsForBridge(bridgeId)
135522
+ });
135523
+ res.status(200).json({ candidates });
135524
+ });
135525
+ router.post(
135526
+ "/bridges/:bridgeId/actions/cleanup-orphans",
135527
+ async (req, res) => {
135528
+ const bridgeId = req.params.bridgeId;
135529
+ const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
135530
+ if (!bridge) {
135531
+ res.status(404).send("Not Found");
135532
+ return;
135533
+ }
135534
+ if (!haRegistry || !identityStorage || !mappingStorage) {
135535
+ res.status(503).json({ error: "Orphan cleanup is not available" });
135536
+ return;
135537
+ }
135538
+ const body = req.body;
135539
+ const requestedKeys = Array.isArray(body?.identityKeys) ? body.identityKeys.filter((k) => typeof k === "string") : [];
135540
+ try {
135541
+ const results = await executeOrphanCleanup({
135542
+ bridgeId,
135543
+ requestedKeys,
135544
+ identities: identityStorage.getBridgeIdentities(bridgeId),
135545
+ presentKeys: buildPresentIdentityKeys(haRegistry.entities),
135546
+ presentEntityIds: buildPresentEntityIds(haRegistry.entities),
135547
+ getMapping: (b, e) => mappingStorage.getMapping(b, e),
135548
+ deleteMapping: (b, e) => mappingStorage.deleteMapping(b, e),
135549
+ deleteIdentity: (b, k) => identityStorage.deleteIdentity(b, k),
135550
+ mappings: mappingStorage.getMappingsForBridge(bridgeId)
135551
+ });
135552
+ res.status(200).json({ results });
135553
+ } catch (e) {
135554
+ res.status(500).json({
135555
+ error: e instanceof Error ? e.message : "Unknown error"
135556
+ });
135557
+ }
135558
+ }
135559
+ );
135185
135560
  router.get("/bridges/:bridgeId/devices", async (req, res) => {
135186
135561
  const bridgeId = req.params.bridgeId;
135187
135562
  const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
@@ -136242,11 +136617,12 @@ var BLOCKED_PREFIXES = [
136242
136617
  function pluginApi(bridgeService, storageLocation) {
136243
136618
  const router = express14.Router();
136244
136619
  const installer = new PluginInstaller(storageLocation);
136245
- const registry2 = new PluginRegistry(storageLocation);
136620
+ const registry3 = new PluginRegistry(storageLocation);
136246
136621
  router.get("/", (_req, res) => {
136247
136622
  const result = [];
136248
136623
  for (const bridge of bridgeService.bridges) {
136249
136624
  const info = bridge.pluginInfo;
136625
+ if (!info) continue;
136250
136626
  const plugins = info.metadata.map((meta) => ({
136251
136627
  name: meta.name,
136252
136628
  version: meta.version,
@@ -136269,41 +136645,41 @@ function pluginApi(bridgeService, storageLocation) {
136269
136645
  }
136270
136646
  res.json(result);
136271
136647
  });
136272
- router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136273
- const bridge = bridgeService.get(req.params.bridgeId);
136648
+ function pluginBridge(bridgeId, res) {
136649
+ const bridge = bridgeService.get(bridgeId);
136274
136650
  if (!bridge) {
136275
136651
  res.status(404).json({ error: "Bridge not found" });
136276
- return;
136652
+ return void 0;
136653
+ }
136654
+ if (typeof bridge.enablePlugin !== "function") {
136655
+ res.status(400).json({ error: "Bridge does not support plugins" });
136656
+ return void 0;
136277
136657
  }
136658
+ return bridge;
136659
+ }
136660
+ router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136661
+ const bridge = pluginBridge(req.params.bridgeId, res);
136662
+ if (!bridge) return;
136278
136663
  const { pluginName } = req.params;
136279
136664
  bridge.enablePlugin(pluginName);
136280
136665
  res.json({ success: true, pluginName, enabled: true });
136281
136666
  });
136282
136667
  router.post("/:bridgeId/:pluginName/disable", (req, res) => {
136283
- const bridge = bridgeService.get(req.params.bridgeId);
136284
- if (!bridge) {
136285
- res.status(404).json({ error: "Bridge not found" });
136286
- return;
136287
- }
136668
+ const bridge = pluginBridge(req.params.bridgeId, res);
136669
+ if (!bridge) return;
136288
136670
  const { pluginName } = req.params;
136289
136671
  bridge.disablePlugin(pluginName);
136290
136672
  res.json({ success: true, pluginName, enabled: false });
136291
136673
  });
136292
136674
  router.get("/:bridgeId/:pluginName/config-schema", (req, res) => {
136293
- const bridge = bridgeService.get(req.params.bridgeId);
136294
- if (!bridge) {
136295
- res.status(404).json({ error: "Bridge not found" });
136296
- return;
136297
- }
136675
+ const bridge = pluginBridge(req.params.bridgeId, res);
136676
+ if (!bridge) return;
136298
136677
  const schema6 = bridge.getPluginConfigSchema(req.params.pluginName);
136299
136678
  res.json({ pluginName: req.params.pluginName, schema: schema6 ?? null });
136300
136679
  });
136301
136680
  router.post("/:bridgeId/:pluginName/config", async (req, res) => {
136302
- const bridge = bridgeService.get(req.params.bridgeId);
136303
- if (!bridge) {
136304
- res.status(404).json({ error: "Bridge not found" });
136305
- return;
136306
- }
136681
+ const bridge = pluginBridge(req.params.bridgeId, res);
136682
+ if (!bridge) return;
136307
136683
  const { config: config11 } = req.body;
136308
136684
  if (!config11 || typeof config11 !== "object") {
136309
136685
  res.status(400).json({ error: "config object is required" });
@@ -136317,17 +136693,14 @@ function pluginApi(bridgeService, storageLocation) {
136317
136693
  res.json({ success: true, pluginName: req.params.pluginName });
136318
136694
  });
136319
136695
  router.post("/:bridgeId/:pluginName/reset", (req, res) => {
136320
- const bridge = bridgeService.get(req.params.bridgeId);
136321
- if (!bridge) {
136322
- res.status(404).json({ error: "Bridge not found" });
136323
- return;
136324
- }
136696
+ const bridge = pluginBridge(req.params.bridgeId, res);
136697
+ if (!bridge) return;
136325
136698
  const { pluginName } = req.params;
136326
136699
  bridge.resetPlugin(pluginName);
136327
136700
  res.json({ success: true, pluginName, reset: true });
136328
136701
  });
136329
136702
  router.get("/installed", (_req, res) => {
136330
- const registered = registry2.getAll();
136703
+ const registered = registry3.getAll();
136331
136704
  const npmInstalled = installer.listInstalled();
136332
136705
  const result = registered.map((entry) => {
136333
136706
  const npm = npmInstalled.find((p) => p.name === entry.packageName);
@@ -136357,7 +136730,7 @@ function pluginApi(bridgeService, storageLocation) {
136357
136730
  });
136358
136731
  return;
136359
136732
  }
136360
- registry2.add(packageName, config11 ?? {});
136733
+ registry3.add(packageName, config11 ?? {});
136361
136734
  res.json({
136362
136735
  success: true,
136363
136736
  packageName,
@@ -136385,7 +136758,7 @@ function pluginApi(bridgeService, storageLocation) {
136385
136758
  });
136386
136759
  return;
136387
136760
  }
136388
- registry2.remove(packageName);
136761
+ registry3.remove(packageName);
136389
136762
  res.json({
136390
136763
  success: true,
136391
136764
  packageName,
@@ -136425,7 +136798,7 @@ function pluginApi(bridgeService, storageLocation) {
136425
136798
  });
136426
136799
  return;
136427
136800
  }
136428
- registry2.add(result.packageName, {});
136801
+ registry3.add(result.packageName, {});
136429
136802
  res.json({
136430
136803
  success: true,
136431
136804
  packageName: result.packageName,
@@ -136458,7 +136831,7 @@ function pluginApi(bridgeService, storageLocation) {
136458
136831
  });
136459
136832
  return;
136460
136833
  }
136461
- registry2.add(result.packageName, {});
136834
+ registry3.add(result.packageName, {});
136462
136835
  res.json({
136463
136836
  success: true,
136464
136837
  packageName: result.packageName,
@@ -137155,7 +137528,7 @@ var WebSocketApi = class {
137155
137528
 
137156
137529
  // src/api/web-api.ts
137157
137530
  var WebApi = class extends Service {
137158
- constructor(logger247, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, identityStorage, lockCredentialStorage, settingsStorage, backupService, props) {
137531
+ constructor(logger251, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, identityStorage, lockCredentialStorage, settingsStorage, backupService, props) {
137159
137532
  super("WebApi");
137160
137533
  this.bridgeService = bridgeService;
137161
137534
  this.haClient = haClient;
@@ -137167,8 +137540,8 @@ var WebApi = class extends Service {
137167
137540
  this.settingsStorage = settingsStorage;
137168
137541
  this.backupService = backupService;
137169
137542
  this.props = props;
137170
- this.logger = logger247;
137171
- this.log = logger247.get(this);
137543
+ this.logger = logger251;
137544
+ this.log = logger251.get(this);
137172
137545
  this.accessLogger = accessLogger(this.log.createChild("Access Log"));
137173
137546
  this.startTime = Date.now();
137174
137547
  this.wsApi = new WebSocketApi(
@@ -137201,7 +137574,12 @@ var WebApi = class extends Service {
137201
137574
  const api = express17.Router();
137202
137575
  api.use(express17.json()).use(nocache()).use(
137203
137576
  "/matter",
137204
- matterApi(this.bridgeService, this.haRegistry, this.identityStorage)
137577
+ matterApi(
137578
+ this.bridgeService,
137579
+ this.haRegistry,
137580
+ this.identityStorage,
137581
+ this.mappingStorage
137582
+ )
137205
137583
  ).use(
137206
137584
  "/health",
137207
137585
  healthApi(
@@ -138354,10 +138732,10 @@ function isTransientConnectError(reason) {
138354
138732
  return msg.includes("socket hang up") || msg.includes("tls") || msg.includes("TLS");
138355
138733
  }
138356
138734
  var HomeAssistantClient = class extends Service {
138357
- constructor(logger247, options) {
138735
+ constructor(logger251, options) {
138358
138736
  super("HomeAssistantClient");
138359
138737
  this.options = options;
138360
- this.log = logger247.get(this);
138738
+ this.log = logger251.get(this);
138361
138739
  }
138362
138740
  options;
138363
138741
  static Options = /* @__PURE__ */ Symbol.for("HomeAssistantClientProps");
@@ -139022,11 +139400,37 @@ var EntityIdentityStorage = class extends Service {
139022
139400
  this.identities.delete(bridgeId);
139023
139401
  await this.flush();
139024
139402
  }
139403
+ // Drop a single identity record. Used by the manual orphan cleanup after the
139404
+ // 7-day tombstone; flush immediately since it is a user-initiated deletion.
139405
+ async deleteIdentity(bridgeId, key) {
139406
+ const bridgeMap = this.identities.get(bridgeId);
139407
+ if (bridgeMap?.delete(key)) {
139408
+ await this.flush();
139409
+ }
139410
+ }
139411
+ // Stamp the tombstone the first time a record's entity is absent from HA. Keeps
139412
+ // the first-seen time on later passes and no-ops once stamped, so a steady
139413
+ // absence does not churn the debounced persist.
139414
+ markIdentityMissing(bridgeId, key, nowIso) {
139415
+ const record = this.identities.get(bridgeId)?.get(key);
139416
+ if (!record || record.missingSince != null) return;
139417
+ record.missingSince = nowIso;
139418
+ this.schedulePersist();
139419
+ }
139420
+ // Clear the tombstone when the entity is present again. No-ops when there is
139421
+ // nothing to clear, so an all-present bridge writes nothing on refresh.
139422
+ clearIdentityMissing(bridgeId, key) {
139423
+ const record = this.identities.get(bridgeId)?.get(key);
139424
+ if (!record || record.missingSince == null) return;
139425
+ delete record.missingSince;
139426
+ this.schedulePersist();
139427
+ }
139025
139428
  };
139026
139429
 
139027
139430
  // src/services/storage/entity-mapping-storage.ts
139028
139431
  init_service();
139029
139432
  var CURRENT_VERSION2 = 1;
139433
+ var PERSIST_DEBOUNCE_MS2 = 500;
139030
139434
  var EntityMappingStorage = class extends Service {
139031
139435
  constructor(appStorage) {
139032
139436
  super("EntityMappingStorage");
@@ -139035,10 +139439,14 @@ var EntityMappingStorage = class extends Service {
139035
139439
  appStorage;
139036
139440
  storage;
139037
139441
  mappings = /* @__PURE__ */ new Map();
139442
+ persistTimer = null;
139038
139443
  async initialize() {
139039
139444
  this.storage = this.appStorage.createContext("entity-mappings");
139040
139445
  await this.load();
139041
139446
  }
139447
+ async dispose() {
139448
+ await this.flush();
139449
+ }
139042
139450
  async load() {
139043
139451
  const stored = await this.storage.get("data", {
139044
139452
  version: CURRENT_VERSION2,
@@ -139073,6 +139481,10 @@ var EntityMappingStorage = class extends Service {
139073
139481
  }
139074
139482
  }
139075
139483
  async persist() {
139484
+ if (this.persistTimer) {
139485
+ clearTimeout(this.persistTimer);
139486
+ this.persistTimer = null;
139487
+ }
139076
139488
  const data = {
139077
139489
  version: CURRENT_VERSION2,
139078
139490
  mappings: {}
@@ -139082,6 +139494,17 @@ var EntityMappingStorage = class extends Service {
139082
139494
  }
139083
139495
  await this.storage.set("data", data);
139084
139496
  }
139497
+ schedulePersist() {
139498
+ if (this.persistTimer) return;
139499
+ this.persistTimer = setTimeout(() => {
139500
+ this.persistTimer = null;
139501
+ void this.persist();
139502
+ }, PERSIST_DEBOUNCE_MS2);
139503
+ }
139504
+ // Flush any pending debounced write, used on dispose and by tests.
139505
+ async flush() {
139506
+ await this.persist();
139507
+ }
139085
139508
  getMappingsForBridge(bridgeId) {
139086
139509
  const bridgeMap = this.mappings.get(bridgeId);
139087
139510
  return bridgeMap ? Array.from(bridgeMap.values()) : [];
@@ -139128,6 +139551,8 @@ var EntityMappingStorage = class extends Service {
139128
139551
  currentEntity: request.currentEntity?.trim() || void 0,
139129
139552
  batteryPowerEntity: request.batteryPowerEntity?.trim() || void 0,
139130
139553
  batteryEnergyEntity: request.batteryEnergyEntity?.trim() || void 0,
139554
+ chargingSwitchEntity: request.chargingSwitchEntity?.trim() || void 0,
139555
+ currentLimitEntity: request.currentLimitEntity?.trim() || void 0,
139131
139556
  pressureEntity: request.pressureEntity?.trim() || void 0,
139132
139557
  suctionLevelEntity: request.suctionLevelEntity?.trim() || void 0,
139133
139558
  mopIntensityEntity: request.mopIntensityEntity?.trim() || void 0,
@@ -139156,7 +139581,7 @@ var EntityMappingStorage = class extends Service {
139156
139581
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0,
139157
139582
  disableMomentaryFlip: request.disableMomentaryFlip || void 0
139158
139583
  };
139159
- if (!config11.matterDeviceType && !config11.customName && !config11.customProductName && !config11.customVendorName && !config11.customSerialNumber && config11.customVendorId === void 0 && config11.disabled !== true && !config11.filterLifeEntity && !config11.cleaningModeEntity && !config11.temperatureEntity && !config11.humidityEntity && !config11.batteryEntity && !config11.disableBatteryMapping && !config11.chargingStateEntity && !config11.roomEntities && !config11.disableLockPin && !config11.lockUsercodeService && config11.lockUsercodeSlot === void 0 && config11.lockPinMinLength === void 0 && config11.lockPinMaxLength === void 0 && !config11.powerEntity && !config11.energyEntity && !config11.voltageEntity && !config11.currentEntity && !config11.batteryPowerEntity && !config11.batteryEnergyEntity && !config11.pressureEntity && !config11.suctionLevelEntity && !config11.mopIntensityEntity && (!config11.customServiceAreas || config11.customServiceAreas.length === 0) && (!config11.customFanSpeedTags || Object.keys(config11.customFanSpeedTags).length === 0) && (!config11.fanWindPresets || (config11.fanWindPresets.natural?.length ?? 0) === 0 && (config11.fanWindPresets.sleep?.length ?? 0) === 0) && !config11.fanRestoreSpeedOnPowerOn && !config11.currentRoomEntity && !config11.cleanedAreaEntity && !config11.disableCustomAreaRoomModes && !config11.valetudoIdentifier && !config11.coverSwapOpenClose && !config11.coverExposeAsDimmableLight && !config11.selectExposeAsSwitch && !config11.selectSwitchOnOption && !config11.selectSwitchOffOption && !config11.coverSliderDebounceMs && !config11.updateThrottleMs && !config11.disableClimateOnOff && !config11.disableClimateFanControl && !config11.climateKeepModeOnIdle && !config11.climateExposeFan && !config11.climateAutoMode && (!config11.composedEntities || config11.composedEntities.length === 0) && !config11.disableMomentaryFlip) {
139584
+ if (!config11.matterDeviceType && !config11.customName && !config11.customProductName && !config11.customVendorName && !config11.customSerialNumber && config11.customVendorId === void 0 && config11.disabled !== true && !config11.filterLifeEntity && !config11.cleaningModeEntity && !config11.temperatureEntity && !config11.humidityEntity && !config11.batteryEntity && !config11.disableBatteryMapping && !config11.chargingStateEntity && !config11.roomEntities && !config11.disableLockPin && !config11.lockUsercodeService && config11.lockUsercodeSlot === void 0 && config11.lockPinMinLength === void 0 && config11.lockPinMaxLength === void 0 && !config11.powerEntity && !config11.energyEntity && !config11.voltageEntity && !config11.currentEntity && !config11.batteryPowerEntity && !config11.batteryEnergyEntity && !config11.chargingSwitchEntity && !config11.currentLimitEntity && !config11.pressureEntity && !config11.suctionLevelEntity && !config11.mopIntensityEntity && (!config11.customServiceAreas || config11.customServiceAreas.length === 0) && (!config11.customFanSpeedTags || Object.keys(config11.customFanSpeedTags).length === 0) && (!config11.fanWindPresets || (config11.fanWindPresets.natural?.length ?? 0) === 0 && (config11.fanWindPresets.sleep?.length ?? 0) === 0) && !config11.fanRestoreSpeedOnPowerOn && !config11.currentRoomEntity && !config11.cleanedAreaEntity && !config11.disableCustomAreaRoomModes && !config11.valetudoIdentifier && !config11.coverSwapOpenClose && !config11.coverExposeAsDimmableLight && !config11.selectExposeAsSwitch && !config11.selectSwitchOnOption && !config11.selectSwitchOffOption && !config11.coverSliderDebounceMs && !config11.updateThrottleMs && !config11.disableClimateOnOff && !config11.disableClimateFanControl && !config11.climateKeepModeOnIdle && !config11.climateExposeFan && !config11.climateAutoMode && (!config11.composedEntities || config11.composedEntities.length === 0) && !config11.disableMomentaryFlip) {
139160
139585
  bridgeMap.delete(request.entityId);
139161
139586
  } else {
139162
139587
  bridgeMap.set(request.entityId, config11);
@@ -139175,6 +139600,23 @@ var EntityMappingStorage = class extends Service {
139175
139600
  this.mappings.delete(bridgeId);
139176
139601
  await this.persist();
139177
139602
  }
139603
+ // Stamp the tombstone the first time a mapping's entity is absent from HA.
139604
+ // Keeps the first-seen time on later passes and no-ops once stamped, so a
139605
+ // steady absence does not churn the debounced persist.
139606
+ markMappingMissing(bridgeId, entityId, nowIso) {
139607
+ const record = this.mappings.get(bridgeId)?.get(entityId);
139608
+ if (!record || record.missingSince != null) return;
139609
+ record.missingSince = nowIso;
139610
+ this.schedulePersist();
139611
+ }
139612
+ // Clear the tombstone when the entity is present again. No-ops when there is
139613
+ // nothing to clear, so an all-present bridge writes nothing on refresh.
139614
+ clearMappingMissing(bridgeId, entityId) {
139615
+ const record = this.mappings.get(bridgeId)?.get(entityId);
139616
+ if (!record || record.missingSince == null) return;
139617
+ delete record.missingSince;
139618
+ this.schedulePersist();
139619
+ }
139178
139620
  };
139179
139621
  function sanitizeVendorId(value) {
139180
139622
  if (value === void 0 || value === null || value === "") {
@@ -149100,6 +149542,7 @@ var EnergyEvseDeviceDefinition = MutableEndpoint({
149100
149542
  )
149101
149543
  });
149102
149544
  Object.freeze(EnergyEvseDeviceDefinition);
149545
+ var EnergyEvseDevice = EnergyEvseDeviceDefinition;
149103
149546
 
149104
149547
  // ../../node_modules/.pnpm/@matter+node@0.17.7_patch_hash=34f8224b2964673272e9f22613c0cb1f8ec12758dd46b4fe303d42373451ad4c/node_modules/@matter/node/dist/esm/devices/extended-color-light.js
149105
149548
  init_IdentifyServer();
@@ -156079,8 +156522,8 @@ var PluginManager = class {
156079
156522
  this.storageDir = storageDir;
156080
156523
  this.homeAssistant = homeAssistant;
156081
156524
  }
156082
- setRegistry(registry2) {
156083
- this.registry = registry2;
156525
+ setRegistry(registry3) {
156526
+ this.registry = registry3;
156084
156527
  }
156085
156528
  /**
156086
156529
  * Load and register a built-in plugin instance.
@@ -156805,16 +157248,96 @@ function seedExistingSessionStarts(startedAt, sessions, now = Date.now()) {
156805
157248
  }
156806
157249
  }
156807
157250
 
157251
+ // src/services/bridges/subscription-summary.ts
157252
+ function readPaths(value) {
157253
+ return Array.isArray(value) ? value : null;
157254
+ }
157255
+ function unknownSummary(id) {
157256
+ return {
157257
+ id,
157258
+ attributePaths: 0,
157259
+ eventPaths: 0,
157260
+ scope: "unknown",
157261
+ endpointIds: []
157262
+ };
157263
+ }
157264
+ function summarizeSubscription(sub) {
157265
+ let id = null;
157266
+ let request;
157267
+ try {
157268
+ const s = sub;
157269
+ if (typeof s.subscriptionId === "number") {
157270
+ id = s.subscriptionId;
157271
+ }
157272
+ request = s.request;
157273
+ } catch {
157274
+ return unknownSummary(id);
157275
+ }
157276
+ if (request == null || typeof request !== "object") {
157277
+ return unknownSummary(id);
157278
+ }
157279
+ const req = request;
157280
+ const attrPaths = readPaths(req.attributeRequests);
157281
+ const eventPaths = readPaths(req.eventRequests);
157282
+ if (attrPaths === null && eventPaths === null) {
157283
+ return unknownSummary(id);
157284
+ }
157285
+ const all = [...attrPaths ?? [], ...eventPaths ?? []];
157286
+ let wildcard = false;
157287
+ const endpointIds = /* @__PURE__ */ new Set();
157288
+ for (const path14 of all) {
157289
+ const ep = path14?.endpointId;
157290
+ if (ep == null) {
157291
+ wildcard = true;
157292
+ } else if (typeof ep === "number" || typeof ep === "bigint") {
157293
+ endpointIds.add(Number(ep));
157294
+ }
157295
+ }
157296
+ return {
157297
+ id,
157298
+ attributePaths: attrPaths?.length ?? 0,
157299
+ eventPaths: eventPaths?.length ?? 0,
157300
+ scope: wildcard ? "wildcard" : "endpoint-specific",
157301
+ endpointIds: [...endpointIds].sort((a, b) => a - b)
157302
+ };
157303
+ }
157304
+ function summarizeSubscriptions(subscriptions) {
157305
+ const out = [];
157306
+ try {
157307
+ for (const sub of subscriptions) {
157308
+ out.push(summarizeSubscription(sub));
157309
+ }
157310
+ } catch {
157311
+ }
157312
+ return out;
157313
+ }
157314
+
157315
+ // src/services/bridges/wedge-watchdog.ts
157316
+ var WEDGE_WARMUP_MS = 15 * 60 * 1e3;
157317
+ var WEDGE_IM_SILENCE_MS = 45 * 60 * 1e3;
157318
+ var WEDGE_MIN_ROTATE_INTERVAL_MS = 60 * 60 * 1e3;
157319
+ function decideWedgeRotation(input) {
157320
+ const {
157321
+ subscriptionCount,
157322
+ sessionAgeMs,
157323
+ lastImRequestMsAgo,
157324
+ lastRotatedMsAgo
157325
+ } = input;
157326
+ const imSilenceMs = lastImRequestMsAgo == null ? sessionAgeMs : lastImRequestMsAgo;
157327
+ return subscriptionCount > 0 && sessionAgeMs > WEDGE_WARMUP_MS && imSilenceMs > WEDGE_IM_SILENCE_MS && (lastRotatedMsAgo == null || lastRotatedMsAgo > WEDGE_MIN_ROTATE_INTERVAL_MS);
157328
+ }
157329
+
156808
157330
  // src/services/bridges/bridge.ts
157331
+ var imWrapMarker = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
156809
157332
  var AUTO_FORCE_SYNC_INTERVAL_MS = 9e4;
156810
157333
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS = 2500;
156811
157334
  var MDNS_ADDRESS_CHECK_INTERVAL_MS = 6e4;
156812
157335
  var Bridge = class {
156813
- constructor(env, logger247, dataProvider, endpointManager, serverOptions) {
157336
+ constructor(env, logger251, dataProvider, endpointManager, serverOptions) {
156814
157337
  this.dataProvider = dataProvider;
156815
157338
  this.endpointManager = endpointManager;
156816
157339
  this.serverOptions = serverOptions;
156817
- this.log = logger247.get(`Bridge / ${dataProvider.id}`);
157340
+ this.log = logger251.get(`Bridge / ${dataProvider.id}`);
156818
157341
  this.server = new BridgeServerNode(
156819
157342
  env,
156820
157343
  this.dataProvider,
@@ -156853,6 +157376,12 @@ var Bridge = class {
156853
157376
  sessionStartedAt = /* @__PURE__ */ new Map();
156854
157377
  rotationTimer = null;
156855
157378
  maxSessionAgeMs = 0;
157379
+ // Wedge watchdog: last inbound Interaction Model request time per session and
157380
+ // last time the watchdog rotated it, both keyed by the long-lived session
157381
+ // object so they clear when the session goes away.
157382
+ lastImRequestAt = /* @__PURE__ */ new WeakMap();
157383
+ wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
157384
+ wedgeWatchdogTimer = null;
156856
157385
  // Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
156857
157386
  // change forces a fresh operational announcement (#415).
156858
157387
  mdnsAddressTimer = null;
@@ -156899,6 +157428,7 @@ var Bridge = class {
156899
157428
  const sessionList = sessions.map((s) => {
156900
157429
  const subCount = s.subscriptions.size;
156901
157430
  totalSubscriptions += subCount;
157431
+ const subscriptions = summarizeSubscriptions(s.subscriptions);
156902
157432
  const fi = typeof s.fabric?.fabricIndex === "number" ? s.fabric.fabricIndex : null;
156903
157433
  if (fi !== null) {
156904
157434
  const existing = fabricMap.get(fi) ?? {
@@ -156912,14 +157442,18 @@ var Bridge = class {
156912
157442
  const nowMs = Date.now();
156913
157443
  const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
156914
157444
  const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
157445
+ const lastImAt = this.lastImRequestAt.get(s);
157446
+ const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
156915
157447
  const startedAt = this.sessionStartedAt.get(s.id);
156916
157448
  return {
156917
157449
  id: s.id,
156918
157450
  peerNodeId: String(s.peerNodeId),
156919
157451
  fabricIndex: fi,
156920
157452
  subscriptionCount: subCount,
157453
+ subscriptions,
156921
157454
  lastActiveMsAgo,
156922
157455
  lastAnyActivityMsAgo,
157456
+ lastImRequestMsAgo,
156923
157457
  isPeerActive: Boolean(s.isPeerActive),
156924
157458
  ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
156925
157459
  };
@@ -157086,6 +157620,7 @@ var Bridge = class {
157086
157620
  this.wireSessionDiagnostics();
157087
157621
  this.wireFabricWarnings();
157088
157622
  this.startSessionRotation();
157623
+ this.startWedgeWatchdog();
157089
157624
  this.startMdnsAddressWatch();
157090
157625
  logMemoryUsage(this.log, "bridge running");
157091
157626
  diagnosticEventBus.emit("bridge_started", `Bridge started`, {
@@ -157285,6 +157820,29 @@ ${e?.toString()}`);
157285
157820
  };
157286
157821
  sessionManager.sessions.added.on(this.sessionAddedHandler);
157287
157822
  sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
157823
+ this.wireImRequestTracking();
157824
+ } catch {
157825
+ }
157826
+ }
157827
+ // Stamp the time of every inbound Interaction Model request per session by
157828
+ // wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
157829
+ // tell a live-but-consuming controller from one that only keeps acking.
157830
+ wireImRequestTracking() {
157831
+ try {
157832
+ const is = this.server.env.get(InteractionServer);
157833
+ const marked = is;
157834
+ if (marked[imWrapMarker]) {
157835
+ return;
157836
+ }
157837
+ const original = is.onNewExchange.bind(is);
157838
+ is.onNewExchange = (exchange, message) => {
157839
+ const session = exchange.session;
157840
+ if (session) {
157841
+ this.lastImRequestAt.set(session, Date.now());
157842
+ }
157843
+ return original(exchange, message);
157844
+ };
157845
+ marked[imWrapMarker] = true;
157288
157846
  } catch {
157289
157847
  }
157290
157848
  }
@@ -157447,6 +158005,7 @@ ${e?.toString()}`);
157447
158005
  }
157448
158006
  this.staleSessionTimers.clear();
157449
158007
  this.stopSessionRotation();
158008
+ this.stopWedgeWatchdog();
157450
158009
  this.stopMdnsAddressWatch();
157451
158010
  this.sessionStartedAt.clear();
157452
158011
  }
@@ -157512,6 +158071,72 @@ ${e?.toString()}`);
157512
158071
  this.rotationTimer = null;
157513
158072
  }
157514
158073
  }
158074
+ // Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
158075
+ // the one session wedged on "Updating" and rotate just that one.
158076
+ startWedgeWatchdog() {
158077
+ this.stopWedgeWatchdog();
158078
+ if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
158079
+ return;
158080
+ }
158081
+ this.wedgeWatchdogTimer = setInterval(
158082
+ () => this.runWedgeWatchdogCheck(),
158083
+ ROTATION_CHECK_INTERVAL_MS
158084
+ );
158085
+ this.log.info(
158086
+ `Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
158087
+ );
158088
+ }
158089
+ stopWedgeWatchdog() {
158090
+ if (this.wedgeWatchdogTimer) {
158091
+ clearInterval(this.wedgeWatchdogTimer);
158092
+ this.wedgeWatchdogTimer = null;
158093
+ }
158094
+ }
158095
+ // Rotate exactly the sessions the pure rule flags as wedged. Closing is the
158096
+ // same graceful-then-force path age rotation uses, so a false positive just
158097
+ // re-CASEs the controller.
158098
+ runWedgeWatchdogCheck() {
158099
+ try {
158100
+ const sessionManager = this.server.env.get(SessionManager);
158101
+ const now = Date.now();
158102
+ const closes = [];
158103
+ for (const s of [...sessionManager.sessions]) {
158104
+ if (s.isClosing) continue;
158105
+ const startedAt = this.sessionStartedAt.get(s.id);
158106
+ const sessionAgeMs = startedAt != null ? now - startedAt : 0;
158107
+ const lastImAt = this.lastImRequestAt.get(s);
158108
+ const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
158109
+ const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
158110
+ const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
158111
+ if (!decideWedgeRotation({
158112
+ subscriptionCount: s.subscriptions.size,
158113
+ sessionAgeMs,
158114
+ lastImRequestMsAgo,
158115
+ lastRotatedMsAgo
158116
+ })) {
158117
+ continue;
158118
+ }
158119
+ const silenceMin = Math.round(
158120
+ (lastImRequestMsAgo ?? sessionAgeMs) / 6e4
158121
+ );
158122
+ this.log.info(
158123
+ `Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
158124
+ );
158125
+ this.wedgeLastRotatedAt.set(s, now);
158126
+ closes.push(
158127
+ s.initiateClose().catch(() => {
158128
+ return s.initiateForceClose({
158129
+ cause: new Error("wedge watchdog, forcing")
158130
+ });
158131
+ })
158132
+ );
158133
+ }
158134
+ if (closes.length > 0) {
158135
+ Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
158136
+ }
158137
+ } catch {
158138
+ }
158139
+ }
157515
158140
  // Poll the interface addresses mDNS advertises and re-announce when they
157516
158141
  // change. matter.js caches the operational records at first announcement, so
157517
158142
  // a dynamic ISP IPv6 prefix change keeps advertising the dead global address
@@ -157643,6 +158268,7 @@ ${e?.toString()}`);
157643
158268
  if (this.status.code === BridgeStatus.Running) {
157644
158269
  this.startAutoForceSyncIfEnabled();
157645
158270
  this.startSessionRotation();
158271
+ this.startWedgeWatchdog();
157646
158272
  }
157647
158273
  } catch (e) {
157648
158274
  const reason = "Failed to update bridge due to error:";
@@ -157765,66 +158391,6 @@ var AggregatorEndpoint2 = class extends Endpoint {
157765
158391
  }
157766
158392
  };
157767
158393
 
157768
- // src/matter/endpoints/entity-endpoint.ts
157769
- init_esm7();
157770
- var EntityEndpoint = class extends Endpoint {
157771
- constructor(type, entityId, customName, mappedEntityIds, endpointId) {
157772
- super(type, { id: endpointId ?? createEndpointId(entityId, customName) });
157773
- this.entityId = entityId;
157774
- this.mappedEntityIds = mappedEntityIds ?? [];
157775
- }
157776
- entityId;
157777
- mappedEntityIds;
157778
- lastMappedStates = {};
157779
- hasMappedEntityChanged(states) {
157780
- let changed = false;
157781
- for (const mappedId of this.mappedEntityIds) {
157782
- const mappedState = states[mappedId];
157783
- if (!mappedState) continue;
157784
- const fp = mappedState.state;
157785
- if (fp !== this.lastMappedStates[mappedId]) {
157786
- this.lastMappedStates[mappedId] = fp;
157787
- changed = true;
157788
- }
157789
- }
157790
- return changed;
157791
- }
157792
- };
157793
- function createEndpointId(entityId, customName) {
157794
- const baseName = customName || entityId;
157795
- return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
157796
- }
157797
- function getMappedEntityIds(mapping) {
157798
- if (!mapping) return [];
157799
- const ids = [];
157800
- if (mapping.batteryEntity && !mapping.disableBatteryMapping) {
157801
- ids.push(mapping.batteryEntity);
157802
- }
157803
- if (mapping.faultEntity) ids.push(mapping.faultEntity);
157804
- if (mapping.chargingStateEntity) ids.push(mapping.chargingStateEntity);
157805
- if (mapping.temperatureEntity) ids.push(mapping.temperatureEntity);
157806
- if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
157807
- if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
157808
- if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
157809
- if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
157810
- if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
157811
- if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
157812
- if (mapping.powerEntity) ids.push(mapping.powerEntity);
157813
- if (mapping.energyEntity) ids.push(mapping.energyEntity);
157814
- if (mapping.voltageEntity) ids.push(mapping.voltageEntity);
157815
- if (mapping.currentEntity) ids.push(mapping.currentEntity);
157816
- if (mapping.batteryPowerEntity) ids.push(mapping.batteryPowerEntity);
157817
- if (mapping.batteryEnergyEntity) ids.push(mapping.batteryEnergyEntity);
157818
- if (mapping.currentRoomEntity) ids.push(mapping.currentRoomEntity);
157819
- if (mapping.cleanedAreaEntity) ids.push(mapping.cleanedAreaEntity);
157820
- if (mapping.composedEntities) {
157821
- for (const sub of mapping.composedEntities) {
157822
- if (sub.entityId) ids.push(sub.entityId);
157823
- }
157824
- }
157825
- return ids;
157826
- }
157827
-
157828
158394
  // src/matter/endpoints/legacy/legacy-endpoint.ts
157829
158395
  init_dist();
157830
158396
  init_esm();
@@ -157903,9 +158469,9 @@ function resolveBatteryPercent(state) {
157903
158469
  return BATTERY_ENUM_PERCENT[state.toLowerCase()] ?? null;
157904
158470
  }
157905
158471
  var EntityStateProvider = class extends Service {
157906
- constructor(registry2) {
158472
+ constructor(registry3) {
157907
158473
  super("EntityStateProvider");
157908
- this.registry = registry2;
158474
+ this.registry = registry3;
157909
158475
  }
157910
158476
  registry;
157911
158477
  /**
@@ -159823,11 +160389,11 @@ function createEndpointId2(entityId, customName) {
159823
160389
  const baseName = customName || entityId;
159824
160390
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
159825
160391
  }
159826
- function buildEntityPayload(registry2, entityId) {
159827
- const state = registry2.initialState(entityId);
160392
+ function buildEntityPayload(registry3, entityId) {
160393
+ const state = registry3.initialState(entityId);
159828
160394
  if (!state) return void 0;
159829
- const entity = registry2.entity(entityId);
159830
- const deviceRegistry = registry2.deviceOf(entityId);
160395
+ const entity = registry3.entity(entityId);
160396
+ const deviceRegistry = registry3.deviceOf(entityId);
159831
160397
  return {
159832
160398
  entity_id: entityId,
159833
160399
  state,
@@ -159842,8 +160408,8 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
159842
160408
  lastStates = /* @__PURE__ */ new Map();
159843
160409
  debouncedUpdates = /* @__PURE__ */ new Map();
159844
160410
  static async create(config11) {
159845
- const { registry: registry2, primaryEntityId } = config11;
159846
- const primaryPayload = buildEntityPayload(registry2, primaryEntityId);
160411
+ const { registry: registry3, primaryEntityId } = config11;
160412
+ const primaryPayload = buildEntityPayload(registry3, primaryEntityId);
159847
160413
  if (!primaryPayload) return void 0;
159848
160414
  const airPurifierAttributes = primaryPayload.state.attributes;
159849
160415
  const supportedFeatures = airPurifierAttributes.supported_features ?? 0;
@@ -159931,7 +160497,7 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
159931
160497
  let tempSub;
159932
160498
  if (config11.temperatureEntityId) {
159933
160499
  const tempPayload = buildEntityPayload(
159934
- registry2,
160500
+ registry3,
159935
160501
  config11.temperatureEntityId
159936
160502
  );
159937
160503
  if (tempPayload) {
@@ -159946,7 +160512,7 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
159946
160512
  }
159947
160513
  let humSub;
159948
160514
  if (config11.humidityEntityId) {
159949
- const humPayload = buildEntityPayload(registry2, config11.humidityEntityId);
160515
+ const humPayload = buildEntityPayload(registry3, config11.humidityEntityId);
159950
160516
  if (humPayload) {
159951
160517
  humSub = new Endpoint(
159952
160518
  HumiditySubType.set({
@@ -161400,14 +161966,14 @@ function createEndpointId3(entityId, customName) {
161400
161966
  const baseName = customName || entityId;
161401
161967
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
161402
161968
  }
161403
- function buildEntityPayload2(registry2, entityId) {
161404
- const state = registry2.initialState(entityId);
161969
+ function buildEntityPayload2(registry3, entityId) {
161970
+ const state = registry3.initialState(entityId);
161405
161971
  if (!state) return void 0;
161406
161972
  return {
161407
161973
  entity_id: entityId,
161408
161974
  state,
161409
- registry: registry2.entity(entityId),
161410
- deviceRegistry: registry2.deviceOf(entityId)
161975
+ registry: registry3.entity(entityId),
161976
+ deviceRegistry: registry3.deviceOf(entityId)
161411
161977
  };
161412
161978
  }
161413
161979
  var ComposedClimateFanEndpoint = class _ComposedClimateFanEndpoint extends Endpoint {
@@ -161417,8 +161983,8 @@ var ComposedClimateFanEndpoint = class _ComposedClimateFanEndpoint extends Endpo
161417
161983
  lastStates = /* @__PURE__ */ new Map();
161418
161984
  debouncedUpdates = /* @__PURE__ */ new Map();
161419
161985
  static async create(config11) {
161420
- const { registry: registry2, primaryEntityId } = config11;
161421
- const payload = buildEntityPayload2(registry2, primaryEntityId);
161986
+ const { registry: registry3, primaryEntityId } = config11;
161987
+ const payload = buildEntityPayload2(registry3, primaryEntityId);
161422
161988
  if (!payload) return void 0;
161423
161989
  const endpointId = config11.endpointId ?? createEndpointId3(primaryEntityId, config11.customName);
161424
161990
  const mapping = config11.mapping ?? {
@@ -161676,11 +162242,11 @@ function createEndpointId4(entityId, customName) {
161676
162242
  const baseName = customName || entityId;
161677
162243
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
161678
162244
  }
161679
- function buildEntityPayload3(registry2, entityId) {
161680
- const state = registry2.initialStateIncludingUnfiltered(entityId);
162245
+ function buildEntityPayload3(registry3, entityId) {
162246
+ const state = registry3.initialStateIncludingUnfiltered(entityId);
161681
162247
  if (!state) return void 0;
161682
- const entity = registry2.entityIncludingUnfiltered(entityId);
161683
- const deviceRegistry = registry2.deviceOfIncludingUnfiltered(entityId);
162248
+ const entity = registry3.entityIncludingUnfiltered(entityId);
162249
+ const deviceRegistry = registry3.deviceOfIncludingUnfiltered(entityId);
161684
162250
  return {
161685
162251
  entity_id: entityId,
161686
162252
  state,
@@ -161695,8 +162261,8 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
161695
162261
  lastStates = /* @__PURE__ */ new Map();
161696
162262
  debouncedUpdates = /* @__PURE__ */ new Map();
161697
162263
  static async create(config11) {
161698
- const { registry: registry2, primaryEntityId } = config11;
161699
- const primaryPayload = buildEntityPayload3(registry2, primaryEntityId);
162264
+ const { registry: registry3, primaryEntityId } = config11;
162265
+ const primaryPayload = buildEntityPayload3(registry3, primaryEntityId);
161700
162266
  if (!primaryPayload) return void 0;
161701
162267
  let parentType = BridgedNodeEndpoint.with(
161702
162268
  BasicInformationServer2,
@@ -161740,7 +162306,7 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
161740
162306
  parts.push(tempSub);
161741
162307
  let humSub;
161742
162308
  if (config11.humidityEntityId) {
161743
- const humPayload = buildEntityPayload3(registry2, config11.humidityEntityId);
162309
+ const humPayload = buildEntityPayload3(registry3, config11.humidityEntityId);
161744
162310
  if (humPayload) {
161745
162311
  humSub = new Endpoint(
161746
162312
  HumiditySubType2.set({
@@ -161754,7 +162320,7 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
161754
162320
  let pressSub;
161755
162321
  if (config11.pressureEntityId) {
161756
162322
  const pressPayload = buildEntityPayload3(
161757
- registry2,
162323
+ registry3,
161758
162324
  config11.pressureEntityId
161759
162325
  );
161760
162326
  if (pressPayload) {
@@ -162708,6 +163274,15 @@ function clearPendingTilt(st) {
162708
163274
  st.pendingTilt = null;
162709
163275
  }
162710
163276
  var coverOptimistic = /* @__PURE__ */ new WeakMap();
163277
+ function haRestEnd(action) {
163278
+ if (action.action.includes("open_cover")) {
163279
+ return "open";
163280
+ }
163281
+ if (action.action.includes("close_cover")) {
163282
+ return "close";
163283
+ }
163284
+ return null;
163285
+ }
162711
163286
  function getCoverOptimistic(endpoint) {
162712
163287
  let st = coverOptimistic.get(endpoint);
162713
163288
  if (!st) {
@@ -162822,7 +163397,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
162822
163397
  if (!entry) {
162823
163398
  return MovementStatus.Stopped;
162824
163399
  }
162825
- const reachedTarget = positionAware && current100ths != null && existingTarget != null && Math.abs(current100ths - existingTarget) < 100;
163400
+ const getExpected = config11.getExpectedRestPosition;
163401
+ const expectedPercent = entry.expectedRestEnd != null && getExpected != null ? getExpected(entry.expectedRestEnd, this.agent) : null;
163402
+ const goal = expectedPercent != null ? expectedPercent * 100 : existingTarget;
163403
+ const reachedTarget = positionAware && current100ths != null && goal != null && Math.abs(current100ths - goal) < 100;
162826
163404
  const expired = Date.now() - entry.startedAt >= optimisticMovementTimeoutMs;
162827
163405
  if (reachedTarget || expired) {
162828
163406
  clearOptimisticAxis(optimistic, axis);
@@ -162927,7 +163505,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
162927
163505
  }
162928
163506
  // Record the optimistic direction and emit it now so a controller gets the
162929
163507
  // moving->stopped edge even when HA never reports a transitional state (#429).
162930
- startOptimisticMovement(axis, status3) {
163508
+ startOptimisticMovement(axis, status3, expectedRestEnd) {
162931
163509
  const optimistic = getCoverOptimistic(this.endpoint);
162932
163510
  clearOptimisticAxis(optimistic, axis);
162933
163511
  const endpoint = this.endpoint;
@@ -162972,7 +163550,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
162972
163550
  })();
162973
163551
  }, optimisticMovementTimeoutMs);
162974
163552
  timer.unref?.();
162975
- optimistic[axis] = { status: status3, startedAt, timer };
163553
+ optimistic[axis] = { status: status3, startedAt, timer, expectedRestEnd };
162976
163554
  this.writeOperationalStatus({ [axis]: status3 });
162977
163555
  }
162978
163556
  async handleMovement(type, _, direction, targetPercent100ths) {
@@ -163031,17 +163609,25 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163031
163609
  }
163032
163610
  handleLiftOpen() {
163033
163611
  clearPendingLift(getCoverDebounce(this.endpoint));
163034
- this.startOptimisticMovement("lift", MovementStatus.Opening);
163035
163612
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163036
163613
  const action = this.state.config.openCoverLift(void 0, this.agent);
163614
+ this.startOptimisticMovement(
163615
+ "lift",
163616
+ MovementStatus.Opening,
163617
+ haRestEnd(action)
163618
+ );
163037
163619
  logger218.info(`handleLiftOpen: calling action=${action.action}`);
163038
163620
  homeAssistant.callAction(action);
163039
163621
  }
163040
163622
  handleLiftClose() {
163041
163623
  clearPendingLift(getCoverDebounce(this.endpoint));
163042
- this.startOptimisticMovement("lift", MovementStatus.Closing);
163043
163624
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163044
163625
  const action = this.state.config.closeCoverLift(void 0, this.agent);
163626
+ this.startOptimisticMovement(
163627
+ "lift",
163628
+ MovementStatus.Closing,
163629
+ haRestEnd(action)
163630
+ );
163045
163631
  logger218.info(`handleLiftClose: calling action=${action.action}`);
163046
163632
  homeAssistant.callAction(action);
163047
163633
  }
@@ -163052,14 +163638,15 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163052
163638
  return;
163053
163639
  }
163054
163640
  this.state.targetPositionLiftPercent100ths = targetPercent100ths;
163055
- this.startOptimisticMovement(
163056
- "lift",
163057
- currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing
163058
- );
163059
163641
  const targetPosition = targetPercent100ths / 100;
163060
163642
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163061
163643
  const action = config11.setLiftPosition(targetPosition, this.agent);
163062
163644
  const entityId = homeAssistant.entityId;
163645
+ this.startOptimisticMovement(
163646
+ "lift",
163647
+ currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing,
163648
+ haRestEnd(action)
163649
+ );
163063
163650
  const actions = this.env.get(HomeAssistantActions);
163064
163651
  const st = getCoverDebounce(this.endpoint);
163065
163652
  st.pendingLift = { action, entityId, actions };
@@ -163092,21 +163679,27 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163092
163679
  const st = getCoverDebounce(this.endpoint);
163093
163680
  clearPendingTilt(st);
163094
163681
  if (st.pendingLift?.action.action.includes("tilt")) clearPendingLift(st);
163095
- this.startOptimisticMovement("tilt", MovementStatus.Opening);
163096
- const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163097
- homeAssistant.callAction(
163098
- this.state.config.openCoverTilt(void 0, this.agent)
163682
+ const action = this.state.config.openCoverTilt(void 0, this.agent);
163683
+ this.startOptimisticMovement(
163684
+ "tilt",
163685
+ MovementStatus.Opening,
163686
+ haRestEnd(action)
163099
163687
  );
163688
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163689
+ homeAssistant.callAction(action);
163100
163690
  }
163101
163691
  handleTiltClose() {
163102
163692
  const st = getCoverDebounce(this.endpoint);
163103
163693
  clearPendingTilt(st);
163104
163694
  if (st.pendingLift?.action.action.includes("tilt")) clearPendingLift(st);
163105
- this.startOptimisticMovement("tilt", MovementStatus.Closing);
163106
- const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163107
- homeAssistant.callAction(
163108
- this.state.config.closeCoverTilt(void 0, this.agent)
163695
+ const action = this.state.config.closeCoverTilt(void 0, this.agent);
163696
+ this.startOptimisticMovement(
163697
+ "tilt",
163698
+ MovementStatus.Closing,
163699
+ haRestEnd(action)
163109
163700
  );
163701
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163702
+ homeAssistant.callAction(action);
163110
163703
  }
163111
163704
  handleGoToTiltPosition(targetPercent100ths) {
163112
163705
  const config11 = this.state.config;
@@ -163115,14 +163708,15 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163115
163708
  return;
163116
163709
  }
163117
163710
  this.state.targetPositionTiltPercent100ths = targetPercent100ths;
163118
- this.startOptimisticMovement(
163119
- "tilt",
163120
- currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing
163121
- );
163122
163711
  const targetPosition = targetPercent100ths / 100;
163123
163712
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163124
163713
  const action = config11.setTiltPosition(targetPosition, this.agent);
163125
163714
  const entityId = homeAssistant.entityId;
163715
+ this.startOptimisticMovement(
163716
+ "tilt",
163717
+ currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing,
163718
+ haRestEnd(action)
163719
+ );
163126
163720
  const actions = this.env.get(HomeAssistantActions);
163127
163721
  const st = getCoverDebounce(this.endpoint);
163128
163722
  st.pendingTilt = { action, entityId, actions };
@@ -163264,6 +163858,7 @@ var adjustPositionForWriting2 = (position, agent) => {
163264
163858
  const matterSem = usesMatterSemantics(agent);
163265
163859
  return adjustPositionForWriting(position, featureFlags, matterSem);
163266
163860
  };
163861
+ var expectedRestPosition = (end, agent) => adjustPositionForReading2(end === "open" ? 100 : 0, agent);
163267
163862
  var shouldSwapOpenClose = (agent) => {
163268
163863
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
163269
163864
  const entitySwap = homeAssistant.state.mapping?.coverSwapOpenClose;
@@ -163351,11 +163946,11 @@ var config6 = {
163351
163946
  }
163352
163947
  return position == null ? null : adjustPositionForReading2(position, agent);
163353
163948
  },
163949
+ getExpectedRestPosition: expectedRestPosition,
163354
163950
  getCoverType: (entity) => deviceClassMapping(entity)?.type,
163355
163951
  getEndProductType: (entity) => deviceClassMapping(entity)?.endProductType,
163356
163952
  getMovementStatus: (entity, agent) => {
163357
- const { featureFlags } = agent.env.get(BridgeDataProvider);
163358
- const swapped = featureFlags?.coverSwapOpenClose === true;
163953
+ const swapped = shouldSwapOpenClose(agent);
163359
163954
  const coverState = entity.state;
163360
163955
  if (coverState === CoverDeviceState.opening) {
163361
163956
  return swapped ? WindowCovering3.MovementStatus.Closing : WindowCovering3.MovementStatus.Opening;
@@ -167397,6 +167992,284 @@ var ElectricalMeterType = ElectricalMeterDevice.with(
167397
167992
  EnergyServer
167398
167993
  );
167399
167994
 
167995
+ // src/matter/endpoints/legacy/sensor/devices/energy-evse.ts
167996
+ init_esm();
167997
+ init_home_assistant_actions();
167998
+ init_home_assistant_entity_behavior();
167999
+
168000
+ // src/matter/endpoints/legacy/sensor/devices/evse-status.ts
168001
+ function mapEvseStatus(rawState, chargingSwitchOn) {
168002
+ const raw = rawState.toLowerCase().trim();
168003
+ const switchSupply = chargingSwitchOn ? EnergyEvse3.SupplyState.ChargingEnabled : EnergyEvse3.SupplyState.Disabled;
168004
+ if (raw === "" || raw === "unknown" || raw === "unavailable") {
168005
+ return {
168006
+ state: null,
168007
+ supplyState: EnergyEvse3.SupplyState.Disabled,
168008
+ faultState: EnergyEvse3.FaultState.NoError
168009
+ };
168010
+ }
168011
+ if (raw.includes("error") || raw.includes("fault")) {
168012
+ return {
168013
+ state: EnergyEvse3.State.Fault,
168014
+ supplyState: EnergyEvse3.SupplyState.DisabledError,
168015
+ faultState: EnergyEvse3.FaultState.Other
168016
+ };
168017
+ }
168018
+ if (raw.includes("not connected") || raw.includes("not_connected") || raw.includes("disconnected") || raw.includes("no vehicle")) {
168019
+ return {
168020
+ state: EnergyEvse3.State.NotPluggedIn,
168021
+ supplyState: switchSupply,
168022
+ faultState: EnergyEvse3.FaultState.NoError
168023
+ };
168024
+ }
168025
+ if (raw.includes("not charging") || raw.includes("not_charging") || raw.includes("not-charging")) {
168026
+ return {
168027
+ state: EnergyEvse3.State.PluggedInNoDemand,
168028
+ supplyState: switchSupply,
168029
+ faultState: EnergyEvse3.FaultState.NoError
168030
+ };
168031
+ }
168032
+ if (raw.includes("charg") && !raw.includes("discharg")) {
168033
+ return {
168034
+ state: EnergyEvse3.State.PluggedInCharging,
168035
+ supplyState: EnergyEvse3.SupplyState.ChargingEnabled,
168036
+ faultState: EnergyEvse3.FaultState.NoError
168037
+ };
168038
+ }
168039
+ if (raw.includes("connected") || raw.includes("ready") || raw.includes("awaiting") || raw.includes("completed") || raw.includes("sleep") || raw.includes("paused")) {
168040
+ return {
168041
+ state: EnergyEvse3.State.PluggedInNoDemand,
168042
+ supplyState: switchSupply,
168043
+ faultState: EnergyEvse3.FaultState.NoError
168044
+ };
168045
+ }
168046
+ return {
168047
+ state: null,
168048
+ supplyState: EnergyEvse3.SupplyState.Disabled,
168049
+ faultState: EnergyEvse3.FaultState.NoError
168050
+ };
168051
+ }
168052
+
168053
+ // src/matter/endpoints/legacy/sensor/devices/energy-evse.ts
168054
+ var logger229 = Logger.get("EnergyEvse");
168055
+ var MIN_CHARGE_CURRENT_MA = 6e3;
168056
+ var MAX_CHARGE_CURRENT_MA = 32e3;
168057
+ var CIRCUIT_CAPACITY_MA = 32e3;
168058
+ var MAX_TIMER_MS = 2147483647;
168059
+ function clampMa(ma) {
168060
+ return Math.min(CIRCUIT_CAPACITY_MA, Math.max(MIN_CHARGE_CURRENT_MA, ma));
168061
+ }
168062
+ var evseChargingTimers = /* @__PURE__ */ new WeakMap();
168063
+ function clearEvseChargingTimer(endpoint) {
168064
+ const timer = evseChargingTimers.get(endpoint);
168065
+ if (timer) {
168066
+ clearTimeout(timer);
168067
+ evseChargingTimers.delete(endpoint);
168068
+ }
168069
+ }
168070
+ var EvseStatusServer = class _EvseStatusServer extends EnergyEvseServer {
168071
+ async initialize() {
168072
+ await super.initialize();
168073
+ const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168074
+ this.update();
168075
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168076
+ }
168077
+ async [Symbol.asyncDispose]() {
168078
+ clearEvseChargingTimer(this.endpoint);
168079
+ await super[Symbol.asyncDispose]();
168080
+ }
168081
+ chargingSwitchIsOn() {
168082
+ const mapping = this.agent.get(HomeAssistantEntityBehavior).state.mapping;
168083
+ const switchEntity = mapping?.chargingSwitchEntity;
168084
+ if (!switchEntity) return false;
168085
+ const stateProvider = this.agent.env.get(EntityStateProvider);
168086
+ return stateProvider.getState(switchEntity)?.state === "on";
168087
+ }
168088
+ update() {
168089
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
168090
+ const raw = homeAssistant.entity.state?.state ?? "";
168091
+ const status3 = mapEvseStatus(raw, this.chargingSwitchIsOn());
168092
+ const patch = {
168093
+ state: status3.state,
168094
+ supplyState: status3.supplyState,
168095
+ faultState: status3.faultState
168096
+ };
168097
+ const mapping = homeAssistant.state.mapping;
168098
+ if (mapping?.currentLimitEntity) {
168099
+ const stateProvider = this.agent.env.get(EntityStateProvider);
168100
+ const amps = stateProvider.getNumericState(mapping.currentLimitEntity);
168101
+ if (amps != null) {
168102
+ patch.maximumChargeCurrent = clampMa(Math.round(amps * 1e3));
168103
+ }
168104
+ }
168105
+ applyPatchState(this.state, patch);
168106
+ }
168107
+ enableCharging(request) {
168108
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
168109
+ const mapping = homeAssistant.state.mapping;
168110
+ const until = request.chargingEnabledUntil;
168111
+ if (until != null && until * 1e3 <= Date.now()) {
168112
+ this.disable();
168113
+ return;
168114
+ }
168115
+ clearEvseChargingTimer(this.endpoint);
168116
+ let dispatched = false;
168117
+ if (mapping?.chargingSwitchEntity) {
168118
+ homeAssistant.callAction({
168119
+ action: "switch.turn_on",
168120
+ target: mapping.chargingSwitchEntity
168121
+ });
168122
+ dispatched = true;
168123
+ }
168124
+ const maxCurrent = request.maximumChargeCurrent;
168125
+ if (mapping?.currentLimitEntity && maxCurrent != null) {
168126
+ const amps = Math.max(
168127
+ MIN_CHARGE_CURRENT_MA / 1e3,
168128
+ Math.floor(clampMa(Number(maxCurrent)) / 1e3)
168129
+ );
168130
+ homeAssistant.callAction({
168131
+ action: "number.set_value",
168132
+ target: mapping.currentLimitEntity,
168133
+ data: { value: amps }
168134
+ });
168135
+ dispatched = true;
168136
+ }
168137
+ if (!dispatched) {
168138
+ logger229.debug(
168139
+ `enableCharging ignored for ${homeAssistant.entityId}: no charging switch or current limit mapped`
168140
+ );
168141
+ return;
168142
+ }
168143
+ const patch = {
168144
+ supplyState: EnergyEvse3.SupplyState.ChargingEnabled,
168145
+ chargingEnabledUntil: until ?? null
168146
+ };
168147
+ if (request.minimumChargeCurrent != null) {
168148
+ patch.minimumChargeCurrent = clampMa(
168149
+ Number(request.minimumChargeCurrent)
168150
+ );
168151
+ }
168152
+ if (maxCurrent != null) {
168153
+ patch.maximumChargeCurrent = clampMa(Number(maxCurrent));
168154
+ }
168155
+ applyPatchState(this.state, patch);
168156
+ if (until != null) {
168157
+ const delay = until * 1e3 - Date.now();
168158
+ if (delay > 0 && delay <= MAX_TIMER_MS) {
168159
+ const endpoint = this.endpoint;
168160
+ const actions = this.env.get(HomeAssistantActions);
168161
+ const entityId = homeAssistant.entityId;
168162
+ const switchEntity = mapping?.chargingSwitchEntity;
168163
+ const timer = setTimeout(() => {
168164
+ void (async () => {
168165
+ try {
168166
+ if (evseChargingTimers.get(endpoint) !== timer) return;
168167
+ if (switchEntity) {
168168
+ actions.call(
168169
+ { action: "switch.turn_off", target: switchEntity },
168170
+ entityId
168171
+ );
168172
+ }
168173
+ await endpoint.setStateOf(_EvseStatusServer, {
168174
+ supplyState: EnergyEvse3.SupplyState.Disabled,
168175
+ chargingEnabledUntil: null
168176
+ });
168177
+ } catch (error) {
168178
+ logger229.debug(
168179
+ `EVSE charge-window expiry write failed (endpoint may be closing): ${error}`
168180
+ );
168181
+ } finally {
168182
+ if (evseChargingTimers.get(endpoint) === timer) {
168183
+ evseChargingTimers.delete(endpoint);
168184
+ }
168185
+ }
168186
+ })();
168187
+ }, delay);
168188
+ timer.unref?.();
168189
+ evseChargingTimers.set(endpoint, timer);
168190
+ }
168191
+ }
168192
+ }
168193
+ disable() {
168194
+ clearEvseChargingTimer(this.endpoint);
168195
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
168196
+ const mapping = homeAssistant.state.mapping;
168197
+ if (mapping?.chargingSwitchEntity) {
168198
+ homeAssistant.callAction({
168199
+ action: "switch.turn_off",
168200
+ target: mapping.chargingSwitchEntity
168201
+ });
168202
+ }
168203
+ if (mapping?.chargingSwitchEntity || mapping?.currentLimitEntity) {
168204
+ applyPatchState(this.state, {
168205
+ supplyState: EnergyEvse3.SupplyState.Disabled,
168206
+ chargingEnabledUntil: null
168207
+ });
168208
+ } else {
168209
+ logger229.debug(
168210
+ `disable ignored for ${homeAssistant.entityId}: no charging switch mapped`
168211
+ );
168212
+ }
168213
+ }
168214
+ };
168215
+ var EvseStatusSeeded = EvseStatusServer.set({
168216
+ supplyState: EnergyEvse3.SupplyState.Disabled,
168217
+ faultState: EnergyEvse3.FaultState.NoError,
168218
+ circuitCapacity: CIRCUIT_CAPACITY_MA,
168219
+ minimumChargeCurrent: MIN_CHARGE_CURRENT_MA,
168220
+ maximumChargeCurrent: MAX_CHARGE_CURRENT_MA
168221
+ });
168222
+ var EvseModeSeeded = EnergyEvseModeServer.set({
168223
+ supportedModes: [
168224
+ {
168225
+ label: "Manual",
168226
+ mode: 1,
168227
+ modeTags: [{ value: EnergyEvseMode3.ModeTag.Manual }]
168228
+ }
168229
+ ],
168230
+ currentMode: 1
168231
+ });
168232
+ var WiredPowerSourceBase = PowerSourceServer.with("Wired");
168233
+ var EvsePowerSourceServer = class extends WiredPowerSourceBase {
168234
+ async initialize() {
168235
+ await super.initialize();
168236
+ const endpointNumber = this.endpoint.number;
168237
+ if (endpointNumber != null) {
168238
+ applyPatchState(this.state, { endpointList: [endpointNumber] });
168239
+ }
168240
+ }
168241
+ };
168242
+ var EvsePowerSource = EvsePowerSourceServer.set({
168243
+ status: PowerSource3.PowerSourceStatus.Active,
168244
+ order: 0,
168245
+ description: "Mains",
168246
+ wiredCurrentType: PowerSource3.WiredCurrentType.Ac
168247
+ });
168248
+ var EnergyEvseBase = EnergyEvseDevice.with(
168249
+ BasicInformationServer2,
168250
+ IdentifyServer2,
168251
+ HomeAssistantEntityBehavior,
168252
+ EvseStatusSeeded,
168253
+ EvseModeSeeded,
168254
+ EvsePowerSource,
168255
+ // Always mount the measurement clusters; both seed safe defaults when no
168256
+ // power/energy entity is mapped, so the endpoint honestly carries the
168257
+ // ElectricalSensor device type.
168258
+ PowerServer,
168259
+ EnergyServer,
168260
+ DescriptorServer
168261
+ ).set({
168262
+ descriptor: {
168263
+ deviceTypeList: [
168264
+ { deviceType: DeviceTypeId(1292), revision: 2 },
168265
+ { deviceType: DeviceTypeId(1296), revision: 1 }
168266
+ ]
168267
+ }
168268
+ });
168269
+ function energyEvseType() {
168270
+ return EnergyEvseBase;
168271
+ }
168272
+
167400
168273
  // src/matter/behaviors/flow-measurement-server.ts
167401
168274
  init_home_assistant_entity_behavior();
167402
168275
  var FlowMeasurementServerBase = class extends FlowMeasurementServer {
@@ -168136,7 +169009,7 @@ var TvocConcentrationMeasurementServer = class extends TvocConcentrationMeasurem
168136
169009
  };
168137
169010
 
168138
169011
  // src/matter/endpoints/legacy/sensor/devices/tvoc-sensor.ts
168139
- var logger229 = Logger.get("TvocSensor");
169012
+ var logger230 = Logger.get("TvocSensor");
168140
169013
  function airQualityFromUgm3(value) {
168141
169014
  if (value <= 300) return AirQuality3.AirQualityEnum.Good;
168142
169015
  if (value <= 1e3) return AirQuality3.AirQualityEnum.Fair;
@@ -168177,17 +169050,17 @@ var TvocAirQualityServer = class extends TvocAirQualityServerBase {
168177
169050
  const attributes9 = entity.state.attributes;
168178
169051
  const deviceClass = attributes9.device_class;
168179
169052
  let airQuality = AirQuality3.AirQualityEnum.Unknown;
168180
- logger229.debug(
169053
+ logger230.debug(
168181
169054
  `[${entity.entity_id}] TVOC update: state="${state}", device_class="${deviceClass}"`
168182
169055
  );
168183
169056
  if (state != null && !Number.isNaN(+state)) {
168184
169057
  const value = +state;
168185
169058
  airQuality = deviceClass === SensorDeviceClass.volatile_organic_compounds ? airQualityFromUgm3(value) : airQualityFromPpb(value);
168186
- logger229.debug(
169059
+ logger230.debug(
168187
169060
  `[${entity.entity_id}] TVOC value=${value} (${deviceClass}) -> airQuality=${AirQuality3.AirQualityEnum[airQuality]}`
168188
169061
  );
168189
169062
  } else {
168190
- logger229.warn(
169063
+ logger230.warn(
168191
169064
  `[${entity.entity_id}] TVOC state not a valid number: "${state}"`
168192
169065
  );
168193
169066
  }
@@ -168401,7 +169274,7 @@ init_home_assistant_entity_behavior();
168401
169274
  // src/matter/behaviors/pm25-concentration-measurement-server.ts
168402
169275
  init_esm();
168403
169276
  init_home_assistant_entity_behavior();
168404
- var logger230 = Logger.get("Pm25ConcentrationMeasurementServer");
169277
+ var logger231 = Logger.get("Pm25ConcentrationMeasurementServer");
168405
169278
  var Pm25ConcentrationMeasurementServerBase = Pm25ConcentrationMeasurementServer.with(
168406
169279
  ConcentrationMeasurement3.Feature.NumericMeasurement
168407
169280
  );
@@ -168425,11 +169298,11 @@ var Pm25ConcentrationMeasurementServer2 = class extends Pm25ConcentrationMeasure
168425
169298
  if (this.state.measurementMedium === void 0) {
168426
169299
  this.state.measurementMedium = ConcentrationMeasurement3.MeasurementMedium.Air;
168427
169300
  }
168428
- logger230.debug(
169301
+ logger231.debug(
168429
169302
  "Pm25ConcentrationMeasurementServer: before super.initialize()"
168430
169303
  );
168431
169304
  await super.initialize();
168432
- logger230.debug(
169305
+ logger231.debug(
168433
169306
  "Pm25ConcentrationMeasurementServer: after super.initialize()"
168434
169307
  );
168435
169308
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
@@ -168452,7 +169325,7 @@ var Pm25ConcentrationMeasurementServer2 = class extends Pm25ConcentrationMeasure
168452
169325
  };
168453
169326
 
168454
169327
  // src/matter/endpoints/legacy/sensor/devices/pm25-sensor.ts
168455
- var logger231 = Logger.get("Pm25AirQualityServer");
169328
+ var logger232 = Logger.get("Pm25AirQualityServer");
168456
169329
  var Pm25AirQualityServerBase = AirQualityServer.with(
168457
169330
  AirQuality3.Feature.Fair,
168458
169331
  AirQuality3.Feature.Moderate,
@@ -168464,9 +169337,9 @@ var Pm25AirQualityServer = class extends Pm25AirQualityServerBase {
168464
169337
  if (this.state.airQuality === void 0) {
168465
169338
  this.state.airQuality = AirQuality3.AirQualityEnum.Unknown;
168466
169339
  }
168467
- logger231.debug("Pm25AirQualityServer: before super.initialize()");
169340
+ logger232.debug("Pm25AirQualityServer: before super.initialize()");
168468
169341
  await super.initialize();
168469
- logger231.debug("Pm25AirQualityServer: after super.initialize()");
169342
+ logger232.debug("Pm25AirQualityServer: after super.initialize()");
168470
169343
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168471
169344
  this.update(homeAssistant.entity);
168472
169345
  this.reactTo(homeAssistant.onChange, this.update, { offline: true });
@@ -168891,7 +169764,7 @@ init_home_assistant_entity_behavior();
168891
169764
  init_dist();
168892
169765
  init_esm();
168893
169766
  init_home_assistant_entity_behavior();
168894
- var logger232 = Logger.get("VacuumIdentifyServer");
169767
+ var logger233 = Logger.get("VacuumIdentifyServer");
168895
169768
  var IDENTIFY_BUTTON_SUFFIXES = ["_identify", "_locate", "_find_me"];
168896
169769
  var VacuumIdentifyServer = class extends IdentifyServer2 {
168897
169770
  triggerEffect(effect) {
@@ -168908,19 +169781,19 @@ var VacuumIdentifyServer = class extends IdentifyServer2 {
168908
169781
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
168909
169782
  const features3 = homeAssistant.entity.state.attributes.supported_features ?? 0;
168910
169783
  if (testBit(features3, VacuumDeviceFeature.LOCATE)) {
168911
- logger232.info(`${source} \u2192 vacuum.locate for ${homeAssistant.entityId}`);
169784
+ logger233.info(`${source} \u2192 vacuum.locate for ${homeAssistant.entityId}`);
168912
169785
  homeAssistant.callAction({ action: "vacuum.locate" });
168913
169786
  return;
168914
169787
  }
168915
169788
  const sibling = this.#findIdentifyButton(homeAssistant);
168916
169789
  if (sibling) {
168917
- logger232.info(
169790
+ logger233.info(
168918
169791
  `${source} \u2192 button.press ${sibling} for ${homeAssistant.entityId}`
168919
169792
  );
168920
169793
  homeAssistant.callAction({ action: "button.press", target: sibling });
168921
169794
  return;
168922
169795
  }
168923
- logger232.warn(
169796
+ logger233.warn(
168924
169797
  `${source} for ${homeAssistant.entityId}, LOCATE not in supported_features (${features3}), trying vacuum.locate anyway`
168925
169798
  );
168926
169799
  homeAssistant.callAction({ action: "vacuum.locate" });
@@ -168928,8 +169801,8 @@ var VacuumIdentifyServer = class extends IdentifyServer2 {
168928
169801
  #findIdentifyButton(homeAssistant) {
168929
169802
  const deviceId = homeAssistant.entity.registry?.device_id;
168930
169803
  if (!deviceId) return void 0;
168931
- const registry2 = this.env.get(HomeAssistantRegistry);
168932
- for (const entity of Object.values(registry2.entities)) {
169804
+ const registry3 = this.env.get(HomeAssistantRegistry);
169805
+ for (const entity of Object.values(registry3.entities)) {
168933
169806
  if (entity.device_id !== deviceId) continue;
168934
169807
  if (!entity.entity_id.startsWith("button.")) continue;
168935
169808
  const uniqueId = entity.unique_id ?? "";
@@ -169119,14 +169992,14 @@ init_esm();
169119
169992
 
169120
169993
  // src/matter/behaviors/service-area-server.ts
169121
169994
  init_esm();
169122
- var logger233 = Logger.get("ServiceAreaServer");
169995
+ var logger234 = Logger.get("ServiceAreaServer");
169123
169996
  var ServiceAreaWithProgress = ServiceAreaBehavior.with(
169124
169997
  ServiceArea3.Feature.ProgressReporting
169125
169998
  );
169126
169999
  var ServiceAreaServerBase = class extends ServiceAreaWithProgress {
169127
170000
  selectAreas(request) {
169128
170001
  const { newAreas } = request;
169129
- logger233.info(
170002
+ logger234.info(
169130
170003
  `ServiceArea selectAreas called with: ${JSON.stringify(newAreas)}`
169131
170004
  );
169132
170005
  const uniqueAreas = [...new Set(newAreas)];
@@ -169135,7 +170008,7 @@ var ServiceAreaServerBase = class extends ServiceAreaWithProgress {
169135
170008
  (id) => !supportedAreaIds.includes(id)
169136
170009
  );
169137
170010
  if (invalidAreas.length > 0) {
169138
- logger233.warn(`Invalid area IDs requested: ${invalidAreas.join(", ")}`);
170011
+ logger234.warn(`Invalid area IDs requested: ${invalidAreas.join(", ")}`);
169139
170012
  return {
169140
170013
  status: ServiceArea3.SelectAreasStatus.UnsupportedArea,
169141
170014
  statusText: `Invalid area IDs: ${invalidAreas.join(", ")}`
@@ -169147,7 +170020,7 @@ var ServiceAreaServerBase = class extends ServiceAreaWithProgress {
169147
170020
  status: ServiceArea3.OperationalStatus.Pending
169148
170021
  }));
169149
170022
  this.state.currentArea = null;
169150
- logger233.info(
170023
+ logger234.info(
169151
170024
  `ServiceArea: Stored ${uniqueAreas.length} areas for cleaning: ${uniqueAreas.join(", ")}`
169152
170025
  );
169153
170026
  return {
@@ -169168,7 +170041,7 @@ var ServiceAreaServerBase = class extends ServiceAreaWithProgress {
169168
170041
  ServiceAreaServerBase2.State = State;
169169
170042
  })(ServiceAreaServerBase || (ServiceAreaServerBase = {}));
169170
170043
  function ServiceAreaServer2(initialState) {
169171
- logger233.info(
170044
+ logger234.info(
169172
170045
  `Creating ServiceAreaServer with ${initialState.supportedAreas.length} areas`
169173
170046
  );
169174
170047
  return ServiceAreaServerBase.set({
@@ -169185,7 +170058,7 @@ var ServiceAreaWithMapsAndProgress = ServiceAreaBehavior.with(
169185
170058
  var ServiceAreaServerWithMapsBase = class extends ServiceAreaWithMapsAndProgress {
169186
170059
  selectAreas(request) {
169187
170060
  const { newAreas } = request;
169188
- logger233.info(
170061
+ logger234.info(
169189
170062
  `ServiceArea selectAreas called with: ${JSON.stringify(newAreas)}`
169190
170063
  );
169191
170064
  const uniqueAreas = [...new Set(newAreas)];
@@ -169194,7 +170067,7 @@ var ServiceAreaServerWithMapsBase = class extends ServiceAreaWithMapsAndProgress
169194
170067
  (id) => !supportedAreaIds.includes(id)
169195
170068
  );
169196
170069
  if (invalidAreas.length > 0) {
169197
- logger233.warn(`Invalid area IDs requested: ${invalidAreas.join(", ")}`);
170070
+ logger234.warn(`Invalid area IDs requested: ${invalidAreas.join(", ")}`);
169198
170071
  return {
169199
170072
  status: ServiceArea3.SelectAreasStatus.UnsupportedArea,
169200
170073
  statusText: `Invalid area IDs: ${invalidAreas.join(", ")}`
@@ -169206,7 +170079,7 @@ var ServiceAreaServerWithMapsBase = class extends ServiceAreaWithMapsAndProgress
169206
170079
  status: ServiceArea3.OperationalStatus.Pending
169207
170080
  }));
169208
170081
  this.state.currentArea = null;
169209
- logger233.info(
170082
+ logger234.info(
169210
170083
  `ServiceArea: Stored ${uniqueAreas.length} areas for cleaning: ${uniqueAreas.join(", ")}`
169211
170084
  );
169212
170085
  return {
@@ -169227,14 +170100,14 @@ var ServiceAreaServerWithMapsBase = class extends ServiceAreaWithMapsAndProgress
169227
170100
  ServiceAreaServerWithMapsBase2.State = State;
169228
170101
  })(ServiceAreaServerWithMapsBase || (ServiceAreaServerWithMapsBase = {}));
169229
170102
  function ServiceAreaServerWithMaps(initialState) {
169230
- logger233.info(
170103
+ logger234.info(
169231
170104
  `Creating ServiceAreaServer with Maps: ${initialState.supportedAreas.length} areas, ${initialState.supportedMaps.length} maps`
169232
170105
  );
169233
170106
  for (const map of initialState.supportedMaps) {
169234
170107
  const areaCount = initialState.supportedAreas.filter(
169235
170108
  (a) => a.mapId === map.mapId
169236
170109
  ).length;
169237
- logger233.info(` Map ${map.mapId}: "${map.name}" (${areaCount} areas)`);
170110
+ logger234.info(` Map ${map.mapId}: "${map.name}" (${areaCount} areas)`);
169238
170111
  }
169239
170112
  return ServiceAreaServerWithMapsBase.set({
169240
170113
  supportedAreas: initialState.supportedAreas,
@@ -169246,7 +170119,7 @@ function ServiceAreaServerWithMaps(initialState) {
169246
170119
  }
169247
170120
 
169248
170121
  // src/matter/endpoints/legacy/vacuum/behaviors/vacuum-service-area-server.ts
169249
- var logger234 = Logger.get("VacuumServiceAreaServer");
170122
+ var logger235 = Logger.get("VacuumServiceAreaServer");
169250
170123
  function toAreaId(roomId) {
169251
170124
  if (typeof roomId === "number") {
169252
170125
  return roomId;
@@ -169325,13 +170198,13 @@ function createVacuumServiceAreaServer(attributes9, roomEntities, includeUnnamed
169325
170198
  let rooms;
169326
170199
  if (roomEntities && roomEntities.length > 0) {
169327
170200
  rooms = buttonEntitiesToRooms(roomEntities, attributes9);
169328
- logger234.info(
170201
+ logger235.info(
169329
170202
  `Using ${rooms.length} button entities as rooms: ${rooms.map((r) => r.name).join(", ")}`
169330
170203
  );
169331
170204
  } else {
169332
170205
  rooms = parseVacuumRooms(attributes9, includeUnnamedRooms);
169333
170206
  if (rooms.length > 0) {
169334
- logger234.info(
170207
+ logger235.info(
169335
170208
  `Using ${rooms.length} rooms from attributes: ${rooms.map((r) => r.name).join(", ")}`
169336
170209
  );
169337
170210
  }
@@ -169385,7 +170258,7 @@ function createCustomServiceAreaServer(customAreas) {
169385
170258
  landmarkInfo: null
169386
170259
  }
169387
170260
  }));
169388
- logger234.info(
170261
+ logger235.info(
169389
170262
  `Using ${customAreas.length} custom service areas: ${customAreas.map((a) => a.name).join(", ")}`
169390
170263
  );
169391
170264
  return ServiceAreaServer2({
@@ -169407,7 +170280,7 @@ function createCleanAreaServiceAreaServer(cleanAreaRooms) {
169407
170280
  landmarkInfo: null
169408
170281
  }
169409
170282
  }));
169410
- logger234.info(
170283
+ logger235.info(
169411
170284
  `Using ${cleanAreaRooms.length} HA areas via CLEAN_AREA: ${cleanAreaRooms.map((r) => r.name).join(", ")}`
169412
170285
  );
169413
170286
  return ServiceAreaServer2({
@@ -169418,11 +170291,11 @@ function createCleanAreaServiceAreaServer(cleanAreaRooms) {
169418
170291
  }
169419
170292
 
169420
170293
  // src/matter/endpoints/legacy/vacuum/behaviors/vacuum-rvc-run-mode-server.ts
169421
- var logger235 = Logger.get("VacuumRvcRunModeServer");
170294
+ var logger236 = Logger.get("VacuumRvcRunModeServer");
169422
170295
  function buildValetudoSegmentAction(vacuumEntityId, segmentIds, valetudoIdentifier) {
169423
170296
  const identifier = valetudoIdentifier || vacuumEntityId.replace(/^vacuum\.valetudo_/, "");
169424
170297
  const topic = `valetudo/${identifier}/MapSegmentationCapability/clean/set`;
169425
- logger235.info(
170298
+ logger236.info(
169426
170299
  `Valetudo: mqtt.publish to ${topic}, segments: ${segmentIds.join(", ")}`
169427
170300
  );
169428
170301
  return {
@@ -169527,14 +170400,14 @@ function mergeBatchData(areas) {
169527
170400
  function handleCustomServiceAreas(selectedAreas, customAreas, session) {
169528
170401
  const matched = selectedAreas.map((areaId) => ({ areaId, area: customAreas[areaId - 1] })).filter((m) => !!m.area);
169529
170402
  if (matched.length === 0) {
169530
- logger235.warn(
170403
+ logger236.warn(
169531
170404
  `Custom service areas: no match for selected IDs ${selectedAreas.join(", ")}`
169532
170405
  );
169533
170406
  return { action: "vacuum.start" };
169534
170407
  }
169535
170408
  const batchArea = matched.find(({ area }) => area.batchDispatch === true);
169536
170409
  if (batchArea) {
169537
- logger235.info(
170410
+ logger236.info(
169538
170411
  `Custom service areas (batch): single call for ${matched.length} room(s): ${matched.map(({ area }) => area.name).join(", ")}`
169539
170412
  );
169540
170413
  session.pendingDispatches = [];
@@ -169555,7 +170428,7 @@ function handleCustomServiceAreas(selectedAreas, customAreas, session) {
169555
170428
  }
169556
170429
  };
169557
170430
  }
169558
- logger235.info(
170431
+ logger236.info(
169559
170432
  `Custom service areas: ${matched.length} room(s) queued: ${matched.map(({ area }) => `${area.service} (${area.name})`).join(", ")}`
169560
170433
  );
169561
170434
  session.pendingDispatches = matched.slice(1).map(({ areaId, area }) => ({
@@ -169579,20 +170452,22 @@ function resolveCleanAreaIds(selectedAreas, cleanAreaRooms) {
169579
170452
  }
169580
170453
  return haAreaIds;
169581
170454
  }
170455
+ var cleaningStates = [
170456
+ VacuumState.cleaning,
170457
+ VacuumState.segment_cleaning,
170458
+ VacuumState.zone_cleaning,
170459
+ VacuumState.spot_cleaning,
170460
+ VacuumState.mop_cleaning,
170461
+ VacuumState.paused
170462
+ ];
170463
+ function vacuumIsCleaning(state) {
170464
+ return state != null && cleaningStates.includes(state);
170465
+ }
169582
170466
  var vacuumRvcRunModeConfig = {
169583
170467
  getCurrentMode: (entity) => {
169584
- const state = entity.state;
169585
- const cleaningStates = [
169586
- VacuumState.cleaning,
169587
- VacuumState.segment_cleaning,
169588
- VacuumState.zone_cleaning,
169589
- VacuumState.spot_cleaning,
169590
- VacuumState.mop_cleaning,
169591
- VacuumState.paused
169592
- ];
169593
- const isCleaning = cleaningStates.includes(state);
169594
- logger235.debug(
169595
- `Vacuum state: "${state}", isCleaning: ${isCleaning}, currentMode: ${isCleaning ? "Cleaning" : "Idle"}`
170468
+ const isCleaning = vacuumIsCleaning(entity.state);
170469
+ logger236.debug(
170470
+ `Vacuum state: "${entity.state}", isCleaning: ${isCleaning}, currentMode: ${isCleaning ? "Cleaning" : "Idle"}`
169596
170471
  );
169597
170472
  return isCleaning ? 1 /* Cleaning */ : 0 /* Idle */;
169598
170473
  },
@@ -169625,7 +170500,7 @@ var vacuumRvcRunModeConfig = {
169625
170500
  if (cleanAreaRooms && cleanAreaRooms.length > 0) {
169626
170501
  const haAreaIds = resolveCleanAreaIds(selectedAreas, cleanAreaRooms);
169627
170502
  if (haAreaIds.length > 0) {
169628
- logger235.info(
170503
+ logger236.info(
169629
170504
  `CLEAN_AREA: cleaning HA areas: ${haAreaIds.join(", ")}`
169630
170505
  );
169631
170506
  return {
@@ -169644,7 +170519,7 @@ var vacuumRvcRunModeConfig = {
169644
170519
  }
169645
170520
  }
169646
170521
  if (matched.length > 0) {
169647
- logger235.info(
170522
+ logger236.info(
169648
170523
  `Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
169649
170524
  );
169650
170525
  session.pendingDispatches = matched.slice(1).map(({ areaId, entityId }) => ({
@@ -169678,14 +170553,14 @@ var vacuumRvcRunModeConfig = {
169678
170553
  }
169679
170554
  }
169680
170555
  if (roomIds.length > 0) {
169681
- logger235.info(
170556
+ logger236.info(
169682
170557
  `Starting cleaning with selected areas: ${roomIds.join(", ")}`
169683
170558
  );
169684
170559
  if (isDreameVacuum(attributes9)) {
169685
170560
  if (targetMapName) {
169686
170561
  const vacName = vacuumEntityId.replace("vacuum.", "");
169687
170562
  const selectedMapEntity = `select.${vacName}_selected_map`;
169688
- logger235.info(
170563
+ logger236.info(
169689
170564
  `Dreame multi-floor: switching to map "${targetMapName}" via ${selectedMapEntity}`
169690
170565
  );
169691
170566
  homeAssistant.callAction({
@@ -169712,7 +170587,7 @@ var vacuumRvcRunModeConfig = {
169712
170587
  }
169713
170588
  if (isEcovacsVacuum(attributes9)) {
169714
170589
  const roomIdStr = roomIds.join(",");
169715
- logger235.info(
170590
+ logger236.info(
169716
170591
  `Ecovacs vacuum: Using spot_area for rooms: ${roomIdStr}`
169717
170592
  );
169718
170593
  return {
@@ -169727,14 +170602,14 @@ var vacuumRvcRunModeConfig = {
169727
170602
  }
169728
170603
  };
169729
170604
  }
169730
- logger235.warn(
170605
+ logger236.warn(
169731
170606
  `Room cleaning via send_command not supported for this vacuum type. Rooms: ${roomIds.join(", ")}. Falling back to vacuum.start`
169732
170607
  );
169733
170608
  }
169734
170609
  }
169735
170610
  } catch {
169736
170611
  }
169737
- logger235.info("Starting regular cleaning (no areas selected)");
170612
+ logger236.info("Starting regular cleaning (no areas selected)");
169738
170613
  return { action: "vacuum.start" };
169739
170614
  },
169740
170615
  returnToBase: () => ({ action: "vacuum.return_to_base" }),
@@ -169749,7 +170624,7 @@ var vacuumRvcRunModeConfig = {
169749
170624
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
169750
170625
  const entity = homeAssistant.entity;
169751
170626
  const attributes9 = entity.state.attributes;
169752
- logger235.info(`cleanRoom called: roomMode=${roomMode}`);
170627
+ logger236.info(`cleanRoom called: roomMode=${roomMode}`);
169753
170628
  const cleanAreaRooms = homeAssistant.state.mapping?.cleanAreaRooms;
169754
170629
  if (cleanAreaRooms && cleanAreaRooms.length > 0) {
169755
170630
  const sorted = [...cleanAreaRooms].sort(
@@ -169758,7 +170633,7 @@ var vacuumRvcRunModeConfig = {
169758
170633
  const areaIndex = roomMode - ROOM_MODE_BASE2 - 1;
169759
170634
  if (areaIndex >= 0 && areaIndex < sorted.length) {
169760
170635
  const area = sorted[areaIndex];
169761
- logger235.info(
170636
+ logger236.info(
169762
170637
  `cleanRoom: CLEAN_AREA "${area.name}" \u2192 vacuum.clean_area(${area.haAreaId})`
169763
170638
  );
169764
170639
  return {
@@ -169775,7 +170650,7 @@ var vacuumRvcRunModeConfig = {
169775
170650
  const areaIndex = roomMode - ROOM_MODE_BASE2 - 1;
169776
170651
  if (areaIndex >= 0 && areaIndex < sorted.length) {
169777
170652
  const area = sorted[areaIndex];
169778
- logger235.info(
170653
+ logger236.info(
169779
170654
  `cleanRoom: custom service area "${area.name}" \u2192 ${area.service}`
169780
170655
  );
169781
170656
  return {
@@ -169796,7 +170671,7 @@ var vacuumRvcRunModeConfig = {
169796
170671
  }
169797
170672
  const rooms = parseVacuumRooms(attributes9);
169798
170673
  const numericIdFromMode = getRoomIdFromMode(roomMode);
169799
- logger235.info(
170674
+ logger236.info(
169800
170675
  `cleanRoom: numericIdFromMode=${numericIdFromMode}, available rooms: ${JSON.stringify(rooms.map((r) => ({ id: r.id, name: r.name, modeValue: getRoomModeValue(r) })))}`
169801
170676
  );
169802
170677
  const room = rooms.find((r) => getRoomModeValue(r) === roomMode);
@@ -169806,7 +170681,7 @@ var vacuumRvcRunModeConfig = {
169806
170681
  if (room.mapName) {
169807
170682
  const vacuumName = vacuumEntityId.replace("vacuum.", "");
169808
170683
  const selectedMapEntity = `select.${vacuumName}_selected_map`;
169809
- logger235.info(
170684
+ logger236.info(
169810
170685
  `Dreame multi-floor: switching to map "${room.mapName}" via ${selectedMapEntity}`
169811
170686
  );
169812
170687
  homeAssistant.callAction({
@@ -169815,7 +170690,7 @@ var vacuumRvcRunModeConfig = {
169815
170690
  data: { option: room.mapName }
169816
170691
  });
169817
170692
  }
169818
- logger235.debug(
170693
+ logger236.debug(
169819
170694
  `Dreame vacuum detected, using dreame_vacuum.vacuum_clean_segment for room ${room.name} (commandId: ${commandId3}, id: ${room.id})`
169820
170695
  );
169821
170696
  return {
@@ -169826,7 +170701,7 @@ var vacuumRvcRunModeConfig = {
169826
170701
  };
169827
170702
  }
169828
170703
  if (isRoborockVacuum(attributes9) || isXiaomiMiotVacuum(attributes9)) {
169829
- logger235.debug(
170704
+ logger236.debug(
169830
170705
  `Using vacuum.send_command with app_segment_clean for room ${room.name} (commandId: ${commandId3}, id: ${room.id})`
169831
170706
  );
169832
170707
  return {
@@ -169839,7 +170714,7 @@ var vacuumRvcRunModeConfig = {
169839
170714
  }
169840
170715
  if (isEcovacsVacuum(attributes9)) {
169841
170716
  const roomIdStr = String(commandId3);
169842
- logger235.info(
170717
+ logger236.info(
169843
170718
  `Ecovacs vacuum: Using spot_area for room ${room.name} (id: ${roomIdStr})`
169844
170719
  );
169845
170720
  return {
@@ -169854,7 +170729,7 @@ var vacuumRvcRunModeConfig = {
169854
170729
  }
169855
170730
  };
169856
170731
  }
169857
- logger235.warn(
170732
+ logger236.warn(
169858
170733
  `Room cleaning via send_command not supported for this vacuum type. Room: ${room.name} (id=${commandId3}). Falling back to vacuum.start`
169859
170734
  );
169860
170735
  }
@@ -169871,20 +170746,20 @@ function createVacuumRvcRunModeServer(attributes9, includeUnnamedRooms = false,
169871
170746
  customAreas,
169872
170747
  disableRoomModes
169873
170748
  );
169874
- logger235.info(
170749
+ logger236.info(
169875
170750
  `Creating VacuumRvcRunModeServer with ${rooms.length} rooms, ${supportedModes2.length} total modes`
169876
170751
  );
169877
170752
  if (rooms.length > 0) {
169878
- logger235.info(`Rooms found: ${rooms.map((r) => r.name).join(", ")}`);
170753
+ logger236.info(`Rooms found: ${rooms.map((r) => r.name).join(", ")}`);
169879
170754
  }
169880
170755
  if (filteredCount > 0) {
169881
170756
  const filtered = allRooms.filter((r) => !rooms.some((x) => x.id === r.id));
169882
- logger235.info(
170757
+ logger236.info(
169883
170758
  `Filtered out ${filteredCount} unnamed room(s): ${filtered.map((r) => r.name).join(", ")}`
169884
170759
  );
169885
170760
  }
169886
170761
  if (allRooms.length === 0) {
169887
- logger235.debug(
170762
+ logger236.debug(
169888
170763
  `No rooms found. Attributes: rooms=${JSON.stringify(attributes9.rooms)}, segments=${JSON.stringify(attributes9.segments)}, room_list=${attributes9.room_list}`
169889
170764
  );
169890
170765
  }
@@ -169918,7 +170793,7 @@ function createCleanAreaRvcRunModeServer(cleanAreaRooms) {
169918
170793
  modeTags: [{ value: RvcRunMode3.ModeTag.Cleaning }]
169919
170794
  });
169920
170795
  }
169921
- logger235.info(
170796
+ logger236.info(
169922
170797
  `Creating CLEAN_AREA RvcRunModeServer with ${cleanAreaRooms.length} HA areas, ${modes.length} total modes`
169923
170798
  );
169924
170799
  return RvcRunModeServer2(vacuumRvcRunModeConfig, {
@@ -169926,11 +170801,12 @@ function createCleanAreaRvcRunModeServer(cleanAreaRooms) {
169926
170801
  currentMode: 0 /* Idle */
169927
170802
  });
169928
170803
  }
169929
- var VacuumRvcRunModeServer = RvcRunModeServer2(vacuumRvcRunModeConfig);
169930
170804
 
169931
170805
  // src/matter/endpoints/legacy/vacuum/behaviors/vacuum-on-off-server.ts
169932
170806
  var VacuumOnOffServer = OnOffServer2({
169933
- isOn: (_, agent) => agent.get(VacuumRvcRunModeServer).state.currentMode === 1 /* Cleaning */,
170807
+ // Derive from the entity directly. Reading the sibling rvcRunMode behavior
170808
+ // crashed with a class mismatch and would lag one tick behind anyway (#428).
170809
+ isOn: (entity) => vacuumIsCleaning(entity.state),
169934
170810
  turnOn: () => ({ action: "vacuum.start" }),
169935
170811
  turnOff: () => ({ action: "vacuum.return_to_base" })
169936
170812
  }).with();
@@ -170007,7 +170883,7 @@ init_nodejs();
170007
170883
 
170008
170884
  // src/matter/behaviors/rvc-clean-mode-server.ts
170009
170885
  init_home_assistant_entity_behavior();
170010
- var logger236 = Logger.get("RvcCleanModeServerBase");
170886
+ var logger237 = Logger.get("RvcCleanModeServerBase");
170011
170887
  var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeServer {
170012
170888
  // Pending mode from a recent changeToMode command.
170013
170889
  // Prevents stale HA state (from a different entity like select.xxx)
@@ -170054,14 +170930,14 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
170054
170930
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
170055
170931
  const { newMode } = request;
170056
170932
  if (newMode !== this.state.currentMode && !this.state.supportedModes.some((m) => m.mode === newMode)) {
170057
- logger236.warn(`changeToMode(${newMode}) rejected: unsupported mode`);
170933
+ logger237.warn(`changeToMode(${newMode}) rejected: unsupported mode`);
170058
170934
  return {
170059
170935
  status: ModeBase3.ModeChangeStatus.UnsupportedMode,
170060
170936
  statusText: `Unsupported mode: ${newMode}`
170061
170937
  };
170062
170938
  }
170063
170939
  const modeLabel = this.state.supportedModes.find((m) => m.mode === newMode);
170064
- logger236.info(
170940
+ logger237.info(
170065
170941
  `changeToMode(${newMode}) "${modeLabel?.label ?? "unknown"}" for ${homeAssistant.entityId}`
170066
170942
  );
170067
170943
  this.pendingMode = newMode;
@@ -170069,7 +170945,7 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
170069
170945
  this.state.currentMode = newMode;
170070
170946
  const action = this.state.config.setCleanMode(newMode, this.agent);
170071
170947
  if (action) {
170072
- logger236.info(
170948
+ logger237.info(
170073
170949
  `changeToMode: dispatching action ${action.action} \u2192 ${action.target ?? homeAssistant.entityId}`
170074
170950
  );
170075
170951
  homeAssistant.callAction(action);
@@ -170102,7 +170978,7 @@ function RvcCleanModeServer2(config11, initialState) {
170102
170978
  }
170103
170979
 
170104
170980
  // src/matter/endpoints/legacy/vacuum/behaviors/vacuum-rvc-clean-mode-server.ts
170105
- var logger237 = Logger.get("VacuumRvcCleanModeServer");
170981
+ var logger238 = Logger.get("VacuumRvcCleanModeServer");
170106
170982
  var MODE_VACUUM = 0;
170107
170983
  var MODE_VACUUM_AND_MOP = 1;
170108
170984
  var MODE_MOP = 2;
@@ -170372,7 +171248,7 @@ function findMatchingCleanOption(ct, availableOptions) {
170372
171248
  const match = availableOptions.find((o) => classifyCleanOption(o) === type);
170373
171249
  if (match) return match;
170374
171250
  }
170375
- logger237.warn(
171251
+ logger238.warn(
170376
171252
  `No match for ${CLEAN_TYPE_LABELS[ct]} in [${availableOptions.join(", ")}]`
170377
171253
  );
170378
171254
  return availableOptions[0];
@@ -170381,7 +171257,7 @@ function buildCleaningModeAction(targetCleanType, agent) {
170381
171257
  const selectEntityId = getCleaningModeSelectEntity(agent);
170382
171258
  const { options } = readSelectEntity(selectEntityId, agent);
170383
171259
  const optionToUse = findMatchingCleanOption(targetCleanType, options);
170384
- logger237.info(
171260
+ logger238.info(
170385
171261
  `Switching cleaning mode to: ${optionToUse} via ${selectEntityId}`
170386
171262
  );
170387
171263
  return {
@@ -170470,7 +171346,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170470
171346
  }
170471
171347
  }
170472
171348
  if (speedMode !== void 0) {
170473
- logger237.debug(
171349
+ logger238.debug(
170474
171350
  `Current mode: Vacuum + fan_speed="${speedState}" -> mode ${speedMode}`
170475
171351
  );
170476
171352
  return speedMode;
@@ -170491,7 +171367,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170491
171367
  }
170492
171368
  }
170493
171369
  if (mopMode !== void 0) {
170494
- logger237.debug(
171370
+ logger238.debug(
170495
171371
  `Current mode: Mop + intensity="${state}" -> mode ${mopMode}`
170496
171372
  );
170497
171373
  return mopMode;
@@ -170509,14 +171385,14 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170509
171385
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
170510
171386
  const vacuumEntityId = homeAssistant.entityId;
170511
171387
  const mapping = homeAssistant.state.mapping;
170512
- logger237.info(
171388
+ logger238.info(
170513
171389
  `setCleanMode(${mode}) for ${vacuumEntityId}, suctionEntity=${mapping?.suctionLevelEntity ?? "none"}, mopEntity=${mapping?.mopIntensityEntity ?? "none"}, fanSpeedList=${JSON.stringify(fanSpeedList ?? [])}, mopIntensityList=${JSON.stringify(mopIntensityList ?? [])}, customTags=${JSON.stringify(customFanSpeedTags ?? {})}`
170514
171390
  );
170515
171391
  if (mopIntensityList && mopIntensityList.length > 0 && isMopIntensityMode(mode)) {
170516
171392
  const mopIndex = mode - MOP_INTENSITY_MODE_BASE;
170517
171393
  const mopName = mopIntensityList[mopIndex];
170518
171394
  if (!mopName) {
170519
- logger237.warn(`Invalid mop intensity mode index: ${mopIndex}`);
171395
+ logger238.warn(`Invalid mop intensity mode index: ${mopIndex}`);
170520
171396
  return void 0;
170521
171397
  }
170522
171398
  if (hasCleanTypes) {
@@ -170529,18 +171405,18 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170529
171405
  mapping.mopIntensityEntity,
170530
171406
  agent
170531
171407
  );
170532
- logger237.info(
171408
+ logger238.info(
170533
171409
  `Mop intensity entity ${mapping.mopIntensityEntity}: current="${state}", options=${JSON.stringify(options ?? [])}`
170534
171410
  );
170535
171411
  let option = matchMopIntensityOption(mopName, options);
170536
171412
  if (!option && options && mopIndex < options.length) {
170537
171413
  option = options[mopIndex];
170538
- logger237.info(
171414
+ logger238.info(
170539
171415
  `Positional match for mop "${mopName}" -> "${option}" (index ${mopIndex})`
170540
171416
  );
170541
171417
  }
170542
171418
  if (option) {
170543
- logger237.info(
171419
+ logger238.info(
170544
171420
  `Setting mop intensity to: ${option} via ${mapping.mopIntensityEntity}`
170545
171421
  );
170546
171422
  return {
@@ -170549,11 +171425,11 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170549
171425
  target: mapping.mopIntensityEntity
170550
171426
  };
170551
171427
  }
170552
- logger237.warn(
171428
+ logger238.warn(
170553
171429
  `No match for mop intensity "${mopName}" in options: [${(options ?? []).join(", ")}]`
170554
171430
  );
170555
171431
  } else {
170556
- logger237.warn(
171432
+ logger238.warn(
170557
171433
  `Mop intensity mode ${mode} requested but no mopIntensityEntity configured`
170558
171434
  );
170559
171435
  }
@@ -170563,7 +171439,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170563
171439
  const fanSpeedIndex = mode - FAN_SPEED_MODE_BASE;
170564
171440
  const fanSpeedName = fanSpeedList[fanSpeedIndex];
170565
171441
  if (!fanSpeedName) {
170566
- logger237.warn(`Invalid fan speed mode index: ${fanSpeedIndex}`);
171442
+ logger238.warn(`Invalid fan speed mode index: ${fanSpeedIndex}`);
170567
171443
  return void 0;
170568
171444
  }
170569
171445
  if (mapping?.suctionLevelEntity) {
@@ -170576,7 +171452,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170576
171452
  mapping.suctionLevelEntity,
170577
171453
  agent
170578
171454
  );
170579
- logger237.info(
171455
+ logger238.info(
170580
171456
  `Suction entity ${mapping.suctionLevelEntity}: current="${state}", options=${JSON.stringify(options ?? [])}`
170581
171457
  );
170582
171458
  let option = matchFanSpeedOption(
@@ -170586,12 +171462,12 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170586
171462
  );
170587
171463
  if (!option && options && fanSpeedIndex < options.length) {
170588
171464
  option = options[fanSpeedIndex];
170589
- logger237.info(
171465
+ logger238.info(
170590
171466
  `Positional match for fan "${fanSpeedName}" -> "${option}" (index ${fanSpeedIndex})`
170591
171467
  );
170592
171468
  }
170593
171469
  if (option) {
170594
- logger237.info(
171470
+ logger238.info(
170595
171471
  `Setting suction to: ${option} via ${mapping.suctionLevelEntity}`
170596
171472
  );
170597
171473
  return {
@@ -170600,7 +171476,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170600
171476
  target: mapping.suctionLevelEntity
170601
171477
  };
170602
171478
  }
170603
- logger237.warn(
171479
+ logger238.warn(
170604
171480
  `No match for fan speed "${fanSpeedName}" in suction options: [${(options ?? []).join(", ")}]`
170605
171481
  );
170606
171482
  return void 0;
@@ -170610,7 +171486,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170610
171486
  buildCleaningModeAction(0 /* Sweeping */, agent)
170611
171487
  );
170612
171488
  }
170613
- logger237.info(
171489
+ logger238.info(
170614
171490
  `Setting fan speed to: ${fanSpeedName} via vacuum.set_fan_speed`
170615
171491
  );
170616
171492
  return {
@@ -170620,7 +171496,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170620
171496
  };
170621
171497
  }
170622
171498
  if (!hasCleanTypes) {
170623
- logger237.debug(
171499
+ logger238.debug(
170624
171500
  `Ignoring cleaning type change (mode=${mode}): no cleaning mode entity`
170625
171501
  );
170626
171502
  return void 0;
@@ -170632,7 +171508,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
170632
171508
  agent
170633
171509
  );
170634
171510
  const optionToUse = findMatchingCleanOption(cleanType, availableOptions);
170635
- logger237.info(
171511
+ logger238.info(
170636
171512
  `Setting cleaning mode to: ${optionToUse} (mode=${mode}) via ${selectEntityId}`
170637
171513
  );
170638
171514
  return {
@@ -170650,10 +171526,10 @@ function createVacuumRvcCleanModeServer(_attributes, fanSpeedList, mopIntensityL
170650
171526
  cleaningModeOptions,
170651
171527
  customFanSpeedTags
170652
171528
  );
170653
- logger237.info(
171529
+ logger238.info(
170654
171530
  `Creating VacuumRvcCleanModeServer with ${supportedModes2.length} modes (fanSpeedList=${JSON.stringify(fanSpeedList ?? [])}, mopIntensityList=${JSON.stringify(mopIntensityList ?? [])}, cleaningModeOptions=${JSON.stringify(cleaningModeOptions ?? [])}, customTags=${JSON.stringify(customFanSpeedTags ?? {})})`
170655
171531
  );
170656
- logger237.info(
171532
+ logger238.info(
170657
171533
  `Modes: ${supportedModes2.map((m) => `${m.mode}:${m.label}[${m.modeTags.map((t) => t.value).join(",")}]`).join(", ")}`
170658
171534
  );
170659
171535
  const initialState = {
@@ -170716,7 +171592,7 @@ function resolveMopIntensityList(mopIntensityEntity) {
170716
171592
  init_dist();
170717
171593
  init_esm();
170718
171594
  init_home_assistant_entity_behavior();
170719
- var logger238 = Logger.get("VacuumRvcOperationalStateServer");
171595
+ var logger239 = Logger.get("VacuumRvcOperationalStateServer");
170720
171596
  function batteryFromAttributes(attrs) {
170721
171597
  const raw = attrs.battery_level ?? attrs.battery;
170722
171598
  if (raw == null) return null;
@@ -170743,7 +171619,7 @@ function isDockedCharging(entity, batteryPercent) {
170743
171619
  }
170744
171620
  function mapVacuumOperationalState(entity, batteryPercent = batteryFromAttributes(entity.attributes), chargingState = null) {
170745
171621
  const state = entity.state;
170746
- const cleaningStates = [
171622
+ const cleaningStates2 = [
170747
171623
  VacuumState.cleaning,
170748
171624
  VacuumState.segment_cleaning,
170749
171625
  VacuumState.zone_cleaning,
@@ -170756,7 +171632,7 @@ function mapVacuumOperationalState(entity, batteryPercent = batteryFromAttribute
170756
171632
  operationalState = charging ? RvcOperationalState4.OperationalState.Charging : RvcOperationalState4.OperationalState.Docked;
170757
171633
  } else if (state === VacuumState.returning) {
170758
171634
  operationalState = RvcOperationalState4.OperationalState.SeekingCharger;
170759
- } else if (cleaningStates.includes(state)) {
171635
+ } else if (cleaningStates2.includes(state)) {
170760
171636
  operationalState = RvcOperationalState4.OperationalState.Running;
170761
171637
  } else if (state === VacuumState.paused) {
170762
171638
  operationalState = RvcOperationalState4.OperationalState.Paused;
@@ -170767,16 +171643,16 @@ function mapVacuumOperationalState(entity, batteryPercent = batteryFromAttribute
170767
171643
  operationalState = RvcOperationalState4.OperationalState.Error;
170768
171644
  } else {
170769
171645
  if (state.toLowerCase().includes("clean")) {
170770
- logger238.info(
171646
+ logger239.info(
170771
171647
  `Unknown vacuum state "${state}" contains 'clean', treating as Running`
170772
171648
  );
170773
171649
  operationalState = RvcOperationalState4.OperationalState.Running;
170774
171650
  } else {
170775
- logger238.info(`Unknown vacuum state "${state}", treating as Stopped`);
171651
+ logger239.info(`Unknown vacuum state "${state}", treating as Stopped`);
170776
171652
  operationalState = RvcOperationalState4.OperationalState.Stopped;
170777
171653
  }
170778
171654
  }
170779
- logger238.debug(
171655
+ logger239.debug(
170780
171656
  `Vacuum operationalState: "${state}" -> ${RvcOperationalState4.OperationalState[operationalState]}`
170781
171657
  );
170782
171658
  return operationalState;
@@ -170805,7 +171681,7 @@ var VacuumRvcOperationalStateServer = RvcOperationalStateServer2({
170805
171681
  });
170806
171682
 
170807
171683
  // src/matter/endpoints/legacy/vacuum/index.ts
170808
- var logger239 = Logger.get("VacuumDevice");
171684
+ var logger240 = Logger.get("VacuumDevice");
170809
171685
  var VacuumEndpointType = RoboticVacuumCleanerDevice.with(
170810
171686
  BasicInformationServer2,
170811
171687
  VacuumIdentifyServer,
@@ -170819,7 +171695,7 @@ function VacuumDevice(homeAssistantEntity, includeOnOff = false, cleaningModeOpt
170819
171695
  const entityId = homeAssistantEntity.entity.entity_id;
170820
171696
  const attributes9 = homeAssistantEntity.entity.state.attributes;
170821
171697
  const customAreas = homeAssistantEntity.mapping?.customServiceAreas;
170822
- logger239.info(
171698
+ logger240.info(
170823
171699
  `Creating vacuum endpoint for ${entityId}, mapping: ${JSON.stringify(homeAssistantEntity.mapping ?? "none")}`
170824
171700
  );
170825
171701
  const cleanAreaRooms = homeAssistantEntity.mapping?.cleanAreaRooms;
@@ -170832,32 +171708,32 @@ function VacuumDevice(homeAssistantEntity, includeOnOff = false, cleaningModeOpt
170832
171708
  )
170833
171709
  ).set({ homeAssistantEntity });
170834
171710
  if (includeOnOff) {
170835
- logger239.info(`${entityId}: Adding OnOff cluster (vacuumOnOff flag enabled)`);
171711
+ logger240.info(`${entityId}: Adding OnOff cluster (vacuumOnOff flag enabled)`);
170836
171712
  device = device.with(VacuumOnOffServer);
170837
171713
  }
170838
171714
  device = device.with(VacuumPowerSourceServer);
170839
171715
  const roomEntities = homeAssistantEntity.mapping?.roomEntities;
170840
171716
  const rooms = parseVacuumRooms(attributes9);
170841
- logger239.info(
171717
+ logger240.info(
170842
171718
  `${entityId}: customAreas=${customAreas?.length ?? 0}, roomEntities=${JSON.stringify(roomEntities ?? [])}, parsedRooms=${rooms.length}, cleanAreaRooms=${cleanAreaRooms?.length ?? 0}`
170843
171719
  );
170844
171720
  if (cleanAreaRooms && cleanAreaRooms.length > 0) {
170845
- logger239.info(
171721
+ logger240.info(
170846
171722
  `${entityId}: Adding ServiceArea (${cleanAreaRooms.length} HA areas via CLEAN_AREA)`
170847
171723
  );
170848
171724
  device = device.with(createCleanAreaServiceAreaServer(cleanAreaRooms));
170849
171725
  } else if (customAreas && customAreas.length > 0) {
170850
- logger239.info(
171726
+ logger240.info(
170851
171727
  `${entityId}: Adding ServiceArea (${customAreas.length} custom areas)`
170852
171728
  );
170853
171729
  device = device.with(createCustomServiceAreaServer(customAreas));
170854
171730
  } else if (rooms.length > 0 || roomEntities && roomEntities.length > 0) {
170855
- logger239.info(`${entityId}: Adding ServiceArea (${rooms.length} rooms)`);
171731
+ logger240.info(`${entityId}: Adding ServiceArea (${rooms.length} rooms)`);
170856
171732
  device = device.with(
170857
171733
  createVacuumServiceAreaServer(attributes9, roomEntities)
170858
171734
  );
170859
171735
  } else {
170860
- logger239.info(`${entityId}: Adding ServiceArea (default single-area)`);
171736
+ logger240.info(`${entityId}: Adding ServiceArea (default single-area)`);
170861
171737
  device = device.with(createDefaultServiceAreaServer());
170862
171738
  }
170863
171739
  const fanSpeedList = resolveFanSpeedList(
@@ -170868,7 +171744,7 @@ function VacuumDevice(homeAssistantEntity, includeOnOff = false, cleaningModeOpt
170868
171744
  homeAssistantEntity.mapping?.mopIntensityEntity
170869
171745
  );
170870
171746
  if (cleaningModeOptions || fanSpeedList || mopIntensityList) {
170871
- logger239.info(
171747
+ logger240.info(
170872
171748
  `${entityId}: Adding RvcCleanMode (multi-mode, cleaningModeOptions=${JSON.stringify(cleaningModeOptions ?? [])}, fanSpeedList=${JSON.stringify(fanSpeedList ?? [])}, mopIntensityList=${JSON.stringify(mopIntensityList ?? [])})`
170873
171749
  );
170874
171750
  device = device.with(
@@ -170881,7 +171757,7 @@ function VacuumDevice(homeAssistantEntity, includeOnOff = false, cleaningModeOpt
170881
171757
  )
170882
171758
  );
170883
171759
  } else {
170884
- logger239.info(`${entityId}: Adding RvcCleanMode (default single-mode)`);
171760
+ logger240.info(`${entityId}: Adding RvcCleanMode (default single-mode)`);
170885
171761
  device = device.with(createDefaultRvcCleanModeServer());
170886
171762
  }
170887
171763
  return device;
@@ -171047,7 +171923,7 @@ var WaterHeaterThermostatServer = ThermostatServer2(
171047
171923
  );
171048
171924
 
171049
171925
  // src/matter/endpoints/legacy/water-heater/index.ts
171050
- var logger240 = Logger.get("WaterHeaterDevice");
171926
+ var logger241 = Logger.get("WaterHeaterDevice");
171051
171927
  var WaterHeaterDeviceType = ThermostatDevice.with(
171052
171928
  BasicInformationServer2,
171053
171929
  IdentifyServer2,
@@ -171063,7 +171939,7 @@ function toMatterTemp2(value) {
171063
171939
  }
171064
171940
  function WaterHeaterDevice(homeAssistantEntity) {
171065
171941
  const attributes9 = homeAssistantEntity.entity.state.attributes;
171066
- logger240.debug(
171942
+ logger241.debug(
171067
171943
  `Creating device for ${homeAssistantEntity.entity.entity_id}, min_temp=${attributes9.min_temp}, max_temp=${attributes9.max_temp}`
171068
171944
  );
171069
171945
  const minLimit = toMatterTemp2(attributes9.min_temp) ?? 0;
@@ -171303,6 +172179,7 @@ var matterDeviceTypeFactories = {
171303
172179
  // Legacy SolarPower alias, kept for existing mappings.
171304
172180
  electrical_sensor: (ha) => ElectricalSensorType.set({ homeAssistantEntity: ha }),
171305
172181
  solar_power: (ha) => ElectricalSensorType.set({ homeAssistantEntity: ha }),
172182
+ evse: (ha) => energyEvseType().set({ homeAssistantEntity: ha }),
171306
172183
  contact_sensor: (ha) => ContactSensorType.set({ homeAssistantEntity: ha }),
171307
172184
  motion_sensor: (ha) => MotionSensorType.set({ homeAssistantEntity: ha }),
171308
172185
  occupancy_sensor: (ha) => OccupancySensorType.set({ homeAssistantEntity: ha }),
@@ -171318,7 +172195,7 @@ var matterDeviceTypeFactories = {
171318
172195
  };
171319
172196
 
171320
172197
  // src/matter/endpoints/composed/user-composed-endpoint.ts
171321
- var logger241 = Logger.get("UserComposedEndpoint");
172198
+ var logger242 = Logger.get("UserComposedEndpoint");
171322
172199
  function stripBasicInformation(type) {
171323
172200
  const behaviors = { ...type.behaviors };
171324
172201
  delete behaviors.bridgedDeviceBasicInformation;
@@ -171328,11 +172205,11 @@ function createEndpointId5(entityId, customName) {
171328
172205
  const baseName = customName || entityId;
171329
172206
  return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
171330
172207
  }
171331
- function buildEntityPayload4(registry2, entityId) {
171332
- const state = registry2.initialStateIncludingUnfiltered(entityId);
172208
+ function buildEntityPayload4(registry3, entityId) {
172209
+ const state = registry3.initialStateIncludingUnfiltered(entityId);
171333
172210
  if (!state) return void 0;
171334
- const entity = registry2.entityIncludingUnfiltered(entityId);
171335
- const deviceRegistry = registry2.deviceOfIncludingUnfiltered(entityId);
172211
+ const entity = registry3.entityIncludingUnfiltered(entityId);
172212
+ const deviceRegistry = registry3.deviceOfIncludingUnfiltered(entityId);
171336
172213
  return {
171337
172214
  entity_id: entityId,
171338
172215
  state,
@@ -171348,8 +172225,8 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171348
172225
  lastMappedStates = /* @__PURE__ */ new Map();
171349
172226
  debouncedUpdates = /* @__PURE__ */ new Map();
171350
172227
  static async create(config11) {
171351
- const { registry: registry2, primaryEntityId, composedEntities } = config11;
171352
- const primaryPayload = buildEntityPayload4(registry2, primaryEntityId);
172228
+ const { registry: registry3, primaryEntityId, composedEntities } = config11;
172229
+ const primaryPayload = buildEntityPayload4(registry3, primaryEntityId);
171353
172230
  if (!primaryPayload) return void 0;
171354
172231
  let parentType = BridgedNodeEndpoint.with(
171355
172232
  BasicInformationServer2,
@@ -171391,10 +172268,10 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171391
172268
  primarySubPayload,
171392
172269
  primaryMapping,
171393
172270
  config11.areaName,
171394
- { vacuumOnOff: registry2.isVacuumOnOffEnabled() }
172271
+ { vacuumOnOff: registry3.isVacuumOnOffEnabled() }
171395
172272
  );
171396
172273
  if (!primaryType) {
171397
- logger241.warn(
172274
+ logger242.warn(
171398
172275
  `Cannot create endpoint type for primary entity ${primaryEntityId}`
171399
172276
  );
171400
172277
  return void 0;
@@ -171407,9 +172284,9 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171407
172284
  for (let i = 0; i < composedEntities.length; i++) {
171408
172285
  const sub = composedEntities[i];
171409
172286
  if (!sub.entityId) continue;
171410
- const subPayload = buildEntityPayload4(registry2, sub.entityId);
172287
+ const subPayload = buildEntityPayload4(registry3, sub.entityId);
171411
172288
  if (!subPayload) {
171412
- logger241.warn(
172289
+ logger242.warn(
171413
172290
  `Cannot find state for composed sub-entity ${sub.entityId}, it does not exist in Home Assistant (removed or renamed?)`
171414
172291
  );
171415
172292
  continue;
@@ -171424,7 +172301,7 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171424
172301
  config11.areaName
171425
172302
  );
171426
172303
  if (!subType) {
171427
- logger241.warn(
172304
+ logger242.warn(
171428
172305
  `Cannot create endpoint type for composed sub-entity ${sub.entityId}`
171429
172306
  );
171430
172307
  continue;
@@ -171437,7 +172314,7 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171437
172314
  mappedIds.push(sub.entityId);
171438
172315
  }
171439
172316
  if (parts.length < 2) {
171440
- logger241.warn(
172317
+ logger242.warn(
171441
172318
  `User composed device ${primaryEntityId}: only ${parts.length} sub-endpoint(s), need at least 2 (primary + one sub-entity). Falling back to standalone.`
171442
172319
  );
171443
172320
  return void 0;
@@ -171461,7 +172338,7 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
171461
172338
  const labels = parts.map(
171462
172339
  (_, i) => i === 0 ? primaryEntityId.split(".")[0] : composedEntities[i - 1]?.entityId?.split(".")[0] ?? "?"
171463
172340
  ).join("+");
171464
- logger241.info(
172341
+ logger242.info(
171465
172342
  `Created user composed device ${primaryEntityId}: ${parts.length} sub-endpoint(s) [${labels}]`
171466
172343
  );
171467
172344
  return endpoint;
@@ -171567,45 +172444,45 @@ function asStandaloneEndpointType(type) {
171567
172444
  }
171568
172445
 
171569
172446
  // src/matter/endpoints/legacy/legacy-endpoint.ts
171570
- var logger242 = Logger.get("LegacyEndpoint");
172447
+ var logger243 = Logger.get("LegacyEndpoint");
171571
172448
  var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171572
- static async create(registry2, entityId, mapping, pluginDomainMappings, standalone = false, endpointId, identityAnchor) {
171573
- const deviceRegistry = registry2.deviceOf(entityId);
171574
- let state = registry2.initialState(entityId);
171575
- const entity = registry2.entity(entityId);
172449
+ static async create(registry3, entityId, mapping, pluginDomainMappings, standalone = false, endpointId, identityAnchor) {
172450
+ const deviceRegistry = registry3.deviceOf(entityId);
172451
+ let state = registry3.initialState(entityId);
172452
+ const entity = registry3.entity(entityId);
171576
172453
  if (!state) {
171577
172454
  return;
171578
172455
  }
171579
- if (registry2.isAutoBatteryMappingEnabled() && registry2.isBatteryEntityUsed(entityId)) {
171580
- logger242.debug(
172456
+ if (registry3.isAutoBatteryMappingEnabled() && registry3.isBatteryEntityUsed(entityId)) {
172457
+ logger243.debug(
171581
172458
  `Skipping ${entityId} - already auto-assigned as battery to another device`
171582
172459
  );
171583
172460
  return;
171584
172461
  }
171585
- if (registry2.isAutoHumidityMappingEnabled() && registry2.isHumidityEntityUsed(entityId)) {
171586
- logger242.debug(
172462
+ if (registry3.isAutoHumidityMappingEnabled() && registry3.isHumidityEntityUsed(entityId)) {
172463
+ logger243.debug(
171587
172464
  `Skipping ${entityId} - already auto-assigned as humidity to a temperature sensor`
171588
172465
  );
171589
172466
  return;
171590
172467
  }
171591
- if (registry2.isAutoPressureMappingEnabled() && registry2.isPressureEntityUsed(entityId)) {
171592
- logger242.debug(
172468
+ if (registry3.isAutoPressureMappingEnabled() && registry3.isPressureEntityUsed(entityId)) {
172469
+ logger243.debug(
171593
172470
  `Skipping ${entityId} - already auto-assigned as pressure to a temperature sensor`
171594
172471
  );
171595
172472
  return;
171596
172473
  }
171597
- if (registry2.isAutoComposedDevicesEnabled() && registry2.isComposedSubEntityUsed(entityId)) {
171598
- logger242.debug(
172474
+ if (registry3.isAutoComposedDevicesEnabled() && registry3.isComposedSubEntityUsed(entityId)) {
172475
+ logger243.debug(
171599
172476
  `Skipping ${entityId} - already consumed by a composed device`
171600
172477
  );
171601
172478
  return;
171602
172479
  }
171603
172480
  let effectiveMapping = mapping;
171604
172481
  if (entity.device_id) {
171605
- if (registry2.isAutoHumidityMappingEnabled()) {
172482
+ if (registry3.isAutoHumidityMappingEnabled()) {
171606
172483
  const attrs = state.attributes;
171607
172484
  if (!mapping?.humidityEntity && entityId.startsWith("sensor.") && attrs.device_class === SensorDeviceClass.temperature) {
171608
- const humidityEntityId = registry2.findHumidityEntityForDevice(
172485
+ const humidityEntityId = registry3.findHumidityEntityForDevice(
171609
172486
  entity.device_id
171610
172487
  );
171611
172488
  if (humidityEntityId && humidityEntityId !== entityId) {
@@ -171614,17 +172491,17 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171614
172491
  entityId: effectiveMapping?.entityId ?? entityId,
171615
172492
  humidityEntity: humidityEntityId
171616
172493
  };
171617
- registry2.markHumidityEntityUsed(humidityEntityId);
171618
- logger242.debug(
172494
+ registry3.markHumidityEntityUsed(humidityEntityId);
172495
+ logger243.debug(
171619
172496
  `Auto-assigned humidity ${humidityEntityId} to ${entityId}`
171620
172497
  );
171621
172498
  }
171622
172499
  }
171623
172500
  }
171624
- if (registry2.isAutoPressureMappingEnabled()) {
172501
+ if (registry3.isAutoPressureMappingEnabled()) {
171625
172502
  const attrs = state.attributes;
171626
172503
  if (!mapping?.pressureEntity && entityId.startsWith("sensor.") && attrs.device_class === SensorDeviceClass.temperature) {
171627
- const pressureEntityId = registry2.findPressureEntityForDevice(
172504
+ const pressureEntityId = registry3.findPressureEntityForDevice(
171628
172505
  entity.device_id
171629
172506
  );
171630
172507
  if (pressureEntityId && pressureEntityId !== entityId) {
@@ -171633,16 +172510,16 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171633
172510
  entityId: effectiveMapping?.entityId ?? entityId,
171634
172511
  pressureEntity: pressureEntityId
171635
172512
  };
171636
- registry2.markPressureEntityUsed(pressureEntityId);
171637
- logger242.debug(
172513
+ registry3.markPressureEntityUsed(pressureEntityId);
172514
+ logger243.debug(
171638
172515
  `Auto-assigned pressure ${pressureEntityId} to ${entityId}`
171639
172516
  );
171640
172517
  }
171641
172518
  }
171642
172519
  }
171643
172520
  const isVacuum = entityId.startsWith("vacuum.");
171644
- if ((registry2.isAutoBatteryMappingEnabled() || isVacuum) && !mapping?.batteryEntity && !mapping?.disableBatteryMapping) {
171645
- const batteryEntityId = registry2.findBatteryEntityForDevice(
172521
+ if ((registry3.isAutoBatteryMappingEnabled() || isVacuum) && !mapping?.batteryEntity && !mapping?.disableBatteryMapping) {
172522
+ const batteryEntityId = registry3.findBatteryEntityForDevice(
171646
172523
  entity.device_id
171647
172524
  );
171648
172525
  if (batteryEntityId && batteryEntityId !== entityId) {
@@ -171651,16 +172528,16 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171651
172528
  entityId: effectiveMapping?.entityId ?? entityId,
171652
172529
  batteryEntity: batteryEntityId
171653
172530
  };
171654
- registry2.markBatteryEntityUsed(batteryEntityId);
171655
- logger242.debug(
172531
+ registry3.markBatteryEntityUsed(batteryEntityId);
172532
+ logger243.debug(
171656
172533
  `Auto-assigned battery ${batteryEntityId} to ${entityId}`
171657
172534
  );
171658
172535
  }
171659
172536
  }
171660
172537
  const alarmDeviceClass = state.attributes.device_class;
171661
172538
  const isSmokeCoAlarm = mapping?.matterDeviceType === "smoke_co_alarm" || entityId.startsWith("binary_sensor.") && (alarmDeviceClass === "smoke" || alarmDeviceClass === "carbon_monoxide" || alarmDeviceClass === "gas");
171662
- if (registry2.isAutoBatteryMappingEnabled() && !mapping?.faultEntity && isSmokeCoAlarm) {
171663
- const faultEntityId = registry2.findProblemEntityForDevice(
172539
+ if (registry3.isAutoBatteryMappingEnabled() && !mapping?.faultEntity && isSmokeCoAlarm) {
172540
+ const faultEntityId = registry3.findProblemEntityForDevice(
171664
172541
  entity.device_id
171665
172542
  );
171666
172543
  if (faultEntityId && faultEntityId !== entityId) {
@@ -171669,13 +172546,13 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171669
172546
  entityId: effectiveMapping?.entityId ?? entityId,
171670
172547
  faultEntity: faultEntityId
171671
172548
  };
171672
- logger242.debug(`Auto-assigned fault ${faultEntityId} to ${entityId}`);
172549
+ logger243.debug(`Auto-assigned fault ${faultEntityId} to ${entityId}`);
171673
172550
  }
171674
172551
  }
171675
172552
  if (!mapping?.powerEntity) {
171676
172553
  const domain = entityId.split(".")[0];
171677
172554
  if (domain === "switch") {
171678
- const powerEntityId = registry2.findPowerEntityForDevice(
172555
+ const powerEntityId = registry3.findPowerEntityForDevice(
171679
172556
  entity.device_id
171680
172557
  );
171681
172558
  if (powerEntityId && powerEntityId !== entityId) {
@@ -171684,15 +172561,15 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171684
172561
  entityId: effectiveMapping?.entityId ?? entityId,
171685
172562
  powerEntity: powerEntityId
171686
172563
  };
171687
- registry2.markPowerEntityUsed(powerEntityId);
171688
- logger242.debug(`Auto-assigned power ${powerEntityId} to ${entityId}`);
172564
+ registry3.markPowerEntityUsed(powerEntityId);
172565
+ logger243.debug(`Auto-assigned power ${powerEntityId} to ${entityId}`);
171689
172566
  }
171690
172567
  }
171691
172568
  }
171692
172569
  if (!mapping?.energyEntity) {
171693
172570
  const domain = entityId.split(".")[0];
171694
172571
  if (domain === "switch") {
171695
- const energyEntityId = registry2.findEnergyEntityForDevice(
172572
+ const energyEntityId = registry3.findEnergyEntityForDevice(
171696
172573
  entity.device_id
171697
172574
  );
171698
172575
  if (energyEntityId && energyEntityId !== entityId) {
@@ -171701,15 +172578,15 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171701
172578
  entityId: effectiveMapping?.entityId ?? entityId,
171702
172579
  energyEntity: energyEntityId
171703
172580
  };
171704
- registry2.markEnergyEntityUsed(energyEntityId);
171705
- logger242.debug(
172581
+ registry3.markEnergyEntityUsed(energyEntityId);
172582
+ logger243.debug(
171706
172583
  `Auto-assigned energy ${energyEntityId} to ${entityId}`
171707
172584
  );
171708
172585
  }
171709
172586
  }
171710
172587
  }
171711
172588
  if (entityId.startsWith("vacuum.")) {
171712
- const vacuumEntities = registry2.findVacuumSelectEntities(
172589
+ const vacuumEntities = registry3.findVacuumSelectEntities(
171713
172590
  entity.device_id
171714
172591
  );
171715
172592
  if (!effectiveMapping?.cleaningModeEntity && vacuumEntities.cleaningModeEntity) {
@@ -171718,7 +172595,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171718
172595
  entityId: effectiveMapping?.entityId ?? entityId,
171719
172596
  cleaningModeEntity: vacuumEntities.cleaningModeEntity
171720
172597
  };
171721
- logger242.info(
172598
+ logger243.info(
171722
172599
  `Auto-assigned cleaningMode ${vacuumEntities.cleaningModeEntity} to ${entityId}`
171723
172600
  );
171724
172601
  }
@@ -171728,7 +172605,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171728
172605
  entityId: effectiveMapping?.entityId ?? entityId,
171729
172606
  suctionLevelEntity: vacuumEntities.suctionLevelEntity
171730
172607
  };
171731
- logger242.info(
172608
+ logger243.info(
171732
172609
  `Auto-assigned suctionLevel ${vacuumEntities.suctionLevelEntity} to ${entityId}`
171733
172610
  );
171734
172611
  }
@@ -171738,7 +172615,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171738
172615
  entityId: effectiveMapping?.entityId ?? entityId,
171739
172616
  mopIntensityEntity: vacuumEntities.mopIntensityEntity
171740
172617
  };
171741
- logger242.info(
172618
+ logger243.info(
171742
172619
  `Auto-assigned mopIntensity ${vacuumEntities.mopIntensityEntity} to ${entityId}`
171743
172620
  );
171744
172621
  }
@@ -171748,12 +172625,12 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171748
172625
  entityId: effectiveMapping?.entityId ?? entityId,
171749
172626
  currentRoomEntity: vacuumEntities.currentRoomEntity
171750
172627
  };
171751
- logger242.info(
172628
+ logger243.info(
171752
172629
  `Auto-assigned currentRoom ${vacuumEntities.currentRoomEntity} to ${entityId}`
171753
172630
  );
171754
172631
  }
171755
172632
  const supportedFeatures = state.attributes.supported_features ?? 0;
171756
- const cleanAreaRooms = await registry2.resolveCleanAreaRooms(
172633
+ const cleanAreaRooms = await registry3.resolveCleanAreaRooms(
171757
172634
  entityId,
171758
172635
  supportedFeatures
171759
172636
  );
@@ -171763,13 +172640,13 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171763
172640
  entityId: effectiveMapping?.entityId ?? entityId,
171764
172641
  cleanAreaRooms
171765
172642
  };
171766
- logger242.info(
172643
+ logger243.info(
171767
172644
  `Using ${cleanAreaRooms.length} HA areas via CLEAN_AREA for ${entityId}`
171768
172645
  );
171769
172646
  }
171770
172647
  const vacAttrs = state.attributes;
171771
172648
  if (cleanAreaRooms.length === 0 && !vacAttrs.rooms && !vacAttrs.segments && !vacAttrs.room_mapping) {
171772
- const valetudoRooms = registry2.findValetudoMapSegments(
172649
+ const valetudoRooms = registry3.findValetudoMapSegments(
171773
172650
  entity.device_id
171774
172651
  );
171775
172652
  if (valetudoRooms.length > 0) {
@@ -171784,11 +172661,11 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171784
172661
  rooms: roomsObj
171785
172662
  }
171786
172663
  };
171787
- logger242.debug(
172664
+ logger243.debug(
171788
172665
  `Auto-detected ${valetudoRooms.length} Valetudo segments for ${entityId}`
171789
172666
  );
171790
172667
  } else {
171791
- const roborockRooms = await registry2.resolveRoborockRooms(entityId);
172668
+ const roborockRooms = await registry3.resolveRoborockRooms(entityId);
171792
172669
  if (roborockRooms.length > 0) {
171793
172670
  const roomsObj = {};
171794
172671
  for (const r of roborockRooms) {
@@ -171801,7 +172678,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171801
172678
  rooms: roomsObj
171802
172679
  }
171803
172680
  };
171804
- logger242.debug(
172681
+ logger243.debug(
171805
172682
  `Auto-detected ${roborockRooms.length} Roborock rooms for ${entityId}`
171806
172683
  );
171807
172684
  }
@@ -171810,14 +172687,14 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171810
172687
  }
171811
172688
  }
171812
172689
  if (standalone && ((effectiveMapping?.composedEntities?.length ?? 0) > 0 || effectiveMapping?.climateExposeFan === true)) {
171813
- logger242.warn(
172690
+ logger243.warn(
171814
172691
  `Composed mappings are not supported in server mode, exposing ${entityId} as a flat standalone endpoint`
171815
172692
  );
171816
172693
  }
171817
- if (!standalone && registry2.isAutoComposedDevicesEnabled() && effectiveMapping?.composedEntities && effectiveMapping.composedEntities.length > 0) {
171818
- const composedAreaName = registry2.getAreaName(entityId);
172694
+ if (!standalone && registry3.isAutoComposedDevicesEnabled() && effectiveMapping?.composedEntities && effectiveMapping.composedEntities.length > 0) {
172695
+ const composedAreaName = registry3.getAreaName(entityId);
171819
172696
  const composed = await UserComposedEndpoint.create({
171820
- registry: registry2,
172697
+ registry: registry3,
171821
172698
  primaryEntityId: entityId,
171822
172699
  mapping: effectiveMapping,
171823
172700
  composedEntities: effectiveMapping.composedEntities,
@@ -171829,16 +172706,16 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171829
172706
  if (composed) {
171830
172707
  return composed;
171831
172708
  }
171832
- logger242.warn(
172709
+ logger243.warn(
171833
172710
  `User composed device creation failed for ${entityId}, falling back to standalone`
171834
172711
  );
171835
172712
  }
171836
- if (!standalone && registry2.isAutoComposedDevicesEnabled()) {
172713
+ if (!standalone && registry3.isAutoComposedDevicesEnabled()) {
171837
172714
  const attrs = state.attributes;
171838
172715
  if (entityId.startsWith("sensor.") && attrs.device_class === SensorDeviceClass.temperature && (effectiveMapping?.humidityEntity || effectiveMapping?.pressureEntity)) {
171839
- const composedAreaName = registry2.getAreaName(entityId);
172716
+ const composedAreaName = registry3.getAreaName(entityId);
171840
172717
  const composed = await ComposedSensorEndpoint.create({
171841
- registry: registry2,
172718
+ registry: registry3,
171842
172719
  primaryEntityId: entityId,
171843
172720
  humidityEntityId: effectiveMapping?.humidityEntity,
171844
172721
  pressureEntityId: effectiveMapping?.pressureEntity,
@@ -171854,12 +172731,12 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171854
172731
  }
171855
172732
  const resolvedMatterType = mapping?.matterDeviceType ?? (entityId.startsWith("fan.") ? "fan" : void 0);
171856
172733
  if (resolvedMatterType === "air_purifier") {
171857
- const temperatureEntityId = effectiveMapping?.temperatureEntity || (entity.device_id ? registry2.findTemperatureEntityForDevice(entity.device_id) : void 0);
171858
- const humidityEntityId = effectiveMapping?.humidityEntity || (entity.device_id ? registry2.findHumidityEntityForDevice(entity.device_id) : void 0);
172734
+ const temperatureEntityId = effectiveMapping?.temperatureEntity || (entity.device_id ? registry3.findTemperatureEntityForDevice(entity.device_id) : void 0);
172735
+ const humidityEntityId = effectiveMapping?.humidityEntity || (entity.device_id ? registry3.findHumidityEntityForDevice(entity.device_id) : void 0);
171859
172736
  if (temperatureEntityId || humidityEntityId) {
171860
- const composedAreaName = registry2.getAreaName(entityId);
172737
+ const composedAreaName = registry3.getAreaName(entityId);
171861
172738
  const composed = await ComposedAirPurifierEndpoint.create({
171862
- registry: registry2,
172739
+ registry: registry3,
171863
172740
  primaryEntityId: entityId,
171864
172741
  temperatureEntityId,
171865
172742
  humidityEntityId,
@@ -171881,9 +172758,9 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171881
172758
  if (!standalone && entityId.startsWith("climate.") && effectiveMapping?.climateExposeFan === true) {
171882
172759
  const climateFeatures = state.attributes.supported_features ?? 0;
171883
172760
  if ((climateFeatures & ClimateDeviceFeature.FAN_MODE) !== 0) {
171884
- const composedAreaName = registry2.getAreaName(entityId);
172761
+ const composedAreaName = registry3.getAreaName(entityId);
171885
172762
  const composed = await ComposedClimateFanEndpoint.create({
171886
- registry: registry2,
172763
+ registry: registry3,
171887
172764
  primaryEntityId: entityId,
171888
172765
  mapping: effectiveMapping,
171889
172766
  customName: effectiveMapping?.customName,
@@ -171894,7 +172771,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171894
172771
  if (composed) {
171895
172772
  return composed;
171896
172773
  }
171897
- logger242.warn(
172774
+ logger243.warn(
171898
172775
  `Companion fan creation failed for ${entityId}, falling back to standalone`
171899
172776
  );
171900
172777
  }
@@ -171908,7 +172785,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171908
172785
  let cleaningModeOptions;
171909
172786
  if (entityId.startsWith("vacuum.")) {
171910
172787
  if (effectiveMapping?.cleaningModeEntity) {
171911
- const cmState = registry2.initialState(
172788
+ const cmState = registry3.initialState(
171912
172789
  effectiveMapping.cleaningModeEntity
171913
172790
  );
171914
172791
  cleaningModeOptions = cmState?.attributes?.options;
@@ -171922,13 +172799,13 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171922
172799
  ];
171923
172800
  }
171924
172801
  }
171925
- const areaName = registry2.getAreaName(entityId);
172802
+ const areaName = registry3.getAreaName(entityId);
171926
172803
  let type = createLegacyEndpointType(
171927
172804
  payload,
171928
172805
  effectiveMapping,
171929
172806
  areaName,
171930
172807
  {
171931
- vacuumOnOff: registry2.isVacuumOnOffEnabled(),
172808
+ vacuumOnOff: registry3.isVacuumOnOffEnabled(),
171932
172809
  cleaningModeOptions,
171933
172810
  pluginDomainMappings
171934
172811
  },
@@ -171974,11 +172851,11 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
171974
172851
  }
171975
172852
  if (mappedChanged) {
171976
172853
  this.pendingMappedChange = true;
171977
- logger242.debug(
172854
+ logger243.debug(
171978
172855
  `Mapped entity change detected for ${this.entityId}, forcing update`
171979
172856
  );
171980
172857
  }
171981
- logger242.debug(
172858
+ logger243.debug(
171982
172859
  `State update received for ${this.entityId}: state=${state.state}`
171983
172860
  );
171984
172861
  this.lastState = state;
@@ -172107,18 +172984,186 @@ var CameraAvStreamServer = class extends Base4 {
172107
172984
  })(CameraAvStreamServer || (CameraAvStreamServer = {}));
172108
172985
 
172109
172986
  // src/plugins/builtin/camera/webrtc-provider-server.ts
172987
+ init_esm();
172110
172988
  init_esm7();
172111
172989
  init_esm3();
172990
+
172991
+ // src/plugins/builtin/camera/requestor-client.ts
172992
+ init_esm();
172993
+ init_esm4();
172994
+ var logger244 = Logger.get("CameraWebRtcRequestor");
172995
+ var registry2 = /* @__PURE__ */ new Map();
172996
+ var pendingDeliveries = /* @__PURE__ */ new Map();
172997
+ var invokeTransport = defaultInvoke;
172998
+ function registerRequestor(sessionId, registration) {
172999
+ registry2.set(sessionId, registration);
173000
+ }
173001
+ function unregisterRequestor(sessionId) {
173002
+ registry2.delete(sessionId);
173003
+ const timer = pendingDeliveries.get(sessionId);
173004
+ if (timer) {
173005
+ clearTimeout(timer);
173006
+ pendingDeliveries.delete(sessionId);
173007
+ }
173008
+ }
173009
+ function unregisterAllRequestors() {
173010
+ for (const id of [...registry2.keys()]) unregisterRequestor(id);
173011
+ for (const timer of pendingDeliveries.values()) clearTimeout(timer);
173012
+ pendingDeliveries.clear();
173013
+ }
173014
+ function deliverAnswerDeferred(sessionId, sdp, onGiveUp) {
173015
+ const prior = pendingDeliveries.get(sessionId);
173016
+ if (prior) clearTimeout(prior);
173017
+ const owner = registry2.get(sessionId);
173018
+ const timer = setTimeout(() => {
173019
+ void (async () => {
173020
+ try {
173021
+ for (const delayMs of [0, 250, 500]) {
173022
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
173023
+ if (registry2.get(sessionId) !== owner || pendingDeliveries.get(sessionId) !== timer) {
173024
+ return;
173025
+ }
173026
+ if (await sendAnswer(sessionId, sdp)) {
173027
+ logger244.info(`answer delivered for session ${sessionId}`);
173028
+ return;
173029
+ }
173030
+ }
173031
+ if (registry2.get(sessionId) !== owner || pendingDeliveries.get(sessionId) !== timer) {
173032
+ return;
173033
+ }
173034
+ logger244.info(
173035
+ `answer delivery failed for session ${sessionId}, giving up`
173036
+ );
173037
+ await onGiveUp();
173038
+ } catch (err) {
173039
+ logger244.info(
173040
+ `answer delivery for session ${sessionId} threw: ${errText(err)}`
173041
+ );
173042
+ } finally {
173043
+ if (pendingDeliveries.get(sessionId) === timer) {
173044
+ pendingDeliveries.delete(sessionId);
173045
+ }
173046
+ }
173047
+ })();
173048
+ }, 0);
173049
+ timer.unref?.();
173050
+ pendingDeliveries.set(sessionId, timer);
173051
+ }
173052
+ function hasRequestor(sessionId) {
173053
+ return registry2.has(sessionId);
173054
+ }
173055
+ function sendAnswer(sessionId, sdp) {
173056
+ return invokeRequestor(sessionId, "answer", {
173057
+ webRtcSessionId: sessionId,
173058
+ sdp
173059
+ });
173060
+ }
173061
+ async function invokeRequestor(sessionId, command, fields) {
173062
+ const registration = registry2.get(sessionId);
173063
+ if (!registration) {
173064
+ logger244.info(`requestor ${command}: no session ${sessionId} registered`);
173065
+ return false;
173066
+ }
173067
+ if (registration.session.isClosed) {
173068
+ logger244.info(`requestor ${command}: session ${sessionId} already closed`);
173069
+ return false;
173070
+ }
173071
+ const request = {
173072
+ endpoint: registration.requestorEndpoint,
173073
+ cluster: WebRtcTransportRequestor3.Cluster,
173074
+ command,
173075
+ fields
173076
+ };
173077
+ try {
173078
+ return await invokeTransport({ registration, request });
173079
+ } catch (err) {
173080
+ logger244.info(
173081
+ `requestor ${command} failed for session ${sessionId}: ${errText(err)}`
173082
+ );
173083
+ return false;
173084
+ }
173085
+ }
173086
+ async function defaultInvoke({
173087
+ registration,
173088
+ request
173089
+ }) {
173090
+ const { session, env } = registration;
173091
+ const exchangeManager = env.get(ExchangeManager);
173092
+ const exchangeProvider = new DedicatedChannelExchangeProvider(
173093
+ exchangeManager,
173094
+ session
173095
+ );
173096
+ const client = new ClientInteraction({ environment: env, exchangeProvider });
173097
+ try {
173098
+ const result = client.invoke(
173099
+ Invoke({
173100
+ commands: [
173101
+ {
173102
+ endpoint: request.endpoint,
173103
+ cluster: request.cluster,
173104
+ command: request.command,
173105
+ // biome-ignore lint/suspicious/noExplicitAny: fields typed per command by caller
173106
+ fields: request.fields
173107
+ }
173108
+ ]
173109
+ })
173110
+ );
173111
+ for await (const chunk of result) {
173112
+ for (const entry of chunk) {
173113
+ if (entry.kind === "cmd-status") {
173114
+ if (entry.status !== Status2.Success) {
173115
+ logger244.info(
173116
+ `requestor ${request.command} status ${entry.status} for session ${String(request.fields.webRtcSessionId)}`
173117
+ );
173118
+ return false;
173119
+ }
173120
+ return true;
173121
+ }
173122
+ }
173123
+ }
173124
+ return true;
173125
+ } finally {
173126
+ await client.close().catch(() => {
173127
+ });
173128
+ }
173129
+ }
173130
+ function errText(err) {
173131
+ return err instanceof Error ? err.message : String(err);
173132
+ }
173133
+
173134
+ // src/plugins/builtin/camera/webrtc-provider-server.ts
173135
+ var logger245 = Logger.get("CameraWebRtc");
173136
+ var nextGlobalSessionId = 0;
173137
+ function mintSessionId() {
173138
+ for (let i = 0; i <= 65534; i++) {
173139
+ const id = nextGlobalSessionId;
173140
+ nextGlobalSessionId = nextGlobalSessionId >= 65534 ? 0 : nextGlobalSessionId + 1;
173141
+ if (!hasRequestor(id)) return id;
173142
+ }
173143
+ return nextGlobalSessionId;
173144
+ }
172112
173145
  var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
172113
173146
  solicitOffer(request) {
172114
- const id = this.state.nextSessionId++;
173147
+ const id = mintSessionId();
172115
173148
  this.trackSession(id, request.streamUsage, request.originatingEndpointId);
172116
- void this.state.bridge.startSession(id, this.state.entityId).catch(() => {
172117
- });
173149
+ logger245.info(
173150
+ `solicitOffer session=${id} (${this.state.entityId}), deferred offer`
173151
+ );
173152
+ void this.state.bridge.startSession(id, this.state.entityId, {
173153
+ iceServers: request.iceServers,
173154
+ iceTransportPolicy: request.iceTransportPolicy
173155
+ }).catch(
173156
+ (err) => logger245.info(
173157
+ `solicitOffer startSession failed for ${this.state.entityId}: ${errText2(err)}`
173158
+ )
173159
+ );
172118
173160
  return { webRtcSessionId: id, deferredOffer: true };
172119
173161
  }
172120
173162
  async provideOffer(request) {
172121
- const id = request.webRtcSessionId ?? this.state.nextSessionId++;
173163
+ const id = request.webRtcSessionId ?? mintSessionId();
173164
+ logger245.info(
173165
+ `provideOffer entry: entityId=${this.state.entityId} session=${id} (sdp ${request.sdp.length} chars)`
173166
+ );
172122
173167
  if (request.webRtcSessionId == null) {
172123
173168
  this.trackSession(
172124
173169
  id,
@@ -172126,37 +173171,96 @@ var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
172126
173171
  request.originatingEndpointId ?? EndpointNumber(0)
172127
173172
  );
172128
173173
  }
172129
- const answerSdp = await this.state.bridge.acceptControllerOffer(
172130
- id,
172131
- this.state.entityId,
172132
- request.sdp
173174
+ const requestorEndpoint = request.originatingEndpointId ?? EndpointNumber(0);
173175
+ const session = this.context.session;
173176
+ if (session) {
173177
+ registerRequestor(id, {
173178
+ session,
173179
+ requestorEndpoint,
173180
+ env: this.env
173181
+ });
173182
+ }
173183
+ let answerSdp;
173184
+ try {
173185
+ answerSdp = await this.state.bridge.acceptControllerOffer(
173186
+ id,
173187
+ this.state.entityId,
173188
+ request.sdp,
173189
+ {
173190
+ iceServers: request.iceServers,
173191
+ iceTransportPolicy: request.iceTransportPolicy
173192
+ }
173193
+ );
173194
+ } catch (err) {
173195
+ const message = errText2(err);
173196
+ logger245.info(
173197
+ `provideOffer failed for ${this.state.entityId} session=${id}: ${message}`
173198
+ );
173199
+ unregisterRequestor(id);
173200
+ this.state.currentSessions = this.state.currentSessions.filter(
173201
+ (s) => s.id !== id
173202
+ );
173203
+ throw new StatusResponseError(
173204
+ `WebRTC offer failed: ${message}`,
173205
+ StatusCode.Failure
173206
+ );
173207
+ }
173208
+ logger245.info(
173209
+ `provideOffer answer computed for ${this.state.entityId} session=${id} (${answerSdp.length} chars); delivering via requestor`
172133
173210
  );
172134
- void answerSdp;
173211
+ const bridge = this.state.bridge;
173212
+ const state = this.state;
173213
+ deliverAnswerDeferred(id, answerSdp, async () => {
173214
+ await bridge.endSession(id).catch(() => {
173215
+ });
173216
+ unregisterRequestor(id);
173217
+ try {
173218
+ state.currentSessions = state.currentSessions.filter(
173219
+ (s) => s.id !== id
173220
+ );
173221
+ } catch {
173222
+ }
173223
+ });
172135
173224
  return { webRtcSessionId: id };
172136
173225
  }
172137
173226
  provideAnswer(request) {
173227
+ logger245.info(
173228
+ `provideAnswer session=${request.webRtcSessionId} (sdp ${request.sdp.length} chars, ${this.state.entityId})`
173229
+ );
172138
173230
  return this.state.bridge.acceptControllerAnswer(
172139
173231
  request.webRtcSessionId,
172140
173232
  request.sdp
172141
173233
  );
172142
173234
  }
172143
173235
  async provideIceCandidates(request) {
173236
+ logger245.info(
173237
+ `provideIceCandidates session=${request.webRtcSessionId}: ${request.iceCandidates.length} candidate(s) (${this.state.entityId})`
173238
+ );
172144
173239
  for (const c of request.iceCandidates) {
172145
173240
  await this.state.bridge.addControllerIceCandidate(
172146
173241
  request.webRtcSessionId,
172147
173242
  c.candidate,
172148
- c.sdpMid
173243
+ c.sdpMid,
173244
+ c.sdpmLineIndex
172149
173245
  );
172150
173246
  }
172151
173247
  }
172152
173248
  async endSession(request) {
173249
+ logger245.info(
173250
+ `endSession session=${request.webRtcSessionId} (${this.state.entityId})`
173251
+ );
172153
173252
  await this.state.bridge.endSession(request.webRtcSessionId);
173253
+ unregisterRequestor(request.webRtcSessionId);
172154
173254
  this.state.currentSessions = this.state.currentSessions.filter(
172155
173255
  (s) => s.id !== request.webRtcSessionId
172156
173256
  );
172157
173257
  }
172158
173258
  trackSession(id, streamUsage, peerEndpointId) {
172159
173259
  const session = this.context.session;
173260
+ const fabricIndex = session?.associatedFabric?.fabricIndex;
173261
+ if (fabricIndex == null) {
173262
+ return;
173263
+ }
172160
173264
  this.state.currentSessions = [
172161
173265
  ...this.state.currentSessions,
172162
173266
  {
@@ -172165,16 +173269,18 @@ var CameraWebRtcProviderServer = class extends WebRtcTransportProviderServer {
172165
173269
  peerEndpointId,
172166
173270
  streamUsage,
172167
173271
  metadataEnabled: false,
172168
- fabricIndex: session?.associatedFabric?.fabricIndex ?? FabricIndex3(0)
173272
+ fabricIndex
172169
173273
  }
172170
173274
  ];
172171
173275
  }
172172
173276
  };
173277
+ function errText2(err) {
173278
+ return err instanceof Error ? err.message : String(err);
173279
+ }
172173
173280
  ((CameraWebRtcProviderServer2) => {
172174
173281
  class State extends WebRtcTransportProviderServer.State {
172175
173282
  bridge;
172176
173283
  entityId;
172177
- nextSessionId = 1;
172178
173284
  }
172179
173285
  CameraWebRtcProviderServer2.State = State;
172180
173286
  })(CameraWebRtcProviderServer || (CameraWebRtcProviderServer = {}));
@@ -172193,8 +173299,7 @@ function createCameraEndpointType(bridge, entityId, sensor = defaultSensorParams
172193
173299
  webRtcTransportProvider: {
172194
173300
  bridge,
172195
173301
  entityId,
172196
- currentSessions: [],
172197
- nextSessionId: 1
173302
+ currentSessions: []
172198
173303
  },
172199
173304
  cameraAvStreamManagement: {
172200
173305
  bridge,
@@ -172256,128 +173361,232 @@ function createCameraEndpointType(bridge, entityId, sensor = defaultSensorParams
172256
173361
  }
172257
173362
 
172258
173363
  // src/plugins/builtin/camera/webrtc-bridge.ts
173364
+ init_esm();
172259
173365
  import {
172260
173366
  createConnection as createConnection3,
172261
173367
  createLongLivedTokenAuth as createLongLivedTokenAuth2
172262
173368
  } from "home-assistant-js-websocket";
172263
173369
  import { RTCPeerConnection } from "werift";
173370
+ var logger246 = Logger.get("CameraWebRtc");
173371
+ var DEFAULT_HA_WEBRTC_TIMEOUT_MS = 15e3;
173372
+ var haWebRtcTimeoutMs = DEFAULT_HA_WEBRTC_TIMEOUT_MS;
172264
173373
  var WebRtcBridge = class {
172265
- constructor(config11) {
173374
+ constructor(config11, deps = {}) {
172266
173375
  this.config = config11;
173376
+ this.deps = deps;
172267
173377
  }
172268
173378
  config;
173379
+ deps;
172269
173380
  connection;
172270
173381
  sessions = /* @__PURE__ */ new Map();
172271
173382
  async ha() {
172272
173383
  if (this.connection) return this.connection;
172273
- const auth = createLongLivedTokenAuth2(
172274
- this.config.haUrl,
172275
- this.config.haToken
172276
- );
172277
- this.connection = await createConnection3({ auth });
173384
+ if (this.deps.connect) {
173385
+ this.connection = await this.deps.connect(this.config);
173386
+ } else {
173387
+ const auth = createLongLivedTokenAuth2(
173388
+ this.config.haUrl,
173389
+ this.config.haToken
173390
+ );
173391
+ this.connection = await createConnection3({ auth });
173392
+ }
172278
173393
  return this.connection;
172279
173394
  }
172280
173395
  // We offer to the controller; it answers. Pulls HA media first, then forwards.
172281
- async startSession(matterSessionId, entityId) {
173396
+ async startSession(matterSessionId, entityId, ice) {
172282
173397
  const haPeer = new RTCPeerConnection();
172283
- const controllerPeer = new RTCPeerConnection();
172284
- haPeer.onTrack.subscribe((track) => {
172285
- const transceiver = controllerPeer.addTransceiver(
172286
- track.kind,
172287
- { direction: "sendonly" }
173398
+ const controllerPeer = new RTCPeerConnection(
173399
+ this.controllerConfig(entityId, ice)
173400
+ );
173401
+ this.wireStateLogging(haPeer, "haPeer", entityId);
173402
+ this.wireStateLogging(controllerPeer, "controllerPeer", entityId);
173403
+ const senders = /* @__PURE__ */ new Map();
173404
+ senders.set(
173405
+ "video",
173406
+ controllerPeer.addTransceiver("video", { direction: "sendonly" })
173407
+ );
173408
+ this.forwardHaTracks(haPeer, senders, entityId);
173409
+ await this.closeExistingPeers(matterSessionId);
173410
+ this.sessions.set(matterSessionId, { entityId, haPeer, controllerPeer });
173411
+ try {
173412
+ haPeer.addTransceiver("video", { direction: "recvonly" });
173413
+ haPeer.addTransceiver("audio", { direction: "recvonly" });
173414
+ const haOffer = await haPeer.createOffer();
173415
+ await haPeer.setLocalDescription(haOffer);
173416
+ const haOfferSdp = this.localSdp(haPeer);
173417
+ logger246.info(`HA offer sent for ${entityId} (${haOfferSdp.length} chars)`);
173418
+ const { answer, sessionId, unsubscribe } = await this.requestHaWebRtc(
173419
+ entityId,
173420
+ haOfferSdp,
173421
+ haPeer
172288
173422
  );
172289
- track.onReceiveRtp.subscribe((rtp) => transceiver.sender.sendRtp(rtp));
172290
- });
172291
- haPeer.addTransceiver("video", { direction: "recvonly" });
172292
- haPeer.addTransceiver("audio", { direction: "recvonly" });
172293
- const haOffer = await haPeer.createOffer();
172294
- await haPeer.setLocalDescription(haOffer);
172295
- const haAnswer = await this.requestHaWebRtc(entityId, haOffer.sdp);
172296
- await haPeer.setRemoteDescription({ type: "answer", sdp: haAnswer });
172297
- const controllerOffer = await controllerPeer.createOffer();
172298
- await controllerPeer.setLocalDescription(controllerOffer);
172299
- const iceCandidates = [];
172300
- controllerPeer.onIceCandidate.subscribe((c) => {
172301
- if (c?.candidate)
172302
- iceCandidates.push({
172303
- candidate: c.candidate,
172304
- sdpMid: c.sdpMid ?? null
172305
- });
172306
- });
172307
- this.sessions.set(matterSessionId, {
172308
- entityId,
172309
- haPeer,
172310
- controllerPeer
172311
- });
172312
- return { sdp: controllerOffer.sdp, iceCandidates };
173423
+ const session = this.sessions.get(matterSessionId);
173424
+ if (session) {
173425
+ session.haSessionId = sessionId;
173426
+ session.haUnsubscribe = unsubscribe;
173427
+ }
173428
+ await haPeer.setRemoteDescription({ type: "answer", sdp: answer });
173429
+ const iceCandidates = [];
173430
+ controllerPeer.onIceCandidate.subscribe((c) => {
173431
+ if (c?.candidate)
173432
+ iceCandidates.push({
173433
+ candidate: c.candidate,
173434
+ sdpMid: c.sdpMid ?? null
173435
+ });
173436
+ });
173437
+ const controllerOffer = await controllerPeer.createOffer();
173438
+ await controllerPeer.setLocalDescription(controllerOffer);
173439
+ const sdp = this.localSdp(controllerPeer);
173440
+ logger246.info(
173441
+ `controller offer created for ${entityId} (${sdp.length} chars)`
173442
+ );
173443
+ return { sdp, iceCandidates };
173444
+ } catch (err) {
173445
+ logger246.info(`startSession failed for ${entityId}: ${errText3(err)}`);
173446
+ await this.endSession(matterSessionId);
173447
+ throw err;
173448
+ }
172313
173449
  }
172314
173450
  // ProvideOffer flow: controller sent its offer. Pull HA media, answer it.
172315
173451
  // Returning the answer to the controller is the unverified Requestor path.
172316
- async acceptControllerOffer(matterSessionId, entityId, controllerOfferSdp) {
173452
+ async acceptControllerOffer(matterSessionId, entityId, controllerOfferSdp, ice) {
172317
173453
  const haPeer = new RTCPeerConnection();
172318
- const controllerPeer = new RTCPeerConnection();
172319
- haPeer.onTrack.subscribe((track) => {
172320
- const transceiver = controllerPeer.addTransceiver(track.kind, {
172321
- direction: "sendonly"
172322
- });
172323
- track.onReceiveRtp.subscribe((rtp) => transceiver.sender.sendRtp(rtp));
172324
- });
173454
+ const controllerPeer = new RTCPeerConnection(
173455
+ this.controllerConfig(entityId, ice)
173456
+ );
173457
+ this.wireStateLogging(haPeer, "haPeer", entityId);
173458
+ this.wireStateLogging(controllerPeer, "controllerPeer", entityId);
173459
+ const senders = /* @__PURE__ */ new Map();
173460
+ for (const kind of offerKinds(controllerOfferSdp)) {
173461
+ senders.set(
173462
+ kind,
173463
+ controllerPeer.addTransceiver(kind, { direction: "sendonly" })
173464
+ );
173465
+ }
173466
+ this.forwardHaTracks(haPeer, senders, entityId);
173467
+ await this.closeExistingPeers(matterSessionId);
172325
173468
  this.sessions.set(matterSessionId, { entityId, haPeer, controllerPeer });
172326
- haPeer.addTransceiver("video", { direction: "recvonly" });
172327
- haPeer.addTransceiver("audio", { direction: "recvonly" });
172328
- const haOffer = await haPeer.createOffer();
172329
- await haPeer.setLocalDescription(haOffer);
172330
- const haAnswer = await this.requestHaWebRtc(entityId, haOffer.sdp);
172331
- await haPeer.setRemoteDescription({ type: "answer", sdp: haAnswer });
172332
- await controllerPeer.setRemoteDescription({
172333
- type: "offer",
172334
- sdp: controllerOfferSdp
172335
- });
172336
- const answer = await controllerPeer.createAnswer();
172337
- await controllerPeer.setLocalDescription(answer);
172338
- return answer.sdp;
173469
+ try {
173470
+ haPeer.addTransceiver("video", { direction: "recvonly" });
173471
+ haPeer.addTransceiver("audio", { direction: "recvonly" });
173472
+ const haOffer = await haPeer.createOffer();
173473
+ await haPeer.setLocalDescription(haOffer);
173474
+ const haOfferSdp = this.localSdp(haPeer);
173475
+ logger246.info(`HA offer sent for ${entityId} (${haOfferSdp.length} chars)`);
173476
+ const { answer, sessionId, unsubscribe } = await this.requestHaWebRtc(
173477
+ entityId,
173478
+ haOfferSdp,
173479
+ haPeer
173480
+ );
173481
+ const session = this.sessions.get(matterSessionId);
173482
+ if (session) {
173483
+ session.haSessionId = sessionId;
173484
+ session.haUnsubscribe = unsubscribe;
173485
+ }
173486
+ await haPeer.setRemoteDescription({ type: "answer", sdp: answer });
173487
+ await controllerPeer.setRemoteDescription({
173488
+ type: "offer",
173489
+ sdp: controllerOfferSdp
173490
+ });
173491
+ const controllerAnswer = await controllerPeer.createAnswer();
173492
+ await controllerPeer.setLocalDescription(controllerAnswer);
173493
+ const answerSdp = this.localSdp(controllerPeer);
173494
+ logger246.info(
173495
+ `controller answer created for ${entityId} (${answerSdp.length} chars)`
173496
+ );
173497
+ return answerSdp;
173498
+ } catch (err) {
173499
+ logger246.info(
173500
+ `acceptControllerOffer failed for ${entityId}: ${errText3(err)}`
173501
+ );
173502
+ await this.endSession(matterSessionId);
173503
+ throw err;
173504
+ }
172339
173505
  }
172340
173506
  /** Apply the Matter controller's SDP answer to the controller peer. */
172341
173507
  async acceptControllerAnswer(matterSessionId, sdp) {
172342
173508
  const session = this.sessions.get(matterSessionId);
172343
173509
  if (!session) return;
173510
+ logger246.debug(`controller answer applied for ${session.entityId}`);
172344
173511
  await session.controllerPeer.setRemoteDescription({ type: "answer", sdp });
172345
173512
  }
172346
173513
  /** Add a remote ICE candidate from the Matter controller. */
172347
- async addControllerIceCandidate(matterSessionId, candidate, sdpMid) {
173514
+ async addControllerIceCandidate(matterSessionId, candidate, sdpMid, sdpMLineIndex) {
172348
173515
  const session = this.sessions.get(matterSessionId);
172349
173516
  if (!session) return;
173517
+ logger246.debug(
173518
+ `controller ICE candidate for ${session.entityId}: ${candidate}`
173519
+ );
172350
173520
  await session.controllerPeer.addIceCandidate({
172351
173521
  candidate,
172352
- sdpMid: sdpMid ?? void 0
173522
+ sdpMid: sdpMid ?? void 0,
173523
+ sdpMLineIndex: sdpMLineIndex ?? void 0
173524
+ });
173525
+ }
173526
+ // Close the peers of a session we are about to overwrite. Leaves no map
173527
+ // entry; the caller replaces it with fresh peers under the same id.
173528
+ async closeExistingPeers(matterSessionId) {
173529
+ const prior = this.sessions.get(matterSessionId);
173530
+ if (!prior) return;
173531
+ logger246.info(
173532
+ `replacing session ${matterSessionId} (${prior.entityId}), closing prior peers`
173533
+ );
173534
+ if (prior.haUnsubscribe) {
173535
+ await Promise.resolve(prior.haUnsubscribe()).catch(
173536
+ (err) => logger246.debug(`HA unsubscribe failed: ${errText3(err)}`)
173537
+ );
173538
+ }
173539
+ await prior.haPeer.close().catch(() => {
173540
+ });
173541
+ await prior.controllerPeer.close().catch(() => {
173542
+ });
173543
+ await this.stopHaStream(prior).catch(() => {
172353
173544
  });
172354
173545
  }
172355
173546
  async endSession(matterSessionId) {
172356
173547
  const session = this.sessions.get(matterSessionId);
172357
173548
  if (!session) return;
173549
+ logger246.info(`ending session ${matterSessionId} (${session.entityId})`);
172358
173550
  this.sessions.delete(matterSessionId);
172359
- await session.haPeer.close();
172360
- await session.controllerPeer.close();
172361
- if (session.haSessionId) {
172362
- try {
172363
- const conn = await this.ha();
172364
- await conn.sendMessagePromise({
172365
- type: "camera/webrtc/candidate",
172366
- entity_id: session.entityId,
172367
- session_id: session.haSessionId,
172368
- candidate: { candidate: "" }
172369
- });
172370
- } catch {
172371
- }
173551
+ if (session.haUnsubscribe) {
173552
+ await Promise.resolve(session.haUnsubscribe()).catch(
173553
+ (err) => logger246.debug(`HA unsubscribe failed: ${errText3(err)}`)
173554
+ );
172372
173555
  }
173556
+ await session.haPeer.close().catch(() => {
173557
+ });
173558
+ await session.controllerPeer.close().catch(() => {
173559
+ });
173560
+ await this.stopHaStream(session).catch(() => {
173561
+ });
173562
+ }
173563
+ // Tell HA to stop the stream if we tracked an HA session id. Best effort.
173564
+ async stopHaStream(session) {
173565
+ if (!session.haSessionId) return;
173566
+ const conn = await this.ha();
173567
+ await conn.sendMessagePromise({
173568
+ type: "camera/webrtc/candidate",
173569
+ entity_id: session.entityId,
173570
+ session_id: session.haSessionId,
173571
+ candidate: { candidate: "" }
173572
+ });
172373
173573
  }
172374
173574
  // Grab a still JPEG via HA's camera proxy (for CaptureSnapshot).
172375
173575
  async snapshot(entityId) {
172376
173576
  const url2 = `${this.config.haUrl}/api/camera_proxy/${entityId}`;
172377
- const res = await fetch(url2, {
172378
- headers: { Authorization: `Bearer ${this.config.haToken}` }
172379
- });
173577
+ let res;
173578
+ try {
173579
+ res = await fetch(url2, {
173580
+ headers: { Authorization: `Bearer ${this.config.haToken}` }
173581
+ });
173582
+ } catch (err) {
173583
+ logger246.info(`snapshot fetch failed for ${entityId}: ${errText3(err)}`);
173584
+ throw err;
173585
+ }
172380
173586
  if (!res.ok) {
173587
+ logger246.info(
173588
+ `snapshot fetch failed for ${entityId}: camera_proxy ${res.status}`
173589
+ );
172381
173590
  throw new Error(`HA camera_proxy ${entityId}: ${res.status}`);
172382
173591
  }
172383
173592
  return new Uint8Array(await res.arrayBuffer());
@@ -172389,29 +173598,157 @@ var WebRtcBridge = class {
172389
173598
  this.connection?.close();
172390
173599
  this.connection = void 0;
172391
173600
  }
172392
- // Offer to HA's WebRTC, resolve on the first answer event off the same sub.
172393
- async requestHaWebRtc(entityId, offerSdp) {
173601
+ // Forward every track HA sends into the matching pre-added controller sender.
173602
+ forwardHaTracks(haPeer, senders, entityId) {
173603
+ haPeer.onTrack.subscribe((track) => {
173604
+ logger246.info(`onTrack ${track.kind} from HA (${entityId})`);
173605
+ const transceiver = senders.get(track.kind);
173606
+ if (!transceiver) {
173607
+ logger246.info(
173608
+ `no controller transceiver for ${track.kind} (${entityId})`
173609
+ );
173610
+ return;
173611
+ }
173612
+ let firstLogged = false;
173613
+ track.onReceiveRtp.subscribe((rtp) => {
173614
+ if (!firstLogged) {
173615
+ firstLogged = true;
173616
+ logger246.info(`first RTP forwarded for ${track.kind} (${entityId})`);
173617
+ }
173618
+ transceiver.sender.sendRtp(rtp);
173619
+ });
173620
+ });
173621
+ }
173622
+ // Log connection/ice state transitions (these events fire on change only).
173623
+ wireStateLogging(peer, label, entityId) {
173624
+ peer.connectionStateChange.subscribe(
173625
+ (s) => logger246.info(`${label} connectionState=${s} (${entityId})`)
173626
+ );
173627
+ peer.iceConnectionStateChange.subscribe(
173628
+ (s) => logger246.info(`${label} iceConnectionState=${s} (${entityId})`)
173629
+ );
173630
+ }
173631
+ // Map the Matter ICE struct onto werift's config. Only the controller peer
173632
+ // uses controller-supplied servers; the HA peer keeps werift defaults.
173633
+ controllerConfig(entityId, ice) {
173634
+ const iceServers = (ice?.iceServers ?? []).flatMap(
173635
+ (s) => (s.urLs ?? []).map((url2) => ({
173636
+ urls: url2,
173637
+ username: s.username,
173638
+ credential: s.credential
173639
+ }))
173640
+ );
173641
+ if (iceServers.length === 0) {
173642
+ logger246.info(`controller peer using default ICE servers (${entityId})`);
173643
+ return void 0;
173644
+ }
173645
+ const policy = ice?.iceTransportPolicy === "relay" || ice?.iceTransportPolicy === "all" ? ice.iceTransportPolicy : void 0;
173646
+ logger246.info(
173647
+ `controller peer using ${iceServers.length} controller ICE server(s)${policy ? ` policy=${policy}` : ""} (${entityId})`
173648
+ );
173649
+ return policy ? { iceServers, iceTransportPolicy: policy } : { iceServers };
173650
+ }
173651
+ localSdp(peer) {
173652
+ const local = peer.localDescription;
173653
+ if (!local) throw new Error("no local description");
173654
+ return local.sdp;
173655
+ }
173656
+ // Feed an HA trickle-ICE candidate into the HA peer.
173657
+ feedHaCandidate(entityId, haPeer, candidate) {
173658
+ const value = candidate?.candidate;
173659
+ if (!value) return;
173660
+ logger246.debug(`HA ICE candidate for ${entityId}: ${value}`);
173661
+ void haPeer.addIceCandidate({
173662
+ candidate: value,
173663
+ sdpMid: candidate?.sdpMid ?? void 0,
173664
+ sdpMLineIndex: candidate?.sdpMLineIndex ?? void 0
173665
+ }).catch(
173666
+ (e) => logger246.debug(
173667
+ `HA addIceCandidate failed for ${entityId}: ${errText3(e)}`
173668
+ )
173669
+ );
173670
+ }
173671
+ // Offer to HA's WebRTC and resolve on the first answer. Rejects on an error
173672
+ // reply or after a timeout; keeps the subscription open past the answer so HA
173673
+ // trickle candidates keep flowing into the HA peer until the session ends.
173674
+ async requestHaWebRtc(entityId, offerSdp, haPeer) {
172394
173675
  const conn = await this.ha();
172395
- return new Promise((resolve11, reject) => {
173676
+ let unsubscribe = () => {
173677
+ };
173678
+ let sessionId;
173679
+ const answer = await new Promise((resolve11, reject) => {
172396
173680
  let settled = false;
173681
+ let rejected = false;
173682
+ let timer;
173683
+ const settle = (fn) => {
173684
+ if (settled) return;
173685
+ settled = true;
173686
+ clearTimeout(timer);
173687
+ fn();
173688
+ };
173689
+ const fail = (err) => settle(() => {
173690
+ rejected = true;
173691
+ unsubscribe();
173692
+ reject(err);
173693
+ });
173694
+ timer = setTimeout(() => {
173695
+ logger246.info(
173696
+ `HA WebRTC timed out after ${haWebRtcTimeoutMs}ms for ${entityId}`
173697
+ );
173698
+ fail(
173699
+ new Error(`HA WebRTC request timed out after ${haWebRtcTimeoutMs}ms`)
173700
+ );
173701
+ }, haWebRtcTimeoutMs);
172397
173702
  conn.subscribeMessage(
172398
173703
  (msg) => {
172399
- if (msg.type === "answer" && msg.answer && !settled) {
172400
- settled = true;
172401
- const session = [...this.sessions.values()].find(
172402
- (s) => s.entityId === entityId
172403
- );
172404
- if (session && msg.session_id) {
172405
- session.haSessionId = msg.session_id;
173704
+ switch (msg.type) {
173705
+ case "session":
173706
+ if (msg.session_id) sessionId = msg.session_id;
173707
+ break;
173708
+ case "answer":
173709
+ if (msg.answer) {
173710
+ if (msg.session_id) sessionId = msg.session_id;
173711
+ const ans = msg.answer;
173712
+ settle(() => {
173713
+ logger246.info(
173714
+ `HA answer received for ${entityId} (${ans.length} chars)`
173715
+ );
173716
+ resolve11(ans);
173717
+ });
173718
+ }
173719
+ break;
173720
+ case "error": {
173721
+ const detail = msg.message ?? msg.code ?? "unknown error";
173722
+ logger246.info(`HA WebRTC error for ${entityId}: ${detail}`);
173723
+ fail(new Error(`HA WebRTC error: ${detail}`));
173724
+ break;
172406
173725
  }
172407
- resolve11(msg.answer);
173726
+ case "candidate":
173727
+ this.feedHaCandidate(entityId, haPeer, msg.candidate);
173728
+ break;
172408
173729
  }
172409
173730
  },
172410
173731
  { type: "camera/webrtc/offer", entity_id: entityId, offer: offerSdp }
172411
- ).catch(reject);
173732
+ ).then((unsub) => {
173733
+ unsubscribe = unsub;
173734
+ if (rejected) unsub();
173735
+ }).catch((err) => {
173736
+ fail(err instanceof Error ? err : new Error(String(err)));
173737
+ });
172412
173738
  });
173739
+ return { answer, sessionId, unsubscribe };
172413
173740
  }
172414
173741
  };
173742
+ function offerKinds(sdp) {
173743
+ const kinds = [];
173744
+ for (const m of sdp.matchAll(/^m=(video|audio)/gm)) {
173745
+ kinds.push(m[1]);
173746
+ }
173747
+ return kinds;
173748
+ }
173749
+ function errText3(err) {
173750
+ return err instanceof Error ? err.message : String(err);
173751
+ }
172415
173752
 
172416
173753
  // src/plugins/builtin/camera/camera-plugin.ts
172417
173754
  var CONFIG_KEY = "config";
@@ -172544,6 +173881,7 @@ var CameraPlugin = class {
172544
173881
  await this.bridge?.close().catch(() => {
172545
173882
  });
172546
173883
  this.bridge = void 0;
173884
+ unregisterAllRequestors();
172547
173885
  }
172548
173886
  };
172549
173887
 
@@ -172557,7 +173895,7 @@ import {
172557
173895
  getCollection
172558
173896
  } from "home-assistant-js-websocket";
172559
173897
  import { atLeastHaVersion } from "home-assistant-js-websocket/dist/util.js";
172560
- var logger243 = Logger.get("SubscribeEntities");
173898
+ var logger247 = Logger.get("SubscribeEntities");
172561
173899
  function processEvent(store, updates) {
172562
173900
  const state = { ...store.state };
172563
173901
  if (updates.a) {
@@ -172583,7 +173921,7 @@ function processEvent(store, updates) {
172583
173921
  for (const entityId in updates.c) {
172584
173922
  let entityState = state[entityId];
172585
173923
  if (!entityState) {
172586
- logger243.warn("Received state update for unknown entity", entityId);
173924
+ logger247.warn("Received state update for unknown entity", entityId);
172587
173925
  continue;
172588
173926
  }
172589
173927
  entityState = { ...entityState };
@@ -172672,7 +174010,7 @@ var subscribeEntities = (conn, onChange, entityIds) => {
172672
174010
  // src/services/bridges/entity-isolation-service.ts
172673
174011
  init_esm();
172674
174012
  init_diagnostic_event_bus();
172675
- var logger244 = Logger.get("EntityIsolation");
174013
+ var logger248 = Logger.get("EntityIsolation");
172676
174014
  var EntityIsolationServiceImpl = class {
172677
174015
  isolatedEntities = /* @__PURE__ */ new Map();
172678
174016
  isolationCallbacks = /* @__PURE__ */ new Map();
@@ -172737,13 +174075,13 @@ var EntityIsolationServiceImpl = class {
172737
174075
  }
172738
174076
  const parsed = this.parseEndpointPath(msg);
172739
174077
  if (!parsed) {
172740
- logger244.warn("Could not parse entity from error:", msg);
174078
+ logger248.warn("Could not parse entity from error:", msg);
172741
174079
  return false;
172742
174080
  }
172743
174081
  const { bridgeId, entityName } = parsed;
172744
174082
  const callback = this.isolationCallbacks.get(bridgeId);
172745
174083
  if (!callback) {
172746
- logger244.warn(
174084
+ logger248.warn(
172747
174085
  `No isolation callback registered for bridge ${bridgeId}, entity: ${entityName}`
172748
174086
  );
172749
174087
  return false;
@@ -172758,7 +174096,7 @@ var EntityIsolationServiceImpl = class {
172758
174096
  reason,
172759
174097
  failedAt: (/* @__PURE__ */ new Date()).toISOString()
172760
174098
  });
172761
- logger244.warn(
174099
+ logger248.warn(
172762
174100
  `Isolating entity "${entityName}" from bridge ${bridgeId} due to: ${reason}`
172763
174101
  );
172764
174102
  diagnosticEventBus.emit("entity_error", `Entity isolated: ${entityName}`, {
@@ -172770,7 +174108,7 @@ var EntityIsolationServiceImpl = class {
172770
174108
  await callback(entityName);
172771
174109
  return true;
172772
174110
  } catch (e) {
172773
- logger244.error(`Failed to isolate entity ${entityName}:`, e);
174111
+ logger248.error(`Failed to isolate entity ${entityName}:`, e);
172774
174112
  return false;
172775
174113
  }
172776
174114
  }
@@ -172799,128 +174137,16 @@ var EntityIsolationServiceImpl = class {
172799
174137
  };
172800
174138
  var EntityIsolationService = new EntityIsolationServiceImpl();
172801
174139
 
172802
- // src/services/bridges/identity-resolver.ts
172803
- var SEP = "\0";
172804
- function identityKey(entity) {
172805
- const uniqueId = entity.registry?.unique_id;
172806
- const platform = entity.registry?.platform;
172807
- if (!uniqueId || !platform) {
172808
- return null;
172809
- }
172810
- const domain = entity.entity_id.split(".")[0];
172811
- return platform + SEP + domain + SEP + uniqueId;
172812
- }
172813
- var IdentityResolver = class {
172814
- constructor(identityStorage, mappingStorage) {
172815
- this.identityStorage = identityStorage;
172816
- this.mappingStorage = mappingStorage;
172817
- }
172818
- identityStorage;
172819
- mappingStorage;
172820
- async resolveIdentity(bridgeId, entity, mapping, opts) {
172821
- const entityId = entity.entity_id;
172822
- const key = identityKey(entity);
172823
- if (key == null) {
172824
- return {
172825
- endpointId: createEndpointId(entityId, mapping?.customName),
172826
- anchorEntityId: entityId,
172827
- protected: false
172828
- };
172829
- }
172830
- const existing = this.identityStorage.getIdentity(bridgeId, key);
172831
- if (existing) {
172832
- const renamedFrom = existing.lastEntityId !== entityId ? existing.lastEntityId ?? existing.anchorEntityId : void 0;
172833
- if (opts.stableIdentity) {
172834
- if (renamedFrom) {
172835
- this.identityStorage.setIdentity(bridgeId, key, {
172836
- ...existing,
172837
- lastEntityId: entityId
172838
- });
172839
- await this.rekeyMapping(bridgeId, renamedFrom, entityId);
172840
- }
172841
- return {
172842
- endpointId: existing.endpointId,
172843
- anchorEntityId: existing.anchorEntityId,
172844
- protected: true,
172845
- renamedFrom
172846
- };
172847
- }
172848
- const liveEndpointId = createEndpointId(entityId, mapping?.customName);
172849
- if (existing.endpointId !== liveEndpointId || existing.anchorEntityId !== entityId || existing.lastEntityId !== entityId) {
172850
- this.identityStorage.setIdentity(bridgeId, key, {
172851
- ...existing,
172852
- endpointId: liveEndpointId,
172853
- anchorEntityId: entityId,
172854
- lastEntityId: entityId
172855
- });
172856
- }
172857
- return {
172858
- endpointId: liveEndpointId,
172859
- anchorEntityId: entityId,
172860
- protected: false,
172861
- renamedFrom
172862
- };
172863
- }
172864
- const desired = createEndpointId(entityId, mapping?.customName);
172865
- let endpointId = desired;
172866
- if (opts.stableIdentity) {
172867
- let n = 2;
172868
- while (opts.isEndpointIdTaken(endpointId, key)) {
172869
- endpointId = `${desired}_${n++}`;
172870
- }
172871
- }
172872
- const record = {
172873
- endpointId,
172874
- anchorEntityId: entityId,
172875
- lastEntityId: entityId,
172876
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
172877
- };
172878
- this.identityStorage.setIdentity(bridgeId, key, record);
172879
- if (opts.stableIdentity) {
172880
- return {
172881
- endpointId: record.endpointId,
172882
- anchorEntityId: record.anchorEntityId,
172883
- protected: true
172884
- };
172885
- }
172886
- return {
172887
- endpointId: desired,
172888
- anchorEntityId: entityId,
172889
- protected: false
172890
- };
172891
- }
172892
- // Carry a mapping from the old entity_id to the new one on rename. anchorEntityId
172893
- // never changes, only the mapping key. Skips when no old mapping exists or a new
172894
- // one already does, so a manual mapping under the new id is never clobbered.
172895
- async rekeyMapping(bridgeId, oldEntityId, newEntityId) {
172896
- if (oldEntityId === newEntityId) {
172897
- return;
172898
- }
172899
- const old = this.mappingStorage.getMapping(bridgeId, oldEntityId);
172900
- if (!old) {
172901
- return;
172902
- }
172903
- if (this.mappingStorage.getMapping(bridgeId, newEntityId)) {
172904
- return;
172905
- }
172906
- await this.mappingStorage.setMapping({
172907
- ...old,
172908
- bridgeId,
172909
- entityId: newEntityId
172910
- });
172911
- await this.mappingStorage.deleteMapping(bridgeId, oldEntityId);
172912
- }
172913
- };
172914
-
172915
174140
  // src/services/bridges/bridge-endpoint-manager.ts
172916
174141
  var MAX_ENTITY_ID_LENGTH = 150;
172917
174142
  var ENDPOINT_REMOVAL_GRACE_MS = 6e4;
172918
174143
  var BridgeEndpointManager = class extends Service {
172919
- constructor(client, registry2, mappingStorage, identityStorage, bridgeId, log, pluginManager, pluginRegistry, pluginInstaller) {
174144
+ constructor(client, registry3, mappingStorage, identityStorage, bridgeId, log, pluginManager, pluginRegistry, pluginInstaller) {
172920
174145
  super("BridgeEndpointManager");
172921
174146
  this.client = client;
172922
- this.registry = registry2;
174147
+ this.registry = registry3;
172923
174148
  this.mappingStorage = mappingStorage;
174149
+ this.identityStorage = identityStorage;
172924
174150
  this.bridgeId = bridgeId;
172925
174151
  this.log = log;
172926
174152
  this.pluginManager = pluginManager;
@@ -172942,6 +174168,7 @@ var BridgeEndpointManager = class extends Service {
172942
174168
  client;
172943
174169
  registry;
172944
174170
  mappingStorage;
174171
+ identityStorage;
172945
174172
  bridgeId;
172946
174173
  log;
172947
174174
  pluginManager;
@@ -173339,6 +174566,17 @@ var BridgeEndpointManager = class extends Service {
173339
174566
  this.mappingFingerprints.set(entityId, fp);
173340
174567
  }
173341
174568
  }
174569
+ const fullEntities = this.registry.fullEntities;
174570
+ stampIdentityPresence(
174571
+ this.identityStorage,
174572
+ this.bridgeId,
174573
+ buildPresentIdentityKeys(fullEntities)
174574
+ );
174575
+ stampMappingPresence(
174576
+ this.mappingStorage,
174577
+ this.bridgeId,
174578
+ buildPresentEntityIds(fullEntities)
174579
+ );
173342
174580
  const existingEndpoints = [];
173343
174581
  const now = Date.now();
173344
174582
  for (const endpoint of endpoints) {
@@ -173617,8 +174855,8 @@ init_send_ha_message();
173617
174855
  import { callService as callService2 } from "home-assistant-js-websocket";
173618
174856
  import { keys as keys2, pickBy, values as values3 } from "lodash-es";
173619
174857
  var BridgeRegistry = class _BridgeRegistry {
173620
- constructor(registry2, dataProvider, client) {
173621
- this.registry = registry2;
174858
+ constructor(registry3, dataProvider, client) {
174859
+ this.registry = registry3;
173622
174860
  this.dataProvider = dataProvider;
173623
174861
  this.client = client;
173624
174862
  this.refresh();
@@ -173658,6 +174896,11 @@ var BridgeRegistry = class _BridgeRegistry {
173658
174896
  initialState(entityId) {
173659
174897
  return this._states[entityId];
173660
174898
  }
174899
+ // The complete HA entity set (unfiltered). Used by orphan tombstone stamping
174900
+ // so a filter change or a scope narrowing never looks like a removal.
174901
+ get fullEntities() {
174902
+ return this.registry.entities;
174903
+ }
173661
174904
  // composed sub-entities may sit outside the bridge filter (#408), so these
173662
174905
  // fall back to the full HA registry. keep them separate from the strict
173663
174906
  // accessors above, every other caller must stay filtered.
@@ -174375,6 +175618,7 @@ init_dist();
174375
175618
  init_esm7();
174376
175619
  import * as os10 from "node:os";
174377
175620
  init_diagnostic_event_bus();
175621
+ var imWrapMarker2 = /* @__PURE__ */ Symbol("hamh.wedgeImWrap");
174378
175622
  var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
174379
175623
  var SHUTDOWN_SESSION_CLOSE_TIMEOUT_MS2 = 2500;
174380
175624
  var MDNS_ADDRESS_CHECK_INTERVAL_MS2 = 6e4;
@@ -174382,11 +175626,11 @@ function makeWarmStartState(state, now = (/* @__PURE__ */ new Date()).toISOStrin
174382
175626
  return { ...state, last_updated: now };
174383
175627
  }
174384
175628
  var ServerModeBridge = class {
174385
- constructor(logger247, dataProvider, endpointManager, server) {
175629
+ constructor(logger251, dataProvider, endpointManager, server) {
174386
175630
  this.dataProvider = dataProvider;
174387
175631
  this.endpointManager = endpointManager;
174388
175632
  this.server = server;
174389
- this.log = logger247.get(`ServerModeBridge / ${dataProvider.id}`);
175633
+ this.log = logger251.get(`ServerModeBridge / ${dataProvider.id}`);
174390
175634
  }
174391
175635
  dataProvider;
174392
175636
  endpointManager;
@@ -174407,6 +175651,12 @@ var ServerModeBridge = class {
174407
175651
  sessionStartedAt = /* @__PURE__ */ new Map();
174408
175652
  rotationTimer = null;
174409
175653
  maxSessionAgeMs = 0;
175654
+ // Wedge watchdog: last inbound Interaction Model request time per session and
175655
+ // last time the watchdog rotated it, both keyed by the long-lived session
175656
+ // object so they clear when the session goes away.
175657
+ lastImRequestAt = /* @__PURE__ */ new WeakMap();
175658
+ wedgeLastRotatedAt = /* @__PURE__ */ new WeakMap();
175659
+ wedgeWatchdogTimer = null;
174410
175660
  // Watches the advertised interface addresses so a dynamic ISP IPv6 prefix
174411
175661
  // change forces a fresh operational announcement (#415).
174412
175662
  mdnsAddressTimer = null;
@@ -174470,6 +175720,7 @@ var ServerModeBridge = class {
174470
175720
  const sessionList = sessions.map((s) => {
174471
175721
  const subCount = s.subscriptions.size;
174472
175722
  totalSubscriptions += subCount;
175723
+ const subscriptions = summarizeSubscriptions(s.subscriptions);
174473
175724
  const fi = typeof s.fabric?.fabricIndex === "number" ? s.fabric.fabricIndex : null;
174474
175725
  if (fi !== null) {
174475
175726
  const existing = fabricMap.get(fi) ?? {
@@ -174483,14 +175734,18 @@ var ServerModeBridge = class {
174483
175734
  const nowMs = Date.now();
174484
175735
  const lastActiveMsAgo = typeof s.activeTimestamp === "number" && s.activeTimestamp > 0 ? nowMs - s.activeTimestamp : null;
174485
175736
  const lastAnyActivityMsAgo = typeof s.timestamp === "number" ? nowMs - s.timestamp : null;
175737
+ const lastImAt = this.lastImRequestAt.get(s);
175738
+ const lastImRequestMsAgo = lastImAt != null ? nowMs - lastImAt : null;
174486
175739
  const startedAt = this.sessionStartedAt.get(s.id);
174487
175740
  return {
174488
175741
  id: s.id,
174489
175742
  peerNodeId: String(s.peerNodeId),
174490
175743
  fabricIndex: fi,
174491
175744
  subscriptionCount: subCount,
175745
+ subscriptions,
174492
175746
  lastActiveMsAgo,
174493
175747
  lastAnyActivityMsAgo,
175748
+ lastImRequestMsAgo,
174494
175749
  isPeerActive: Boolean(s.isPeerActive),
174495
175750
  ageMsFromOpen: startedAt != null ? nowMs - startedAt : null
174496
175751
  };
@@ -174554,6 +175809,7 @@ var ServerModeBridge = class {
174554
175809
  this.wireSessionDiagnostics();
174555
175810
  this.wireFabricWarnings();
174556
175811
  this.startSessionRotation();
175812
+ this.startWedgeWatchdog();
174557
175813
  this.startMdnsAddressWatch();
174558
175814
  this.scheduleWarmStart();
174559
175815
  logMemoryUsage(this.log, "server mode bridge running");
@@ -174571,6 +175827,7 @@ ${e?.toString()}`);
174571
175827
  }
174572
175828
  async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
174573
175829
  this.stopSessionRotation();
175830
+ this.stopWedgeWatchdog();
174574
175831
  this.stopMdnsAddressWatch();
174575
175832
  this.unwireSessionDiagnostics();
174576
175833
  this.unwireFabricWarnings();
@@ -174603,6 +175860,7 @@ ${e?.toString()}`);
174603
175860
  if (this.status.code === BridgeStatus.Running) {
174604
175861
  this.startAutoForceSyncIfEnabled();
174605
175862
  this.startSessionRotation();
175863
+ this.startWedgeWatchdog();
174606
175864
  }
174607
175865
  } catch (e) {
174608
175866
  const reason = "Failed to update server mode bridge due to error:";
@@ -174787,6 +176045,29 @@ ${e?.toString()}`);
174787
176045
  sessionManager.sessions.added.on(this.sessionAddedHandler);
174788
176046
  sessionManager.sessions.deleted.on(this.sessionDeletedHandler);
174789
176047
  seedExistingSessionStarts(this.sessionStartedAt, sessionManager.sessions);
176048
+ this.wireImRequestTracking();
176049
+ } catch {
176050
+ }
176051
+ }
176052
+ // Stamp the time of every inbound Interaction Model request per session by
176053
+ // wrapping InteractionServer.onNewExchange. The wedge watchdog reads these to
176054
+ // tell a live-but-consuming controller from one that only keeps acking.
176055
+ wireImRequestTracking() {
176056
+ try {
176057
+ const is = this.server.env.get(InteractionServer);
176058
+ const marked = is;
176059
+ if (marked[imWrapMarker2]) {
176060
+ return;
176061
+ }
176062
+ const original = is.onNewExchange.bind(is);
176063
+ is.onNewExchange = (exchange, message) => {
176064
+ const session = exchange.session;
176065
+ if (session) {
176066
+ this.lastImRequestAt.set(session, Date.now());
176067
+ }
176068
+ return original(exchange, message);
176069
+ };
176070
+ marked[imWrapMarker2] = true;
174790
176071
  } catch {
174791
176072
  }
174792
176073
  }
@@ -175004,6 +176285,72 @@ ${e?.toString()}`);
175004
176285
  this.rotationTimer = null;
175005
176286
  }
175006
176287
  }
176288
+ // Opt-in wedge watchdog: reuse the rotation check cadence (5min) to look for
176289
+ // the one session wedged on "Updating" and rotate just that one.
176290
+ startWedgeWatchdog() {
176291
+ this.stopWedgeWatchdog();
176292
+ if (!this.dataProvider.featureFlags?.wedgeWatchdog) {
176293
+ return;
176294
+ }
176295
+ this.wedgeWatchdogTimer = setInterval(
176296
+ () => this.runWedgeWatchdogCheck(),
176297
+ ROTATION_CHECK_INTERVAL_MS
176298
+ );
176299
+ this.log.info(
176300
+ `Wedge watchdog: checking every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
176301
+ );
176302
+ }
176303
+ stopWedgeWatchdog() {
176304
+ if (this.wedgeWatchdogTimer) {
176305
+ clearInterval(this.wedgeWatchdogTimer);
176306
+ this.wedgeWatchdogTimer = null;
176307
+ }
176308
+ }
176309
+ // Rotate exactly the sessions the pure rule flags as wedged. Closing is the
176310
+ // same graceful-then-force path age rotation uses, so a false positive just
176311
+ // re-CASEs the controller.
176312
+ runWedgeWatchdogCheck() {
176313
+ try {
176314
+ const sessionManager = this.server.env.get(SessionManager);
176315
+ const now = Date.now();
176316
+ const closes = [];
176317
+ for (const s of [...sessionManager.sessions]) {
176318
+ if (s.isClosing) continue;
176319
+ const startedAt = this.sessionStartedAt.get(s.id);
176320
+ const sessionAgeMs = startedAt != null ? now - startedAt : 0;
176321
+ const lastImAt = this.lastImRequestAt.get(s);
176322
+ const lastImRequestMsAgo = lastImAt != null ? now - lastImAt : null;
176323
+ const lastRotatedAt = this.wedgeLastRotatedAt.get(s);
176324
+ const lastRotatedMsAgo = lastRotatedAt != null ? now - lastRotatedAt : null;
176325
+ if (!decideWedgeRotation({
176326
+ subscriptionCount: s.subscriptions.size,
176327
+ sessionAgeMs,
176328
+ lastImRequestMsAgo,
176329
+ lastRotatedMsAgo
176330
+ })) {
176331
+ continue;
176332
+ }
176333
+ const silenceMin = Math.round(
176334
+ (lastImRequestMsAgo ?? sessionAgeMs) / 6e4
176335
+ );
176336
+ this.log.info(
176337
+ `Wedge watchdog: rotating session ${s.id}, no inbound interaction for ${silenceMin}min`
176338
+ );
176339
+ this.wedgeLastRotatedAt.set(s, now);
176340
+ closes.push(
176341
+ s.initiateClose().catch(() => {
176342
+ return s.initiateForceClose({
176343
+ cause: new Error("wedge watchdog, forcing")
176344
+ });
176345
+ })
176346
+ );
176347
+ }
176348
+ if (closes.length > 0) {
176349
+ Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
176350
+ }
176351
+ } catch {
176352
+ }
176353
+ }
175007
176354
  // Poll the interface addresses mDNS advertises and re-announce when they
175008
176355
  // change. matter.js caches the operational records at first announcement, so
175009
176356
  // a dynamic ISP IPv6 prefix change keeps advertising the dead global address
@@ -175313,12 +176660,12 @@ function ServerModeVacuumDevice(homeAssistantEntity, includeOnOff = false, clean
175313
176660
  }
175314
176661
 
175315
176662
  // src/matter/endpoints/server-mode-vacuum-endpoint.ts
175316
- var logger245 = Logger.get("ServerModeVacuumEndpoint");
176663
+ var logger249 = Logger.get("ServerModeVacuumEndpoint");
175317
176664
  var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEndpoint {
175318
- static async create(registry2, entityId, mapping, endpointId) {
175319
- const deviceRegistry = registry2.deviceOf(entityId);
175320
- let state = registry2.initialState(entityId);
175321
- const entity = registry2.entity(entityId);
176665
+ static async create(registry3, entityId, mapping, endpointId) {
176666
+ const deviceRegistry = registry3.deviceOf(entityId);
176667
+ let state = registry3.initialState(entityId);
176668
+ const entity = registry3.entity(entityId);
175322
176669
  if (!state) {
175323
176670
  return void 0;
175324
176671
  }
@@ -175326,12 +176673,12 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175326
176673
  if (mapping?.disableBatteryMapping && mapping.batteryEntity) {
175327
176674
  effectiveMapping = { ...mapping, batteryEntity: void 0 };
175328
176675
  }
175329
- logger245.info(
176676
+ logger249.info(
175330
176677
  `${entityId}: device_id=${entity.device_id}, manualBattery=${mapping?.batteryEntity ?? "none"}`
175331
176678
  );
175332
176679
  if (entity.device_id) {
175333
176680
  if (!mapping?.batteryEntity && !mapping?.disableBatteryMapping) {
175334
- const batteryEntityId = registry2.findBatteryEntityForDevice(
176681
+ const batteryEntityId = registry3.findBatteryEntityForDevice(
175335
176682
  entity.device_id
175336
176683
  );
175337
176684
  if (batteryEntityId && batteryEntityId !== entityId) {
@@ -175340,22 +176687,22 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175340
176687
  entityId: effectiveMapping?.entityId ?? entityId,
175341
176688
  batteryEntity: batteryEntityId
175342
176689
  };
175343
- registry2.markBatteryEntityUsed(batteryEntityId);
175344
- logger245.info(`${entityId}: Auto-assigned battery ${batteryEntityId}`);
176690
+ registry3.markBatteryEntityUsed(batteryEntityId);
176691
+ logger249.info(`${entityId}: Auto-assigned battery ${batteryEntityId}`);
175345
176692
  } else {
175346
176693
  const attrs = state.attributes;
175347
176694
  if (attrs.battery_level != null || attrs.battery != null) {
175348
- logger245.info(
176695
+ logger249.info(
175349
176696
  `${entityId}: No battery entity found, using battery attribute from vacuum state`
175350
176697
  );
175351
176698
  } else {
175352
- logger245.warn(
176699
+ logger249.warn(
175353
176700
  `${entityId}: No battery entity found for device ${entity.device_id}`
175354
176701
  );
175355
176702
  }
175356
176703
  }
175357
176704
  }
175358
- const vacuumEntities = registry2.findVacuumSelectEntities(
176705
+ const vacuumEntities = registry3.findVacuumSelectEntities(
175359
176706
  entity.device_id
175360
176707
  );
175361
176708
  if (!effectiveMapping?.cleaningModeEntity && vacuumEntities.cleaningModeEntity) {
@@ -175364,7 +176711,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175364
176711
  entityId: effectiveMapping?.entityId ?? entityId,
175365
176712
  cleaningModeEntity: vacuumEntities.cleaningModeEntity
175366
176713
  };
175367
- logger245.info(
176714
+ logger249.info(
175368
176715
  `${entityId}: Auto-assigned cleaningMode ${vacuumEntities.cleaningModeEntity}`
175369
176716
  );
175370
176717
  }
@@ -175374,7 +176721,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175374
176721
  entityId: effectiveMapping?.entityId ?? entityId,
175375
176722
  suctionLevelEntity: vacuumEntities.suctionLevelEntity
175376
176723
  };
175377
- logger245.info(
176724
+ logger249.info(
175378
176725
  `${entityId}: Auto-assigned suctionLevel ${vacuumEntities.suctionLevelEntity}`
175379
176726
  );
175380
176727
  }
@@ -175384,7 +176731,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175384
176731
  entityId: effectiveMapping?.entityId ?? entityId,
175385
176732
  mopIntensityEntity: vacuumEntities.mopIntensityEntity
175386
176733
  };
175387
- logger245.info(
176734
+ logger249.info(
175388
176735
  `${entityId}: Auto-assigned mopIntensity ${vacuumEntities.mopIntensityEntity}`
175389
176736
  );
175390
176737
  }
@@ -175394,12 +176741,12 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175394
176741
  entityId: effectiveMapping?.entityId ?? entityId,
175395
176742
  currentRoomEntity: vacuumEntities.currentRoomEntity
175396
176743
  };
175397
- logger245.info(
176744
+ logger249.info(
175398
176745
  `${entityId}: Auto-assigned currentRoom ${vacuumEntities.currentRoomEntity}`
175399
176746
  );
175400
176747
  }
175401
176748
  const supportedFeatures = state.attributes.supported_features ?? 0;
175402
- const cleanAreaRooms = await registry2.resolveCleanAreaRooms(
176749
+ const cleanAreaRooms = await registry3.resolveCleanAreaRooms(
175403
176750
  entityId,
175404
176751
  supportedFeatures
175405
176752
  );
@@ -175409,13 +176756,13 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175409
176756
  entityId: effectiveMapping?.entityId ?? entityId,
175410
176757
  cleanAreaRooms
175411
176758
  };
175412
- logger245.info(
176759
+ logger249.info(
175413
176760
  `${entityId}: Using ${cleanAreaRooms.length} HA areas via CLEAN_AREA`
175414
176761
  );
175415
176762
  }
175416
176763
  const vacAttrs = state.attributes;
175417
176764
  if (cleanAreaRooms.length === 0 && !vacAttrs.rooms && !vacAttrs.segments && !vacAttrs.room_mapping) {
175418
- const valetudoRooms = registry2.findValetudoMapSegments(
176765
+ const valetudoRooms = registry3.findValetudoMapSegments(
175419
176766
  entity.device_id
175420
176767
  );
175421
176768
  if (valetudoRooms.length > 0) {
@@ -175430,11 +176777,11 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175430
176777
  rooms: roomsObj
175431
176778
  }
175432
176779
  };
175433
- logger245.info(
176780
+ logger249.info(
175434
176781
  `${entityId}: Auto-detected ${valetudoRooms.length} Valetudo segments`
175435
176782
  );
175436
176783
  } else {
175437
- const roborockRooms = await registry2.resolveRoborockRooms(entityId);
176784
+ const roborockRooms = await registry3.resolveRoborockRooms(entityId);
175438
176785
  if (roborockRooms.length > 0) {
175439
176786
  const roomsObj = {};
175440
176787
  for (const r of roborockRooms) {
@@ -175447,14 +176794,14 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175447
176794
  rooms: roomsObj
175448
176795
  }
175449
176796
  };
175450
- logger245.info(
176797
+ logger249.info(
175451
176798
  `${entityId}: Auto-detected ${roborockRooms.length} Roborock rooms`
175452
176799
  );
175453
176800
  }
175454
176801
  }
175455
176802
  }
175456
176803
  } else {
175457
- logger245.warn(`${entityId}: No device_id, cannot auto-assign battery`);
176804
+ logger249.warn(`${entityId}: No device_id, cannot auto-assign battery`);
175458
176805
  }
175459
176806
  const payload = {
175460
176807
  entity_id: entityId,
@@ -175465,7 +176812,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175465
176812
  const vacAttrsForClean = state.attributes;
175466
176813
  let cleaningModeOptions;
175467
176814
  if (effectiveMapping?.cleaningModeEntity) {
175468
- const cmState = registry2.initialState(
176815
+ const cmState = registry3.initialState(
175469
176816
  effectiveMapping.cleaningModeEntity
175470
176817
  );
175471
176818
  cleaningModeOptions = cmState?.attributes?.options;
@@ -175485,7 +176832,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175485
176832
  customName,
175486
176833
  mapping: effectiveMapping
175487
176834
  },
175488
- registry2.isServerModeVacuumOnOffEnabled(),
176835
+ registry3.isServerModeVacuumOnOffEnabled(),
175489
176836
  cleaningModeOptions
175490
176837
  );
175491
176838
  if (!endpointType) {
@@ -175527,11 +176874,11 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175527
176874
  }
175528
176875
  if (mappedChanged) {
175529
176876
  this.pendingMappedChange = true;
175530
- logger245.debug(
176877
+ logger249.debug(
175531
176878
  `Mapped entity change detected for ${this.entityId}, forcing update`
175532
176879
  );
175533
176880
  }
175534
- logger245.debug(
176881
+ logger249.debug(
175535
176882
  `State update received for ${this.entityId}: state=${state.state}`
175536
176883
  );
175537
176884
  this.lastState = state;
@@ -175571,12 +176918,13 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
175571
176918
  // src/services/bridges/server-mode-endpoint-manager.ts
175572
176919
  var MAX_SERVER_MODE_DEVICES = 10;
175573
176920
  var ServerModeEndpointManager = class extends Service {
175574
- constructor(serverNode, client, registry2, mappingStorage, identityStorage, dataProvider, log) {
176921
+ constructor(serverNode, client, registry3, mappingStorage, identityStorage, dataProvider, log) {
175575
176922
  super("ServerModeEndpointManager");
175576
176923
  this.serverNode = serverNode;
175577
176924
  this.client = client;
175578
- this.registry = registry2;
176925
+ this.registry = registry3;
175579
176926
  this.mappingStorage = mappingStorage;
176927
+ this.identityStorage = identityStorage;
175580
176928
  this.dataProvider = dataProvider;
175581
176929
  this.log = log;
175582
176930
  this.identityResolver = new IdentityResolver(
@@ -175588,6 +176936,7 @@ var ServerModeEndpointManager = class extends Service {
175588
176936
  client;
175589
176937
  registry;
175590
176938
  mappingStorage;
176939
+ identityStorage;
175591
176940
  dataProvider;
175592
176941
  log;
175593
176942
  entityIds = [];
@@ -175687,6 +177036,17 @@ var ServerModeEndpointManager = class extends Service {
175687
177036
  this.registry.refresh();
175688
177037
  this._failedEntities = [];
175689
177038
  this.entityIds = this.registry.entityIds;
177039
+ const fullEntities = this.registry.fullEntities;
177040
+ stampIdentityPresence(
177041
+ this.identityStorage,
177042
+ this.dataProvider.id,
177043
+ buildPresentIdentityKeys(fullEntities)
177044
+ );
177045
+ stampMappingPresence(
177046
+ this.mappingStorage,
177047
+ this.dataProvider.id,
177048
+ buildPresentEntityIds(fullEntities)
177049
+ );
175690
177050
  try {
175691
177051
  if (this.entityIds.length === 0) {
175692
177052
  this.log.warn("Server mode bridge has no entities configured");
@@ -176096,10 +177456,10 @@ var BridgeEnvironmentFactory = class extends BridgeFactory {
176096
177456
  // src/core/ioc/app-environment.ts
176097
177457
  var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176098
177458
  constructor(rootEnv, options) {
176099
- const logger247 = rootEnv.get(LoggerService);
177459
+ const logger251 = rootEnv.get(LoggerService);
176100
177460
  super({
176101
177461
  id: "App",
176102
- log: logger247.get("AppContainer"),
177462
+ log: logger251.get("AppContainer"),
176103
177463
  parent: rootEnv
176104
177464
  });
176105
177465
  this.options = options;
@@ -176113,8 +177473,8 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176113
177473
  }
176114
177474
  construction;
176115
177475
  async init() {
176116
- const logger247 = this.get(LoggerService);
176117
- this.set(LoggerService, logger247);
177476
+ const logger251 = this.get(LoggerService);
177477
+ this.set(LoggerService, logger251);
176118
177478
  this.set(AppStorage, new AppStorage(await this.load(StorageService)));
176119
177479
  this.set(BridgeStorage, new BridgeStorage(await this.load(AppStorage)));
176120
177480
  this.set(
@@ -176135,7 +177495,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176135
177495
  );
176136
177496
  this.set(
176137
177497
  HomeAssistantClient,
176138
- new HomeAssistantClient(logger247, this.options.homeAssistant)
177498
+ new HomeAssistantClient(logger251, this.options.homeAssistant)
176139
177499
  );
176140
177500
  this.set(
176141
177501
  HomeAssistantConfig,
@@ -176143,7 +177503,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176143
177503
  );
176144
177504
  this.set(
176145
177505
  HomeAssistantActions,
176146
- new HomeAssistantActions(logger247, await this.load(HomeAssistantClient))
177506
+ new HomeAssistantActions(logger251, await this.load(HomeAssistantClient))
176147
177507
  );
176148
177508
  this.set(
176149
177509
  HomeAssistantRegistry,
@@ -176179,7 +177539,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176179
177539
  this.set(
176180
177540
  WebApi,
176181
177541
  new WebApi(
176182
- logger247,
177542
+ logger251,
176183
177543
  await this.load(BridgeService),
176184
177544
  await this.load(HomeAssistantClient),
176185
177545
  await this.load(HomeAssistantRegistry),
@@ -176200,7 +177560,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
176200
177560
 
176201
177561
  // src/matter/patches/patch-level-control-tlv.ts
176202
177562
  init_esm();
176203
- var logger246 = Logger.get("PatchLevelControlTlv");
177563
+ var logger250 = Logger.get("PatchLevelControlTlv");
176204
177564
  var optionalFieldModels = /* @__PURE__ */ new WeakSet();
176205
177565
  var fieldModelMandatoryGetterPatched = false;
176206
177566
  function patchLevelControlTlv() {
@@ -176256,11 +177616,11 @@ function patchLevelControlTlv() {
176256
177616
  patched++;
176257
177617
  }
176258
177618
  if (patched > 0) {
176259
- logger246.info(
177619
+ logger250.info(
176260
177620
  `Patched ${patched} LevelControl TLV schema(s): transitionTime is now optional (Google Home compatibility)`
176261
177621
  );
176262
177622
  } else {
176263
- logger246.warn(
177623
+ logger250.warn(
176264
177624
  "Failed to patch LevelControl TLV schemas, field definitions not found. Google Home brightness adjustment may not work.",
176265
177625
  {
176266
177626
  moveToLevel: describeFields(