@riddix/hamh 2.1.0-alpha.829 → 2.1.0-alpha.831
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
|
@@ -131529,6 +131529,12 @@ var init_bridge_config_schema = __esm({
|
|
|
131529
131529
|
description: "When a controller drops all subscriptions, clean up the dead session and re-announce after 5 seconds instead of 60. Opt-in for Google Home users whose devices go offline after a cancelled subscription (#386). It shortens the offline window but cannot stop the controller from rejecting the subscription. Default off.",
|
|
131530
131530
|
type: "boolean",
|
|
131531
131531
|
default: false
|
|
131532
|
+
},
|
|
131533
|
+
stableIdentity: {
|
|
131534
|
+
title: "Stable Device Identity",
|
|
131535
|
+
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.",
|
|
131536
|
+
type: "boolean",
|
|
131537
|
+
default: false
|
|
131532
131538
|
}
|
|
131533
131539
|
}
|
|
131534
131540
|
};
|
|
@@ -132293,6 +132299,12 @@ var init_home_assistant_entity_behavior = __esm({
|
|
|
132293
132299
|
customName;
|
|
132294
132300
|
/** Entity mapping configuration (optional, used for advanced features like filter life sensor) */
|
|
132295
132301
|
mapping;
|
|
132302
|
+
/**
|
|
132303
|
+
* Stable identity anchor for uniqueId/serialNumber. Set to the entity_id the
|
|
132304
|
+
* identity was first seeded under, so those stay frozen across HA renames.
|
|
132305
|
+
* Undefined falls back to the live entity_id (legacy behaviour).
|
|
132306
|
+
*/
|
|
132307
|
+
identityAnchor;
|
|
132296
132308
|
}
|
|
132297
132309
|
HomeAssistantEntityBehavior2.State = State;
|
|
132298
132310
|
class Events2 extends EventEmitter {
|
|
@@ -133576,7 +133588,7 @@ function diagnosticApi(bridgeService, haClient, haRegistry, version2, startTime)
|
|
|
133576
133588
|
|
|
133577
133589
|
// src/api/entity-mapping-api.ts
|
|
133578
133590
|
import express7 from "express";
|
|
133579
|
-
function entityMappingApi(mappingStorage) {
|
|
133591
|
+
function entityMappingApi(mappingStorage, identityStorage) {
|
|
133580
133592
|
const router = express7.Router();
|
|
133581
133593
|
router.get("/:bridgeId", (req, res) => {
|
|
133582
133594
|
const { bridgeId } = req.params;
|
|
@@ -133657,6 +133669,7 @@ function entityMappingApi(mappingStorage) {
|
|
|
133657
133669
|
router.delete("/:bridgeId", async (req, res) => {
|
|
133658
133670
|
const { bridgeId } = req.params;
|
|
133659
133671
|
await mappingStorage.deleteBridgeMappings(bridgeId);
|
|
133672
|
+
await identityStorage.deleteBridgeIdentities(bridgeId);
|
|
133660
133673
|
res.status(204).send();
|
|
133661
133674
|
});
|
|
133662
133675
|
return router;
|
|
@@ -134523,7 +134536,7 @@ function endpointToJson(endpoint, parentId) {
|
|
|
134523
134536
|
|
|
134524
134537
|
// src/api/matter-api.ts
|
|
134525
134538
|
var ajv = new Ajv();
|
|
134526
|
-
function matterApi(bridgeService, haRegistry) {
|
|
134539
|
+
function matterApi(bridgeService, haRegistry, identityStorage) {
|
|
134527
134540
|
const router = express11.Router();
|
|
134528
134541
|
router.get("/", (_, res) => {
|
|
134529
134542
|
res.status(200).json({});
|
|
@@ -134600,6 +134613,7 @@ function matterApi(bridgeService, haRegistry) {
|
|
|
134600
134613
|
const bridgeId = req.params.bridgeId;
|
|
134601
134614
|
try {
|
|
134602
134615
|
await bridgeService.delete(bridgeId);
|
|
134616
|
+
await identityStorage?.deleteBridgeIdentities(bridgeId);
|
|
134603
134617
|
res.status(204).send();
|
|
134604
134618
|
} catch (e) {
|
|
134605
134619
|
res.status(500).json({
|
|
@@ -134615,6 +134629,7 @@ function matterApi(bridgeService, haRegistry) {
|
|
|
134615
134629
|
return;
|
|
134616
134630
|
}
|
|
134617
134631
|
try {
|
|
134632
|
+
await identityStorage?.deleteBridgeIdentities(bridgeId);
|
|
134618
134633
|
await bridge.factoryReset();
|
|
134619
134634
|
await bridge.start();
|
|
134620
134635
|
res.status(200).json(bridge.data);
|
|
@@ -136597,13 +136612,14 @@ var WebSocketApi = class {
|
|
|
136597
136612
|
|
|
136598
136613
|
// src/api/web-api.ts
|
|
136599
136614
|
var WebApi = class extends Service {
|
|
136600
|
-
constructor(logger245, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, lockCredentialStorage, settingsStorage, backupService, props) {
|
|
136615
|
+
constructor(logger245, bridgeService, haClient, haRegistry, bridgeStorage, mappingStorage, identityStorage, lockCredentialStorage, settingsStorage, backupService, props) {
|
|
136601
136616
|
super("WebApi");
|
|
136602
136617
|
this.bridgeService = bridgeService;
|
|
136603
136618
|
this.haClient = haClient;
|
|
136604
136619
|
this.haRegistry = haRegistry;
|
|
136605
136620
|
this.bridgeStorage = bridgeStorage;
|
|
136606
136621
|
this.mappingStorage = mappingStorage;
|
|
136622
|
+
this.identityStorage = identityStorage;
|
|
136607
136623
|
this.lockCredentialStorage = lockCredentialStorage;
|
|
136608
136624
|
this.settingsStorage = settingsStorage;
|
|
136609
136625
|
this.backupService = backupService;
|
|
@@ -136623,6 +136639,7 @@ var WebApi = class extends Service {
|
|
|
136623
136639
|
haRegistry;
|
|
136624
136640
|
bridgeStorage;
|
|
136625
136641
|
mappingStorage;
|
|
136642
|
+
identityStorage;
|
|
136626
136643
|
lockCredentialStorage;
|
|
136627
136644
|
settingsStorage;
|
|
136628
136645
|
backupService;
|
|
@@ -136639,7 +136656,10 @@ var WebApi = class extends Service {
|
|
|
136639
136656
|
}
|
|
136640
136657
|
async initialize() {
|
|
136641
136658
|
const api = express17.Router();
|
|
136642
|
-
api.use(express17.json()).use(nocache()).use(
|
|
136659
|
+
api.use(express17.json()).use(nocache()).use(
|
|
136660
|
+
"/matter",
|
|
136661
|
+
matterApi(this.bridgeService, this.haRegistry, this.identityStorage)
|
|
136662
|
+
).use(
|
|
136643
136663
|
"/health",
|
|
136644
136664
|
healthApi(
|
|
136645
136665
|
this.bridgeService,
|
|
@@ -136650,7 +136670,10 @@ var WebApi = class extends Service {
|
|
|
136650
136670
|
).use("/bridges", bridgeExportApi(this.bridgeStorage)).use("/bridge-icons", bridgeIconApi(this.props.storageLocation)).use(
|
|
136651
136671
|
"/device-images",
|
|
136652
136672
|
deviceImageApi(this.props.storageLocation, this.haRegistry)
|
|
136653
|
-
).use(
|
|
136673
|
+
).use(
|
|
136674
|
+
"/entity-mappings",
|
|
136675
|
+
entityMappingApi(this.mappingStorage, this.identityStorage)
|
|
136676
|
+
).use("/mapping-profiles", mappingProfileApi(this.mappingStorage)).use("/lock-credentials", lockCredentialApi(this.lockCredentialStorage)).use(
|
|
136654
136677
|
"/settings",
|
|
136655
136678
|
settingsApi(this.settingsStorage, this.bridgeService, this.props.auth)
|
|
136656
136679
|
).use(
|
|
@@ -138355,9 +138378,112 @@ var BridgeStorage = class extends Service {
|
|
|
138355
138378
|
}
|
|
138356
138379
|
};
|
|
138357
138380
|
|
|
138358
|
-
// src/services/storage/entity-
|
|
138381
|
+
// src/services/storage/entity-identity-storage.ts
|
|
138359
138382
|
init_service();
|
|
138360
138383
|
var CURRENT_VERSION = 1;
|
|
138384
|
+
var PERSIST_DEBOUNCE_MS = 500;
|
|
138385
|
+
var EntityIdentityStorage = class extends Service {
|
|
138386
|
+
constructor(appStorage) {
|
|
138387
|
+
super("EntityIdentityStorage");
|
|
138388
|
+
this.appStorage = appStorage;
|
|
138389
|
+
}
|
|
138390
|
+
appStorage;
|
|
138391
|
+
storage;
|
|
138392
|
+
identities = /* @__PURE__ */ new Map();
|
|
138393
|
+
persistTimer = null;
|
|
138394
|
+
async initialize() {
|
|
138395
|
+
this.storage = this.appStorage.createContext("entity-identities");
|
|
138396
|
+
await this.load();
|
|
138397
|
+
}
|
|
138398
|
+
async dispose() {
|
|
138399
|
+
await this.flush();
|
|
138400
|
+
}
|
|
138401
|
+
async load() {
|
|
138402
|
+
const stored = await this.storage.get("data", {
|
|
138403
|
+
version: CURRENT_VERSION,
|
|
138404
|
+
identities: {}
|
|
138405
|
+
});
|
|
138406
|
+
if (!stored || Object.keys(stored).length === 0) {
|
|
138407
|
+
return;
|
|
138408
|
+
}
|
|
138409
|
+
const data = stored;
|
|
138410
|
+
if (data.version !== CURRENT_VERSION) {
|
|
138411
|
+
await this.migrate(data);
|
|
138412
|
+
return;
|
|
138413
|
+
}
|
|
138414
|
+
for (const [bridgeId, records] of Object.entries(data.identities)) {
|
|
138415
|
+
const bridgeMap = /* @__PURE__ */ new Map();
|
|
138416
|
+
for (const [key, record] of Object.entries(records)) {
|
|
138417
|
+
bridgeMap.set(key, record);
|
|
138418
|
+
}
|
|
138419
|
+
this.identities.set(bridgeId, bridgeMap);
|
|
138420
|
+
}
|
|
138421
|
+
}
|
|
138422
|
+
async migrate(data) {
|
|
138423
|
+
if (data.version < CURRENT_VERSION) {
|
|
138424
|
+
for (const [bridgeId, records] of Object.entries(data.identities)) {
|
|
138425
|
+
const bridgeMap = /* @__PURE__ */ new Map();
|
|
138426
|
+
for (const [key, record] of Object.entries(records)) {
|
|
138427
|
+
bridgeMap.set(key, record);
|
|
138428
|
+
}
|
|
138429
|
+
this.identities.set(bridgeId, bridgeMap);
|
|
138430
|
+
}
|
|
138431
|
+
await this.persist();
|
|
138432
|
+
}
|
|
138433
|
+
}
|
|
138434
|
+
async persist() {
|
|
138435
|
+
const data = {
|
|
138436
|
+
version: CURRENT_VERSION,
|
|
138437
|
+
identities: {}
|
|
138438
|
+
};
|
|
138439
|
+
for (const [bridgeId, bridgeMap] of this.identities) {
|
|
138440
|
+
const records = {};
|
|
138441
|
+
for (const [key, record] of bridgeMap) {
|
|
138442
|
+
records[key] = record;
|
|
138443
|
+
}
|
|
138444
|
+
data.identities[bridgeId] = records;
|
|
138445
|
+
}
|
|
138446
|
+
await this.storage.set("data", data);
|
|
138447
|
+
}
|
|
138448
|
+
schedulePersist() {
|
|
138449
|
+
if (this.persistTimer) return;
|
|
138450
|
+
this.persistTimer = setTimeout(() => {
|
|
138451
|
+
this.persistTimer = null;
|
|
138452
|
+
void this.persist();
|
|
138453
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
138454
|
+
}
|
|
138455
|
+
// Flush any pending debounced write, used on dispose and by tests.
|
|
138456
|
+
async flush() {
|
|
138457
|
+
if (this.persistTimer) {
|
|
138458
|
+
clearTimeout(this.persistTimer);
|
|
138459
|
+
this.persistTimer = null;
|
|
138460
|
+
}
|
|
138461
|
+
await this.persist();
|
|
138462
|
+
}
|
|
138463
|
+
getIdentity(bridgeId, key) {
|
|
138464
|
+
return this.identities.get(bridgeId)?.get(key);
|
|
138465
|
+
}
|
|
138466
|
+
getBridgeIdentities(bridgeId) {
|
|
138467
|
+
return this.identities.get(bridgeId) ?? /* @__PURE__ */ new Map();
|
|
138468
|
+
}
|
|
138469
|
+
setIdentity(bridgeId, key, record) {
|
|
138470
|
+
let bridgeMap = this.identities.get(bridgeId);
|
|
138471
|
+
if (!bridgeMap) {
|
|
138472
|
+
bridgeMap = /* @__PURE__ */ new Map();
|
|
138473
|
+
this.identities.set(bridgeId, bridgeMap);
|
|
138474
|
+
}
|
|
138475
|
+
bridgeMap.set(key, record);
|
|
138476
|
+
this.schedulePersist();
|
|
138477
|
+
}
|
|
138478
|
+
async deleteBridgeIdentities(bridgeId) {
|
|
138479
|
+
this.identities.delete(bridgeId);
|
|
138480
|
+
await this.flush();
|
|
138481
|
+
}
|
|
138482
|
+
};
|
|
138483
|
+
|
|
138484
|
+
// src/services/storage/entity-mapping-storage.ts
|
|
138485
|
+
init_service();
|
|
138486
|
+
var CURRENT_VERSION2 = 1;
|
|
138361
138487
|
var EntityMappingStorage = class extends Service {
|
|
138362
138488
|
constructor(appStorage) {
|
|
138363
138489
|
super("EntityMappingStorage");
|
|
@@ -138372,14 +138498,14 @@ var EntityMappingStorage = class extends Service {
|
|
|
138372
138498
|
}
|
|
138373
138499
|
async load() {
|
|
138374
138500
|
const stored = await this.storage.get("data", {
|
|
138375
|
-
version:
|
|
138501
|
+
version: CURRENT_VERSION2,
|
|
138376
138502
|
mappings: {}
|
|
138377
138503
|
});
|
|
138378
138504
|
if (!stored || Object.keys(stored).length === 0) {
|
|
138379
138505
|
return;
|
|
138380
138506
|
}
|
|
138381
138507
|
const data = stored;
|
|
138382
|
-
if (data.version !==
|
|
138508
|
+
if (data.version !== CURRENT_VERSION2) {
|
|
138383
138509
|
await this.migrate(data);
|
|
138384
138510
|
return;
|
|
138385
138511
|
}
|
|
@@ -138392,7 +138518,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
138392
138518
|
}
|
|
138393
138519
|
}
|
|
138394
138520
|
async migrate(data) {
|
|
138395
|
-
if (data.version <
|
|
138521
|
+
if (data.version < CURRENT_VERSION2) {
|
|
138396
138522
|
for (const [bridgeId, configs] of Object.entries(data.mappings)) {
|
|
138397
138523
|
const bridgeMap = /* @__PURE__ */ new Map();
|
|
138398
138524
|
for (const config11 of configs) {
|
|
@@ -138405,7 +138531,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
138405
138531
|
}
|
|
138406
138532
|
async persist() {
|
|
138407
138533
|
const data = {
|
|
138408
|
-
version:
|
|
138534
|
+
version: CURRENT_VERSION2,
|
|
138409
138535
|
mappings: {}
|
|
138410
138536
|
};
|
|
138411
138537
|
for (const [bridgeId, bridgeMap] of this.mappings) {
|
|
@@ -138565,7 +138691,7 @@ function sanitizeThrottleMs(value) {
|
|
|
138565
138691
|
// src/services/storage/lock-credential-storage.ts
|
|
138566
138692
|
init_service();
|
|
138567
138693
|
import { pbkdf2Sync, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
138568
|
-
var
|
|
138694
|
+
var CURRENT_VERSION3 = 2;
|
|
138569
138695
|
var HASH_ITERATIONS = 1e5;
|
|
138570
138696
|
var HASH_KEY_LENGTH = 64;
|
|
138571
138697
|
var HASH_ALGORITHM = "sha512";
|
|
@@ -138584,14 +138710,14 @@ var LockCredentialStorage = class extends Service {
|
|
|
138584
138710
|
}
|
|
138585
138711
|
async load() {
|
|
138586
138712
|
const stored = await this.storage.get("data", {
|
|
138587
|
-
version:
|
|
138713
|
+
version: CURRENT_VERSION3,
|
|
138588
138714
|
credentials: []
|
|
138589
138715
|
});
|
|
138590
138716
|
if (!stored || Object.keys(stored).length === 0) {
|
|
138591
138717
|
return;
|
|
138592
138718
|
}
|
|
138593
138719
|
const data = stored;
|
|
138594
|
-
if (data.version !==
|
|
138720
|
+
if (data.version !== CURRENT_VERSION3) {
|
|
138595
138721
|
await this.migrate(data);
|
|
138596
138722
|
return;
|
|
138597
138723
|
}
|
|
@@ -138620,7 +138746,7 @@ var LockCredentialStorage = class extends Service {
|
|
|
138620
138746
|
}
|
|
138621
138747
|
async persist() {
|
|
138622
138748
|
const data = {
|
|
138623
|
-
version:
|
|
138749
|
+
version: CURRENT_VERSION3,
|
|
138624
138750
|
credentials: Array.from(this.credentials.values())
|
|
138625
138751
|
};
|
|
138626
138752
|
await this.storage.set("data", data);
|
|
@@ -156975,8 +157101,8 @@ var AggregatorEndpoint2 = class extends Endpoint {
|
|
|
156975
157101
|
// src/matter/endpoints/entity-endpoint.ts
|
|
156976
157102
|
init_esm7();
|
|
156977
157103
|
var EntityEndpoint = class extends Endpoint {
|
|
156978
|
-
constructor(type, entityId, customName, mappedEntityIds) {
|
|
156979
|
-
super(type, { id: createEndpointId(entityId, customName) });
|
|
157104
|
+
constructor(type, entityId, customName, mappedEntityIds, endpointId) {
|
|
157105
|
+
super(type, { id: endpointId ?? createEndpointId(entityId, customName) });
|
|
156980
157106
|
this.entityId = entityId;
|
|
156981
157107
|
this.mappedEntityIds = mappedEntityIds ?? [];
|
|
156982
157108
|
}
|
|
@@ -157262,13 +157388,14 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
|
|
|
157262
157388
|
const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
|
|
157263
157389
|
const device = entity.deviceRegistry;
|
|
157264
157390
|
const mapping = homeAssistant.state.mapping;
|
|
157391
|
+
const anchor = homeAssistant.state.identityAnchor ?? entity.entity_id;
|
|
157265
157392
|
const registryName = featureFlags?.preferEntityRegistryName ? entity.registry?.name ?? entity.registry?.original_name : void 0;
|
|
157266
157393
|
const nodeLabel = ellipse(32, homeAssistant.state.customName) ?? ellipse(32, registryName) ?? ellipse(32, entity.state?.attributes?.friendly_name) ?? ellipse(32, entity.entity_id);
|
|
157267
157394
|
const productNameFromNodeLabel = featureFlags?.productNameFromNodeLabel === true ? ellipse(32, sanitizeMatterString(nodeLabel ?? "")) ?? void 0 : void 0;
|
|
157268
157395
|
const serialNumberSuffix = this.env.get(BridgeDataProvider).serialNumberSuffix;
|
|
157269
157396
|
const maxRawLen = 32 - (serialNumberSuffix?.length ?? 0);
|
|
157270
157397
|
const registrySerial = featureFlags?.useHaRegistrySerial ? ellipse(maxRawLen, device?.serial_number) : void 0;
|
|
157271
|
-
const rawSerial = ellipse(maxRawLen, mapping?.customSerialNumber) ?? registrySerial ?? hash(maxRawLen,
|
|
157398
|
+
const rawSerial = ellipse(maxRawLen, mapping?.customSerialNumber) ?? registrySerial ?? hash(maxRawLen, anchor);
|
|
157272
157399
|
const serialNumber = rawSerial && serialNumberSuffix ? `${rawSerial}${serialNumberSuffix}` : rawSerial;
|
|
157273
157400
|
const customVendorId = mapping?.customVendorId;
|
|
157274
157401
|
const vendorId3 = isValidVendorId(customVendorId) ? customVendorId : basicInformation.vendorId;
|
|
@@ -157288,18 +157415,18 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
|
|
|
157288
157415
|
// multiple fabric connections. Using MD5 hash of entity_id for stability.
|
|
157289
157416
|
// uniqueIdSuffix mints a fresh identity so stale controller cloud
|
|
157290
157417
|
// records keyed on it can be shed (#385).
|
|
157291
|
-
uniqueId: this.frozenUniqueId(
|
|
157418
|
+
uniqueId: this.frozenUniqueId(anchor)
|
|
157292
157419
|
});
|
|
157293
157420
|
logger201.debug(
|
|
157294
157421
|
`[${entity.entity_id}] basicInfo vendor=${this.state.vendorName} product=${this.state.productName} label=${this.state.productLabel} serial=${this.state.serialNumber} node=${this.state.nodeLabel} uniqueId=${this.state.uniqueId}`
|
|
157295
157422
|
);
|
|
157296
157423
|
}
|
|
157297
|
-
frozenUniqueId(
|
|
157424
|
+
frozenUniqueId(anchor) {
|
|
157298
157425
|
const provider = this.env.get(BridgeDataProvider);
|
|
157299
|
-
const key = `${provider.id}:${
|
|
157426
|
+
const key = `${provider.id}:${anchor}`;
|
|
157300
157427
|
let uniqueId = appliedUniqueIds.get(key);
|
|
157301
157428
|
if (uniqueId == null) {
|
|
157302
|
-
uniqueId = crypto6.createHash("md5").update(
|
|
157429
|
+
uniqueId = crypto6.createHash("md5").update(anchor + (provider.uniqueIdSuffix ?? "")).digest("hex").substring(0, 32);
|
|
157303
157430
|
appliedUniqueIds.set(key, uniqueId);
|
|
157304
157431
|
}
|
|
157305
157432
|
return uniqueId;
|
|
@@ -159071,7 +159198,7 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
|
|
|
159071
159198
|
})
|
|
159072
159199
|
);
|
|
159073
159200
|
}
|
|
159074
|
-
const endpointId = createEndpointId2(primaryEntityId, config11.customName);
|
|
159201
|
+
const endpointId = config11.endpointId ?? createEndpointId2(primaryEntityId, config11.customName);
|
|
159075
159202
|
const parts = [];
|
|
159076
159203
|
const airPurifierSub = new Endpoint(
|
|
159077
159204
|
airPurifierSubType.set({
|
|
@@ -159116,7 +159243,8 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
|
|
|
159116
159243
|
homeAssistantEntity: {
|
|
159117
159244
|
entity: primaryPayload,
|
|
159118
159245
|
customName: config11.customName,
|
|
159119
|
-
mapping: parentMapping
|
|
159246
|
+
mapping: parentMapping,
|
|
159247
|
+
identityAnchor: config11.identityAnchor
|
|
159120
159248
|
}
|
|
159121
159249
|
});
|
|
159122
159250
|
const mappedIds = [];
|
|
@@ -160574,7 +160702,7 @@ var ComposedClimateFanEndpoint = class _ComposedClimateFanEndpoint extends Endpo
|
|
|
160574
160702
|
const { registry: registry2, primaryEntityId } = config11;
|
|
160575
160703
|
const payload = buildEntityPayload2(registry2, primaryEntityId);
|
|
160576
160704
|
if (!payload) return void 0;
|
|
160577
|
-
const endpointId = createEndpointId3(primaryEntityId, config11.customName);
|
|
160705
|
+
const endpointId = config11.endpointId ?? createEndpointId3(primaryEntityId, config11.customName);
|
|
160578
160706
|
const mapping = config11.mapping ?? {
|
|
160579
160707
|
entityId: primaryEntityId
|
|
160580
160708
|
};
|
|
@@ -160618,7 +160746,8 @@ var ComposedClimateFanEndpoint = class _ComposedClimateFanEndpoint extends Endpo
|
|
|
160618
160746
|
homeAssistantEntity: {
|
|
160619
160747
|
entity: payload,
|
|
160620
160748
|
customName: config11.customName,
|
|
160621
|
-
mapping
|
|
160749
|
+
mapping,
|
|
160750
|
+
identityAnchor: config11.identityAnchor
|
|
160622
160751
|
}
|
|
160623
160752
|
});
|
|
160624
160753
|
const endpoint = new _ComposedClimateFanEndpoint(
|
|
@@ -160882,7 +161011,7 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
|
|
|
160882
161011
|
})
|
|
160883
161012
|
);
|
|
160884
161013
|
}
|
|
160885
|
-
const endpointId = createEndpointId4(primaryEntityId, config11.customName);
|
|
161014
|
+
const endpointId = config11.endpointId ?? createEndpointId4(primaryEntityId, config11.customName);
|
|
160886
161015
|
const parts = [];
|
|
160887
161016
|
const tempSub = new Endpoint(
|
|
160888
161017
|
TemperatureSubType2.set({
|
|
@@ -160924,7 +161053,8 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
|
|
|
160924
161053
|
homeAssistantEntity: {
|
|
160925
161054
|
entity: primaryPayload,
|
|
160926
161055
|
customName: config11.customName,
|
|
160927
|
-
mapping
|
|
161056
|
+
mapping,
|
|
161057
|
+
identityAnchor: config11.identityAnchor
|
|
160928
161058
|
}
|
|
160929
161059
|
});
|
|
160930
161060
|
const mappedIds = [];
|
|
@@ -169960,7 +170090,7 @@ function WeatherDevice(homeAssistant) {
|
|
|
169960
170090
|
|
|
169961
170091
|
// src/matter/endpoints/legacy/create-legacy-endpoint-type.ts
|
|
169962
170092
|
var legacyLogger = Logger.get("LegacyEndpointType");
|
|
169963
|
-
function createLegacyEndpointType(entity, mapping, areaName, options) {
|
|
170093
|
+
function createLegacyEndpointType(entity, mapping, areaName, options, identityAnchor) {
|
|
169964
170094
|
if (mapping?.disableBatteryMapping) {
|
|
169965
170095
|
entity = {
|
|
169966
170096
|
...entity,
|
|
@@ -169973,11 +170103,12 @@ function createLegacyEndpointType(entity, mapping, areaName, options) {
|
|
|
169973
170103
|
}
|
|
169974
170104
|
const domain = entity.entity_id.split(".")[0];
|
|
169975
170105
|
const customName = mapping?.customName;
|
|
170106
|
+
const ha = { entity, customName, mapping, identityAnchor };
|
|
169976
170107
|
let type;
|
|
169977
170108
|
if (mapping?.matterDeviceType) {
|
|
169978
170109
|
const overrideFactory = matterDeviceTypeFactories[mapping.matterDeviceType];
|
|
169979
170110
|
if (overrideFactory) {
|
|
169980
|
-
type = overrideFactory(
|
|
170111
|
+
type = overrideFactory(ha);
|
|
169981
170112
|
if (type && mapping.batteryEntity && !hasPowerSource(type)) {
|
|
169982
170113
|
type = addPowerSource(type);
|
|
169983
170114
|
}
|
|
@@ -169986,14 +170117,14 @@ function createLegacyEndpointType(entity, mapping, areaName, options) {
|
|
|
169986
170117
|
if (!type) {
|
|
169987
170118
|
if (domain === "vacuum") {
|
|
169988
170119
|
type = VacuumDevice(
|
|
169989
|
-
|
|
170120
|
+
ha,
|
|
169990
170121
|
options?.vacuumOnOff,
|
|
169991
170122
|
options?.cleaningModeOptions
|
|
169992
170123
|
);
|
|
169993
170124
|
} else {
|
|
169994
170125
|
const factory = deviceCtrs[domain];
|
|
169995
170126
|
if (factory) {
|
|
169996
|
-
type = factory(
|
|
170127
|
+
type = factory(ha);
|
|
169997
170128
|
} else if (options?.pluginDomainMappings?.has(domain)) {
|
|
169998
170129
|
const mappedType = options.pluginDomainMappings.get(domain);
|
|
169999
170130
|
const mappedFactory = matterDeviceTypeFactories[mappedType];
|
|
@@ -170001,7 +170132,7 @@ function createLegacyEndpointType(entity, mapping, areaName, options) {
|
|
|
170001
170132
|
legacyLogger.info(
|
|
170002
170133
|
`Using plugin domain mapping for "${domain}" \u2192 "${mappedType}"`
|
|
170003
170134
|
);
|
|
170004
|
-
type = mappedFactory(
|
|
170135
|
+
type = mappedFactory(ha);
|
|
170005
170136
|
}
|
|
170006
170137
|
} else {
|
|
170007
170138
|
return void 0;
|
|
@@ -170186,7 +170317,7 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
|
|
|
170186
170317
|
if (hasParentBattery) {
|
|
170187
170318
|
parentType = parentType.with(DefaultPowerSourceServer);
|
|
170188
170319
|
}
|
|
170189
|
-
const endpointId = createEndpointId5(primaryEntityId, config11.customName);
|
|
170320
|
+
const endpointId = config11.endpointId ?? createEndpointId5(primaryEntityId, config11.customName);
|
|
170190
170321
|
const parts = [];
|
|
170191
170322
|
const subEndpointMap = /* @__PURE__ */ new Map();
|
|
170192
170323
|
const mappedIds = [];
|
|
@@ -170264,7 +170395,8 @@ var UserComposedEndpoint = class _UserComposedEndpoint extends Endpoint {
|
|
|
170264
170395
|
homeAssistantEntity: {
|
|
170265
170396
|
entity: primaryPayload,
|
|
170266
170397
|
customName: config11.customName,
|
|
170267
|
-
mapping: config11.mapping
|
|
170398
|
+
mapping: config11.mapping,
|
|
170399
|
+
identityAnchor: config11.identityAnchor
|
|
170268
170400
|
}
|
|
170269
170401
|
});
|
|
170270
170402
|
const endpoint = new _UserComposedEndpoint(
|
|
@@ -170386,7 +170518,7 @@ function asStandaloneEndpointType(type) {
|
|
|
170386
170518
|
// src/matter/endpoints/legacy/legacy-endpoint.ts
|
|
170387
170519
|
var logger240 = Logger.get("LegacyEndpoint");
|
|
170388
170520
|
var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
170389
|
-
static async create(registry2, entityId, mapping, pluginDomainMappings, standalone = false) {
|
|
170521
|
+
static async create(registry2, entityId, mapping, pluginDomainMappings, standalone = false, endpointId, identityAnchor) {
|
|
170390
170522
|
const deviceRegistry = registry2.deviceOf(entityId);
|
|
170391
170523
|
let state = registry2.initialState(entityId);
|
|
170392
170524
|
const entity = registry2.entity(entityId);
|
|
@@ -170639,7 +170771,9 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170639
170771
|
mapping: effectiveMapping,
|
|
170640
170772
|
composedEntities: effectiveMapping.composedEntities,
|
|
170641
170773
|
customName: effectiveMapping?.customName,
|
|
170642
|
-
areaName: composedAreaName
|
|
170774
|
+
areaName: composedAreaName,
|
|
170775
|
+
endpointId,
|
|
170776
|
+
identityAnchor
|
|
170643
170777
|
});
|
|
170644
170778
|
if (composed) {
|
|
170645
170779
|
return composed;
|
|
@@ -170661,7 +170795,9 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170661
170795
|
powerEntityId: effectiveMapping?.powerEntity,
|
|
170662
170796
|
energyEntityId: effectiveMapping?.energyEntity,
|
|
170663
170797
|
customName: effectiveMapping?.customName,
|
|
170664
|
-
areaName: composedAreaName
|
|
170798
|
+
areaName: composedAreaName,
|
|
170799
|
+
endpointId,
|
|
170800
|
+
identityAnchor
|
|
170665
170801
|
});
|
|
170666
170802
|
return composed;
|
|
170667
170803
|
}
|
|
@@ -170681,7 +170817,9 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170681
170817
|
energyEntityId: effectiveMapping?.energyEntity,
|
|
170682
170818
|
mapping: effectiveMapping,
|
|
170683
170819
|
customName: effectiveMapping?.customName,
|
|
170684
|
-
areaName: composedAreaName
|
|
170820
|
+
areaName: composedAreaName,
|
|
170821
|
+
endpointId,
|
|
170822
|
+
identityAnchor
|
|
170685
170823
|
});
|
|
170686
170824
|
if (composed) {
|
|
170687
170825
|
return composed;
|
|
@@ -170698,7 +170836,9 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170698
170836
|
primaryEntityId: entityId,
|
|
170699
170837
|
mapping: effectiveMapping,
|
|
170700
170838
|
customName: effectiveMapping?.customName,
|
|
170701
|
-
areaName: composedAreaName
|
|
170839
|
+
areaName: composedAreaName,
|
|
170840
|
+
endpointId,
|
|
170841
|
+
identityAnchor
|
|
170702
170842
|
});
|
|
170703
170843
|
if (composed) {
|
|
170704
170844
|
return composed;
|
|
@@ -170732,11 +170872,17 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170732
170872
|
}
|
|
170733
170873
|
}
|
|
170734
170874
|
const areaName = registry2.getAreaName(entityId);
|
|
170735
|
-
let type = createLegacyEndpointType(
|
|
170736
|
-
|
|
170737
|
-
|
|
170738
|
-
|
|
170739
|
-
|
|
170875
|
+
let type = createLegacyEndpointType(
|
|
170876
|
+
payload,
|
|
170877
|
+
effectiveMapping,
|
|
170878
|
+
areaName,
|
|
170879
|
+
{
|
|
170880
|
+
vacuumOnOff: registry2.isVacuumOnOffEnabled(),
|
|
170881
|
+
cleaningModeOptions,
|
|
170882
|
+
pluginDomainMappings
|
|
170883
|
+
},
|
|
170884
|
+
identityAnchor
|
|
170885
|
+
);
|
|
170740
170886
|
if (!type) {
|
|
170741
170887
|
return;
|
|
170742
170888
|
}
|
|
@@ -170750,11 +170896,12 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
170750
170896
|
entityId,
|
|
170751
170897
|
customName,
|
|
170752
170898
|
mappedIds,
|
|
170753
|
-
effectiveMapping?.updateThrottleMs
|
|
170899
|
+
effectiveMapping?.updateThrottleMs,
|
|
170900
|
+
endpointId
|
|
170754
170901
|
);
|
|
170755
170902
|
}
|
|
170756
|
-
constructor(type, entityId, customName, mappedEntityIds, throttleMs) {
|
|
170757
|
-
super(type, entityId, customName, mappedEntityIds);
|
|
170903
|
+
constructor(type, entityId, customName, mappedEntityIds, throttleMs, endpointId) {
|
|
170904
|
+
super(type, entityId, customName, mappedEntityIds, endpointId);
|
|
170758
170905
|
this.flushUpdate = throttleMs && throttleMs > 50 ? throttleLatest(this.flushPendingUpdate.bind(this), throttleMs) : debounce6(this.flushPendingUpdate.bind(this), 50);
|
|
170759
170906
|
}
|
|
170760
170907
|
lastState;
|
|
@@ -171601,11 +171748,124 @@ var EntityIsolationServiceImpl = class {
|
|
|
171601
171748
|
};
|
|
171602
171749
|
var EntityIsolationService = new EntityIsolationServiceImpl();
|
|
171603
171750
|
|
|
171751
|
+
// src/services/bridges/identity-resolver.ts
|
|
171752
|
+
var SEP = "\0";
|
|
171753
|
+
function identityKey(entity) {
|
|
171754
|
+
const uniqueId = entity.registry?.unique_id;
|
|
171755
|
+
const platform = entity.registry?.platform;
|
|
171756
|
+
if (!uniqueId || !platform) {
|
|
171757
|
+
return null;
|
|
171758
|
+
}
|
|
171759
|
+
const domain = entity.entity_id.split(".")[0];
|
|
171760
|
+
return platform + SEP + domain + SEP + uniqueId;
|
|
171761
|
+
}
|
|
171762
|
+
var IdentityResolver = class {
|
|
171763
|
+
constructor(identityStorage, mappingStorage) {
|
|
171764
|
+
this.identityStorage = identityStorage;
|
|
171765
|
+
this.mappingStorage = mappingStorage;
|
|
171766
|
+
}
|
|
171767
|
+
identityStorage;
|
|
171768
|
+
mappingStorage;
|
|
171769
|
+
async resolveIdentity(bridgeId, entity, mapping, opts) {
|
|
171770
|
+
const entityId = entity.entity_id;
|
|
171771
|
+
const key = identityKey(entity);
|
|
171772
|
+
if (key == null) {
|
|
171773
|
+
return {
|
|
171774
|
+
endpointId: createEndpointId(entityId, mapping?.customName),
|
|
171775
|
+
anchorEntityId: entityId,
|
|
171776
|
+
protected: false
|
|
171777
|
+
};
|
|
171778
|
+
}
|
|
171779
|
+
const existing = this.identityStorage.getIdentity(bridgeId, key);
|
|
171780
|
+
if (existing) {
|
|
171781
|
+
const renamedFrom = existing.lastEntityId !== entityId ? existing.lastEntityId ?? existing.anchorEntityId : void 0;
|
|
171782
|
+
if (opts.stableIdentity) {
|
|
171783
|
+
if (renamedFrom) {
|
|
171784
|
+
this.identityStorage.setIdentity(bridgeId, key, {
|
|
171785
|
+
...existing,
|
|
171786
|
+
lastEntityId: entityId
|
|
171787
|
+
});
|
|
171788
|
+
await this.rekeyMapping(bridgeId, renamedFrom, entityId);
|
|
171789
|
+
}
|
|
171790
|
+
return {
|
|
171791
|
+
endpointId: existing.endpointId,
|
|
171792
|
+
anchorEntityId: existing.anchorEntityId,
|
|
171793
|
+
protected: true,
|
|
171794
|
+
renamedFrom
|
|
171795
|
+
};
|
|
171796
|
+
}
|
|
171797
|
+
const liveEndpointId = createEndpointId(entityId, mapping?.customName);
|
|
171798
|
+
if (existing.endpointId !== liveEndpointId || existing.anchorEntityId !== entityId || existing.lastEntityId !== entityId) {
|
|
171799
|
+
this.identityStorage.setIdentity(bridgeId, key, {
|
|
171800
|
+
...existing,
|
|
171801
|
+
endpointId: liveEndpointId,
|
|
171802
|
+
anchorEntityId: entityId,
|
|
171803
|
+
lastEntityId: entityId
|
|
171804
|
+
});
|
|
171805
|
+
}
|
|
171806
|
+
return {
|
|
171807
|
+
endpointId: liveEndpointId,
|
|
171808
|
+
anchorEntityId: entityId,
|
|
171809
|
+
protected: false,
|
|
171810
|
+
renamedFrom
|
|
171811
|
+
};
|
|
171812
|
+
}
|
|
171813
|
+
const desired = createEndpointId(entityId, mapping?.customName);
|
|
171814
|
+
let endpointId = desired;
|
|
171815
|
+
if (opts.stableIdentity) {
|
|
171816
|
+
let n = 2;
|
|
171817
|
+
while (opts.isEndpointIdTaken(endpointId, key)) {
|
|
171818
|
+
endpointId = `${desired}_${n++}`;
|
|
171819
|
+
}
|
|
171820
|
+
}
|
|
171821
|
+
const record = {
|
|
171822
|
+
endpointId,
|
|
171823
|
+
anchorEntityId: entityId,
|
|
171824
|
+
lastEntityId: entityId,
|
|
171825
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
171826
|
+
};
|
|
171827
|
+
this.identityStorage.setIdentity(bridgeId, key, record);
|
|
171828
|
+
if (opts.stableIdentity) {
|
|
171829
|
+
return {
|
|
171830
|
+
endpointId: record.endpointId,
|
|
171831
|
+
anchorEntityId: record.anchorEntityId,
|
|
171832
|
+
protected: true
|
|
171833
|
+
};
|
|
171834
|
+
}
|
|
171835
|
+
return {
|
|
171836
|
+
endpointId: desired,
|
|
171837
|
+
anchorEntityId: entityId,
|
|
171838
|
+
protected: false
|
|
171839
|
+
};
|
|
171840
|
+
}
|
|
171841
|
+
// Carry a mapping from the old entity_id to the new one on rename. anchorEntityId
|
|
171842
|
+
// never changes, only the mapping key. Skips when no old mapping exists or a new
|
|
171843
|
+
// one already does, so a manual mapping under the new id is never clobbered.
|
|
171844
|
+
async rekeyMapping(bridgeId, oldEntityId, newEntityId) {
|
|
171845
|
+
if (oldEntityId === newEntityId) {
|
|
171846
|
+
return;
|
|
171847
|
+
}
|
|
171848
|
+
const old = this.mappingStorage.getMapping(bridgeId, oldEntityId);
|
|
171849
|
+
if (!old) {
|
|
171850
|
+
return;
|
|
171851
|
+
}
|
|
171852
|
+
if (this.mappingStorage.getMapping(bridgeId, newEntityId)) {
|
|
171853
|
+
return;
|
|
171854
|
+
}
|
|
171855
|
+
await this.mappingStorage.setMapping({
|
|
171856
|
+
...old,
|
|
171857
|
+
bridgeId,
|
|
171858
|
+
entityId: newEntityId
|
|
171859
|
+
});
|
|
171860
|
+
await this.mappingStorage.deleteMapping(bridgeId, oldEntityId);
|
|
171861
|
+
}
|
|
171862
|
+
};
|
|
171863
|
+
|
|
171604
171864
|
// src/services/bridges/bridge-endpoint-manager.ts
|
|
171605
171865
|
var MAX_ENTITY_ID_LENGTH = 150;
|
|
171606
171866
|
var ENDPOINT_REMOVAL_GRACE_MS = 6e4;
|
|
171607
171867
|
var BridgeEndpointManager = class extends Service {
|
|
171608
|
-
constructor(client, registry2, mappingStorage, bridgeId, log, pluginManager, pluginRegistry, pluginInstaller) {
|
|
171868
|
+
constructor(client, registry2, mappingStorage, identityStorage, bridgeId, log, pluginManager, pluginRegistry, pluginInstaller) {
|
|
171609
171869
|
super("BridgeEndpointManager");
|
|
171610
171870
|
this.client = client;
|
|
171611
171871
|
this.registry = registry2;
|
|
@@ -171616,6 +171876,10 @@ var BridgeEndpointManager = class extends Service {
|
|
|
171616
171876
|
this.pluginRegistry = pluginRegistry;
|
|
171617
171877
|
this.pluginInstaller = pluginInstaller;
|
|
171618
171878
|
this.root = new AggregatorEndpoint2("aggregator");
|
|
171879
|
+
this.identityResolver = new IdentityResolver(
|
|
171880
|
+
identityStorage,
|
|
171881
|
+
mappingStorage
|
|
171882
|
+
);
|
|
171619
171883
|
EntityIsolationService.registerIsolationCallback(
|
|
171620
171884
|
bridgeId,
|
|
171621
171885
|
this.isolateEntity.bind(this)
|
|
@@ -171654,6 +171918,7 @@ var BridgeEndpointManager = class extends Service {
|
|
|
171654
171918
|
failedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
171655
171919
|
});
|
|
171656
171920
|
}
|
|
171921
|
+
identityResolver;
|
|
171657
171922
|
wirePluginCallbacks() {
|
|
171658
171923
|
if (!this.pluginManager) return;
|
|
171659
171924
|
this.pluginManager.onDeviceRegistered = async (pluginName, device) => {
|
|
@@ -171993,10 +172258,57 @@ var BridgeEndpointManager = class extends Service {
|
|
|
171993
172258
|
if (humId) this.registry.markComposedSubEntityUsed(humId);
|
|
171994
172259
|
}
|
|
171995
172260
|
}
|
|
172261
|
+
const stableIdentity = this.registry.isStableIdentityEnabled();
|
|
172262
|
+
const resolvedByEntity = /* @__PURE__ */ new Map();
|
|
172263
|
+
const endpointIdToEntity = /* @__PURE__ */ new Map();
|
|
172264
|
+
const claimedEndpointIds = /* @__PURE__ */ new Map();
|
|
172265
|
+
for (const entityId of this.entityIds) {
|
|
172266
|
+
const entityInfo = {
|
|
172267
|
+
entity_id: entityId,
|
|
172268
|
+
registry: this.registry.entity(entityId)
|
|
172269
|
+
};
|
|
172270
|
+
const resolved = await this.identityResolver.resolveIdentity(
|
|
172271
|
+
this.bridgeId,
|
|
172272
|
+
entityInfo,
|
|
172273
|
+
this.getEntityMapping(entityId),
|
|
172274
|
+
{
|
|
172275
|
+
stableIdentity,
|
|
172276
|
+
isEndpointIdTaken: (id, key) => claimedEndpointIds.has(id) && claimedEndpointIds.get(id) !== key
|
|
172277
|
+
}
|
|
172278
|
+
);
|
|
172279
|
+
resolvedByEntity.set(entityId, resolved);
|
|
172280
|
+
claimedEndpointIds.set(
|
|
172281
|
+
resolved.endpointId,
|
|
172282
|
+
identityKey(entityInfo) ?? `\0e:${entityId}`
|
|
172283
|
+
);
|
|
172284
|
+
endpointIdToEntity.set(resolved.endpointId, entityId);
|
|
172285
|
+
if (resolved.renamedFrom && this.mappingFingerprints.has(resolved.renamedFrom)) {
|
|
172286
|
+
const fp = this.mappingFingerprints.get(resolved.renamedFrom);
|
|
172287
|
+
this.mappingFingerprints.delete(resolved.renamedFrom);
|
|
172288
|
+
this.mappingFingerprints.set(entityId, fp);
|
|
172289
|
+
}
|
|
172290
|
+
}
|
|
171996
172291
|
const existingEndpoints = [];
|
|
171997
172292
|
const now = Date.now();
|
|
171998
172293
|
for (const endpoint of endpoints) {
|
|
171999
172294
|
const present = this.entityIds.includes(endpoint.entityId);
|
|
172295
|
+
const claimant = endpointIdToEntity.get(endpoint.id);
|
|
172296
|
+
if (!present && claimant != null && claimant !== endpoint.entityId) {
|
|
172297
|
+
this.log.info(
|
|
172298
|
+
`Entity renamed ${endpoint.entityId} -> ${claimant}, keeping endpoint ${endpoint.id}`
|
|
172299
|
+
);
|
|
172300
|
+
try {
|
|
172301
|
+
await endpoint.close();
|
|
172302
|
+
} catch (e) {
|
|
172303
|
+
this.log.warn(
|
|
172304
|
+
`Failed to close renamed endpoint ${endpoint.entityId}:`,
|
|
172305
|
+
e
|
|
172306
|
+
);
|
|
172307
|
+
}
|
|
172308
|
+
this.pendingRemovals.delete(endpoint.entityId);
|
|
172309
|
+
this.mappingFingerprints.delete(endpoint.entityId);
|
|
172310
|
+
continue;
|
|
172311
|
+
}
|
|
172000
172312
|
if (present) {
|
|
172001
172313
|
this.pendingRemovals.delete(endpoint.entityId);
|
|
172002
172314
|
}
|
|
@@ -172039,7 +172351,8 @@ var BridgeEndpointManager = class extends Service {
|
|
|
172039
172351
|
this.log.info(
|
|
172040
172352
|
`Mapping changed for ${endpoint.entityId}, recreating endpoint`
|
|
172041
172353
|
);
|
|
172042
|
-
const
|
|
172354
|
+
const resolvedId = resolvedByEntity.get(endpoint.entityId)?.endpointId ?? createEndpointId(endpoint.entityId, currentMapping?.customName);
|
|
172355
|
+
const sameId = resolvedId === endpoint.id;
|
|
172043
172356
|
try {
|
|
172044
172357
|
if (sameId) {
|
|
172045
172358
|
await endpoint.close();
|
|
@@ -172097,11 +172410,15 @@ var BridgeEndpointManager = class extends Service {
|
|
|
172097
172410
|
if (!endpoint) {
|
|
172098
172411
|
try {
|
|
172099
172412
|
const domainMappings = this.getPluginDomainMappings();
|
|
172413
|
+
const resolved = resolvedByEntity.get(entityId);
|
|
172100
172414
|
endpoint = await LegacyEndpoint.create(
|
|
172101
172415
|
this.registry,
|
|
172102
172416
|
entityId,
|
|
172103
172417
|
mapping,
|
|
172104
|
-
domainMappings
|
|
172418
|
+
domainMappings,
|
|
172419
|
+
false,
|
|
172420
|
+
resolved?.endpointId,
|
|
172421
|
+
resolved?.anchorEntityId
|
|
172105
172422
|
);
|
|
172106
172423
|
} catch (e) {
|
|
172107
172424
|
const reason = this.extractErrorReason(e);
|
|
@@ -172513,6 +172830,11 @@ var BridgeRegistry = class _BridgeRegistry {
|
|
|
172513
172830
|
isVacuumOnOffEnabled() {
|
|
172514
172831
|
return this.dataProvider.featureFlags?.vacuumOnOff === true;
|
|
172515
172832
|
}
|
|
172833
|
+
// Consume frozen device identities (#404). Seeding always runs; only
|
|
172834
|
+
// consumption of the stored endpoint id/anchor is gated on this flag.
|
|
172835
|
+
isStableIdentityEnabled() {
|
|
172836
|
+
return this.dataProvider.featureFlags?.stableIdentity === true;
|
|
172837
|
+
}
|
|
172516
172838
|
/**
|
|
172517
172839
|
* Check if the vacuum OnOff cluster should be included for server-mode vacuums.
|
|
172518
172840
|
* Defaults to OFF. OnOff is NOT part of the RoboticVacuumCleaner (0x74) device
|
|
@@ -173942,7 +174264,7 @@ function ServerModeVacuumDevice(homeAssistantEntity, includeOnOff = false, clean
|
|
|
173942
174264
|
// src/matter/endpoints/server-mode-vacuum-endpoint.ts
|
|
173943
174265
|
var logger243 = Logger.get("ServerModeVacuumEndpoint");
|
|
173944
174266
|
var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEndpoint {
|
|
173945
|
-
static async create(registry2, entityId, mapping) {
|
|
174267
|
+
static async create(registry2, entityId, mapping, endpointId) {
|
|
173946
174268
|
const deviceRegistry = registry2.deviceOf(entityId);
|
|
173947
174269
|
let state = registry2.initialState(entityId);
|
|
173948
174270
|
const entity = registry2.entity(entityId);
|
|
@@ -174123,14 +174445,15 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
|
|
|
174123
174445
|
endpointType,
|
|
174124
174446
|
entityId,
|
|
174125
174447
|
customName,
|
|
174126
|
-
mappedIds
|
|
174448
|
+
mappedIds,
|
|
174449
|
+
endpointId
|
|
174127
174450
|
);
|
|
174128
174451
|
}
|
|
174129
174452
|
lastState;
|
|
174130
174453
|
pendingMappedChange = false;
|
|
174131
174454
|
flushUpdate;
|
|
174132
|
-
constructor(type, entityId, customName, mappedEntityIds) {
|
|
174133
|
-
super(type, entityId, customName, mappedEntityIds);
|
|
174455
|
+
constructor(type, entityId, customName, mappedEntityIds, endpointId) {
|
|
174456
|
+
super(type, entityId, customName, mappedEntityIds, endpointId);
|
|
174134
174457
|
this.flushUpdate = debounce7(this.flushPendingUpdate.bind(this), 50);
|
|
174135
174458
|
}
|
|
174136
174459
|
clearTimers() {
|
|
@@ -174197,7 +174520,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
|
|
|
174197
174520
|
// src/services/bridges/server-mode-endpoint-manager.ts
|
|
174198
174521
|
var MAX_SERVER_MODE_DEVICES = 10;
|
|
174199
174522
|
var ServerModeEndpointManager = class extends Service {
|
|
174200
|
-
constructor(serverNode, client, registry2, mappingStorage, dataProvider, log) {
|
|
174523
|
+
constructor(serverNode, client, registry2, mappingStorage, identityStorage, dataProvider, log) {
|
|
174201
174524
|
super("ServerModeEndpointManager");
|
|
174202
174525
|
this.serverNode = serverNode;
|
|
174203
174526
|
this.client = client;
|
|
@@ -174205,6 +174528,10 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174205
174528
|
this.mappingStorage = mappingStorage;
|
|
174206
174529
|
this.dataProvider = dataProvider;
|
|
174207
174530
|
this.log = log;
|
|
174531
|
+
this.identityResolver = new IdentityResolver(
|
|
174532
|
+
identityStorage,
|
|
174533
|
+
mappingStorage
|
|
174534
|
+
);
|
|
174208
174535
|
}
|
|
174209
174536
|
serverNode;
|
|
174210
174537
|
client;
|
|
@@ -174223,6 +174550,7 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174223
174550
|
get devices() {
|
|
174224
174551
|
return [...this.endpoints.values()].map((entry) => entry.endpoint);
|
|
174225
174552
|
}
|
|
174553
|
+
identityResolver;
|
|
174226
174554
|
getEntityMapping(entityId) {
|
|
174227
174555
|
return this.mappingStorage.getMapping(this.dataProvider.id, entityId);
|
|
174228
174556
|
}
|
|
@@ -174291,6 +174619,19 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174291
174619
|
this.endpoints.delete(entityId);
|
|
174292
174620
|
}
|
|
174293
174621
|
}
|
|
174622
|
+
// Like removeEndpoints but close() keeps the persisted number pre-allocated,
|
|
174623
|
+
// so a same-id recreate reuses it. Used on rename and same-id recreates (#404).
|
|
174624
|
+
async closeEndpoint(entityId) {
|
|
174625
|
+
const entry = this.endpoints.get(entityId);
|
|
174626
|
+
if (!entry) return;
|
|
174627
|
+
try {
|
|
174628
|
+
await entry.endpoint.close();
|
|
174629
|
+
} catch (e) {
|
|
174630
|
+
this.log.warn(`Failed to close endpoint ${entityId}:`, e);
|
|
174631
|
+
}
|
|
174632
|
+
this.serverNode.forgetDevice(entry.endpoint);
|
|
174633
|
+
this.endpoints.delete(entityId);
|
|
174634
|
+
}
|
|
174294
174635
|
async refreshDevices() {
|
|
174295
174636
|
this.registry.refresh();
|
|
174296
174637
|
this._failedEntities = [];
|
|
@@ -174318,10 +174659,45 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174318
174659
|
`Server mode node is capped at ${MAX_SERVER_MODE_DEVICES} devices, ${surplus.length} entities skipped`
|
|
174319
174660
|
);
|
|
174320
174661
|
}
|
|
174662
|
+
const stableIdentity = this.dataProvider.featureFlags?.stableIdentity === true;
|
|
174663
|
+
const resolvedByEntity = /* @__PURE__ */ new Map();
|
|
174664
|
+
const endpointIdToEntity = /* @__PURE__ */ new Map();
|
|
174665
|
+
const claimedEndpointIds = /* @__PURE__ */ new Map();
|
|
174666
|
+
for (const entityId of orderedIds) {
|
|
174667
|
+
const entityInfo = {
|
|
174668
|
+
entity_id: entityId,
|
|
174669
|
+
registry: this.registry.entity(entityId)
|
|
174670
|
+
};
|
|
174671
|
+
const resolved = await this.identityResolver.resolveIdentity(
|
|
174672
|
+
this.dataProvider.id,
|
|
174673
|
+
entityInfo,
|
|
174674
|
+
this.getEntityMapping(entityId),
|
|
174675
|
+
{
|
|
174676
|
+
stableIdentity,
|
|
174677
|
+
isEndpointIdTaken: (id, key) => claimedEndpointIds.has(id) && claimedEndpointIds.get(id) !== key
|
|
174678
|
+
}
|
|
174679
|
+
);
|
|
174680
|
+
resolvedByEntity.set(entityId, resolved);
|
|
174681
|
+
claimedEndpointIds.set(
|
|
174682
|
+
resolved.endpointId,
|
|
174683
|
+
identityKey(entityInfo) ?? `\0e:${entityId}`
|
|
174684
|
+
);
|
|
174685
|
+
endpointIdToEntity.set(resolved.endpointId, entityId);
|
|
174686
|
+
}
|
|
174321
174687
|
const keep = new Set(orderedIds);
|
|
174322
174688
|
const removed = [...this.endpoints.keys()].filter((id) => !keep.has(id));
|
|
174323
174689
|
let structureChanged = removed.length > 0;
|
|
174324
|
-
|
|
174690
|
+
const genuinelyRemoved = [];
|
|
174691
|
+
for (const oldId of removed) {
|
|
174692
|
+
const entry = this.endpoints.get(oldId);
|
|
174693
|
+
const claimant = entry ? endpointIdToEntity.get(entry.endpoint.id) : void 0;
|
|
174694
|
+
if (entry && claimant != null && claimant !== oldId) {
|
|
174695
|
+
await this.closeEndpoint(oldId);
|
|
174696
|
+
} else {
|
|
174697
|
+
genuinelyRemoved.push(oldId);
|
|
174698
|
+
}
|
|
174699
|
+
}
|
|
174700
|
+
await this.removeEndpoints(genuinelyRemoved);
|
|
174325
174701
|
for (const entityId of orderedIds) {
|
|
174326
174702
|
const mapping = this.getEntityMapping(entityId);
|
|
174327
174703
|
if (mapping?.disabled) {
|
|
@@ -174344,9 +174720,14 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174344
174720
|
this.log.debug(`Device endpoint already exists for ${entityId}`);
|
|
174345
174721
|
continue;
|
|
174346
174722
|
}
|
|
174723
|
+
const resolved = resolvedByEntity.get(entityId);
|
|
174347
174724
|
if (existing) {
|
|
174348
174725
|
this.log.info(`Mapping changed for ${entityId}, recreating endpoint`);
|
|
174349
|
-
|
|
174726
|
+
if (resolved && existing.endpoint.id === resolved.endpointId) {
|
|
174727
|
+
await this.closeEndpoint(entityId);
|
|
174728
|
+
} else {
|
|
174729
|
+
await this.removeEndpoints([entityId]);
|
|
174730
|
+
}
|
|
174350
174731
|
structureChanged = true;
|
|
174351
174732
|
}
|
|
174352
174733
|
const claimedBy = [...this.endpoints.entries()].find(
|
|
@@ -174359,7 +174740,7 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174359
174740
|
});
|
|
174360
174741
|
continue;
|
|
174361
174742
|
}
|
|
174362
|
-
const endpointId = createEndpointId(entityId, mapping?.customName);
|
|
174743
|
+
const endpointId = resolved?.endpointId ?? createEndpointId(entityId, mapping?.customName);
|
|
174363
174744
|
const collision = [...this.endpoints.entries()].find(
|
|
174364
174745
|
([, entry]) => entry.endpoint.id === endpointId
|
|
174365
174746
|
);
|
|
@@ -174382,12 +174763,18 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174382
174763
|
}
|
|
174383
174764
|
try {
|
|
174384
174765
|
const domain = entityId.split(".")[0];
|
|
174385
|
-
const endpoint = domain === "vacuum" ? await this.createServerModeVacuumEndpoint(
|
|
174766
|
+
const endpoint = domain === "vacuum" ? await this.createServerModeVacuumEndpoint(
|
|
174767
|
+
entityId,
|
|
174768
|
+
mapping,
|
|
174769
|
+
resolved?.endpointId
|
|
174770
|
+
) : await LegacyEndpoint.create(
|
|
174386
174771
|
this.registry,
|
|
174387
174772
|
entityId,
|
|
174388
174773
|
mapping,
|
|
174389
174774
|
void 0,
|
|
174390
|
-
true
|
|
174775
|
+
true,
|
|
174776
|
+
resolved?.endpointId,
|
|
174777
|
+
resolved?.anchorEntityId
|
|
174391
174778
|
);
|
|
174392
174779
|
if (!endpoint) {
|
|
174393
174780
|
this._failedEntities.push({
|
|
@@ -174455,8 +174842,13 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
174455
174842
|
* This makes the vacuum appear as a standalone Matter device, which is required
|
|
174456
174843
|
* for Apple Home Siri voice commands and Alexa discovery.
|
|
174457
174844
|
*/
|
|
174458
|
-
async createServerModeVacuumEndpoint(entityId, mapping) {
|
|
174459
|
-
return ServerModeVacuumEndpoint.create(
|
|
174845
|
+
async createServerModeVacuumEndpoint(entityId, mapping, endpointId) {
|
|
174846
|
+
return ServerModeVacuumEndpoint.create(
|
|
174847
|
+
this.registry,
|
|
174848
|
+
entityId,
|
|
174849
|
+
mapping,
|
|
174850
|
+
endpointId
|
|
174851
|
+
);
|
|
174460
174852
|
}
|
|
174461
174853
|
};
|
|
174462
174854
|
|
|
@@ -174540,6 +174932,7 @@ var BridgeEnvironment = class _BridgeEnvironment extends EnvironmentBase {
|
|
|
174540
174932
|
await this.load(HomeAssistantClient),
|
|
174541
174933
|
this.get(BridgeRegistry),
|
|
174542
174934
|
await this.load(EntityMappingStorage),
|
|
174935
|
+
await this.load(EntityIdentityStorage),
|
|
174543
174936
|
bridgeId,
|
|
174544
174937
|
this.endpointManagerLogger,
|
|
174545
174938
|
pluginManager,
|
|
@@ -174625,6 +175018,7 @@ var BridgeEnvironmentFactory = class extends BridgeFactory {
|
|
|
174625
175018
|
await env.load(HomeAssistantClient),
|
|
174626
175019
|
env.get(BridgeRegistry),
|
|
174627
175020
|
await env.load(EntityMappingStorage),
|
|
175021
|
+
await env.load(EntityIdentityStorage),
|
|
174628
175022
|
dataProvider,
|
|
174629
175023
|
loggerService.get("ServerModeEndpointManager")
|
|
174630
175024
|
);
|
|
@@ -174676,6 +175070,10 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
|
|
|
174676
175070
|
EntityMappingStorage,
|
|
174677
175071
|
new EntityMappingStorage(await this.load(AppStorage))
|
|
174678
175072
|
);
|
|
175073
|
+
this.set(
|
|
175074
|
+
EntityIdentityStorage,
|
|
175075
|
+
new EntityIdentityStorage(await this.load(AppStorage))
|
|
175076
|
+
);
|
|
174679
175077
|
this.set(
|
|
174680
175078
|
LockCredentialStorage,
|
|
174681
175079
|
new LockCredentialStorage(await this.load(AppStorage))
|
|
@@ -174736,6 +175134,7 @@ var AppEnvironment = class _AppEnvironment extends EnvironmentBase {
|
|
|
174736
175134
|
await this.load(HomeAssistantRegistry),
|
|
174737
175135
|
await this.load(BridgeStorage),
|
|
174738
175136
|
await this.load(EntityMappingStorage),
|
|
175137
|
+
await this.load(EntityIdentityStorage),
|
|
174739
175138
|
await this.load(LockCredentialStorage),
|
|
174740
175139
|
await this.load(AppSettingsStorage),
|
|
174741
175140
|
await this.load(BackupService),
|