@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.
@@ -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. Add only ONE vacuum to this bridge.",
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 the entity as a standalone Matter device instead of a bridged device. Works for any supported device type, e.g. robot vacuums need it for Apple Home Siri voice commands. IMPORTANT: Only ONE device should be in this bridge when server mode is enabled.",
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
- deviceEndpoint;
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 the device endpoint to this server node.
146042
- * In server mode, only ONE device is allowed.
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.deviceEndpoint) {
146049
+ if (this.deviceEndpoints.has(endpoint.id)) {
146047
146050
  return;
146048
146051
  }
146049
- this.deviceEndpoint = endpoint;
146052
+ this.deviceEndpoints.set(endpoint.id, endpoint);
146050
146053
  await this.add(endpoint);
146051
146054
  }
146052
146055
  /**
146053
- * Clear the device reference after the endpoint has been deleted externally.
146054
- * Must be called before addDevice() when replacing the device endpoint.
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
- clearDevice() {
146057
- this.deviceEndpoint = void 0;
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 (registry2.isAutoComposedDevicesEnabled() && effectiveMapping?.composedEntities && effectiveMapping.composedEntities.length > 0) {
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
- lastSyncedState;
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.device ? 1 : 0,
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.lastSyncedState = void 0;
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 device = this.endpointManager.device;
163716
- if (!device) {
163743
+ const devices = this.endpointManager.devices;
163744
+ if (devices.length === 0) {
163717
163745
  return;
163718
163746
  }
163719
- try {
163720
- const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
163721
- if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
163722
- return;
163723
- }
163724
- const behavior = device.stateOf(HomeAssistantEntityBehavior2);
163725
- const currentEntity = behavior.entity;
163726
- if (currentEntity?.state) {
163727
- await device.setStateOf(HomeAssistantEntityBehavior2, {
163728
- entity: {
163729
- ...currentEntity,
163730
- state: makeWarmStartState(currentEntity.state)
163731
- }
163732
- });
163733
- this.log.info("Warm-start: Pushed initial device state");
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
- } catch (e) {
163736
- this.log.debug("Warm-start: Failed to push state:", e);
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 device = this.endpointManager.device;
163755
- if (!device) {
163788
+ const devices = this.endpointManager.devices;
163789
+ if (devices.length === 0) {
163756
163790
  return 0;
163757
163791
  }
163758
- try {
163759
- const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
163760
- if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
163761
- return 0;
163762
- }
163763
- const behavior = device.stateOf(HomeAssistantEntityBehavior2);
163764
- const currentEntity = behavior.entity;
163765
- if (currentEntity?.state) {
163766
- const stateJson = JSON.stringify({
163767
- s: currentEntity.state.state,
163768
- a: currentEntity.state.attributes
163769
- });
163770
- if (stateJson !== this.lastSyncedState) {
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.lastSyncedState = stateJson;
163778
- this.log.info("Force sync: Pushed 1 changed device");
163779
- return 1;
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
- return 0;
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
- deviceEndpoint;
164132
- mappingFingerprint = "";
164171
+ endpoints = /* @__PURE__ */ new Map();
164133
164172
  get failedEntities() {
164134
164173
  return this._failedEntities;
164135
164174
  }
164136
- /**
164137
- * Returns the device endpoint (for server mode, this is the single device)
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
- if (this.deviceEndpoint) {
164188
+ for (const [entityId, entry] of this.endpoints) {
164152
164189
  try {
164153
- await this.deviceEndpoint.close();
164190
+ await entry.endpoint.close();
164154
164191
  } catch (e) {
164155
- this.log.warn(`Failed to close device endpoint during dispose:`, e);
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
- if (this.deviceEndpoint) {
164175
- const mappedIds = this.deviceEndpoint.mappedEntityIds;
164176
- if (mappedIds) {
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
- if (this.entityIds.length === 0) {
164193
- this.log.warn("Server mode bridge has no entities configured");
164194
- this._failedEntities.push({
164195
- entityId: this.dataProvider.filter?.include?.[0]?.value ?? "(no entity configured)",
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.entityIds[i],
164207
- reason: "Server mode only supports a single device. Remove other entities from this bridge."
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.log.info(
164232
- entityChanged ? `Entity changed from ${this.deviceEndpoint.entityId} to ${entityId}, recreating endpoint` : `Mapping changed for ${entityId}, recreating endpoint`
164233
- );
164234
- try {
164235
- await this.deviceEndpoint.delete();
164236
- } catch (e) {
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
- `Failed to delete endpoint ${entityId} for mapping change:`,
164239
- e
164271
+ `Server mode node is capped at ${MAX_SERVER_MODE_DEVICES} devices, ${surplus.length} entities skipped`
164240
164272
  );
164241
164273
  }
164242
- this.serverNode.clearDevice();
164243
- this.deviceEndpoint = void 0;
164244
- this.mappingFingerprint = "";
164245
- }
164246
- if (isHeapUnderPressure()) {
164247
- this.log.error(
164248
- "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)."
164249
- );
164250
- this._failedEntities.push({
164251
- entityId,
164252
- reason: "Skipped due to memory pressure, reduce entities or increase heap size"
164253
- });
164254
- return;
164255
- }
164256
- try {
164257
- const domain = entityId.split(".")[0];
164258
- if (domain === "vacuum") {
164259
- const endpoint2 = await this.createServerModeVacuumEndpoint(
164260
- entityId,
164261
- mapping
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 (!endpoint2) {
164308
+ if (claimedBy) {
164264
164309
  this._failedEntities.push({
164265
164310
  entityId,
164266
- reason: "Failed to create vacuum endpoint - unsupported device"
164311
+ reason: `Already exposed through ${claimedBy[0]} on this node.`
164267
164312
  });
164268
- return;
164313
+ continue;
164269
164314
  }
164270
- await this.serverNode.addDevice(endpoint2);
164271
- this.deviceEndpoint = endpoint2;
164272
- this.mappingFingerprint = currentFp;
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
- return;
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
- const endpoint = await LegacyEndpoint.create(
164280
- this.registry,
164281
- entityId,
164282
- mapping,
164283
- void 0,
164284
- true
164285
- );
164286
- if (!endpoint) {
164287
- this._failedEntities.push({
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
- if (this.deviceEndpoint) {
164380
+ for (const [entityId, entry] of this.endpoints) {
164311
164381
  try {
164312
- await this.deviceEndpoint.updateStates(states);
164382
+ await entry.endpoint.updateStates(states);
164313
164383
  } catch (e) {
164314
- this.log.warn("State update failed for server mode endpoint:", e);
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 = this.deviceEndpoint?.type?.deviceType;
164401
+ const deviceType = endpoint?.type?.deviceType;
164329
164402
  if (deviceType != null) {
164330
164403
  await this.serverNode.updateAdvertisedDeviceType(deviceType);
164331
164404
  }