@riddix/hamh 2.1.0-alpha.836 → 2.1.0-alpha.837

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.
@@ -135040,6 +135040,298 @@ function resolveLabelValue(value, labels) {
135040
135040
  return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
135041
135041
  }
135042
135042
 
135043
+ // src/matter/endpoints/entity-endpoint.ts
135044
+ init_esm7();
135045
+ var EntityEndpoint = class extends Endpoint {
135046
+ constructor(type, entityId, customName, mappedEntityIds, endpointId) {
135047
+ super(type, { id: endpointId ?? createEndpointId(entityId, customName) });
135048
+ this.entityId = entityId;
135049
+ this.mappedEntityIds = mappedEntityIds ?? [];
135050
+ }
135051
+ entityId;
135052
+ mappedEntityIds;
135053
+ lastMappedStates = {};
135054
+ hasMappedEntityChanged(states) {
135055
+ let changed = false;
135056
+ for (const mappedId of this.mappedEntityIds) {
135057
+ const mappedState = states[mappedId];
135058
+ if (!mappedState) continue;
135059
+ const fp = mappedState.state;
135060
+ if (fp !== this.lastMappedStates[mappedId]) {
135061
+ this.lastMappedStates[mappedId] = fp;
135062
+ changed = true;
135063
+ }
135064
+ }
135065
+ return changed;
135066
+ }
135067
+ };
135068
+ function createEndpointId(entityId, customName) {
135069
+ const baseName = customName || entityId;
135070
+ return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
135071
+ }
135072
+ function getMappedEntityIds(mapping) {
135073
+ if (!mapping) return [];
135074
+ const ids = [];
135075
+ if (mapping.batteryEntity && !mapping.disableBatteryMapping) {
135076
+ ids.push(mapping.batteryEntity);
135077
+ }
135078
+ if (mapping.faultEntity) ids.push(mapping.faultEntity);
135079
+ if (mapping.chargingStateEntity) ids.push(mapping.chargingStateEntity);
135080
+ if (mapping.temperatureEntity) ids.push(mapping.temperatureEntity);
135081
+ if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
135082
+ if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
135083
+ if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
135084
+ if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
135085
+ if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
135086
+ if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
135087
+ if (mapping.powerEntity) ids.push(mapping.powerEntity);
135088
+ if (mapping.energyEntity) ids.push(mapping.energyEntity);
135089
+ if (mapping.voltageEntity) ids.push(mapping.voltageEntity);
135090
+ if (mapping.currentEntity) ids.push(mapping.currentEntity);
135091
+ if (mapping.batteryPowerEntity) ids.push(mapping.batteryPowerEntity);
135092
+ if (mapping.batteryEnergyEntity) ids.push(mapping.batteryEnergyEntity);
135093
+ if (mapping.currentRoomEntity) ids.push(mapping.currentRoomEntity);
135094
+ if (mapping.cleanedAreaEntity) ids.push(mapping.cleanedAreaEntity);
135095
+ if (mapping.composedEntities) {
135096
+ for (const sub of mapping.composedEntities) {
135097
+ if (sub.entityId) ids.push(sub.entityId);
135098
+ }
135099
+ }
135100
+ return ids;
135101
+ }
135102
+
135103
+ // src/services/bridges/identity-resolver.ts
135104
+ var SEP = "\0";
135105
+ function identityKey(entity) {
135106
+ const uniqueId = entity.registry?.unique_id;
135107
+ const platform = entity.registry?.platform;
135108
+ if (!uniqueId || !platform) {
135109
+ return null;
135110
+ }
135111
+ const domain = entity.entity_id.split(".")[0];
135112
+ return platform + SEP + domain + SEP + uniqueId;
135113
+ }
135114
+ var IdentityResolver = class {
135115
+ constructor(identityStorage, mappingStorage) {
135116
+ this.identityStorage = identityStorage;
135117
+ this.mappingStorage = mappingStorage;
135118
+ }
135119
+ identityStorage;
135120
+ mappingStorage;
135121
+ async resolveIdentity(bridgeId, entity, mapping, opts) {
135122
+ const entityId = entity.entity_id;
135123
+ const key = identityKey(entity);
135124
+ if (key == null) {
135125
+ return {
135126
+ endpointId: createEndpointId(entityId, mapping?.customName),
135127
+ anchorEntityId: entityId,
135128
+ protected: false
135129
+ };
135130
+ }
135131
+ const existing = this.identityStorage.getIdentity(bridgeId, key);
135132
+ if (existing) {
135133
+ const renamedFrom = existing.lastEntityId !== entityId ? existing.lastEntityId ?? existing.anchorEntityId : void 0;
135134
+ if (opts.stableIdentity) {
135135
+ if (renamedFrom) {
135136
+ this.identityStorage.setIdentity(bridgeId, key, {
135137
+ ...existing,
135138
+ lastEntityId: entityId
135139
+ });
135140
+ await this.rekeyMapping(bridgeId, renamedFrom, entityId);
135141
+ }
135142
+ return {
135143
+ endpointId: existing.endpointId,
135144
+ anchorEntityId: existing.anchorEntityId,
135145
+ protected: true,
135146
+ renamedFrom
135147
+ };
135148
+ }
135149
+ const liveEndpointId = createEndpointId(entityId, mapping?.customName);
135150
+ if (existing.endpointId !== liveEndpointId || existing.anchorEntityId !== entityId || existing.lastEntityId !== entityId) {
135151
+ this.identityStorage.setIdentity(bridgeId, key, {
135152
+ ...existing,
135153
+ endpointId: liveEndpointId,
135154
+ anchorEntityId: entityId,
135155
+ lastEntityId: entityId
135156
+ });
135157
+ }
135158
+ return {
135159
+ endpointId: liveEndpointId,
135160
+ anchorEntityId: entityId,
135161
+ protected: false,
135162
+ renamedFrom
135163
+ };
135164
+ }
135165
+ const desired = createEndpointId(entityId, mapping?.customName);
135166
+ let endpointId = desired;
135167
+ if (opts.stableIdentity) {
135168
+ let n = 2;
135169
+ while (opts.isEndpointIdTaken(endpointId, key)) {
135170
+ endpointId = `${desired}_${n++}`;
135171
+ }
135172
+ }
135173
+ const record = {
135174
+ endpointId,
135175
+ anchorEntityId: entityId,
135176
+ lastEntityId: entityId,
135177
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
135178
+ };
135179
+ this.identityStorage.setIdentity(bridgeId, key, record);
135180
+ if (opts.stableIdentity) {
135181
+ return {
135182
+ endpointId: record.endpointId,
135183
+ anchorEntityId: record.anchorEntityId,
135184
+ protected: true
135185
+ };
135186
+ }
135187
+ return {
135188
+ endpointId: desired,
135189
+ anchorEntityId: entityId,
135190
+ protected: false
135191
+ };
135192
+ }
135193
+ // Carry a mapping from the old entity_id to the new one on rename. anchorEntityId
135194
+ // never changes, only the mapping key. Skips when no old mapping exists or a new
135195
+ // one already does, so a manual mapping under the new id is never clobbered.
135196
+ async rekeyMapping(bridgeId, oldEntityId, newEntityId) {
135197
+ if (oldEntityId === newEntityId) {
135198
+ return;
135199
+ }
135200
+ const old = this.mappingStorage.getMapping(bridgeId, oldEntityId);
135201
+ if (!old) {
135202
+ return;
135203
+ }
135204
+ if (this.mappingStorage.getMapping(bridgeId, newEntityId)) {
135205
+ return;
135206
+ }
135207
+ await this.mappingStorage.setMapping({
135208
+ ...old,
135209
+ bridgeId,
135210
+ entityId: newEntityId
135211
+ });
135212
+ await this.mappingStorage.deleteMapping(bridgeId, oldEntityId);
135213
+ }
135214
+ };
135215
+
135216
+ // src/services/storage/orphan-cleanup.ts
135217
+ var ORPHAN_TOMBSTONE_MS = 7 * 24 * 60 * 60 * 1e3;
135218
+ function buildPresentIdentityKeys(entities) {
135219
+ const keys3 = /* @__PURE__ */ new Set();
135220
+ for (const entity of Object.values(entities ?? {})) {
135221
+ if (!entity?.entity_id) continue;
135222
+ const key = identityKey({ entity_id: entity.entity_id, registry: entity });
135223
+ if (key != null) keys3.add(key);
135224
+ }
135225
+ return keys3;
135226
+ }
135227
+ function buildPresentEntityIds(entities) {
135228
+ const ids = /* @__PURE__ */ new Set();
135229
+ for (const entity of Object.values(entities ?? {})) {
135230
+ if (entity?.entity_id) ids.add(entity.entity_id);
135231
+ }
135232
+ return ids;
135233
+ }
135234
+ function stampIdentityPresence(store, bridgeId, presentKeys, now = Date.now()) {
135235
+ const nowIso = new Date(now).toISOString();
135236
+ for (const key of [...store.getBridgeIdentities(bridgeId).keys()]) {
135237
+ if (presentKeys.has(key)) {
135238
+ store.clearIdentityMissing(bridgeId, key);
135239
+ } else {
135240
+ store.markIdentityMissing(bridgeId, key, nowIso);
135241
+ }
135242
+ }
135243
+ }
135244
+ function stampMappingPresence(store, bridgeId, presentEntityIds, now = Date.now()) {
135245
+ const nowIso = new Date(now).toISOString();
135246
+ const entityIds = store.getMappingsForBridge(bridgeId).map((m) => m.entityId);
135247
+ for (const entityId of entityIds) {
135248
+ if (presentEntityIds.has(entityId)) {
135249
+ store.clearMappingMissing(bridgeId, entityId);
135250
+ } else {
135251
+ store.markMappingMissing(bridgeId, entityId, nowIso);
135252
+ }
135253
+ }
135254
+ }
135255
+ function computeOrphanCandidates(input) {
135256
+ const now = input.now ?? Date.now();
135257
+ const candidates = [];
135258
+ for (const [key, record] of input.identities) {
135259
+ if (!record.missingSince) continue;
135260
+ const missingFor = now - Date.parse(record.missingSince);
135261
+ if (!(missingFor >= ORPHAN_TOMBSTONE_MS)) continue;
135262
+ if (input.presentKeys.has(key)) continue;
135263
+ const lastEntityId = record.lastEntityId ?? record.anchorEntityId;
135264
+ candidates.push({
135265
+ identityKey: key,
135266
+ lastEntityId,
135267
+ missingSince: record.missingSince,
135268
+ // Only a true orphan mapping, not one a live entity holds under a reused
135269
+ // entity_id (a different identity key). Guards the sweep from clobbering
135270
+ // that live entity's custom mapping.
135271
+ hasMapping: input.hasMapping(lastEntityId) && !input.presentEntityIds.has(lastEntityId),
135272
+ kind: "identity"
135273
+ });
135274
+ }
135275
+ if (input.mappings) {
135276
+ const linkedByIdentity = /* @__PURE__ */ new Set();
135277
+ for (const candidate of candidates) {
135278
+ if (candidate.hasMapping) linkedByIdentity.add(candidate.lastEntityId);
135279
+ }
135280
+ for (const mapping of input.mappings) {
135281
+ if (!mapping.missingSince) continue;
135282
+ const missingFor = now - Date.parse(mapping.missingSince);
135283
+ if (!(missingFor >= ORPHAN_TOMBSTONE_MS)) continue;
135284
+ if (input.presentEntityIds.has(mapping.entityId)) continue;
135285
+ if (linkedByIdentity.has(mapping.entityId)) continue;
135286
+ candidates.push({
135287
+ identityKey: mapping.entityId,
135288
+ lastEntityId: mapping.entityId,
135289
+ missingSince: mapping.missingSince,
135290
+ hasMapping: true,
135291
+ kind: "mapping"
135292
+ });
135293
+ }
135294
+ }
135295
+ return candidates;
135296
+ }
135297
+ async function executeOrphanCleanup(input) {
135298
+ const candidates = computeOrphanCandidates({
135299
+ bridgeId: input.bridgeId,
135300
+ identities: input.identities,
135301
+ presentKeys: input.presentKeys,
135302
+ presentEntityIds: input.presentEntityIds,
135303
+ hasMapping: (entityId) => input.getMapping(input.bridgeId, entityId) != null,
135304
+ mappings: input.mappings,
135305
+ now: input.now
135306
+ });
135307
+ const byKey = new Map(candidates.map((c) => [c.identityKey, c]));
135308
+ const results = [];
135309
+ for (const key of input.requestedKeys) {
135310
+ const candidate = byKey.get(key);
135311
+ if (!candidate) {
135312
+ results.push({
135313
+ identityKey: key,
135314
+ deleted: false,
135315
+ reason: "not a current orphan candidate"
135316
+ });
135317
+ continue;
135318
+ }
135319
+ if (candidate.kind === "mapping") {
135320
+ if (!input.presentEntityIds.has(candidate.lastEntityId)) {
135321
+ await input.deleteMapping(input.bridgeId, candidate.lastEntityId);
135322
+ }
135323
+ results.push({ identityKey: key, deleted: true });
135324
+ continue;
135325
+ }
135326
+ if (candidate.hasMapping && !input.presentEntityIds.has(candidate.lastEntityId)) {
135327
+ await input.deleteMapping(input.bridgeId, candidate.lastEntityId);
135328
+ }
135329
+ await input.deleteIdentity(input.bridgeId, key);
135330
+ results.push({ identityKey: key, deleted: true });
135331
+ }
135332
+ return results;
135333
+ }
135334
+
135043
135335
  // src/utils/json/endpoint-to-json.ts
