Skip to content

test(trace): ingest live F461 2.0.11 2026-10-24 climate setpoint capture (knob, antifreeze split, presets, bezel, HA write) - #810

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:test/trace-f461-live-2026-10-24
Oct 24, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:v2-phase1-architecturefrom
gdluck:test/trace-f461-live-2026-10-24

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown

Requested by A

What is the problem

The review of #791 (comment 6388207693) asked for a causal before / trigger / after capture on the physical F461 + KW4691 plant: "Could you try capturing one continuous session (monitor listener + command polls) on zone 2 with this sequence? ... Re-convergence check: While 12 != 14 is active, tap +0.5 °C once on the KW4691 ... and then call climate.set_temperature from Home Assistant so we have on-wire proof of what moves 12 vs. 14 in both directions". The convention since #714 / #753 / #799 is that every physical capture is ingested as tests/fixtures/traces/<id>/ with a README and a replay test module, so the frames the discussion relies on are in the repository. Refs #791.


What is the solution

A new derived trace archive, tests/fixtures/traces/f461_live_2026-10-24/, in the format of f461_live_2026-10-23 (#799, with its review conventions: command-session replies at their request's t with t_estimated, events_after bounded by the ACK, one monotonic t axis with per-run t_offset), plus tests/test_f461_live_2026_10_24_trace.py (14 tests) and two entries in PHYSICAL_ONLY_TX_FRAMES.

  • Plant: BTicino F461 2.0.11, Living Now KW4691 standalone thermostat at zone 2 (BT-F430/2 valve as actuator 1, BT-F430/4 fan coil as actuator 2), heating season, no central unit. A local Home Assistant at fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791's head (abf6330f, PyPI OWNd pin) wrote climate.set_temperature.
  • Sessions: two event-only listeners covered the whole capture while a probe sent the requests on its own command session. Each run takes its event frames from the listener that covered it (one source, no duplicate frames); the probe's command rows are placed on the listener clock through the frames both sessions received (capture.runs[].probe_segments[].listener_offset_s; the probe's own event log matched the listener's frames exactly over every request window before it was dropped). Home Assistant's ten requests (four *#4*2*#14*…*3## writes, three *#4*2## polls with their *#4*2*60##) went through its own session: they are listed in capture.home_assistant_requests from its debug log (not included), their echoes and replies are in the frames, and they have no tx / result rows.
  • Runs (capture.runs): run1_knob_turn 38 rows / 21 frames, run2_fast_poll_dim12_dim14 2902 / 742, run3_antifreeze_split_and_restore 74 / 34, run4_preset_templates 93 / 51, run5_bezel_in_manual_and_in_antifreeze_then_ha_write 93 / 46; 894 event-session frames and 3200 rows in all.
  • Privacy: MAC 00:03:50:00:10:23 and IP 192.0.2.92 in the gateway block only (no WHO 13 identity frame was exchanged); generic entity name in the README; no password, no local paths, no logs or diagnostics. python scripts/anonymize_plant_fixture.py --check on the new files: 0 with personal data; tests/test_fixture_privacy.py passes.
  • tests/test_trace_oracle_validation.py: *#4*2*#7*1*3## and *#4*2*#7*2*3## join PHYSICAL_ONLY_TX_FRAMES (no oracle stimulus for the dimension 7 templates; every other request canonicalises to an existing verdict). Tier 4: every rx frame parses in the pinned OWNd. python scripts/update_trace_matrix.py was run: the F461 row already covers WHO 4 and 13, README.md / docs/trace-availability.md are unchanged.
  • Naming note: *#4*2*#7*2*3## is context 2 (cooling), state 3 (comfort), Cooling Comfort; Heating Eco would be *#4*2*#7*1*4## and was not sent. The archive keeps the probe step name eco_template_and_reads under which the request was sent during the capture (the fixture was not regenerated for it); the test locals and prose say cooling comfort.

State Diagram

sequenceDiagram
    participant K as KW4691 bezel
    participant Z as F461 zone 2
    participant P as probe or Home Assistant
    Note over K,P: run 1, bezel turn
    K->>Z: dial 29.0 then 18.0
    Z-->>P: *#4*2*12*0290*3##, *4*1*2##, *#4*2*0*0239##
    Z-->>P: *#4*2*12*0180*3##, *4*1*2## (no dimension 14, no 13)
    P->>Z: *#4*2*14## 49 s later
    Z-->>P: *#4*2*14*0180*3##
    Note over K,P: run 3 and 5, antifreeze splits 12 from 14
    P->>Z: *4*102*2##
    Z-->>P: *4*102*2##, *#4*2*12*0070*3##, *4*102*2##
    P->>Z: *#4*2##
    Z-->>P: 0, *#4*2*12*0070*3##, *4*102*2##, *#4*2*14*0235*3##, 13
    K->>Z: +0.5 in antifreeze
    Z-->>P: *4*1*2##, *#4*2*12*0075*3## (14 reads 7.5 later)
    Note over K,P: run 2, 4, 5, a dimension 14 write realigns both
    P->>Z: *#4*2*#14*0235*3##
    Z-->>P: *4*1*2##, *#4*2*12*0235*3## (no dimension 14 echo)
    P->>Z: *#4*2##
    Z-->>P: 0, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0235*3##, 13
Loading

Hardware Verification

F461 2.0.11, KW4691 zone 2, 2026-10-24 18:07-19:12 UTC; every frame is in the archive.

  • A bezel turn emits dimension 12 only. *#4*2*12*0290*3##, *4*1*2##, *#4*2*0*0239##, then *#4*2*12*0180*3##, *4*1*2## 2.25 s later when the dial settled; no dimension 14 and no dimension 13 frame. The first *#4*2*14## read, 49 s later, already answered 18.0, so the thermostat had moved dimension 14 silently by then. Run 5 repeats it (*#4*2*12*0240*3##; the valve opens 3 s later; the *#4*2## sent 10.8 s later, result at 12.2 s, already in antifreeze, reads 14 = 24.0).
  • A dimension 14 write echoes dimension 12, never dimension 14. All six *#4*2*#14*T*3## (four from Home Assistant, two from the probe) are echoed as *#4*2*12*T*3##, *4*1*2##, *#4*2*0*…##. Only the two probe writes have measured reply_time_s rows (ACK in 63 ms and 77 ms); Home Assistant's writes have no rows here, their timings come from its debug log. During the 0.6 s alternating poll (480 reads, all ACKed in 90-330 ms) the next dimension 14 read already carried T: 0.7 s after the dimension 12 read for 21.0; for 23.5 the echo arrived while a dimension 14 read was pending and that reply was already 23.5. All 240 dimension 14 frames of run 2 are read replies.
  • Among the operations exercised in this capture, antifreeze is the only one that split 12 from 14. *4*102*2## (ACK 65-80 ms) is echoed as *4*102*2##, *#4*2*12*0070*3##, *4*102*2##, *#4*2*0*…##; *#4*2## then answers 0, 12 = 7.0, *4*102*2## (in the season frame's place), 14 = the last commanded value (23.5 / 24.0 / 7.5), 13. Of the ten *#4*2## replies, the three with 12 != 14 are the three in antifreeze; the seven in the heating season have 12 == 14 (also on Home Assistant's own polls). This is not a hardware invariant: on this plant f461_live_2026-10-23 run 1 recorded 12 = 23.5 != 14 = 21.0 in active heating (*4*1*2##) after days without a write; that transition was not reproduced here.
  • A bezel press in antifreeze leaves antifreeze at the protection value plus one step: *4*1*2##, *#4*2*12*0075*3##, *4*1*2##, *#4*2*0*0239##; 27 s later 12 and 14 both read 7.5. A second *4*102*2## split them again (7.0 / 7.5).
  • A Home Assistant write while 12 != 14 realigns both: climate.set_temperature 23.5 in antifreeze gave *4*1*2##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*0*0238##; the *#4*2## sent 7.5 s after the echo (result at 8.9 s): 12 = 14 = 23.5, heating.
  • Dimension 7 templates. *#4*2*#7*1*3## -> *#4*2*7*1*3## (heating comfort), *#4*2*12*0210*3##, *4*108*2##; *#4*2*#7*2*3## -> *#4*2*7*2*3## (cooling comfort), *#4*2*12*0250*3##, *4*208*2##. 47 s later *4*0*2## (conditioning, 25.0), 3 s later *4*1*2## (heating, 25.0), 2 s later *#4*2*12*0225*3##; 12 and 14 both read 22.5 without a dimension 14 write. *4*108*2## / *4*208*2## parse in the pinned OWNd with no known mode.
  • Observation (not pinned): after *#4*2*7*2*3## the climate entity at fix(climate): show the setpoint in force (dimension 12), keep dimension 14 as the commanded value #791's head showed hvac_mode: cool and kept it through the later *4*0*2## / *4*1*2## frames, the restore write and a *#4*2## poll carrying *4*1*2## (the dimension 7 context sets the mode, a season frame does not change a zone that has one). Frames in run 4; for a separate issue.

How is it tested

  • pytest tests/test_f461_live_2026_10_24_trace.py: 14 passed. Schema and synthetic identity, every frame parses in the pinned OWNd, result rows consistent with their tx / rx rows, the facts above per run, and four replays of the event frames through the climate entity as it is on the base branch (the bezel turn shows 29.0 then 18.0 from dimension 12; antifreeze turns the zone off and the restore write brings heating and 23.5 back; 24.0 from the bezel, 7.5 after the bezel press in antifreeze, 23.5 after the Home Assistant write; the template run's setpoints 21.0, 25.0, 22.5, 23.5).
  • pytest tests/test_trace_oracle_validation.py tests/test_device_health_corpus.py tests/test_fixture_privacy.py: 253 passed (tier 1 failed on the two dimension 7 template requests before the PHYSICAL_ONLY_TX_FRAMES entries).
  • Full suite against the PyPI-pinned OWNd behaviour (plain upstream OWNd master, f04d8aa, no fix PR applied): pytest tests/ -n 4 --cov=custom_components.myhome: 4513 passed, 1 skipped, 100 % statement coverage (16,071 / 16,071).
  • ruff check ., scripts/typing_ratchet.py (0 strict errors), scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/update_trace_matrix.py (no change): all pass.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
Bezel turn tests/fixtures/traces/f461_live_2026-10-24/myhome_trace_F461_live_2026-10-24.json, run1_knob_turn F461 2.0.11, KW4691 zone 2 *#4*2*12*0290*3##, *4*1*2##, *#4*2*12*0180*3##; *#4*2*14## -> *#4*2*14*0180*3## 49 s later; *#4*2## -> 0, 12, *4*1*2##, 14, 13
Writes under a 0.6 s poll same file, run2_fast_poll_dim12_dim14 same 240 *#4*2*12## + 240 *#4*2*14## replies; the write echoes *#4*2*12*0210*3## / *#4*2*12*0235*3## + *4*1*2## + *#4*2*0*0239##
Antifreeze split and restore same file, run3_antifreeze_split_and_restore same *4*102*2## -> *#4*2*12*0070*3##; *#4*2## -> 0, 12 = 0070, *4*102*2##, 14 = 0235, 13; *#4*2*#14*0235*3## -> *4*1*2##, *#4*2*12*0235*3##; *#4*2#1*#20*0##, *#4*2#1*20*0##
Dimension 7 templates same file, run4_preset_templates same *#4*2*#7*1*3## -> *#4*2*7*1*3##, *4*108*2##; *#4*2*#7*2*3## -> *#4*2*7*2*3##, *#4*2*12*0250*3##, *4*208*2##; *4*0*2##, *4*1*2##, *#4*2*12*0225*3##, *#4*2*14*0225*3##
Bezel in Manual / in antifreeze, HA write while split same file, run5_bezel_in_manual_and_in_antifreeze_then_ha_write same *#4*2*12*0240*3##, *#4*2#1*#20*1##; *#4*2## -> 12 = 0070, *4*102*2##, 14 = 0240; *4*1*2##, *#4*2*12*0075*3##, *#4*2*14*0075*3##; 12 = 0070, 14 = 0075; *#4*2*12*0235*3##, then 12 = 14 = 0235
12 != 14 in active heating (referenced, not duplicated) tests/fixtures/traces/f461_live_2026-10-23/, run1_oracle_replay_readonly (#799) same plant, 2026-10-23 *#4*2## -> *#4*2*0*0239##, *#4*2*12*0235*3##, *4*1*2##, *#4*2*14*0210*3##, *#4*2*13*00##

What does the Firmware Oracle / Encyclopedia say

  1. Firmware Oracle: the emulated gateways have no thermostat behind them, so no oracle row produces a dimension 12 / 14 pair or a dimension 7 echo; *#4*2##, *#4*2*12##, *#4*2*14##, *#4*2*#14*…*3## and *4*102*2## canonicalise to existing verdicts (tier 1 passes), while *#4*2*#7*1*3## / *#4*2*#7*2*3## have no stimulus and are recorded in PHYSICAL_ONLY_TX_FRAMES with their live replies. The one-at-a-time ACK timings here (status reads 90-330 ms, writes 63-93 ms, *#4*2## 1.3-1.5 s) match the F461 rows of f461_live_2026-10-23.
  2. OpenWebNet Encyclopedia (WHO 4 dimensions): dimension 12 is the complete probe status with the setpoint after the local offset, dimension 14 the setpoint, dimension 13 the local offset; dimension 7 is *#4*where*#7*[CONTEXT]*[STATE]## with context 1 heating / 2 cooling and state 3 comfort / 4 eco, which is why *#4*2*#7*2*3## is Cooling Comfort. The mode frames *4*108*2## / *4*208*2## that accompany the dimension 7 echoes are documented neither there nor decoded by the pinned OWNd (mode = None); run 4 is the evidence for an upstream OpenWebNet-HA/OWNd follow-up.

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): The fixture is the physical trace itself; every test pins a frame of it.
  • V2 Architecture: N/A (fixture and tests only; the entity replays go through handle_event()).
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

🤖 Generated with Claude Code

…ure (knob, antifreeze split, presets, bezel, HA write)

The before / trigger / after capture asked for on OpenWebNet-HA#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 *michnovka#4*2*michnovka#7*1*3## /
*michnovka#4*2*michnovka#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 OpenWebNet-HA#791

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.

Thanks for capturing the full before / trigger / after sequence on the physical F461 + KW4691 plant and carrying over all the archive conventions from #799 (t_estimated on batched command replies, events_after bounded to the pending request window, monotonic t across runs, synthetic identity, and PHYSICAL_ONLY_TX_FRAMES coverage).

On your draft question: No need to hold this PR for additional hardware runs — let's land these 5 runs once the minor items below are tidied up and handle the remaining DIMENSION 7 / mode items in follow-up PRs and issues.


Minor changes requested for this PR (#810)

  1. PR Description Template (.github/pull_request_template.md):
    • Please reformat the PR description to use the 8-section PR #778 template (## What is the problem, ## What is the solution, ## State Diagram, ## Hardware Verification, ## How is it tested, ## Where are the traces, ## What does the Firmware Oracle / Encyclopedia say, ## Checklist) used on v2-phase1-architecture (same layout as #809).
  2. Scope the 12 != 14 antifreeze statement to this capture (tests/test_f461_live_2026_10_24_trace.py line 19, README.md line 82, and PR description):
    • Qualify "*4*102*2## (antifreeze) is the only operation that splits 12 from 14" with "among the operations exercised in this capture". On this same physical plant in f461_live_2026-10-23 (run1_oracle_replay_readonly), *#4*2## returned 12 = 23.5 and 14 = 21.0 alongside *4*1*2## (active heating, not antifreeze).
  3. Rename misleading eco locals for *#4*2*#7*2*3## (tests/test_f461_live_2026_10_24_trace.py lines 452–475):
    • Rename eco, after_eco, and t_eco to cooling_comfort, after_cooling_comfort, and t_cooling_comfort since *#4*2*#7*2*3## is Cooling Comfort (context=2, state=3), not Heating Eco (*#4*2*#7*1*4##). Keeping "step": "eco_template_and_reads" in the JSON archive (with a short comment explaining the probe step name) is fine so you don't have to regenerate the fixture.
  4. Small metadata & README.md prose alignments:
    • Align run5's UTC window "19:10:42-19:12:35" → "19:10:42-19:12:37" in myhome_trace_F461_live_2026-10-24.json (capture.runs[4].utc, line 68) and README.md (line 66) to match capture.window.last, probe_segments[3].utc, and the final result row ("utc": "19:12:37").
    • Clarify in README.md (line 77) and the PR description that only the two probe *#4*2*#14*0235*3## writes have measured ACK times in the archive (63 ms and 77 ms), whereas Home Assistant's four writes went through HA's own session (timings from its debug log), and tighten the two rounded run5 intervals (~11–12 s and ~8–9 s).
    • Replace climate.thermo_ofice in README.md (line 22) with generic climate.climate_zone_2.

Follow-up PRs / issues (out of scope for #810)

  1. Follow-up trace PR for DIMENSION 7 Heating Eco (*#4*2*#7*1*4##) & non-matching Comfort baseline:
    • Per the WHO 4 DIMENSION 7 parameter model (*#4*where*#7*[CONTEXT]*[STATE]##: CONTEXT 1=Heating, 2=Cooling; STATE 3=Comfort, 4=Eco), the parenthetical example in #791 (*#4*2*#7*2*3##) was Cooling Comfort (context=2, state=3), so true Heating Eco (*#4*2*#7*1*4##) was not sent yet.
    • In run4, *#4*2*#7*1*3## (Heating Comfort) was sent from a 21.0 °C baseline that already matched the KW4691's 21.0 °C comfort preset, and during the 47.6 s 25.0 °C cooling-comfort window no *#4*2*14## read was sent before the zone reverted to 22.5 °C. A small follow-up capture sending *#4*2*#7*1*3## from 23.5 °C and *#4*2*#7*1*4## — polling *#4*2*12## and *#4*2*14## immediately while active — (plus any schedule / Home + Control transitions if available) can land in a separate PR.
  2. Separate issue / PR for standalone hvac_mode stuck on cool after *#4*2*7*2*3##:
    • Confirmed in climate.py: _apply_zone_state() sets HVACMode.COOL from the DIMENSION 7 cooling context, and MESSAGE_TYPE_SEASON (*4*1*2##) intentionally preserves a non-OFF hvac_mode on standalone zones while the KW4691 never emits *4*110*2## on return to heating or on *#4*2*#14*T*3##. Worth opening a dedicated issue/PR.
  3. Upstream OWNd & OpenWebNet-Encyclopedia follow-up for *4*108*2## / *4*208*2##:
    • Nice find in run4: neither WHAT=108 nor WHAT=208 (emitted alongside the DIMENSION 7 heating/cooling comfort echoes) is documented in the Encyclopedia or decoded by OWNd yet (mode=None). We can track both upstream backed by run4.

``*#4*2*14##`` read, 49 s later, already answered 18.0.
- A ``*#4*2*#14*T*3##`` write echoes dimension 12, the season frame and dimension 0 (never
dimension 14); a dimension 14 read 0.1-0.7 s later already carries T.
- ``*4*102*2##`` (antifreeze) is the only operation that splits the two: dimension 12 reports

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.

Please qualify this with "among the operations exercised in this capture". On this same F461 + KW4691 plant in f461_live_2026-10-23 (run1_oracle_replay_readonly), *#4*2## returned *#4*2*12*0235*3##, *4*1*2## (active heating, not antifreeze), and *#4*2*14*0210*3## (12 != 14 outside antifreeze). Stating this unqualified in the present tense reads like a general hardware invariant.

comfort = _result(rows, RUN4, "*#4*2*#7*1*3##")
assert comfort["reply_time_s"] < 0.1 and comfort["events_after"] == []
assert _evt_between(rows, RUN4, "*#4*2*#7*1*3##") == ["*#4*2*7*1*3##", dim(12, "0210"), "*4*108*2##", "*#4*2*0*0239##"]
eco = _result(rows, RUN4, "*#4*2*#7*2*3##")

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.

Since *#4*2*#7*2*3## is context=2, state=3 (Cooling Comfort), not Heating Eco (*#4*2*#7*1*4##), please rename eco, after_eco, and t_eco (lines 452–475) to cooling_comfort, after_cooling_comfort, and t_cooling_comfort so the test locals don't perpetuate the parameter mix-up. Keeping "step": "eco_template_and_reads" in the JSON fixture (with a brief inline comment noting why the probe step carried that name) is fine so you don't need to regenerate the archive.

|---|---|
| Gateway | F461, firmware 2.0.11 (`*#13**16*2*0*11##`), kernel 5.10.35, distribution 2.26.4, WHO 13 device type `200`, WHO 1013 object model `134.15.5.0`, HMAC SHA-256 on both sessions (identified in `f461_live_2026-10-23`) |
| Climate | Living Now KW4691 standalone thermostat at zone 2 with a BT-F430/2 heating valve (actuator 1) and a BT-F430/4 fan coil (actuator 2); heating season; no central unit; zone 2 is the only WHO 4 device |
| Home Assistant | a local instance running the integration at #791's head (`abf6330f`) with the PyPI OWNd pin, `climate.thermo_ofice` = zone 2; its own requests are listed in `capture.home_assistant_requests` (from its debug log, which is not included) |

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.

Minor anonymization nit: please replace the room-named entity ID climate.thermo_ofice with generic climate.climate_zone_2 to match repository fixture conventions.

| `run2_fast_poll_dim12_dim14` | 18:09:45-18:57:35 | 480 alternating `*#4*2*12##` / `*#4*2*14##` reads 0.6 s apart for 5 min 45 s while Home Assistant wrote 21.0 (18:11:46) and 23.5 (18:13:39); then 42 min of idle listening |
| `run3_antifreeze_split_and_restore` | 18:58:04-19:01:32 | `*4*102*2##`, reads of 12 / 14 / `*#4*2##` in antifreeze, Home Assistant's poll, `*#4*2*#14*0235*3##`, the same reads after it; the listener ends on the valve's dimension 20 echo |
| `run4_preset_templates` | 19:02:04-19:10:00 | Home Assistant's 21.0 write (the listener's first frames are its echo), the probe's `*#4*2##`, `*#4*2*#7*1*3##`, `*#4*2*#7*2*3##` 54 s later, the season flips 47 s after that, reads of 12 / 14 / `*#4*2##`, `*#4*2*#14*0235*3##` and `*#4*2##`, Home Assistant's poll |
| `run5_bezel_in_manual_and_in_antifreeze_then_ha_write` | 19:10:42-19:12:35 | bezel +0.5 in Manual, `*4*102*2##` and `*#4*2##`, bezel +0.5 in antifreeze, reads of 12 / 14 / `*#4*2##`, `*4*102*2##` and `*#4*2##` again, Home Assistant's `climate.set_temperature` 23.5 while 12 != 14, `*#4*2##` |

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.

run5's UTC window here and in myhome_trace_F461_live_2026-10-24.json (capture.runs[4].utc, line 68) says 19:10:42-19:12:35, whereas capture.window.last is 2026-10-24T19:12:37+00:00, probe_segments[3].utc is 19:12:35-19:12:37, and the final result row (t=3731.149) has "utc": "19:12:37". Please align both to 19:10:42-19:12:37.

`*#4*2*0*0239##` and the valve opening (`*#4*2#1*#20*1##`, `*#4*2#1*20*1##`) 3 s later; the `*#4*2##` 15 s
later (already in antifreeze) read dimension 14 = 24.0.
* **A dimension 14 write echoes dimension 12, never dimension 14.** Every `*#4*2*#14*T*3##` (four from Home
Assistant, two from the probe) is ACKed in 63-78 ms and echoed as `*#4*2*12*T*3##`, `*4*1*2##`,

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.

Two small accuracy tweaks in this section (and the matching bullets in the PR description):

  1. Under "each row has its frames in the archive", only the two probe *#4*2*#14*0235*3## writes have measured reply_time_s rows in the archive (63 ms and 77 ms); Home Assistant's four writes went through HA's separate command session and have no tx/result rows here — please clarify that HA's ACK timings came from its debug log rather than archive rows.
  2. In run5, the first *#4*2## after the bezel press (t=3618.02) was sent 10.8 s later (t=3628.77, result at 12.2 s) rather than 15 s later (line 74), and the final *#4*2## after HA's write echo (t=3722.28) was sent 7.5 s later (t=3729.83, result at 8.9 s) rather than 10 s later (line 94).

next dimension 14 read already carried T: 0.7 s after the dimension 12 read for 21.0, and for 23.5 the echo
arrived while a dimension 14 read was pending, whose reply (0.1 s later) was already 23.5. The 480 reads were
all ACKed in 90-330 ms; the 240 dimension 14 frames of run 2 are all read replies.
* **`*4*102*2##` (antifreeze) is the only operation that split 12 from 14.** ACK in 65-80 ms, then

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.

Same qualification as in test_f461_live_2026_10_24_trace.py: please scope the bolded statement (and the matching bullet in the PR description) to "among the operations exercised in this capture", since f461_live_2026-10-23 (run1) already recorded 12 = 23.5 != 14 = 21.0 in active heating (*4*1*2##) on this plant.

Review 5639746797 on OpenWebNet-HA#810: the antifreeze statement is scoped to the
operations exercised in this capture (f461_live_2026-10-23 run 1 had
12 = 23.5 != 14 = 21.0 in active heating on this plant); the test locals
for *michnovka#4*2*michnovka#7*2*3## say cooling comfort (context 2, state 3), the archive
keeps the probe step name with a comment; run 5's UTC window ends at the
last result (19:12:37); the README says only the two probe writes have
measured reply_time_s rows and gives the run 5 intervals exactly
(10.8 s / 12.2 s, 7.5 s / 8.9 s); generic entity name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@gdluck
gdluck marked this pull request as ready for review October 24, 2026 20:14
@gdluck

gdluck commented Oct 24, 2026

Copy link
Copy Markdown
Author

All six tidy-ups are in f741018 and the PR is out of draft as you asked:

  1. The antifreeze statement is scoped to "among the operations exercised in this capture" in the test docstring, the README and the description, each citing f461_live_2026-10-23 run 1 (12 = 23.5 ≠ 14 = 21.0 in active heating on this plant).
  2. eco / after_eco / t_eco → cooling_comfort / after_cooling_comfort / t_cooling_comfort, with a comment that *#4*2*#7*2*3## is context 2 / state 3 and that the archive keeps the probe step name eco_template_and_reads under which the request was sent (fixture not regenerated).
  3. capture.runs[4].utc and the README row read 19:10:42-19:12:37.
  4. README and description say that only the two probe *#4*2*#14*0235*3## writes have measured reply_time_s rows (63 ms, 77 ms) and that Home Assistant's four writes went through its own session (debug-log timings); the run-5 intervals are now 10.8 s (result 12.2 s) and 7.5 s (result 8.9 s), checked against the archive rows.
  5. climate.thermo_ofice → climate.climate_zone_2.
  6. Description in the 8-section template, diagram validated.

Checks: trace module + privacy + oracle validation + health corpus 267 passed, anonymizer 0 files with personal data, ruff and docs sync clean, full suite 4513 passed at 100 % coverage.

On your follow-ups: the Heating Comfort / Heating Eco capture from the 23.5 baseline (*#4*2*#7*1*3## → 12 = 0210, *4*108*2##; *#4*2*#7*1*4## → 12 = 0180, *4*107*2##; 14 stays 0235 throughout, HA poll and entity 18.0) is recorded and posted on #791; it will come as a separate small trace PR after this one lands. The hvac_mode stuck on cool is #811; the undecoded 107 / 108 / 208 mode frames are OpenWebNet-HA/OWNd#146.

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

Thanks for the fast turnaround — verified all 6 review items in f7410188 and the updated PR description:

  1. PR #778 Template Applied: Full 8-section PR description with trace matrix and Encyclopedia / Firmware Oracle summary.
  2. Antifreeze Split Claim Scoped: Qualified to "among the operations exercised in this capture" across test_f461_live_2026_10_24_trace.py, README.md, and the PR body, cross-referencing f461_live_2026-10-23 run1.
  3. Cooling Comfort Naming Clarified: Test locals renamed to cooling_comfort / after_cooling_comfort / t_cooling_comfort with a clear note on the retained eco_template_and_reads probe step name.
  4. Metadata & Prose Aligned: capture.runs[4].utc and README.md end at 19:12:37, probe vs. HA session ACK timings are distinguished (63 ms / 77 ms), run5 intervals match the archive (10.8 s / 12.2 s, 7.5 s / 8.9 s), and the README uses generic climate.climate_zone_2.
  5. Follow-ups Split Out Cleanly: #811 (hvac_mode stuck on cool), OpenWebNet-HA/OWNd#146 (107 / 108 / 208 mode frames), and the separate DIMENSION 7 Heating Comfort / Heating Eco trace capture.

LGTM!

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 798d82c into OpenWebNet-HA:v2-phase1-architecture Oct 24, 2026
16 checks passed
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>
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