@riddix/hamh 2.1.0-alpha.736 → 2.1.0-alpha.737
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
|
@@ -123729,7 +123729,7 @@ var init_bridge_templates = __esm({
|
|
|
123729
123729
|
{
|
|
123730
123730
|
id: "robot_vacuum",
|
|
123731
123731
|
name: "Robot Vacuum (Server Mode)",
|
|
123732
|
-
description: "Single vacuum bridge with Server Mode enabled. Required for Apple Home Siri commands and proper Alexa discovery.
|
|
123732
|
+
description: "Single vacuum bridge with Server Mode enabled. Required for Apple Home Siri commands and proper Alexa discovery. Best with just the vacuum on this bridge.",
|
|
123733
123733
|
icon: "vacuum",
|
|
123734
123734
|
filter: {
|
|
123735
123735
|
include: [{ type: HomeAssistantMatcherType.Domain, value: "vacuum" }],
|
|
@@ -124664,7 +124664,7 @@ var init_bridge_config_schema = __esm({
|
|
|
124664
124664
|
},
|
|
124665
124665
|
serverMode: {
|
|
124666
124666
|
title: "Server Mode (standalone device)",
|
|
124667
|
-
description: "Expose
|
|
124667
|
+
description: "Expose entities as standalone Matter devices instead of bridged ones. Works for any supported device type, e.g. robot vacuums need it for Apple Home Siri voice commands. One node carries up to 10 devices; the first entity is the primary and drives the node name and type (experimental beyond one device).",
|
|
124668
124668
|
type: "boolean",
|
|
124669
124669
|
default: false
|
|
124670
124670
|
},
|
|
@@ -145992,7 +145992,7 @@ function trimToLength(value, maxLength, suffix) {
|
|
|
145992
145992
|
// src/matter/endpoints/server-mode-server-node.ts
|
|
145993
145993
|
var logger183 = Logger.get("ServerModeServerNode");
|
|
145994
145994
|
var ServerModeServerNode = class extends ServerNode {
|
|
145995
|
-
|
|
145995
|
+
deviceEndpoints = /* @__PURE__ */ new Map();
|
|
145996
145996
|
featureFlags;
|
|
145997
145997
|
serialNumberSuffix;
|
|
145998
145998
|
constructor(env, bridgeData) {
|
|
@@ -146037,24 +146037,31 @@ var ServerModeServerNode = class extends ServerNode {
|
|
|
146037
146037
|
this.featureFlags = bridgeData.featureFlags;
|
|
146038
146038
|
this.serialNumberSuffix = bridgeData.serialNumberSuffix;
|
|
146039
146039
|
}
|
|
146040
|
+
/** Number of device endpoints currently attached. */
|
|
146041
|
+
get deviceCount() {
|
|
146042
|
+
return this.deviceEndpoints.size;
|
|
146043
|
+
}
|
|
146040
146044
|
/**
|
|
146041
|
-
* Add
|
|
146042
|
-
*
|
|
146043
|
-
* This method is idempotent - if a device already exists, it's a no-op.
|
|
146045
|
+
* Add a device endpoint to this server node. Several endpoints per node are
|
|
146046
|
+
* supported (#301); the call is idempotent per endpoint id.
|
|
146044
146047
|
*/
|
|
146045
146048
|
async addDevice(endpoint) {
|
|
146046
|
-
if (this.
|
|
146049
|
+
if (this.deviceEndpoints.has(endpoint.id)) {
|
|
146047
146050
|
return;
|
|
146048
146051
|
}
|
|
146049
|
-
this.
|
|
146052
|
+
this.deviceEndpoints.set(endpoint.id, endpoint);
|
|
146050
146053
|
await this.add(endpoint);
|
|
146051
146054
|
}
|
|
146052
146055
|
/**
|
|
146053
|
-
*
|
|
146054
|
-
* Must be called before
|
|
146056
|
+
* Drop one device reference after the endpoint has been deleted externally.
|
|
146057
|
+
* Must be called before re-adding an endpoint with the same id.
|
|
146055
146058
|
*/
|
|
146056
|
-
|
|
146057
|
-
this.
|
|
146059
|
+
forgetDevice(endpoint) {
|
|
146060
|
+
this.deviceEndpoints.delete(endpoint.id);
|
|
146061
|
+
}
|
|
146062
|
+
/** Drop all device references after the endpoints were deleted externally. */
|
|
146063
|
+
clearDevices() {
|
|
146064
|
+
this.deviceEndpoints.clear();
|
|
146058
146065
|
}
|
|
146059
146066
|
/**
|
|
146060
146067
|
* Update root-level BasicInformation with entity-specific data.
|
|
@@ -161497,7 +161504,12 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
161497
161504
|
}
|
|
161498
161505
|
}
|
|
161499
161506
|
}
|
|
161500
|
-
if (
|
|
161507
|
+
if (standalone && ((effectiveMapping?.composedEntities?.length ?? 0) > 0 || effectiveMapping?.climateExposeFan === true)) {
|
|
161508
|
+
logger228.warn(
|
|
161509
|
+
`Composed mappings are not supported in server mode, exposing ${entityId} as a flat standalone endpoint`
|
|
161510
|
+
);
|
|
161511
|
+
}
|
|
161512
|
+
if (!standalone && registry2.isAutoComposedDevicesEnabled() && effectiveMapping?.composedEntities && effectiveMapping.composedEntities.length > 0) {
|
|
161501
161513
|
const composedAreaName = registry2.getAreaName(entityId);
|
|
161502
161514
|
const composed = await UserComposedEndpoint.create({
|
|
161503
161515
|
registry: registry2,
|
|
@@ -161514,7 +161526,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
161514
161526
|
`User composed device creation failed for ${entityId}, falling back to standalone`
|
|
161515
161527
|
);
|
|
161516
161528
|
}
|
|
161517
|
-
if (registry2.isAutoComposedDevicesEnabled()) {
|
|
161529
|
+
if (!standalone && registry2.isAutoComposedDevicesEnabled()) {
|
|
161518
161530
|
const attrs = state.attributes;
|
|
161519
161531
|
if (entityId.startsWith("sensor.") && attrs.device_class === SensorDeviceClass.temperature && (effectiveMapping?.humidityEntity || effectiveMapping?.pressureEntity)) {
|
|
161520
161532
|
const composedAreaName = registry2.getAreaName(entityId);
|
|
@@ -161555,7 +161567,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
|
|
|
161555
161567
|
}
|
|
161556
161568
|
}
|
|
161557
161569
|
}
|
|
161558
|
-
if (entityId.startsWith("climate.") && effectiveMapping?.climateExposeFan === true) {
|
|
161570
|
+
if (!standalone && entityId.startsWith("climate.") && effectiveMapping?.climateExposeFan === true) {
|
|
161559
161571
|
const climateFeatures = state.attributes.supported_features ?? 0;
|
|
161560
161572
|
if ((climateFeatures & ClimateDeviceFeature.FAN_MODE) !== 0) {
|
|
161561
161573
|
const composedAreaName = registry2.getAreaName(entityId);
|
|
@@ -163193,6 +163205,22 @@ var BridgeRegistry = class _BridgeRegistry {
|
|
|
163193
163205
|
}
|
|
163194
163206
|
}
|
|
163195
163207
|
}
|
|
163208
|
+
/**
|
|
163209
|
+
* The first already-matched entity the given matcher tests true for.
|
|
163210
|
+
* Server mode pins the primary entity to the first include matcher with
|
|
163211
|
+
* this, independent of HA registry order (#301).
|
|
163212
|
+
*/
|
|
163213
|
+
firstEntityMatching(matcher) {
|
|
163214
|
+
const labels = this.registry.labels;
|
|
163215
|
+
for (const entity of values3(this._entities)) {
|
|
163216
|
+
const device = this.registry.devices[entity.device_id];
|
|
163217
|
+
const state = this.registry.states[entity.entity_id];
|
|
163218
|
+
if (testMatchers([matcher], device, entity, "any", state, labels)) {
|
|
163219
|
+
return entity.entity_id;
|
|
163220
|
+
}
|
|
163221
|
+
}
|
|
163222
|
+
return void 0;
|
|
163223
|
+
}
|
|
163196
163224
|
matchesFilter(filter, entity, device, entityState) {
|
|
163197
163225
|
const labels = this.registry.labels;
|
|
163198
163226
|
if (filter.include.length > 0 && !testMatchers(
|
|
@@ -163256,7 +163284,7 @@ var ServerModeBridge = class {
|
|
|
163256
163284
|
rotationTimer = null;
|
|
163257
163285
|
maxSessionAgeMs = 0;
|
|
163258
163286
|
// Tracks the last synced state JSON per entity to avoid pushing unchanged states.
|
|
163259
|
-
|
|
163287
|
+
lastSyncedStates = /* @__PURE__ */ new Map();
|
|
163260
163288
|
// Session lifecycle diagnostic handlers (non-destructive, logging only).
|
|
163261
163289
|
// biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
|
|
163262
163290
|
sessionDiagHandler;
|
|
@@ -163271,7 +163299,7 @@ var ServerModeBridge = class {
|
|
|
163271
163299
|
return this.dataProvider.withMetadata(
|
|
163272
163300
|
this.status,
|
|
163273
163301
|
this.server,
|
|
163274
|
-
this.endpointManager.
|
|
163302
|
+
this.endpointManager.devices.length,
|
|
163275
163303
|
this.endpointManager.failedEntities
|
|
163276
163304
|
);
|
|
163277
163305
|
}
|
|
@@ -163328,7 +163356,7 @@ var ServerModeBridge = class {
|
|
|
163328
163356
|
if (this.status.code === BridgeStatus.Running) {
|
|
163329
163357
|
return;
|
|
163330
163358
|
}
|
|
163331
|
-
this.
|
|
163359
|
+
this.lastSyncedStates.clear();
|
|
163332
163360
|
try {
|
|
163333
163361
|
this.setStatus({
|
|
163334
163362
|
code: BridgeStatus.Starting,
|
|
@@ -163712,28 +163740,34 @@ ${e?.toString()}`);
|
|
|
163712
163740
|
if (this.status.code !== BridgeStatus.Running) {
|
|
163713
163741
|
return;
|
|
163714
163742
|
}
|
|
163715
|
-
const
|
|
163716
|
-
if (
|
|
163743
|
+
const devices = this.endpointManager.devices;
|
|
163744
|
+
if (devices.length === 0) {
|
|
163717
163745
|
return;
|
|
163718
163746
|
}
|
|
163719
|
-
|
|
163720
|
-
|
|
163721
|
-
|
|
163722
|
-
|
|
163723
|
-
|
|
163724
|
-
|
|
163725
|
-
|
|
163726
|
-
|
|
163727
|
-
|
|
163728
|
-
|
|
163729
|
-
|
|
163730
|
-
|
|
163731
|
-
|
|
163732
|
-
|
|
163733
|
-
|
|
163747
|
+
const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
|
|
163748
|
+
let pushed = 0;
|
|
163749
|
+
for (const device of devices) {
|
|
163750
|
+
try {
|
|
163751
|
+
if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
|
|
163752
|
+
continue;
|
|
163753
|
+
}
|
|
163754
|
+
const behavior = device.stateOf(HomeAssistantEntityBehavior2);
|
|
163755
|
+
const currentEntity = behavior.entity;
|
|
163756
|
+
if (currentEntity?.state) {
|
|
163757
|
+
await device.setStateOf(HomeAssistantEntityBehavior2, {
|
|
163758
|
+
entity: {
|
|
163759
|
+
...currentEntity,
|
|
163760
|
+
state: makeWarmStartState(currentEntity.state)
|
|
163761
|
+
}
|
|
163762
|
+
});
|
|
163763
|
+
pushed++;
|
|
163764
|
+
}
|
|
163765
|
+
} catch (e) {
|
|
163766
|
+
this.log.debug("Warm-start: Failed to push state:", e);
|
|
163734
163767
|
}
|
|
163735
|
-
}
|
|
163736
|
-
|
|
163768
|
+
}
|
|
163769
|
+
if (pushed > 0) {
|
|
163770
|
+
this.log.info(`Warm-start: Pushed initial state for ${pushed} devices`);
|
|
163737
163771
|
}
|
|
163738
163772
|
}
|
|
163739
163773
|
async delete() {
|
|
@@ -163751,38 +163785,43 @@ ${e?.toString()}`);
|
|
|
163751
163785
|
if (!this.dataProvider.featureFlags?.autoForceSync) {
|
|
163752
163786
|
return 0;
|
|
163753
163787
|
}
|
|
163754
|
-
const
|
|
163755
|
-
if (
|
|
163788
|
+
const devices = this.endpointManager.devices;
|
|
163789
|
+
if (devices.length === 0) {
|
|
163756
163790
|
return 0;
|
|
163757
163791
|
}
|
|
163758
|
-
|
|
163759
|
-
|
|
163760
|
-
|
|
163761
|
-
|
|
163762
|
-
|
|
163763
|
-
|
|
163764
|
-
|
|
163765
|
-
|
|
163766
|
-
const
|
|
163767
|
-
|
|
163768
|
-
|
|
163769
|
-
|
|
163770
|
-
|
|
163771
|
-
await device.setStateOf(HomeAssistantEntityBehavior2, {
|
|
163772
|
-
entity: {
|
|
163773
|
-
...currentEntity,
|
|
163774
|
-
state: { ...currentEntity.state }
|
|
163775
|
-
}
|
|
163792
|
+
const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
|
|
163793
|
+
let synced = 0;
|
|
163794
|
+
for (const device of devices) {
|
|
163795
|
+
try {
|
|
163796
|
+
if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
|
|
163797
|
+
continue;
|
|
163798
|
+
}
|
|
163799
|
+
const behavior = device.stateOf(HomeAssistantEntityBehavior2);
|
|
163800
|
+
const currentEntity = behavior.entity;
|
|
163801
|
+
if (currentEntity?.state) {
|
|
163802
|
+
const stateJson = JSON.stringify({
|
|
163803
|
+
s: currentEntity.state.state,
|
|
163804
|
+
a: currentEntity.state.attributes
|
|
163776
163805
|
});
|
|
163777
|
-
this.
|
|
163778
|
-
|
|
163779
|
-
|
|
163806
|
+
if (stateJson !== this.lastSyncedStates.get(device.entityId)) {
|
|
163807
|
+
await device.setStateOf(HomeAssistantEntityBehavior2, {
|
|
163808
|
+
entity: {
|
|
163809
|
+
...currentEntity,
|
|
163810
|
+
state: { ...currentEntity.state }
|
|
163811
|
+
}
|
|
163812
|
+
});
|
|
163813
|
+
this.lastSyncedStates.set(device.entityId, stateJson);
|
|
163814
|
+
synced++;
|
|
163815
|
+
}
|
|
163780
163816
|
}
|
|
163817
|
+
} catch (e) {
|
|
163818
|
+
this.log.debug("Force sync: Failed due to error:", e);
|
|
163781
163819
|
}
|
|
163782
|
-
} catch (e) {
|
|
163783
|
-
this.log.debug("Force sync: Failed due to error:", e);
|
|
163784
163820
|
}
|
|
163785
|
-
|
|
163821
|
+
if (synced > 0) {
|
|
163822
|
+
this.log.info(`Force sync: Pushed ${synced} changed devices`);
|
|
163823
|
+
}
|
|
163824
|
+
return synced;
|
|
163786
163825
|
}
|
|
163787
163826
|
};
|
|
163788
163827
|
|
|
@@ -164109,6 +164148,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
|
|
|
164109
164148
|
};
|
|
164110
164149
|
|
|
164111
164150
|
// src/services/bridges/server-mode-endpoint-manager.ts
|
|
164151
|
+
var MAX_SERVER_MODE_DEVICES = 10;
|
|
164112
164152
|
var ServerModeEndpointManager = class extends Service {
|
|
164113
164153
|
constructor(serverNode, client, registry2, mappingStorage, dataProvider, log) {
|
|
164114
164154
|
super("ServerModeEndpointManager");
|
|
@@ -164128,16 +164168,13 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164128
164168
|
entityIds = [];
|
|
164129
164169
|
unsubscribe;
|
|
164130
164170
|
_failedEntities = [];
|
|
164131
|
-
|
|
164132
|
-
mappingFingerprint = "";
|
|
164171
|
+
endpoints = /* @__PURE__ */ new Map();
|
|
164133
164172
|
get failedEntities() {
|
|
164134
164173
|
return this._failedEntities;
|
|
164135
164174
|
}
|
|
164136
|
-
/**
|
|
164137
|
-
|
|
164138
|
-
|
|
164139
|
-
get device() {
|
|
164140
|
-
return this.deviceEndpoint;
|
|
164175
|
+
/** All device endpoints, primary first. */
|
|
164176
|
+
get devices() {
|
|
164177
|
+
return [...this.endpoints.values()].map((entry) => entry.endpoint);
|
|
164141
164178
|
}
|
|
164142
164179
|
getEntityMapping(entityId) {
|
|
164143
164180
|
return this.mappingStorage.getMapping(this.dataProvider.id, entityId);
|
|
@@ -164148,14 +164185,17 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164148
164185
|
}
|
|
164149
164186
|
async dispose() {
|
|
164150
164187
|
this.stopObserving();
|
|
164151
|
-
|
|
164188
|
+
for (const [entityId, entry] of this.endpoints) {
|
|
164152
164189
|
try {
|
|
164153
|
-
await
|
|
164190
|
+
await entry.endpoint.close();
|
|
164154
164191
|
} catch (e) {
|
|
164155
|
-
this.log.warn(
|
|
164192
|
+
this.log.warn(
|
|
164193
|
+
`Failed to close endpoint ${entityId} during dispose:`,
|
|
164194
|
+
e
|
|
164195
|
+
);
|
|
164156
164196
|
}
|
|
164157
|
-
this.deviceEndpoint = void 0;
|
|
164158
164197
|
}
|
|
164198
|
+
this.endpoints.clear();
|
|
164159
164199
|
}
|
|
164160
164200
|
async startObserving() {
|
|
164161
164201
|
this.stopObserving();
|
|
@@ -164171,12 +164211,9 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164171
164211
|
}
|
|
164172
164212
|
collectSubscriptionEntityIds() {
|
|
164173
164213
|
const ids = new Set(this.entityIds);
|
|
164174
|
-
|
|
164175
|
-
const
|
|
164176
|
-
|
|
164177
|
-
for (const mappedId of mappedIds) {
|
|
164178
|
-
ids.add(mappedId);
|
|
164179
|
-
}
|
|
164214
|
+
for (const entry of this.endpoints.values()) {
|
|
164215
|
+
for (const mappedId of entry.endpoint.mappedEntityIds) {
|
|
164216
|
+
ids.add(mappedId);
|
|
164180
164217
|
}
|
|
164181
164218
|
}
|
|
164182
164219
|
return [...ids];
|
|
@@ -164185,120 +164222,153 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164185
164222
|
this.unsubscribe?.();
|
|
164186
164223
|
this.unsubscribe = void 0;
|
|
164187
164224
|
}
|
|
164225
|
+
/** Primary first (the entity the first include matcher tests true for). */
|
|
164226
|
+
orderEntityIds(ids) {
|
|
164227
|
+
const firstMatcher = this.dataProvider.filter?.include?.[0];
|
|
164228
|
+
const primary = firstMatcher ? this.registry.firstEntityMatching(firstMatcher) : void 0;
|
|
164229
|
+
if (!primary || !ids.includes(primary)) {
|
|
164230
|
+
return [...ids];
|
|
164231
|
+
}
|
|
164232
|
+
return [primary, ...ids.filter((id) => id !== primary)];
|
|
164233
|
+
}
|
|
164234
|
+
async removeEndpoints(entityIds) {
|
|
164235
|
+
for (const entityId of entityIds) {
|
|
164236
|
+
const entry = this.endpoints.get(entityId);
|
|
164237
|
+
if (!entry) continue;
|
|
164238
|
+
try {
|
|
164239
|
+
await entry.endpoint.delete();
|
|
164240
|
+
} catch (e) {
|
|
164241
|
+
this.log.warn(`Failed to delete endpoint ${entityId}:`, e);
|
|
164242
|
+
}
|
|
164243
|
+
this.serverNode.forgetDevice(entry.endpoint);
|
|
164244
|
+
this.endpoints.delete(entityId);
|
|
164245
|
+
}
|
|
164246
|
+
}
|
|
164188
164247
|
async refreshDevices() {
|
|
164189
164248
|
this.registry.refresh();
|
|
164190
164249
|
this._failedEntities = [];
|
|
164191
164250
|
this.entityIds = this.registry.entityIds;
|
|
164192
|
-
|
|
164193
|
-
this.
|
|
164194
|
-
|
|
164195
|
-
|
|
164196
|
-
reason: "No Home Assistant entity matched this bridge's filter. Check for typos or renamed/removed entities."
|
|
164197
|
-
});
|
|
164198
|
-
return;
|
|
164199
|
-
}
|
|
164200
|
-
if (this.entityIds.length > 1) {
|
|
164201
|
-
this.log.warn(
|
|
164202
|
-
`Server mode only supports a single device, but ${this.entityIds.length} entities are configured. Only the first entity will be exposed. Remove other entities from this bridge for proper operation.`
|
|
164203
|
-
);
|
|
164204
|
-
for (let i = 1; i < this.entityIds.length; i++) {
|
|
164251
|
+
try {
|
|
164252
|
+
if (this.entityIds.length === 0) {
|
|
164253
|
+
this.log.warn("Server mode bridge has no entities configured");
|
|
164254
|
+
await this.removeEndpoints([...this.endpoints.keys()]);
|
|
164205
164255
|
this._failedEntities.push({
|
|
164206
|
-
entityId: this.
|
|
164207
|
-
reason: "
|
|
164256
|
+
entityId: this.dataProvider.filter?.include?.[0]?.value ?? "(no entity configured)",
|
|
164257
|
+
reason: "No Home Assistant entity matched this bridge's filter. Check for typos or renamed/removed entities."
|
|
164208
164258
|
});
|
|
164209
|
-
}
|
|
164210
|
-
this.entityIds = [this.entityIds[0]];
|
|
164211
|
-
}
|
|
164212
|
-
const entityId = this.entityIds[0];
|
|
164213
|
-
const mapping = this.getEntityMapping(entityId);
|
|
164214
|
-
if (mapping?.disabled) {
|
|
164215
|
-
this.log.warn(
|
|
164216
|
-
`The only entity in server mode bridge is disabled: ${entityId}`
|
|
164217
|
-
);
|
|
164218
|
-
this._failedEntities.push({
|
|
164219
|
-
entityId,
|
|
164220
|
-
reason: "The configured entity is disabled for this bridge."
|
|
164221
|
-
});
|
|
164222
|
-
return;
|
|
164223
|
-
}
|
|
164224
|
-
const currentFp = this.computeMappingFingerprint(mapping);
|
|
164225
|
-
if (this.deviceEndpoint) {
|
|
164226
|
-
const entityChanged = this.deviceEndpoint.entityId !== entityId;
|
|
164227
|
-
if (!entityChanged && currentFp === this.mappingFingerprint) {
|
|
164228
|
-
this.log.debug(`Device endpoint already exists for ${entityId}`);
|
|
164229
164259
|
return;
|
|
164230
164260
|
}
|
|
164231
|
-
this.
|
|
164232
|
-
|
|
164233
|
-
)
|
|
164234
|
-
|
|
164235
|
-
|
|
164236
|
-
|
|
164261
|
+
const orderedIds = this.orderEntityIds(this.entityIds);
|
|
164262
|
+
const surplus = orderedIds.splice(MAX_SERVER_MODE_DEVICES);
|
|
164263
|
+
for (const entityId of surplus) {
|
|
164264
|
+
this._failedEntities.push({
|
|
164265
|
+
entityId,
|
|
164266
|
+
reason: `Server mode exposes at most ${MAX_SERVER_MODE_DEVICES} devices per node. Remove extra entities or create another standalone device.`
|
|
164267
|
+
});
|
|
164268
|
+
}
|
|
164269
|
+
if (surplus.length > 0) {
|
|
164237
164270
|
this.log.warn(
|
|
164238
|
-
`
|
|
164239
|
-
e
|
|
164271
|
+
`Server mode node is capped at ${MAX_SERVER_MODE_DEVICES} devices, ${surplus.length} entities skipped`
|
|
164240
164272
|
);
|
|
164241
164273
|
}
|
|
164242
|
-
|
|
164243
|
-
this.
|
|
164244
|
-
|
|
164245
|
-
|
|
164246
|
-
|
|
164247
|
-
|
|
164248
|
-
|
|
164249
|
-
|
|
164250
|
-
|
|
164251
|
-
|
|
164252
|
-
|
|
164253
|
-
|
|
164254
|
-
|
|
164255
|
-
|
|
164256
|
-
|
|
164257
|
-
|
|
164258
|
-
|
|
164259
|
-
|
|
164260
|
-
|
|
164261
|
-
|
|
164274
|
+
const keep = new Set(orderedIds);
|
|
164275
|
+
const removed = [...this.endpoints.keys()].filter((id) => !keep.has(id));
|
|
164276
|
+
let structureChanged = removed.length > 0;
|
|
164277
|
+
await this.removeEndpoints(removed);
|
|
164278
|
+
for (const entityId of orderedIds) {
|
|
164279
|
+
const mapping = this.getEntityMapping(entityId);
|
|
164280
|
+
if (mapping?.disabled) {
|
|
164281
|
+
this.log.warn(
|
|
164282
|
+
`Entity in server mode bridge is disabled: ${entityId}`
|
|
164283
|
+
);
|
|
164284
|
+
if (this.endpoints.has(entityId)) {
|
|
164285
|
+
await this.removeEndpoints([entityId]);
|
|
164286
|
+
structureChanged = true;
|
|
164287
|
+
}
|
|
164288
|
+
this._failedEntities.push({
|
|
164289
|
+
entityId,
|
|
164290
|
+
reason: "The configured entity is disabled for this bridge."
|
|
164291
|
+
});
|
|
164292
|
+
continue;
|
|
164293
|
+
}
|
|
164294
|
+
const fingerprint = this.computeMappingFingerprint(mapping);
|
|
164295
|
+
const existing = this.endpoints.get(entityId);
|
|
164296
|
+
if (existing && existing.fingerprint === fingerprint) {
|
|
164297
|
+
this.log.debug(`Device endpoint already exists for ${entityId}`);
|
|
164298
|
+
continue;
|
|
164299
|
+
}
|
|
164300
|
+
if (existing) {
|
|
164301
|
+
this.log.info(`Mapping changed for ${entityId}, recreating endpoint`);
|
|
164302
|
+
await this.removeEndpoints([entityId]);
|
|
164303
|
+
structureChanged = true;
|
|
164304
|
+
}
|
|
164305
|
+
const claimedBy = [...this.endpoints.entries()].find(
|
|
164306
|
+
([, entry]) => entry.endpoint.mappedEntityIds.includes(entityId)
|
|
164262
164307
|
);
|
|
164263
|
-
if (
|
|
164308
|
+
if (claimedBy) {
|
|
164264
164309
|
this._failedEntities.push({
|
|
164265
164310
|
entityId,
|
|
164266
|
-
reason:
|
|
164311
|
+
reason: `Already exposed through ${claimedBy[0]} on this node.`
|
|
164267
164312
|
});
|
|
164268
|
-
|
|
164313
|
+
continue;
|
|
164269
164314
|
}
|
|
164270
|
-
|
|
164271
|
-
|
|
164272
|
-
|
|
164273
|
-
await this.updateServerNodeIdentity(entityId, mapping);
|
|
164274
|
-
this.log.info(
|
|
164275
|
-
`Server mode: Added vacuum ${entityId} as standalone device`
|
|
164315
|
+
const endpointId = createEndpointId(entityId, mapping?.customName);
|
|
164316
|
+
const collision = [...this.endpoints.entries()].find(
|
|
164317
|
+
([, entry]) => entry.endpoint.id === endpointId
|
|
164276
164318
|
);
|
|
164277
|
-
|
|
164319
|
+
if (collision) {
|
|
164320
|
+
this._failedEntities.push({
|
|
164321
|
+
entityId,
|
|
164322
|
+
reason: `Endpoint id collides with ${collision[0]}. Set distinct custom names.`
|
|
164323
|
+
});
|
|
164324
|
+
continue;
|
|
164325
|
+
}
|
|
164326
|
+
if (isHeapUnderPressure()) {
|
|
164327
|
+
this.log.error(
|
|
164328
|
+
"Memory pressure detected, cannot create device endpoint. Reduce entities on other bridges or increase the Node.js heap size (NODE_OPTIONS=--max-old-space-size=1024)."
|
|
164329
|
+
);
|
|
164330
|
+
this._failedEntities.push({
|
|
164331
|
+
entityId,
|
|
164332
|
+
reason: "Skipped due to memory pressure, reduce entities or increase heap size"
|
|
164333
|
+
});
|
|
164334
|
+
continue;
|
|
164335
|
+
}
|
|
164336
|
+
try {
|
|
164337
|
+
const domain = entityId.split(".")[0];
|
|
164338
|
+
const endpoint = domain === "vacuum" ? await this.createServerModeVacuumEndpoint(entityId, mapping) : await LegacyEndpoint.create(
|
|
164339
|
+
this.registry,
|
|
164340
|
+
entityId,
|
|
164341
|
+
mapping,
|
|
164342
|
+
void 0,
|
|
164343
|
+
true
|
|
164344
|
+
);
|
|
164345
|
+
if (!endpoint) {
|
|
164346
|
+
this._failedEntities.push({
|
|
164347
|
+
entityId,
|
|
164348
|
+
reason: "Failed to create endpoint - unsupported device type"
|
|
164349
|
+
});
|
|
164350
|
+
continue;
|
|
164351
|
+
}
|
|
164352
|
+
await this.serverNode.addDevice(endpoint);
|
|
164353
|
+
this.endpoints.set(entityId, { endpoint, fingerprint });
|
|
164354
|
+
structureChanged = true;
|
|
164355
|
+
this.log.info(`Server mode: Added device ${entityId}`);
|
|
164356
|
+
} catch (e) {
|
|
164357
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
164358
|
+
this.log.error(`Failed to create server mode device ${entityId}:`, e);
|
|
164359
|
+
this._failedEntities.push({ entityId, reason });
|
|
164360
|
+
}
|
|
164278
164361
|
}
|
|
164279
|
-
|
|
164280
|
-
this.
|
|
164281
|
-
|
|
164282
|
-
|
|
164283
|
-
|
|
164284
|
-
|
|
164285
|
-
|
|
164286
|
-
|
|
164287
|
-
|
|
164288
|
-
entityId,
|
|
164289
|
-
reason: "Failed to create endpoint - unsupported device type"
|
|
164290
|
-
});
|
|
164291
|
-
return;
|
|
164362
|
+
if (structureChanged) {
|
|
164363
|
+
const primary = orderedIds.find((id) => this.endpoints.has(id));
|
|
164364
|
+
if (primary) {
|
|
164365
|
+
await this.updateServerNodeIdentity(
|
|
164366
|
+
primary,
|
|
164367
|
+
this.getEntityMapping(primary),
|
|
164368
|
+
this.endpoints.get(primary)?.endpoint
|
|
164369
|
+
);
|
|
164370
|
+
}
|
|
164292
164371
|
}
|
|
164293
|
-
await this.serverNode.addDevice(endpoint);
|
|
164294
|
-
this.deviceEndpoint = endpoint;
|
|
164295
|
-
this.mappingFingerprint = currentFp;
|
|
164296
|
-
await this.updateServerNodeIdentity(entityId, mapping);
|
|
164297
|
-
this.log.info(`Server mode: Added device ${entityId}`);
|
|
164298
|
-
} catch (e) {
|
|
164299
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
164300
|
-
this.log.error(`Failed to create server mode device ${entityId}:`, e);
|
|
164301
|
-
this._failedEntities.push({ entityId, reason });
|
|
164302
164372
|
} finally {
|
|
164303
164373
|
if (this.unsubscribe) {
|
|
164304
164374
|
this.startObserving();
|
|
@@ -164307,15 +164377,18 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164307
164377
|
}
|
|
164308
164378
|
async updateStates(states) {
|
|
164309
164379
|
this.registry.mergeExternalStates(states);
|
|
164310
|
-
|
|
164380
|
+
for (const [entityId, entry] of this.endpoints) {
|
|
164311
164381
|
try {
|
|
164312
|
-
await
|
|
164382
|
+
await entry.endpoint.updateStates(states);
|
|
164313
164383
|
} catch (e) {
|
|
164314
|
-
this.log.warn(
|
|
164384
|
+
this.log.warn(
|
|
164385
|
+
`State update failed for server mode endpoint ${entityId}:`,
|
|
164386
|
+
e
|
|
164387
|
+
);
|
|
164315
164388
|
}
|
|
164316
164389
|
}
|
|
164317
164390
|
}
|
|
164318
|
-
async updateServerNodeIdentity(entityId, mapping) {
|
|
164391
|
+
async updateServerNodeIdentity(entityId, mapping, endpoint) {
|
|
164319
164392
|
const device = this.registry.deviceOf(entityId);
|
|
164320
164393
|
const state = this.registry.initialState(entityId);
|
|
164321
164394
|
const friendlyName = state?.attributes?.friendly_name;
|
|
@@ -164325,7 +164398,7 @@ var ServerModeEndpointManager = class extends Service {
|
|
|
164325
164398
|
mapping,
|
|
164326
164399
|
friendlyName
|
|
164327
164400
|
);
|
|
164328
|
-
const deviceType =
|
|
164401
|
+
const deviceType = endpoint?.type?.deviceType;
|
|
164329
164402
|
if (deviceType != null) {
|
|
164330
164403
|
await this.serverNode.updateAdvertisedDeviceType(deviceType);
|
|
164331
164404
|
}
|