@riddix/hamh 2.1.0-alpha.841 → 2.1.0-alpha.843

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.51 | 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,20 @@ 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.51)</strong> - Click to expand</summary>
65
65
 
66
- **New in v2.0.50:**
66
+ **New in v2.0.51:**
67
+
68
+ - 🐕 **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))
69
+ - 🧹 **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)
70
+ - 🚗 **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
71
+ - 🩺 **Subscription scope on the health card**: see whether each controller holds a whole-node wildcard or watches specific endpoints
72
+ - 📷 **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)
73
+ - 🪟 **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))
74
+ - 🤖 **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))
75
+ - 🌍 **Brazilian Portuguese completed**: 602 of 615 strings ([#420](https://github.com/RiDDiX/home-assistant-matter-hub/issues/420))
76
+
77
+ **Previously in v2.0.50:**
67
78
 
68
79
  - 🆔 **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
80
  - ⚡ **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 +266,7 @@ Re-assign the affected devices to their rooms after they reconnect. See the [doc
255
266
  <details>
256
267
  <summary><strong>🧪 Alpha Features (v2.1.0-alpha.x)</strong> - Click to expand</summary>
257
268
 
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.
269
+ **Alpha is currently level with Stable (v2.0.51).** All alpha work up to the latest pre-release has been promoted into v2.0.51. New alpha work continues from the next pre-release tag onward and will appear here as development progresses.
259
270
 
260
271
  </details>
261
272
 
@@ -136622,6 +136622,7 @@ function pluginApi(bridgeService, storageLocation) {
136622
136622
  const result = [];
136623
136623
  for (const bridge of bridgeService.bridges) {
136624
136624
  const info = bridge.pluginInfo;
136625
+ if (!info) continue;
136625
136626
  const plugins = info.metadata.map((meta) => ({
136626
136627
  name: meta.name,
136627
136628
  version: meta.version,
@@ -136644,41 +136645,41 @@ function pluginApi(bridgeService, storageLocation) {
136644
136645
  }
136645
136646
  res.json(result);
136646
136647
  });
136647
- router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136648
- const bridge = bridgeService.get(req.params.bridgeId);
136648
+ function pluginBridge(bridgeId, res) {
136649
+ const bridge = bridgeService.get(bridgeId);
136649
136650
  if (!bridge) {
136650
136651
  res.status(404).json({ error: "Bridge not found" });
136651
- return;
136652
+ return void 0;
136652
136653
  }
136654
+ if (typeof bridge.enablePlugin !== "function") {
136655
+ res.status(400).json({ error: "Bridge does not support plugins" });
136656
+ return void 0;
136657
+ }
136658
+ return bridge;
136659
+ }
136660
+ router.post("/:bridgeId/:pluginName/enable", (req, res) => {
136661
+ const bridge = pluginBridge(req.params.bridgeId, res);
136662
+ if (!bridge) return;
136653
136663
  const { pluginName } = req.params;
136654
136664
  bridge.enablePlugin(pluginName);
136655
136665
  res.json({ success: true, pluginName, enabled: true });
136656
136666
  });
136657
136667
  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
- }
136668
+ const bridge = pluginBridge(req.params.bridgeId, res);
136669
+ if (!bridge) return;
136663
136670
  const { pluginName } = req.params;
136664
136671
  bridge.disablePlugin(pluginName);
136665
136672
  res.json({ success: true, pluginName, enabled: false });
136666
136673
  });
136667
136674
  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
- }
136675
+ const bridge = pluginBridge(req.params.bridgeId, res);
136676
+ if (!bridge) return;
136673
136677
  const schema6 = bridge.getPluginConfigSchema(req.params.pluginName);
136674
136678
  res.json({ pluginName: req.params.pluginName, schema: schema6 ?? null });
136675
136679
  });
136676
136680
  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
- }
136681
+ const bridge = pluginBridge(req.params.bridgeId, res);
136682
+ if (!bridge) return;
136682
136683
  const { config: config11 } = req.body;
136683
136684
  if (!config11 || typeof config11 !== "object") {
136684
136685
  res.status(400).json({ error: "config object is required" });
@@ -136692,11 +136693,8 @@ function pluginApi(bridgeService, storageLocation) {
136692
136693
  res.json({ success: true, pluginName: req.params.pluginName });
136693
136694
  });
136694
136695
  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
- }
136696
+ const bridge = pluginBridge(req.params.bridgeId, res);
136697
+ if (!bridge) return;
136700
136698
  const { pluginName } = req.params;
136701
136699
  bridge.resetPlugin(pluginName);
136702
136700
  res.json({ success: true, pluginName, reset: true });