135044
135336
  function safeClone(value, depth = 0) {
135045
135337
  if (depth > 20 || value === null || value === void 0) return value;
@@ -135085,7 +135377,7 @@ function endpointToJson(endpoint, parentId) {
135085
135377
 
135086
135378
  // src/api/matter-api.ts
135087
135379
  var ajv = new Ajv();
135088
- function matterApi(bridgeService, haRegistry, identityStorage) {
135380
+ function matterApi(bridgeService, haRegistry, identityStorage, mappingStorage) {
135089
135381
  const router = express11.Router();
135090
135382
  router.get("/", (_, res) => {
135091
135383
  res.status(200).json({});
@@ -135188,6 +135480,64 @@ function matterApi(bridgeService, haRegistry, identityStorage) {
135188
135480
  });
135189
135481
  }
135190
135482
  });
135483
+ router.get("/bridges/:bridgeId/orphans", async (req, res) => {
135484
+ const bridgeId = req.params.bridgeId;
135485
+ const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
135486
+ if (!bridge) {
135487
+ res.status(404).send("Not Found");
135488
+ return;
135489
+ }
135490
+ if (!haRegistry || !identityStorage) {
135491
+ res.status(503).json({ error: "Orphan cleanup is not available" });
135492
+ return;
135493
+ }
135494
+ const presentKeys = buildPresentIdentityKeys(haRegistry.entities);
135495
+ const presentEntityIds = buildPresentEntityIds(haRegistry.entities);
135496
+ const candidates = computeOrphanCandidates({
135497
+ bridgeId,
135498
+ identities: identityStorage.getBridgeIdentities(bridgeId),
135499
+ presentKeys,
135500
+ presentEntityIds,
135501
+ hasMapping: (entityId) => mappingStorage?.getMapping(bridgeId, entityId) != null,
135502
+ mappings: mappingStorage?.getMappingsForBridge(bridgeId)
135503
+ });
135504
+ res.status(200).json({ candidates });
135505
+ });
135506
+ router.post(
135507
+ "/bridges/:bridgeId/actions/cleanup-orphans",
135508
+ async (req, res) => {
135509
+ const bridgeId = req.params.bridgeId;
135510
+ const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
135511
+ if (!bridge) {
135512
+ res.status(404).send("Not Found");
135513
+ return;
135514
+ }
135515
+ if (!haRegistry || !identityStorage || !mappingStorage) {
135516
+ res.status(503).json({ error: "Orphan cleanup is not available" });
135517
+ return;
135518
+ }
135519
+ const body = req.body;
135520
+ const requestedKeys = Array.isArray(body?.identityKeys) ? body.identityKeys.filter((k) => typeof k === "string") : [];
135521
+ try {
135522
+ const results = await executeOrphanCleanup({
135523
+ bridgeId,
135524
+ requestedKeys,
135525
+ identities: identityStorage.getBridgeIdentities(bridgeId),
135526
+ presentKeys: buildPresentIdentityKeys(haRegistry.entities),
135527
+ presentEntityIds: buildPresentEntityIds(haRegistry.entities),
135528
+ getMapping: (b, e) => mappingStorage.getMapping(b, e),
135529
+ deleteMapping: (b, e) => mappingStorage.deleteMapping(b, e),
135530
+ deleteIdentity: (b, k) => identityStorage.deleteIdentity(b, k),
135531
+ mappings: mappingStorage.getMappingsForBridge(bridgeId)
135532
+ });
135533
+ res.status(200).json({ results });
135534
+ } catch (e) {
135535
+ res.status(500).json({
135536
+ error: e instanceof Error ? e.message : "Unknown error"
135537
+ });
135538
+ }
135539
+ }
135540
+ );
135191
135541
  router.get("/bridges/:bridgeId/devices", async (req, res) => {
135192
135542
  const bridgeId = req.params.bridgeId;
135193
135543
  const bridge = bridgeService.bridges.find((b) => b.id === bridgeId);
@@ -137207,7 +137557,12 @@ var WebApi = class extends Service {
137207
137557
  const api = express17.Router();
137208
137558
  api.use(express17.json()).use(nocache()).use(
137209
137559
  "/matter",
137210
- matterApi(this.bridgeService, this.haRegistry, this.identityStorage)
137560
+ matterApi(
137561
+ this.bridgeService,
137562
+ this.haRegistry,
137563
+ this.identityStorage,
137564
+ this.mappingStorage
137565
+ )
137211
137566
  ).use(
137212
137567
  "/health",
137213
137568
  healthApi(
@@ -139028,11 +139383,37 @@ var EntityIdentityStorage = class extends Service {
139028
139383
  this.identities.delete(bridgeId);
139029
139384
  await this.flush();
139030
139385
  }
139386
+ // Drop a single identity record. Used by the manual orphan cleanup after the
139387
+ // 7-day tombstone; flush immediately since it is a user-initiated deletion.
139388
+ async deleteIdentity(bridgeId, key) {
139389
+ const bridgeMap = this.identities.get(bridgeId);
139390
+ if (bridgeMap?.delete(key)) {
139391
+ await this.flush();
139392
+ }
139393
+ }
139394
+ // Stamp the tombstone the first time a record's entity is absent from HA. Keeps
139395
+ // the first-seen time on later passes and no-ops once stamped, so a steady
139396
+ // absence does not churn the debounced persist.
139397
+ markIdentityMissing(bridgeId, key, nowIso) {
139398
+ const record = this.identities.get(bridgeId)?.get(key);
139399
+ if (!record || record.missingSince != null) return;
139400
+ record.missingSince = nowIso;
139401
+ this.schedulePersist();
139402
+ }
139403
+ // Clear the tombstone when the entity is present again. No-ops when there is
139404
+ // nothing to clear, so an all-present bridge writes nothing on refresh.
139405
+ clearIdentityMissing(bridgeId, key) {
139406
+ const record = this.identities.get(bridgeId)?.get(key);
139407
+ if (!record || record.missingSince == null) return;
139408
+ delete record.missingSince;
139409
+ this.schedulePersist();
139410
+ }
139031
139411
  };
139032
139412
 
139033
139413
  // src/services/storage/entity-mapping-storage.ts
139034
139414
  init_service();
139035
139415
  var CURRENT_VERSION2 = 1;
139416
+ var PERSIST_DEBOUNCE_MS2 = 500;
139036
139417
  var EntityMappingStorage = class extends Service {
139037
139418
  constructor(appStorage) {
139038
139419
  super("EntityMappingStorage");
@@ -139041,10 +139422,14 @@ var EntityMappingStorage = class extends Service {
139041
139422
  appStorage;
139042
139423
  storage;
139043
139424
  mappings = /* @__PURE__ */ new Map();
139425
+ persistTimer = null;
139044
139426
  async initialize() {
139045
139427
  this.storage = this.appStorage.createContext("entity-mappings");
139046
139428
  await this.load();
139047
139429
  }
139430
+ async dispose() {
139431
+ await this.flush();
139432
+ }
139048
139433
  async load() {
139049
139434
  const stored = await this.storage.get("data", {
139050
139435
  version: CURRENT_VERSION2,
@@ -139079,6 +139464,10 @@ var EntityMappingStorage = class extends Service {
139079
139464
  }
139080
139465
  }
139081
139466
  async persist() {
139467
+ if (this.persistTimer) {
139468
+ clearTimeout(this.persistTimer);
139469
+ this.persistTimer = null;
139470
+ }
139082
139471
  const data = {
139083
139472
  version: CURRENT_VERSION2,
139084
139473
  mappings: {}
@@ -139088,6 +139477,17 @@ var EntityMappingStorage = class extends Service {
139088
139477
  }
139089
139478
  await this.storage.set("data", data);
139090
139479
  }
139480
+ schedulePersist() {
139481
+ if (this.persistTimer) return;
139482
+ this.persistTimer = setTimeout(() => {
139483
+ this.persistTimer = null;
139484
+ void this.persist();
139485
+ }, PERSIST_DEBOUNCE_MS2);
139486
+ }
139487
+ // Flush any pending debounced write, used on dispose and by tests.
139488
+ async flush() {
139489
+ await this.persist();
139490
+ }
139091
139491
  getMappingsForBridge(bridgeId) {
139092
139492
  const bridgeMap = this.mappings.get(bridgeId);
139093
139493
  return bridgeMap ? Array.from(bridgeMap.values()) : [];
@@ -139181,6 +139581,23 @@ var EntityMappingStorage = class extends Service {
139181
139581
  this.mappings.delete(bridgeId);
139182
139582
  await this.persist();
139183
139583
  }
139584
+ // Stamp the tombstone the first time a mapping's entity is absent from HA.
139585
+ // Keeps the first-seen time on later passes and no-ops once stamped, so a
139586
+ // steady absence does not churn the debounced persist.
139587
+ markMappingMissing(bridgeId, entityId, nowIso) {
139588
+ const record = this.mappings.get(bridgeId)?.get(entityId);
139589
+ if (!record || record.missingSince != null) return;
139590
+ record.missingSince = nowIso;
139591
+ this.schedulePersist();
139592
+ }
139593
+ // Clear the tombstone when the entity is present again. No-ops when there is
139594
+ // nothing to clear, so an all-present bridge writes nothing on refresh.
139595
+ clearMappingMissing(bridgeId, entityId) {
139596
+ const record = this.mappings.get(bridgeId)?.get(entityId);
139597
+ if (!record || record.missingSince == null) return;
139598
+ delete record.missingSince;
139599
+ this.schedulePersist();
139600
+ }
139184
139601
  };
139185
139602
  function sanitizeVendorId(value) {
139186
139603
  if (value === void 0 || value === null || value === "") {
@@ -157888,66 +158305,6 @@ var AggregatorEndpoint2 = class extends Endpoint {
157888
158305
  }
157889
158306
  };
157890
158307
 
157891
- // src/matter/endpoints/entity-endpoint.ts
157892
- init_esm7();
157893
- var EntityEndpoint = class extends Endpoint {
157894
- constructor(type, entityId, customName, mappedEntityIds, endpointId) {
157895
- super(type, { id: endpointId ?? createEndpointId(entityId, customName) });
157896
- this.entityId = entityId;
157897
- this.mappedEntityIds = mappedEntityIds ?? [];
157898
- }
157899
- entityId;
157900
- mappedEntityIds;
157901
- lastMappedStates = {};
157902
- hasMappedEntityChanged(states) {
157903
- let changed = false;
157904
- for (const mappedId of this.mappedEntityIds) {
157905
- const mappedState = states[mappedId];
157906
- if (!mappedState) continue;
157907
- const fp = mappedState.state;
157908
- if (fp !== this.lastMappedStates[mappedId]) {
157909
- this.lastMappedStates[mappedId] = fp;
157910
- changed = true;
157911
- }
157912
- }
157913
- return changed;
157914
- }
157915
- };
157916
- function createEndpointId(entityId, customName) {
157917
- const baseName = customName || entityId;
157918
- return baseName.replace(/\./g, "_").replace(/\s+/g, "_");
157919
- }
157920
- function getMappedEntityIds(mapping) {
157921
- if (!mapping) return [];
157922
- const ids = [];
157923
- if (mapping.batteryEntity && !mapping.disableBatteryMapping) {
157924
- ids.push(mapping.batteryEntity);
157925
- }
157926
- if (mapping.faultEntity) ids.push(mapping.faultEntity);
157927
- if (mapping.chargingStateEntity) ids.push(mapping.chargingStateEntity);
157928
- if (mapping.temperatureEntity) ids.push(mapping.temperatureEntity);
157929
- if (mapping.humidityEntity) ids.push(mapping.humidityEntity);
157930
- if (mapping.pressureEntity) ids.push(mapping.pressureEntity);
157931
- if (mapping.cleaningModeEntity) ids.push(mapping.cleaningModeEntity);
157932
- if (mapping.suctionLevelEntity) ids.push(mapping.suctionLevelEntity);
157933
- if (mapping.mopIntensityEntity) ids.push(mapping.mopIntensityEntity);
157934
- if (mapping.filterLifeEntity) ids.push(mapping.filterLifeEntity);
157935
- if (mapping.powerEntity) ids.push(mapping.powerEntity);
157936
- if (mapping.energyEntity) ids.push(mapping.energyEntity);
157937
- if (mapping.voltageEntity) ids.push(mapping.voltageEntity);
157938
- if (mapping.currentEntity) ids.push(mapping.currentEntity);
157939
- if (mapping.batteryPowerEntity) ids.push(mapping.batteryPowerEntity);
157940
- if (mapping.batteryEnergyEntity) ids.push(mapping.batteryEnergyEntity);
157941
- if (mapping.currentRoomEntity) ids.push(mapping.currentRoomEntity);
157942
- if (mapping.cleanedAreaEntity) ids.push(mapping.cleanedAreaEntity);
157943
- if (mapping.composedEntities) {
157944
- for (const sub of mapping.composedEntities) {
157945
- if (sub.entityId) ids.push(sub.entityId);
157946
- }
157947
- }
157948
- return ids;
157949
- }
157950
-
157951
158308
  // src/matter/endpoints/legacy/legacy-endpoint.ts
157952
158309
  init_dist();
157953
158310
  init_esm();
@@ -172925,119 +173282,6 @@ var EntityIsolationServiceImpl = class {
172925
173282
  };
172926
173283
  var EntityIsolationService = new EntityIsolationServiceImpl();
172927
173284
 
172928
- // src/services/bridges/identity-resolver.ts
172929
- var SEP = "\0";
172930
- function identityKey(entity) {
172931
- const uniqueId = entity.registry?.unique_id;
172932
- const platform = entity.registry?.platform;
172933
- if (!uniqueId || !platform) {
172934
- return null;
172935
- }
172936
- const domain = entity.entity_id.split(".")[0];
172937
- return platform + SEP + domain + SEP + uniqueId;
172938
- }
172939
- var IdentityResolver = class {
172940
- constructor(identityStorage, mappingStorage) {
172941
- this.identityStorage = identityStorage;
172942
- this.mappingStorage = mappingStorage;
172943
- }
172944
- identityStorage;
172945
- mappingStorage;
172946
- async resolveIdentity(bridgeId, entity, mapping, opts) {
172947
- const entityId = entity.entity_id;
172948
- const key = identityKey(entity);
172949
- if (key == null) {
172950
- return {
172951
- endpointId: createEndpointId(entityId, mapping?.customName),
172952
- anchorEntityId: entityId,
172953
- protected: false
172954
- };
172955
- }
172956
- const existing = this.identityStorage.getIdentity(bridgeId, key);
172957
- if (existing) {
172958
- const renamedFrom = existing.lastEntityId !== entityId ? existing.lastEntityId ?? existing.anchorEntityId : void 0;
172959
- if (opts.stableIdentity) {
172960
- if (renamedFrom) {
172961
- this.identityStorage.setIdentity(bridgeId, key, {
172962
- ...existing,
172963
- lastEntityId: entityId
172964
- });
172965
- await this.rekeyMapping(bridgeId, renamedFrom, entityId);
172966
- }
172967
- return {
172968
- endpointId: existing.endpointId,
172969
- anchorEntityId: existing.anchorEntityId,
172970
- protected: true,
172971
- renamedFrom
172972
- };
172973
- }
172974
- const liveEndpointId = createEndpointId(entityId, mapping?.customName);
172975
- if (existing.endpointId !== liveEndpointId || existing.anchorEntityId !== entityId || existing.lastEntityId !== entityId) {
172976
- this.identityStorage.setIdentity(bridgeId, key, {
172977
- ...existing,
172978
- endpointId: liveEndpointId,
172979
- anchorEntityId: entityId,
172980
- lastEntityId: entityId
172981
- });
172982
- }
172983
- return {
172984
- endpointId: liveEndpointId,
172985
- anchorEntityId: entityId,
172986
- protected: false,
172987
- renamedFrom
172988
- };
172989
- }
172990
- const desired = createEndpointId(entityId, mapping?.customName);
172991
- let endpointId = desired;
172992
- if (opts.stableIdentity) {
172993
- let n = 2;
172994
- while (opts.isEndpointIdTaken(endpointId, key)) {
172995
- endpointId = `${desired}_${n++}`;
172996
- }
172997
- }
172998
- const record = {
172999
- endpointId,
173000
- anchorEntityId: entityId,
173001
- lastEntityId: entityId,
173002
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
173003
- };
173004
- this.identityStorage.setIdentity(bridgeId, key, record);
173005
- if (opts.stableIdentity) {
173006
- return {
173007
- endpointId: record.endpointId,
173008
- anchorEntityId: record.anchorEntityId,
173009
- protected: true
173010
- };
173011
- }
173012
- return {
173013
- endpointId: desired,
173014
- anchorEntityId: entityId,
173015
- protected: false
173016
- };
173017
- }
173018
- // Carry a mapping from the old entity_id to the new one on rename. anchorEntityId
173019
- // never changes, only the mapping key. Skips when no old mapping exists or a new
173020
- // one already does, so a manual mapping under the new id is never clobbered.
173021
- async rekeyMapping(bridgeId, oldEntityId, newEntityId) {
173022
- if (oldEntityId === newEntityId) {
173023
- return;
173024
- }
173025
- const old = this.mappingStorage.getMapping(bridgeId, oldEntityId);
173026
- if (!old) {
173027
- return;
173028
- }
173029
- if (this.mappingStorage.getMapping(bridgeId, newEntityId)) {
173030
- return;
173031
- }
173032
- await this.mappingStorage.setMapping({
173033
- ...old,
173034
- bridgeId,
173035
- entityId: newEntityId
173036
- });
173037
- await this.mappingStorage.deleteMapping(bridgeId, oldEntityId);
173038
- }
173039
- };
173040
-
173041
173285
  // src/services/bridges/bridge-endpoint-manager.ts
173042
173286
  var MAX_ENTITY_ID_LENGTH = 150;
173043
173287
  var ENDPOINT_REMOVAL_GRACE_MS = 6e4;
@@ -173047,6 +173291,7 @@ var BridgeEndpointManager = class extends Service {
173047
173291
  this.client = client;
173048
173292
  this.registry = registry2;
173049
173293
  this.mappingStorage = mappingStorage;
173294
+ this.identityStorage = identityStorage;
173050
173295
  this.bridgeId = bridgeId;
173051
173296
  this.log = log;
173052
173297
  this.pluginManager = pluginManager;
@@ -173068,6 +173313,7 @@ var BridgeEndpointManager = class extends Service {
173068
173313
  client;
173069
173314
  registry;
173070
173315
  mappingStorage;
173316
+ identityStorage;
173071
173317
  bridgeId;
173072
173318
  log;
173073
173319
  pluginManager;
@@ -173465,6 +173711,17 @@ var BridgeEndpointManager = class extends Service {
173465
173711
  this.mappingFingerprints.set(entityId, fp);
173466
173712
  }
173467
173713
  }
173714
+ const fullEntities = this.registry.fullEntities;
173715
+ stampIdentityPresence(
173716
+ this.identityStorage,
173717
+ this.bridgeId,
173718
+ buildPresentIdentityKeys(fullEntities)
173719
+ );
173720
+ stampMappingPresence(
173721
+ this.mappingStorage,
173722
+ this.bridgeId,
173723
+ buildPresentEntityIds(fullEntities)
173724
+ );
173468
173725
  const existingEndpoints = [];
173469
173726
  const now = Date.now();
173470
173727
  for (const endpoint of endpoints) {
@@ -173784,6 +174041,11 @@ var BridgeRegistry = class _BridgeRegistry {
173784
174041
  initialState(entityId) {
173785
174042
  return this._states[entityId];
173786
174043
  }
174044
+ // The complete HA entity set (unfiltered). Used by orphan tombstone stamping
174045
+ // so a filter change or a scope narrowing never looks like a removal.
174046
+ get fullEntities() {
174047
+ return this.registry.entities;
174048
+ }
173787
174049
  // composed sub-entities may sit outside the bridge filter (#408), so these
173788
174050
  // fall back to the full HA registry. keep them separate from the strict
173789
174051
  // accessors above, every other caller must stay filtered.
@@ -175805,6 +176067,7 @@ var ServerModeEndpointManager = class extends Service {
175805
176067
  this.client = client;
175806
176068
  this.registry = registry2;
175807
176069
  this.mappingStorage = mappingStorage;
176070
+ this.identityStorage = identityStorage;
175808
176071
  this.dataProvider = dataProvider;
175809
176072
  this.log = log;
175810
176073
  this.identityResolver = new IdentityResolver(
@@ -175816,6 +176079,7 @@ var ServerModeEndpointManager = class extends Service {
175816
176079
  client;
175817
176080
  registry;
175818
176081
  mappingStorage;
176082
+ identityStorage;
175819
176083
  dataProvider;
175820
176084
  log;
175821
176085
  entityIds = [];
@@ -175915,6 +176179,17 @@ var ServerModeEndpointManager = class extends Service {
175915
176179
  this.registry.refresh();
175916
176180
  this._failedEntities = [];
175917
176181
  this.entityIds = this.registry.entityIds;
176182
+ const fullEntities = this.registry.fullEntities;
176183
+ stampIdentityPresence(
176184
+ this.identityStorage,
176185
+ this.dataProvider.id,
176186
+ buildPresentIdentityKeys(fullEntities)
176187
+ );
176188
+ stampMappingPresence(
176189
+ this.mappingStorage,
176190
+ this.dataProvider.id,
176191
+ buildPresentEntityIds(fullEntities)
176192
+ );
175918
176193
  try {
175919
176194
  if (this.entityIds.length === 0) {
175920
176195
  this.log.warn("Server mode bridge has no entities configured");