@riddix/hamh 2.1.0-alpha.842 → 2.1.0-alpha.844

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ of port forwarding etc.
37
37
 
38
38
  | Channel | Branch | Current Version | Description |
39
39
  |---------|--------|-----------------|-------------|
40
- | **Stable** | `main` | v2.0.50 | Production-ready, recommended for most users |
40
+ | **Stable** | `main` | v2.0.52 | Production-ready, recommended for most users |
41
41
  | **Alpha** | `alpha` | v2.1.0-alpha.x (next) | Currently level with Stable; next pre-release lands here first |
42
42
  | **Testing** | `testing` | v4.1.0-testing.x | ⚠️ **Highly unstable!** Experimental features, may break |
43
43
 
@@ -61,9 +61,24 @@ Re-assign the affected devices to their rooms after they reconnect. See the [doc
61
61
  ## 🎉 What's New
62
62
 
63
63
  <details>
64
- <summary><strong>📦 Stable Features (v2.0.50)</strong> - Click to expand</summary>
64
+ <summary><strong>📦 Stable Features (v2.0.52)</strong> - Click to expand</summary>
65
65
 
66
- **New in v2.0.50:**
66
+ **New in v2.0.52 (hotfix release):**
67
+
68
+ - 🔌 The plugins page no longer fails with a 500 when a server mode bridge exists ([#430](https://github.com/RiDDiX/home-assistant-matter-hub/issues/430))
69
+
70
+ **Previously in v2.0.51:**
71
+
72
+ - 🐕 **Opt-in wedge watchdog**: rotates a controller session that keeps acking but stops talking at the interaction layer, the automated "Play Sound" fix for stuck Apple devices ([#287](https://github.com/RiDDiX/home-assistant-matter-hub/issues/287), [#428](https://github.com/RiDDiX/home-assistant-matter-hub/issues/428))
73
+ - 🧹 **Clean up orphaned records**: bridge menu action with a dry-run preview that removes identity and mapping leftovers of entities deleted from HA (7 day tombstone guards against false positives)
74
+ - 🚗 **EV charger support**: new EVSE device-type override with charging switch and current limit mapping, EnableCharging/Disable from the controller; Aqara and Home Assistant render it, keep it off Alexa bridges
75
+ - 🩺 **Subscription scope on the health card**: see whether each controller holds a whole-node wildcard or watches specific endpoints
76
+ - 📷 **Camera live view signaling completed**: the WebRTC answer now reaches the controller over the requestor cluster, plus full media path logging, HA signaling timeouts, trickle ICE and ICE server support (still experimental until verified on real hardware)
77
+ - 🪟 **Cover stop detection in every position space**: the v2.0.50 cover fix now also works with the Alexa percentage flag and the per-cover swap toggle, and swapped covers report the right direction ([#429](https://github.com/RiDDiX/home-assistant-matter-hub/issues/429))
78
+ - 🤖 **Vacuum on/off crash fixed**: state updates no longer throw on vacuums with the on/off toggle ([#428](https://github.com/RiDDiX/home-assistant-matter-hub/issues/428))
79
+ - 🌍 **Brazilian Portuguese completed**: 602 of 615 strings ([#420](https://github.com/RiDDiX/home-assistant-matter-hub/issues/420))
80
+
81
+ **Previously in v2.0.50:**
67
82
 
68
83
  - 🆔 **Stable device identity (opt-in)**: endpoints keep their identity when an entity is renamed or re-registered, keyed on the HA registry unique id; enable the `stableIdentity` feature flag per bridge ([#404](https://github.com/RiDDiX/home-assistant-matter-hub/issues/404), [#407](https://github.com/RiDDiX/home-assistant-matter-hub/issues/407))
69
84
  - ⚡ **Energy suite stage 1**: consumption sensors expose an **Electrical Meter** that Google Home and SmartThings render, home batteries become a **Battery Storage** device with signed charge/discharge power and lifetime energy, and voltage/current/energy sensors group onto one endpoint via new mapping fields
@@ -255,7 +270,7 @@ Re-assign the affected devices to their rooms after they reconnect. See the [doc
255
270
  <details>
256
271
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
257
272
 
258
- **Alpha is currently level with Stable (v2.0.50).** All alpha work up to the latest pre-release has been promoted into v2.0.50. New alpha work continues from the next pre-release tag onward and will appear here as development progresses.
273
+ **Alpha is currently level with Stable (v2.0.52).** All alpha work up to the latest pre-release has been promoted into v2.0.52. New alpha work continues from the next pre-release tag onward and will appear here as development progresses.
259
274
 
260
275
  </details>
261
276
 
@@ -133183,7 +133183,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133183
133183
  fanWindPresets: config11.fanWindPresets,
133184
133184
  fanRestoreSpeedOnPowerOn: config11.fanRestoreSpeedOnPowerOn,
133185
133185
  composedEntities: config11.composedEntities,
133186
- disableMomentaryFlip: config11.disableMomentaryFlip
133186
+ disableMomentaryFlip: config11.disableMomentaryFlip,
133187
+ vacuumAscendingRoomOrder: config11.vacuumAscendingRoomOrder
133187
133188
  });
133188
133189
  mappingsRestored++;
133189
133190
  }
@@ -133346,7 +133347,8 @@ WARNING: ${includeIdentity ? "This backup contains sensitive Matter identity dat
133346
133347
  customFanSpeedTags: config11.customFanSpeedTags,
133347
133348
  fanWindPresets: config11.fanWindPresets,
133348
133349
  composedEntities: config11.composedEntities,
133349
- disableMomentaryFlip: config11.disableMomentaryFlip
133350
+ disableMomentaryFlip: config11.disableMomentaryFlip,
133351
+ vacuumAscendingRoomOrder: config11.vacuumAscendingRoomOrder
133350
133352
  });
133351
133353
  mappingsRestored++;
133352
133354
  }
@@ -134195,6 +134197,7 @@ function entityMappingApi(mappingStorage, identityStorage) {
134195
134197
  fanRestoreSpeedOnPowerOn: body.fanRestoreSpeedOnPowerOn,
134196
134198
  currentRoomEntity: body.currentRoomEntity,
134197
134199
  cleanedAreaEntity: body.cleanedAreaEntity,
134200
+ vacuumAscendingRoomOrder: body.vacuumAscendingRoomOrder,
134198
134201
  disableCustomAreaRoomModes: body.disableCustomAreaRoomModes,
134199
134202
  valetudoIdentifier: body.valetudoIdentifier,
134200
134203
  coverSwapOpenClose: body.coverSwapOpenClose,
@@ -134704,7 +134707,8 @@ function configToProfileEntry(config11) {
134704
134707
  climateKeepModeOnIdle: config11.climateKeepModeOnIdle,
134705
134708
  climateExposeFan: config11.climateExposeFan,
134706
134709
  climateAutoMode: config11.climateAutoMode,
134707
- disableMomentaryFlip: config11.disableMomentaryFlip
134710
+ disableMomentaryFlip: config11.disableMomentaryFlip,
134711
+ vacuumAscendingRoomOrder: config11.vacuumAscendingRoomOrder
134708
134712
  };
134709
134713
  }
134710
134714
  function mappingProfileApi(mappingStorage) {
@@ -134845,7 +134849,8 @@ function mappingProfileApi(mappingStorage) {
134845
134849
  climateKeepModeOnIdle: entry.climateKeepModeOnIdle,
134846
134850
  climateExposeFan: entry.climateExposeFan,
134847
134851
  climateAutoMode: entry.climateAutoMode,
134848
- disableMomentaryFlip: entry.disableMomentaryFlip
134852
+ disableMomentaryFlip: entry.disableMomentaryFlip,
134853
+ vacuumAscendingRoomOrder: entry.vacuumAscendingRoomOrder
134849
134854
  });
134850
134855
  applied++;
134851
134856
  } catch (e) {
@@ -136622,6 +136627,7 @@ function pluginApi(bridgeService, storageLocation) {
136622
136627
  const result = [];
136623
136628
  for (const bridge of bridgeService.bridges) {
136624
136629
  const info = bridge.pluginInfo;
136630
+ if (!info) continue;
136625
136631
  const plugins = info.metadata.map((meta) => ({
136626
136632
  name: meta.name,
136627
136633
  version: meta.version,
@@ -136644,41 +136650,41 @@ function pluginApi(bridgeService, storageLocation) {
136644
136650
  }
136645
136651
  res.json(result);
136646
136652
  });
136647
- router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136648
- const bridge = bridgeService.get(req.params.bridgeId);
136653
+ function pluginBridge(bridgeId, res) {
136654
+ const bridge = bridgeService.get(bridgeId);
136649
136655
  if (!bridge) {
136650
136656
  res.status(404).json({ error: "Bridge not found" });
136651
- return;
136657
+ return void 0;
136658
+ }
136659
+ if (typeof bridge.enablePlugin !== "function") {
136660
+ res.status(400).json({ error: "Bridge does not support plugins" });
136661
+ return void 0;
136652
136662
  }
136663
+ return bridge;
136664
+ }
136665
+ router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136666
+ const bridge = pluginBridge(req.params.bridgeId, res);
136667
+ if (!bridge) return;
136653
136668
  const { pluginName } = req.params;
136654
136669
  bridge.enablePlugin(pluginName);
136655
136670
  res.json({ success: true, pluginName, enabled: true });
136656
136671
  });
136657
136672
  router.post("/:bridgeId/:pluginName/disable", (req, res) => {
136658
- const bridge = bridgeService.get(req.params.bridgeId);
136659
- if (!bridge) {
136660
- res.status(404).json({ error: "Bridge not found" });
136661
- return;
136662
- }
136673
+ const bridge = pluginBridge(req.params.bridgeId, res);
136674
+ if (!bridge) return;
136663
136675
  const { pluginName } = req.params;
136664
136676
  bridge.disablePlugin(pluginName);
136665
136677
  res.json({ success: true, pluginName, enabled: false });
136666
136678
  });
136667
136679
  router.get("/:bridgeId/:pluginName/config-schema", (req, res) => {
136668
- const bridge = bridgeService.get(req.params.bridgeId);
136669
- if (!bridge) {
136670
- res.status(404).json({ error: "Bridge not found" });
136671
- return;
136672
- }
136680
+ const bridge = pluginBridge(req.params.bridgeId, res);
136681
+ if (!bridge) return;
136673
136682
  const schema6 = bridge.getPluginConfigSchema(req.params.pluginName);
136674
136683
  res.json({ pluginName: req.params.pluginName, schema: schema6 ?? null });
136675
136684
  });
136676
136685
  router.post("/:bridgeId/:pluginName/config", async (req, res) => {
136677
- const bridge = bridgeService.get(req.params.bridgeId);
136678
- if (!bridge) {
136679
- res.status(404).json({ error: "Bridge not found" });
136680
- return;
136681
- }
136686
+ const bridge = pluginBridge(req.params.bridgeId, res);
136687
+ if (!bridge) return;
136682
136688
  const { config: config11 } = req.body;
136683
136689
  if (!config11 || typeof config11 !== "object") {
136684
136690
  res.status(400).json({ error: "config object is required" });
@@ -136692,11 +136698,8 @@ function pluginApi(bridgeService, storageLocation) {
136692
136698
  res.json({ success: true, pluginName: req.params.pluginName });
136693
136699
  });
136694
136700
  router.post("/:bridgeId/:pluginName/reset", (req, res) => {
136695
- const bridge = bridgeService.get(req.params.bridgeId);
136696
- if (!bridge) {
136697
- res.status(404).json({ error: "Bridge not found" });
136698
- return;
136699
- }
136701
+ const bridge = pluginBridge(req.params.bridgeId, res);
136702
+ if (!bridge) return;
136700
136703
  const { pluginName } = req.params;
136701
136704
  bridge.resetPlugin(pluginName);
136702
136705
  res.json({ success: true, pluginName, reset: true });
@@ -139566,6 +139569,7 @@ var EntityMappingStorage = class extends Service {
139566
139569
  fanRestoreSpeedOnPowerOn: request.fanRestoreSpeedOnPowerOn || void 0,
139567
139570
  currentRoomEntity: request.currentRoomEntity?.trim() || void 0,
139568
139571
  cleanedAreaEntity: request.cleanedAreaEntity?.trim() || void 0,
139572
+ vacuumAscendingRoomOrder: request.vacuumAscendingRoomOrder || void 0,
139569
139573
  disableCustomAreaRoomModes: request.disableCustomAreaRoomModes || void 0,
139570
139574
  valetudoIdentifier: request.valetudoIdentifier?.trim() || void 0,
139571
139575
  coverSwapOpenClose: request.coverSwapOpenClose || void 0,
@@ -139583,7 +139587,7 @@ var EntityMappingStorage = class extends Service {
139583
139587
  composedEntities: request.composedEntities?.filter((e) => e.entityId?.trim()) ?? void 0,
139584
139588
  disableMomentaryFlip: request.disableMomentaryFlip || void 0
139585
139589
  };
139586
- if (!config11.matterDeviceType && !config11.customName && !config11.customProductName && !config11.customVendorName && !config11.customSerialNumber && config11.customVendorId === void 0 && config11.disabled !== true && !config11.filterLifeEntity && !config11.cleaningModeEntity && !config11.temperatureEntity && !config11.humidityEntity && !config11.batteryEntity && !config11.disableBatteryMapping && !config11.chargingStateEntity && !config11.roomEntities && !config11.disableLockPin && !config11.lockUsercodeService && config11.lockUsercodeSlot === void 0 && config11.lockPinMinLength === void 0 && config11.lockPinMaxLength === void 0 && !config11.powerEntity && !config11.energyEntity && !config11.voltageEntity && !config11.currentEntity && !config11.batteryPowerEntity && !config11.batteryEnergyEntity && !config11.chargingSwitchEntity && !config11.currentLimitEntity && !config11.pressureEntity && !config11.suctionLevelEntity && !config11.mopIntensityEntity && (!config11.customServiceAreas || config11.customServiceAreas.length === 0) && (!config11.customFanSpeedTags || Object.keys(config11.customFanSpeedTags).length === 0) && (!config11.fanWindPresets || (config11.fanWindPresets.natural?.length ?? 0) === 0 && (config11.fanWindPresets.sleep?.length ?? 0) === 0) && !config11.fanRestoreSpeedOnPowerOn && !config11.currentRoomEntity && !config11.cleanedAreaEntity && !config11.disableCustomAreaRoomModes && !config11.valetudoIdentifier && !config11.coverSwapOpenClose && !config11.coverExposeAsDimmableLight && !config11.selectExposeAsSwitch && !config11.selectSwitchOnOption && !config11.selectSwitchOffOption && !config11.coverSliderDebounceMs && !config11.updateThrottleMs && !config11.disableClimateOnOff && !config11.disableClimateFanControl && !config11.climateKeepModeOnIdle && !config11.climateExposeFan && !config11.climateAutoMode && (!config11.composedEntities || config11.composedEntities.length === 0) && !config11.disableMomentaryFlip) {
139590
+ if (!config11.matterDeviceType && !config11.customName && !config11.customProductName && !config11.customVendorName && !config11.customSerialNumber && config11.customVendorId === void 0 && config11.disabled !== true && !config11.filterLifeEntity && !config11.cleaningModeEntity && !config11.temperatureEntity && !config11.humidityEntity && !config11.batteryEntity && !config11.disableBatteryMapping && !config11.chargingStateEntity && !config11.roomEntities && !config11.disableLockPin && !config11.lockUsercodeService && config11.lockUsercodeSlot === void 0 && config11.lockPinMinLength === void 0 && config11.lockPinMaxLength === void 0 && !config11.powerEntity && !config11.energyEntity && !config11.voltageEntity && !config11.currentEntity && !config11.batteryPowerEntity && !config11.batteryEnergyEntity && !config11.chargingSwitchEntity && !config11.currentLimitEntity && !config11.pressureEntity && !config11.suctionLevelEntity && !config11.mopIntensityEntity && (!config11.customServiceAreas || config11.customServiceAreas.length === 0) && (!config11.customFanSpeedTags || Object.keys(config11.customFanSpeedTags).length === 0) && (!config11.fanWindPresets || (config11.fanWindPresets.natural?.length ?? 0) === 0 && (config11.fanWindPresets.sleep?.length ?? 0) === 0) && !config11.fanRestoreSpeedOnPowerOn && !config11.currentRoomEntity && !config11.cleanedAreaEntity && !config11.vacuumAscendingRoomOrder && !config11.disableCustomAreaRoomModes && !config11.valetudoIdentifier && !config11.coverSwapOpenClose && !config11.coverExposeAsDimmableLight && !config11.selectExposeAsSwitch && !config11.selectSwitchOnOption && !config11.selectSwitchOffOption && !config11.coverSliderDebounceMs && !config11.updateThrottleMs && !config11.disableClimateOnOff && !config11.disableClimateFanControl && !config11.climateKeepModeOnIdle && !config11.climateExposeFan && !config11.climateAutoMode && (!config11.composedEntities || config11.composedEntities.length === 0) && !config11.disableMomentaryFlip) {
139587
139591
  bridgeMap.delete(request.entityId);
139588
139592
  } else {
139589
139593
  bridgeMap.set(request.entityId, config11);
@@ -165087,6 +165091,23 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
165087
165091
  }
165088
165092
  }
165089
165093
  }
165094
+ /**
165095
+ * Snapshot the controller selection in the order the vacuum will actually
165096
+ * clean it. Roborock-class devices clean a batch of segments in ascending
165097
+ * id order and ignore the order they were dispatched in, so tracking has
165098
+ * to follow that instead of the tap order (#368).
165099
+ */
165100
+ orderSelectedAreas(selectedAreas) {
165101
+ const areas = [...selectedAreas];
165102
+ try {
165103
+ const mapping = this.agent.get(HomeAssistantEntityBehavior).state.mapping;
165104
+ if (mapping?.vacuumAscendingRoomOrder) {
165105
+ areas.sort((a, b) => a - b);
165106
+ }
165107
+ } catch {
165108
+ }
165109
+ return areas;
165110
+ }
165090
165111
  /** Read the cumulative cleaned-area sensor (m2), or null if not configured. */
165091
165112
  readCleanedAreaSqm() {
165092
165113
  try {
@@ -165276,7 +165297,10 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
165276
165297
  try {
165277
165298
  const serviceArea = this.agent.get(ServiceAreaBehavior);
165278
165299
  if (serviceArea.state.selectedAreas?.length > 0) {
165279
- s.activeAreas = [...serviceArea.state.selectedAreas];
165300
+ s.activeAreas = this.orderSelectedAreas(
165301
+ serviceArea.state.selectedAreas
165302
+ );
165303
+ serviceArea.state.selectedAreas = [...s.activeAreas];
165280
165304
  s.completedAreas.clear();
165281
165305
  s.lastCurrentArea = null;
165282
165306
  s.loggedShortCircuits.clear();
@@ -165315,7 +165339,10 @@ var RvcRunModeServerBase = class extends RvcRunModeServer {
165315
165339
  try {
165316
165340
  const serviceArea = this.agent.get(ServiceAreaBehavior);
165317
165341
  if (serviceArea.state.selectedAreas?.length > 0) {
165318
- s.activeAreas = [...serviceArea.state.selectedAreas];
165342
+ s.activeAreas = this.orderSelectedAreas(
165343
+ serviceArea.state.selectedAreas
165344
+ );
165345
+ serviceArea.state.selectedAreas = [...s.activeAreas];
165319
165346
  s.completedAreas.clear();
165320
165347
  s.lastCurrentArea = null;
165321
165348
  s.loggedShortCircuits.clear();
@@ -169994,7 +170021,16 @@ init_esm();
169994
170021
 
169995
170022
  // src/matter/behaviors/service-area-server.ts
169996
170023
  init_esm();
170024
+ init_home_assistant_entity_behavior();
169997
170025
  var logger234 = Logger.get("ServiceAreaServer");
170026
+ function orderSelection(areas, agent) {
170027
+ try {
170028
+ const ascending = agent?.get(HomeAssistantEntityBehavior).state.mapping?.vacuumAscendingRoomOrder === true;
170029
+ return ascending ? [...areas].sort((a, b) => a - b) : areas;
170030
+ } catch {
170031
+ return areas;
170032
+ }
170033
+ }
169998
170034
  var ServiceAreaWithProgress = ServiceAreaBehavior.with(
169999
170035
  ServiceArea3.Feature.ProgressReporting
170000
170036
  );
@@ -170004,7 +170040,7 @@ var ServiceAreaServerBase = class extends ServiceAreaWithProgress {
170004
170040
  logger234.info(
170005
170041
  `ServiceArea selectAreas called with: ${JSON.stringify(newAreas)}`
170006
170042
  );
170007
- const uniqueAreas = [...new Set(newAreas)];
170043
+ const uniqueAreas = orderSelection([...new Set(newAreas)], this.agent);
170008
170044
  const supportedAreaIds = this.state.supportedAreas.map((a) => a.areaId);
170009
170045
  const invalidAreas = uniqueAreas.filter(
170010
170046
  (id) => !supportedAreaIds.includes(id)
@@ -170063,7 +170099,7 @@ var ServiceAreaServerWithMapsBase = class extends ServiceAreaWithMapsAndProgress
170063
170099
  logger234.info(
170064
170100
  `ServiceArea selectAreas called with: ${JSON.stringify(newAreas)}`
170065
170101
  );
170066
- const uniqueAreas = [...new Set(newAreas)];
170102
+ const uniqueAreas = orderSelection([...new Set(newAreas)], this.agent);
170067
170103
  const supportedAreaIds = this.state.supportedAreas.map((a) => a.areaId);
170068
170104
  const invalidAreas = uniqueAreas.filter(
170069
170105
  (id) => !supportedAreaIds.includes(id)