@@ -163276,6 +163274,15 @@ function clearPendingTilt(st) {
163276
163274
  st.pendingTilt = null;
163277
163275
  }
163278
163276
  var coverOptimistic = /* @__PURE__ */ new WeakMap();
163277
+ function haRestEnd(action) {
163278
+ if (action.action.includes("open_cover")) {
163279
+ return "open";
163280
+ }
163281
+ if (action.action.includes("close_cover")) {
163282
+ return "close";
163283
+ }
163284
+ return null;
163285
+ }
163279
163286
  function getCoverOptimistic(endpoint) {
163280
163287
  let st = coverOptimistic.get(endpoint);
163281
163288
  if (!st) {
@@ -163390,7 +163397,10 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163390
163397
  if (!entry) {
163391
163398
  return MovementStatus.Stopped;
163392
163399
  }
163393
- const reachedTarget = positionAware && current100ths != null && existingTarget != null && Math.abs(current100ths - existingTarget) < 100;
163400
+ const getExpected = config11.getExpectedRestPosition;
163401
+ const expectedPercent = entry.expectedRestEnd != null && getExpected != null ? getExpected(entry.expectedRestEnd, this.agent) : null;
163402
+ const goal = expectedPercent != null ? expectedPercent * 100 : existingTarget;
163403
+ const reachedTarget = positionAware && current100ths != null && goal != null && Math.abs(current100ths - goal) < 100;
163394
163404
  const expired = Date.now() - entry.startedAt >= optimisticMovementTimeoutMs;
163395
163405
  if (reachedTarget || expired) {
163396
163406
  clearOptimisticAxis(optimistic, axis);
@@ -163495,7 +163505,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163495
163505
  }
163496
163506
  // Record the optimistic direction and emit it now so a controller gets the
163497
163507
  // moving->stopped edge even when HA never reports a transitional state (#429).
163498
- startOptimisticMovement(axis, status3) {
163508
+ startOptimisticMovement(axis, status3, expectedRestEnd) {
163499
163509
  const optimistic = getCoverOptimistic(this.endpoint);
163500
163510
  clearOptimisticAxis(optimistic, axis);
163501
163511
  const endpoint = this.endpoint;
@@ -163540,7 +163550,7 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163540
163550
  })();
163541
163551
  }, optimisticMovementTimeoutMs);
163542
163552
  timer.unref?.();
163543
- optimistic[axis] = { status: status3, startedAt, timer };
163553
+ optimistic[axis] = { status: status3, startedAt, timer, expectedRestEnd };
163544
163554
  this.writeOperationalStatus({ [axis]: status3 });
163545
163555
  }
163546
163556
  async handleMovement(type, _, direction, targetPercent100ths) {
@@ -163599,17 +163609,25 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163599
163609
  }
163600
163610
  handleLiftOpen() {
163601
163611
  clearPendingLift(getCoverDebounce(this.endpoint));
163602
- this.startOptimisticMovement("lift", MovementStatus.Opening);
163603
163612
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163604
163613
  const action = this.state.config.openCoverLift(void 0, this.agent);
163614
+ this.startOptimisticMovement(
163615
+ "lift",
163616
+ MovementStatus.Opening,
163617
+ haRestEnd(action)
163618
+ );
163605
163619
  logger218.info(`handleLiftOpen: calling action=${action.action}`);
163606
163620
  homeAssistant.callAction(action);
163607
163621
  }
163608
163622
  handleLiftClose() {
163609
163623
  clearPendingLift(getCoverDebounce(this.endpoint));
163610
- this.startOptimisticMovement("lift", MovementStatus.Closing);
163611
163624
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163612
163625
  const action = this.state.config.closeCoverLift(void 0, this.agent);
163626
+ this.startOptimisticMovement(
163627
+ "lift",
163628
+ MovementStatus.Closing,
163629
+ haRestEnd(action)
163630
+ );
163613
163631
  logger218.info(`handleLiftClose: calling action=${action.action}`);
163614
163632
  homeAssistant.callAction(action);
163615
163633
  }
@@ -163620,14 +163638,15 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163620
163638
  return;
163621
163639
  }
163622
163640
  this.state.targetPositionLiftPercent100ths = targetPercent100ths;
163623
- this.startOptimisticMovement(
163624
- "lift",
163625
- currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing
163626
- );
163627
163641
  const targetPosition = targetPercent100ths / 100;
163628
163642
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163629
163643
  const action = config11.setLiftPosition(targetPosition, this.agent);
163630
163644
  const entityId = homeAssistant.entityId;
163645
+ this.startOptimisticMovement(
163646
+ "lift",
163647
+ currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing,
163648
+ haRestEnd(action)
163649
+ );
163631
163650
  const actions = this.env.get(HomeAssistantActions);
