@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.
package/dist/backend/cli.js
CHANGED
|
@@ -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(
|
|
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");
|