@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.
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" }],
|
|
@@ -124663,8 +124663,8 @@ var init_bridge_config_schema = __esm({
|
|
|
124663
124663
|
default: false
|
|
124664
124664
|
},
|
|
124665
124665
|
serverMode: {
|
|
124666
|
-
title: "Server Mode (
|
|
124667
|
-
description: "Expose
|
|
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 =
|
|
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
|
-
|
|
130506
|
-
|
|
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
|
-
|
|
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
|
|
146037
|
-
*
|
|
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.
|
|
146049
|
+
if (this.deviceEndpoints.has(endpoint.id)) {
|
|
146042
146050
|
return;
|
|
146043
146051
|
}
|
|
146044
|
-
this.
|
|
146052
|
+
this.deviceEndpoints.set(endpoint.id, endpoint);
|
|
146045
146053
|
await this.add(endpoint);
|
|
146046
146054
|
}
|
|
146047
146055
|
/**
|
|
146048
|
-
*
|
|
146049
|
-
* 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.
|
|
146050
146058
|
*/
|
|
146051
|
-
|
|
146052
|
-
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();
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
163711
|
-
if (
|
|
163743
|
+
const devices = this.endpointManager.devices;
|
|
163744
|
+
if (devices.length === 0) {
|
|
163712
163745
|
return;
|
|
163713
163746
|
}
|
|
163714
|
-
|
|
163715
|
-
|
|
163716
|
-
|
|
163717
|
-
|
|
163718
|
-
|
|
163719
|
-
|
|
163720
|
-
|
|
163721
|
-
|
|
163722
|
-
|
|
163723
|
-
|
|
163724
|
-
|
|
163725
|
-
|
|
163726
|
-
|
|
163727
|
-
|
|
163728
|
-
|
|
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
|
-
}
|
|
163731
|
-
|
|
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
|
|
163750
|
-
if (
|
|
163788
|
+
const devices = this.endpointManager.devices;
|
|
163789
|
+
if (devices.length === 0) {
|
|
163751
163790
|
return 0;
|
|
163752
163791
|
}
|
|
163753
|
-
|
|
163754
|
-
|
|
163755
|
-
|
|
163756
|
-
|
|
163757
|
-
|
|
163758
|
-
|
|
163759
|
-
|
|
163760
|
-
|
|
163761
|
-
const
|
|
163762
|
-
|
|
163763
|
-
|
|
163764
|
-
|
|
163765
|
-
|
|
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.
|
|
163773
|
-
|
|
163774
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
164159
|
+
this.dataProvider = dataProvider;
|
|
164115
164160
|
this.log = log;
|
|
164116
164161
|
}
|
|
164117
164162
|
serverNode;
|
|
164118
164163
|
client;
|
|
164119
164164
|
registry;
|
|
164120
164165
|
mappingStorage;
|
|
164121
|
-
|
|
164166
|
+
dataProvider;
|
|
164122
164167
|
log;
|
|
164123
164168
|
entityIds = [];
|
|
164124
164169
|
unsubscribe;
|
|
164125
164170
|
_failedEntities = [];
|
|
164126
|
-
|
|
164127
|
-
mappingFingerprint = "";
|
|
164171
|
+
endpoints = /* @__PURE__ */ new Map();
|
|
164128
164172
|
get failedEntities() {
|
|
164129
164173
|
return this._failedEntities;
|
|
164130
164174
|
}
|
|
164131
|
-
/**
|
|
164132
|
-
|
|
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.
|
|
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
|
-
|
|
164188
|
+
for (const [entityId, entry] of this.endpoints) {
|
|
164147
164189
|
try {
|
|
164148
|
-
await
|
|
164190
|
+
await entry.endpoint.close();
|
|
164149
164191
|
} catch (e) {
|
|
164150
|
-
this.log.warn(
|
|
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
|
-
|
|
164170
|
-
const
|
|
164171
|
-
|
|
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
|
-
|
|
164188
|
-
this.
|
|
164189
|
-
|
|
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.
|
|
164198
|
-
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."
|
|
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.
|
|
164219
|
-
|
|
164220
|
-
)
|
|
164221
|
-
|
|
164222
|
-
|
|
164223
|
-
|
|
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
|
-
`
|
|
164226
|
-
e
|
|
164271
|
+
`Server mode node is capped at ${MAX_SERVER_MODE_DEVICES} devices, ${surplus.length} entities skipped`
|
|
164227
164272
|
);
|
|
164228
164273
|
}
|
|
164229
|
-
|
|
164230
|
-
this.
|
|
164231
|
-
|
|
164232
|
-
|
|
164233
|
-
|
|
164234
|
-
|
|
164235
|
-
|
|
164236
|
-
|
|
164237
|
-
|
|
164238
|
-
|
|
164239
|
-
|
|
164240
|
-
|
|
164241
|
-
|
|
164242
|
-
|
|
164243
|
-
|
|
164244
|
-
|
|
164245
|
-
|
|
164246
|
-
|
|
164247
|
-
|
|
164248
|
-
|
|
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 (
|
|
164308
|
+
if (claimedBy) {
|
|
164251
164309
|
this._failedEntities.push({
|
|
164252
164310
|
entityId,
|
|
164253
|
-
reason:
|
|
164311
|
+
reason: `Already exposed through ${claimedBy[0]} on this node.`
|
|
164254
164312
|
});
|
|
164255
|
-
|
|
164313
|
+
continue;
|
|
164256
164314
|
}
|
|
164257
|
-
|
|
164258
|
-
|
|
164259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164267
|
-
this.
|
|
164268
|
-
|
|
164269
|
-
|
|
164270
|
-
|
|
164271
|
-
|
|
164272
|
-
|
|
164273
|
-
|
|
164274
|
-
|
|
164275
|
-
|
|
164276
|
-
|
|
164277
|
-
|
|
164278
|
-
|
|
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
|
-
|
|
164380
|
+
for (const [entityId, entry] of this.endpoints) {
|
|
164297
164381
|
try {
|
|
164298
|
-
await
|
|
164382
|
+
await entry.endpoint.updateStates(states);
|
|
164299
164383
|
} catch (e) {
|
|
164300
|
-
this.log.warn(
|
|
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 =
|
|
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
|
|
164572
|
+
dataProvider,
|
|
164486
164573
|
loggerService.get("ServerModeEndpointManager")
|
|
164487
164574
|
);
|
|
164488
164575
|
class ServerModeBridgeWithEnvironment extends ServerModeBridge {
|