163632
163651
  const st = getCoverDebounce(this.endpoint);
163633
163652
  st.pendingLift = { action, entityId, actions };
@@ -163660,21 +163679,27 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163660
163679
  const st = getCoverDebounce(this.endpoint);
163661
163680
  clearPendingTilt(st);
163662
163681
  if (st.pendingLift?.action.action.includes("tilt")) clearPendingLift(st);
163663
- this.startOptimisticMovement("tilt", MovementStatus.Opening);
163664
- const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163665
- homeAssistant.callAction(
163666
- this.state.config.openCoverTilt(void 0, this.agent)
163682
+ const action = this.state.config.openCoverTilt(void 0, this.agent);
163683
+ this.startOptimisticMovement(
163684
+ "tilt",
163685
+ MovementStatus.Opening,
163686
+ haRestEnd(action)
163667
163687
  );
163688
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163689
+ homeAssistant.callAction(action);
163668
163690
  }
163669
163691
  handleTiltClose() {
163670
163692
  const st = getCoverDebounce(this.endpoint);
163671
163693
  clearPendingTilt(st);
163672
163694
  if (st.pendingLift?.action.action.includes("tilt")) clearPendingLift(st);
163673
- this.startOptimisticMovement("tilt", MovementStatus.Closing);
163674
- const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163675
- homeAssistant.callAction(
163676
- this.state.config.closeCoverTilt(void 0, this.agent)
163695
+ const action = this.state.config.closeCoverTilt(void 0, this.agent);
163696
+ this.startOptimisticMovement(
163697
+ "tilt",
163698
+ MovementStatus.Closing,
163699
+ haRestEnd(action)
163677
163700
  );
163701
+ const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163702
+ homeAssistant.callAction(action);
163678
163703
  }
163679
163704
  handleGoToTiltPosition(targetPercent100ths) {
163680
163705
  const config11 = this.state.config;
@@ -163683,14 +163708,15 @@ var WindowCoveringServerBase = class _WindowCoveringServerBase extends FeaturedB
163683
163708
  return;
163684
163709
  }
163685
163710
  this.state.targetPositionTiltPercent100ths = targetPercent100ths;
163686
- this.startOptimisticMovement(
163687
- "tilt",
163688
- currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing
163689
- );
163690
163711
  const targetPosition = targetPercent100ths / 100;
163691
163712
  const homeAssistant = this.agent.get(HomeAssistantEntityBehavior);
163692
163713
  const action = config11.setTiltPosition(targetPosition, this.agent);
163693
163714
  const entityId = homeAssistant.entityId;
163715
+ this.startOptimisticMovement(
163716
+ "tilt",
163717
+ currentPositionMatter != null && targetPercent100ths < currentPositionMatter ? MovementStatus.Opening : MovementStatus.Closing,
163718
+ haRestEnd(action)
163719
+ );
163694
163720
  const actions = this.env.get(HomeAssistantActions);
163695
163721
  const st = getCoverDebounce(this.endpoint);
163696
163722
  st.pendingTilt = { action, entityId, actions };
@@ -163832,6 +163858,7 @@ var adjustPositionForWriting2 = (position, agent) => {
163832
163858
  const matterSem = usesMatterSemantics(agent);
163833
163859
  return adjustPositionForWriting(position, featureFlags, matterSem);
163834
163860
  };
163861
+ var expectedRestPosition = (end, agent) => adjustPositionForReading2(end === "open" ? 100 : 0, agent);
163835
163862
  var shouldSwapOpenClose = (agent) => {
163836
163863
  const homeAssistant = agent.get(HomeAssistantEntityBehavior);
163837
163864
  const entitySwap = homeAssistant.state.mapping?.coverSwapOpenClose;
@@ -163919,11 +163946,11 @@ var config6 = {
163919
163946
  }
163920
163947
  return position == null ? null : adjustPositionForReading2(position, agent);
163921
163948
  },
163949
+ getExpectedRestPosition: expectedRestPosition,
163922
163950
  getCoverType: (entity) => deviceClassMapping(entity)?.type,
163923
163951
  getEndProductType: (entity) => deviceClassMapping(entity)?.endProductType,
163924
163952
  getMovementStatus: (entity, agent) => {
163925
- const { featureFlags } = agent.env.get(BridgeDataProvider);
163926
- const swapped = featureFlags?.coverSwapOpenClose === true;
163953
+ const swapped = shouldSwapOpenClose(agent);
163927
163954
  const coverState = entity.state;
163928
163955
  if (coverState === CoverDeviceState.opening) {
163929
163956
  return swapped ? WindowCovering3.MovementStatus.Closing : WindowCovering3.MovementStatus.Opening;