@riddix/hamh 2.1.0-alpha.735 → 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" }],
@@ -124663,8 +124663,8 @@ var init_bridge_config_schema = __esm({
124663
124663
  default: false
124664
124664
  },
124665
124665
  serverMode: {
124666
- title: "Server Mode (for Robot Vacuums)",
124667
- description: "Expose the device as a standalone Matter device instead of a bridged device. This is required for Apple Home to properly support Siri voice commands for Robot Vacuums. IMPORTANT: Only ONE device should be in this bridge when server mode is enabled.",
124666
+ title: "Server Mode (standalone device)",
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
  },
@@ -124822,7 +124822,10 @@ var init_create_bridge_request_schema = __esm({
124822
124822
  "../common/dist/schemas/create-bridge-request-schema.js"() {
124823
124823
  "use strict";
124824
124824
  init_bridge_config_schema();
124825
- createBridgeRequestSchema = bridgeConfigSchema;
124825
+ createBridgeRequestSchema = {
124826
+ ...bridgeConfigSchema,
124827
+ required: ["name", "filter"]
124828
+ };
124826
124829
  }
124827
124830
  });
124828
124831
 
@@ -130502,11 +130505,13 @@ var BridgeService = class extends Service {
130502
130505
  return this.bridges.find((bridge) => bridge.id === id);
130503
130506
  }
130504
130507
  async create(request) {
130505
- if (this.portUsed(request.port)) {
130506
- throw new Error(`Port already in use: ${request.port}`);
130508
+ const port = request.port ?? this.getNextAvailablePort();
130509
+ if (this.portUsed(port)) {
130510
+ throw new Error(`Port already in use: ${port}`);
130507
130511
  }
130508
130512
  const bridge = await this.addBridge({
130509
130513
  ...request,
130514
+ port,
130510
130515
  id: crypto3.randomUUID().replace(/-/g, ""),
130511
130516
  basicInformation: this.props.basicInformation
130512
130517
  });
@@ -145987,7 +145992,7 @@ function trimToLength(value, maxLength, suffix) {
145987
145992
  // src/matter/endpoints/server-mode-server-node.ts
145988
145993
  var logger183 = Logger.get("ServerModeServerNode");
145989
145994
  var ServerModeServerNode = class extends ServerNode {
145990
- deviceEndpoint;
145995
+ deviceEndpoints = /* @__PURE__ */ new Map();
145991
145996
  featureFlags;
145992
145997
  serialNumberSuffix;
145993
145998
  constructor(env, bridgeData) {
@@ -146032,24 +146037,31 @@ var ServerModeServerNode = class extends ServerNode {
146032
146037
  this.featureFlags = bridgeData.featureFlags;
146033
146038
  this.serialNumberSuffix = bridgeData.serialNumberSuffix;
146034
146039
  }
146040
+ /** Number of device endpoints currently attached. */
146041
+ get deviceCount() {
146042
+ return this.deviceEndpoints.size;
146043
+ }
146035
146044
  /**
146036
- * Add the device endpoint to this server node.
146037
- * In server mode, only ONE device is allowed.
146038
- * 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.
146039
146047
  */
146040
146048
  async addDevice(endpoint) {
146041
- if (this.deviceEndpoint) {
146049
+ if (this.deviceEndpoints.has(endpoint.id)) {
146042
146050
  return;
146043
146051
  }
146044
- this.deviceEndpoint = endpoint;
146052
+ this.deviceEndpoints.set(endpoint.id, endpoint);
146045
146053
  await this.add(endpoint);
146046
146054
  }
146047
146055
  /**
146048
- * Clear the device reference after the endpoint has been deleted externally.
146049
- * 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.
146050
146058
  */
146051
- clearDevice() {
146052
- 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();
146053
146065
  }
146054
146066
  /**
146055
146067
  * Update root-level BasicInformation with entity-specific data.
@@ -161492,7 +161504,12 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
161492
161504
  }
161493
161505
  }
161494
161506
  }
161495
- 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) {
161496
161513
  const composedAreaName = registry2.getAreaName(entityId);
161497
161514
  const composed = await UserComposedEndpoint.create({
161498
161515
  registry: registry2,
@@ -161509,7 +161526,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
161509
161526
  `User composed device creation failed for ${entityId}, falling back to standalone`
161510
161527
  );
161511
161528
  }
161512
- if (registry2.isAutoComposedDevicesEnabled()) {
161529
+ if (!standalone && registry2.isAutoComposedDevicesEnabled()) {
161513
161530
  const attrs = state.attributes;
161514
161531
  if (entityId.startsWith("sensor.") && attrs.device_class === SensorDeviceClass.temperature && (effectiveMapping?.humidityEntity || effectiveMapping?.pressureEntity)) {
161515
161532
  const composedAreaName = registry2.getAreaName(entityId);
@@ -161550,7 +161567,7 @@ var LegacyEndpoint = class _LegacyEndpoint extends EntityEndpoint {
161550
161567
  }
161551
161568
  }
161552
161569
  }
161553
- if (entityId.startsWith("climate.") && effectiveMapping?.climateExposeFan === true) {
161570
+ if (!standalone && entityId.startsWith("climate.") && effectiveMapping?.climateExposeFan === true) {
161554
161571
  const climateFeatures = state.attributes.supported_features ?? 0;
161555
161572
  if ((climateFeatures & ClimateDeviceFeature.FAN_MODE) !== 0) {
161556
161573
  const composedAreaName = registry2.getAreaName(entityId);
@@ -163188,6 +163205,22 @@ var BridgeRegistry = class _BridgeRegistry {
163188
163205
  }
163189
163206
  }
163190
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
+ }
163191
163224
  matchesFilter(filter, entity, device, entityState) {
163192
163225
  const labels = this.registry.labels;
163193
163226
  if (filter.include.length > 0 && !testMatchers(
@@ -163251,7 +163284,7 @@ var ServerModeBridge = class {
163251
163284
  rotationTimer = null;
163252
163285
  maxSessionAgeMs = 0;
163253
163286
  // Tracks the last synced state JSON per entity to avoid pushing unchanged states.
163254
- lastSyncedState;
163287
+ lastSyncedStates = /* @__PURE__ */ new Map();
163255
163288
  // Session lifecycle diagnostic handlers (non-destructive, logging only).
163256
163289
  // biome-ignore lint/suspicious/noExplicitAny: matter.js internal types
163257
163290
  sessionDiagHandler;
@@ -163266,7 +163299,7 @@ var ServerModeBridge = class {
163266
163299
  return this.dataProvider.withMetadata(
163267
163300
  this.status,
163268
163301
  this.server,
163269
- this.endpointManager.device ? 1 : 0,
163302
+ this.endpointManager.devices.length,
163270
163303
  this.endpointManager.failedEntities
163271
163304
  );
163272
163305
  }
@@ -163323,7 +163356,7 @@ var ServerModeBridge = class {
163323
163356
  if (this.status.code === BridgeStatus.Running) {
163324
163357
  return;
163325
163358
  }
163326
- this.lastSyncedState = void 0;
163359
+ this.lastSyncedStates.clear();
163327
163360
  try {
163328
163361
  this.setStatus({
163329
163362
  code: BridgeStatus.Starting,
@@ -163707,28 +163740,34 @@ ${e?.toString()}`);
163707
163740
  if (this.status.code !== BridgeStatus.Running) {
163708
163741
  return;
163709
163742
  }
163710
- const device = this.endpointManager.device;
163711
- if (!device) {
163743
+ const devices = this.endpointManager.devices;
163744
+ if (devices.length === 0) {
163712
163745
  return;
163713
163746
  }
163714
- try {
163715
- const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
163716
- if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
163717
- return;
163718
- }
163719
- const behavior = device.stateOf(HomeAssistantEntityBehavior2);
163720
- const currentEntity = behavior.entity;
163721
- if (currentEntity?.state) {
163722
- await device.setStateOf(HomeAssistantEntityBehavior2, {
163723
- entity: {
163724
- ...currentEntity,
163725
- state: makeWarmStartState(currentEntity.state)
163726
- }
163727
- });
163728
- 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);
163729
163767
  }
163730
- } catch (e) {
163731
- 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`);
163732
163771
  }
163733
163772
  }
163734
163773
  async delete() {
@@ -163746,38 +163785,43 @@ ${e?.toString()}`);
163746
163785
  if (!this.dataProvider.featureFlags?.autoForceSync) {
163747
163786
  return 0;
163748
163787
  }
163749
- const device = this.endpointManager.device;
163750
- if (!device) {
163788
+ const devices = this.endpointManager.devices;
163789
+ if (devices.length === 0) {
163751
163790
  return 0;
163752
163791
  }
163753
- try {
163754
- const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
163755
- if (!device.behaviors.has(HomeAssistantEntityBehavior2)) {
163756
- return 0;
163757
- }
163758
- const behavior = device.stateOf(HomeAssistantEntityBehavior2);
163759
- const currentEntity = behavior.entity;
163760
- if (currentEntity?.state) {
163761
- const stateJson = JSON.stringify({
163762
- s: currentEntity.state.state,
163763
- a: currentEntity.state.attributes
163764
- });
163765
- if (stateJson !== this.lastSyncedState) {
163766
- await device.setStateOf(HomeAssistantEntityBehavior2, {
163767
- entity: {
163768
- ...currentEntity,
163769
- state: { ...currentEntity.state }
163770
- }
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
163771
163805
  });
163772
- this.lastSyncedState = stateJson;
163773
- this.log.info("Force sync: Pushed 1 changed device");
163774
- 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
+ }
163775
163816
  }
163817
+ } catch (e) {
163818
+ this.log.debug("Force sync: Failed due to error:", e);
163776
163819
  }
163777
- } catch (e) {
163778
- this.log.debug("Force sync: Failed due to error:", e);
163779
163820
  }
163780
- return 0;
163821
+ if (synced > 0) {
163822
+ this.log.info(`Force sync: Pushed ${synced} changed devices`);
163823
+ }
163824
+ return synced;
163781
163825
  }
163782
163826
  };
163783
163827
 
@@ -164104,38 +164148,36 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
164104
164148
  };
164105
164149
 
164106
164150
  // src/services/bridges/server-mode-endpoint-manager.ts
164151
+ var MAX_SERVER_MODE_DEVICES = 10;
164107
164152
  var ServerModeEndpointManager = class extends Service {
164108
- constructor(serverNode, client, registry2, mappingStorage, bridgeId, log) {
164153
+ constructor(serverNode, client, registry2, mappingStorage, dataProvider, log) {
164109
164154
  super("ServerModeEndpointManager");
164110
164155
  this.serverNode = serverNode;
164111
164156
  this.client = client;
164112
164157
  this.registry = registry2;
164113
164158
  this.mappingStorage = mappingStorage;
164114
- this.bridgeId = bridgeId;
164159
+ this.dataProvider = dataProvider;
164115
164160
  this.log = log;
164116
164161
  }
164117
164162
  serverNode;
164118
164163
  client;
164119
164164
  registry;
164120
164165
  mappingStorage;
164121
- bridgeId;
164166
+ dataProvider;
164122
164167
  log;
164123
164168
  entityIds = [];
164124
164169
  unsubscribe;
164125
164170
  _failedEntities = [];
164126
- deviceEndpoint;
164127
- mappingFingerprint = "";
164171
+ endpoints = /* @__PURE__ */ new Map();
164128
164172
  get failedEntities() {
164129
164173
  return this._failedEntities;
164130
164174
  }
164131
- /**
164132
- * Returns the device endpoint (for server mode, this is the single device)
164133
- */
164134
- get device() {
164135
- return this.deviceEndpoint;
164175
+ /** All device endpoints, primary first. */
164176
+ get devices() {
164177
+ return [...this.endpoints.values()].map((entry) => entry.endpoint);
164136
164178
  }
164137
164179
  getEntityMapping(entityId) {
164138
- return this.mappingStorage.getMapping(this.bridgeId, entityId);
164180
+ return this.mappingStorage.getMapping(this.dataProvider.id, entityId);
164139
164181
  }
164140
164182
  computeMappingFingerprint(mapping) {
164141
164183
  if (!mapping) return "";
@@ -164143,14 +164185,17 @@ var ServerModeEndpointManager = class extends Service {
164143
164185
  }
164144
164186
  async dispose() {
164145
164187
  this.stopObserving();
164146
- if (this.deviceEndpoint) {
164188
+ for (const [entityId, entry] of this.endpoints) {
164147
164189
  try {
164148
- await this.deviceEndpoint.close();
164190
+ await entry.endpoint.close();
164149
164191
  } catch (e) {
164150
- 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
+ );
164151
164196
  }
164152
- this.deviceEndpoint = void 0;
164153
164197
  }
164198
+ this.endpoints.clear();
164154
164199
  }
164155
164200
  async startObserving() {
164156
164201
  this.stopObserving();
@@ -164166,12 +164211,9 @@ var ServerModeEndpointManager = class extends Service {
164166
164211
  }
164167
164212
  collectSubscriptionEntityIds() {
164168
164213
  const ids = new Set(this.entityIds);
164169
- if (this.deviceEndpoint) {
164170
- const mappedIds = this.deviceEndpoint.mappedEntityIds;
164171
- if (mappedIds) {
164172
- for (const mappedId of mappedIds) {
164173
- ids.add(mappedId);
164174
- }
164214
+ for (const entry of this.endpoints.values()) {
164215
+ for (const mappedId of entry.endpoint.mappedEntityIds) {
164216
+ ids.add(mappedId);
164175
164217
  }
164176
164218
  }
164177
164219
  return [...ids];
@@ -164180,128 +164222,173 @@ var ServerModeEndpointManager = class extends Service {
164180
164222
  this.unsubscribe?.();
164181
164223
  this.unsubscribe = void 0;
164182
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
+ }
164183
164247
  async refreshDevices() {
164184
164248
  this.registry.refresh();
164185
164249
  this._failedEntities = [];
164186
164250
  this.entityIds = this.registry.entityIds;
164187
- if (this.entityIds.length === 0) {
164188
- this.log.warn("Server mode bridge has no entities configured");
164189
- return;
164190
- }
164191
- if (this.entityIds.length > 1) {
164192
- this.log.warn(
164193
- `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.`
164194
- );
164195
- 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()]);
164196
164255
  this._failedEntities.push({
164197
- entityId: this.entityIds[i],
164198
- 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."
164199
164258
  });
164200
- }
164201
- this.entityIds = [this.entityIds[0]];
164202
- }
164203
- const entityId = this.entityIds[0];
164204
- const mapping = this.getEntityMapping(entityId);
164205
- if (mapping?.disabled) {
164206
- this.log.warn(
164207
- `The only entity in server mode bridge is disabled: ${entityId}`
164208
- );
164209
- return;
164210
- }
164211
- const currentFp = this.computeMappingFingerprint(mapping);
164212
- if (this.deviceEndpoint) {
164213
- const entityChanged = this.deviceEndpoint.entityId !== entityId;
164214
- if (!entityChanged && currentFp === this.mappingFingerprint) {
164215
- this.log.debug(`Device endpoint already exists for ${entityId}`);
164216
164259
  return;
164217
164260
  }
164218
- this.log.info(
164219
- entityChanged ? `Entity changed from ${this.deviceEndpoint.entityId} to ${entityId}, recreating endpoint` : `Mapping changed for ${entityId}, recreating endpoint`
164220
- );
164221
- try {
164222
- await this.deviceEndpoint.delete();
164223
- } 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) {
164224
164270
  this.log.warn(
164225
- `Failed to delete endpoint ${entityId} for mapping change:`,
164226
- e
164271
+ `Server mode node is capped at ${MAX_SERVER_MODE_DEVICES} devices, ${surplus.length} entities skipped`
164227
164272
  );
164228
164273
  }
164229
- this.serverNode.clearDevice();
164230
- this.deviceEndpoint = void 0;
164231
- this.mappingFingerprint = "";
164232
- }
164233
- if (isHeapUnderPressure()) {
164234
- this.log.error(
164235
- "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)."
164236
- );
164237
- this._failedEntities.push({
164238
- entityId,
164239
- reason: "Skipped due to memory pressure, reduce entities or increase heap size"
164240
- });
164241
- return;
164242
- }
164243
- try {
164244
- const domain = entityId.split(".")[0];
164245
- if (domain === "vacuum") {
164246
- const endpoint2 = await this.createServerModeVacuumEndpoint(
164247
- entityId,
164248
- 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)
164249
164307
  );
164250
- if (!endpoint2) {
164308
+ if (claimedBy) {
164251
164309
  this._failedEntities.push({
164252
164310
  entityId,
164253
- reason: "Failed to create vacuum endpoint - unsupported device"
164311
+ reason: `Already exposed through ${claimedBy[0]} on this node.`
164254
164312
  });
164255
- return;
164313
+ continue;
164256
164314
  }
164257
- await this.serverNode.addDevice(endpoint2);
164258
- this.deviceEndpoint = endpoint2;
164259
- this.mappingFingerprint = currentFp;
164260
- await this.updateServerNodeIdentity(entityId, mapping);
164261
- this.log.info(
164262
- `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
164263
164318
  );
164264
- 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
+ }
164265
164361
  }
164266
- const endpoint = await LegacyEndpoint.create(
164267
- this.registry,
164268
- entityId,
164269
- mapping,
164270
- void 0,
164271
- true
164272
- );
164273
- if (!endpoint) {
164274
- this._failedEntities.push({
164275
- entityId,
164276
- reason: "Failed to create endpoint - unsupported device type"
164277
- });
164278
- 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
+ }
164371
+ }
164372
+ } finally {
164373
+ if (this.unsubscribe) {
164374
+ this.startObserving();
164279
164375
  }
164280
- await this.serverNode.addDevice(endpoint);
164281
- this.deviceEndpoint = endpoint;
164282
- this.mappingFingerprint = currentFp;
164283
- await this.updateServerNodeIdentity(entityId, mapping);
164284
- this.log.info(`Server mode: Added device ${entityId}`);
164285
- } catch (e) {
164286
- const reason = e instanceof Error ? e.message : String(e);
164287
- this.log.error(`Failed to create server mode device ${entityId}:`, e);
164288
- this._failedEntities.push({ entityId, reason });
164289
- }
164290
- if (this.unsubscribe) {
164291
- this.startObserving();
164292
164376
  }
164293
164377
  }
164294
164378
  async updateStates(states) {
164295
164379
  this.registry.mergeExternalStates(states);
164296
- if (this.deviceEndpoint) {
164380
+ for (const [entityId, entry] of this.endpoints) {
164297
164381
  try {
164298
- await this.deviceEndpoint.updateStates(states);
164382
+ await entry.endpoint.updateStates(states);
164299
164383
  } catch (e) {
164300
- 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
+ );
164301
164388
  }
164302
164389
  }
164303
164390
  }
164304
- async updateServerNodeIdentity(entityId, mapping) {
164391
+ async updateServerNodeIdentity(entityId, mapping, endpoint) {
164305
164392
  const device = this.registry.deviceOf(entityId);
164306
164393
  const state = this.registry.initialState(entityId);
164307
164394
  const friendlyName = state?.attributes?.friendly_name;
@@ -164311,7 +164398,7 @@ var ServerModeEndpointManager = class extends Service {
164311
164398
  mapping,
164312
164399
  friendlyName
164313
164400
  );
164314
- const deviceType = this.deviceEndpoint?.type?.deviceType;
164401
+ const deviceType = endpoint?.type?.deviceType;
164315
164402
  if (deviceType != null) {
164316
164403
  await this.serverNode.updateAdvertisedDeviceType(deviceType);
164317
164404
  }
@@ -164482,7 +164569,7 @@ var BridgeEnvironmentFactory = class extends BridgeFactory {
164482
164569
  await env.load(HomeAssistantClient),
164483
164570
  env.get(BridgeRegistry),
164484
164571
  await env.load(EntityMappingStorage),
164485
- dataProvider.id,
164572
+ dataProvider,
164486
164573
  loggerService.get("ServerModeEndpointManager")
164487
164574
  );
164488
164575
  class ServerModeBridgeWithEnvironment extends ServerModeBridge {