@riddix/hamh 2.0.42 → 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.
@@ -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",
@@ -146781,7 +146800,7 @@ var init_bridge_config_schema = __esm({
146781
146800
  },
146782
146801
  autoComposedDevices: {
146783
146802
  title: "Auto Composed Devices",
146784
- description: "Master toggle: combine related entities from the same Home Assistant device into a single Matter endpoint. Turns on battery, humidity, pressure, power, and energy auto-mapping at once \u2014 a Shelly Plug shows up as one device with power monitoring instead of several siblings.",
146803
+ description: "Master toggle: combine related entities from the same Home Assistant device into a single Matter endpoint. Turns on battery, humidity, pressure, power, and energy auto-mapping at once, a Shelly Plug shows up as one device with power monitoring instead of several siblings.",
146785
146804
  type: "boolean",
146786
146805
  default: false
146787
146806
  },
@@ -146799,13 +146818,13 @@ var init_bridge_config_schema = __esm({
146799
146818
  },
146800
146819
  preferEntityRegistryName: {
146801
146820
  title: "Prefer Entity Registry Name (HA 2026.4 workaround)",
146802
- description: "Use the entity registry name (or original_name) as nodeLabel instead of the composed friendly_name. Since Home Assistant 2026.4, friendly_name is prefixed with the device name, which breaks voice commands that relied on the short entity name. Resolution order: customName \u2192 registry name \u2192 registry original_name \u2192 friendly_name \u2192 entity_id. Matter has no alias concept \u2014 this only changes which single name is reported.",
146821
+ description: "Use the entity registry name (or original_name) as nodeLabel instead of the composed friendly_name. Since Home Assistant 2026.4, friendly_name is prefixed with the device name, which breaks voice commands that relied on the short entity name. Resolution order: customName \u2192 registry name \u2192 registry original_name \u2192 friendly_name \u2192 entity_id. Matter has no alias concept, this only changes which single name is reported.",
146803
146822
  type: "boolean",
146804
146823
  default: false
146805
146824
  },
146806
146825
  vacuumOnOff: {
146807
146826
  title: "Vacuum: Include OnOff Cluster (Alexa)",
146808
- description: "Add an OnOff cluster to robot vacuum endpoints. Alexa REQUIRES this (PowerController) to show robotic vacuums in the app. Without it, Alexa commissions the device but never displays it. In Server Mode this is enabled automatically \u2014 only check this for bridge mode. WARNING: OnOff is NOT part of the Matter RVC device type specification. Enabling this may break Apple Home (shows 'Updating') and Google Home.",
146827
+ description: "Add an OnOff cluster to robot vacuum endpoints. Alexa REQUIRES this (PowerController) to show robotic vacuums in the app. Without it, Alexa commissions the device but never displays it. In Server Mode this is enabled automatically, only check this for bridge mode. WARNING: OnOff is NOT part of the Matter RVC device type specification. Enabling this may break Apple Home (shows 'Updating') and Google Home.",
146809
146828
  type: "boolean"
146810
146829
  },
146811
146830
  alexaPreserveBrightnessOnTurnOn: {
@@ -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
 
@@ -148596,7 +148638,14 @@ function addLogEntry(entry) {
148596
148638
  function logsApi(_logger) {
148597
148639
  const router = express5.Router();
148598
148640
  router.get("/", (req, res) => {
148599
- const { level, search, limit = "100", offset = "0" } = req.query;
148641
+ const {
148642
+ level,
148643
+ search,
148644
+ category,
148645
+ facility,
148646
+ limit = "100",
148647
+ offset = "0"
148648
+ } = req.query;
148600
148649
  const limitNum = Math.min(
148601
148650
  500,
148602
148651
  Math.max(1, parseInt(limit, 10) || 100)
@@ -148607,6 +148656,18 @@ function logsApi(_logger) {
148607
148656
  const levels = level.split(",").map((l) => l.toLowerCase().trim());
148608
148657
  entries = entries.filter((e) => levels.includes(e.level.toLowerCase()));
148609
148658
  }
148659
+ if (category && typeof category === "string") {
148660
+ const cats = category.split(",").map((c) => c.toLowerCase().trim());
148661
+ entries = entries.filter(
148662
+ (e) => e.category != null && cats.includes(e.category.toLowerCase())
148663
+ );
148664
+ }
148665
+ if (facility && typeof facility === "string") {
148666
+ const facLower = facility.toLowerCase();
148667
+ entries = entries.filter(
148668
+ (e) => e.facility?.toLowerCase().includes(facLower)
148669
+ );
148670
+ }
148610
148671
  if (search && typeof search === "string") {
148611
148672
  const searchLower = search.toLowerCase();
148612
148673
  entries = entries.filter(
@@ -148823,6 +148884,7 @@ function entityMappingApi(mappingStorage) {
148823
148884
  customProductName: body.customProductName,
148824
148885
  customVendorName: body.customVendorName,
148825
148886
  customSerialNumber: body.customSerialNumber,
148887
+ customVendorId: body.customVendorId,
148826
148888
  disabled: body.disabled,
148827
148889
  filterLifeEntity: body.filterLifeEntity,
148828
148890
  cleaningModeEntity: body.cleaningModeEntity,
@@ -148841,8 +148903,10 @@ function entityMappingApi(mappingStorage) {
148841
148903
  currentRoomEntity: body.currentRoomEntity,
148842
148904
  valetudoIdentifier: body.valetudoIdentifier,
148843
148905
  coverSwapOpenClose: body.coverSwapOpenClose,
148906
+ coverSliderDebounceMs: body.coverSliderDebounceMs,
148844
148907
  disableClimateOnOff: body.disableClimateOnOff,
148845
148908
  disableClimateFanControl: body.disableClimateFanControl,
148909
+ climateKeepModeOnIdle: body.climateKeepModeOnIdle,
148846
148910
  composedEntities: body.composedEntities
148847
148911
  };
148848
148912
  const config10 = await mappingStorage.setMapping(request);
@@ -149306,8 +149370,10 @@ function configToProfileEntry(config10) {
149306
149370
  customFanSpeedTags: config10.customFanSpeedTags,
149307
149371
  valetudoIdentifier: config10.valetudoIdentifier,
149308
149372
  coverSwapOpenClose: config10.coverSwapOpenClose,
149373
+ coverSliderDebounceMs: config10.coverSliderDebounceMs,
149309
149374
  disableClimateOnOff: config10.disableClimateOnOff,
149310
- disableClimateFanControl: config10.disableClimateFanControl
149375
+ disableClimateFanControl: config10.disableClimateFanControl,
149376
+ climateKeepModeOnIdle: config10.climateKeepModeOnIdle
149311
149377
  };
149312
149378
  }
149313
149379
  function mappingProfileApi(mappingStorage) {
@@ -149429,8 +149495,10 @@ function mappingProfileApi(mappingStorage) {
149429
149495
  customFanSpeedTags: entry.customFanSpeedTags,
149430
149496
  valetudoIdentifier: entry.valetudoIdentifier,
149431
149497
  coverSwapOpenClose: entry.coverSwapOpenClose,
149498
+ coverSliderDebounceMs: entry.coverSliderDebounceMs,
149432
149499
  disableClimateOnOff: entry.disableClimateOnOff,
149433
- disableClimateFanControl: entry.disableClimateFanControl
149500
+ disableClimateFanControl: entry.disableClimateFanControl,
149501
+ climateKeepModeOnIdle: entry.climateKeepModeOnIdle
149434
149502
  });
149435
149503
  applied++;
149436
149504
  } catch (e) {
@@ -149482,6 +149550,18 @@ function testMatcher(matcher, device, entity, entityState, labels) {
149482
149550
  const slug = resolveLabelValue(matcher.value, labels);
149483
149551
  return !!device?.labels && device.labels.includes(slug);
149484
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
+ );
149485
149565
  case "entity_category":
149486
149566
  return entity?.entity_category === matcher.value;
149487
149567
  case "platform":
@@ -149516,6 +149596,62 @@ function testRegex(pattern2, value) {
149516
149596
  return false;
149517
149597
  }
149518
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
+ }
149519
149655
  function testDeviceName(pattern2, device) {
149520
149656
  if (!device) {
149521
149657
  return false;
@@ -151608,6 +151744,17 @@ init_esm7();
151608
151744
 
151609
151745
  // src/core/app/logger.ts
151610
151746
  init_esm();
151747
+ var MATTER_TRAFFIC_FACILITIES = /* @__PURE__ */ new Set([
151748
+ "InteractionServer",
151749
+ "MessageExchange",
151750
+ "MessageChannel",
151751
+ "ServerSubscription",
151752
+ "IncomingInteractionServerMessenger",
151753
+ "ExchangeManager"
151754
+ ]);
151755
+ function categoryFor(facility) {
151756
+ return MATTER_TRAFFIC_FACILITIES.has(facility) ? "matter-traffic" : void 0;
151757
+ }
151611
151758
  function logLevelFromString(level) {
151612
151759
  const customNames = {
151613
151760
  SILLY: -1 /* SILLY */
@@ -151653,6 +151800,26 @@ var LoggerService = class {
151653
151800
  MessageExchange: resolvedProtocolLevel
151654
151801
  };
151655
151802
  Logger.format = options.disableColors ? LogFormat.PLAIN : LogFormat.ANSI;
151803
+ const destinationLevel = this.customLogLevelMapping[this._level] ?? this._level;
151804
+ Logger.destinations["hamh-buffer"] = LogDestination({
151805
+ name: "hamh-buffer",
151806
+ level: destinationLevel,
151807
+ facilityLevels: {
151808
+ MessageChannel: resolvedProtocolLevel,
151809
+ MessageExchange: resolvedProtocolLevel
151810
+ },
151811
+ write: (text, message) => {
151812
+ const category = categoryFor(message.facility);
151813
+ if (!category) return;
151814
+ addLogEntry({
151815
+ timestamp: message.now.toISOString(),
151816
+ level: logLevelToString(message.level).toLowerCase(),
151817
+ message: text,
151818
+ facility: message.facility,
151819
+ category
151820
+ });
151821
+ }
151822
+ });
151656
151823
  }
151657
151824
  get(nameOrService) {
151658
151825
  let name;
@@ -151992,7 +152159,8 @@ var Options = class {
151992
152159
  return {
151993
152160
  url: this.startOptions.homeAssistantUrl,
151994
152161
  accessToken: this.startOptions.homeAssistantAccessToken,
151995
- refreshInterval: this.startOptions.homeAssistantRefreshInterval
152162
+ refreshInterval: this.startOptions.homeAssistantRefreshInterval,
152163
+ messageTimeoutMs: this.startOptions.haMessageTimeout
151996
152164
  };
151997
152165
  }
151998
152166
  get webApi() {
@@ -152684,25 +152852,33 @@ init_retry();
152684
152852
 
152685
152853
  // src/services/home-assistant/api/get-registry.ts
152686
152854
  init_send_ha_message();
152687
- async function getRegistry(connection) {
152688
- return sendHaMessage(connection, {
152689
- type: "config/entity_registry/list"
152690
- });
152855
+ async function getRegistry(connection, timeoutMs) {
152856
+ return sendHaMessage(
152857
+ connection,
152858
+ { type: "config/entity_registry/list" },
152859
+ timeoutMs
152860
+ );
152691
152861
  }
152692
- async function getDeviceRegistry(connection) {
152693
- return sendHaMessage(connection, {
152694
- type: "config/device_registry/list"
152695
- });
152862
+ async function getDeviceRegistry(connection, timeoutMs) {
152863
+ return sendHaMessage(
152864
+ connection,
152865
+ { type: "config/device_registry/list" },
152866
+ timeoutMs
152867
+ );
152696
152868
  }
152697
- async function getLabelRegistry(connection) {
152698
- return sendHaMessage(connection, {
152699
- type: "config/label_registry/list"
152700
- });
152869
+ async function getLabelRegistry(connection, timeoutMs) {
152870
+ return sendHaMessage(
152871
+ connection,
152872
+ { type: "config/label_registry/list" },
152873
+ timeoutMs
152874
+ );
152701
152875
  }
152702
- async function getAreaRegistry(connection) {
152703
- return sendHaMessage(connection, {
152704
- type: "config/area_registry/list"
152705
- });
152876
+ async function getAreaRegistry(connection, timeoutMs) {
152877
+ return sendHaMessage(
152878
+ connection,
152879
+ { type: "config/area_registry/list" },
152880
+ timeoutMs
152881
+ );
152706
152882
  }
152707
152883
 
152708
152884
  // src/services/home-assistant/home-assistant-registry.ts
@@ -152748,7 +152924,7 @@ var HomeAssistantRegistry = class extends Service {
152748
152924
  let refreshing = false;
152749
152925
  this.autoRefresh = setInterval(async () => {
152750
152926
  if (refreshing) {
152751
- logger146.debug("Skipping registry refresh \u2014 previous tick still running");
152927
+ logger146.debug("Skipping registry refresh, previous tick still running");
152752
152928
  return;
152753
152929
  }
152754
152930
  refreshing = true;
@@ -152805,12 +152981,15 @@ var HomeAssistantRegistry = class extends Service {
152805
152981
  }
152806
152982
  });
152807
152983
  }
152984
+ const timeoutMs = this.options.messageTimeoutMs;
152808
152985
  const [entityRegistry, statesList, deviceRegistry, labels, areas] = await Promise.all([
152809
- getRegistry(connection),
152986
+ getRegistry(connection, timeoutMs),
152810
152987
  getStates(connection),
152811
- getDeviceRegistry(connection),
152812
- getLabelRegistry(connection).catch(() => []),
152813
- getAreaRegistry(connection).catch(
152988
+ getDeviceRegistry(connection, timeoutMs),
152989
+ getLabelRegistry(connection, timeoutMs).catch(
152990
+ () => []
152991
+ ),
152992
+ getAreaRegistry(connection, timeoutMs).catch(
152814
152993
  () => []
152815
152994
  )
152816
152995
  ]);
@@ -153279,6 +153458,7 @@ var EntityMappingStorage = class extends Service {
153279
153458
  customProductName: request.customProductName?.trim() || void 0,
153280
153459
  customVendorName: request.customVendorName?.trim() || void 0,
153281
153460
  customSerialNumber: request.customSerialNumber?.trim() || void 0,
153461
+ customVendorId: sanitizeVendorId(request.customVendorId),
153282
153462
  disabled: request.disabled,
153283
153463
  filterLifeEntity: request.filterLifeEntity?.trim() || void 0,
153284
153464
  cleaningModeEntity: request.cleaningModeEntity?.trim() || void 0,
@@ -153299,11 +153479,13 @@ var EntityMappingStorage = class extends Service {
153299
153479
  currentRoomEntity: request.currentRoomEntity?.trim() || void 0,
153300
153480
  valetudoIdentifier: request.valetudoIdentifier?.trim() || void 0,
153301
153481
  coverSwapOpenClose: request.coverSwapOpenClose || void 0,
153482
+ coverSliderDebounceMs: sanitizeDebounceMs(request.coverSliderDebounceMs),
153302
153483
  disableClimateOnOff: request.disableClimateOnOff || void 0,
153303
153484
  disableClimateFanControl: request.disableClimateFanControl || void 0,
153485
+ climateKeepModeOnIdle: request.climateKeepModeOnIdle || void 0,
153304
153486
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0
153305
153487
  };
153306
- 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)) {
153307
153489
  bridgeMap.delete(request.entityId);
153308
153490
  } else {
153309
153491
  bridgeMap.set(request.entityId, config10);
@@ -153323,6 +153505,26 @@ var EntityMappingStorage = class extends Service {
153323
153505
  await this.persist();
153324
153506
  }
153325
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
+ }
153326
153528
 
153327
153529
  // src/services/storage/lock-credential-storage.ts
153328
153530
  init_service();
@@ -165337,14 +165539,16 @@ var ServerModeServerNode = class extends ServerNode {
165337
165539
  /**
165338
165540
  * Update root-level BasicInformation with entity-specific data.
165339
165541
  * In server mode, controllers (Apple Home, Alexa) read the root node's
165340
- * BasicInformation not the device endpoint's BridgedDeviceBasicInformation.
165542
+ * BasicInformation, not the device endpoint's BridgedDeviceBasicInformation.
165341
165543
  * Without this, server-mode devices show bridge defaults (e.g. "riddix" / "MatterHub").
165342
165544
  */
165343
165545
  async updateDeviceIdentity(entityId, device, mapping, friendlyName) {
165344
165546
  const nodeLabel = trimToLength(mapping?.customName, 32, "...") ?? trimToLength(friendlyName, 32, "...") ?? trimToLength(entityId, 32, "...");
165345
165547
  const productNameFromNodeLabel = this.featureFlags?.productNameFromNodeLabel === true ? trimToLength(sanitizeMatterString(nodeLabel ?? ""), 32, "...") ?? void 0 : void 0;
165346
- const rawSerial = trimToLength(mapping?.customSerialNumber, 32, "...");
165347
- 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;
165348
165552
  const basicInformation = dropUndefined({
165349
165553
  vendorName: trimToLength(mapping?.customVendorName, 32, "...") ?? trimToLength(device?.manufacturer, 32, "..."),
165350
165554
  productName: trimToLength(mapping?.customProductName, 32, "...") ?? productNameFromNodeLabel ?? trimToLength(device?.model_id, 32, "...") ?? trimToLength(device?.model, 32, "..."),
@@ -166447,6 +166651,9 @@ var BridgeDataProvider = class extends Service {
166447
166651
  get serialNumberSuffix() {
166448
166652
  return this.data.serialNumberSuffix;
166449
166653
  }
166654
+ get sessionMaxAgeHours() {
166655
+ return this.data.sessionMaxAgeHours;
166656
+ }
166450
166657
  /************************************************
166451
166658
  * Functions
166452
166659
  ************************************************/
@@ -166472,6 +166679,7 @@ var BridgeDataProvider = class extends Service {
166472
166679
  icon: this.icon,
166473
166680
  priority: this.priority,
166474
166681
  serialNumberSuffix: this.serialNumberSuffix,
166682
+ sessionMaxAgeHours: this.sessionMaxAgeHours,
166475
166683
  status: status3.code,
166476
166684
  statusReason: status3.reason,
166477
166685
  commissioning: commissioning ? {
@@ -167848,7 +168056,7 @@ ${e?.toString()}`);
167848
168056
  );
167849
168057
  if (totalSubs === 0 && sessions.length > 0) {
167850
168058
  this.log.warn(
167851
- `All subscriptions lost \u2014 ${sessions.length} session(s) still active, waiting for controller to re-subscribe`
168059
+ `All subscriptions lost, ${sessions.length} session(s) still active, waiting for controller to re-subscribe`
167852
168060
  );
167853
168061
  if (!this.deadSessionTimer) {
167854
168062
  this.deadSessionTimer = setTimeout(() => {
@@ -167896,7 +168104,7 @@ ${e?.toString()}`);
167896
168104
  for (const s of [...sessionManager.sessions]) {
167897
168105
  if (s !== newSession && !s.isClosing && s.peerNodeId === newSession.peerNodeId && s.fabric?.fabricIndex === newSession.fabric?.fabricIndex && s.subscriptions.size === 0) {
167898
168106
  this.log.info(
167899
- `Closing stale session ${s.id} (peer ${s.peerNodeId}, 0 subs) \u2014 replaced by session ${newSession.id}`
168107
+ `Closing stale session ${s.id} (peer ${s.peerNodeId}, 0 subs), replaced by session ${newSession.id}`
167900
168108
  );
167901
168109
  s.initiateForceClose().catch(() => {
167902
168110
  });
@@ -168068,6 +168276,12 @@ ${e?.toString()}`);
168068
168276
  if (!this.dataProvider.featureFlags?.autoForceSync) {
168069
168277
  return 0;
168070
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
+ }
168071
168285
  const { HomeAssistantEntityBehavior: HomeAssistantEntityBehavior2 } = await Promise.resolve().then(() => (init_home_assistant_entity_behavior(), home_assistant_entity_behavior_exports));
168072
168286
  const allEndpoints = [];
168073
168287
  const collect = (ep) => {
@@ -168351,7 +168565,7 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
168351
168565
  await super.initialize();
168352
168566
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168353
168567
  this.update(homeAssistant.entity);
168354
- this.reactTo(homeAssistant.onChange, this.update);
168568
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168355
168569
  }
168356
168570
  update(entity) {
168357
168571
  if (!entity.state || !entity.state.attributes) {
@@ -168365,10 +168579,14 @@ var BasicInformationServer2 = class extends BridgedDeviceBasicInformationServer
168365
168579
  const nodeLabel = ellipse(32, homeAssistant.state.customName) ?? ellipse(32, registryName) ?? ellipse(32, entity.state?.attributes?.friendly_name) ?? ellipse(32, entity.entity_id);
168366
168580
  const productNameFromNodeLabel = featureFlags?.productNameFromNodeLabel === true ? ellipse(32, sanitizeMatterString(nodeLabel ?? "")) ?? void 0 : void 0;
168367
168581
  const serialNumberSuffix = this.env.get(BridgeDataProvider).serialNumberSuffix;
168368
- const rawSerial = ellipse(32, mapping?.customSerialNumber) ?? hash(32, entity.entity_id);
168369
- 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;
168370
168588
  applyPatchState(this.state, {
168371
- vendorId: VendorId(basicInformation.vendorId),
168589
+ vendorId: VendorId(vendorId3),
168372
168590
  vendorName: ellipse(32, mapping?.customVendorName) ?? ellipse(32, device?.manufacturer) ?? hash(32, basicInformation.vendorName),
168373
168591
  productName: ellipse(32, mapping?.customProductName) ?? productNameFromNodeLabel ?? ellipse(32, device?.model_id) ?? ellipse(32, device?.model) ?? hash(32, basicInformation.productName),
168374
168592
  productLabel: ellipse(64, device?.model) ?? hash(64, basicInformation.productLabel),
@@ -168393,6 +168611,9 @@ function hash(maxLength, value) {
168393
168611
  const suffix = crypto6.createHash("md5").update(value ?? "").digest("hex").substring(0, hashLength);
168394
168612
  return trimToLength(value, maxLength, suffix);
168395
168613
  }
168614
+ function isValidVendorId(value) {
168615
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65534;
168616
+ }
168396
168617
 
168397
168618
  // src/matter/behaviors/electrical-energy-measurement-server.ts
168398
168619
  init_esm();
@@ -168417,7 +168638,7 @@ var ElectricalEnergyMeasurementServerBase = class extends FeaturedBase {
168417
168638
  );
168418
168639
  }
168419
168640
  this.update();
168420
- this.reactTo(homeAssistant.onChange, this.update);
168641
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168421
168642
  }
168422
168643
  update() {
168423
168644
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
@@ -168443,6 +168664,10 @@ var ElectricalEnergyMeasurementServerBase = class extends FeaturedBase {
168443
168664
  ElectricalEnergyMeasurementServerBase2.State = State;
168444
168665
  })(ElectricalEnergyMeasurementServerBase || (ElectricalEnergyMeasurementServerBase = {}));
168445
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 },
168446
168671
  accuracy: {
168447
168672
  measurementType: ElectricalPowerMeasurement3.MeasurementType.ElectricalEnergy,
168448
168673
  measured: true,
@@ -168477,7 +168702,7 @@ var ElectricalPowerMeasurementServerBase = class extends ElectricalPowerMeasurem
168477
168702
  );
168478
168703
  }
168479
168704
  this.update();
168480
- this.reactTo(homeAssistant.onChange, this.update);
168705
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168481
168706
  }
168482
168707
  update() {
168483
168708
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
@@ -168530,7 +168755,7 @@ var HumidityMeasurementServerBase = class extends RelativeHumidityMeasurementSer
168530
168755
  await super.initialize();
168531
168756
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168532
168757
  this.update(homeAssistant.entity);
168533
- this.reactTo(homeAssistant.onChange, this.update);
168758
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168534
168759
  }
168535
168760
  update(entity) {
168536
168761
  if (!entity.state || !entity.state.attributes) {
@@ -168594,7 +168819,7 @@ var PowerSourceServerBase = class extends FeaturedBase2 {
168594
168819
  );
168595
168820
  }
168596
168821
  this.update(homeAssistant.entity);
168597
- this.reactTo(homeAssistant.onChange, this.update);
168822
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168598
168823
  }
168599
168824
  update(entity) {
168600
168825
  if (!entity.state || !entity.state.attributes) {
@@ -168673,6 +168898,11 @@ var defaultBatteryConfig = {
168673
168898
  };
168674
168899
  var DefaultPowerSourceServer = PowerSourceServer2(defaultBatteryConfig);
168675
168900
 
168901
+ // src/matter/behaviors/power-topology-server.ts
168902
+ var HaPowerTopologyServer = PowerTopologyServer.with(
168903
+ PowerTopology3.Feature.NodeTopology
168904
+ );
168905
+
168676
168906
  // src/matter/behaviors/temperature-measurement-server.ts
168677
168907
  init_home_assistant_entity_behavior();
168678
168908
  var TemperatureMeasurementServerBase = class extends TemperatureMeasurementServer {
@@ -168680,7 +168910,7 @@ var TemperatureMeasurementServerBase = class extends TemperatureMeasurementServe
168680
168910
  await super.initialize();
168681
168911
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168682
168912
  this.update(homeAssistant.entity);
168683
- this.reactTo(homeAssistant.onChange, this.update);
168913
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168684
168914
  }
168685
168915
  update(entity) {
168686
168916
  if (!entity.state || !entity.state.attributes) {
@@ -168722,7 +168952,7 @@ var HepaFilterMonitoringServerBase = class extends FeaturedBase3 {
168722
168952
  await super.initialize();
168723
168953
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
168724
168954
  this.update(homeAssistant.entity);
168725
- this.reactTo(homeAssistant.onChange, this.update);
168955
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
168726
168956
  }
168727
168957
  update(entity) {
168728
168958
  if (!entity.state || !entity.state.attributes) {
@@ -169200,7 +169430,7 @@ var FeaturedBase5 = FanControlServer.with(
169200
169430
  "Rocking",
169201
169431
  "Wind"
169202
169432
  ).set({
169203
- // rockSupport / windSupport are Fixed quality attributes they MUST be set
169433
+ // rockSupport / windSupport are Fixed quality attributes, they MUST be set
169204
169434
  // via .set() at behavior creation time, NOT in initialize().
169205
169435
  // Without these, controllers reject attempts to enable rocking/wind.
169206
169436
  rockSupport: { rockUpDown: true },
@@ -169776,6 +170006,9 @@ var ComposedAirPurifierEndpoint = class _ComposedAirPurifierEndpoint extends End
169776
170006
  if (config10.batteryEntityId) {
169777
170007
  parentType = parentType.with(PowerSourceServer2(batteryConfig));
169778
170008
  }
170009
+ if (config10.powerEntityId || config10.energyEntityId) {
170010
+ parentType = parentType.with(HaPowerTopologyServer);
170011
+ }
169779
170012
  if (config10.powerEntityId) {
169780
170013
  parentType = parentType.with(HaElectricalPowerMeasurementServer);
169781
170014
  }
@@ -169976,7 +170209,7 @@ var PressureMeasurementServerBase = class extends PressureMeasurementServer {
169976
170209
  await super.initialize();
169977
170210
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
169978
170211
  this.update(homeAssistant.entity);
169979
- this.reactTo(homeAssistant.onChange, this.update);
170212
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
169980
170213
  }
169981
170214
  update(entity) {
169982
170215
  if (!entity.state || !entity.state.attributes) {
@@ -170111,6 +170344,9 @@ var ComposedSensorEndpoint = class _ComposedSensorEndpoint extends Endpoint {
170111
170344
  if (config10.batteryEntityId) {
170112
170345
  parentType = parentType.with(PowerSourceServer2(batteryConfig2));
170113
170346
  }
170347
+ if (config10.powerEntityId || config10.energyEntityId) {
170348
+ parentType = parentType.with(HaPowerTopologyServer);
170349
+ }
170114
170350
  if (config10.powerEntityId) {
170115
170351
  parentType = parentType.with(HaElectricalPowerMeasurementServer);
170116
170352
  }
@@ -170536,7 +170772,7 @@ var BooleanStateServerBase = class extends BooleanStateServer {
170536
170772
  await super.initialize();
170537
170773
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170538
170774
  this.update(homeAssistant.entity);
170539
- this.reactTo(homeAssistant.onChange, this.update);
170775
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170540
170776
  }
170541
170777
  update(entity) {
170542
170778
  if (!entity.state || !entity.state.attributes) {
@@ -170614,7 +170850,7 @@ var PirOccupancySensingServer = class extends PirOccupancySensingServerBase {
170614
170850
  await super.initialize();
170615
170851
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170616
170852
  this.update(homeAssistant.entity);
170617
- this.reactTo(homeAssistant.onChange, this.update);
170853
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170618
170854
  }
170619
170855
  update(entity) {
170620
170856
  if (!entity.state || !entity.state.attributes) {
@@ -170682,7 +170918,7 @@ var OccupancySensingServer2 = class extends OccupancySensingServerBase {
170682
170918
  await super.initialize();
170683
170919
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170684
170920
  this.update(homeAssistant.entity);
170685
- this.reactTo(homeAssistant.onChange, this.update);
170921
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170686
170922
  }
170687
170923
  update(entity) {
170688
170924
  if (!entity.state || !entity.state.attributes) {
@@ -170797,7 +171033,7 @@ var SmokeAlarmServerImpl = class extends SmokeAlarmServerWithFeature {
170797
171033
  await super.initialize();
170798
171034
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170799
171035
  this.update(homeAssistant.entity);
170800
- this.reactTo(homeAssistant.onChange, this.update);
171036
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170801
171037
  }
170802
171038
  update(entity) {
170803
171039
  const isOn = this.agent.get(HomeAssistantEntityBehavior).isAvailable && entity.state.state === "on";
@@ -170811,7 +171047,7 @@ var CoAlarmServerImpl = class extends CoAlarmServerWithFeature {
170811
171047
  await super.initialize();
170812
171048
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
170813
171049
  this.update(homeAssistant.entity);
170814
- this.reactTo(homeAssistant.onChange, this.update);
171050
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
170815
171051
  }
170816
171052
  update(entity) {
170817
171053
  const isOn = this.agent.get(HomeAssistantEntityBehavior).isAvailable && entity.state.state === "on";
@@ -171092,6 +171328,23 @@ var ClimateOnOffServer = OnOffServer2({
171092
171328
 
171093
171329
  // src/matter/endpoints/legacy/climate/behaviors/climate-thermostat-server.ts
171094
171330
  init_dist();
171331
+
171332
+ // src/utils/converters/snap-to-step.ts
171333
+ function snapToStep(target, current, step) {
171334
+ if (step == null || !Number.isFinite(step) || step <= 0) return target;
171335
+ const ratio = target / step;
171336
+ const lower = Math.floor(ratio) * step;
171337
+ const upper = Math.ceil(ratio) * step;
171338
+ if (lower === upper) return target;
171339
+ const distLower = target - lower;
171340
+ const distUpper = upper - target;
171341
+ if (distLower < distUpper) return lower;
171342
+ if (distUpper < distLower) return upper;
171343
+ if (current == null || !Number.isFinite(current)) return upper;
171344
+ return target > current ? upper : lower;
171345
+ }
171346
+
171347
+ // src/matter/endpoints/legacy/climate/behaviors/climate-thermostat-server.ts
171095
171348
  init_home_assistant_entity_behavior();
171096
171349
 
171097
171350
  // src/matter/behaviors/thermostat-server.ts
@@ -171287,7 +171540,7 @@ var ThermostatServerBase = class extends FullFeaturedBase {
171287
171540
  // Heat/Cool/Off but SKIPS Auto (see #handleSystemModeChange in Matter.js).
171288
171541
  // Without this, switching Heat→Auto leaves runningMode stale at Heat.
171289
171542
  // 889010b set for ALL modes (conflicted with reactor), 0678d35 set for NONE
171290
- // (stale in Auto). da04b2e's Auto-only approach is correct the issues
171543
+ // (stale in Auto). da04b2e's Auto-only approach is correct, the issues
171291
171544
  // reported after it were caused by localTemperature fallback, not runningMode.
171292
171545
  ...this.features.heating && this.features.cooling && systemMode === Thermostat3.SystemMode.Auto ? { thermostatRunningMode: runningMode } : {}
171293
171546
  });
@@ -171572,7 +171825,7 @@ var CoolingOnlyThermostatServerBase = class extends CoolingOnlyFeaturedBase {
171572
171825
  static State = class extends CoolingOnlyFeaturedBase.State {
171573
171826
  config;
171574
171827
  };
171575
- // Each variant MUST define its own initialize() see HeatingOnly comment above.
171828
+ // Each variant MUST define its own initialize(), see HeatingOnly comment above.
171576
171829
  async initialize() {
171577
171830
  thermostatPreInitialize(this);
171578
171831
  await super.initialize();
@@ -171594,7 +171847,7 @@ var HeatingAndCoolingThermostatServerBase = class extends HeatingAndCoolingFeatu
171594
171847
  static State = class extends HeatingAndCoolingFeaturedBase.State {
171595
171848
  config;
171596
171849
  };
171597
- // Each variant MUST define its own initialize() see HeatingOnly comment above.
171850
+ // Each variant MUST define its own initialize(), see HeatingOnly comment above.
171598
171851
  async initialize() {
171599
171852
  thermostatPreInitialize(this);
171600
171853
  await super.initialize();
@@ -171675,6 +171928,18 @@ var getTemp = (agent, entity, attributeName) => {
171675
171928
  return Temperature.withUnit(+temperature3, unit);
171676
171929
  }
171677
171930
  };
171931
+ var getStep = (entity) => {
171932
+ const raw = attributes4(entity).target_temp_step;
171933
+ if (raw == null) return void 0;
171934
+ const num = typeof raw === "string" ? parseFloat(raw) : raw;
171935
+ return Number.isFinite(num) && num > 0 ? num : void 0;
171936
+ };
171937
+ var getRefTemp = (entity, attr) => {
171938
+ const raw = attributes4(entity)[attr];
171939
+ if (raw == null) return void 0;
171940
+ const num = typeof raw === "string" ? parseFloat(raw) : raw;
171941
+ return Number.isFinite(num) ? num : void 0;
171942
+ };
171678
171943
  var systemModeToHvacMode = {
171679
171944
  [Thermostat3.SystemMode.Auto]: ClimateHvacMode.heat_cool,
171680
171945
  [Thermostat3.SystemMode.Precooling]: ClimateHvacMode.cool,
@@ -171712,6 +171977,7 @@ function isHeatCoolOnly(modes) {
171712
171977
  return modes.includes(ClimateHvacMode.heat_cool) && !modes.includes(ClimateHvacMode.heat) && !modes.includes(ClimateHvacMode.cool);
171713
171978
  }
171714
171979
  var lastHvacDirection = /* @__PURE__ */ new Map();
171980
+ var climateFreezeState = /* @__PURE__ */ new Map();
171715
171981
  function getHeatCoolOnlyDirection(entity, agent) {
171716
171982
  const action = attributes4(entity).hvac_action;
171717
171983
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
@@ -171726,6 +171992,95 @@ function getHeatCoolOnlyDirection(entity, agent) {
171726
171992
  }
171727
171993
  return lastHvacDirection.get(entityId) ?? "heating";
171728
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
+ }
171729
172084
  var config4 = {
171730
172085
  // Temperature range (target_temp_low/high) only works in heat_cool mode.
171731
172086
  // In heat or cool mode, HA expects a single "temperature" value.
@@ -171745,37 +172100,14 @@ var config4 = {
171745
172100
  getTargetHeatingTemperature: (entity, agent) => getTemp(agent, entity, "target_temp_low") ?? getTemp(agent, entity, "target_temperature") ?? getTemp(agent, entity, "temperature"),
171746
172101
  getTargetCoolingTemperature: (entity, agent) => getTemp(agent, entity, "target_temp_high") ?? getTemp(agent, entity, "target_temperature") ?? getTemp(agent, entity, "temperature"),
171747
172102
  getSystemMode: (entity, agent) => {
171748
- const hvacMode = entity.state;
171749
- const systemMode = hvacModeToSystemMode[hvacMode] ?? Thermostat3.SystemMode.Off;
171750
- if (systemMode === Thermostat3.SystemMode.Auto) {
171751
- const modes = attributes4(entity).hvac_modes ?? [];
171752
- if (isHeatCoolOnly(modes)) {
171753
- const direction = getHeatCoolOnlyDirection(entity, agent);
171754
- return direction === "cooling" ? Thermostat3.SystemMode.Cool : Thermostat3.SystemMode.Heat;
171755
- }
171756
- const hasCoolCapability = modes.includes(ClimateHvacMode.cool) || modes.includes(ClimateHvacMode.heat_cool);
171757
- const hasHeatCapability = modes.includes(ClimateHvacMode.heat) || modes.includes(ClimateHvacMode.heat_cool);
171758
- const hasMatterAuto = (modes.includes(ClimateHvacMode.heat_cool) || modes.includes(ClimateHvacMode.auto)) && hasCoolCapability && hasHeatCapability;
171759
- if (hasMatterAuto) {
171760
- return systemMode;
171761
- }
171762
- const hasCooling = modes.some((m) => m === ClimateHvacMode.cool);
171763
- const hasHeating = modes.some(
171764
- (m) => m === ClimateHvacMode.heat || m === ClimateHvacMode.auto
171765
- );
171766
- if (hasHeating && !hasCooling) {
171767
- return Thermostat3.SystemMode.Heat;
171768
- }
171769
- if (hasCooling && !hasHeating) {
171770
- return Thermostat3.SystemMode.Cool;
171771
- }
171772
- const action = attributes4(entity).hvac_action;
171773
- if (action === ClimateHvacAction.cooling) {
171774
- return Thermostat3.SystemMode.Cool;
171775
- }
171776
- return Thermostat3.SystemMode.Heat;
171777
- }
171778
- 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
+ );
171779
172111
  },
171780
172112
  getRunningMode: (entity) => {
171781
172113
  const action = attributes4(entity).hvac_action;
@@ -171835,19 +172167,37 @@ var config4 = {
171835
172167
  data: { hvac_mode: targetMode }
171836
172168
  };
171837
172169
  },
171838
- setTargetTemperature: (value, agent) => ({
171839
- action: "climate.set_temperature",
171840
- data: {
171841
- temperature: value.toUnit(getUnit(agent))
171842
- }
171843
- }),
171844
- setTargetTemperatureRange: ({ low, high }, agent) => ({
171845
- action: "climate.set_temperature",
171846
- data: {
171847
- target_temp_low: low.toUnit(getUnit(agent)),
171848
- target_temp_high: high.toUnit(getUnit(agent))
171849
- }
171850
- })
172170
+ setTargetTemperature: (value, agent) => {
172171
+ const homeAssistant = agent.get(HomeAssistantEntityBehavior);
172172
+ const entity = homeAssistant.entity.state;
172173
+ const unit = getUnit(agent);
172174
+ const target = value.toUnit(unit);
172175
+ const step = getStep(entity);
172176
+ const current = getRefTemp(entity, "temperature") ?? getRefTemp(entity, "target_temperature");
172177
+ return {
172178
+ action: "climate.set_temperature",
172179
+ data: {
172180
+ temperature: snapToStep(target, current, step)
172181
+ }
172182
+ };
172183
+ },
172184
+ setTargetTemperatureRange: ({ low, high }, agent) => {
172185
+ const homeAssistant = agent.get(HomeAssistantEntityBehavior);
172186
+ const entity = homeAssistant.entity.state;
172187
+ const unit = getUnit(agent);
172188
+ const step = getStep(entity);
172189
+ const lowTarget = low.toUnit(unit);
172190
+ const highTarget = high.toUnit(unit);
172191
+ const lowCurrent = getRefTemp(entity, "target_temp_low") ?? getRefTemp(entity, "temperature");
172192
+ const highCurrent = getRefTemp(entity, "target_temp_high") ?? getRefTemp(entity, "temperature");
172193
+ return {
172194
+ action: "climate.set_temperature",
172195
+ data: {
172196
+ target_temp_low: snapToStep(lowTarget, lowCurrent, step),
172197
+ target_temp_high: snapToStep(highTarget, highCurrent, step)
172198
+ }
172199
+ };
172200
+ }
171851
172201
  };
171852
172202
  function ClimateThermostatServer(initialState = {}, features2) {
171853
172203
  return ThermostatServer2(config4, initialState, features2);
@@ -171941,7 +172291,7 @@ function ClimateDevice(homeAssistantEntity) {
171941
172291
  minCoolSetpointLimit: toMatterTemp(attributes7.min_temp) ?? 0,
171942
172292
  maxCoolSetpointLimit: toMatterTemp(attributes7.max_temp) ?? 5e3
171943
172293
  };
171944
- const autoMode = supportsHeating && supportsCooling && (attributes7.hvac_modes.includes(ClimateHvacMode.heat_cool) || attributes7.hvac_modes.includes(ClimateHvacMode.auto)) && (attributes7.hvac_modes.includes(ClimateHvacMode.heat) || attributes7.hvac_modes.includes(ClimateHvacMode.cool));
172294
+ const autoMode = supportsHeating && supportsCooling && attributes7.hvac_modes.includes(ClimateHvacMode.heat_cool) && (attributes7.hvac_modes.includes(ClimateHvacMode.heat) || attributes7.hvac_modes.includes(ClimateHvacMode.cool));
171945
172295
  return ClimateDeviceType(
171946
172296
  supportsOnOff,
171947
172297
  supportsHumidity,
@@ -172020,6 +172370,18 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172020
172370
  static DEBOUNCE_INITIAL_MS = 400;
172021
172371
  static DEBOUNCE_SUBSEQUENT_MS = 150;
172022
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
+ }
172023
172385
  async [Symbol.asyncDispose]() {
172024
172386
  if (this.liftDebounceTimer) {
172025
172387
  clearTimeout(this.liftDebounceTimer);
@@ -172034,23 +172396,9 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172034
172396
  await super[Symbol.asyncDispose]();
172035
172397
  }
172036
172398
  async initialize() {
172037
- if (this.features.lift) {
172038
- if (this.state.installedOpenLimitLift == null) {
172039
- this.state.installedOpenLimitLift = 0;
172040
- }
172041
- if (this.state.installedClosedLimitLift == null) {
172042
- this.state.installedClosedLimitLift = 1e4;
172043
- }
172044
- }
172045
- if (this.features.tilt) {
172046
- if (this.state.installedOpenLimitTilt == null) {
172047
- this.state.installedOpenLimitTilt = 0;
172048
- }
172049
- if (this.state.installedClosedLimitTilt == null) {
172050
- this.state.installedClosedLimitTilt = 1e4;
172051
- }
172052
- }
172399
+ this.internal.disableOperationalModeHandling = true;
172053
172400
  if (this.features.positionAwareLift) {
172401
+ this.state.currentPositionLiftPercentage = void 0;
172054
172402
  if (this.state.currentPositionLiftPercent100ths === void 0) {
172055
172403
  this.state.currentPositionLiftPercent100ths = null;
172056
172404
  }
@@ -172059,6 +172407,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172059
172407
  }
172060
172408
  }
172061
172409
  if (this.features.positionAwareTilt) {
172410
+ this.state.currentPositionTiltPercentage = void 0;
172062
172411
  if (this.state.currentPositionTiltPercent100ths === void 0) {
172063
172412
  this.state.currentPositionTiltPercent100ths = null;
172064
172413
  }
@@ -172066,6 +172415,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172066
172415
  this.state.targetPositionTiltPercent100ths = null;
172067
172416
  }
172068
172417
  }
172418
+ this.state.type = this.features.lift && this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindLift : this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindTiltOnly : WindowCovering3.WindowCoveringType.Rollershade;
172069
172419
  await super.initialize();
172070
172420
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
172071
172421
  this.update(homeAssistant.entity);
@@ -172114,41 +172464,39 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172114
172464
  );
172115
172465
  const overrideType = config10.getCoverType?.(state, this.agent);
172116
172466
  const overrideEndProduct = config10.getEndProductType?.(state, this.agent);
172467
+ const previousStatus = this.state.operationalStatus?.global;
172468
+ const startedMoving = !isStopped && previousStatus === MovementStatus.Stopped;
172117
172469
  const appliedPatch = applyPatchState(
172118
172470
  this.state,
172119
172471
  {
172120
172472
  type: overrideType ?? (this.features.lift && this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindLift : this.features.tilt ? WindowCovering3.WindowCoveringType.TiltBlindTiltOnly : WindowCovering3.WindowCoveringType.Rollershade),
172121
172473
  endProductType: overrideEndProduct ?? (this.features.lift && this.features.tilt ? WindowCovering3.EndProductType.SheerShade : this.features.tilt ? WindowCovering3.EndProductType.TiltOnlyInteriorBlind : WindowCovering3.EndProductType.RollerShade),
172122
- operationalStatus: {
172123
- global: movementStatus,
172124
- ...this.features.lift ? { lift: movementStatus } : {},
172125
- ...this.features.tilt ? { tilt: movementStatus } : {}
172126
- },
172127
- ...this.features.absolutePosition && this.features.lift ? {
172128
- installedOpenLimitLift: 0,
172129
- installedClosedLimitLift: 1e4,
172130
- currentPositionLift: currentLift100ths
172131
- } : {},
172132
- ...this.features.absolutePosition && this.features.tilt ? {
172133
- installedOpenLimitTilt: 0,
172134
- installedClosedLimitTilt: 1e4,
172135
- currentPositionTilt: currentTilt100ths
172136
- } : {},
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).
172137
172478
  ...this.features.positionAwareLift ? {
172138
- currentPositionLiftPercentage: currentLift,
172139
- currentPositionLiftPercent100ths: currentLift100ths,
172140
172479
  targetPositionLiftPercent100ths: inferTarget(
172141
172480
  currentLift100ths,
172142
172481
  this.state.targetPositionLiftPercent100ths
172143
172482
  )
172144
172483
  } : {},
172145
172484
  ...this.features.positionAwareTilt ? {
172146
- currentPositionTiltPercentage: currentTilt,
172147
- currentPositionTiltPercent100ths: currentTilt100ths,
172148
172485
  targetPositionTiltPercent100ths: inferTarget(
172149
172486
  currentTilt100ths,
172150
172487
  this.state.targetPositionTiltPercent100ths
172151
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
172152
172500
  } : {}
172153
172501
  }
172154
172502
  );
@@ -172165,7 +172513,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172165
172513
  const currentLift = this.state.currentPositionLiftPercent100ths ?? 0;
172166
172514
  const currentTilt = this.state.currentPositionTiltPercent100ths ?? 0;
172167
172515
  logger179.info(
172168
- `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}`
172169
172517
  );
172170
172518
  if (type === MovementType.Lift) {
172171
172519
  this.lastLiftMovementMs = Date.now();
@@ -172174,7 +172522,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172174
172522
  this.handleLiftOpen();
172175
172523
  } else if (targetPercent100ths === 1e4) {
172176
172524
  this.handleLiftClose();
172177
- } else if (targetPercent100ths != null && this.features.absolutePosition) {
172525
+ } else if (targetPercent100ths != null && this.features.positionAwareLift) {
172178
172526
  this.handleGoToLiftPosition(targetPercent100ths);
172179
172527
  } else if (direction === MovementDirection.Open) {
172180
172528
  this.handleLiftOpen();
@@ -172184,7 +172532,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172184
172532
  } else if (type === MovementType.Tilt) {
172185
172533
  if (targetPercent100ths == null && this.lastLiftMovementDirection === direction && Date.now() - this.lastLiftMovementMs < 50) {
172186
172534
  logger179.info(
172187
- `Skipping tilt ${MovementDirection[direction]} \u2014 lift already moving in same direction`
172535
+ `Skipping tilt ${MovementDirection[direction]}, lift already moving in same direction`
172188
172536
  );
172189
172537
  return;
172190
172538
  }
@@ -172192,7 +172540,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172192
172540
  this.handleTiltOpen();
172193
172541
  } else if (targetPercent100ths === 1e4) {
172194
172542
  this.handleTiltClose();
172195
- } else if (targetPercent100ths != null && this.features.absolutePosition) {
172543
+ } else if (targetPercent100ths != null && this.features.positionAwareTilt) {
172196
172544
  this.handleGoToTiltPosition(targetPercent100ths);
172197
172545
  } else if (direction === MovementDirection.Open) {
172198
172546
  this.handleTiltOpen();
@@ -172234,9 +172582,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172234
172582
  const timeSinceLastCommand = now - this.lastLiftCommandTime;
172235
172583
  this.lastLiftCommandTime = now;
172236
172584
  const isFirstInSequence = timeSinceLastCommand > _WindowCoveringServerBase.COMMAND_SEQUENCE_THRESHOLD_MS;
172237
- 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;
172238
172587
  logger179.debug(
172239
- `Lift command: target=${targetPosition}%, debounce=${debounceMs}ms (${isFirstInSequence ? "initial" : "subsequent"})`
172588
+ `Lift command: target=${targetPosition}%, debounce=${debounceMs}ms (${overrideMs != null ? "override" : isFirstInSequence ? "initial" : "subsequent"})`
172240
172589
  );
172241
172590
  if (this.liftDebounceTimer) {
172242
172591
  clearTimeout(this.liftDebounceTimer);
@@ -172283,9 +172632,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
172283
172632
  const timeSinceLastCommand = now - this.lastTiltCommandTime;
172284
172633
  this.lastTiltCommandTime = now;
172285
172634
  const isFirstInSequence = timeSinceLastCommand > _WindowCoveringServerBase.COMMAND_SEQUENCE_THRESHOLD_MS;
172286
- 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;
172287
172637
  logger179.debug(
172288
- `Tilt command: target=${targetPosition}%, debounce=${debounceMs}ms (${isFirstInSequence ? "initial" : "subsequent"})`
172638
+ `Tilt command: target=${targetPosition}%, debounce=${debounceMs}ms (${overrideMs != null ? "override" : isFirstInSequence ? "initial" : "subsequent"})`
172289
172639
  );
172290
172640
  if (this.tiltDebounceTimer) {
172291
172641
  clearTimeout(this.tiltDebounceTimer);
@@ -172358,6 +172708,14 @@ var DEVICE_CLASS_TO_MATTER_TYPE = {
172358
172708
  shade: {
172359
172709
  type: WindowCovering3.WindowCoveringType.Rollershade,
172360
172710
  endProductType: WindowCovering3.EndProductType.RollerShade
172711
+ },
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).
172716
+ window: {
172717
+ type: WindowCovering3.WindowCoveringType.Rollershade,
172718
+ endProductType: WindowCovering3.EndProductType.RollerShade
172361
172719
  }
172362
172720
  };
172363
172721
  var deviceClassMapping = (entity) => {
@@ -172366,12 +172724,21 @@ var deviceClassMapping = (entity) => {
172366
172724
  const mapping = DEVICE_CLASS_TO_MATTER_TYPE[raw.toLowerCase()];
172367
172725
  if (!mapping) return void 0;
172368
172726
  const supportedFeatures = attributes5(entity).supported_features ?? 0;
172727
+ const hasLift = (supportedFeatures & CoverSupportedFeatures.support_open) !== 0;
172369
172728
  const hasTilt = (supportedFeatures & CoverSupportedFeatures.support_open_tilt) !== 0;
172370
- if (mapping.type === WindowCovering3.WindowCoveringType.TiltBlindTiltOnly && !hasTilt) {
172371
- return {
172372
- type: WindowCovering3.WindowCoveringType.Rollershade,
172373
- endProductType: mapping.endProductType
172374
- };
172729
+ if (mapping.type === WindowCovering3.WindowCoveringType.TiltBlindTiltOnly) {
172730
+ if (!hasTilt) {
172731
+ return {
172732
+ type: WindowCovering3.WindowCoveringType.Rollershade,
172733
+ endProductType: mapping.endProductType
172734
+ };
172735
+ }
172736
+ if (hasLift) {
172737
+ return {
172738
+ type: WindowCovering3.WindowCoveringType.TiltBlindLift,
172739
+ endProductType: mapping.endProductType
172740
+ };
172741
+ }
172375
172742
  }
172376
172743
  return mapping;
172377
172744
  };
@@ -172501,24 +172868,17 @@ var CoverWindowCoveringServer = WindowCoveringServer2(config5);
172501
172868
 
172502
172869
  // src/matter/endpoints/legacy/cover/index.ts
172503
172870
  var logger181 = Logger.get("CoverDevice");
172504
- var DISCRETE_COVER_CLASSES = /* @__PURE__ */ new Set(["garage", "gate"]);
172505
- var CoverDeviceType = (supportedFeatures, hasBattery, entityId, isDiscrete) => {
172871
+ var CoverDeviceType = (supportedFeatures, hasBattery, entityId) => {
172506
172872
  const features2 = /* @__PURE__ */ new Set();
172507
172873
  if (testBit(supportedFeatures, CoverSupportedFeatures.support_open)) {
172508
172874
  features2.add("Lift");
172509
172875
  features2.add("PositionAwareLift");
172510
- if (!isDiscrete) {
172511
- features2.add("AbsolutePosition");
172512
- }
172513
172876
  } else {
172514
172877
  logger181.warn(
172515
172878
  `[${entityId}] Cover has no support_open feature (supported_features=${supportedFeatures}), adding Lift anyway`
172516
172879
  );
172517
172880
  features2.add("Lift");
172518
172881
  features2.add("PositionAwareLift");
172519
- if (!isDiscrete) {
172520
- features2.add("AbsolutePosition");
172521
- }
172522
172882
  }
172523
172883
  if (testBit(supportedFeatures, CoverSupportedFeatures.support_open_tilt)) {
172524
172884
  features2.add("Tilt");
@@ -172527,7 +172887,6 @@ var CoverDeviceType = (supportedFeatures, hasBattery, entityId, isDiscrete) => {
172527
172887
  CoverSupportedFeatures.support_set_tilt_position
172528
172888
  )) {
172529
172889
  features2.add("PositionAwareTilt");
172530
- features2.add("AbsolutePosition");
172531
172890
  }
172532
172891
  }
172533
172892
  logger181.info(
@@ -172562,18 +172921,10 @@ function CoverDevice(homeAssistantEntity) {
172562
172921
  `[${entityId}] Creating cover without battery (batteryAttr=${hasBatteryAttr}, batteryEntity=${homeAssistantEntity.mapping?.batteryEntity ?? "none"})`
172563
172922
  );
172564
172923
  }
172565
- const deviceClass = attributes7.device_class;
172566
- const isDiscrete = typeof deviceClass === "string" && DISCRETE_COVER_CLASSES.has(deviceClass.toLowerCase());
172567
- if (isDiscrete) {
172568
- logger181.info(
172569
- `[${entityId}] Garage/gate cover (device_class=${deviceClass}): using discrete Open/Close mode`
172570
- );
172571
- }
172572
172924
  return CoverDeviceType(
172573
172925
  attributes7.supported_features ?? 0,
172574
172926
  hasBattery,
172575
- entityId,
172576
- isDiscrete
172927
+ entityId
172577
172928
  ).set({
172578
172929
  homeAssistantEntity
172579
172930
  });
@@ -173640,6 +173991,9 @@ function LightDevice(homeAssistantEntity) {
173640
173991
  const hasPowerEntity = !!homeAssistantEntity.mapping?.powerEntity;
173641
173992
  const hasEnergyEntity = !!homeAssistantEntity.mapping?.energyEntity;
173642
173993
  let device = deviceType;
173994
+ if (hasPowerEntity || hasEnergyEntity) {
173995
+ device = device.with(HaPowerTopologyServer);
173996
+ }
173643
173997
  if (hasPowerEntity) {
173644
173998
  device = device.with(HaElectricalPowerMeasurementServer);
173645
173999
  }
@@ -174603,7 +174957,7 @@ var SpeakerLevelControlServerBase = class extends FeaturedBase8 {
174603
174957
  *
174604
174958
  * The base class registers a reactor on onOff$Changed that sets
174605
174959
  * currentLevel = onLevel. This is designed for lights (restore brightness
174606
- * on power-on) but is wrong for speakers it overwrites the correct
174960
+ * on power-on) but is wrong for speakers, it overwrites the correct
174607
174961
  * volume (e.g. 191 for 75%) with a stale onLevel value, causing Google
174608
174962
  * Home to display the wrong percentage (Issue #79).
174609
174963
  */
@@ -174950,7 +175304,7 @@ var AirQualitySensorServerImpl = class extends AirQualityServerWithFeatures {
174950
175304
  await super.initialize();
174951
175305
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
174952
175306
  this.update(homeAssistant.entity);
174953
- this.reactTo(homeAssistant.onChange, this.update);
175307
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
174954
175308
  }
174955
175309
  update(entity) {
174956
175310
  if (!entity.state || !entity.state.attributes) {
@@ -175061,7 +175415,7 @@ var CarbonMonoxideConcentrationMeasurementServer2 = class extends CarbonMonoxide
175061
175415
  await super.initialize();
175062
175416
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175063
175417
  this.update(homeAssistant.entity);
175064
- this.reactTo(homeAssistant.onChange, this.update);
175418
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175065
175419
  }
175066
175420
  update(entity) {
175067
175421
  if (!entity.state || !entity.state.attributes) {
@@ -175092,7 +175446,7 @@ var CoAirQualityServer = class extends CoAirQualityServerBase {
175092
175446
  await super.initialize();
175093
175447
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175094
175448
  this.update(homeAssistant.entity);
175095
- this.reactTo(homeAssistant.onChange, this.update);
175449
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175096
175450
  }
175097
175451
  update(entity) {
175098
175452
  const state = entity.state.state;
@@ -175132,7 +175486,7 @@ var StandalonePowerServer = class extends ElectricalPowerMeasurementServer {
175132
175486
  await super.initialize();
175133
175487
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175134
175488
  this.update(homeAssistant.entity);
175135
- this.reactTo(homeAssistant.onChange, this.update);
175489
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175136
175490
  }
175137
175491
  update(entity) {
175138
175492
  if (!entity.state || !entity.state.attributes) {
@@ -175165,6 +175519,10 @@ var StandalonePowerServer = class extends ElectricalPowerMeasurementServer {
175165
175519
  var PowerServer = StandalonePowerServer.set({
175166
175520
  powerMode: ElectricalPowerMeasurement3.PowerMode.Ac,
175167
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,
175168
175526
  accuracy: [
175169
175527
  {
175170
175528
  measurementType: ElectricalPowerMeasurement3.MeasurementType.ActivePower,
@@ -175190,7 +175548,7 @@ var StandaloneEnergyServer = class extends EnergyFeaturedBase {
175190
175548
  await super.initialize();
175191
175549
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175192
175550
  this.update(homeAssistant.entity);
175193
- this.reactTo(homeAssistant.onChange, this.update);
175551
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175194
175552
  }
175195
175553
  update(entity) {
175196
175554
  if (!entity.state || !entity.state.attributes) {
@@ -175217,6 +175575,9 @@ var StandaloneEnergyServer = class extends EnergyFeaturedBase {
175217
175575
  StandaloneEnergyServer2.State = State;
175218
175576
  })(StandaloneEnergyServer || (StandaloneEnergyServer = {}));
175219
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 },
175220
175581
  accuracy: {
175221
175582
  measurementType: ElectricalPowerMeasurement3.MeasurementType.ElectricalEnergy,
175222
175583
  measured: true,
@@ -175235,6 +175596,7 @@ var ElectricalSensorType = SolarPowerDevice.with(
175235
175596
  BasicInformationServer2,
175236
175597
  IdentifyServer2,
175237
175598
  HomeAssistantEntityBehavior,
175599
+ HaPowerTopologyServer,
175238
175600
  PowerServer,
175239
175601
  EnergyServer
175240
175602
  );
@@ -175246,7 +175608,7 @@ var FlowMeasurementServerBase = class extends FlowMeasurementServer {
175246
175608
  await super.initialize();
175247
175609
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175248
175610
  this.update(homeAssistant.entity);
175249
- this.reactTo(homeAssistant.onChange, this.update);
175611
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175250
175612
  }
175251
175613
  update(entity) {
175252
175614
  if (!entity.state || !entity.state.attributes) {
@@ -175338,7 +175700,7 @@ var FormaldehydeConcentrationMeasurementServer2 = class extends FormaldehydeConc
175338
175700
  await super.initialize();
175339
175701
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175340
175702
  this.update(homeAssistant.entity);
175341
- this.reactTo(homeAssistant.onChange, this.update);
175703
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175342
175704
  }
175343
175705
  update(entity) {
175344
175706
  if (!entity.state || !entity.state.attributes) {
@@ -175369,7 +175731,7 @@ var FormaldehydeAirQualityServer = class extends FormaldehydeAirQualityServerBas
175369
175731
  await super.initialize();
175370
175732
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175371
175733
  this.update(homeAssistant.entity);
175372
- this.reactTo(homeAssistant.onChange, this.update);
175734
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175373
175735
  }
175374
175736
  update(entity) {
175375
175737
  const state = entity.state.state;
@@ -175428,7 +175790,7 @@ var IlluminanceMeasurementServerBase = class extends IlluminanceMeasurementServe
175428
175790
  await super.initialize();
175429
175791
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175430
175792
  this.update(homeAssistant.entity);
175431
- this.reactTo(homeAssistant.onChange, this.update);
175793
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175432
175794
  }
175433
175795
  update(entity) {
175434
175796
  if (!entity.state || !entity.state.attributes) {
@@ -175512,7 +175874,7 @@ var NitrogenDioxideConcentrationMeasurementServer2 = class extends NitrogenDioxi
175512
175874
  await super.initialize();
175513
175875
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175514
175876
  this.update(homeAssistant.entity);
175515
- this.reactTo(homeAssistant.onChange, this.update);
175877
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175516
175878
  }
175517
175879
  update(entity) {
175518
175880
  if (!entity.state || !entity.state.attributes) {
@@ -175542,7 +175904,7 @@ var No2AirQualityServer = class extends No2AirQualityServerBase {
175542
175904
  await super.initialize();
175543
175905
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175544
175906
  this.update(homeAssistant.entity);
175545
- this.reactTo(homeAssistant.onChange, this.update);
175907
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175546
175908
  }
175547
175909
  update(entity) {
175548
175910
  const state = entity.state.state;
@@ -175605,7 +175967,7 @@ var OzoneConcentrationMeasurementServer2 = class extends OzoneConcentrationMeasu
175605
175967
  await super.initialize();
175606
175968
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175607
175969
  this.update(homeAssistant.entity);
175608
- this.reactTo(homeAssistant.onChange, this.update);
175970
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175609
175971
  }
175610
175972
  update(entity) {
175611
175973
  if (!entity.state || !entity.state.attributes) {
@@ -175635,7 +175997,7 @@ var OzoneAirQualityServer = class extends OzoneAirQualityServerBase {
175635
175997
  await super.initialize();
175636
175998
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175637
175999
  this.update(homeAssistant.entity);
175638
- this.reactTo(homeAssistant.onChange, this.update);
176000
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175639
176001
  }
175640
176002
  update(entity) {
175641
176003
  const state = entity.state.state;
@@ -175698,7 +176060,7 @@ var Pm1ConcentrationMeasurementServer2 = class extends Pm1ConcentrationMeasureme
175698
176060
  await super.initialize();
175699
176061
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175700
176062
  this.update(homeAssistant.entity);
175701
- this.reactTo(homeAssistant.onChange, this.update);
176063
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175702
176064
  }
175703
176065
  update(entity) {
175704
176066
  if (!entity.state || !entity.state.attributes) {
@@ -175728,7 +176090,7 @@ var Pm1AirQualityServer = class extends Pm1AirQualityServerBase {
175728
176090
  await super.initialize();
175729
176091
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175730
176092
  this.update(homeAssistant.entity);
175731
- this.reactTo(homeAssistant.onChange, this.update);
176093
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175732
176094
  }
175733
176095
  update(entity) {
175734
176096
  const state = entity.state.state;
@@ -175811,7 +176173,7 @@ var RadonConcentrationMeasurementServer2 = class extends RadonConcentrationMeasu
175811
176173
  await super.initialize();
175812
176174
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175813
176175
  this.update(homeAssistant.entity);
175814
- this.reactTo(homeAssistant.onChange, this.update);
176176
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175815
176177
  }
175816
176178
  update(entity) {
175817
176179
  if (!entity.state || !entity.state.attributes) {
@@ -175841,7 +176203,7 @@ var RadonAirQualityServer = class extends RadonAirQualityServerBase {
175841
176203
  await super.initialize();
175842
176204
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175843
176205
  this.update(homeAssistant.entity);
175844
- this.reactTo(homeAssistant.onChange, this.update);
176206
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175845
176207
  }
175846
176208
  update(entity) {
175847
176209
  const state = entity.state.state;
@@ -175959,7 +176321,7 @@ var TvocConcentrationMeasurementServer = class extends TvocConcentrationMeasurem
175959
176321
  await super.initialize();
175960
176322
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
175961
176323
  this.update(homeAssistant.entity);
175962
- this.reactTo(homeAssistant.onChange, this.update);
176324
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
175963
176325
  }
175964
176326
  update(entity) {
175965
176327
  if (!entity.state || !entity.state.attributes) {
@@ -176009,7 +176371,7 @@ var TvocAirQualityServer = class extends TvocAirQualityServerBase {
176009
176371
  await super.initialize();
176010
176372
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176011
176373
  this.update(homeAssistant.entity);
176012
- this.reactTo(homeAssistant.onChange, this.update);
176374
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176013
176375
  }
176014
176376
  update(entity) {
176015
176377
  if (!entity.state || !entity.state.attributes) {
@@ -176076,7 +176438,7 @@ var CarbonDioxideConcentrationMeasurementServer2 = class extends CarbonDioxideCo
176076
176438
  await super.initialize();
176077
176439
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176078
176440
  this.update(homeAssistant.entity);
176079
- this.reactTo(homeAssistant.onChange, this.update);
176441
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176080
176442
  }
176081
176443
  update(entity) {
176082
176444
  if (!entity.state || !entity.state.attributes) {
@@ -176109,7 +176471,7 @@ var Co2AirQualityServer = class extends Co2AirQualityServerBase {
176109
176471
  await super.initialize();
176110
176472
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176111
176473
  this.update(homeAssistant.entity);
176112
- this.reactTo(homeAssistant.onChange, this.update);
176474
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176113
176475
  }
176114
176476
  update(entity) {
176115
176477
  const state = entity.state.state;
@@ -176172,7 +176534,7 @@ var Pm10ConcentrationMeasurementServer2 = class extends Pm10ConcentrationMeasure
176172
176534
  await super.initialize();
176173
176535
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176174
176536
  this.update(homeAssistant.entity);
176175
- this.reactTo(homeAssistant.onChange, this.update);
176537
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176176
176538
  }
176177
176539
  update(entity) {
176178
176540
  if (!entity.state || !entity.state.attributes) {
@@ -176204,7 +176566,7 @@ var Pm10AirQualityServer = class extends Pm10AirQualityServerBase {
176204
176566
  await super.initialize();
176205
176567
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176206
176568
  this.update(homeAssistant.entity);
176207
- this.reactTo(homeAssistant.onChange, this.update);
176569
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176208
176570
  }
176209
176571
  update(entity) {
176210
176572
  const state = entity.state.state;
@@ -176276,7 +176638,7 @@ var Pm25ConcentrationMeasurementServer2 = class extends Pm25ConcentrationMeasure
176276
176638
  );
176277
176639
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176278
176640
  this.update(homeAssistant.entity);
176279
- this.reactTo(homeAssistant.onChange, this.update);
176641
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176280
176642
  }
176281
176643
  update(entity) {
176282
176644
  if (!entity.state || !entity.state.attributes) {
@@ -176311,7 +176673,7 @@ var Pm25AirQualityServer = class extends Pm25AirQualityServerBase {
176311
176673
  logger189.debug("Pm25AirQualityServer: after super.initialize()");
176312
176674
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
176313
176675
  this.update(homeAssistant.entity);
176314
- this.reactTo(homeAssistant.onChange, this.update);
176676
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
176315
176677
  }
176316
176678
  update(entity) {
176317
176679
  const state = entity.state.state;
@@ -176608,7 +176970,7 @@ function SensorDevice(homeAssistantEntity) {
176608
176970
  if (deviceClass) {
176609
176971
  diagnosticEventBus.emit(
176610
176972
  "entity_warning",
176611
- `Sensor "${homeAssistantEntity.entity.entity_id}" has unsupported device_class "${deviceClass}" \u2014 skipped`,
176973
+ `Sensor "${homeAssistantEntity.entity.entity_id}" has unsupported device_class "${deviceClass}", skipped`,
176612
176974
  {
176613
176975
  entityId: homeAssistantEntity.entity.entity_id,
176614
176976
  details: { device_class: deviceClass }
@@ -176677,6 +177039,9 @@ function SwitchDevice(homeAssistantEntity) {
176677
177039
  const hasPowerEntity = !!homeAssistantEntity.mapping?.powerEntity;
176678
177040
  const hasEnergyEntity = !!homeAssistantEntity.mapping?.energyEntity;
176679
177041
  let device = hasBatteryAttr || hasBatteryEntity ? SwitchWithBatteryEndpointType : SwitchEndpointType;
177042
+ if (hasPowerEntity || hasEnergyEntity) {
177043
+ device = device.with(HaPowerTopologyServer);
177044
+ }
176680
177045
  if (hasPowerEntity) {
176681
177046
  device = device.with(HaElectricalPowerMeasurementServer);
176682
177047
  }
@@ -176695,6 +177060,7 @@ init_dist();
176695
177060
  init_esm();
176696
177061
  init_home_assistant_entity_behavior();
176697
177062
  var logger190 = Logger.get("VacuumIdentifyServer");
177063
+ var IDENTIFY_BUTTON_SUFFIXES = ["_identify", "_locate", "_find_me"];
176698
177064
  var VacuumIdentifyServer = class extends IdentifyServer2 {
176699
177065
  triggerEffect(effect) {
176700
177066
  this.#locate("triggerEffect");
@@ -176709,16 +177075,40 @@ var VacuumIdentifyServer = class extends IdentifyServer2 {
176709
177075
  #locate(source) {
176710
177076
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
176711
177077
  const features2 = homeAssistant.entity.state.attributes.supported_features ?? 0;
176712
- const hasLocate = testBit(features2, VacuumDeviceFeature.LOCATE);
176713
- if (!hasLocate) {
176714
- logger190.warn(
176715
- `${source} for ${homeAssistant.entityId} \u2014 LOCATE not in supported_features (${features2}), calling vacuum.locate anyway`
176716
- );
176717
- } else {
177078
+ if (testBit(features2, VacuumDeviceFeature.LOCATE)) {
176718
177079
  logger190.info(`${source} \u2192 vacuum.locate for ${homeAssistant.entityId}`);
177080
+ homeAssistant.callAction({ action: "vacuum.locate" });
177081
+ return;
177082
+ }
177083
+ const sibling = this.#findIdentifyButton(homeAssistant);
177084
+ if (sibling) {
177085
+ logger190.info(
177086
+ `${source} \u2192 button.press ${sibling} for ${homeAssistant.entityId}`
177087
+ );
177088
+ homeAssistant.callAction({ action: "button.press", target: sibling });
177089
+ return;
176719
177090
  }
177091
+ logger190.warn(
177092
+ `${source} for ${homeAssistant.entityId}, LOCATE not in supported_features (${features2}), trying vacuum.locate anyway`
177093
+ );
176720
177094
  homeAssistant.callAction({ action: "vacuum.locate" });
176721
177095
  }
177096
+ #findIdentifyButton(homeAssistant) {
177097
+ const deviceId = homeAssistant.entity.registry?.device_id;
177098
+ if (!deviceId) return void 0;
177099
+ const registry2 = this.env.get(HomeAssistantRegistry);
177100
+ for (const entity of Object.values(registry2.entities)) {
177101
+ if (entity.device_id !== deviceId) continue;
177102
+ if (!entity.entity_id.startsWith("button.")) continue;
177103
+ const uniqueId = entity.unique_id ?? "";
177104
+ if (IDENTIFY_BUTTON_SUFFIXES.some(
177105
+ (s) => entity.entity_id.endsWith(s) || uniqueId.endsWith(s)
177106
+ )) {
177107
+ return entity.entity_id;
177108
+ }
177109
+ }
177110
+ return void 0;
177111
+ }
176722
177112
  };
176723
177113
 
176724
177114
  // src/matter/behaviors/rvc-run-mode-server.ts
@@ -176747,7 +177137,9 @@ function getSession(endpoint) {
176747
177137
  completedAreas: /* @__PURE__ */ new Set(),
176748
177138
  lastCurrentArea: null,
176749
177139
  activeAreas: [],
176750
- loggedShortCircuits: /* @__PURE__ */ new Set()
177140
+ loggedShortCircuits: /* @__PURE__ */ new Set(),
177141
+ observedCleaning: false,
177142
+ pendingDispatches: []
176751
177143
  };
176752
177144
  cleaningSessions.set(endpoint, session);
176753
177145
  }
@@ -176778,17 +177170,45 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176778
177170
  },
176779
177171
  { force: true }
176780
177172
  );
177173
+ if (newMode === 1 /* Cleaning */) {
177174
+ s.observedCleaning = true;
177175
+ }
176781
177176
  if (previousMode !== newMode) {
176782
177177
  if (newMode === 0 /* Idle */) {
176783
177178
  if (s.lastCurrentArea !== null) {
176784
177179
  s.completedAreas.add(s.lastCurrentArea);
176785
177180
  s.lastCurrentArea = null;
177181
+ }
177182
+ if (s.pendingDispatches.length > 0 && s.observedCleaning) {
177183
+ try {
177184
+ const serviceArea = this.agent.get(ServiceAreaBehavior);
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) {
176786
177201
  try {
176787
177202
  const serviceArea = this.agent.get(ServiceAreaBehavior);
176788
- serviceArea.state.currentArea = null;
177203
+ const last = serviceArea.state.currentArea;
177204
+ if (typeof last === "number") {
177205
+ s.completedAreas.add(last);
177206
+ serviceArea.state.currentArea = null;
177207
+ }
176789
177208
  this.updateProgressFromTracking(serviceArea);
176790
177209
  } catch {
176791
177210
  }
177211
+ s.observedCleaning = false;
176792
177212
  }
176793
177213
  s.loggedShortCircuits.clear();
176794
177214
  } else if (newMode === 1 /* Cleaning */) {
@@ -176802,6 +177222,15 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176802
177222
  }
176803
177223
  }
176804
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
+ }
176805
177234
  if (newMode === 1 /* Cleaning */) {
176806
177235
  this.updateCurrentRoomFromSensor();
176807
177236
  }
@@ -176829,7 +177258,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176829
177258
  if (!currentRoomEntityId) {
176830
177259
  this.logShortCircuitOnce(
176831
177260
  "no-mapping",
176832
- "currentRoom sensor: no currentRoomEntity in mapping \u2014 auto-detect did not run or sensor not on same HA device"
177261
+ "currentRoom sensor: no currentRoomEntity in mapping, auto-detect did not run or sensor not on same HA device"
176833
177262
  );
176834
177263
  return;
176835
177264
  }
@@ -176842,26 +177271,22 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176842
177271
  );
176843
177272
  return;
176844
177273
  }
176845
- if (s.activeAreas.length === 0) {
176846
- this.logShortCircuitOnce(
176847
- "no-active-areas",
176848
- `currentRoom sensor: activeAreas empty while cleaning \u2014 sensor=${currentRoomEntityId} state="${roomState.state}"`
176849
- );
176850
- return;
176851
- }
176852
177274
  const serviceArea = this.agent.get(ServiceAreaBehavior);
177275
+ const externalSession = s.activeAreas.length === 0;
177276
+ const supportedAreaIds = serviceArea.state.supportedAreas.map(
177277
+ (a) => a.areaId
177278
+ );
177279
+ const isAllowedArea = (id) => externalSession ? supportedAreaIds.includes(id) : s.activeAreas.includes(id);
176853
177280
  const sensorAttrs = roomState.attributes;
176854
177281
  const segmentId = sensorAttrs.segment_id ?? sensorAttrs.room_id;
176855
177282
  const roomName = roomState.state;
176856
177283
  let matchedAreaId = null;
176857
- if (segmentId != null) {
176858
- if (s.activeAreas.includes(segmentId)) {
176859
- matchedAreaId = segmentId;
176860
- }
177284
+ if (segmentId != null && isAllowedArea(segmentId)) {
177285
+ matchedAreaId = segmentId;
176861
177286
  }
176862
177287
  if (matchedAreaId === null && segmentId != null) {
176863
177288
  for (const area of serviceArea.state.supportedAreas) {
176864
- if (s.activeAreas.includes(area.areaId) && area.areaId % 1e4 === segmentId) {
177289
+ if (isAllowedArea(area.areaId) && area.areaId % 1e4 === segmentId) {
176865
177290
  matchedAreaId = area.areaId;
176866
177291
  break;
176867
177292
  }
@@ -176871,7 +177296,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176871
177296
  const area = serviceArea.state.supportedAreas.find(
176872
177297
  (a) => a.areaInfo.locationInfo?.locationName?.toLowerCase() === roomName.toLowerCase()
176873
177298
  );
176874
- if (area && s.activeAreas.includes(area.areaId)) {
177299
+ if (area && isAllowedArea(area.areaId)) {
176875
177300
  matchedAreaId = area.areaId;
176876
177301
  }
176877
177302
  }
@@ -176990,6 +177415,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
176990
177415
  s.completedAreas.clear();
176991
177416
  s.lastCurrentArea = null;
176992
177417
  s.loggedShortCircuits.clear();
177418
+ s.pendingDispatches = [];
176993
177419
  this.trySetCurrentArea(s.activeAreas[0]);
176994
177420
  homeAssistant.callAction(this.state.config.start(void 0, this.agent));
176995
177421
  this.state.currentMode = newMode;
@@ -177006,6 +177432,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
177006
177432
  s.completedAreas.clear();
177007
177433
  s.lastCurrentArea = null;
177008
177434
  s.loggedShortCircuits.clear();
177435
+ s.pendingDispatches = [];
177009
177436
  this.trySetCurrentArea(areaId);
177010
177437
  homeAssistant.callAction(
177011
177438
  this.state.config.cleanRoom(newMode, this.agent)
@@ -177026,6 +177453,7 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
177026
177453
  s.completedAreas.clear();
177027
177454
  s.lastCurrentArea = null;
177028
177455
  s.loggedShortCircuits.clear();
177456
+ s.pendingDispatches = [];
177029
177457
  this.trySetCurrentArea(s.activeAreas[0]);
177030
177458
  }
177031
177459
  } catch {
@@ -177039,6 +177467,8 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
177039
177467
  s.lastCurrentArea = null;
177040
177468
  s.activeAreas = [];
177041
177469
  s.loggedShortCircuits.clear();
177470
+ s.pendingDispatches = [];
177471
+ s.observedCleaning = false;
177042
177472
  homeAssistant.callAction(
177043
177473
  this.state.config.returnToBase(void 0, this.agent)
177044
177474
  );
@@ -177615,8 +178045,8 @@ function buildSupportedModes2(attributes7, includeUnnamedRooms = false, customAr
177615
178045
  }
177616
178046
  return modes;
177617
178047
  }
177618
- function handleCustomServiceAreas(selectedAreas, customAreas, homeAssistant) {
177619
- 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);
177620
178050
  if (matched.length === 0) {
177621
178051
  logger194.warn(
177622
178052
  `Custom service areas: no match for selected IDs ${selectedAreas.join(", ")}`
@@ -177624,17 +178054,13 @@ function handleCustomServiceAreas(selectedAreas, customAreas, homeAssistant) {
177624
178054
  return { action: "vacuum.start" };
177625
178055
  }
177626
178056
  logger194.info(
177627
- `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(", ")}`
177628
178058
  );
177629
- for (let i = 1; i < matched.length; i++) {
177630
- const area = matched[i];
177631
- homeAssistant.callAction({
177632
- action: area.service,
177633
- target: area.target,
177634
- data: area.data
177635
- });
177636
- }
177637
- 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;
177638
178064
  return {
177639
178065
  action: first.service,
177640
178066
  target: first.target,
@@ -177668,9 +178094,14 @@ var vacuumRvcRunModeConfig = {
177668
178094
  );
177669
178095
  return isCleaning ? 1 /* Cleaning */ : 0 /* Idle */;
177670
178096
  },
177671
- getSupportedModes: (entity) => {
178097
+ getSupportedModes: (entity, agent) => {
177672
178098
  const attributes7 = entity.attributes;
177673
- 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
+ );
177674
178105
  },
177675
178106
  // biome-ignore lint/suspicious/noConfusingVoidType: Required by ValueSetter<void> interface
177676
178107
  start: (_, agent) => {
@@ -177681,13 +178112,10 @@ var vacuumRvcRunModeConfig = {
177681
178112
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
177682
178113
  const entity = homeAssistant.entity;
177683
178114
  const attributes7 = entity.state.attributes;
178115
+ const session = getSession(homeAssistant.endpoint);
177684
178116
  const customAreas = homeAssistant.state.mapping?.customServiceAreas;
177685
178117
  if (customAreas && customAreas.length > 0) {
177686
- return handleCustomServiceAreas(
177687
- selectedAreas,
177688
- customAreas,
177689
- homeAssistant
177690
- );
178118
+ return handleCustomServiceAreas(selectedAreas, customAreas, session);
177691
178119
  }
177692
178120
  const cleanAreaRooms = homeAssistant.state.mapping?.cleanAreaRooms;
177693
178121
  if (cleanAreaRooms && cleanAreaRooms.length > 0) {
@@ -177704,28 +178132,24 @@ var vacuumRvcRunModeConfig = {
177704
178132
  }
177705
178133
  const roomEntities = homeAssistant.state.mapping?.roomEntities;
177706
178134
  if (roomEntities && roomEntities.length > 0) {
177707
- const buttonEntityIds = [];
178135
+ const matched = [];
177708
178136
  for (const areaId of selectedAreas) {
177709
- const buttonEntityId = roomEntities.find(
177710
- (id) => toAreaId(id) === areaId
177711
- );
177712
- if (buttonEntityId) {
177713
- buttonEntityIds.push(buttonEntityId);
178137
+ const entityId = roomEntities.find((id) => toAreaId(id) === areaId);
178138
+ if (entityId) {
178139
+ matched.push({ areaId, entityId });
177714
178140
  }
177715
178141
  }
177716
- if (buttonEntityIds.length > 0) {
178142
+ if (matched.length > 0) {
177717
178143
  logger194.info(
177718
- `Roborock: Pressing button entities for selected rooms: ${buttonEntityIds.join(", ")}`
178144
+ `Roborock: ${matched.length} room button(s) queued: ${matched.map((m) => m.entityId).join(", ")}`
177719
178145
  );
177720
- for (let i = 1; i < buttonEntityIds.length; i++) {
177721
- homeAssistant.callAction({
177722
- action: "button.press",
177723
- target: buttonEntityIds[i]
177724
- });
177725
- }
178146
+ session.pendingDispatches = matched.slice(1).map(({ areaId, entityId }) => ({
178147
+ areaId,
178148
+ action: { action: "button.press", target: entityId }
178149
+ }));
177726
178150
  return {
177727
178151
  action: "button.press",
177728
- target: buttonEntityIds[0]
178152
+ target: matched[0].entityId
177729
178153
  };
177730
178154
  }
177731
178155
  }
@@ -178062,7 +178486,7 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
178062
178486
  await super.initialize();
178063
178487
  const homeAssistant = await this.agent.load(HomeAssistantEntityBehavior);
178064
178488
  this.update(homeAssistant.entity);
178065
- this.reactTo(homeAssistant.onChange, this.update);
178489
+ this.reactTo(homeAssistant.onChange, this.update, { offline: true });
178066
178490
  }
178067
178491
  update(entity) {
178068
178492
  if (!entity.state || !entity.state.attributes) {
@@ -178081,13 +178505,17 @@ var RvcCleanModeServerBase = class _RvcCleanModeServerBase extends RvcCleanModeS
178081
178505
  currentMode = this.pendingMode;
178082
178506
  }
178083
178507
  }
178084
- applyPatchState(this.state, {
178085
- currentMode,
178086
- supportedModes: this.state.config.getSupportedModes(
178087
- entity.state,
178088
- this.agent
178089
- )
178090
- });
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
+ );
178091
178519
  }
178092
178520
  changeToMode(request) {
178093
178521
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
@@ -178210,33 +178638,24 @@ function buildSupportedModes3(fanSpeedList, mopIntensityList, cleaningModeOption
178210
178638
  }
178211
178639
  return modes;
178212
178640
  }
178213
- var CLEANING_MODE_ALIASES = {
178214
- [0 /* Sweeping */]: [
178215
- "Sweeping",
178216
- "Vacuum",
178217
- "Vacuuming",
178218
- "Sweep",
178219
- "vacuum",
178220
- "sweeping"
178221
- ],
178222
- [1 /* Mopping */]: ["Mopping", "Mop", "mopping", "mop", "wet_mop"],
178223
- [2 /* SweepingAndMopping */]: [
178224
- "Sweeping and mopping",
178225
- "Vacuum and mop",
178226
- "Vacuum & Mop",
178227
- "Vacuum & mop",
178228
- "vacuum_and_mop",
178229
- "sweeping_and_mopping"
178230
- ],
178231
- [3 /* MoppingAfterSweeping */]: [
178232
- "Mopping after sweeping",
178233
- "mopping_after_sweeping",
178234
- "Vacuum then mop",
178235
- "Mop after vacuum",
178236
- "vacuum_then_mop",
178237
- "mop_after_vacuum"
178238
- ]
178641
+ var CLEAN_TOKENS = {
178642
+ vacuum: /\b(vacuum|vacuuming|sweep|sweeping|suction)\b/i,
178643
+ mop: /\b(mop|mopping|wipe|wet|wash|scrub)\b/i,
178644
+ sequential: /\b(then|after|before|followed|following|first|secondly|sequentially)\b/i
178239
178645
  };
178646
+ function normalizeCleanLabel(s) {
178647
+ return s.toLowerCase().replace(/[_\-+&/. ]+/g, " ").replace(/\band\b/g, " ").trim();
178648
+ }
178649
+ function classifyCleanOption(option) {
178650
+ const s = normalizeCleanLabel(option);
178651
+ const hasVac = CLEAN_TOKENS.vacuum.test(s);
178652
+ const hasMop = CLEAN_TOKENS.mop.test(s);
178653
+ const hasSeq = CLEAN_TOKENS.sequential.test(s);
178654
+ if (hasVac && hasMop && hasSeq) return 3 /* MoppingAfterSweeping */;
178655
+ if (hasVac && hasMop) return 2 /* SweepingAndMopping */;
178656
+ if (hasMop) return 1 /* Mopping */;
178657
+ return 0 /* Sweeping */;
178658
+ }
178240
178659
  var CLEAN_TYPE_LABELS = {
178241
178660
  [0 /* Sweeping */]: "Sweeping",
178242
178661
  [1 /* Mopping */]: "Mopping",
@@ -178369,17 +178788,7 @@ function matchMopIntensityOption(name, availableOptions) {
178369
178788
  }
178370
178789
  function parseCleanType(modeString) {
178371
178790
  if (!modeString) return 0 /* Sweeping */;
178372
- const s = modeString.toLowerCase();
178373
- if (s.includes("mopping after") || s.includes("after sweeping") || s.includes("then_mop") || s.includes("then mop")) {
178374
- return 3 /* MoppingAfterSweeping */;
178375
- }
178376
- if (s.includes("and") || s.includes("sweeping and mopping")) {
178377
- return 2 /* SweepingAndMopping */;
178378
- }
178379
- if (s === "mopping" || s.includes("mop")) {
178380
- return 1 /* Mopping */;
178381
- }
178382
- return 0 /* Sweeping */;
178791
+ return classifyCleanOption(modeString);
178383
178792
  }
178384
178793
  function cleanTypeToModeId(ct) {
178385
178794
  switch (ct) {
@@ -178420,30 +178829,20 @@ var CLEAN_TYPE_FALLBACK = {
178420
178829
  [3 /* MoppingAfterSweeping */]: 2 /* SweepingAndMopping */
178421
178830
  };
178422
178831
  function findMatchingCleanOption(ct, availableOptions) {
178423
- const aliases = CLEANING_MODE_ALIASES[ct];
178424
- if (!availableOptions || availableOptions.length === 0) return aliases[0];
178832
+ if (!availableOptions || availableOptions.length === 0) {
178833
+ return CLEAN_TYPE_LABELS[ct];
178834
+ }
178425
178835
  const typesToTry = [ct];
178426
178836
  const fallback = CLEAN_TYPE_FALLBACK[ct];
178427
178837
  if (fallback !== void 0) typesToTry.push(fallback);
178428
178838
  for (const type of typesToTry) {
178429
- const typeAliases = CLEANING_MODE_ALIASES[type];
178430
- for (const alias of typeAliases) {
178431
- const match = availableOptions.find(
178432
- (o) => o.toLowerCase() === alias.toLowerCase()
178433
- );
178434
- if (match) return match;
178435
- }
178436
- for (const alias of typeAliases) {
178437
- const match = availableOptions.find(
178438
- (o) => o.toLowerCase().includes(alias.toLowerCase())
178439
- );
178440
- if (match) return match;
178441
- }
178839
+ const match = availableOptions.find((o) => classifyCleanOption(o) === type);
178840
+ if (match) return match;
178442
178841
  }
178443
178842
  logger196.warn(
178444
178843
  `No match for ${CLEAN_TYPE_LABELS[ct]} in [${availableOptions.join(", ")}]`
178445
178844
  );
178446
- return aliases[0];
178845
+ return availableOptions[0];
178447
178846
  }
178448
178847
  function buildCleaningModeAction(targetCleanType, agent) {
178449
178848
  const selectEntityId = getCleaningModeSelectEntity(agent);
@@ -178578,7 +178977,7 @@ function createCleanModeConfig(fanSpeedList, mopIntensityList, cleaningModeOptio
178578
178977
  const vacuumEntityId = homeAssistant.entityId;
178579
178978
  const mapping = homeAssistant.state.mapping;
178580
178979
  logger196.info(
178581
- `setCleanMode(${mode}) for ${vacuumEntityId} \u2014 suctionEntity=${mapping?.suctionLevelEntity ?? "none"}, mopEntity=${mapping?.mopIntensityEntity ?? "none"}, fanSpeedList=${JSON.stringify(fanSpeedList ?? [])}, mopIntensityList=${JSON.stringify(mopIntensityList ?? [])}, customTags=${JSON.stringify(customFanSpeedTags ?? {})}`
178980
+ `setCleanMode(${mode}) for ${vacuumEntityId}, suctionEntity=${mapping?.suctionLevelEntity ?? "none"}, mopEntity=${mapping?.mopIntensityEntity ?? "none"}, fanSpeedList=${JSON.stringify(fanSpeedList ?? [])}, mopIntensityList=${JSON.stringify(mopIntensityList ?? [])}, customTags=${JSON.stringify(customFanSpeedTags ?? {})}`
178582
178981
  );
178583
178982
  if (mopIntensityList && mopIntensityList.length > 0 && isMopIntensityMode(mode)) {
178584
178983
  const mopIndex = mode - MOP_INTENSITY_MODE_BASE;
@@ -178919,58 +179318,65 @@ function RvcOperationalStateServer2(config10) {
178919
179318
  var logger198 = Logger.get("VacuumRvcOperationalStateServer");
178920
179319
  function isCharging(entity) {
178921
179320
  const attrs = entity.attributes;
178922
- if (attrs.battery_icon?.includes("charging")) return true;
178923
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;
178924
179327
  if (typeof attrs.status === "string" && attrs.status.toLowerCase().includes("charg"))
178925
179328
  return true;
178926
179329
  return false;
178927
179330
  }
178928
- var VacuumRvcOperationalStateServer = RvcOperationalStateServer2({
178929
- getOperationalState(entity) {
178930
- const state = entity.state;
178931
- const cleaningStates = [
178932
- VacuumState.cleaning,
178933
- VacuumState.segment_cleaning,
178934
- VacuumState.zone_cleaning,
178935
- VacuumState.spot_cleaning,
178936
- VacuumState.mop_cleaning
178937
- ];
178938
- let operationalState;
178939
- if (state === VacuumState.docked) {
178940
- if (isCharging(entity)) {
178941
- operationalState = RvcOperationalState3.OperationalState.Charging;
178942
- } else {
178943
- operationalState = RvcOperationalState3.OperationalState.Docked;
178944
- }
178945
- } else if (state === VacuumState.returning) {
178946
- operationalState = RvcOperationalState3.OperationalState.SeekingCharger;
178947
- } 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
+ );
178948
179366
  operationalState = RvcOperationalState3.OperationalState.Running;
178949
- } else if (state === VacuumState.paused) {
178950
- operationalState = RvcOperationalState3.OperationalState.Paused;
178951
- } else if (state === VacuumState.idle) {
178952
- if (isCharging(entity)) {
178953
- operationalState = RvcOperationalState3.OperationalState.Charging;
178954
- } else {
178955
- operationalState = RvcOperationalState3.OperationalState.Stopped;
178956
- }
178957
- } else if (state === VacuumState.error || state === "unavailable") {
178958
- operationalState = RvcOperationalState3.OperationalState.Error;
178959
179367
  } else {
178960
- if (state.toLowerCase().includes("clean")) {
178961
- logger198.info(
178962
- `Unknown vacuum state "${state}" contains 'clean', treating as Running`
178963
- );
178964
- operationalState = RvcOperationalState3.OperationalState.Running;
178965
- } else {
178966
- logger198.info(`Unknown vacuum state "${state}", treating as Stopped`);
178967
- operationalState = RvcOperationalState3.OperationalState.Stopped;
178968
- }
179368
+ logger198.info(`Unknown vacuum state "${state}", treating as Stopped`);
179369
+ operationalState = RvcOperationalState3.OperationalState.Stopped;
178969
179370
  }
178970
- logger198.debug(
178971
- `Vacuum operationalState: "${state}" -> ${RvcOperationalState3.OperationalState[operationalState]}`
178972
- );
178973
- 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);
178974
179380
  },
178975
179381
  pause: (_, agent) => {
178976
179382
  const supportedFeatures = agent.get(HomeAssistantEntityBehavior).entity.state.attributes.supported_features ?? 0;
@@ -180545,7 +180951,7 @@ var BridgeEndpointManager = class extends Service {
180545
180951
  this.mappingFingerprints.delete(endpoint.entityId);
180546
180952
  } else if (this.registry.isAutoComposedDevicesEnabled() && this.registry.isComposedSubEntityUsed(endpoint.entityId)) {
180547
180953
  this.log.info(
180548
- `Deleting standalone endpoint ${endpoint.entityId} \u2014 consumed by composed device`
180954
+ `Deleting standalone endpoint ${endpoint.entityId}, consumed by composed device`
180549
180955
  );
180550
180956
  try {
180551
180957
  await endpoint.delete();
@@ -180583,14 +180989,14 @@ var BridgeEndpointManager = class extends Service {
180583
180989
  if (!memoryLimitReached && isHeapUnderPressure()) {
180584
180990
  memoryLimitReached = true;
180585
180991
  this.log.error(
180586
- "Memory pressure detected \u2014 skipping remaining entities to prevent OOM crash. Reduce the number of entities in this bridge or increase the Node.js heap size (NODE_OPTIONS=--max-old-space-size=1024)."
180992
+ "Memory pressure detected, skipping remaining entities to prevent OOM crash. Reduce the number of entities in this bridge or increase the Node.js heap size (NODE_OPTIONS=--max-old-space-size=1024)."
180587
180993
  );
180588
180994
  }
180589
180995
  if (memoryLimitReached) {
180590
180996
  if (!existingEndpoints.some((e) => e.entityId === entityId)) {
180591
180997
  this._failedEntities.push({
180592
180998
  entityId,
180593
- reason: "Skipped due to memory pressure \u2014 reduce entities or increase heap size"
180999
+ reason: "Skipped due to memory pressure, reduce entities or increase heap size"
180594
181000
  });
180595
181001
  }
180596
181002
  continue;
@@ -180602,7 +181008,7 @@ var BridgeEndpointManager = class extends Service {
180602
181008
  }
180603
181009
  if (this.registry.isAutoComposedDevicesEnabled() && this.registry.isComposedSubEntityUsed(entityId)) {
180604
181010
  this.log.debug(
180605
- `Skipping ${entityId} \u2014 already part of a composed device`
181011
+ `Skipping ${entityId}, already part of a composed device`
180606
181012
  );
180607
181013
  continue;
180608
181014
  }
@@ -180702,7 +181108,7 @@ var BridgeEndpointManager = class extends Service {
180702
181108
  }
180703
181109
  /**
180704
181110
  * Log detailed behavior error information for debugging "Behaviors have errors".
180705
- * Matter.js EndpointBehaviorsError extends AggregateError the `errors` array
181111
+ * Matter.js EndpointBehaviorsError extends AggregateError, the `errors` array
180706
181112
  * contains individual behavior crash errors (one per failed behavior).
180707
181113
  */
180708
181114
  logDetailedError(entityId, error) {
@@ -180851,7 +181257,7 @@ var BridgeRegistry = class _BridgeRegistry {
180851
181257
  * Check if auto composed devices mode is enabled.
180852
181258
  * When enabled, temperature sensors with auto-mapped humidity/pressure/battery
180853
181259
  * build real Matter Composed Devices (BridgedNodeEndpoint with sub-endpoints)
180854
- * rather than stacking extra clusters onto a flat TemperatureSensor
181260
+ * rather than stacking extra clusters onto a flat TemperatureSensor.
180855
181261
  * Apple Home, Google Home, and Alexa render each sub-endpoint using its
180856
181262
  * own device type.
180857
181263
  */
@@ -180997,8 +181403,8 @@ var BridgeRegistry = class _BridgeRegistry {
180997
181403
  } else if (id.endsWith("_mode")) {
180998
181404
  const options = state.attributes?.options;
180999
181405
  if (options?.some(
181000
- (o) => /^(vacuum|mop|sweep|vacuum_and_mop|vacuum_then_mop|mopping|sweeping|sweeping_and_mopping|mopping_after_sweeping)$/i.test(
181001
- o
181406
+ (o) => /^(vacuum|mop|sweep|sweep_mop|sweep_before_mopping|sweep_then_mop|vacuum_and_mop|vacuum_then_mop|mopping|sweeping|sweeping_and_mopping|mopping_after_sweeping)$/i.test(
181407
+ o.replace(/\s+/g, "_")
181002
181408
  )
181003
181409
  )) {
181004
181410
  cleaningModeEntity = entity.entity_id;
@@ -181156,14 +181562,14 @@ var BridgeRegistry = class _BridgeRegistry {
181156
181562
  const segments = areaMapping[haAreaId];
181157
181563
  if (!segments || segments.length === 0) {
181158
181564
  _BridgeRegistry.cleanAreaLogger.debug(
181159
- `${entityId}: Skipping HA area ${haAreaId} \u2014 no segments mapped`
181565
+ `${entityId}: Skipping HA area ${haAreaId}, no segments mapped`
181160
181566
  );
181161
181567
  continue;
181162
181568
  }
181163
181569
  if (validSegmentIds && !segments.some((sid) => validSegmentIds.has(sid))) {
181164
181570
  const areaName2 = this.registry.areas.get(haAreaId) ?? haAreaId;
181165
181571
  _BridgeRegistry.cleanAreaLogger.info(
181166
- `${entityId}: Skipping stale HA area "${areaName2}" (${haAreaId}) \u2014 segments [${segments.join(", ")}] no longer exist on vacuum`
181572
+ `${entityId}: Skipping stale HA area "${areaName2}" (${haAreaId}), segments [${segments.join(", ")}] no longer exist on vacuum`
181167
181573
  );
181168
181574
  continue;
181169
181575
  }
@@ -181434,6 +181840,19 @@ init_dist();
181434
181840
  init_diagnostic_event_bus();
181435
181841
  var AUTO_FORCE_SYNC_INTERVAL_MS2 = 9e4;
181436
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
+ }
181437
181856
  var ServerModeBridge = class {
181438
181857
  constructor(logger207, dataProvider, endpointManager, server) {
181439
181858
  this.dataProvider = dataProvider;
@@ -181456,6 +181875,10 @@ var ServerModeBridge = class {
181456
181875
  deadSessionTimer = null;
181457
181876
  staleSessionTimers = /* @__PURE__ */ new Map();
181458
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;
181459
181882
  // Tracks the last synced state JSON per entity to avoid pushing unchanged states.
181460
181883
  lastSyncedState;
181461
181884
  // Session lifecycle diagnostic handlers (non-destructive, logging only).
@@ -181540,6 +181963,7 @@ var ServerModeBridge = class {
181540
181963
  });
181541
181964
  }
181542
181965
  this.wireSessionDiagnostics();
181966
+ this.startSessionRotation();
181543
181967
  this.scheduleWarmStart();
181544
181968
  logMemoryUsage(this.log, "server mode bridge running");
181545
181969
  this.log.info("Server mode bridge started");
@@ -181555,6 +181979,7 @@ ${e?.toString()}`);
181555
181979
  }
181556
181980
  }
181557
181981
  async stop(code = BridgeStatus.Stopped, reason = "Manually stopped") {
181982
+ this.stopSessionRotation();
181558
181983
  this.unwireSessionDiagnostics();
181559
181984
  this.cancelWarmStart();
181560
181985
  this.stopAutoForceSync();
@@ -181583,6 +182008,7 @@ ${e?.toString()}`);
181583
182008
  await this.refreshDevices();
181584
182009
  if (this.status.code === BridgeStatus.Running) {
181585
182010
  this.startAutoForceSyncIfEnabled();
182011
+ this.startSessionRotation();
181586
182012
  }
181587
182013
  } catch (e) {
181588
182014
  const reason = "Failed to update server mode bridge due to error:";
@@ -181639,7 +182065,7 @@ ${e?.toString()}`);
181639
182065
  );
181640
182066
  if (totalSubs === 0 && sessions.length > 0) {
181641
182067
  this.log.warn(
181642
- `All subscriptions lost \u2014 ${sessions.length} session(s) still active, waiting for controller to re-subscribe`
182068
+ `All subscriptions lost, ${sessions.length} session(s) still active, waiting for controller to re-subscribe`
181643
182069
  );
181644
182070
  if (!this.deadSessionTimer) {
181645
182071
  this.deadSessionTimer = setTimeout(() => {
@@ -181672,13 +182098,14 @@ ${e?.toString()}`);
181672
182098
  };
181673
182099
  sessionManager.subscriptionsChanged.on(this.sessionDiagHandler);
181674
182100
  this.sessionAddedHandler = (newSession) => {
182101
+ this.sessionStartedAt.set(newSession.id, Date.now());
181675
182102
  this.log.info(
181676
182103
  `Session opened: id=${newSession.id} peer=${newSession.peerNodeId}`
181677
182104
  );
181678
182105
  for (const s of [...sessionManager.sessions]) {
181679
182106
  if (s !== newSession && !s.isClosing && s.peerNodeId === newSession.peerNodeId && s.fabric?.fabricIndex === newSession.fabric?.fabricIndex && s.subscriptions.size === 0) {
181680
182107
  this.log.info(
181681
- `Closing stale session ${s.id} (peer ${s.peerNodeId}, 0 subs) \u2014 replaced by session ${newSession.id}`
182108
+ `Closing stale session ${s.id} (peer ${s.peerNodeId}, 0 subs), replaced by session ${newSession.id}`
181682
182109
  );
181683
182110
  s.initiateForceClose().catch(() => {
181684
182111
  });
@@ -181686,6 +182113,7 @@ ${e?.toString()}`);
181686
182113
  }
181687
182114
  };
181688
182115
  this.sessionDeletedHandler = (session) => {
182116
+ this.sessionStartedAt.delete(session.id);
181689
182117
  const sessions = [...sessionManager.sessions];
181690
182118
  this.log.warn(
181691
182119
  `Session closed: id=${session.id} peer=${session.peerNodeId} | remaining sessions=${sessions.length}`
@@ -181777,6 +182205,85 @@ ${e?.toString()}`);
181777
182205
  clearTimeout(timer);
181778
182206
  }
181779
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
+ }
181780
182287
  }
181781
182288
  stopAutoForceSync() {
181782
182289
  if (this.autoForceSyncTimer) {
@@ -182099,7 +182606,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
182099
182606
  }
182100
182607
  }
182101
182608
  } else {
182102
- logger205.warn(`${entityId}: No device_id \u2014 cannot auto-assign battery`);
182609
+ logger205.warn(`${entityId}: No device_id, cannot auto-assign battery`);
182103
182610
  }
182104
182611
  const payload = {
182105
182612
  entity_id: entityId,
@@ -182155,7 +182662,7 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
182155
182662
  * Previous approaches that pushed state through
182156
182663
  * HomeAssistantEntityBehavior failed because the reactor writes
182157
182664
  * (RvcOperationalStateServer.update()) run inside the postCommit
182158
- * phase of the HAEntityBehavior transaction those writes are
182665
+ * phase of the HAEntityBehavior transaction, those writes are
182159
182666
  * buffered but never committed, so no attrsChanged event reaches
182160
182667
  * the ServerSubscription.
182161
182668
  *
@@ -182193,6 +182700,11 @@ var ServerModeVacuumEndpoint = class _ServerModeVacuumEndpoint extends EntityEnd
182193
182700
  * unlike errorStateLabel (id 1) which requires errorStateId 128-191.
182194
182701
  */
182195
182702
  async pushKeepalive() {
182703
+ try {
182704
+ await this.construction.ready;
182705
+ } catch {
182706
+ return;
182707
+ }
182196
182708
  try {
182197
182709
  this.keepaliveCounter++;
182198
182710
  const counter = this.keepaliveCounter;
@@ -182401,11 +182913,11 @@ var ServerModeEndpointManager = class extends Service {
182401
182913
  }
182402
182914
  if (isHeapUnderPressure()) {
182403
182915
  this.log.error(
182404
- "Memory pressure detected \u2014 cannot create device endpoint. Reduce entities on other bridges or increase the Node.js heap size (NODE_OPTIONS=--max-old-space-size=1024)."
182916
+ "Memory pressure detected, cannot create device endpoint. Reduce entities on other bridges or increase the Node.js heap size (NODE_OPTIONS=--max-old-space-size=1024)."
182405
182917
  );
182406
182918
  this._failedEntities.push({
182407
182919
  entityId,
182408
- reason: "Skipped due to memory pressure \u2014 reduce entities or increase heap size"
182920
+ reason: "Skipped due to memory pressure, reduce entities or increase heap size"
182409
182921
  });
182410
182922
  return;
182411
182923
  }
@@ -182795,7 +183307,7 @@ function patchLevelControlTlv() {
182795
183307
  );
182796
183308
  } else {
182797
183309
  logger206.warn(
182798
- "Failed to patch LevelControl TLV schemas \u2014 field definitions not found. Google Home brightness adjustment may not work."
183310
+ "Failed to patch LevelControl TLV schemas, field definitions not found. Google Home brightness adjustment may not work."
182799
183311
  );
182800
183312
  }
182801
183313
  }
@@ -182967,6 +183479,10 @@ function startOptionsBuilder(yargs2) {
182967
183479
  type: "number",
182968
183480
  description: "The refresh rate (in seconds) to detect new devices & entities or their configurations",
182969
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
182970
183486
  }).option("http-auth-username", {
182971
183487
  type: "string",
182972
183488
  description: "Username for HTTP basic authentication (optional)"