@riddix/hamh 2.0.43 → 2.0.45

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.
@@ -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: "Full regular expression matching entity IDs. Use ^ and $ for anchors. Example: '^(light|switch)\\.kitchen_.*' matches all kitchen lights and switches."
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 = 3e4;
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(connection, {
152739
- type: "config/entity_registry/list"
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(connection, {
152744
- type: "config/device_registry/list"
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(connection, {
152749
- type: "config/label_registry/list"
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(connection, {
152754
- type: "config/area_registry/list"
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
- getAreaRegistry(connection).catch(
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 rawSerial = trimToLength(mapping?.customSerialNumber, 32, "...");
165397
- const serialNumber = rawSerial && this.serialNumberSuffix ? trimToLength(`${rawSerial}${this.serialNumberSuffix}`, 32, "...") : rawSerial;
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 rawSerial = ellipse(32, mapping?.customSerialNumber) ?? hash(32, entity.entity_id);
168419
- const serialNumber = serialNumberSuffix ? ellipse(32, `${rawSerial}${serialNumberSuffix}`) : rawSerial;
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(basicInformation.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 hvacMode = entity.state;
171828
- const systemMode = hvacModeToSystemMode[hvacMode] ?? Thermostat3.SystemMode.Off;
171829
- if (systemMode === Thermostat3.SystemMode.Auto) {
171830
- const modes = attributes4(entity).hvac_modes ?? [];
171831
- if (isHeatCoolOnly(modes)) {
171832
- const direction = getHeatCoolOnlyDirection(entity, agent);
171833
- return direction === "cooling" ? Thermostat3.SystemMode.Cool : Thermostat3.SystemMode.Heat;
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
- if (this.features.lift) {
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
- global: movementStatus,
172243
- ...this.features.lift ? { lift: movementStatus } : {},
172244
- ...this.features.tilt ? { tilt: movementStatus } : {}
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}, absolutePosition=${this.features.absolutePosition}`
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.absolutePosition) {
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.absolutePosition) {
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 debounceMs = isFirstInSequence ? _WindowCoveringServerBase.DEBOUNCE_INITIAL_MS : _WindowCoveringServerBase.DEBOUNCE_SUBSEQUENT_MS;
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 debounceMs = isFirstInSequence ? _WindowCoveringServerBase.DEBOUNCE_INITIAL_MS : _WindowCoveringServerBase.DEBOUNCE_SUBSEQUENT_MS;
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. There's no Matter
172482
- // WindowCoveringType for "window", so we map to lift-only Rollershade
172483
- // and use Unknown end-product to avoid implying a specific physical form.
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.Unknown
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 DISCRETE_COVER_CLASSES = /* @__PURE__ */ new Set(["garage", "gate"]);
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 = null;
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, homeAssistant) {
177775
- const matched = selectedAreas.map((areaId) => customAreas[areaId - 1]).filter(Boolean);
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: calling ${matched.length} service(s): ${matched.map((a) => `${a.service} (${a.name})`).join(", ")}`
178057
+ `Custom service areas: ${matched.length} room(s) queued: ${matched.map(({ area }) => `${area.service} (${area.name})`).join(", ")}`
177784
178058
  );
177785
- for (let i = 1; i < matched.length; i++) {
177786
- const area = matched[i];
177787
- homeAssistant.callAction({
177788
- action: area.service,
177789
- target: area.target,
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
- return buildSupportedModes2(attributes7);
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 buttonEntityIds = [];
178135
+ const matched = [];
177864
178136
  for (const areaId of selectedAreas) {
177865
- const buttonEntityId = roomEntities.find(
177866
- (id) => toAreaId(id) === areaId
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 (buttonEntityIds.length > 0) {
178142
+ if (matched.length > 0) {
177873
178143
  logger194.info(
177874
- `Roborock: Pressing button entities for selected rooms: ${buttonEntityIds.join(", ")}`
178144
+ `Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
177875
178145
  );
177876
- for (let i = 1; i < buttonEntityIds.length; i++) {
177877
- homeAssistant.callAction({
177878
- action: "button.press",
177879
- target: buttonEntityIds[i]
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: buttonEntityIds[0]
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(this.state, {
178241
- currentMode,
178242
- supportedModes: this.state.config.getSupportedModes(
178243
- entity.state,
178244
- this.agent
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
- var VacuumRvcOperationalStateServer = RvcOperationalStateServer2({
179056
- getOperationalState(entity) {
179057
- const state = entity.state;
179058
- const cleaningStates = [
179059
- VacuumState.cleaning,
179060
- VacuumState.segment_cleaning,
179061
- VacuumState.zone_cleaning,
179062
- VacuumState.spot_cleaning,
179063
- VacuumState.mop_cleaning
179064
- ];
179065
- let operationalState;
179066
- if (state === VacuumState.docked) {
179067
- if (isCharging(entity)) {
179068
- operationalState = RvcOperationalState3.OperationalState.Charging;
179069
- } else {
179070
- operationalState = RvcOperationalState3.OperationalState.Docked;
179071
- }
179072
- } else if (state === VacuumState.returning) {
179073
- operationalState = RvcOperationalState3.OperationalState.SeekingCharger;
179074
- } else if (cleaningStates.includes(state)) {
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
- if (state.toLowerCase().includes("clean")) {
179088
- logger198.info(
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
- logger198.debug(
179098
- `Vacuum operationalState: "${state}" -> ${RvcOperationalState3.OperationalState[operationalState]}`
179099
- );
179100
- return operationalState;
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)"