Skip to content

fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value - #791

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-setpoint-in-force
Oct 24, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-setpoint-in-force

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

A zone's displayed target temperature was overwritten by dimension 14 (the commanded setpoint) even when the thermostat had reported a different setpoint in force (dimension 12). On a live F461 2.0.11 plant the reply to *#4*2## carried *#4*2*12*0235*3## (23.5 in force) followed by *#4*2*14*0210*3## (21.0 commanded): the dimension 14 handler recomputed _local_target_temperature = target + offset, and the dimension 13 handler added the knob offset once more on top of a dimension 12 value that already includes it. The entity showed 21.0 while the thermostat regulated to 23.5. On this gateway dimension 12 follows every write (*#4*2*#14*0235*3## is ACKed in 78 ms and answered with *#4*2*12*0235*3##), so it is the value in force.

Refs #383 and #454 (dimension 12 while OFF is the protection setpoint; both test sets pass unchanged). No open issue is closed by this change.


What is the solution

  1. climate.py: a dimension 12 report holds the display (_local_target_reported):
    • the dimension 14 handler still stores the commanded value in _target_temperature, but no longer overwrites the displayed value while a dimension 12 report is known;
    • the dimension 13 handler no longer adds the knob offset on top of a dimension 12 report (the report already includes it).
  2. _show_nominal() (nominal + offset, clears the hold) replaces the recompute in four places: dimension 7 setpoint, season OFF→on, mode OFF→on and mode-with-setpoint. Leaving OFF therefore still drops a protection value seen while OFF ([BUG} Climate entities show 17 °C instead of the active 7 °C antifreeze target #383). The live reply order puts dimension 12 before the season frame, so when a zone is switched on at the thermostat the value reported while Home Assistant still saw it OFF is taken as protection and the commanded setpoint is shown until the running zone reports dimension 12 again (its next poll); a test pins that order.
  3. async_set_temperature clears the hold after writing *#4*Z*#14*T*3##: the write's echo decides what is shown next, dimension 12 where the gateway reports it (F461, MyHomeServer1), otherwise a dimension-14-only echo. Without this a gateway that echoes only dimension 14 after a write would keep showing the previous value in force.
  4. Known limit: with dimension 12 known, a dimension 14 change that is not the echo of our own write does not change the display until the next dimension 12 report. Both captured plants send dimension 12 after every write.
  5. Docs (docs/configuration/climate.md) and the b16 release-notes delta.

Companion PR: None required. Related: OpenWebNet-HA/OWNd#138 (draft) parses #-prefixed dimension 14 values; this PR does not depend on it.


State Diagram

stateDiagram-v2
    direction TB

    state "1. Before: the last frame wins" as Before {
        [*] --> InForce : *#4*2*12*0235*3##\nshow 23.5
        InForce --> Commanded : *#4*2*14*0210*3##\nshow 21.0 + offset
        Commanded --> Commanded : *#4*2*13*OO##\nadd the offset again
    }

    state "2. After: dimension 12 holds until the zone reports again" as After {
        [*] --> Nominal : *#4*2*14*T*3## with no dimension 12 known\nshow T + offset, reported = False
        Nominal --> InForce : *#4*2*12*T*3## , zone running\nshow T as is, reported = True
        InForce --> InForce : *#4*2*14*..## or *#4*2*13*..##\ndisplay unchanged, commanded value stored
        InForce --> Nominal : OFF to on , *4*1*2## after *4*102*2##\n_show_nominal, #383
        InForce --> Nominal : climate.set_temperature\n*#4*2*#14*0220*3## written, hold cleared
        Nominal --> InForce : echo *#4*2*12*0220*3##
        Nominal --> Nominal : echo *#4*2*14*0220*3## only\nshow 22.0 + offset
    }
Loading

Hardware Verification

  • F461 firmware 2.0.11, standalone zone 2 (Living Now KW4691 thermostat, F430/2 heating valve as actuator 1, F430/4 fan coil as actuator 2), 2026-10-23, read-only probe plus a local Home Assistant on the unpatched branch: *#4*2## answered in 1.3 s with *#4*2*0*0239##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0210*3##, *#4*2*13*00##; Home Assistant showed temperature: 21.0 while 23.5 was in force. climate.set_temperature 22.0 wrote *#4*2*#14*0220*3## and the bus monitor saw *#4*2*12*0220*3## then *4*1*2## 2 ms later. With this change the same plant shows 23.5 after start and 22.0 after the write.
  • MyHomeServer1 firmware 3.87.13 (*#13**16*3*87*13##, WHO 1013 model 67; the oracle emulates 2.82.06), 2026-10-24, status reads only: *#4*35## → *#4*35*0*0231##, *#4*35*12*0200*3##, *4*1*35##, *#4*35*14*0200*3##, *#4*35*13*00##. Same reply order as the F461 (dimension 12 before dimension 14), so the handler change applies to this gateway family too; dimension 12 and 14 were equal there (20.0), so this plant confirms the ordering only, not a differing setpoint.

Live retest (2026-10-24)

Consensus label: partial. On the F461 with this branch (combined build): climate.set_temperature 22 → tx *#4*2*#14*0220*3## → rx *#4*2*12*0220*3##, *4*1*2##, *#4*2*0*0241##; the poll *#4*2## → *#4*2*0*0241##, *#4*2*12*0220*3##, *4*1*2##, *#4*2*14*0220*3##, *#4*2*13*00##; the entity showed temperature: 22.0 and local_target_temperature: 22.0 within 0.14 s of the dimension 12 reply, and 23.5 at startup from *#4*2*12*0235*3##. The command, the ACK and the dimension-12-driven state are confirmed; the dimension 12 ≠ dimension 14 display case was not reproduced live on this build (one write sets both on this plant; it was seen on 2026-10-23 only) and rests on the unit test replaying that capture.


How is it tested

  • tests/test_climate_setpoint_in_force.py (12 cases):
    • test_live_f461_poll_reply_shows_the_setpoint_in_force: the captured reply order shows 23.5 and keeps 21.0 as the commanded value;
    • test_dimension_14_before_dimension_12_shows_the_setpoint_in_force, test_running_zone_keeps_dimension_12_in_either_order (×2), test_second_poll_does_not_flip_the_target;
    • test_offset_is_never_added_to_dimension_12: knob offset +2 with 23.5 in force stays 23.5;
    • test_without_dimension_12_the_commanded_setpoint_plus_offset_is_shown;
    • test_manual_write_echo_updates_the_setpoint_in_force: the F461 echo *#4*2*12*0220*3##, *4*1*2##;
    • test_set_temperature_writes_dimension_14_and_shows_the_echo (new): async_set_temperature(22.0) sends exactly *#4*2*#14*0220*3##, the hold is cleared, a dimension-14-only echo shows 22.0, the dimension 12 echo re-arms the hold;
    • test_dimension_12_just_before_leaving_off_is_treated_as_protection (new): OFF with 7.0, then dimension 12 (23.5) right before *4*1*2##: 21.0 is shown until the running zone reports dimension 12 again;
    • test_protection_then_back_on_shows_the_nominal_again ([BUG} Climate entities show 17 °C instead of the active 7 °C antifreeze target #383), test_nothing_is_shown_before_any_setpoint_is_known.
  • Fail-before / pass-after: 9 of the 12 cases fail on the unpatched base (the three that pass: 14 before 12, no dimension 12 at all, and the manual-write echo, where the last frame happens to be the right one); all 12 pass with the change. The set_temperature case also fails on the previous revision of this branch (the hold was not cleared by a write). test_issue_454_dim12_mode3.py, test_component_climate.py (antifreeze sweep) and test_issue_429_zone_state.py pass unchanged.
  • Full suite against the PyPI-pinned OWNd behaviour (plain upstream OWNd master, f04d8aa, on PYTHONPATH): 4467 passed, 1 skipped, 100.0 % statement coverage (15970 statements).
  • ruff check ., scripts/typing_ratchet.py (0 strict errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check: all pass.

Where are the traces

Trace PR: #799 (merged, ad8431ff).

Subsystem Capture Plant Frames verified by this PR
WHO 4 zone status reply order, dimension 12 ≠ 14 tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, runs run1_oracle_replay_readonly and run4_final_state F461 2.0.11, standalone zone 2 *#4*2## → *#4*2*0*0239##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0210*3##, *#4*2*13*00## (run 1, 1.3 s); run 4 shows 12 = 14 = 21.0 after the Home Assistant session
Setpoint write and its dimension 12 echo same archive, run3_climate_setpoint_restore, run4b_final_state_after_restore F461 2.0.11 *#4*2*#14*0235*3## ACK 78 ms, then *#4*2*12*0235*3## and *4*1*2##; two hours later 12 = 14 = 23.5
Write echo on the event session same archive, run7_localha_bus_monitor F461 2.0.11, local Home Assistant on the unpatched branch set_temperature 22 → *#4*2*12*0220*3##, *4*1*2## (the outgoing *#4*2*#14*0220*3## went on the command session and is not in the event-session archive; it is the frame the new test pins)
Reply order on a second gateway family tests/fixtures/traces/myhomeserver1_live_2026-10-24/myhome_trace_MyHomeServer1_live_2026-10-24.json, run run2_reads_thermo_sound_alarm_lights MyHomeServer1 3.87.13 *#4*35## → *#4*35*0*0231##, *#4*35*12*0200*3##, *4*1*35##, *#4*35*14*0200*3##, *#4*35*13*00## (ordering only: 12 = 14 = 20.0)

The frames are also inlined in tests/test_climate_setpoint_in_force.py.


What does the Firmware Oracle / Encyclopedia say

  1. OpenWebNet Encyclopedia (functional/who-4-temperature-control/dimensions.md):
    • DIMENSION 12 "complete probe status": the response *#4*WHERE*12*T*3## gives the setpoint after local offset; the trailing 3 is fixed in the published flow and the heating / conditioning / protection state comes in a separate *4*WHAT*WHERE## frame. The historical touchscreen client reconstructs its base setpoint by subtracting the local offset from T (offset 03 with T=0250 gives 0220).
    • DIMENSION 14 "setpoint temperature", read / write; DIMENSION 13 "local set offset", separate from the central target.
    • That is the split this PR implements: dimension 12 is shown as reported (offset included), dimension 14 is kept as the commanded value, dimension 13 is not added on top of dimension 12.
  2. Firmware Oracle: no oracle stimulus produces a dimension 12 ≠ dimension 14 pair (the emulated gateways have no thermostat behind them). The published results/<P>/<V>/oracle/full/trace-corpus.tsv rows *#4*1*12## and *#4*1*14## only show that both requests are forwarded to the bus on all twelve images (nack, out on eleven; ack, out on MH202 010024); the values come from the two live plants above.

Checklist

  • PR Description: Follows the structured sections above (What is the problem, What is the solution, State Diagram, How is it tested, Where are the traces, What does the Firmware Oracle / Encyclopedia say).
  • Trace-Backed (if protocol/device change): Behavior is verified against real OpenWebNet bus traces or gateway firmware fixtures rather than guessed.
  • V2 Architecture: New or modified entities implement handle_event() and do not manually subscribe via async_dispatcher_connect.
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

🤖 Generated with Claude Code

@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!

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
…penWebNet-HA#136-OpenWebNet-HA#142 and MyHOME OpenWebNet-HA#791-OpenWebNet-HA#798

The maintainer asked on OpenWebNet-HA/OWNd#137 (comment 6384484511) for the
physical trace behind the 2026-10-24 pull requests to be added to this repo
like every capture before. Same plant and format as f461_live_2026-10-15 and
f461_live_2026-10-18: a BTicino F461 2.0.11 (WHO 13 device type 200, WHO 1013
134.15.5.0) with BT-F411/4 lights at 06 / 07, an F422 light at 09 whose
actuator lacked configurator 2 at capture time, a BT-F411/2 5 s shutter at 05
and a KW4691 standalone thermostat at zone 2 with a BT-F430/2 valve and a
BT-F430/4 fan coil; no central unit.

tests/fixtures/traces/f461_live_2026-10-23/: the derived trace archive (299
event-session frames, 939 tx / rx / result rows in ten runs: the oracle replay
of 165 read-only requests, reviewer reads, setpoint restore, final state,
report-form writes, the event session during a local Home Assistant run, the
bus monitor of an earlier run, two flicker re-tries) and its README with the
plant, the runs, what the plant established and which pull request each frame
backs. MAC / IP are synthetic in both their text and WHO 13 byte forms.

tests/test_f461_live_2026_10_23_trace.py pins the facts the pull requests rely
on: the *michnovka#4*2## reply order with dimension 12 before and different from 14,
*michnovka#4*2*20## followed by two actuator frames and *michnovka#4*#0*20## answering both,
*#1*0007## NACK vs *#1*07## ACK vs *#1*7## empty, *OpenWebNet-HA#13**0## echoed on the
event session, every WHO 16 request refused in under 10 ms, the dimension 27
model name frame, *michnovka#4*#2*14## and the other central-unit forms refused, one
*2*0*0##, the shutter's own stop 5.1-5.9 s after each run; plus entity replays
through handle_event that pass on the base branch with the pinned OWNd.

tests/test_trace_oracle_validation.py: tier 4 tolerates the authentic
*OpenWebNet-HA#13**27*70#52#54#49## reply until the pinned OWNd carries the dimension 27
decoder (OWNd#139); it parses to None without raising. The *4*311*2## NACK /
*4*311*#2## ACK pair cited by OWNd#137 is already in f461_live_2026-10-15 and
is referenced, not duplicated. scripts/update_trace_matrix.py leaves the
matrix unchanged (the F461 row already covers WHO 1, 2, 4, 13, 1013).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit that referenced this pull request Oct 24, 2026
test(trace): ingest live F461 2.0.11 2026-10-23 capture and MyHomeServer1 3.87.13 reads behind OWNd #136-#142 and MyHOME #791-#798
@gdluck
gdluck force-pushed the fix/climate-setpoint-in-force branch from 9ac02da to d268c0c Compare October 24, 2026 15:54
…on 14 as the commanded value

The dimension 14 handler recomputed the displayed target from the commanded
setpoint plus the knob offset, overwriting a dimension 12 report received in
the same poll. On a live F461 2.0.11 the reply to *michnovka#4*2## is *michnovka#4*2*0*0239##,
*michnovka#4*2*12*0235*3##, *4*1*2##, *michnovka#4*2*14*0210*3##, *michnovka#4*2*13*00##: Home Assistant
showed 21.0 while the thermostat regulated to 23.5. Dimension 12 follows every
write (*michnovka#4*2*OpenWebNet-HA#14*0220*3## is answered with *michnovka#4*2*12*0220*3##).

A dimension 12 report now stays what is shown (flag _local_target_reported);
dimension 14 is kept as the commanded value and dimension 13 no longer adds
the offset on top of a dimension 12 report. _show_nominal() restores the
nominal plus offset when the zone leaves OFF or a frame carries a setpoint of
its own, so a protection value seen while OFF is still dropped (OpenWebNet-HA#383, OpenWebNet-HA#454).

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

@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.

Code review / audit for #791

Summary

The direction is correct and the protocol grounding is solid: dimension 12 is the setpoint after local offset and should be what the entity shows when present; dimension 14 stays the commanded value. The _local_target_reported hold, the clear-on-write, and the _show_nominal() path for leaving OFF look like the right shape, and the new tests exercise the captured F461 frame order, both arrival orders, offset double-counting, protection transitions (#383), and the write-echo path.

The verification for the motivating case (dim12 ≠ dim14) is only partial, which is the main blocker for me.

Verification gap

The unequal case that triggered the change was observed on 2026-10-23 on the unpatched branch (*#4*2*12*0235*3## then *#4*2*14*0210*3##). The 2026-10-24 live retest on this branch is labelled partial in the PR itself: the write path and the equal 12=14 case were confirmed, but the dim12 ≠ dim14 display case was not reproduced live with the patched code. It rests entirely on the unit test replaying the old capture.

The Firmware Oracle produces no dim12 ≠ dim14 stimulus, and the MyHomeServer1 capture only confirms reply order (values were equal). For a climate setpoint change that touches protection-window logic and display hold, that is thinner than the bar we have been applying.

Specific points

  1. Live confirmation of the unequal case
    Please reproduce dim12 ≠ dim14 on the F461 (or another plant) while running this branch and confirm the entity shows the dimension-12 value. A short bus-monitor + entity-state snippet is enough. Without that, the core behavioural claim is only unit-test-replayed.

  2. Known limit
    The stated limit (“once dim12 is known, a dim14 that is not the echo of our own write does not update the display until the next dim12”) is acceptable if both captured plants always emit dim12 after a write, but it should be called out more visibly in the docs / release note (not only in the PR body). If we ever see a gateway that echoes only dim14 for an external change, the display will be stale until the next poll.

  3. Write-clear interaction
    Clearing _local_target_reported inside async_set_temperature is the right fix so a dim14-only echo is not ignored. The new test covers it; just make sure future changes that touch the hold flag also cover the “write then dim14-only echo” path.

  4. Housekeeping
    Draft + dirty mergeable state. Needs a rebase and to be marked ready once the live confirmation is attached.

Recommendation

Request changes until the unequal dim12/dim14 case is shown live on this branch (or equivalent additional independent evidence is added). The implementation and unit tests look good; the missing piece is the live proof of the exact condition the change is meant to fix.

@gdluck
gdluck force-pushed the fix/climate-setpoint-in-force branch from d268c0c to 4566943 Compare October 24, 2026 18:01
…itations and the release note

Review follow-up on OpenWebNet-HA#791: the limit (a commanded setpoint that arrives without a
following dimension 12 does not move the slider once the zone has reported a
setpoint in force) was only in the PR body. It is now a row in the WHO 4 section
of docs/configuration/known_limitations.md and a sentence in the b16 release note,
with the two captured plants that always emit dimension 12 named.

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

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Thanks for the audit. Points 2 and 3 are done; point 1 was attempted live today and I want to be exact about what it did and did not show.

Point 1 (live dim 12 ≠ dim 14 on this branch). Local Home Assistant on this branch head, pinned OWNd 2.0.0b11, F461 2.0.11, KW4691 zone 2, second event-session listener recording. Baseline: *#4*2*12*0235*3##, *#4*2*14*0235*3##, entity 23.5.

  • Knob turned at the thermostat (18:07 UTC): the bus carried only dimension 12, *#4*2*12*0290*3## then *#4*2*12*0180*3## (29.0 while turning, 18.0 final), season and temperature frames, no dimension 14 frame. The entity followed the dimension-12 frame within 60 ms (temperature: 18.0, local_target_temperature: 18.0, last_reported 18:07:57.891). That is the dimension-12-drives-the-display path, but it does not discriminate from the previous code, which also had no dimension 14 to overwrite it with.
  • Reading dimension 14 fifty seconds later: *#4*2*14*0180*3##. On this thermostat the commanded value follows a local change, so by the next poll both are equal again. A 0.6 s alternating poll of dimensions 12 and 14 over the following minutes never caught them apart.
  • Write from Home Assistant (set_temperature 21.0): *#4*2*12*0210*3## and *#4*2*14*0210*3## within the same second; equal again.
  • Second plant, MyHomeServer1 3.87.13: all 36 zones read, dimension 12 equals dimension 14 on every one.

So I could not produce the unequal state today on either plant. The only physical record of it remains the 2026-10-23 capture in the merged f461_live_2026-10-23 fixture (run 1, 20:43 UTC: *#4*2*0*0239##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0210*3##, *#4*2*13*00##), when the previous code showed 21.0 and the thermostat regulated to 23.5; the 21.0 had been written from Home Assistant on an earlier day and the 23.5 was set at the thermostat afterwards, and the two stayed apart until our own write realigned them. Which local operation leaves dimension 14 behind for days while today's knob turn updates it within a minute is something I cannot tell from the captures; I would rather say that than guess. tests/test_climate_setpoint_in_force.py replays exactly that fixture sequence and asserts the entity shows 23.5. If that physical-trace replay is not enough, I am happy to keep the PR in draft with a monitor on the plant until the state recurs naturally, or to run any other sequence you suggest.

Point 2. Commit abf6330 adds the limit as a row in the WHO 4 section of docs/configuration/known_limitations.md and a sentence in the b16 release note, naming the two captured plants that always emit dimension 12.

Point 3. Agreed; the "write then dimension-14-only echo" path is pinned by test_set_temperature_writes_dimension_14_and_shows_the_echo.

Point 4. Rebased on the current base (mergeable). It stays draft for now because the un-drafting is being done in the phases announced on #795; say the word and I flip it.

@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.

Audit notes
Logic is correct and fail-soft. The diagnostic path cannot raise into the outer generic ERROR.
MAC normalization is consistent with the existing matching loop that already uses dr.format_mac.
Disabled-entry behavior is intentional and now documented; it differs from the top-level-platforms path but is reasonable (the entry still exists and owns its section).
Logging is precise, actionable, and does not spam multi-gateway setups.
No protocol, entity, or runtime behavior changes outside the missing-MAC diagnostic.
Style, typing, and HA standards checks pass.
Minor: the broad except Exception is appropriate for a diagnostic-only helper.

@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review October 24, 2026 18:34
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 27dd8e3 into OpenWebNet-HA:v2-phase1-architecture Oct 24, 2026
17 checks passed
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

Thanks for running the live experiments on both plants and reporting the exact results transparently — keeping this in draft until we have a causal before -> trigger -> after physical trace is definitely the right call.

1. How to trigger DIMENSION 12 != DIMENSION 14 on the F461 + KW4691

Looking at why 2026-10-23 had 12 = 23.5 °C vs. 14 = 21.0 °C (13 = 00) for days while 2026-10-24's physical +/- press updated both 12 and 14 within a minute:

  • Both run3_climate_setpoint_restore (*#4*2*#14*0235*3##) at the end of 2026-10-23 and climate.set_temperature (*#4*2*#14*0210*3##) on 2026-10-24 wrote DIMENSION 14 (M=3), which puts the KW4691 into Manual override. While the thermostat is already in Manual override, pressing +/- on the bezel edits that same active manual setpoint, so the KW4691 updates DIMENSION 14 alongside DIMENSION 12.
  • By contrast, when the KW4691 transitions out of Manual into an Automatic / Weekly Schedule time slot (e.g. scheduled 23.5 °C), Comfort / Eco preset, or Boost — either via Legrand Home + Control, the mode toggle on the KW4691 itself, or a scheduled slot boundary — DIMENSION 12 reflects the active program/preset setpoint in force (23.5 °C) with DIMENSION 13 = 00, while DIMENSION 14 retains the last OpenWebNet manual setpoint (21.0 °C).

Could you try capturing one continuous session (monitor listener + command polls) on zone 2 with this sequence?

  1. Baseline (before): Start the monitor listener, send climate.set_temperature (21.0 °C, *#4*2*#14*0210*3##), and poll *#4*2## to confirm 12 = 21.0, 14 = 21.0, 13 = 00 in Manual mode.
  2. Trigger divergence: Without writing DIMENSION 14 from Home Assistant, switch the KW4691 from Manual back to Automatic / Weekly Schedule (on a slot whose target differs from 21.0 °C, e.g. 23.5 °C), or toggle Comfort / Eco / Boost via Home + Control or the KW4691 mode button. (If you also want to test the pure OpenWebNet preset path afterwards, DIMENSION 7 template activation *#4*2*#7*1*3## / *#4*2*#7*2*3## activates Comfort / Eco without writing DIMENSION 14.)
  3. Verify divergence (after): Record all spontaneous monitor frames emitted during the transition, then poll *#4*2## ~60 s later to confirm 12 != 14 (12 = <schedule/preset>, 14 = 21.0, 13 = 00) persists across polls.
  4. Re-convergence check: While 12 != 14 is active, tap +0.5 °C once on the KW4691 (wait ~60 s and poll *#4*2##), and then call climate.set_temperature from Home Assistant so we have on-wire proof of what moves 12 vs. 14 in both directions.

2. Why we need that transition trace before latching _local_target_reported (Point 2)

In commit abf6330f, the new docs/configuration/known_limitations.md entry notes that if an external change emits DIMENSION 14 without DIMENSION 12, the slider stays on the previous value and suggests calling homeassistant.update_entity to poll *#4*Z## (and asks for a trace if seen).

Checking our existing trace corpus shows that central-unit plants (MH200N, F453AV) emit spontaneous DIMENSION 12 frames on the monitor session — arming _local_target_reported = True — and then later emit standalone DIMENSION 14 (and DIMENSION 13) updates with no companion DIMENSION 12. Because MyHOMEClimate.async_update() skips *#4*Z## when not self._gateway_supports_zone_status (gateway.py lines 328–345, climate.py lines 526–554), homeassistant.update_entity cannot refresh DIMENSION 12 to clear a stale latch either:

  • MH200N single-capture regression (tests/fixtures/traces/issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json): With no *#4*2## poll sent, zone 2 spontaneously emits rx *#4*2*12*0105*3## (10.5 °C, frame 161), and 9.7 s / 20.6 s later updates to 10.0 °C via rx *#4*2*14*0100*3## + *#4*2*13*00## + *4*110*#2## (frames 168–170 and 182–191) with zero DIMENSION 12 frames. Current base ends at 10.0 °C; with _local_target_reported = True, zone 2 stays latched at 10.5 °C. (In the same trace, frames 106–130 also show physical ±3 °C dial turns on zone 4 emitting *#4*4*13*00..03..13..11## with zero DIMENSION 12 frames.)
  • F453AV central-mode regression (tests/fixtures/traces/issue_619/): Across sequential passive monitor captures (tx = 0) on the same plant on 2026-10-11, zones 1..8, 10..14, 17 spontaneously emit *#4*Z*12*...*3## (10-29-57.json and 10-49-26.Central.to.OFF.json), and when Daily Holiday is activated (11-02-55.Holiday.ON.json, *4*115#1101*#0##), the bus emits 23 *#4*Z*14*0100*3## (10.0 °C) frames and zero DIMENSION 12 frames — leaving any zone latched at 22.0/23.0 °C from 10:49:26 stuck at 22.0/23.0 °C throughout Holiday mode.

Once we have the causal F461 + KW4691 divergence trace alongside those MH200N and F453AV fixtures, we can see the exact frame sequence on both plant architectures and design the transition rule so KW4691 shows the active setpoint in force without latching central-unit zones on stale DIMENSION 12 values.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Follow-up on point 1: the unequal case is now reproduced live on this branch, with the help of a second opinion that pointed at the protection modes as the gateway-triggerable way to split the two dimensions.

Setup unchanged: local Home Assistant on head abf6330, pinned OWNd 2.0.0b11, F461 2.0.11, KW4691 zone 2, second event-session listener. Baseline *#4*2*12*0235*3## / *#4*2*14*0235*3##, entity heat, 23.5.

Trigger (18:58 UTC): *4*102*2## (antifreeze) → ACK 0.08 s; event session: *4*102*2##, *#4*2*12*0070*3##, *#4*2*0*0239##. Reads afterwards: *#4*2*12## → *#4*2*12*0070*3##; *#4*2*14## → *#4*2*14*0235*3##. So dimension 12 = 7.0 (in force) while dimension 14 = 23.5 (commanded), offset *#4*2*13*00##.

Home Assistant's own poll of the zone (18:58:46, homeassistant.update_entity), bus monitor:

tx *#4*2##
rx *#4*2*0*0239##
rx *#4*2*12*0070*3##
rx *4*102*2##
rx *#4*2*14*0235*3##
rx *#4*2*13*00##

Entity after that reply: climate.thermo_ofice state off, temperature: 7.0, local_target_temperature: 7.0, hvac_action: off. The dimension-14 frame that follows dimension 12 in the same reply did not overwrite the display (the previous code would have shown 23.5 here).

Restore (18:59): *#4*2*#14*0235*3## → ACK, event session *4*1*2##, *#4*2*12*0235*3##; reads 12 = 14 = 0235; entity heat, 23.5 within the same second (the OFF → heating path with dimension 12 arriving before the season frame, i.e. the second new test). Plant left as found.

Raw rows (protection_test.jsonl, restore_after_102.jsonl, HA bus-monitor dump) can go into the f461_live_2026-10-23 fixture as an extra run if you want them in the corpus; say so and I add a trace PR. Also noted for the record: a knob turn at this thermostat emits only dimension 12 and dimension 14 follows within a minute, and a gateway write realigns both at once, so the split is produced here by the protection modes (and presumably by the thermostat's own program), not by the knob.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Here is the one-session before → trigger → after → re-convergence capture you asked for (section 1), recorded 19:02–19:12 UTC on the F461 2.0.11 / KW4691 zone 2 with the event-session listener running throughout and Home Assistant on the merged head; the thermostat was in Manual after the earlier writes.

Before. climate.set_temperature 21.0 → *#4*2*#14*0210*3##; poll *#4*2## → 0*0239, 12*0210*3, *4*1*2##, 14*0210*3, 13*00; entity heat 21.0.

Trigger attempts without a dimension-14 write.

  • Comfort template *#4*2*#7*1*3##: ACK; event session *#4*2*7*1*3##, *#4*2*12*0210*3##, *4*108*2## (the Comfort value here is 21.0, no split).
  • Eco template *#4*2*#7*2*3##: ACK; the KW4691 took it as cooling comfort: *#4*2*7*2*3##, *#4*2*12*0250*3##, *4*208*2##, *4*0*2##, then *4*1*2## and *#4*2*12*0225*3##; 60 s later 12 = 14 = 0225 (dimension 14 followed the preset within the minute, as it followed the bezel earlier). Side observation for a separate issue: the entity's hvac_mode stayed cool after *4*1*2## (season heating) and after a poll.
  • Antifreeze *4*102*2##: ACK; *#4*2*12*0070*3## while dimension 14 keeps the manual value. This is the only operation on this unit that leaves the two apart for as long as the mode lasts.

After (split). Poll at 19:10:54: 0*0239, 12*0070*3, *4*102*2##, 14*0240*3, 13*00 (dimension 14 was 24.0 at that point because of a bezel +0.5 taken in Manual just before, see below). Entity off, temperature 7.0: dimension 12 shown, the dimension 14 that follows in the same reply does not overwrite it.

Re-convergence, both directions.

  • Bezel +0.5 in Manual (19:10:42): *#4*2*12*0240*3##, *4*1*2##; dimension 14 read 0240 in the next poll. A bezel press in Manual moves both.
  • Bezel +0.5 while in antifreeze (19:11:10): the thermostat leaves protection into Manual at the protection value plus the step: *4*1*2##, *#4*2*12*0075*3##; reads: 12 = 0075, 14 = 0075. Entity heat 7.5. Again both move; dimension 14 follows the local value.
  • climate.set_temperature 23.5 from Home Assistant while split (antifreeze re-entered, 12 = 0070, 14 = 0075): *#4*2*#14*0235*3## → poll 12 = 14 = 0235, *4*1*2##; entity heat 23.5. A Home Assistant write moves both and ends protection.

So on this thermostat the split is produced by the protection modes (and, per your explanation, by the schedule/preset paths on units whose preset value differs and that do not realign dimension 14); every local or remote setpoint change realigns dimension 14 to dimension 12 within a minute. I am preparing the whole session as a trace fixture PR (f461_live_2026-10-24, runs: knob, protection split, presets, bezel in protection, HA write) so it sits next to the other captures; the MH200N / F453AV concern of your section 2 is being handled in a follow-up draft PR with regression tests built from those two fixtures.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

Section 2 (central-unit plants latching on a stale dimension 12) is addressed in draft #809 with regression tests built from the two fixtures you named. Summary, details in the PR body:

  • Replaying issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json zone 2 (no *#4*2## sent): on the merged head the entity stays at 10.5 after frames 168–191 instead of following *#4*2*14*0100*3##; the test pins 10.0.
  • Replaying issue_619 F453AV: the Central.to.OFF → Holiday.ON path you described does not get stuck on the merged head (those zones go OFF via *4*303*Z## and the Holiday *4*111*#Z## runs the OFF → heat path, which clears the hold; all end at 10.0 on both revisions). The path that does stick is 10-29-57 sweep → Holiday on zone 14 (12 = 0080 ≠ 14 = 0100 in the sweep, Holiday sends *#4*14*14*0100*3## and no dimension 12): 8.0 on the merged head, 10.0 on base. That is the second regression test.
  • Rule chosen after trying three: the hold is armed only where a *#4*Z## poll can refresh it (_gateway_supports_zone_status, i.e. F461 / MyHomeServer1); SCS central-unit gateways (F454, F455, MH200N, MH201, MH202, …) keep the b15 behaviour (commanded setpoint plus offset). A time window was rejected with numbers from the corpus (same-zone 12 → 14 gaps of 1.1–14 s on the F453AV sweep vs ≤ 0.72 s inside one reply on pollable gateways), not self._central and has_central_unit do not separate the cases at all. Known Limitations, climate.md and the b16 release note are reworded accordingly.
  • Full suite 4507 passed, 100 % coverage; 5 of the 9 new cases fail on the merged head, both regressions pass on the pre-fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 base.

A per-zone "report epoch" rule (a mode/program transition invalidates the hold) is the better long-term shape, but it needs the transition captures on both plant types; the F461 + KW4691 session above is the first half of that evidence.

@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

The session above is now a trace-fixture draft, #810 (f461_live_2026-10-24, five runs: knob turn, fast dimension 12/14 poll, antifreeze split and restore, preset templates, bezel in Manual and in antifreeze followed by the HA write; 14 replay tests, synthetic identity, 4513 passed). It stays draft until you say whether the Automatic/Weekly-schedule and Home + Control transitions should be captured as well; those were not in today's session (the dimension 7 templates were used instead).

@gdluck

gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown
Author

Follow-up capture with the DIMENSION 7 frames you named, from the 23.5 baseline (20:08–20:11 UTC, same setup, the same Home Assistant instance as the earlier runs, still on the branch head abf6330 that was merged, event listener recording). This one produces the persistent split on the F461 + KW4691 exactly as your explanation predicts:

Step Written Zone echoes *#4*2*12## *#4*2*14##
baseline — *#4*2## → 0*0238, 12*0235*3, *4*1*2##, 14*0235*3, 13*00 23.5 23.5
Heating Comfort *#4*2*#7*1*3## (ACK 68 ms) *#4*2*7*1*3##, *#4*2*12*0210*3##, *4*108*2## 21.0 (+1 s and +13 s) 23.5
Heating Eco *#4*2*#7*1*4## (ACK 89 ms) *#4*2*7*1*4##, *#4*2*12*0180*3##, *4*107*2## 18.0 (+1 s, +13 s, +48 s) 23.5
zone poll in Eco *#4*2## 0*0238, 12*0180*3, *4*107*2##, 14*0235*3, 13*00
HA write climate.set_temperature 23.5 → *#4*2*#14*0235*3## *4*1*2##, 12*0235*3; poll 12 = 14 = 0235 23.5 23.5

So a preset changes dimension 12 and leaves dimension 14 at the last manual value for as long as the preset is active (≥ 48 s here, no realignment), while a manual write realigns both. Home Assistant received the probe's dimension 12 and 14 reads on its event session (12 = 0180 then 14 = 0235; HA sent no poll of its own in that window, correction below), and the entity showed temperature: 18.0, hvac_mode: heat, hvac_action: idle (the dimension 14 that followed did not overwrite it). The earlier "Eco" attempt was *#4*2*#7*2*3##, Cooling Comfort, as you pointed out; it flipped the season and realigned, which is why it did not split.

The zone echoes a mode frame for each preset that OWNd does not decode yet: *4*108*2## (heating comfort), *4*107*2## (heating eco), *4*208*2## (cooling comfort) → OpenWebNet-HA/OWNd#146. The hvac_mode stuck on cool after the cooling-comfort echo → #811. This capture will be added as a run to the trace fixture once #810 has landed (or as a small follow-up trace PR, whichever you prefer).

[Edit: two corrections while turning this into the trace fixture (#812): Home Assistant issued no *#4*2## poll during the Eco window (the *#4*2*60*40## at 20:10:22 was a humidity push; the 12 / 14 values HA saw came from the probe's reads on its event session), and the HA instance was the one started on the #791 branch head, not restarted on the merged base. The third Eco read pair is at +47.8 / +49.0 s, not +48 s. The capture itself is unchanged and is now #812.]

GreenGrassBlueOcean pushed a commit that referenced this pull request Oct 24, 2026
…ure (knob, antifreeze split, presets, bezel, HA write)

The before / trigger / after capture asked for on #791 (comment 6388207693):
what moves WHO 4 dimension 12 (the setpoint in force) and dimension 14 (the
last commanded value) on the Living Now KW4691 standalone zone 2 behind the
physical F461 2.0.11, as a derived_trace_archive with a README and a replay
test module. Five runs: a bezel turn, 480 alternating dimension 12 / 14 reads
0.6 s apart across two Home Assistant writes, the antifreeze command
*4*102*2## and the restore write, the dimension 7 templates *#4*2*#7*1*3## /
*#4*2*#7*2*3##, and bezel presses in Manual and in antifreeze followed by a
Home Assistant write while 12 != 14.

What the plant said: a bezel turn emits dimension 12 (and the season frame)
only, the first dimension 14 read 49 s later already carries the value; a
dimension 14 write echoes dimension 12, never dimension 14, and both read the
new value within a poll cycle; antifreeze is the only operation that left 12
(7.0, with the mode frame *4*102*2##) apart from 14; a bezel press in
antifreeze leaves it at the protection value plus one step on dimension 12
with 14 following; a write realigns both. The dimension 7 templates are
echoed as heating / cooling comfort with the mode frames *4*108*2## /
*4*208*2##; the zone then flipped season twice and settled at 22.5 on both
dimensions without a write.

Two event-only listeners covered the whole capture; each run takes its
frames from the listener and the probe's command rows are aligned to the
listener clock through the frames both sessions received (the probe's own
event log matched the listener exactly over every request window). Home
Assistant's own requests (its polls and writes) are listed in
capture.home_assistant_requests from its debug log, which is not included.
Synthetic MAC / IP as in f461_live_2026-10-23; no WHO 13 identity frame was
exchanged.

tests/test_trace_oracle_validation.py: the two dimension 7 template
requests join PHYSICAL_ONLY_TX_FRAMES (no oracle stimulus for them).

Refs #791

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 24, 2026
…michnovka#4*Z##

OpenWebNet-HA#791 made a zone's dimension 12 report (the setpoint in force, knob offset
included) hold the displayed target against later dimension 14 and 13 frames.
Central-unit plants behind an SCS gateway report dimension 12 once and then
update a zone with dimension 14 and 13 alone, and async_update never polls
*michnovka#4*Z## there (OpenWebNet-HA#674), so the zone stayed on the stale value:

- MH200N sweep (tests/fixtures/traces/issue_466/myhome_sweep_MH200N_all_
  2026-09-25T14-40-10.json, frames 159-191, no *michnovka#4*2## sent): zone 2 reports
  *michnovka#4*2*12*0105*3## spontaneously, then *michnovka#4*2*14*0100*3## + *michnovka#4*2*13*00## +
  *4*110*#2## 9.7 s and 20.6 s later with no dimension 12; the entity stayed
  at 10.5 instead of 10.0.
- F453AV (tests/fixtures/traces/issue_619/): the 10:29:57 sweep reports zone
  14 with 12 = 8.0 then 14 = 10.0; Daily Holiday (11-02-55.Holiday.ON) emits
  23 dimension 14 frames and no dimension 12; the entity stayed at 8.0.

The hold is now armed only when the gateway answers the per-zone status
request (F461, MyHomeServer1), where every poll and every write echo repeats
dimension 12 and homeassistant.update_entity can refresh it. On SCS gateways
the slider shows the commanded setpoint plus the knob offset, as before b16.

A time window between dimension 12 and 14 was rejected: the F453AV sweep
sends all dimension 12 frames first and all dimension 14 frames afterwards
(1.1 s to 14 s apart for one zone, the 14 repeated 1.3 s later), which
overlaps the 9.7 s of the MH200N update, and the F461 answers separate
dimension reads 2.6 s apart. Gating on the zone's central flag changes
nothing: zones of a central-unit plant are not flagged central.

tests/test_climate_setpoint_latch_central_plants.py replays both captures
(2 of 9 cases fail on d8bac15, both pass on the pre-OpenWebNet-HA#791 base) and pins the
F461 live poll reply, write echo (run1, run3 of f461_live_2026-10-23) and the
2026-10-24 antifreeze split on a pollable gateway.

Refs OpenWebNet-HA#791, OpenWebNet-HA#674, OpenWebNet-HA#383, OpenWebNet-HA#454.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean pushed a commit that referenced this pull request Oct 25, 2026
…ure (dimension 12 split from 14) (#812)

* test(trace): ingest the F461 2.0.11 heating comfort / eco preset capture (dimension 12 split from 14)

Follow-up asked for in the review of #810: from a 23.5 baseline on the
physical F461 2.0.11 + Living Now KW4691 (zone 2, heating season), the
dimension 7 templates *#4*2*#7*1*3## (heating comfort) and *#4*2*#7*1*4##
(heating eco), each followed at once by reads of dimension 12 and 14, a
*#4*2## poll and reads 48 s into eco, then Home Assistant's
climate.set_temperature 23.5 while the two dimensions differed
(2026-10-24 20:08-20:12 UTC; results posted on #791).

What the plant said: a heating preset moves dimension 12 (21.0, then 18.0)
and leaves dimension 14 at the last commanded value (23.5) for as long as
it was observed (48 s); the mode frame *4*108*2## / *4*107*2## takes the
season frame's place in the dimension 12 and *#4*2## replies; the write is
echoed as *4*1*2##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*0*0238## and the
poll 7.4 s later reads 12 = 14 = 23.5. On the base branch the climate
entity shows 23.5 -> 21.0 -> 18.0 in heat and 23.5 after the write.

Archive tests/fixtures/traces/f461_presets_live_2026-10-24/ in the format
of f461_live_2026-10-24 (47 event-session frames, 122 rows, two runs, one
listener, probe rows aligned to the listener clock, t_estimated on the
command-session replies, events_after bounded by the ACK, synthetic MAC /
IP), its README, tests/test_f461_presets_live_2026_10_24_trace.py (9 tests)
and *#4*2*#7*1*4## in PHYSICAL_ONLY_TX_FRAMES (no oracle stimulus; tier 1
failed on it before). *4*107*2## / *4*108*2## parse with mode = None
(OpenWebNet-HA/OWNd#146).

Refs #791

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

* test(trace): accept the OWNd#146 decoding of WHAT 107 / 108 / 208 beside the pinned None

Review of #812 (5642383839): OWNd's required MyHOME conformance job runs
this suite against every OWNd PR, so the two trace tests that pinned
`mode is None` for *4*107*2##, *4*108*2## and *4*208*2## would fail the
moment OpenWebNet-HA/OWNd#146 decodes them (heating eco / comfort,
cooling comfort). Both locations (this PR's presets test and the merged
f461_live_2026-10-24 test) now accept None or the decoded mode / season,
with the "mode is unknown" log tied to the None case, as
tests/test_message_edges.py does for the two OWNd generations.

Rounding nit: the *#4*2## poll 15.53 s into eco is written +15.5 s
everywhere (test docstring, archive purpose), as in the README.

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

---------

Co-authored-by: gdluck <innowebdevs@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
GreenGrassBlueOcean pushed a commit that referenced this pull request Oct 26, 2026
… the nominal (#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, `*#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>
GreenGrassBlueOcean added a commit that referenced this pull request Oct 26, 2026
…set-and-scope-cover-settle

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

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Author

Record note from the WHO 4 PDF audit of 2026-10-26 (no change requested): this PR writes the setpoint on the plain zone, *#4*Z*#14*T*3##, and reads the result from the dimension 12 echo. The Legrand WHO 4 document (openwebnet-who-4.pdf) only defines that write on #Z / #0 (pp. 8, 23) and echoes it as *4*110*#Z## plus dimension 0 (pp. 8-9); dimension 12 appears in its echoes only with a non-zero local offset (p. 16). The plain-zone write and the dimension 12 echo are what the F461 2.0.11 and the MyHomeServer1 3.87.13 actually do (fixtures in #810 / #812 / #815), so the integration follows the gateways over the document. The dimension 12 / 13 / 14 value layouts used here match pp. 16-18.

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