@riddix/hamh 2.0.43 → 2.0.44
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/README.md +28 -6
- package/dist/backend/cli.js +634 -245
- package/dist/frontend/assets/index-DFWoxgED.js +469 -0
- package/dist/frontend/index.html +1 -1
- package/package.json +4 -4
- package/dist/frontend/assets/index-eVn09I6J.js +0 -473
package/dist/backend/cli.js
CHANGED
|
@@ -145734,6 +145734,9 @@ var init_home_assistant_filter = __esm({
|
|
|
145734
145734
|
HomeAssistantMatcherType2["Label"] = "label";
|
|
145735
145735
|
HomeAssistantMatcherType2["EntityLabel"] = "entity_label";
|
|
145736
145736
|
HomeAssistantMatcherType2["DeviceLabel"] = "device_label";
|
|
145737
|
+
HomeAssistantMatcherType2["EntityLabelRegex"] = "entity_label_regex";
|
|
145738
|
+
HomeAssistantMatcherType2["DeviceLabelRegex"] = "device_label_regex";
|
|
145739
|
+
HomeAssistantMatcherType2["AnyFieldRegex"] = "any_field_regex";
|
|
145737
145740
|
HomeAssistantMatcherType2["Area"] = "area";
|
|
145738
145741
|
HomeAssistantMatcherType2["EntityCategory"] = "entity_category";
|
|
145739
145742
|
HomeAssistantMatcherType2["DeviceName"] = "device_name";
|
|
@@ -146125,6 +146128,7 @@ var init_clusters2 = __esm({
|
|
|
146125
146128
|
ClusterId3["pm10ConcentrationMeasurement"] = "pm10ConcentrationMeasurement";
|
|
146126
146129
|
ClusterId3["pm25ConcentrationMeasurement"] = "pm25ConcentrationMeasurement";
|
|
146127
146130
|
ClusterId3["powerSource"] = "powerSource";
|
|
146131
|
+
ClusterId3["powerTopology"] = "powerTopology";
|
|
146128
146132
|
ClusterId3["pressureMeasurement"] = "pressureMeasurement";
|
|
146129
146133
|
ClusterId3["relativeHumidityMeasurement"] = "relativeHumidityMeasurement";
|
|
146130
146134
|
ClusterId3["smokeCoAlarm"] = "smokeCoAlarm";
|
|
@@ -146638,7 +146642,7 @@ var init_bridge_config_schema = __esm({
|
|
|
146638
146642
|
{
|
|
146639
146643
|
const: "regex",
|
|
146640
146644
|
title: "regex",
|
|
146641
|
-
description: "
|
|
146645
|
+
description: "Regex tested against the entity_id only (e.g. 'light.kitchen_lamp'). For labels use entity_label_regex or device_label_regex. Example: '^(light|switch)\\.kitchen_.*'."
|
|
146642
146646
|
},
|
|
146643
146647
|
{
|
|
146644
146648
|
const: "domain",
|
|
@@ -146665,6 +146669,21 @@ var init_bridge_config_schema = __esm({
|
|
|
146665
146669
|
title: "device_label",
|
|
146666
146670
|
description: "Matches ALL entities of a device if the device has this label. Use this to include a complete device with all its entities."
|
|
146667
146671
|
},
|
|
146672
|
+
{
|
|
146673
|
+
const: "entity_label_regex",
|
|
146674
|
+
title: "entity_label_regex",
|
|
146675
|
+
description: "Regex tested against entity-label slugs and display names. Matches if any label assigned to the entity matches. Example: '^(matter|voice).*'."
|
|
146676
|
+
},
|
|
146677
|
+
{
|
|
146678
|
+
const: "device_label_regex",
|
|
146679
|
+
title: "device_label_regex",
|
|
146680
|
+
description: "Regex tested against device-label slugs and display names. Matches ALL entities of a device whose label matches. Example: '^(matter|voice).*'."
|
|
146681
|
+
},
|
|
146682
|
+
{
|
|
146683
|
+
const: "any_field_regex",
|
|
146684
|
+
title: "any_field_regex",
|
|
146685
|
+
description: "Regex tested against a single-line key=value haystack covering entity_id, domain, platform, area, entity_category, device_class, entity_labels, entity_label_names, device_labels, device_label_names, device_name, product_name. Use lookaheads for AND, alternation for OR. Example: '(?=.*domain=light)(?=.*area=living_room)|(?=.*domain=switch)(?=.*entity_labels=.*\\bvoice\\b)'."
|
|
146686
|
+
},
|
|
146668
146687
|
{
|
|
146669
146688
|
const: "area",
|
|
146670
146689
|
title: "area",
|
|
@@ -146813,6 +146832,20 @@ var init_bridge_config_schema = __esm({
|
|
|
146813
146832
|
description: "Workaround for Alexa resetting light brightness to 100% after subscription renewal. When enabled, the bridge ignores brightness commands that set lights to 100% within 200ms of a turn-on command for the same light. WARNING: breaks Apple Home's 'set room to 100%' Siri commands, which use the same on() + moveToLevel(254) pattern. Only enable on Alexa-only bridges.",
|
|
146814
146833
|
type: "boolean",
|
|
146815
146834
|
default: false
|
|
146835
|
+
},
|
|
146836
|
+
useHaRegistrySerial: {
|
|
146837
|
+
title: "Use HA Registry Serial Number",
|
|
146838
|
+
description: "Fall back to the Home Assistant device registry serial_number when no per-entity customSerialNumber is configured. Default off because changing serialNumber after commissioning can confuse controllers. A per-entity customSerialNumber still takes precedence.",
|
|
146839
|
+
type: "boolean",
|
|
146840
|
+
default: false
|
|
146841
|
+
},
|
|
146842
|
+
coverSliderDebounceMs: {
|
|
146843
|
+
title: "Cover Slider Debounce (ms)",
|
|
146844
|
+
description: "Override the cover position-update debounce window for this bridge. Some controllers (Apple Home) stream slider updates while the user is still dragging, causing covers to start moving toward an intermediate target. Set to the time the bridge should wait after the last update before sending the final value to Home Assistant. 0 keeps the built-in two-phase debounce (400 ms initial / 150 ms subsequent), which fits most controllers. Try 800\u20131500 ms for slow blinds. A per-entity override on a single cover wins over this flag.",
|
|
146845
|
+
type: "number",
|
|
146846
|
+
minimum: 0,
|
|
146847
|
+
maximum: 5e3,
|
|
146848
|
+
default: 0
|
|
146816
146849
|
}
|
|
146817
146850
|
}
|
|
146818
146851
|
};
|
|
@@ -146879,6 +146912,14 @@ var init_bridge_config_schema = __esm({
|
|
|
146879
146912
|
description: "Append a suffix to every entity serial number on this bridge. Useful for forcing controllers like Aqara to treat devices as new and bypass cached device data. Leave empty for default behavior.",
|
|
146880
146913
|
maxLength: 16
|
|
146881
146914
|
},
|
|
146915
|
+
sessionMaxAgeHours: {
|
|
146916
|
+
title: "Session Rotation Max Age (hours)",
|
|
146917
|
+
type: "number",
|
|
146918
|
+
description: "Server Mode only. Rotate matter sessions older than this many hours so iPhone clients re-subscribe and Apple Home unsticks 'Updating' tiles. Set 0 to disable rotation. Range 0-168, default 4. (#287)",
|
|
146919
|
+
default: 4,
|
|
146920
|
+
minimum: 0,
|
|
146921
|
+
maximum: 168
|
|
146922
|
+
},
|
|
146882
146923
|
filter: homeAssistantFilterSchema,
|
|
146883
146924
|
featureFlags: featureFlagSchema
|
|
146884
146925
|
},
|
|
@@ -147325,7 +147366,7 @@ var HA_MESSAGE_TIMEOUT_MS;
|
|
|
147325
147366
|
var init_send_ha_message = __esm({
|
|
147326
147367
|
"src/utils/send-ha-message.ts"() {
|
|
147327
147368
|
"use strict";
|
|
147328
|
-
HA_MESSAGE_TIMEOUT_MS =
|
|
147369
|
+
HA_MESSAGE_TIMEOUT_MS = 6e4;
|
|
147329
147370
|
}
|
|
147330
147371
|
});
|
|
147331
147372
|
|
|
@@ -147873,6 +147914,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
|
|
|
147873
147914
|
mopIntensityEntity: config10.mopIntensityEntity,
|
|
147874
147915
|
valetudoIdentifier: config10.valetudoIdentifier,
|
|
147875
147916
|
coverSwapOpenClose: config10.coverSwapOpenClose,
|
|
147917
|
+
coverSliderDebounceMs: config10.coverSliderDebounceMs,
|
|
147876
147918
|
disableClimateOnOff: config10.disableClimateOnOff,
|
|
147877
147919
|
disableClimateFanControl: config10.disableClimateFanControl,
|
|
147878
147920
|
customServiceAreas: config10.customServiceAreas,
|
|
@@ -148020,6 +148062,7 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
|
|
|
148020
148062
|
temperatureEntity: config10.temperatureEntity,
|
|
148021
148063
|
valetudoIdentifier: config10.valetudoIdentifier,
|
|
148022
148064
|
coverSwapOpenClose: config10.coverSwapOpenClose,
|
|
148065
|
+
coverSliderDebounceMs: config10.coverSliderDebounceMs,
|
|
148023
148066
|
disableClimateOnOff: config10.disableClimateOnOff,
|
|
148024
148067
|
disableClimateFanControl: config10.disableClimateFanControl,
|
|
148025
148068
|
customServiceAreas: config10.customServiceAreas,
|
|
@@ -148390,6 +148433,15 @@ function bridgeIconApi(storageLocation) {
|
|
|
148390
148433
|
res.json({ success: true, iconUrl });
|
|
148391
148434
|
}
|
|
148392
148435
|
);
|
|
148436
|
+
router.get(
|
|
148437
|
+
"/:bridgeId/exists",
|
|
148438
|
+
(req, res) => {
|
|
148439
|
+
const bridgeId = req.params.bridgeId;
|
|
148440
|
+
const files = fs2.readdirSync(iconsDir);
|
|
148441
|
+
const exists = files.some((f) => f.startsWith(`${bridgeId}.`));
|
|
148442
|
+
res.json({ exists });
|
|
148443
|
+
}
|
|
148444
|
+
);
|
|
148393
148445
|
router.get("/:bridgeId", (req, res) => {
|
|
148394
148446
|
const bridgeId = req.params.bridgeId;
|
|
148395
148447
|
const files = fs2.readdirSync(iconsDir);
|
|
@@ -148413,16 +148465,6 @@ function bridgeIconApi(storageLocation) {
|
|
|
148413
148465
|
fs2.unlinkSync(filePath);
|
|
148414
148466
|
res.json({ success: true });
|
|
148415
148467
|
});
|
|
148416
|
-
router.head("/:bridgeId", (req, res) => {
|
|
148417
|
-
const bridgeId = req.params.bridgeId;
|
|
148418
|
-
const files = fs2.readdirSync(iconsDir);
|
|
148419
|
-
const iconFile = files.find((f) => f.startsWith(`${bridgeId}.`));
|
|
148420
|
-
if (iconFile) {
|
|
148421
|
-
res.status(200).end();
|
|
148422
|
-
} else {
|
|
148423
|
-
res.status(404).end();
|
|
148424
|
-
}
|
|
148425
|
-
});
|
|
148426
148468
|
return router;
|
|
148427
148469
|
}
|
|
148428
148470
|
|
|
@@ -148842,6 +148884,7 @@ function entityMappingApi(mappingStorage) {
|
|
|
148842
148884
|
customProductName: body.customProductName,
|
|
148843
148885
|
customVendorName: body.customVendorName,
|
|
148844
148886
|
customSerialNumber: body.customSerialNumber,
|
|
148887
|
+
customVendorId: body.customVendorId,
|
|
148845
148888
|
disabled: body.disabled,
|
|
148846
148889
|
filterLifeEntity: body.filterLifeEntity,
|
|
148847
148890
|
cleaningModeEntity: body.cleaningModeEntity,
|
|
@@ -148860,8 +148903,10 @@ function entityMappingApi(mappingStorage) {
|
|
|
148860
148903
|
currentRoomEntity: body.currentRoomEntity,
|
|
148861
148904
|
valetudoIdentifier: body.valetudoIdentifier,
|
|
148862
148905
|
coverSwapOpenClose: body.coverSwapOpenClose,
|
|
148906
|
+
coverSliderDebounceMs: body.coverSliderDebounceMs,
|
|
148863
148907
|
disableClimateOnOff: body.disableClimateOnOff,
|
|
148864
148908
|
disableClimateFanControl: body.disableClimateFanControl,
|
|
148909
|
+
climateKeepModeOnIdle: body.climateKeepModeOnIdle,
|
|
148865
148910
|
composedEntities: body.composedEntities
|
|
148866
148911
|
};
|
|
148867
148912
|
const config10 = await mappingStorage.setMapping(request);
|
|
@@ -149325,8 +149370,10 @@ function configToProfileEntry(config10) {
|
|
|
149325
149370
|
customFanSpeedTags: config10.customFanSpeedTags,
|
|
149326
149371
|
valetudoIdentifier: config10.valetudoIdentifier,
|
|
149327
149372
|
coverSwapOpenClose: config10.coverSwapOpenClose,
|
|
149373
|
+
coverSliderDebounceMs: config10.coverSliderDebounceMs,
|
|
149328
149374
|
disableClimateOnOff: config10.disableClimateOnOff,
|
|
149329
|
-
disableClimateFanControl: config10.disableClimateFanControl
|
|
149375
|
+
disableClimateFanControl: config10.disableClimateFanControl,
|
|
149376
|
+
climateKeepModeOnIdle: config10.climateKeepModeOnIdle
|
|
149330
149377
|
};
|
|
149331
149378
|
}
|
|
149332
149379
|
function mappingProfileApi(mappingStorage) {
|
|
@@ -149448,8 +149495,10 @@ function mappingProfileApi(mappingStorage) {
|
|
|
149448
149495
|
customFanSpeedTags: entry.customFanSpeedTags,
|
|
149449
149496
|
valetudoIdentifier: entry.valetudoIdentifier,
|
|
149450
149497
|
coverSwapOpenClose: entry.coverSwapOpenClose,
|
|
149498
|
+
coverSliderDebounceMs: entry.coverSliderDebounceMs,
|
|
149451
149499
|
disableClimateOnOff: entry.disableClimateOnOff,
|
|
149452
|
-
disableClimateFanControl: entry.disableClimateFanControl
|
|
149500
|
+
disableClimateFanControl: entry.disableClimateFanControl,
|
|
149501
|
+
climateKeepModeOnIdle: entry.climateKeepModeOnIdle
|
|
149453
149502
|
});
|
|
149454
149503
|
applied++;
|
|
149455
149504
|
} catch (e) {
|
|
@@ -149501,6 +149550,18 @@ function testMatcher(matcher, device, entity, entityState, labels) {
|
|
|
149501
149550
|
const slug = resolveLabelValue(matcher.value, labels);
|
|
149502
149551
|
return !!device?.labels && device.labels.includes(slug);
|
|
149503
149552
|
}
|
|
149553
|
+
case "entity_label_regex":
|
|
149554
|
+
return testLabelRegex(matcher.value, entity?.labels, labels);
|
|
149555
|
+
case "device_label_regex":
|
|
149556
|
+
return testLabelRegex(matcher.value, device?.labels, labels);
|
|
149557
|
+
case "any_field_regex":
|
|
149558
|
+
return testAnyFieldRegex(
|
|
149559
|
+
matcher.value,
|
|
149560
|
+
entity,
|
|
149561
|
+
device,
|
|
149562
|
+
entityState,
|
|
149563
|
+
labels
|
|
149564
|
+
);
|
|
149504
149565
|
case "entity_category":
|
|
149505
149566
|
return entity?.entity_category === matcher.value;
|
|
149506
149567
|
case "platform":
|
|
@@ -149535,6 +149596,62 @@ function testRegex(pattern2, value) {
|
|
|
149535
149596
|
return false;
|
|
149536
149597
|
}
|
|
149537
149598
|
}
|
|
149599
|
+
function testLabelRegex(pattern2, assigned, labels) {
|
|
149600
|
+
if (!assigned?.length) {
|
|
149601
|
+
return false;
|
|
149602
|
+
}
|
|
149603
|
+
let regex;
|
|
149604
|
+
try {
|
|
149605
|
+
regex = new RegExp(pattern2);
|
|
149606
|
+
} catch {
|
|
149607
|
+
return false;
|
|
149608
|
+
}
|
|
149609
|
+
for (const slug of assigned) {
|
|
149610
|
+
if (regex.test(slug)) {
|
|
149611
|
+
return true;
|
|
149612
|
+
}
|
|
149613
|
+
const named = labels?.find((l) => l.label_id === slug)?.name;
|
|
149614
|
+
if (named && regex.test(named)) {
|
|
149615
|
+
return true;
|
|
149616
|
+
}
|
|
149617
|
+
}
|
|
149618
|
+
return false;
|
|
149619
|
+
}
|
|
149620
|
+
function testAnyFieldRegex(pattern2, entity, device, entityState, labels) {
|
|
149621
|
+
let regex;
|
|
149622
|
+
try {
|
|
149623
|
+
regex = new RegExp(pattern2);
|
|
149624
|
+
} catch {
|
|
149625
|
+
return false;
|
|
149626
|
+
}
|
|
149627
|
+
return regex.test(buildEntityHaystack(entity, device, entityState, labels));
|
|
149628
|
+
}
|
|
149629
|
+
function buildEntityHaystack(entity, device, entityState, labels) {
|
|
149630
|
+
const domain = entity.entity_id.split(".")[0];
|
|
149631
|
+
const entityArea = entity.area_id ?? (typeof device?.area_id === "string" ? device.area_id : "");
|
|
149632
|
+
const entityCategory = typeof entity.entity_category === "string" ? entity.entity_category : "";
|
|
149633
|
+
const deviceClass = typeof entityState?.attributes?.device_class === "string" ? entityState.attributes.device_class : "";
|
|
149634
|
+
const entityLabelSlugs = entity.labels ?? [];
|
|
149635
|
+
const deviceLabelSlugs = device?.labels ?? [];
|
|
149636
|
+
const entityLabelNames = entityLabelSlugs.map((slug) => labels?.find((l) => l.label_id === slug)?.name ?? "").filter((name) => name.length > 0);
|
|
149637
|
+
const deviceLabelNames = deviceLabelSlugs.map((slug) => labels?.find((l) => l.label_id === slug)?.name ?? "").filter((name) => name.length > 0);
|
|
149638
|
+
const deviceName = device?.name_by_user ?? device?.name ?? device?.default_name ?? "";
|
|
149639
|
+
const productName = device?.model ?? device?.default_model ?? "";
|
|
149640
|
+
return [
|
|
149641
|
+
`entity_id=${entity.entity_id}`,
|
|
149642
|
+
`domain=${domain}`,
|
|
149643
|
+
`platform=${entity.platform ?? ""}`,
|
|
149644
|
+
`area=${entityArea ?? ""}`,
|
|
149645
|
+
`entity_category=${entityCategory}`,
|
|
149646
|
+
`device_class=${deviceClass}`,
|
|
149647
|
+
`entity_labels=${entityLabelSlugs.join(",")}`,
|
|
149648
|
+
`entity_label_names=${entityLabelNames.join(",")}`,
|
|
149649
|
+
`device_labels=${deviceLabelSlugs.join(",")}`,
|
|
149650
|
+
`device_label_names=${deviceLabelNames.join(",")}`,
|
|
149651
|
+
`device_name=${deviceName}`,
|
|
149652
|
+
`product_name=${productName}`
|
|
149653
|
+
].join(" ");
|
|
149654
|
+
}
|
|
149538
149655
|
function testDeviceName(pattern2, device) {
|
|
149539
149656
|
if (!device) {
|
|
149540
149657
|
return false;
|
|
@@ -152042,7 +152159,8 @@ var Options = class {
|
|
|
152042
152159
|
return {
|
|
152043
152160
|
url: this.startOptions.homeAssistantUrl,
|
|
152044
152161
|
accessToken: this.startOptions.homeAssistantAccessToken,
|
|
152045
|
-
refreshInterval: this.startOptions.homeAssistantRefreshInterval
|
|
152162
|
+
refreshInterval: this.startOptions.homeAssistantRefreshInterval,
|
|
152163
|
+
messageTimeoutMs: this.startOptions.haMessageTimeout
|
|
152046
152164
|
};
|
|
152047
152165
|
}
|
|
152048
152166
|
get webApi() {
|
|
@@ -152734,25 +152852,33 @@ init_retry();
|
|
|
152734
152852
|
|
|
152735
152853
|
// src/services/home-assistant/api/get-registry.ts
|
|
152736
152854
|
init_send_ha_message();
|
|
152737
|
-
async function getRegistry(connection) {
|
|
152738
|
-
return sendHaMessage(
|
|
152739
|
-
|
|
152740
|
-
|
|
152855
|
+
async function getRegistry(connection, timeoutMs) {
|
|
152856
|
+
return sendHaMessage(
|
|
152857
|
+
connection,
|
|
152858
|
+
{ type: "config/entity_registry/list" },
|
|
152859
|
+
timeoutMs
|
|
152860
|
+
);
|
|
152741
152861
|
}
|
|
152742
|
-
async function getDeviceRegistry(connection) {
|
|
152743
|
-
return sendHaMessage(
|
|
152744
|
-
|
|
152745
|
-
|
|
152862
|
+
async function getDeviceRegistry(connection, timeoutMs) {
|
|
152863
|
+
return sendHaMessage(
|
|
152864
|
+
connection,
|
|
152865
|
+
{ type: "config/device_registry/list" },
|
|
152866
|
+
timeoutMs
|
|
152867
|
+
);
|
|
152746
152868
|
}
|
|
152747
|
-
async function getLabelRegistry(connection) {
|
|
152748
|
-
return sendHaMessage(
|
|
152749
|
-
|
|
152750
|
-
|
|
152869
|
+
async function getLabelRegistry(connection, timeoutMs) {
|
|
152870
|
+
return sendHaMessage(
|
|
152871
|
+
connection,
|
|
152872
|
+
{ type: "config/label_registry/list" },
|
|
152873
|
+
timeoutMs
|
|
152874
|
+
);
|
|
152751
152875
|
}
|
|
152752
|
-
async function getAreaRegistry(connection) {
|
|
152753
|
-
return sendHaMessage(
|
|
152754
|
-
|
|
152755
|
-
|
|
152876
|
+
async function getAreaRegistry(connection, timeoutMs) {
|
|
152877
|
+
return sendHaMessage(
|
|
152878
|
+
connection,
|
|
152879
|
+
{ type: "config/area_registry/list" },
|
|
152880
|
+
timeoutMs
|
|
152881
|
+
);
|
|
152756
152882
|
}
|
|
152757
152883
|
|
|
152758
152884
|
// src/services/home-assistant/home-assistant-registry.ts
|
|
@@ -152855,12 +152981,15 @@ var HomeAssistantRegistry = class extends Service {
|
|
|
152855
152981
|
}
|
|
152856
152982
|
});
|
|
152857
152983
|
}
|
|
152984
|
+
const timeoutMs = this.options.messageTimeoutMs;
|
|
152858
152985
|
const [entityRegistry, statesList, deviceRegistry, labels, areas] = await Promise.all([
|
|
152859
|
-
getRegistry(connection),
|
|
152986
|
+
getRegistry(connection, timeoutMs),
|
|
152860
152987
|
getStates(connection),
|
|
152861
|
-
getDeviceRegistry(connection),
|
|
152862
|
-
getLabelRegistry(connection).catch(
|
|
152863
|
-
|
|
152988
|
+
getDeviceRegistry(connection, timeoutMs),
|
|
152989
|
+
getLabelRegistry(connection, timeoutMs).catch(
|
|
152990
|
+
() => []
|
|
152991
|
+
),
|
|
152992
|
+
getAreaRegistry(connection, timeoutMs).catch(
|
|
152864
152993
|
() => []
|
|
152865
152994
|
)
|
|
152866
152995
|
]);
|
|
@@ -153329,6 +153458,7 @@ var EntityMappingStorage = class extends Service {
|
|
|
153329
153458
|
customProductName: request.customProductName?.trim() || void 0,
|
|
153330
153459
|
customVendorName: request.customVendorName?.trim() || void 0,
|
|
153331
153460
|
customSerialNumber: request.customSerialNumber?.trim() || void 0,
|
|
153461
|
+
customVendorId: sanitizeVendorId(request.customVendorId),
|
|
153332
153462
|
disabled: request.disabled,
|
|
153333
153463
|
filterLifeEntity: request.filterLifeEntity?.trim() || void 0,
|
|
153334
153464
|
cleaningModeEntity: request.cleaningModeEntity?.trim() || void 0,
|
|
@@ -153349,11 +153479,13 @@ var EntityMappingStorage = class extends Service {
|
|
|
153349
153479
|
currentRoomEntity: request.currentRoomEntity?.trim() || void 0,
|
|
153350
153480
|
valetudoIdentifier: request.valetudoIdentifier?.trim() || void 0,
|
|
153351
153481
|
coverSwapOpenClose: request.coverSwapOpenClose || void 0,
|
|
153482
|
+
coverSliderDebounceMs: sanitizeDebounceMs(request.coverSliderDebounceMs),
|
|
153352
153483
|
disableClimateOnOff: request.disableClimateOnOff || void 0,
|
|
153353
153484
|
disableClimateFanControl: request.disableClimateFanControl || void 0,
|
|
153485
|
+
climateKeepModeOnIdle: request.climateKeepModeOnIdle || void 0,
|
|
153354
153486
|
composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0
|
|
153355
153487
|
};
|
|
153356
|
-
if (!config10.matterDeviceType && !config10.customName && !config10.customProductName && !config10.customVendorName && !config10.customSerialNumber && config10.disabled !== true && !config10.filterLifeEntity && !config10.cleaningModeEntity && !config10.temperatureEntity && !config10.humidityEntity && !config10.batteryEntity && !config10.roomEntities && !config10.disableLockPin && !config10.powerEntity && !config10.energyEntity && !config10.pressureEntity && !config10.suctionLevelEntity && !config10.mopIntensityEntity && (!config10.customServiceAreas || config10.customServiceAreas.length === 0) && (!config10.customFanSpeedTags || Object.keys(config10.customFanSpeedTags).length === 0) && !config10.currentRoomEntity && !config10.valetudoIdentifier && !config10.coverSwapOpenClose && !config10.disableClimateOnOff && !config10.disableClimateFanControl && (!config10.composedEntities || config10.composedEntities.length === 0)) {
|
|
153488
|
+
if (!config10.matterDeviceType && !config10.customName && !config10.customProductName && !config10.customVendorName && !config10.customSerialNumber && config10.customVendorId === void 0 && config10.disabled !== true && !config10.filterLifeEntity && !config10.cleaningModeEntity && !config10.temperatureEntity && !config10.humidityEntity && !config10.batteryEntity && !config10.roomEntities && !config10.disableLockPin && !config10.powerEntity && !config10.energyEntity && !config10.pressureEntity && !config10.suctionLevelEntity && !config10.mopIntensityEntity && (!config10.customServiceAreas || config10.customServiceAreas.length === 0) && (!config10.customFanSpeedTags || Object.keys(config10.customFanSpeedTags).length === 0) && !config10.currentRoomEntity && !config10.valetudoIdentifier && !config10.coverSwapOpenClose && !config10.coverSliderDebounceMs && !config10.disableClimateOnOff && !config10.disableClimateFanControl && !config10.climateKeepModeOnIdle && (!config10.composedEntities || config10.composedEntities.length === 0)) {
|
|
153357
153489
|
bridgeMap.delete(request.entityId);
|
|
153358
153490
|
} else {
|
|
153359
153491
|
bridgeMap.set(request.entityId, config10);
|
|
@@ -153373,6 +153505,26 @@ var EntityMappingStorage = class extends Service {
|
|
|
153373
153505
|
await this.persist();
|
|
153374
153506
|
}
|
|
153375
153507
|
};
|
|
153508
|
+
function sanitizeVendorId(value) {
|
|
153509
|
+
if (value === void 0 || value === null || value === "") {
|
|
153510
|
+
return void 0;
|
|
153511
|
+
}
|
|
153512
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
153513
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 65534) {
|
|
153514
|
+
return void 0;
|
|
153515
|
+
}
|
|
153516
|
+
return n;
|
|
153517
|
+
}
|
|
153518
|
+
function sanitizeDebounceMs(value) {
|
|
153519
|
+
if (value === void 0 || value === null || value === "") {
|
|
153520
|
+
return void 0;
|
|
153521
|
+
}
|
|
153522
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
153523
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) {
|
|
153524
|
+
return void 0;
|
|
153525
|
+
}
|
|
153526
|
+
return Math.min(5e3, Math.round(n));
|
|
153527
|
+
}
|
|
153376
153528
|
|
|
153377
153529
|
// src/services/storage/lock-credential-storage.ts
|
|
153378
153530
|
init_service();
|
|
@@ -165393,8 +165545,10 @@ var ServerModeServerNode = class extends ServerNode {
|
|
|
165393
165545
|
async updateDeviceIdentity(entityId, device, mapping, friendlyName) {
|
|
165394
165546
|
const nodeLabel = trimToLength(mapping?.customName, 32, "...") ?? trimToLength(friendlyName, 32, "...") ?? trimToLength(entityId, 32, "...");
|
|
165395
165547
|
const productNameFromNodeLabel = this.featureFlags?.productNameFromNodeLabel === true ? trimToLength(sanitizeMatterString(nodeLabel ?? ""), 32, "...") ?? void 0 : void 0;
|
|
165396
|
-
const
|
|
165397
|
-
const
|
|
165548
|
+
const maxRawLen = 32 - (this.serialNumberSuffix?.length ?? 0);
|
|
165549
|
+
const registrySerial = this.featureFlags?.useHaRegistrySerial ? trimToLength(device?.serial_number, maxRawLen, "...") : void 0;
|
|
165550
|
+
const rawSerial = trimToLength(mapping?.customSerialNumber, maxRawLen, "...") ?? registrySerial;
|
|
165551
|
+
const serialNumber = rawSerial && this.serialNumberSuffix ? `${rawSerial}${this.serialNumberSuffix}` : rawSerial;
|
|
165398
165552
|
const basicInformation = dropUndefined({
|
|
165399
165553
|
vendorName: trimToLength(mapping?.customVendorName, 32, "...") ?? trimToLength(device?.manufacturer, 32, "..."),
|
|
165400
165554
|
productName: trimToLength(mapping?.customProductName, 32, "...") ?? productNameFromNodeLabel ?? trimToLength(device?.model_id, 32, "...") ?? trimToLength(device?.model, 32, "..."),
|
|
@@ -166497,6 +166651,9 @@ var BridgeDataProvider = class extends Service {
|
|
|
166497
166651
|
get serialNumberSuffix() {
|
|
166498
166652
|
return this.data.serialNumberSuffix;
|
|
166499
166653
|
}
|
|
166654
|
+
get sessionMaxAgeHours() {
|
|
166655
|
+
return this.data.sessionMaxAgeHours;
|
|
166656
|
+
}
|
|
166500
166657
|
/************************************************
|
|
166501
166658
|
* Functions
|
|
166502
166659
|
************************************************/
|
|
@@ -166522,6 +166679,7 @@ var BridgeDataProvider = class extends Service {
|
|
|
166522
166679
|
icon: this.icon,
|
|
166523
166680
|
priority: this.priority,
|
|
166524
166681
|
serialNumberSuffix: this.serialNumberSuffix,
|
|
166682
|
+
sessionMaxAgeHours: this.sessionMaxAgeHours,
|
|
166525
166683
|
status: status3.code,
|
|
166526
166684
|
statusReason: status3.reason,
|
|
166527
166685
|
commissioning: commissioning ? {
|
|
@@ -168118,6 +168276,12 @@ ${e?.toString()}`);
|
|
|
168118
168276
|
if (!this.dataProvider.featureFlags?.autoForceSync) {
|
|
168119
168277
|
return 0;
|
|
168120
168278
|
}
|
|
168279
|
+
if (isHeapUnderPressure()) {
|
|
168280
|
+
this.log.warn(
|
|
168281
|
+
"Force sync skipped: heap under pressure, reduce entities or raise NODE_OPTIONS=--max-old-space-size"
|
|
168282
|
+
);
|
|
168283
|
+
return 0;
|
|
168284
|
+
}
|
|
168121
168285
|
const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
|
|
168122
168286
|
const allEndpoints = [];
|
|
168123
168287
|
const collect = (ep) => {
|
|
@@ -168415,10 +168579,14 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
|
|
|
168415
168579
|
const nodeLabel = ellipse(32, homeAssistant.state.customName) ?? ellipse(32, registryName) ?? ellipse(32, entity.state?.attributes?.friendly_name) ?? ellipse(32, entity.entity_id);
|
|
168416
168580
|
const productNameFromNodeLabel = featureFlags?.productNameFromNodeLabel === true ? ellipse(32, sanitizeMatterString(nodeLabel ?? "")) ?? void 0 : void 0;
|
|
168417
168581
|
const serialNumberSuffix = this.env.get(BridgeDataProvider).serialNumberSuffix;
|
|
168418
|
-
const
|
|
168419
|
-
const
|
|
168582
|
+
const maxRawLen = 32 - (serialNumberSuffix?.length ?? 0);
|
|
168583
|
+
const registrySerial = featureFlags?.useHaRegistrySerial ? ellipse(maxRawLen, device?.serial_number) : void 0;
|
|
168584
|
+
const rawSerial = ellipse(maxRawLen, mapping?.customSerialNumber) ?? registrySerial ?? hash(maxRawLen, entity.entity_id);
|
|
168585
|
+
const serialNumber = rawSerial && serialNumberSuffix ? `${rawSerial}${serialNumberSuffix}` : rawSerial;
|
|
168586
|
+
const customVendorId = mapping?.customVendorId;
|
|
168587
|
+
const vendorId3 = isValidVendorId(customVendorId) ? customVendorId : basicInformation.vendorId;
|
|
168420
168588
|
applyPatchState(this.state, {
|
|
168421
|
-
vendorId: VendorId(
|
|
168589
|
+
vendorId: VendorId(vendorId3),
|
|
168422
168590
|
vendorName: ellipse(32, mapping?.customVendorName) ?? ellipse(32, device?.manufacturer) ?? hash(32, basicInformation.vendorName),
|
|
168423
168591
|
productName: ellipse(32, mapping?.customProductName) ?? productNameFromNodeLabel ?? ellipse(32, device?.model_id) ?? ellipse(32, device?.model) ?? hash(32, basicInformation.productName),
|
|
168424
168592
|
productLabel: ellipse(64, device?.model) ?? hash(64, basicInformation.productLabel),
|
|
@@ -168443,6 +168611,9 @@ function hash(maxLength, value) {
|
|
|
168443
168611
|
const suffix = crypto6.createHash("md5").update(value ?? "").digest("hex").substring(0, hashLength);
|
|
168444
168612
|
return trimToLength(value, maxLength, suffix);
|
|
168445
168613
|
}
|
|
168614
|
+
function isValidVendorId(value) {
|
|
168615
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65534;
|
|
168616
|
+
}
|
|
168446
168617
|
|
|
168447
168618
|
// src/matter/behaviors/electrical-energy-measurement-server.ts
|
|
168448
168619
|
init_esm();
|
|
@@ -168493,6 +168664,10 @@ var ElectricalEnergyMeasurementServerBase = class extends FeaturedBase {
|
|
|
168493
168664
|
ElectricalEnergyMeasurementServerBase2.State = State;
|
|
168494
168665
|
})(ElectricalEnergyMeasurementServerBase || (ElectricalEnergyMeasurementServerBase = {}));
|
|
168495
168666
|
var HaElectricalEnergyMeasurementServer = ElectricalEnergyMeasurementServerBase.set({
|
|
168667
|
+
// Match the activePower=0 default in HaElectricalPowerMeasurementServer
|
|
168668
|
+
// so SmartThings doesn't show "- kWh" before the first state update or
|
|
168669
|
+
// when an entity only carries power (not energy) data.
|
|
168670
|
+
cumulativeEnergyImported: { energy: 0 },
|
|
168496
168671
|
accuracy: {
|
|
168497
168672
|
measurementType: ElectricalPowerMeasurement3.MeasurementType.ElectricalEnergy,
|
|
168498
168673
|
measured: true,
|
|
@@ -168723,6 +168898,11 @@ var defaultBatteryConfig = {
|
|
|
168723
168898
|
};
|
|
168724
168899
|
var DefaultPowerSourceServer = PowerSourceServer2(defaultBatteryConfig);
|
|
168725
168900
|
|
|
168901
|
+
// src/matter/behaviors/power-topology-server.ts
|
|
168902
|
+
var HaPowerTopologyServer = PowerTopologyServer.with(
|
|
168903
|
+
PowerTopology3.Feature.NodeTopology
|
|
168904
|
+
);
|
|
168905
|
+
|
|
168726
168906
|
// src/matter/behaviors/temperature-measurement-server.ts
|
|
168727
168907
|
init_home_assistant_entity_behavior();
|
|
168728
168908
|
var TemperatureMeasurementServerBase = class extends TemperatureMeasurementServer {
|
|
@@ -169826,6 +170006,9 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
|
|
|
169826
170006
|
if (config10.batteryEntityId) {
|
|
169827
170007
|
parentType = parentType.with(PowerSourceServer2(batteryConfig));
|
|
169828
170008
|
}
|
|
170009
|
+
if (config10.powerEntityId || config10.energyEntityId) {
|
|
170010
|
+
parentType = parentType.with(HaPowerTopologyServer);
|
|
170011
|
+
}
|
|
169829
170012
|
if (config10.powerEntityId) {
|
|
169830
170013
|
parentType = parentType.with(HaElectricalPowerMeasurementServer);
|
|
169831
170014
|
}
|
|
@@ -170161,6 +170344,9 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
|
|
|
170161
170344
|
if (config10.batteryEntityId) {
|
|
170162
170345
|
parentType = parentType.with(PowerSourceServer2(batteryConfig2));
|
|
170163
170346
|
}
|
|
170347
|
+
if (config10.powerEntityId || config10.energyEntityId) {
|
|
170348
|
+
parentType = parentType.with(HaPowerTopologyServer);
|
|
170349
|
+
}
|
|
170164
170350
|
if (config10.powerEntityId) {
|
|
170165
170351
|
parentType = parentType.with(HaElectricalPowerMeasurementServer);
|
|
170166
170352
|
}
|
|
@@ -171791,6 +171977,7 @@ function isHeatCoolOnly(modes) {
|
|
|
171791
171977
|
return modes.includes(ClimateHvacMode.heat_cool) && !modes.includes(ClimateHvacMode.heat) && !modes.includes(ClimateHvacMode.cool);
|
|
171792
171978
|
}
|
|
171793
171979
|
var lastHvacDirection = /* @__PURE__ */ new Map();
|
|
171980
|
+
var climateFreezeState = /* @__PURE__ */ new Map();
|
|
171794
171981
|
function getHeatCoolOnlyDirection(entity, agent) {
|
|
171795
171982
|
const action = attributes4(entity).hvac_action;
|
|
171796
171983
|
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
@@ -171805,6 +171992,95 @@ function getHeatCoolOnlyDirection(entity, agent) {
|
|
|
171805
171992
|
}
|
|
171806
171993
|
return lastHvacDirection.get(entityId) ?? "heating";
|
|
171807
171994
|
}
|
|
171995
|
+
function computeSystemMode(entity, agent) {
|
|
171996
|
+
const hvacMode = entity.state;
|
|
171997
|
+
const systemMode = hvacModeToSystemMode[hvacMode] ?? Thermostat3.SystemMode.Off;
|
|
171998
|
+
if (systemMode === Thermostat3.SystemMode.Auto) {
|
|
171999
|
+
const modes = attributes4(entity).hvac_modes ?? [];
|
|
172000
|
+
if (isHeatCoolOnly(modes)) {
|
|
172001
|
+
const direction = getHeatCoolOnlyDirection(entity, agent);
|
|
172002
|
+
return direction === "cooling" ? Thermostat3.SystemMode.Cool : Thermostat3.SystemMode.Heat;
|
|
172003
|
+
}
|
|
172004
|
+
const hasMatterAuto = modes.includes(ClimateHvacMode.heat_cool) && (modes.includes(ClimateHvacMode.heat) || modes.includes(ClimateHvacMode.cool));
|
|
172005
|
+
if (hasMatterAuto) {
|
|
172006
|
+
return systemMode;
|
|
172007
|
+
}
|
|
172008
|
+
const hasCooling = modes.some((m) => m === ClimateHvacMode.cool);
|
|
172009
|
+
const hasHeating = modes.some(
|
|
172010
|
+
(m) => m === ClimateHvacMode.heat || m === ClimateHvacMode.auto
|
|
172011
|
+
);
|
|
172012
|
+
if (hasHeating && !hasCooling) {
|
|
172013
|
+
return Thermostat3.SystemMode.Heat;
|
|
172014
|
+
}
|
|
172015
|
+
if (hasCooling && !hasHeating) {
|
|
172016
|
+
return Thermostat3.SystemMode.Cool;
|
|
172017
|
+
}
|
|
172018
|
+
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
172019
|
+
const entityId = homeAssistant.entityId;
|
|
172020
|
+
const action = attributes4(entity).hvac_action;
|
|
172021
|
+
if (action === ClimateHvacAction.cooling) {
|
|
172022
|
+
lastHvacDirection.set(entityId, "cooling");
|
|
172023
|
+
return Thermostat3.SystemMode.Cool;
|
|
172024
|
+
}
|
|
172025
|
+
if (action === ClimateHvacAction.heating) {
|
|
172026
|
+
lastHvacDirection.set(entityId, "heating");
|
|
172027
|
+
return Thermostat3.SystemMode.Heat;
|
|
172028
|
+
}
|
|
172029
|
+
const remembered = lastHvacDirection.get(entityId);
|
|
172030
|
+
if (remembered) {
|
|
172031
|
+
return remembered === "cooling" ? Thermostat3.SystemMode.Cool : Thermostat3.SystemMode.Heat;
|
|
172032
|
+
}
|
|
172033
|
+
const current = attributes4(entity).current_temperature;
|
|
172034
|
+
const target = attributes4(entity).temperature;
|
|
172035
|
+
if (typeof current === "number" && typeof target === "number") {
|
|
172036
|
+
if (current > target) {
|
|
172037
|
+
lastHvacDirection.set(entityId, "cooling");
|
|
172038
|
+
return Thermostat3.SystemMode.Cool;
|
|
172039
|
+
}
|
|
172040
|
+
if (current < target) {
|
|
172041
|
+
lastHvacDirection.set(entityId, "heating");
|
|
172042
|
+
return Thermostat3.SystemMode.Heat;
|
|
172043
|
+
}
|
|
172044
|
+
}
|
|
172045
|
+
return Thermostat3.SystemMode.Cool;
|
|
172046
|
+
}
|
|
172047
|
+
return systemMode;
|
|
172048
|
+
}
|
|
172049
|
+
function applyClimateFreezeForKeepModeOnIdle(computed, entity, entityId, keepModeOnIdle) {
|
|
172050
|
+
if (computed !== Thermostat3.SystemMode.Off) {
|
|
172051
|
+
const existing2 = climateFreezeState.get(entityId);
|
|
172052
|
+
if (existing2) {
|
|
172053
|
+
existing2.lastNonOffMode = computed;
|
|
172054
|
+
existing2.pending = true;
|
|
172055
|
+
existing2.confirmedOff = false;
|
|
172056
|
+
} else {
|
|
172057
|
+
climateFreezeState.set(entityId, {
|
|
172058
|
+
lastNonOffMode: computed,
|
|
172059
|
+
pending: true,
|
|
172060
|
+
confirmedOff: false
|
|
172061
|
+
});
|
|
172062
|
+
}
|
|
172063
|
+
return computed;
|
|
172064
|
+
}
|
|
172065
|
+
const action = attributes4(entity).hvac_action;
|
|
172066
|
+
const existing = climateFreezeState.get(entityId);
|
|
172067
|
+
if (action === ClimateHvacAction.off) {
|
|
172068
|
+
if (existing) {
|
|
172069
|
+
if (existing.confirmedOff) {
|
|
172070
|
+
existing.pending = false;
|
|
172071
|
+
existing.confirmedOff = false;
|
|
172072
|
+
} else {
|
|
172073
|
+
existing.confirmedOff = true;
|
|
172074
|
+
}
|
|
172075
|
+
}
|
|
172076
|
+
} else if (existing) {
|
|
172077
|
+
existing.confirmedOff = false;
|
|
172078
|
+
}
|
|
172079
|
+
if (keepModeOnIdle && existing?.pending) {
|
|
172080
|
+
return existing.lastNonOffMode;
|
|
172081
|
+
}
|
|
172082
|
+
return computed;
|
|
172083
|
+
}
|
|
171808
172084
|
var config4 = {
|
|
171809
172085
|
// Temperature range (target_temp_low/high) only works in heat_cool mode.
|
|
171810
172086
|
// In heat or cool mode, HA expects a single "temperature" value.
|
|
@@ -171824,58 +172100,14 @@ var config4 = {
|
|
|
171824
172100
|
getTargetHeatingTemperature: (entity, agent) => getTemp(agent, entity, "target_temp_low") ?? getTemp(agent, entity, "target_temperature") ?? getTemp(agent, entity, "temperature"),
|
|
171825
172101
|
getTargetCoolingTemperature: (entity, agent) => getTemp(agent, entity, "target_temp_high") ?? getTemp(agent, entity, "target_temperature") ?? getTemp(agent, entity, "temperature"),
|
|
171826
172102
|
getSystemMode: (entity, agent) => {
|
|
171827
|
-
const
|
|
171828
|
-
const
|
|
171829
|
-
|
|
171830
|
-
|
|
171831
|
-
|
|
171832
|
-
|
|
171833
|
-
|
|
171834
|
-
|
|
171835
|
-
const hasMatterAuto = modes.includes(ClimateHvacMode.heat_cool) && (modes.includes(ClimateHvacMode.heat) || modes.includes(ClimateHvacMode.cool));
|
|
171836
|
-
if (hasMatterAuto) {
|
|
171837
|
-
return systemMode;
|
|
171838
|
-
}
|
|
171839
|
-
const hasCooling = modes.some((m) => m === ClimateHvacMode.cool);
|
|
171840
|
-
const hasHeating = modes.some(
|
|
171841
|
-
(m) => m === ClimateHvacMode.heat || m === ClimateHvacMode.auto
|
|
171842
|
-
);
|
|
171843
|
-
if (hasHeating && !hasCooling) {
|
|
171844
|
-
return Thermostat3.SystemMode.Heat;
|
|
171845
|
-
}
|
|
171846
|
-
if (hasCooling && !hasHeating) {
|
|
171847
|
-
return Thermostat3.SystemMode.Cool;
|
|
171848
|
-
}
|
|
171849
|
-
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
171850
|
-
const entityId = homeAssistant.entityId;
|
|
171851
|
-
const action = attributes4(entity).hvac_action;
|
|
171852
|
-
if (action === ClimateHvacAction.cooling) {
|
|
171853
|
-
lastHvacDirection.set(entityId, "cooling");
|
|
171854
|
-
return Thermostat3.SystemMode.Cool;
|
|
171855
|
-
}
|
|
171856
|
-
if (action === ClimateHvacAction.heating) {
|
|
171857
|
-
lastHvacDirection.set(entityId, "heating");
|
|
171858
|
-
return Thermostat3.SystemMode.Heat;
|
|
171859
|
-
}
|
|
171860
|
-
const remembered = lastHvacDirection.get(entityId);
|
|
171861
|
-
if (remembered) {
|
|
171862
|
-
return remembered === "cooling" ? Thermostat3.SystemMode.Cool : Thermostat3.SystemMode.Heat;
|
|
171863
|
-
}
|
|
171864
|
-
const current = attributes4(entity).current_temperature;
|
|
171865
|
-
const target = attributes4(entity).temperature;
|
|
171866
|
-
if (typeof current === "number" && typeof target === "number") {
|
|
171867
|
-
if (current > target) {
|
|
171868
|
-
lastHvacDirection.set(entityId, "cooling");
|
|
171869
|
-
return Thermostat3.SystemMode.Cool;
|
|
171870
|
-
}
|
|
171871
|
-
if (current < target) {
|
|
171872
|
-
lastHvacDirection.set(entityId, "heating");
|
|
171873
|
-
return Thermostat3.SystemMode.Heat;
|
|
171874
|
-
}
|
|
171875
|
-
}
|
|
171876
|
-
return Thermostat3.SystemMode.Cool;
|
|
171877
|
-
}
|
|
171878
|
-
return systemMode;
|
|
172103
|
+
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
172104
|
+
const computed = computeSystemMode(entity, agent);
|
|
172105
|
+
return applyClimateFreezeForKeepModeOnIdle(
|
|
172106
|
+
computed,
|
|
172107
|
+
entity,
|
|
172108
|
+
homeAssistant.entityId,
|
|
172109
|
+
homeAssistant.state.mapping?.climateKeepModeOnIdle === true
|
|
172110
|
+
);
|
|
171879
172111
|
},
|
|
171880
172112
|
getRunningMode: (entity) => {
|
|
171881
172113
|
const action = attributes4(entity).hvac_action;
|
|
@@ -172138,6 +172370,18 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172138
172370
|
static DEBOUNCE_INITIAL_MS = 400;
|
|
172139
172371
|
static DEBOUNCE_SUBSEQUENT_MS = 150;
|
|
172140
172372
|
static COMMAND_SEQUENCE_THRESHOLD_MS = 600;
|
|
172373
|
+
// Per-entity override wins over per-bridge flag; both must be > 0 to count.
|
|
172374
|
+
resolveDebounceOverride(homeAssistant) {
|
|
172375
|
+
const fromEntity = homeAssistant.state.mapping?.coverSliderDebounceMs;
|
|
172376
|
+
if (typeof fromEntity === "number" && fromEntity > 0) {
|
|
172377
|
+
return fromEntity;
|
|
172378
|
+
}
|
|
172379
|
+
const fromBridge = this.env.get(BridgeDataProvider).featureFlags?.coverSliderDebounceMs;
|
|
172380
|
+
if (typeof fromBridge === "number" && fromBridge > 0) {
|
|
172381
|
+
return fromBridge;
|
|
172382
|
+
}
|
|
172383
|
+
return null;
|
|
172384
|
+
}
|
|
172141
172385
|
async [Symbol.asyncDispose]() {
|
|
172142
172386
|
if (this.liftDebounceTimer) {
|
|
172143
172387
|
clearTimeout(this.liftDebounceTimer);
|
|
@@ -172152,23 +172396,9 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172152
172396
|
await super[Symbol.asyncDispose]();
|
|
172153
172397
|
}
|
|
172154
172398
|
async initialize() {
|
|
172155
|
-
|
|
172156
|
-
if (this.state.installedOpenLimitLift == null) {
|
|
172157
|
-
this.state.installedOpenLimitLift = 0;
|
|
172158
|
-
}
|
|
172159
|
-
if (this.state.installedClosedLimitLift == null) {
|
|
172160
|
-
this.state.installedClosedLimitLift = 1e4;
|
|
172161
|
-
}
|
|
172162
|
-
}
|
|
172163
|
-
if (this.features.tilt) {
|
|
172164
|
-
if (this.state.installedOpenLimitTilt == null) {
|
|
172165
|
-
this.state.installedOpenLimitTilt = 0;
|
|
172166
|
-
}
|
|
172167
|
-
if (this.state.installedClosedLimitTilt == null) {
|
|
172168
|
-
this.state.installedClosedLimitTilt = 1e4;
|
|
172169
|
-
}
|
|
172170
|
-
}
|
|
172399
|
+
this.internal.disableOperationalModeHandling = true;
|
|
172171
172400
|
if (this.features.positionAwareLift) {
|
|
172401
|
+
this.state.currentPositionLiftPercentage = void 0;
|
|
172172
172402
|
if (this.state.currentPositionLiftPercent100ths === void 0) {
|
|
172173
172403
|
this.state.currentPositionLiftPercent100ths = null;
|
|
172174
172404
|
}
|
|
@@ -172177,6 +172407,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172177
172407
|
}
|
|
172178
172408
|
}
|
|
172179
172409
|
if (this.features.positionAwareTilt) {
|
|
172410
|
+
this.state.currentPositionTiltPercentage = void 0;
|
|
172180
172411
|
if (this.state.currentPositionTiltPercent100ths === void 0) {
|
|
172181
172412
|
this.state.currentPositionTiltPercent100ths = null;
|
|
172182
172413
|
}
|
|
@@ -172233,41 +172464,39 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172233
172464
|
);
|
|
172234
172465
|
const overrideType = config10.getCoverType?.(state, this.agent);
|
|
172235
172466
|
const overrideEndProduct = config10.getEndProductType?.(state, this.agent);
|
|
172467
|
+
const previousStatus = this.state.operationalStatus?.global;
|
|
172468
|
+
const startedMoving = !isStopped && previousStatus === MovementStatus.Stopped;
|
|
172236
172469
|
const appliedPatch = applyPatchState(
|
|
172237
172470
|
this.state,
|
|
172238
172471
|
{
|
|
172239
172472
|
type: overrideType ?? (this.features.lift && this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindLift : this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindTiltOnly : WindowCovering3.WindowCoveringType.Rollershade),
|
|
172240
172473
|
endProductType: overrideEndProduct ?? (this.features.lift && this.features.tilt ? WindowCovering3.EndProductType.SheerShade : this.features.tilt ? WindowCovering3.EndProductType.TiltOnlyInteriorBlind : WindowCovering3.EndProductType.RollerShade),
|
|
172241
|
-
operationalStatus
|
|
172242
|
-
|
|
172243
|
-
|
|
172244
|
-
|
|
172245
|
-
},
|
|
172246
|
-
...this.features.absolutePosition && this.features.lift ? {
|
|
172247
|
-
installedOpenLimitLift: 0,
|
|
172248
|
-
installedClosedLimitLift: 1e4,
|
|
172249
|
-
currentPositionLift: currentLift100ths
|
|
172250
|
-
} : {},
|
|
172251
|
-
...this.features.absolutePosition && this.features.tilt ? {
|
|
172252
|
-
installedOpenLimitTilt: 0,
|
|
172253
|
-
installedClosedLimitTilt: 1e4,
|
|
172254
|
-
currentPositionTilt: currentTilt100ths
|
|
172255
|
-
} : {},
|
|
172474
|
+
// Target before operationalStatus so the wire order matches the
|
|
172475
|
+
// certified Eve MotionBlinds (state, target, current). Patch insertion
|
|
172476
|
+
// order propagates into matter.js's changeList via for-in over values
|
|
172477
|
+
// (Datasource.js:414), then through attrsChanged.emit (#328).
|
|
172256
172478
|
...this.features.positionAwareLift ? {
|
|
172257
|
-
currentPositionLiftPercentage: currentLift,
|
|
172258
|
-
currentPositionLiftPercent100ths: currentLift100ths,
|
|
172259
172479
|
targetPositionLiftPercent100ths: inferTarget(
|
|
172260
172480
|
currentLift100ths,
|
|
172261
172481
|
this.state.targetPositionLiftPercent100ths
|
|
172262
172482
|
)
|
|
172263
172483
|
} : {},
|
|
172264
172484
|
...this.features.positionAwareTilt ? {
|
|
172265
|
-
currentPositionTiltPercentage: currentTilt,
|
|
172266
|
-
currentPositionTiltPercent100ths: currentTilt100ths,
|
|
172267
172485
|
targetPositionTiltPercent100ths: inferTarget(
|
|
172268
172486
|
currentTilt100ths,
|
|
172269
172487
|
this.state.targetPositionTiltPercent100ths
|
|
172270
172488
|
)
|
|
172489
|
+
} : {},
|
|
172490
|
+
operationalStatus: {
|
|
172491
|
+
global: movementStatus,
|
|
172492
|
+
...this.features.lift ? { lift: movementStatus } : {},
|
|
172493
|
+
...this.features.tilt ? { tilt: movementStatus } : {}
|
|
172494
|
+
},
|
|
172495
|
+
...this.features.positionAwareLift && !startedMoving ? {
|
|
172496
|
+
currentPositionLiftPercent100ths: currentLift100ths
|
|
172497
|
+
} : {},
|
|
172498
|
+
...this.features.positionAwareTilt && !startedMoving ? {
|
|
172499
|
+
currentPositionTiltPercent100ths: currentTilt100ths
|
|
172271
172500
|
} : {}
|
|
172272
172501
|
}
|
|
172273
172502
|
);
|
|
@@ -172284,7 +172513,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172284
172513
|
const currentLift = this.state.currentPositionLiftPercent100ths ?? 0;
|
|
172285
172514
|
const currentTilt = this.state.currentPositionTiltPercent100ths ?? 0;
|
|
172286
172515
|
logger179.info(
|
|
172287
|
-
`handleMovement: type=${MovementType[type]}, direction=${MovementDirection[direction]}, target=${targetPercent100ths}, currentLift=${currentLift}, currentTilt=${currentTilt}
|
|
172516
|
+
`handleMovement: type=${MovementType[type]}, direction=${MovementDirection[direction]}, target=${targetPercent100ths}, currentLift=${currentLift}, currentTilt=${currentTilt}`
|
|
172288
172517
|
);
|
|
172289
172518
|
if (type === MovementType.Lift) {
|
|
172290
172519
|
this.lastLiftMovementMs = Date.now();
|
|
@@ -172293,7 +172522,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172293
172522
|
this.handleLiftOpen();
|
|
172294
172523
|
} else if (targetPercent100ths === 1e4) {
|
|
172295
172524
|
this.handleLiftClose();
|
|
172296
|
-
} else if (targetPercent100ths != null && this.features.
|
|
172525
|
+
} else if (targetPercent100ths != null && this.features.positionAwareLift) {
|
|
172297
172526
|
this.handleGoToLiftPosition(targetPercent100ths);
|
|
172298
172527
|
} else if (direction === MovementDirection.Open) {
|
|
172299
172528
|
this.handleLiftOpen();
|
|
@@ -172311,7 +172540,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172311
172540
|
this.handleTiltOpen();
|
|
172312
172541
|
} else if (targetPercent100ths === 1e4) {
|
|
172313
172542
|
this.handleTiltClose();
|
|
172314
|
-
} else if (targetPercent100ths != null && this.features.
|
|
172543
|
+
} else if (targetPercent100ths != null && this.features.positionAwareTilt) {
|
|
172315
172544
|
this.handleGoToTiltPosition(targetPercent100ths);
|
|
172316
172545
|
} else if (direction === MovementDirection.Open) {
|
|
172317
172546
|
this.handleTiltOpen();
|
|
@@ -172353,9 +172582,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172353
172582
|
const timeSinceLastCommand = now - this.lastLiftCommandTime;
|
|
172354
172583
|
this.lastLiftCommandTime = now;
|
|
172355
172584
|
const isFirstInSequence = timeSinceLastCommand > _WindowCoveringServerBase.COMMAND_SEQUENCE_THRESHOLD_MS;
|
|
172356
|
-
const
|
|
172585
|
+
const overrideMs = this.resolveDebounceOverride(homeAssistant);
|
|
172586
|
+
const debounceMs = overrideMs != null ? overrideMs : isFirstInSequence ? _WindowCoveringServerBase.DEBOUNCE_INITIAL_MS : _WindowCoveringServerBase.DEBOUNCE_SUBSEQUENT_MS;
|
|
172357
172587
|
logger179.debug(
|
|
172358
|
-
`Lift command: target=${targetPosition}%, debounce=${debounceMs}ms (${isFirstInSequence ? "initial" : "subsequent"})`
|
|
172588
|
+
`Lift command: target=${targetPosition}%, debounce=${debounceMs}ms (${overrideMs != null ? "override" : isFirstInSequence ? "initial" : "subsequent"})`
|
|
172359
172589
|
);
|
|
172360
172590
|
if (this.liftDebounceTimer) {
|
|
172361
172591
|
clearTimeout(this.liftDebounceTimer);
|
|
@@ -172402,9 +172632,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
|
|
|
172402
172632
|
const timeSinceLastCommand = now - this.lastTiltCommandTime;
|
|
172403
172633
|
this.lastTiltCommandTime = now;
|
|
172404
172634
|
const isFirstInSequence = timeSinceLastCommand > _WindowCoveringServerBase.COMMAND_SEQUENCE_THRESHOLD_MS;
|
|
172405
|
-
const
|
|
172635
|
+
const overrideMs = this.resolveDebounceOverride(homeAssistant);
|
|
172636
|
+
const debounceMs = overrideMs != null ? overrideMs : isFirstInSequence ? _WindowCoveringServerBase.DEBOUNCE_INITIAL_MS : _WindowCoveringServerBase.DEBOUNCE_SUBSEQUENT_MS;
|
|
172406
172637
|
logger179.debug(
|
|
172407
|
-
`Tilt command: target=${targetPosition}%, debounce=${debounceMs}ms (${isFirstInSequence ? "initial" : "subsequent"})`
|
|
172638
|
+
`Tilt command: target=${targetPosition}%, debounce=${debounceMs}ms (${overrideMs != null ? "override" : isFirstInSequence ? "initial" : "subsequent"})`
|
|
172408
172639
|
);
|
|
172409
172640
|
if (this.tiltDebounceTimer) {
|
|
172410
172641
|
clearTimeout(this.tiltDebounceTimer);
|
|
@@ -172478,12 +172709,13 @@ var DEVICE_CLASS_TO_MATTER_TYPE = {
|
|
|
172478
172709
|
type: WindowCovering3.WindowCoveringType.Rollershade,
|
|
172479
172710
|
endProductType: WindowCovering3.EndProductType.RollerShade
|
|
172480
172711
|
},
|
|
172481
|
-
// Velux-style motorized roof/casement windows.
|
|
172482
|
-
//
|
|
172483
|
-
//
|
|
172712
|
+
// Velux-style motorized roof/casement windows. No Matter WindowCoveringType
|
|
172713
|
+
// for "window", so map to Rollershade + RollerShade (the spec default pair).
|
|
172714
|
+
// EndProductType is a FixedAttribute, and Alexa's routine picker drops
|
|
172715
|
+
// devices it can't categorize - Unknown (255) lands there (#312).
|
|
172484
172716
|
window: {
|
|
172485
172717
|
type: WindowCovering3.WindowCoveringType.Rollershade,
|
|
172486
|
-
endProductType: WindowCovering3.EndProductType.
|
|
172718
|
+
endProductType: WindowCovering3.EndProductType.RollerShade
|
|
172487
172719
|
}
|
|
172488
172720
|
};
|
|
172489
172721
|
var deviceClassMapping = (entity) => {
|
|
@@ -172636,24 +172868,17 @@ var CoverWindowCoveringServer = WindowCoveringServer2(config5);
|
|
|
172636
172868
|
|
|
172637
172869
|
// src/matter/endpoints/legacy/cover/index.ts
|
|
172638
172870
|
var logger181 = Logger.get("CoverDevice");
|
|
172639
|
-
var
|
|
172640
|
-
var CoverDeviceType = (supportedFeatures, hasBattery, entityId, isDiscrete) => {
|
|
172871
|
+
var CoverDeviceType = (supportedFeatures, hasBattery, entityId) => {
|
|
172641
172872
|
const features2 = /* @__PURE__ */ new Set();
|
|
172642
172873
|
if (testBit(supportedFeatures, CoverSupportedFeatures.support_open)) {
|
|
172643
172874
|
features2.add("Lift");
|
|
172644
172875
|
features2.add("PositionAwareLift");
|
|
172645
|
-
if (!isDiscrete) {
|
|
172646
|
-
features2.add("AbsolutePosition");
|
|
172647
|
-
}
|
|
172648
172876
|
} else {
|
|
172649
172877
|
logger181.warn(
|
|
172650
172878
|
`[${entityId}] Cover has no support_open feature (supported_features=${supportedFeatures}), adding Lift anyway`
|
|
172651
172879
|
);
|
|
172652
172880
|
features2.add("Lift");
|
|
172653
172881
|
features2.add("PositionAwareLift");
|
|
172654
|
-
if (!isDiscrete) {
|
|
172655
|
-
features2.add("AbsolutePosition");
|
|
172656
|
-
}
|
|
172657
172882
|
}
|
|
172658
172883
|
if (testBit(supportedFeatures, CoverSupportedFeatures.support_open_tilt)) {
|
|
172659
172884
|
features2.add("Tilt");
|
|
@@ -172662,7 +172887,6 @@ var CoverDeviceType = (supportedFeatures, hasBattery, entityId, isDiscrete) => {
|
|
|
172662
172887
|
CoverSupportedFeatures.support_set_tilt_position
|
|
172663
172888
|
)) {
|
|
172664
172889
|
features2.add("PositionAwareTilt");
|
|
172665
|
-
features2.add("AbsolutePosition");
|
|
172666
172890
|
}
|
|
172667
172891
|
}
|
|
172668
172892
|
logger181.info(
|
|
@@ -172697,18 +172921,10 @@ function CoverDevice(homeAssistantEntity) {
|
|
|
172697
172921
|
`[${entityId}] Creating cover without battery (batteryAttr=${hasBatteryAttr}, batteryEntity=${homeAssistantEntity.mapping?.batteryEntity ?? "none"})`
|
|
172698
172922
|
);
|
|
172699
172923
|
}
|
|
172700
|
-
const deviceClass = attributes7.device_class;
|
|
172701
|
-
const isDiscrete = typeof deviceClass === "string" && DISCRETE_COVER_CLASSES.has(deviceClass.toLowerCase());
|
|
172702
|
-
if (isDiscrete) {
|
|
172703
|
-
logger181.info(
|
|
172704
|
-
`[${entityId}] Garage/gate cover (device_class=${deviceClass}): using discrete Open/Close mode`
|
|
172705
|
-
);
|
|
172706
|
-
}
|
|
172707
172924
|
return CoverDeviceType(
|
|
172708
172925
|
attributes7.supported_features ?? 0,
|
|
172709
172926
|
hasBattery,
|
|
172710
|
-
entityId
|
|
172711
|
-
isDiscrete
|
|
172927
|
+
entityId
|
|
172712
172928
|
).set({
|
|
172713
172929
|
homeAssistantEntity
|
|
172714
172930
|
});
|
|
@@ -173775,6 +173991,9 @@ function LightDevice(homeAssistantEntity) {
|
|
|
173775
173991
|
const hasPowerEntity = !!homeAssistantEntity.mapping?.powerEntity;
|
|
173776
173992
|
const hasEnergyEntity = !!homeAssistantEntity.mapping?.energyEntity;
|
|
173777
173993
|
let device = deviceType;
|
|
173994
|
+
if (hasPowerEntity || hasEnergyEntity) {
|
|
173995
|
+
device = device.with(HaPowerTopologyServer);
|
|
173996
|
+
}
|
|
173778
173997
|
if (hasPowerEntity) {
|
|
173779
173998
|
device = device.with(HaElectricalPowerMeasurementServer);
|
|
173780
173999
|
}
|
|
@@ -175300,6 +175519,10 @@ var StandalonePowerServer = class extends ElectricalPowerMeasurementServer {
|
|
|
175300
175519
|
var PowerServer = StandalonePowerServer.set({
|
|
175301
175520
|
powerMode: ElectricalPowerMeasurement3.PowerMode.Ac,
|
|
175302
175521
|
numberOfMeasurementTypes: 1,
|
|
175522
|
+
// SmartThings keeps the endpoint in a "device not yet updated" state
|
|
175523
|
+
// when activePower stays null. Seed 0 so the cluster reports a value
|
|
175524
|
+
// for energy/voltage/current-only sensors that never feed activePower.
|
|
175525
|
+
activePower: 0,
|
|
175303
175526
|
accuracy: [
|
|
175304
175527
|
{
|
|
175305
175528
|
measurementType: ElectricalPowerMeasurement3.MeasurementType.ActivePower,
|
|
@@ -175352,6 +175575,9 @@ var StandaloneEnergyServer = class extends EnergyFeaturedBase {
|
|
|
175352
175575
|
StandaloneEnergyServer2.State = State;
|
|
175353
175576
|
})(StandaloneEnergyServer || (StandaloneEnergyServer = {}));
|
|
175354
175577
|
var EnergyServer = StandaloneEnergyServer.set({
|
|
175578
|
+
// Match the activePower=0 default so SmartThings doesn't show "- kWh"
|
|
175579
|
+
// on a SolarPower endpoint whose mapped entity only carries power data.
|
|
175580
|
+
cumulativeEnergyImported: { energy: 0 },
|
|
175355
175581
|
accuracy: {
|
|
175356
175582
|
measurementType: ElectricalPowerMeasurement3.MeasurementType.ElectricalEnergy,
|
|
175357
175583
|
measured: true,
|
|
@@ -175370,6 +175596,7 @@ var ElectricalSensorType = SolarPowerDevice.with(
|
|
|
175370
175596
|
BasicInformationServer2,
|
|
175371
175597
|
IdentifyServer2,
|
|
175372
175598
|
HomeAssistantEntityBehavior,
|
|
175599
|
+
HaPowerTopologyServer,
|
|
175373
175600
|
PowerServer,
|
|
175374
175601
|
EnergyServer
|
|
175375
175602
|
);
|
|
@@ -176812,6 +177039,9 @@ function SwitchDevice(homeAssistantEntity) {
|
|
|
176812
177039
|
const hasPowerEntity = !!homeAssistantEntity.mapping?.powerEntity;
|
|
176813
177040
|
const hasEnergyEntity = !!homeAssistantEntity.mapping?.energyEntity;
|
|
176814
177041
|
let device = hasBatteryAttr || hasBatteryEntity ? SwitchWithBatteryEndpointType : SwitchEndpointType;
|
|
177042
|
+
if (hasPowerEntity || hasEnergyEntity) {
|
|
177043
|
+
device = device.with(HaPowerTopologyServer);
|
|
177044
|
+
}
|
|
176815
177045
|
if (hasPowerEntity) {
|
|
176816
177046
|
device = device.with(HaElectricalPowerMeasurementServer);
|
|
176817
177047
|
}
|
|
@@ -176907,7 +177137,9 @@ function getSession(endpoint) {
|
|
|
176907
177137
|
completedAreas: /* @__PURE__ */ new Set(),
|
|
176908
177138
|
lastCurrentArea: null,
|
|
176909
177139
|
activeAreas: [],
|
|
176910
|
-
loggedShortCircuits: /* @__PURE__ */ new Set()
|
|
177140
|
+
loggedShortCircuits: /* @__PURE__ */ new Set(),
|
|
177141
|
+
observedCleaning: false,
|
|
177142
|
+
pendingDispatches: []
|
|
176911
177143
|
};
|
|
176912
177144
|
cleaningSessions.set(endpoint, session);
|
|
176913
177145
|
}
|
|
@@ -176938,17 +177170,45 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
176938
177170
|
},
|
|
176939
177171
|
{ force: true }
|
|
176940
177172
|
);
|
|
177173
|
+
if (newMode === 1 /* Cleaning */) {
|
|
177174
|
+
s.observedCleaning = true;
|
|
177175
|
+
}
|
|
176941
177176
|
if (previousMode !== newMode) {
|
|
176942
177177
|
if (newMode === 0 /* Idle */) {
|
|
176943
177178
|
if (s.lastCurrentArea !== null) {
|
|
176944
177179
|
s.completedAreas.add(s.lastCurrentArea);
|
|
176945
177180
|
s.lastCurrentArea = null;
|
|
177181
|
+
}
|
|
177182
|
+
if (s.pendingDispatches.length > 0 && s.observedCleaning) {
|
|
176946
177183
|
try {
|
|
176947
177184
|
const serviceArea = this.agent.get(ServiceAreaBehavior);
|
|
176948
|
-
serviceArea.state.currentArea
|
|
177185
|
+
const prev = serviceArea.state.currentArea;
|
|
177186
|
+
if (typeof prev === "number") {
|
|
177187
|
+
s.completedAreas.add(prev);
|
|
177188
|
+
}
|
|
177189
|
+
const next = s.pendingDispatches.shift();
|
|
177190
|
+
if (next) {
|
|
177191
|
+
const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
|
|
177192
|
+
homeAssistant.callAction(next.action);
|
|
177193
|
+
this.trySetCurrentArea(next.areaId);
|
|
177194
|
+
}
|
|
177195
|
+
} catch {
|
|
177196
|
+
}
|
|
177197
|
+
s.loggedShortCircuits.clear();
|
|
177198
|
+
return;
|
|
177199
|
+
}
|
|
177200
|
+
if (s.observedCleaning) {
|
|
177201
|
+
try {
|
|
177202
|
+
const serviceArea = this.agent.get(ServiceAreaBehavior);
|
|
177203
|
+
const last = serviceArea.state.currentArea;
|
|
177204
|
+
if (typeof last === "number") {
|
|
177205
|
+
s.completedAreas.add(last);
|
|
177206
|
+
serviceArea.state.currentArea = null;
|
|
177207
|
+
}
|
|
176949
177208
|
this.updateProgressFromTracking(serviceArea);
|
|
176950
177209
|
} catch {
|
|
176951
177210
|
}
|
|
177211
|
+
s.observedCleaning = false;
|
|
176952
177212
|
}
|
|
176953
177213
|
s.loggedShortCircuits.clear();
|
|
176954
177214
|
} else if (newMode === 1 /* Cleaning */) {
|
|
@@ -176962,6 +177222,15 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
176962
177222
|
}
|
|
176963
177223
|
}
|
|
176964
177224
|
}
|
|
177225
|
+
if (newMode === 0 /* Idle */ && s.activeAreas.length === 0) {
|
|
177226
|
+
try {
|
|
177227
|
+
const serviceArea = this.agent.get(ServiceAreaBehavior);
|
|
177228
|
+
if (serviceArea.state.currentArea !== null) {
|
|
177229
|
+
serviceArea.state.currentArea = null;
|
|
177230
|
+
}
|
|
177231
|
+
} catch {
|
|
177232
|
+
}
|
|
177233
|
+
}
|
|
176965
177234
|
if (newMode === 1 /* Cleaning */) {
|
|
176966
177235
|
this.updateCurrentRoomFromSensor();
|
|
176967
177236
|
}
|
|
@@ -177146,6 +177415,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
177146
177415
|
s.completedAreas.clear();
|
|
177147
177416
|
s.lastCurrentArea = null;
|
|
177148
177417
|
s.loggedShortCircuits.clear();
|
|
177418
|
+
s.pendingDispatches = [];
|
|
177149
177419
|
this.trySetCurrentArea(s.activeAreas[0]);
|
|
177150
177420
|
homeAssistant.callAction(this.state.config.start(void 0, this.agent));
|
|
177151
177421
|
this.state.currentMode = newMode;
|
|
@@ -177162,6 +177432,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
177162
177432
|
s.completedAreas.clear();
|
|
177163
177433
|
s.lastCurrentArea = null;
|
|
177164
177434
|
s.loggedShortCircuits.clear();
|
|
177435
|
+
s.pendingDispatches = [];
|
|
177165
177436
|
this.trySetCurrentArea(areaId);
|
|
177166
177437
|
homeAssistant.callAction(
|
|
177167
177438
|
this.state.config.cleanRoom(newMode, this.agent)
|
|
@@ -177182,6 +177453,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
177182
177453
|
s.completedAreas.clear();
|
|
177183
177454
|
s.lastCurrentArea = null;
|
|
177184
177455
|
s.loggedShortCircuits.clear();
|
|
177456
|
+
s.pendingDispatches = [];
|
|
177185
177457
|
this.trySetCurrentArea(s.activeAreas[0]);
|
|
177186
177458
|
}
|
|
177187
177459
|
} catch {
|
|
@@ -177195,6 +177467,8 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
|
|
|
177195
177467
|
s.lastCurrentArea = null;
|
|
177196
177468
|
s.activeAreas = [];
|
|
177197
177469
|
s.loggedShortCircuits.clear();
|
|
177470
|
+
s.pendingDispatches = [];
|
|
177471
|
+
s.observedCleaning = false;
|
|
177198
177472
|
homeAssistant.callAction(
|
|
177199
177473
|
this.state.config.returnToBase(void 0, this.agent)
|
|
177200
177474
|
);
|
|
@@ -177771,8 +178045,8 @@ function buildSupportedModes2(attributes7, includeUnnamedRooms = false, customAr
|
|
|
177771
178045
|
}
|
|
177772
178046
|
return modes;
|
|
177773
178047
|
}
|
|
177774
|
-
function handleCustomServiceAreas(selectedAreas, customAreas,
|
|
177775
|
-
const matched = selectedAreas.map((areaId) => customAreas[areaId - 1]).filter(
|
|
178048
|
+
function handleCustomServiceAreas(selectedAreas, customAreas, session) {
|
|
178049
|
+
const matched = selectedAreas.map((areaId) => ({ areaId, area: customAreas[areaId - 1] })).filter((m) => !!m.area);
|
|
177776
178050
|
if (matched.length === 0) {
|
|
177777
178051
|
logger194.warn(
|
|
177778
178052
|
`Custom service areas: no match for selected IDs ${selectedAreas.join(", ")}`
|
|
@@ -177780,17 +178054,13 @@ function handleCustomServiceAreas(selectedAreas, customAreas, homeAssistant) {
|
|
|
177780
178054
|
return { action: "vacuum.start" };
|
|
177781
178055
|
}
|
|
177782
178056
|
logger194.info(
|
|
177783
|
-
`Custom service areas:
|
|
178057
|
+
`Custom service areas: ${matched.length} room(s) queued: ${matched.map(({ area }) => `${area.service} (${area.name})`).join(", ")}`
|
|
177784
178058
|
);
|
|
177785
|
-
|
|
177786
|
-
|
|
177787
|
-
|
|
177788
|
-
|
|
177789
|
-
|
|
177790
|
-
data: area.data
|
|
177791
|
-
});
|
|
177792
|
-
}
|
|
177793
|
-
const first = matched[0];
|
|
178059
|
+
session.pendingDispatches = matched.slice(1).map(({ areaId, area }) => ({
|
|
178060
|
+
areaId,
|
|
178061
|
+
action: { action: area.service, target: area.target, data: area.data }
|
|
178062
|
+
}));
|
|
178063
|
+
const first = matched[0].area;
|
|
177794
178064
|
return {
|
|
177795
178065
|
action: first.service,
|
|
177796
178066
|
target: first.target,
|
|
@@ -177824,9 +178094,14 @@ var vacuumRvcRunModeConfig = {
|
|
|
177824
178094
|
);
|
|
177825
178095
|
return isCleaning ? 1 /* Cleaning */ : 0 /* Idle */;
|
|
177826
178096
|
},
|
|
177827
|
-
getSupportedModes: (entity) => {
|
|
178097
|
+
getSupportedModes: (entity, agent) => {
|
|
177828
178098
|
const attributes7 = entity.attributes;
|
|
177829
|
-
|
|
178099
|
+
const customAreas = agent.get(HomeAssistantEntityBehavior).state.mapping?.customServiceAreas;
|
|
178100
|
+
return buildSupportedModes2(
|
|
178101
|
+
attributes7,
|
|
178102
|
+
false,
|
|
178103
|
+
customAreas && customAreas.length > 0 ? customAreas : void 0
|
|
178104
|
+
);
|
|
177830
178105
|
},
|
|
177831
178106
|
// biome-ignore lint/suspicious/noConfusingVoidType: Required by ValueSetter<void> interface
|
|
177832
178107
|
start: (_, agent) => {
|
|
@@ -177837,13 +178112,10 @@ var vacuumRvcRunModeConfig = {
|
|
|
177837
178112
|
const homeAssistant = agent.get(HomeAssistantEntityBehavior);
|
|
177838
178113
|
const entity = homeAssistant.entity;
|
|
177839
178114
|
const attributes7 = entity.state.attributes;
|
|
178115
|
+
const session = getSession(homeAssistant.endpoint);
|
|
177840
178116
|
const customAreas = homeAssistant.state.mapping?.customServiceAreas;
|
|
177841
178117
|
if (customAreas && customAreas.length > 0) {
|
|
177842
|
-
return handleCustomServiceAreas(
|
|
177843
|
-
selectedAreas,
|
|
177844
|
-
customAreas,
|
|
177845
|
-
homeAssistant
|
|
177846
|
-
);
|
|
178118
|
+
return handleCustomServiceAreas(selectedAreas, customAreas, session);
|
|
177847
178119
|
}
|
|
177848
178120
|
const cleanAreaRooms = homeAssistant.state.mapping?.cleanAreaRooms;
|
|
177849
178121
|
if (cleanAreaRooms && cleanAreaRooms.length > 0) {
|
|
@@ -177860,28 +178132,24 @@ var vacuumRvcRunModeConfig = {
|
|
|
177860
178132
|
}
|
|
177861
178133
|
const roomEntities = homeAssistant.state.mapping?.roomEntities;
|
|
177862
178134
|
if (roomEntities && roomEntities.length > 0) {
|
|
177863
|
-
const
|
|
178135
|
+
const matched = [];
|
|
177864
178136
|
for (const areaId of selectedAreas) {
|
|
177865
|
-
const
|
|
177866
|
-
|
|
177867
|
-
|
|
177868
|
-
if (buttonEntityId) {
|
|
177869
|
-
buttonEntityIds.push(buttonEntityId);
|
|
178137
|
+
const entityId = roomEntities.find((id) => toAreaId(id) === areaId);
|
|
178138
|
+
if (entityId) {
|
|
178139
|
+
matched.push({ areaId, entityId });
|
|
177870
178140
|
}
|
|
177871
178141
|
}
|
|
177872
|
-
if (
|
|
178142
|
+
if (matched.length > 0) {
|
|
177873
178143
|
logger194.info(
|
|
177874
|
-
`Roborock:
|
|
178144
|
+
`Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
|
|
177875
178145
|
);
|
|
177876
|
-
|
|
177877
|
-
|
|
177878
|
-
|
|
177879
|
-
|
|
177880
|
-
});
|
|
177881
|
-
}
|
|
178146
|
+
session.pendingDispatches = matched.slice(1).map(({ areaId, entityId }) => ({
|
|
178147
|
+
areaId,
|
|
178148
|
+
action: { action: "button.press", target: entityId }
|
|
178149
|
+
}));
|
|
177882
178150
|
return {
|
|
177883
178151
|
action: "button.press",
|
|
177884
|
-
target:
|
|
178152
|
+
target: matched[0].entityId
|
|
177885
178153
|
};
|
|
177886
178154
|
}
|
|
177887
178155
|
}
|
|
@@ -178218,7 +178486,7 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
|
|
|
178218
178486
|
await super.initialize();
|
|
178219
178487
|
const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
|
|
178220
178488
|
this.update(homeAssistant.entity);
|
|
178221
|
-
this.reactTo(homeAssistant.onChange, this.update);
|
|
178489
|
+
this.reactTo(homeAssistant.onChange, this.update, { offline: true });
|
|
178222
178490
|
}
|
|
178223
178491
|
update(entity) {
|
|
178224
178492
|
if (!entity.state || !entity.state.attributes) {
|
|
@@ -178237,13 +178505,17 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
|
|
|
178237
178505
|
currentMode = this.pendingMode;
|
|
178238
178506
|
}
|
|
178239
178507
|
}
|
|
178240
|
-
applyPatchState(
|
|
178241
|
-
|
|
178242
|
-
|
|
178243
|
-
|
|
178244
|
-
this.
|
|
178245
|
-
|
|
178246
|
-
|
|
178508
|
+
applyPatchState(
|
|
178509
|
+
this.state,
|
|
178510
|
+
{
|
|
178511
|
+
currentMode,
|
|
178512
|
+
supportedModes: this.state.config.getSupportedModes(
|
|
178513
|
+
entity.state,
|
|
178514
|
+
this.agent
|
|
178515
|
+
)
|
|
178516
|
+
},
|
|
178517
|
+
{ force: true }
|
|
178518
|
+
);
|
|
178247
178519
|
}
|
|
178248
178520
|
changeToMode(request) {
|
|
178249
178521
|
const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
|
|
@@ -179046,58 +179318,65 @@ function RvcOperationalStateServer2(config10) {
|
|
|
179046
179318
|
var logger198 = Logger.get("VacuumRvcOperationalStateServer");
|
|
179047
179319
|
function isCharging(entity) {
|
|
179048
179320
|
const attrs = entity.attributes;
|
|
179049
|
-
if (attrs.battery_icon?.includes("charging")) return true;
|
|
179050
179321
|
if (attrs.is_charging === true || attrs.charging === true) return true;
|
|
179322
|
+
if (attrs.is_charging === false || attrs.charging === false) return false;
|
|
179323
|
+
const raw = attrs.battery_level ?? attrs.battery;
|
|
179324
|
+
const level = typeof raw === "number" ? raw : Number.parseFloat(String(raw ?? ""));
|
|
179325
|
+
if (Number.isFinite(level) && level >= 100) return false;
|
|
179326
|
+
if (attrs.battery_icon?.includes("charging")) return true;
|
|
179051
179327
|
if (typeof attrs.status === "string" && attrs.status.toLowerCase().includes("charg"))
|
|
179052
179328
|
return true;
|
|
179053
179329
|
return false;
|
|
179054
179330
|
}
|
|
179055
|
-
|
|
179056
|
-
|
|
179057
|
-
|
|
179058
|
-
|
|
179059
|
-
|
|
179060
|
-
|
|
179061
|
-
|
|
179062
|
-
|
|
179063
|
-
|
|
179064
|
-
|
|
179065
|
-
|
|
179066
|
-
if (
|
|
179067
|
-
|
|
179068
|
-
|
|
179069
|
-
|
|
179070
|
-
|
|
179071
|
-
|
|
179072
|
-
|
|
179073
|
-
|
|
179074
|
-
|
|
179331
|
+
function mapVacuumOperationalState(entity) {
|
|
179332
|
+
const state = entity.state;
|
|
179333
|
+
const cleaningStates = [
|
|
179334
|
+
VacuumState.cleaning,
|
|
179335
|
+
VacuumState.segment_cleaning,
|
|
179336
|
+
VacuumState.zone_cleaning,
|
|
179337
|
+
VacuumState.spot_cleaning,
|
|
179338
|
+
VacuumState.mop_cleaning
|
|
179339
|
+
];
|
|
179340
|
+
let operationalState;
|
|
179341
|
+
if (state === VacuumState.docked) {
|
|
179342
|
+
if (isCharging(entity)) {
|
|
179343
|
+
operationalState = RvcOperationalState3.OperationalState.Charging;
|
|
179344
|
+
} else {
|
|
179345
|
+
operationalState = RvcOperationalState3.OperationalState.Docked;
|
|
179346
|
+
}
|
|
179347
|
+
} else if (state === VacuumState.returning) {
|
|
179348
|
+
operationalState = RvcOperationalState3.OperationalState.SeekingCharger;
|
|
179349
|
+
} else if (cleaningStates.includes(state)) {
|
|
179350
|
+
operationalState = RvcOperationalState3.OperationalState.Running;
|
|
179351
|
+
} else if (state === VacuumState.paused) {
|
|
179352
|
+
operationalState = RvcOperationalState3.OperationalState.Paused;
|
|
179353
|
+
} else if (state === VacuumState.idle) {
|
|
179354
|
+
if (isCharging(entity)) {
|
|
179355
|
+
operationalState = RvcOperationalState3.OperationalState.Charging;
|
|
179356
|
+
} else {
|
|
179357
|
+
operationalState = RvcOperationalState3.OperationalState.Stopped;
|
|
179358
|
+
}
|
|
179359
|
+
} else if (state === VacuumState.error || state === "unavailable") {
|
|
179360
|
+
operationalState = RvcOperationalState3.OperationalState.Error;
|
|
179361
|
+
} else {
|
|
179362
|
+
if (state.toLowerCase().includes("clean")) {
|
|
179363
|
+
logger198.info(
|
|
179364
|
+
`Unknown vacuum state "${state}" contains 'clean', treating as Running`
|
|
179365
|
+
);
|
|
179075
179366
|
operationalState = RvcOperationalState3.OperationalState.Running;
|
|
179076
|
-
} else if (state === VacuumState.paused) {
|
|
179077
|
-
operationalState = RvcOperationalState3.OperationalState.Paused;
|
|
179078
|
-
} else if (state === VacuumState.idle) {
|
|
179079
|
-
if (isCharging(entity)) {
|
|
179080
|
-
operationalState = RvcOperationalState3.OperationalState.Charging;
|
|
179081
|
-
} else {
|
|
179082
|
-
operationalState = RvcOperationalState3.OperationalState.Stopped;
|
|
179083
|
-
}
|
|
179084
|
-
} else if (state === VacuumState.error || state === "unavailable") {
|
|
179085
|
-
operationalState = RvcOperationalState3.OperationalState.Error;
|
|
179086
179367
|
} else {
|
|
179087
|
-
|
|
179088
|
-
|
|
179089
|
-
`Unknown vacuum state "${state}" contains 'clean', treating as Running`
|
|
179090
|
-
);
|
|
179091
|
-
operationalState = RvcOperationalState3.OperationalState.Running;
|
|
179092
|
-
} else {
|
|
179093
|
-
logger198.info(`Unknown vacuum state "${state}", treating as Stopped`);
|
|
179094
|
-
operationalState = RvcOperationalState3.OperationalState.Stopped;
|
|
179095
|
-
}
|
|
179368
|
+
logger198.info(`Unknown vacuum state "${state}", treating as Stopped`);
|
|
179369
|
+
operationalState = RvcOperationalState3.OperationalState.Stopped;
|
|
179096
179370
|
}
|
|
179097
|
-
|
|
179098
|
-
|
|
179099
|
-
|
|
179100
|
-
|
|
179371
|
+
}
|
|
179372
|
+
logger198.debug(
|
|
179373
|
+
`Vacuum operationalState: "${state}" -> ${RvcOperationalState3.OperationalState[operationalState]}`
|
|
179374
|
+
);
|
|
179375
|
+
return operationalState;
|
|
179376
|
+
}
|
|
179377
|
+
var VacuumRvcOperationalStateServer = RvcOperationalStateServer2({
|
|
179378
|
+
getOperationalState(entity) {
|
|
179379
|
+
return mapVacuumOperationalState(entity);
|
|
179101
179380
|
},
|
|
179102
179381
|
pause: (_, agent) => {
|
|
179103
179382
|
const supportedFeatures = agent.get(HomeAssistantEntityBehavior).entity.state.attributes.supported_features ?? 0;
|
|
@@ -181561,6 +181840,19 @@ init_dist();
|
|
|
181561
181840
|
init_diagnostic_event_bus();
|
|
181562
181841
|
var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
|
|
181563
181842
|
var DEAD_SESSION_TIMEOUT_MS2 = 6e4;
|
|
181843
|
+
var DEFAULT_SESSION_MAX_AGE_HOURS = 4;
|
|
181844
|
+
var SESSION_MAX_AGE_HOURS_RANGE = { min: 1, max: 168 };
|
|
181845
|
+
var ROTATION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
|
|
181846
|
+
function parseSessionMaxAgeHours(raw) {
|
|
181847
|
+
if (raw == null || raw === "") return DEFAULT_SESSION_MAX_AGE_HOURS;
|
|
181848
|
+
const n = Number.parseInt(raw, 10);
|
|
181849
|
+
if (Number.isNaN(n) || n < 0) return null;
|
|
181850
|
+
if (n === 0) return 0;
|
|
181851
|
+
const { min, max } = SESSION_MAX_AGE_HOURS_RANGE;
|
|
181852
|
+
if (n < min) return min;
|
|
181853
|
+
if (n > max) return max;
|
|
181854
|
+
return n;
|
|
181855
|
+
}
|
|
181564
181856
|
var ServerModeBridge = class {
|
|
181565
181857
|
constructor(logger207, dataProvider, endpointManager, server) {
|
|
181566
181858
|
this.dataProvider = dataProvider;
|
|
@@ -181583,6 +181875,10 @@ var ServerModeBridge = class {
|
|
|
181583
181875
|
deadSessionTimer = null;
|
|
181584
181876
|
staleSessionTimers = /* @__PURE__ */ new Map();
|
|
181585
181877
|
warmStartTimer = null;
|
|
181878
|
+
// Tracks when each session opened, used for age-based rotation (#287).
|
|
181879
|
+
sessionStartedAt = /* @__PURE__ */ new Map();
|
|
181880
|
+
rotationTimer = null;
|
|
181881
|
+
maxSessionAgeMs = 0;
|
|
181586
181882
|
// Tracks the last synced state JSON per entity to avoid pushing unchanged states.
|
|
181587
181883
|
lastSyncedState;
|
|
181588
181884
|
// Session lifecycle diagnostic handlers (non-destructive, logging only).
|
|
@@ -181667,6 +181963,7 @@ var ServerModeBridge = class {
|
|
|
181667
181963
|
});
|
|
181668
181964
|
}
|
|
181669
181965
|
this.wireSessionDiagnostics();
|
|
181966
|
+
this.startSessionRotation();
|
|
181670
181967
|
this.scheduleWarmStart();
|
|
181671
181968
|
logMemoryUsage(this.log, "server mode bridge running");
|
|
181672
181969
|
this.log.info("Server mode bridge started");
|
|
@@ -181682,6 +181979,7 @@ ${e?.toString()}`);
|
|
|
181682
181979
|
}
|
|
181683
181980
|
}
|
|
181684
181981
|
async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
|
|
181982
|
+
this.stopSessionRotation();
|
|
181685
181983
|
this.unwireSessionDiagnostics();
|
|
181686
181984
|
this.cancelWarmStart();
|
|
181687
181985
|
this.stopAutoForceSync();
|
|
@@ -181710,6 +182008,7 @@ ${e?.toString()}`);
|
|
|
181710
182008
|
await this.refreshDevices();
|
|
181711
182009
|
if (this.status.code === BridgeStatus.Running) {
|
|
181712
182010
|
this.startAutoForceSyncIfEnabled();
|
|
182011
|
+
this.startSessionRotation();
|
|
181713
182012
|
}
|
|
181714
182013
|
} catch (e) {
|
|
181715
182014
|
const reason = "Failed to update server mode bridge due to error:";
|
|
@@ -181799,6 +182098,7 @@ ${e?.toString()}`);
|
|
|
181799
182098
|
};
|
|
181800
182099
|
sessionManager.subscriptionsChanged.on(this.sessionDiagHandler);
|
|
181801
182100
|
this.sessionAddedHandler = (newSession) => {
|
|
182101
|
+
this.sessionStartedAt.set(newSession.id, Date.now());
|
|
181802
182102
|
this.log.info(
|
|
181803
182103
|
`Session opened: id=${newSession.id} peer=${newSession.peerNodeId}`
|
|
181804
182104
|
);
|
|
@@ -181813,6 +182113,7 @@ ${e?.toString()}`);
|
|
|
181813
182113
|
}
|
|
181814
182114
|
};
|
|
181815
182115
|
this.sessionDeletedHandler = (session) => {
|
|
182116
|
+
this.sessionStartedAt.delete(session.id);
|
|
181816
182117
|
const sessions = [...sessionManager.sessions];
|
|
181817
182118
|
this.log.warn(
|
|
181818
182119
|
`Session closed: id=${session.id} peer=${session.peerNodeId} | remaining sessions=${sessions.length}`
|
|
@@ -181904,6 +182205,85 @@ ${e?.toString()}`);
|
|
|
181904
182205
|
clearTimeout(timer);
|
|
181905
182206
|
}
|
|
181906
182207
|
this.staleSessionTimers.clear();
|
|
182208
|
+
this.sessionStartedAt.clear();
|
|
182209
|
+
}
|
|
182210
|
+
// Start the periodic age-based session rotation (#287).
|
|
182211
|
+
startSessionRotation() {
|
|
182212
|
+
this.stopSessionRotation();
|
|
182213
|
+
const hours = this.readSessionMaxAgeHours();
|
|
182214
|
+
if (hours === 0) {
|
|
182215
|
+
this.log.info(
|
|
182216
|
+
"Session rotation disabled (HAMH_MATTER_SESSION_MAX_AGE_HOURS=0)"
|
|
182217
|
+
);
|
|
182218
|
+
return;
|
|
182219
|
+
}
|
|
182220
|
+
this.maxSessionAgeMs = hours * 60 * 60 * 1e3;
|
|
182221
|
+
this.rotationTimer = setInterval(
|
|
182222
|
+
() => this.rotateAgedSessions(),
|
|
182223
|
+
ROTATION_CHECK_INTERVAL_MS
|
|
182224
|
+
);
|
|
182225
|
+
this.log.info(
|
|
182226
|
+
`Session rotation: max age ${hours}h, check every ${ROTATION_CHECK_INTERVAL_MS / 6e4}min`
|
|
182227
|
+
);
|
|
182228
|
+
}
|
|
182229
|
+
stopSessionRotation() {
|
|
182230
|
+
if (this.rotationTimer) {
|
|
182231
|
+
clearInterval(this.rotationTimer);
|
|
182232
|
+
this.rotationTimer = null;
|
|
182233
|
+
}
|
|
182234
|
+
}
|
|
182235
|
+
// Resolve session rotation max age. Bridge config wins, then env var,
|
|
182236
|
+
// then built-in default. 0 disables, otherwise clamped to range.
|
|
182237
|
+
readSessionMaxAgeHours() {
|
|
182238
|
+
const { min, max } = SESSION_MAX_AGE_HOURS_RANGE;
|
|
182239
|
+
const fromConfig = this.dataProvider.sessionMaxAgeHours;
|
|
182240
|
+
if (fromConfig != null && Number.isFinite(fromConfig) && fromConfig >= 0) {
|
|
182241
|
+
if (fromConfig === 0) return 0;
|
|
182242
|
+
if (fromConfig < min) return min;
|
|
182243
|
+
if (fromConfig > max) return max;
|
|
182244
|
+
return fromConfig;
|
|
182245
|
+
}
|
|
182246
|
+
const raw = process.env.HAMH_MATTER_SESSION_MAX_AGE_HOURS;
|
|
182247
|
+
const parsed = parseSessionMaxAgeHours(raw);
|
|
182248
|
+
if (parsed == null) {
|
|
182249
|
+
this.log.warn(
|
|
182250
|
+
`Invalid HAMH_MATTER_SESSION_MAX_AGE_HOURS=${raw}, falling back to ${DEFAULT_SESSION_MAX_AGE_HOURS}h`
|
|
182251
|
+
);
|
|
182252
|
+
return DEFAULT_SESSION_MAX_AGE_HOURS;
|
|
182253
|
+
}
|
|
182254
|
+
return parsed;
|
|
182255
|
+
}
|
|
182256
|
+
// Gracefully close sessions older than maxSessionAgeMs so controllers
|
|
182257
|
+
// re-establish CASE and re-subscribe. Stale (0-sub) sessions are handled
|
|
182258
|
+
// by the existing dead-session path, so only rotate ones with subscriptions.
|
|
182259
|
+
rotateAgedSessions() {
|
|
182260
|
+
if (this.maxSessionAgeMs === 0) return;
|
|
182261
|
+
try {
|
|
182262
|
+
const sessionManager = this.server.env.get(SessionManager);
|
|
182263
|
+
const now = Date.now();
|
|
182264
|
+
const closes = [];
|
|
182265
|
+
for (const s of [...sessionManager.sessions]) {
|
|
182266
|
+
const startedAt = this.sessionStartedAt.get(s.id);
|
|
182267
|
+
if (startedAt == null) continue;
|
|
182268
|
+
const ageMs = now - startedAt;
|
|
182269
|
+
if (ageMs < this.maxSessionAgeMs || s.isClosing || s.subscriptions.size === 0) {
|
|
182270
|
+
continue;
|
|
182271
|
+
}
|
|
182272
|
+
const ageMin = Math.round(ageMs / 6e4);
|
|
182273
|
+
this.log.info(
|
|
182274
|
+
`Rotating session ${s.id} (peer ${s.peerNodeId}, age ${ageMin}min, subs ${s.subscriptions.size})`
|
|
182275
|
+
);
|
|
182276
|
+
closes.push(
|
|
182277
|
+
s.initiateClose().catch(() => {
|
|
182278
|
+
return s.initiateForceClose();
|
|
182279
|
+
})
|
|
182280
|
+
);
|
|
182281
|
+
}
|
|
182282
|
+
if (closes.length > 0) {
|
|
182283
|
+
Promise.allSettled(closes).then(() => this.triggerMdnsReAnnounce());
|
|
182284
|
+
}
|
|
182285
|
+
} catch {
|
|
182286
|
+
}
|
|
181907
182287
|
}
|
|
181908
182288
|
stopAutoForceSync() {
|
|
181909
182289
|
if (this.autoForceSyncTimer) {
|
|
@@ -182320,6 +182700,11 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
|
|
|
182320
182700
|
* unlike errorStateLabel (id 1) which requires errorStateId 128-191.
|
|
182321
182701
|
*/
|
|
182322
182702
|
async pushKeepalive() {
|
|
182703
|
+
try {
|
|
182704
|
+
await this.construction.ready;
|
|
182705
|
+
} catch {
|
|
182706
|
+
return;
|
|
182707
|
+
}
|
|
182323
182708
|
try {
|
|
182324
182709
|
this.keepaliveCounter++;
|
|
182325
182710
|
const counter = this.keepaliveCounter;
|
|
@@ -183094,6 +183479,10 @@ function startOptionsBuilder(yargs2) {
|
|
|
183094
183479
|
type: "number",
|
|
183095
183480
|
description: "The refresh rate (in seconds) to detect new devices & entities or their configurations",
|
|
183096
183481
|
default: 60
|
|
183482
|
+
}).option("ha-message-timeout", {
|
|
183483
|
+
type: "number",
|
|
183484
|
+
description: "Timeout in milliseconds for individual Home Assistant WebSocket requests (registry fetches, action calls). Raise this if you see 'HA message ... timed out' warnings on a large or slow HA instance.",
|
|
183485
|
+
default: 6e4
|
|
183097
183486
|
}).option("http-auth-username", {
|
|
183098
183487
|
type: "string",
|
|
183099
183488
|
description: "Username for HTTP basic authentication (optional)"
|