Skip to content

fix(climate,cover): restore the setpoint with its knob offset; scope cover members settle from unknown (#791 / #795 follow-up) - #820

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-restore-no-double-offset-and-scope-cover-settle
Oct 26, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-restore-no-double-offset-and-scope-cover-settle

Conversation

@gdluck

@gdluck gdluck commented Oct 26, 2026

Copy link
Copy Markdown

Requested by A

Before: after a Home Assistant restart a climate zone whose knob offset is not zero showed the saved setpoint plus the offset a second time (23.5 saved, offset +2 → 25.5 shown) until the next dimension 12 / 14 report; a scope cover's open / close on the main bus left its timed member covers at unknown when the actuator stopped itself before travel_time, while the same member commanded directly settled to 100 %.

After: the restore path applies the #791 rule (the saved value is the setpoint in force, the nominal is that less the saved offset) and the members of a main-bus scope command settle on their travel timer exactly as a direct command does.

What is the problem

Two follow-ups on merged PRs of mine, found by an adversarial re-read of our PRs after @GreenGrassBlueOcean's #140 review on OWNd showed what our verification was missing. Both are code paths the live captures never exercised. @GreenGrassBlueOcean could you confirm the readings (or say if you disagree), especially the scope-cover one?

  1. fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 restore path (climate.py async_restore_last_state). Context: fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 made the slider show the setpoint in force (dimension 12, knob offset included) and stopped dimension 13 from adding the offset on top of it, gated by _local_target_reported. How it was found: reading the restore path against the live handlers instead of the PR's claim. The saved temperature (now always an offset-included value) was restored into _target_temperature, the nominal slot, with the flag False and the offset 0, and local_offset / local_target_temperature were saved but never read back; so the first *#4*2*13*02## after a restart ran the same nominal + offset line the live path had just stopped running on a dimension-12 value. A scratch replay showed 23.5 restored → 25.5 shown; the live path shows 23.5.
  2. fix(cover): a timed cover with no restored state starts unknown instead of "open 50 %" #795 scope cover members (cover.py / cover_scope.py). Context: fix(cover): a timed cover with no restored state starts unknown instead of "open 50 %" #795 made a timed cover with no position settle on travel_time + 2 s even when the actuator's own stop status comes earlier, and set _settles_open_close_from_unknown = False on the scope cover with the comment "the members settle their own runs". How it was found: following the one frame a main-bus area / group cover sends into its members. The gateway relays *2*1*1## to them, the direction branch of handle_event takes it as an external run (_start_position None, _end_run_from_unknown False), so an early stop at 5 s froze the position to None and the member stayed unknown until another event anchored it. The class comment only holds on the F422 fan-out path, where the members run their own async_open_cover; the release note and docs promised the settle for "the open / close buttons" of every timed cover.

Fixes nothing tracked in an issue; both are regressions of intent in #791 and #795.


What is the solution

  1. Restore with the offset (5d97429):
    • the saved local_offset is restored, the nominal becomes shown - offset, the shown value goes into the local-target slot; the flag stays False so a dimension 14 before the next dimension 12 behaves as on a fresh entity; a state without a usable offset is read as before.
  2. Members of a scope command settle (bf1d50a):
    • _async_move's state setup is now _begin_run, the settle of _async_run_to_end is _settle_run_to_end;
    • a scope open / close / set_cover_position 0 / 100 without F422 fan-out opens the run of every member that _runs_to_end_from_unknown before the single scope frame is queued, tracks the write on each (a lost frame aborts their runs as an own command's would) and settles them on their travel timer; the relayed scope frame is their motor-start echo (_handle_echo matches on direction, not WHERE);
    • known-position members keep timing from their position as for a direct command; a scope stop cancels the members' settles first, like a direct stop, so the relayed stop ends their run with the position still unknown instead of being taken for the actuator's early stop;
    • class comment, release note (2.0.0b16) and docs say so; the stale "(default 50)" test comment reads "(preset to 50)".

State Diagram

stateDiagram-v2
    direction LR
    state "Restart: saved temperature 23.5, local_offset 2" as Saved
    state "Before: nominal 23.5, offset 0" as Before
    state "After: nominal 21.5, local 23.5, offset 2" as After
    state "*#4*2*13*02## arrives" as Dim13
    Saved --> Before : old restore
    Saved --> After : this PR
    Before --> Dim13
    After --> Dim13
    Dim13 --> Shown25 : nominal + offset = 25.5 (wrong)
    Dim13 --> Shown23 : nominal + offset = 23.5 (as live)
    state "slider 25.5" as Shown25
    state "slider 23.5" as Shown23
Loading

Hardware Verification

  • Live reads on 2026-10-26 (F461 2.0.11 test plant, MyHomeServer1 3.87.13 read-only): every zone checked has *#4*Z*13*00## (zone 2 on the F461; zones 35, 43, 47, 60 on the MyHomeServer1), so the double-offset restore cannot be shown on these plants; the proof is the replay test below, which fails on the base and passes after.
  • The scope-cover path needs an area / group cover with F411/2 actuators timed shorter than travel_time; the test plant has a single cover (05), so N/A on hardware; the two scratch replays that found it are now the regression tests.

How is it tested

  • tests/test_climate_setpoint_in_force.py::test_restored_setpoint_in_force_does_not_get_the_offset_added_again, ::test_restored_setpoint_follows_the_live_rule_before_dimension_12 (fail on the base: 3 failures incl. a parametrised case; pass after), plus the no-offset / unreadable-offset cases (+7 tests).
  • tests/test_cover_unknown_start.py (+6 tests: scope open on the main bus with an early stop settles every unknown member, known-position members unchanged, a lost scope frame aborts the members' runs, a scope stop leaves them unknown, a refused frame; 5 of 6 fail on the base); tests/test_issue_433_cover_scope.py (settle cleanup in two existing tests).
  • pytest tests -n 4 --cov=custom_components/myhome: 5004 passed, 1 skipped, TOTAL 16845 0 100.0%; ruff check . clean; scripts/typing_ratchet.py mypy --strict: 0 error(s); scripts/verify_ha_standards.py SUCCESS; scripts/sync_documentation.py --check SUCCESS; scripts/manage_translations.py check no drift.
  • Kimi (independent diff review): climate arithmetic confirmed ("shown 23.5, offset +2 → nominal 21.5, local 23.5; the next dimension 13 recomputes 23.5"); cover ordering _begin_run → send → relayed echo → _track_write confirmed; its four remarks are all failure / cancellation windows where the scope path now behaves exactly as the direct-command path already does (re-arm of an already-settling member, a settle cancelled before a lost stop write, a CancelledError in the one-await queueing window, a stop racing a pending send); left as is and documented in the tests.

Where are the traces

  • tests/fixtures/traces/f461_live_2026-10-24/ (run run4_preset_templates, the dimension 12 / 13 / 14 frames the restore test replays); the cover change has no trace (no plant with an area cover and an early-stopping F411/2 in the corpus).

What does the Firmware Oracle / Encyclopedia say

  1. OpenWebNet Encyclopedia (WHO = 4, Dimensions 12 / 13 / 14; WHO = 2, WHAT 1 / 2 on an area / group WHERE):
    • dimension 12 is the setpoint in force, 14 the commanded one, 13 the local knob offset; a WHO 2 general / area / group command is relayed to the members as the same frame.
  2. Firmware Oracle:
    • N/A (no gateway behaviour changes; both fixes are in the integration's own state handling).

🤖 Generated with Claude Code

gdluck and others added 2 commits October 26, 2026 19:38
… the nominal (OpenWebNet-HA#791 follow-up)

The saved `temperature` is what the slider showed, which always carries the
knob offset: the setpoint in force (dimension 12, offset included) once the
zone has reported it, else the commanded setpoint plus the offset. The restore
path put that value into _target_temperature, the nominal slot, with the
offset still 0, so the first dimension 13 after a restart added the offset a
second time: 23.5 restored with offset +2, `*michnovka#4*2*13*02##` -> 25.5 shown until
the next dimension 12 report (the live path showed 23.5). The offset and the
local target were saved as attributes but never read back.

The restore now follows the live rule: the saved `local_offset` is restored,
the nominal is the shown value less that offset and the shown value goes into
the local-target slot. The flag stays False, so a dimension 14 before the next
dimension 12 shows the commanded setpoint plus the offset, as a fresh entity
does. A state saved without a usable offset is read as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ir travel timer too (OpenWebNet-HA#795 follow-up)

On the main bus (and for a general frame, which crosses an F422 natively) an
area / group cover sends one scope frame; the gateway relays it to the members,
which took it as an external run. A member with no position then froze to no
position when its F411/2 stopped itself before `travel_time`, and stayed
unknown until another event anchored it, while the same member commanded
directly settles on travel_time + 2 s. The class comment "the members settle
their own runs" held only for the F422 fan-out path, and the release note and
docs promised the settle for the open / close buttons of every timed cover.

The state setup of _async_move becomes _begin_run and the settle of
_async_run_to_end becomes _settle_run_to_end. A scope open / close /
set_cover_position 0 / 100 without fan-out opens the run of every member that
_runs_to_end_from_unknown before the frame is queued, tracks the write on each
(a lost frame aborts their runs as an own command's would) and settles them on
their travel timer; the relayed scope frame is their motor-start echo. Members
with a known position keep timing the run from it, as for a direct command. A
scope stop cancels the members' settles first, as a direct stop does, so the
relayed stop ends their run with the position still unknown instead of being
taken for the actuator's own early stop.

Docs and release note say so; the stale "(default 50)" comment in the restore
test says "(preset to 50)". Tests: early actuator stop after a scope open,
a known member next to an unknown one on a scope close, a scope frame refused
at once and one lost after queueing, set_cover_position 0 on the scope, a
scope stop during the settle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gdluck Confirmed on both readings — both are genuine gaps in #791 / #795 and both fixes are sound:

  1. Climate restore with local_offset (5d97429, #791 follow-up):

    • Confirmed: since #791 stores _local_target_temperature (which already includes local_offset) in state.attributes["temperature"], restoring it into self._target_temperature with _local_offset = 0 and _local_target_reported = False caused the first post-restart *#4*Z*13*02## to double-add the offset (23.5 + 2 = 25.5 °C) and caused an immediate async_set_temperature before Dim 13 arrived to subtract 0 instead of 2.
    • Restoring self._local_offset = int(float(last_state.attributes.get("local_offset") or 0)) (consistent with integer Dim 13 knob codes -3..+3 in ownkb:chunk:r000582 and f"{self._local_offset:+d}"), self._target_temperature = shown - self._local_offset, and self._local_target_temperature = shown with _local_target_reported = False matches the live state machine across Dim 13, Dim 14, Dim 12, negative offsets, and HVACMode.OFF.
  2. Main-bus scope cover members settling from unknown (bf1d50a, #795 follow-up):

    • Confirmed: without F422 interface fan-out, MyHOMEScopeCover sends a single scope frame (*2*1*1## / *2*2*0##), and relay_scope / relay_general previously routed that relayed frame to unknown-position timed members through the external-run branch (_start_position = None, _end_run_from_unknown = False, _stop_task = None), leaving early-stopping F411/2 actuators frozen at unknown instead of settling at travel_time + END_SETTLE_MARGIN.
    • Pre-opening _runs_to_end_from_unknown(opening) members via _begin_run(direction), aborting on send failure, arming _follow_scope_run_to_end(opening, written), and clearing _end_run_from_unknown / cancelling member _stop_tasks in async_stop_cover behaves cleanly across echo-assisted, non-echo, and user-stopped runs.
    • (Two minor non-blocking notes, both fine as-is: except Exception: in _async_run_members_to_end mirrors MyHOMECover._async_move, and known-position timed members on area.set_cover_position(0/100) keep pre-existing #702 behavior.)

We are merging #822 → #820 → #817 now, leaving a per-item review on #821, and will rebase/un-draft #816 once OpenWebNet-Encyclopedia#52 lands (own-firmware-oracle#65 is now merged).

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 5d3e6ed into OpenWebNet-HA:v2-phase1-architecture Oct 26, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants