Skip to content

fix(climate): do not latch the setpoint in force on central-unit zones that report dimension 14 alone - #809

Closed
gdluck wants to merge 1 commit into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-setpoint-latch-central-plants
Closed

gdluck wants to merge 1 commit into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:fix/climate-setpoint-latch-central-plants

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026

Copy link
Copy Markdown

Requested by A

What is the problem

#791 made a zone's dimension 12 report (the setpoint in force, knob offset included) hold the displayed target temperature against later dimension 14 (commanded setpoint) and dimension 13 (knob offset) frames. The review of #791 (comment 6388207693, point 2) showed where that hold goes stale:

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 [...], 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.
  • F453AV central-mode regression (tests/fixtures/traces/issue_619/): [...] 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.

Both reproduce by replaying the fixture frames through MyHOMEClimate.handle_event on d8bac157 (which contains abf6330f): the MH200N zone 2 ends at 10.5 instead of 10.0, and F453AV zone 14, which the 10:29:57 sweep reports with *#4*14*12*0080*3## then *#4*14*14*0100*3##, ends at 8.0 instead of 10.0 after Daily Holiday. (The 10:49:26 Central.to.OFF capture latches zones at 22.0/23.0 while they are OFF, but the *4*111*#Z## frame of the Holiday capture takes them through OFF → heating, which already clears the hold, so those zones end at 10.0 on both revisions; the sweep → Holiday path on zone 14 is the one that stays stuck.)

Refs #791 (merged), #674 (why *#4*Z## is not polled on SCS gateways), #383 and #454 (antifreeze tests, unchanged). No open issue is closed by this change.


What is the solution

  1. climate.py: the hold is armed only where a poll can refresh it. In the dimension 12 handler, _local_target_reported = value is not None and self._gateway_supports_zone_status. On an F461 or MyHomeServer1 (the gateways that answer *#4*Z##) nothing changes: every poll reply and every write echo repeats dimension 12 before dimension 14, and homeassistant.update_entity refreshes it. On a gateway that is never polled per zone (F454, F455, MH200N, MH201, MH202, F452, F453, F458, F459, H4684: the supports_zone_status table in gateway.py) a dimension 14 or 13 frame updates the slider as before b16 (commanded setpoint plus knob offset).
  2. Rules evaluated and rejected, each against all existing climate tests, the [BUG} Climate entities show 17 °C instead of the active 7 °C antifreeze target #383/climate: trailing '3' on WHO 4 dimension 12 is read as protection/off, so _target_temperature goes stale #454 antifreeze tests, the fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 tests and the two new replays:
    • A time window between dimension 12 and 14 ("a dimension 14 arriving on its own ≥ 1–2 s after the 12 is a new report"): no window separates the two cases. The F453AV status sweep sends dimension 12 for every zone first and dimension 14 for every zone afterwards, 1.1 s to 14 s apart for the same zone, and repeats each dimension 14 1.3 s later (zone 14: 12 = 8.0, then 14 = 10.0 at +1.1 s and +2.4 s); the MH200N update arrives 9.7 s after its dimension 12; the F461 answers the four dimension reads sent separately 2.6 s apart (f461_live_2026-10-23, run 1). A 2 s window passed the MH200N replay but turned the F453AV sweep into "12 for 2 s, then 14" and would flip an F461 zone to the commanded value after a *#4*Z*14## read by hand.
    • Hold only for not self._central: zones of a central-unit plant are not flagged central in discovery (only #0 / #0#1 are), so both replays still fail with it.
    • Hold only on plants without a central unit (gateway.has_central_unit): the MH200N capture has no #0 entity (its zones answer as sub-centrals, *4*110*#2##), so the MH200N replay still fails.
  3. Known limit, stated in docs/configuration/known_limitations.md, docs/configuration/climate.md and the b16 release note: a thermostat behind an SCS gateway that regulates to another setpoint than the commanded one (no such capture exists; the 12 ≠ 14 split was seen on the F461 only) is shown with the commanded value plus the offset there.
  4. The second opinion obtained on the rule (an independent model review of the fixture evidence) agreed to ship this gate with the limit documented rather than another timing heuristic, and noted the open points: a KW4691 behind an F454-class gateway would not show a genuine 12 ≠ 14, and on a pollable gateway a stale hold is only refreshed by the next poll. A per-zone "report epoch" rule (a program or operating-state transition invalidates the hold) was suggested as a follow-up once transition captures exist on both plant types; the F461 + KW4691 transition trace requested in the fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 review is exactly that capture.

Companion PR: none.


State Diagram

stateDiagram-v2
    direction TB

    state "Gateway answers *#4*Z## (F461, MyHomeServer1)" as Pollable {
        [*] --> Nominal : *#4*2*14*T*3## , no dimension 12 known\nshow T + offset
        Nominal --> InForce : *#4*2*12*T*3## , zone running\nshow T as is, hold armed
        InForce --> InForce : *#4*2*14*..## or *#4*2*13*..##\ndisplay unchanged, commanded value stored
        InForce --> InForce : next poll *#4*2##\n12 first, then 14 and 13
        InForce --> Nominal : OFF to on (#383) or climate.set_temperature\nhold cleared
    }

    state "Gateway cannot be polled per zone (MH200N, F453AV, F454, ...)" as Scs {
        [*] --> Shown : *#4*2*12*0105*3## spontaneous\nshow 10.5, no hold
        Shown --> Shown : *#4*2*14*0100*3## 9.7 s later , no dimension 12\nshow 10.0 + offset
        Shown --> Shown : *#4*Z*14*0100*3## on Daily Holiday , no dimension 12\nshow 10.0 + offset
    }
Loading

Hardware Verification

  • F461 2.0.11, standalone zone 2 (KW4691), 2026-10-23 and 2026-10-24: unchanged by this PR (the gateway answers *#4*2##). The live sequences are pinned by the new tests: run 1 poll reply *#4*2*0*0239##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0210*3##, *#4*2*13*00## → 23.5 shown, 21.0 kept as the commanded value; run 3 write *#4*2*#14*0235*3## → echo *#4*2*12*0235*3##, *4*1*2##, then the next poll with 14 = 23.5 → 23.5; 2026-10-24 antifreeze split *4*102*2## → *#4*2*12*0070*3## while 14 stays 23.5, poll reply 0, 12 = 0070, 102, 14 = 0235, 13 = 00 → off, 7.0; restore → *4*1*2##, 12 = 14 = 23.5 → heat, 23.5.
  • MH200N, F453AV: not available to me; the change on those gateways is verified on the community captures below (the entity ends where the pre-b16 code ended).

How is it tested

  • tests/test_climate_setpoint_latch_central_plants.py (9 cases):
    • test_mh200n_zone_2_follows_the_dimension_14_update_reported_without_dimension_12: every received WHO 4 frame of zone 2 (2 and #2) of the MH200N sweep, in capture order; asserts no *#4*2## was sent, one *#4*2*12*0105*3## and no dimension 12 after it; 10.5 after frame 161, 10.0 at the end;
    • test_f453av_zone_14_holiday_on_updates_the_zone_without_dimension_12: zone 14 frames of the 10:29:57 sweep (8.0 after 12*0080, 10.0 after 14*0100), then its Holiday frames *4*111*#14##, *4*111*#14##, *#4*14*0*0280##, *#4*14*14*0100*3## → 10.0; asserts the Holiday capture holds 23 dimension 14 frames (22 of them 0100) and no dimension 12;
    • test_a_knob_turn_reported_by_dimension_13_alone_moves_the_slider (MH200N dial turns, frames 106–130 pattern), test_an_scs_gateway_shows_the_commanded_setpoint_plus_offset_as_before (the F461 reply order on a non-pollable gateway: 21.0, status quo);
    • test_f461_live_poll_replies_show_the_setpoint_in_force, test_f461_live_write_echo_then_poll_keeps_the_written_setpoint (runs 1 and 3 of the 2026-10-23 archive), test_antifreeze_split_seen_live_on_2026_10_24, test_a_poll_or_report_with_dimension_12_last_shows_it_on_both_kinds_of_gateway (×2).
  • Fail-before / pass-after: the two replays fail on d8bac157 (MH200N 10.5 ≠ 10.0, F453AV 8.0 ≠ 10.0) and pass on the pre-fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 base 974306b5; 5 of the 9 cases fail on d8bac157, all 9 pass with the change. tests/test_climate_setpoint_in_force.py (12), test_issue_454_dim12_mode3.py, test_component_climate.py ([BUG} Climate entities show 17 °C instead of the active 7 °C antifreeze target #383 antifreeze sweep), test_issue_429_zone_state.py, test_f461_live_2026_10_23_trace.py, test_issue_466_mh200n_trace.py and test_issue_619_who4_trace.py pass unchanged.
  • Full suite against the PyPI-pinned OWNd behaviour (plain upstream OWNd master, f04d8aa, on PYTHONPATH): 4507 passed, 1 skipped, 100.0 % statement coverage (16071 statements), 3 min with -n 4.
  • ruff check ., scripts/typing_ratchet.py (0 strict errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, tests/test_fixture_privacy.py: all pass. No new fixture files.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
Spontaneous dimension 12, then dimension 14 + 13 alone tests/fixtures/traces/issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json, frames 159–191 (#466) MH200N, central-unit plant, no *#4*2## sent *#4*2*12*0105*3## (14:39:27), *#4*2*14*0100*3## + *#4*2*13*00## + *4*110*#2## (14:39:37 and 14:39:48)
Sweep with 12 ≠ 14 on one zone, then Daily Holiday with dimension 14 only tests/fixtures/traces/issue_619/myhome_trace_F453AV_who4_2026-10-11T10-29-57.json and ...11-02-55.Holiday.ON.json (#619) F453AV 3.0.14, central unit #0 zone 14: *#4*14*12*0080*3##, *4*1*14##, *#4*14*14*0100*3## ×2; Holiday: *4*115#1101*#0##, 23 *#4*Z*14*…*3##, no dimension 12
Poll reply and write echo on a pollable gateway (unchanged path) tests/fixtures/traces/f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, runs run1_oracle_replay_readonly, run3_climate_setpoint_restore (#799) F461 2.0.11, standalone zone 2 *#4*2## → 0, 12 = 0235, *4*1*2##, 14 = 0210, 13 = 00; write *#4*2*#14*0235*3## → *#4*2*12*0235*3##, *4*1*2##, then the poll with 14 = 0235
Antifreeze split (12 = 7.0 while 14 = 23.5) live 2026-10-24 on the same F461, bus monitor + *#4*2## poll (frames inlined in test_antifreeze_split_seen_live_on_2026_10_24; not a fixture file) F461 2.0.11, zone 2 *4*102*2## → *#4*2*12*0070*3##; poll 0, 12 = 0070, 102, 14 = 0235, 13 = 00; restore *4*1*2##, 12 = 14 = 0235

What does the Firmware Oracle / Encyclopedia say

  1. OpenWebNet Encyclopedia (functional/who-4-temperature-control/dimensions.md): DIMENSION 12 is the complete probe status with the setpoint after the local offset; DIMENSION 14 the setpoint, DIMENSION 13 the local offset. The encyclopedia describes the frames, not which gateway repeats dimension 12 on a change: that comes from the captures above (F461 and MyHomeServer1 do on every poll and write; MH200N and F453AV update with dimension 14 alone).
  2. Firmware Oracle: *#4*Z## is the request the gate is keyed on. The published results/<P>/<V>/oracle/full/trace-corpus.tsv rows show *#4*1*12## and *#4*1*14## forwarded to the bus on all twelve images (no thermostat behind the emulated gateways, so no 12 ≠ 14 pair is produced); the per-zone status NACK/timeout of the SCS gateways that supports_zone_status encodes is the F455 multiple timeouts #674 finding (F455, 10 s command-session timeout per zone), not an oracle verdict.

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!

…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>
@gdluck
gdluck force-pushed the fix/climate-setpoint-latch-central-plants branch from e485728 to 52d965b Compare October 24, 2026 21:10
@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Author

Status after #818 merged: this PR rebases cleanly, but #818's test_hl4684_trace_f430_4_pure_valve_vs_fancoil_zones fails on it (zone 66 shows 20.0, the test expects 17.0), and the failure shows the gate here is the wrong proxy. Not pushed; the logic and a fix are below, cross-checked with an independent second-opinion review.

What the traces say. The hold of #791 (dimension 12, the setpoint in force, held against later dimension 14 / 13 frames) was challenged with two central-unit plants where a lone dimension 14 must win: MH200N zone 2 emits 12 = 10.5, then 9.7 s and 20.6 s later 14 = 10.0 with 13 = 00 and *4*110*#2##, no new 12; the F453AV Daily Holiday emits 23 lone 14 = 10.0 frames and no 12. This PR answered by arming the hold only on gateways that answer *#4*Z## (F461, MyHomeServer1), treating "SCS-only gateway" as "central unit changes the setpoint with 14 alone". The H/L4684 trace of #818 (SCS-only, with a 3550 central unit, zones standalone=False) refutes that proxy: zone 66 reports, as one status report, 0 = 22.8, *4*1*66##, 12 = 17.0 and 0.7 s later 14 = 20.0, then *4*303*66##; 17.0 is the value in force (the zone is in its eco / off regime while 20.0 is the nominal) and the entity must keep it. So on the same kind of plant a 14 right after a 12 is the companion nominal, while a 14 seconds later with no 12 is a new setpoint. What separates the cases in all four captures is timing and company, not the gateway model: companions arrive 0.08-0.7 s after the 12 (F461 poll reply 0, 12, season, 14, 13 within 0.2 s; H/L4684 0.7 s), lone updates 9.7 s or more later.

Proposed fix (replaces the gateway gate). A per-zone report window: on dimension 12 record its arrival and arm the hold; on dimension 14 or 13, if a dimension 12 arrived within REPORT_WINDOW (2 s, an empirical bound between the 0.7 s companion gap and the 9.7 s lone update seen) keep the hold, otherwise apply the new value through the existing nominal / offset handling and clear the hold (the next 12 re-arms it). Checked against the four captures: F461 poll replies and bezel presses keep the hold (a press pushes 12 and 14 realigned), MH200N and the F453AV holiday update, H/L4684 zone 66 keeps 17.0 and goes off on *4*303*66##. Where a poll was sent, correlating the reply burst with the poll is stronger than timing and should be preferred; the window is for spontaneous reports.

Known limits of the heuristic (to be tests, not surprises): a central unit that changes the setpoint with a 14 within 2 s of a 12 would be read as a companion; a companion delayed beyond 2 s would be read as an update; a reversed 14 then 12 shows the 14 for a moment before the 12 restores and re-arms the hold; a lone 14 while the zone keeps regulating to a preset would show the nominal until the next 12 (the per-zone poll of #794 refreshes it on the plants that answer); dimension 13 must go through the offset handling in either order with 14. None of the four captures hits these; the tests should.

I will rework the PR along these lines if you agree with the rule; say if you would rather close it and keep #791 as merged.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor

Agreed that gating _local_target_reported on _gateway_supports_zone_status (52d965bd) is the wrong proxy — and please do not rework this around REPORT_WINDOW = 2 s either. Between a wall-clock window heuristic and keeping #791 as merged, we should keep #791 as merged (or, if you want to rework #809, use a strictly clock-free value/state transition rule — details at the end).

1. Why _gateway_supports_zone_status fails beyond #818

Besides H/L4684 Zone 66 in #818 (12 = 17.0 °C, +0.700 s 14 = 20.0 °C, no DIM 13), three other SCS-only captures in tests/fixtures/traces/ already show why gateway poll capability does not track 12 ≠ 14:

  • MH200N Zone 3 (issue_466/config_entry-myhome-80a1577fb7ae6f68f05e0cc5a1ead27d.json, frames 96–99): *#4*3*0*0232##, *#4*3*12*0090*3## (9.0 °C), *4*1*3##, +0.310 s *#4*3*14*0100*3## (10.0 °C, no DIM 13) → 52d965bd shows 10.0 °C instead of 9.0 °C.
  • MH201 Zone 3 (issue_466/myhome_trace_MH201_all_2026-09-25T19-05-42.json, frames 1, 2, 8): *4*1*3##, *#4*3*12*0090*3## (9.0 °C), *#4*3*14*0100*3## (10.0 °C, no DIM 13) → 52d965bd shows 10.0 °C instead of 9.0 °C.
  • MH200N Zones 3 & 4 (issue_612/myhome_trace_MH200N_startup_cen_scene_2026-10-07T20-38-24.json, frames 178–191): 12*0230*3 (23.0 °C) arrives before 13*03 (+3 °C) and 14*0200*3 (20.0 °C). On an already-running zone where _local_offset is still 0 (e.g. after HA state restore, since async_restore_last_state does not restore _local_offset), disabling the hold causes 12*0230*3 to set _target_temperature = 23.0 and 13*03 to spike target_temperature to 26.0 °C (23.0 + 3.0) until 14*0200*3 arrives.

2. Why REPORT_WINDOW = 2 s fails on the committed captures

Bullet 2 of this PR's own description explicitly rejected a 1–2 s window ("no window separates the two cases"). That original rejection was right — replaying the committed fixtures with their capture timestamps hits the failure modes directly:

  1. Breaks F461 run1_oracle_replay_readonly (f461_live_2026-10-23/myhome_trace_F461_live_2026-10-23.json, frames 57–60):

    • t = 355.447: *#4*2*12*0235*3## (23.5 °C in force)
    • t = 356.769 (+1.322 s): *#4*2*13*00##
    • t = 358.062 (+2.615 s > 2.0 s): *#4*2*14*0210*3## (21.0 °C nominal)
      Under REPORT_WINDOW = 2 s, frame 60 clears the hold and flips F461 Zone 2 from 23.5 °C back to 21.0 °C.
  2. Breaks F453AV (issue_619/myhome_trace_F453AV_who4_2026-10-11T10-29-57.json) — companion DIM 14 gaps are +0.07 s to +16.78 s:
    The central unit scans all zones for DIM 0 + 12 first and emits DIM 14 twice per zone at the tail of the sweep. In 12 of the 13 zones that report both 12 and 14, same-sweep companion DIM 14 frames arrive > 2.0 s after DIM 12:

    • Zone 1: +14.171 s and +16.781 s
    • Zone 2: +12.171 s and +14.840 s
    • Zone 3: +13.411 s
    • Zone 4: +9.192 s and +12.050 s
    • Zone 10: +2.077 s and +5.430 s
    • Zone 14: 12*0080*3 at [95], first 14*0100*3 at +1.110 s (<= 2.0 s, holds 8.0 °C), second 14*0100*3 of the same sweep at +2.410 s (> 2.0 s, flips Zone 14 to 10.0 °C).
      Because intra-sweep companions span 0.07–16.78 s while MH200N Zone 2's standalone update arrives at 9.66 s, no scalar window W separates companions from updates.
  3. Clobbers F461 Comfort/Eco presets (f461_presets_live_2026-10-24) on any lone 14 or 13 read — and fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways #794 does not refresh DIM 12:

    (the per-zone poll of fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways #794 refreshes it on the plants that answer)

    Two issues here:

    • fix(gateway): skip the startup requests the firmware refuses, poll each zone's temperature on SCS-only gateways #794 polls *#4*Z*0## (OWNHeatingCommand.get_temperature), not *#4*Z## or *#4*Z*12##. *#4*Z*0## only returns ambient DIMENSION 0; it never refreshes DIMENSION 12.
    • MyHOMEClimate has should_poll = False, so even on F461 and MyHomeServer1, async_update runs only on startup / reconnect (or manual homeassistant.update_entity), not periodically. While Comfort (21.0 °C) or Eco (18.0 °C) is active with 14 = 23.5 and 13 = 00, any external bus read of *#4*2*14## or *#4*2*13## > 2 s later (lone 13*00 reads at +38.7 s and +67.1 s are in f461_live_2026-10-15 and issue_649/myhome_trace_MyHomeServer1_live_2026-10-07.json) clears the hold and permanently reverts the entity to 23.5 °C.
  4. DIMENSION 13 fails in both directions under a time window:

    • At dt > 2 s, an unchanged 13*00 frame clears the hold and reverts DIM 12 to _target_temperature + 0.
    • At dt <= 2 s, a genuine physical knob turn (*#4*4*13*02##) within 2 s of a DIM 12 frame is swallowed as a companion and never updates the slider on non-polled gateways.
  5. Poll correlation and synchronous test-clock skew:

    • Correlating replies with tx polls cannot cover external bus peers (H4684/H4890 touchscreens, Home + Control, sibling gateways) whose reads never pass through HA's command session, nor 15–20 s interleaved *#4*0## replies with no transaction IDs.
    • Synchronous trace replay tests (for raw in frames: zone.handle_event(...)) run in < 1 ms wall-clock, so a time.monotonic() window treats 16 s and 33 min trace gaps as < 2 s in tests while behaving oppositely on a live bus.

3. Note on test_f453av_zone_14_holiday_on_updates_the_zone_without_dimension_12

In 10-29-57.json, frame [0] is *4*115#1101*#0## — the plant was already in Daily Holiday (10.0 °C across zones 1..13), and Zone 14 reported 12*0080*3 (8.0 °C = 10.0 °C with a -2 °C probe offset; passive F453AV captures carry no DIM 13 frames) followed by 14*0100*3 (10.0 °C). Between 10:29:57 and 11:02:55, 10-49-26.Central.to.OFF.json occurred (12 = 14 = 22.0 °C on Zone 14). Asserting zone.target_temperature == 10.0 at the end of 10-29-57.json itself overwrites Zone 14's real 8.0 °C setpoint in force during Holiday — the exact bug that broke H/L4684 Zone 66. The genuine F453AV Holiday regression is a running zone in myhome_sweep_F453AV_all_2026-10-04T19-45-11.json (e.g. Zone 4 or Zone 7 at 12 = 22.0, 14 = 22.0) receiving 11-02-55.Holiday.ON.json (*4*111*#Z## + *#4*Z*14*0100*3##, zero DIM 12 frames).


Path forward

  • Option A (simplest / safest for b16): Close fix(climate): do not latch the setpoint in force on central-unit zones that report dimension 14 alone #809 and keep fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 as merged. fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791 is deterministic and keeps F461 (2026-10-23 + 2026-10-24 presets), H/L4684 Zone 66, MH200N Zone 3 / issue_612, MH201 Zone 3, and F453AV 10-29-57 Zone 14 accurate, with the documented limitation for central-unit DIM 14-only updates.
  • Option B (if you want to rework fix(climate): do not latch the setpoint in force on central-unit zones that report dimension 14 alone #809 without any timer): Gate hold updates strictly on protocol value / state changes (zero time.monotonic()):
    1. DIMENSION 13: Track the last seen (local_control_state, local_offset). While _local_target_reported is True, the first DIM 13 (prev is None) or an unchanged DIM 13 (cur == prev, e.g. 13*00) leaves _local_target_temperature and the hold untouched; an actual dial change (prev is not None and cur != prev) steps _local_target_temperature by new_offset - prev_offset (or clears the hold).
    2. DIMENSION 14: Track the companion nominal associated with the active DIM 12 hold (_held_dim14, reset to None when DIM 12 arms the hold and populated by the first companion DIM 14 after it) plus whether an explicit zone/sub-central operating-mode frame (MESSAGE_TYPE_MODE with WHAT not in ("0", "1"), e.g. *4*110*#Z## / *4*111*#Z##) arrived after DIM 12 and its companion offset. A DIM 14 frame keeps the hold when it is that first companion DIM 14 or repeats the same value (new_14 == _held_dim14), and clears _local_target_reported only when _held_dim14 changes (_held_dim14 is not None and new_14 != _held_dim14, e.g. F453AV 22.0 → 10.0, MH200N 18.0 → 10.0) or when a DIM 12 burst had no DIM 14 (_held_dim14 is None, DIM 13 already seen) and a new *4*110*#Z## / *4*111*#Z## cycle reports a different DIM 14 (MH200N mid-stream at frame 159: 12 = 10.5 → *4*110*#2## + 14 = 10.0).

@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Author

Thank you for replaying the committed captures against the window; the F461 run1 frames 57-60 (14 at +2.6 s), the F453AV sweep companions at up to +16.8 s and the lone 13*00 reads settle it: no wall-clock window separates a companion from an update, exactly as this PR's own description had said before I proposed one, and *#4*Z*0## does not refresh dimension 12. Taking option A: closing this PR and keeping #791 as merged, with its documented limitation for central-unit updates that carry dimension 14 alone. If that limitation starts to bite on a real plant, option B (a clock-free rule on the held companion 14 and on 13 value changes, with the *4*110*#Z## / *4*111*#Z## cycles as the trigger) is the way, and I will open it against the captures you listed, including the 11-02-55.Holiday.ON.json case on a running zone, rather than the 10-29-57 one.

@gdluck gdluck closed this Oct 26, 2026
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