Skip to content

test(trace): ingest the F461 and MyHomeServer1 captures of 2026-10-25 (actuator status, fan speed and humidity per zone) - #815

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

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

Conversation

@gdluck

@gdluck gdluck commented Oct 25, 2026 •

Copy link
Copy Markdown

Requested by A

Refs #792, #793

What is the problem

Review 5642758778 of #792 asked for the 2026-10-25 active valve-switching capture (*#4*2#1*20*1## on 23.5 -> 26.0 °C, comment 6395632510) to be committed if the raw capture was still on disk, and for the climate entity tests to be tied to the real plant traces (the actuator burst of a MyHomeServer1 replayed through MyHOMEClimate, pump frames 0#1..0#4 creating no zone). 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. This PR backs #792 with two captures of 2026-10-25, one per gateway family, without touching the integration.


What is the solution

Two derived trace archives in the format of f461_presets_live_2026-10-24 (#812) and myhomeserver1_live_2026-10-24 (#799), each with its README and replay test module, plus 34 entries in PHYSICAL_ONLY_TX_FRAMES.

tests/fixtures/traces/f461_actuators_live_2026-10-25/ (20 event-session frames, 51 rows, 3 runs; tests/test_f461_actuators_live_2026_10_25_trace.py, 10 tests)

  • 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 feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792's head (0e44bcd7, PyPI OWNd pin) was restarted with an event-only listener on the bus.
  • Sessions: one event-only listener; no probe command session. Every request is Home Assistant's, taken from its debug log (not included) and placed on the listener's clock through the 14 frames both logs hold (spread 3 ms), so every tx / command-session rx / result row carries source: "home_assistant" and t_estimated: true; tx.t is the worker's hand-over, result.t the ACK, and the replies sit at the time the log collected each one. capture.home_assistant_entity_states holds the entity's recorded service calls and state changes for the first write (the harness's second call was dropped by Home Assistant and is listed with that note; the restore went through REST and its state change was read back, not recorded).
  • Runs: run1_startup_sweep_and_actuator_request (08:31:43-08:31:48 UTC, *#2*05##, *#4*2##, *#4*2*60##, *#4*2*19##, *#4*#0*20##), run2_setpoint_26_0_valve_on (08:32:59-08:33:02), run3_restore_23_5_valve_off (08:35:55-08:35:57).
  • Privacy: synthetic MAC 00:03:50:00:10:25 and IP 192.0.2.92 in the gateway block only (no WHO 13 frame was exchanged); generic climate.climate_zone_2; no log, no diagnostics, no local path.

tests/fixtures/traces/myhomeserver1_live_2026-10-25/ (677 frames, 1224 rows, 4 runs; tests/test_myhomeserver1_live_2026_10_25_trace.py, 10 tests)

  • Plant: the MyHomeServer1 3.87.13 of myhomeserver1_live_2026-10-24 (WHO 4 zones 35-70; 34 zones answer, 54 and 56 do not exist; fan coils Z#2 on 47, 48, 57, 60, 70; pumps 0#1-0#4; humidity probes on 17 zones). Status reads only, probe on its own command + event session, 1.2 s pause per request.
  • Runs: run1_identify_and_all_zones_actuators (*#13**16##, *#13**0##, *#4*#0*20##), run2_per_zone_reads_19_20_11_60 (zones 35..70 x *#4*Z*19##, *#4*Z*20##, *#4*Z*11##, *#4*Z*60##, 144 requests), run3_general_humidity (*#4*0*60##), run4_listen_600_s.
  • Privacy: synthetic MAC 00:03:50:00:10:26 and IP 192.0.2.9 in the gateway block only (no *#13**12## was sent); the gateway identity repeats the 2026-10-24 fixture (noted in gateway.identification_note).
  • tests/test_trace_oracle_validation.py: the 34 per-zone *#4*Z*60## reads (zones 36-70 but 39) join PHYSICAL_ONLY_TX_FRAMES with their live reply (no oracle stimulus for a per-zone humidity read; tier 1 failed on exactly those 34 frames before the entries); the *#4*35*60##, *#4*39*60## and *#4*2*60## notes record this capture's replies. Every other request (dimensions 19 / 20 / 11, *#4*#0*20##, the dimension 14 writes, *#2*05##) canonicalises to an existing verdict. Tier 4: every rx frame parses in the pinned OWNd; *#4*2#1*#20*1## / *#4*Z#1*#20*0## (the dimension written as #20) parse as OWNHeatingCommand with no zone, so no pending_decoder_rx entry is needed; the tests accept an event decoding as well (no OWNd change decodes the #20 write-form echo yet). python scripts/update_trace_matrix.py was run: the F461 and MyHomeServer1 rows already cover WHO 1 / 2 / 4 / 13, README.md / docs/trace-availability.md are unchanged. No release-notes edit, as test(trace): ingest live F461 2.0.11 2026-10-24 climate setpoint capture (knob, antifreeze split, presets, bezel, HA write) #810 / test(trace): ingest the F461 2.0.11 heating comfort / eco preset capture (dimension 12 split from 14) #812.

State Diagram

sequenceDiagram
    participant Z as F461 zone 2 KW4691
    participant H as Home Assistant (#792 head)
    Note over Z,H: run 1, startup
    H->>Z: *#4*2##, *#4*2*60##, *#4*2*19##
    Z-->>H: 0, 12 = 23.5, *4*1*2##, 14 = 23.5, 13; 60 = 34; *#4*2*19*5*0##
    H->>Z: *#4*#0*20## (+0.75 s)
    Z-->>H: *#4*2#2*20*5## (+88 ms), *#4*2#1*20*0## (+128 ms), ACK at +2.13 s
    Note over Z,H: run 2, climate.set_temperature 26.0
    H->>Z: *#4*2*#14*0260*3## (ACK 90 ms, no reply)
    Z-->>H: *#4*2*12*0260*3##, *4*1*2##, *#4*2*0*0240##
    Z-->>H: *#4*2#1*#20*1## (+2.2 s), *#4*2#1*20*1## (+37 ms): heating
    Note over Z,H: run 3, climate.set_temperature 23.5
    H->>Z: *#4*2*#14*0235*3## (ACK 104 ms, no reply)
    Z-->>H: *#4*2*12*0235*3##, *4*1*2##, *#4*2*0*0240##
    Z-->>H: *#4*2#1*#20*0## (+2.1 s), *#4*2#1*20*0## (+3 ms): idle
Loading

Review follow-up (review 5644290768)

  1. Cross-PR break with feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792: the three assertions that pinned _actuator_states as a whole dict (with the base branch's "valve" key) now assert the per-actuator keys "1" / "2" only, so both modules pass before and after feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792 merges (checked against feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792's head).
  2. Wrong companion citation: fix(message): parse '#'-prefixed dimension 14 setpoint values OWNd#138 parses #-prefixed dimension values on dimension 14 only; the *#4*Z#N*#20*V## write-form echo has no OWNd decoder yet. The attribution is dropped from both READMEs, both test modules and this description; the forward-compatible event-decoding branch stays.
  3. is_status_request pin: the F461 module accepts cmd.is_dimension_request beside the archived is_status_request value, so a later OWNd that classes dimension reads as status requests does not fail its conformance gate; the archive row itself is still pinned.

Hardware Verification

F461 2.0.11, KW4691 zone 2, 2026-10-25 08:31-08:36 UTC (every frame in the archive):

  • The all-zones actuator request is answered by both actuators within 0.2 s. *#4*#0*20##, sent 0.75 s after the startup sweep's last ACK, was answered by *#4*2#2*20*5## (fan coil, off) at +88 ms and *#4*2#1*20*0## (valve, off) at +128 ms; the ACK came 2.13 s after the request, 2.0 s after the last reply. With feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792 the entity showed hvac_action: idle, actuators: {"1": "off", "2": "off"} 12.1 s after it was added; on the previous build the first dimension 20 frame after a start was the F461's periodic broadcast 56 min later (not in the archive).
  • A setpoint write above the measured temperature switches the valve on 2.2 s after the echo. *#4*2*#14*0260*3## (ACK 90 ms, no reply) was echoed as *#4*2*12*0260*3##, *4*1*2##, *#4*2*0*0240## (no dimension 14 frame); 2.203 s after the echo *#4*2#1*#20*1##, 37 ms later *#4*2#1*20*1##. The restore (*#4*2*#14*0235*3##, ACK 104 ms) was echoed the same way; 2.126 s later *#4*2#1*#20*0##, then *#4*2#1*20*0## 3 ms later. The fan coil sent nothing but its startup reply.
  • Entity replay on the base branch: hvac_action None until *#4*2*19*5*0##, idle through the startup actuator replies, heating on *#4*2#1*20*1##, idle on *#4*2#1*20*0##; the setpoint follows the dimension 12 echoes 23.5 -> 26.0 -> 23.5 in heat. The actuators attribute is asserted only when the entity exposes it (feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792), and the internal actuator states are asserted per key "1" / "2" (the base branch adds a "valve" key from dimension 19, feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792 moves that to _zone_active), so the modules stay green on both sides of that merge.

MyHomeServer1 3.87.13, zones 35-70, 2026-10-25 10:46-11:02 UTC:

  • *#4*#0*20##: ACK in 0.50 s with 8 frames on the command session; the event session received 47 frames from +0.18 s to +1.91 s (39 after the ACK), 43 distinct: 34 Z#1, 5 Z#2 at 5, 0#1-0#4; every actuator off (the same 43 frames as the 2026-10-24 capture).
  • *#4*Z*19## / *#4*Z*20##: ACK on 34 zones in 0.16-0.25 s, NACK after 2.1 s on 54 and 56; a fan-coil zone reads *#4*Z*19*5*0## and answers Z#2 and Z#1. *#4*Z*11##: NACK on all 36 zones (the fan-speed dimension is refused on a valve zone, the F461's heating-season split of f461_live_2026-10-15). *#4*Z*60##: ACK on the 17 zones with a probe (0.19-0.29 s), NACK on the other 19; *#4*0*60## answers the same 17 in 0.66 s.
  • Entity replay on the base branch: zones 35 and 47 configured, the burst routed as the platform routes it (_zone_route_keys): both idle with every actuator off, zone 47 with fan_mode: auto / running_fan_speed: off; the pump frames have no route key and reach no entity; the per-zone reads keep both idle and give zone 47 its humidity; the listen gives both their heating season. Nine spontaneous valve off reports, each *#4*Z#1*#20*0## then the status form within 50 ms, each about 2 s after the zone's pump call.

How is it tested

  • pytest tests/test_f461_actuators_live_2026_10_25_trace.py: 10 passed. Schema and synthetic identity, every frame parses in the pinned OWNd (the #20 command form as OWNHeatingCommand, an event decoding accepted), result rows consistent with their tx / rx rows (replies between the request and its ACK, writes with no reply and their echo after the ACK), the startup sweep, the actuator request answered within 0.2 s, the two writes and their valve reports with offsets, the fan coil staying 5 / off, and two replays through the climate entity (idle -> heating -> idle; the recorded live states).
  • pytest tests/test_myhomeserver1_live_2026_10_25_trace.py: 10 passed. Schema, parsing and route keys (zone frames to their zone, pump frames to none, a pump call to its zone), result-row consistency with NACKs, identification, the 43-frame burst, dimensions 19 / 20 per zone, dimension 11 refused everywhere, humidity only where a probe exists (per zone and general), the house's traffic, and the replay of zones 35 and 47.
  • pytest tests/test_trace_oracle_validation.py tests/test_device_health_corpus.py tests/test_fixture_privacy.py: 256 passed (tier 1 failed on the 34 per-zone humidity reads 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: 4581 passed, 1 skipped, 100 % statement coverage (16,151 / 16,151).
  • 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), scripts/anonymize_plant_fixture.py --check (6 files, 0 with personal data): all pass.

Where are the traces

Subsystem Capture Plant Frames verified by this PR
Actuator status at startup tests/fixtures/traces/f461_actuators_live_2026-10-25/myhome_trace_F461_actuators_live_2026-10-25.json, run1_startup_sweep_and_actuator_request F461 2.0.11, KW4691 zone 2 *#4*2## -> 0, 12, *4*1*2##, 14, 13; *#4*2*19## -> *#4*2*19*5*0##; *#4*#0*20## -> *#4*2#2*20*5##, *#4*2#1*20*0## (+88 / +128 ms, ACK +2.13 s)
Valve on after a setpoint write same file, run2_setpoint_26_0_valve_on same *#4*2*#14*0260*3## -> *#4*2*12*0260*3##, *4*1*2##, *#4*2*0*0240##; *#4*2#1*#20*1##, *#4*2#1*20*1## (+2.2 s)
Valve off after the restore same file, run3_restore_23_5_valve_off same *#4*2*#14*0235*3## -> *#4*2*12*0235*3##, *4*1*2##, *#4*2*0*0240##; *#4*2#1*#20*0##, *#4*2#1*20*0## (+2.1 s)
All-zones actuator burst, pumps tests/fixtures/traces/myhomeserver1_live_2026-10-25/myhome_trace_MyHomeServer1_live_2026-10-25.json, run1_identify_and_all_zones_actuators MyHomeServer1 3.87.13, zones 35-70 *#4*#0*20## -> 43 distinct *#4*Z#1*20*0## / *#4*Z#2*20*5## / *#4*0#N*20*0##
Per-zone valves, actuators, fan speed, humidity same file, run2_per_zone_reads_19_20_11_60, run3_general_humidity same *#4*Z*19##, *#4*Z*20## (34 ACK / 2 NACK), *#4*Z*11## (36 NACK), *#4*Z*60## (17 ACK / 19 NACK), *#4*0*60## (17 zones)
Previous actuator pair and 43-frame burst (referenced, not duplicated) f461_live_2026-10-23 run 6, myhomeserver1_live_2026-10-24 run 2 (#799) same plants *#4*2#1*20*0## + *#4*2#2*20*5## 2.2 s after a zone poll; *#4*#0*20## -> 43 actuator frames

What does the Firmware Oracle / Encyclopedia say

  1. Firmware Oracle: the emulated gateways have no thermostat behind them, so no oracle row produces an actuator burst or a valve switch; *#4*#0*20##, *#4*Z*19##, *#4*Z*20##, *#4*Z*11## and the dimension 14 writes canonicalise to existing verdicts (tier 1 passes), while the per-zone *#4*Z*60## reads have no stimulus and are recorded in PHYSICAL_ONLY_TX_FRAMES with their live replies. own-firmware-oracle#60 (*#4*#0*20## forwarding matrix) gets the physical reply orders and ACK timings of both families (F461: both actuators within 0.13 s, ACK after 2.13 s; MyHomeServer1: ACK in 0.50 s ahead of a 1.9 s burst).
  2. OpenWebNet Encyclopedia (WHO 4 dimensions): dimension 20 is *#4*Z#N*20*V## with V 0 / 1 for a valve and 5-8 for a fan coil, dimension 19 the valve status per side, dimension 11 the fan speed; the *#4*Z#N*#20*V## form the F461 and the MyHomeServer1 emit before each status form is the write form of the same dimension, decoded by neither the Encyclopedia nor the pinned OWNd; no OWNd PR covers it yet (fix(message): parse '#'-prefixed dimension 14 setpoint values OWNd#138 only parses #-prefixed dimension values on dimension 14).

Checklist

  • PR Description: Follows the structured sections above (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).
  • Trace-Backed (if protocol/device change): The fixtures are the physical traces themselves; every test pins a frame of them.
  • V2 Architecture: N/A (fixtures and tests only; the entity replays go through handle_event() and the platform's route keys).
  • Tests & Linters: pytest, ruff, and scripts/verify_ha_standards.py pass.

🤖 Generated with Claude Code

… (actuator status, fan speed and humidity per zone)

Two physical captures behind OpenWebNet-HA#792 (actuator status at startup, hvac_action from
dimension 20) and OpenWebNet-HA#793 (humidity gate), asked for in review 5642758778 of OpenWebNet-HA#792.

f461_actuators_live_2026-10-25: a local Home Assistant at OpenWebNet-HA#792's head restarted
with an event-only listener on the F461 2.0.11 / KW4691 zone 2 plant. The
startup sweep is followed by *michnovka#4*#0*20##, answered by both actuators within
0.13 s (ACK after 2.13 s); climate.set_temperature 26.0 makes the valve report
itself on 2.2 s after the dimension 12 echo (*michnovka#4*2#1*OpenWebNet-HA#20*1##, then
*michnovka#4*2#1*20*1##), the restore to 23.5 makes it report itself off 2.1 s after the
echo; the fan coil stays 5 / off. Every request is Home Assistant's, from its
debug log, placed on the listener clock (source: home_assistant, t_estimated).
The replay through the climate entity gives hvac_action idle -> heating -> idle
and 23.5 -> 26.0 -> 23.5; the actuators attribute is asserted only where the
entity exposes it.

myhomeserver1_live_2026-10-25: status reads only on the MyHomeServer1 3.87.13
plant (zones 35-70): *michnovka#4*#0*20## ACKed in 0.50 s ahead of a 1.9 s burst of 43
distinct actuator frames (34 Z#1, 5 Z#2, the pumps 0#1..0#4, every one off);
per zone *michnovka#4*Z*19## / *michnovka#4*Z*20## ACK on 34 zones and NACK on 54 / 56,
*michnovka#4*Z*11## NACK on all 36 (the fan-speed dimension is refused on a valve zone),
*michnovka#4*Z*60## ACK on the 17 zones with a probe and NACK on 19, *michnovka#4*0*60## answers
the same 17; a 600 s listen with the house's own traffic. The replay of zones 35
and 47 through the climate entity gives idle with every actuator off; the pump
frames have no route key and reach no zone.

The 34 per-zone *michnovka#4*Z*60## reads join PHYSICAL_ONLY_TX_FRAMES (no oracle
stimulus for a per-zone humidity read). Every rx frame parses in the pinned
OWNd; the 'OpenWebNet-HA#20' command forms parse as OWNHeatingCommand (OpenWebNet-HA/OWNd#138
decodes them, both are accepted). Synthetic MACs 00:03:50:00:10:25 /
00:03:50:00:10:26, documentation IPs; no log, no local path.

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.

Audit of PR #815 (b8b72b5b)

Branch / Base: gdluck:test/trace-f461-actuators-live-2026-10-25 (b8b72b5b) → v2-phase1-architecture (089fef51) — 7 files (+19,611 / -3)

Verification & Trace Forensics:

  • GitHub CI & Validators: All 16 check-runs pass, Codecov diff coverage is 100%, and pytest (OWNd==2.0.0b11 + OWNd dev), ruff check, scripts/verify_ha_standards.py, scripts/sync_documentation.py --check, scripts/typing_ratchet.py, scripts/update_trace_matrix.py, and scripts/anonymize_plant_fixture.py --check (0 with personal data) all pass cleanly.
  • Physical Trace Authenticity (Verified):
    • myhomeserver1_live_2026-10-25 (MyHomeServer1 fw 3.87.13, 677 evt frames / 1,224 rows): verified genuine hardware capture — 7 / 7 outlier NACKs (2.81–5.81 s) coincide with spontaneous background house traffic resetting the gateway's SCS bus-quiet window (vs 1 / 52 normal 2.09–2.19 s NACKs), 42 / 43 positions in the *#4*#0*20## burst arrive in a non-deterministic CSMA/CA order relative to 2026-10-24, internal WHO 13 reads answer in 17–25 ms vs 139–290 ms (median 185 ms) on the 9600-baud SCS bus, the 12:50:00 → 13:00:00 Sunday 25/10/2026 clock broadcasts sit 600.048 s apart under manual TZ 999, all 34 active zones / 17 humidity probes / 74 of 75 WHO 1 addresses match 2026-10-24, and all 36 *#4*Z*60## annotations in PHYSICAL_ONLY_TX_FRAMES match the session log rows.
    • f461_actuators_live_2026-10-25 (F461 fw 2.0.11, 20 evt frames / 51 rows): verified genuine OWNEventSession capture with reconstructed HA cmd rows transparently marked (source: "home_assistant", t_estimated: true, 1–2 ms spread across all 10 dual-logged run1 replies, matching #792 comment 6395632510 and the [306, 1, 326, 93] ms *#4*2## burst fingerprint of f461_live_2026-10-23 / -24).

Findings to Address Before Merge

1. [High — Cross-PR CI Break with #792] Replay tests hardcode "valve": False in ._actuator_states

  • Where:
    • tests/test_f461_actuators_live_2026_10_25_trace.py lines 507 and 517:
      • assert zone.hvac_action == HVACAction.HEATING and zone._actuator_states == {"valve": False, "2": False, "1": True}
      • assert zone._actuator_states == {"valve": False, "2": False, "1": False}
    • tests/test_myhomeserver1_live_2026_10_25_trace.py line 461:
      • assert fan_coil._actuator_states == {"2": False, "1": False, "valve": False}, "dimension 19 adds the valve key"
  • What happens: The PR description states "The actuators attribute is asserted only when the entity exposes it (#792), so the module stays green on both sides of that merge." However, #792 (4a41da61, commit a24ad505) separates Dimension 19 (self._zone_active) from self._actuator_states so "valve" is no longer added. Merging b8b72b5b + 4a41da61 immediately fails both test_replay_hvac_action_idle_heating_idle and test_replay_actuator_burst_gives_idle_and_pumps_reach_no_zone ({'1': True, '2': False} != {'1': True, '2': False, 'valve': False}).
  • Recommendation: Assert the per-actuator keys "1" and "2" (as line 498 already does), or branch on "actuators" in zone.extra_state_attributes ({"1": ..., "2": ...} when #792 is present vs {..., "valve": False} on base) so both modules actually pass before and after #792 merges.

2. [Low — Wrong Companion PR Citation] OpenWebNet-HA/OWNd#138 does not cover *#4*Z#N*#20*V##

  • Where: PR body (2×), tests/fixtures/traces/f461_actuators_live_2026-10-25/README.md (L97, L113), tests/fixtures/traces/myhomeserver1_live_2026-10-25/README.md (L70, L91), tests/test_f461_actuators_live_2026_10_25_trace.py (L32, L318), and tests/test_myhomeserver1_live_2026_10_25_trace.py (L255).
  • What happens: OpenWebNet-HA/OWNd#138 (fix(message): parse '#'-prefixed dimension 14 setpoint values) only parses #-prefixed dimension values on Dimension 14 (*#4*#1*14*#0230*1##), and its test_hash_values_elsewhere_still_do_not_parse explicitly rejects # elsewhere. *#4*2#1*#20*1## / *#4*Z#1*#20*0## have a #-prefixed dimension number (DIMENSION_WRITING echo) and are not covered by OWNd#138.
  • Recommendation: Drop the (OpenWebNet-HA/OWNd#138) attribution (or note that no OWNd PR covers #20 write-form echoes yet while keeping the forward-compatible OWNHeatingEvent branch in the parser assertions).

3. [Nit — Forward-Compatibility] Avoid pinning cmd.is_status_request == False on WHO 4 dimension reads

  • Where: tests/test_f461_actuators_live_2026_10_25_trace.py line 302 (assert bool(cmd.is_status_request) == r["is_status_request"]).
  • What happens: In the fixture, *#4*2*60##, *#4*2*19##, and *#4*#0*20## record "is_status_request": false (OWNd <= 2.0.0b11). If OWNd later classifies is_dimension_request frames as is_status_request = True, line 302 will fail OWNd's conformance gate (whereas line 401 checks only the static JSON field tx["is_status_request"], and myhomeserver1_live_2026-10-25 does not assert equality against OWNCommand.parse). Allowing cmd.is_dimension_request when r["is_status_request"] is False keeps the test forward-compatible.

if r["dir"] == "tx":
cmd = OWNCommand.parse(r["frame"])
assert cmd is not None, r
assert bool(cmd.is_status_request) == r["is_status_request"], r

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.

If OWNd later classifies WHO 4 dimension reads (*#4*2*60##, *#4*2*19##, *#4*#0*20##, where cmd.is_dimension_request is True) as is_status_request = True, this strict equality against the captured r["is_status_request"] (False) will fail OWNd CI. Allowing bool(cmd.is_status_request) == r["is_status_request"] or getattr(cmd, "is_dimension_request", False) keeps this forward-compatible while still pinning tx["is_status_request"] on the archive row at line 401.

assert OWNMessage.parse(FAN_COIL_OFF).fan_on is False, "value 5 is the fan-coil side off (OWNd's dimension 20 >= 5 rule)"
# the '#20' command form the F461 emits 3 - 37 ms before each status form: the pinned OWNd
# (<= 2.0.0b11) parses it as a command with no zone (the integration logs "Unsupported message
# type" and the entity never sees it); OpenWebNet-HA/OWNd#138 decodes the '#'-prefixed dimension

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.

OpenWebNet-HA/OWNd#138 only parses #-prefixed values on Dimension 14 (*#4*#1*14*#0230*1##) and explicitly rejects # elsewhere — it does not decode #-prefixed dimension numbers like *#4*2#1*#20*1##. Please update this reference here, in the module docstrings (test_f461_...: L32, test_myhomeserver1_...: L255), in both fixture README.md files, and in the PR description (keeping the else event-decoding branch itself is great).

assert seen[0][1:] == (HVACAction.IDLE, 26.0, HVACMode.HEAT), "the echo moves the setpoint, not the action"
assert all(action == HVACAction.IDLE for frame, action, *_ in seen if not frame.startswith("*#4*2#1")), "idle until the valve reports"
assert seen[-1] == (VALVE_ON, HVACAction.HEATING, 26.0, HVACMode.HEAT)
assert zone.hvac_action == HVACAction.HEATING and zone._actuator_states == {"valve": False, "2": False, "1": True}

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.

Hardcoding "valve": False here and at line 517 breaks as soon as #792 (a24ad505) merges, because #792 moves Dimension 19 onto self._zone_active (zone._actuator_states becomes {"2": False, "1": True} / {"2": False, "1": False}). Please check keys "1" and "2" (as line 498 does) or branch on "actuators" in zone.extra_state_attributes so the test stays green on both sides of #792.

assert valve.extra_state_attributes["fan_coil_heating"] is False and valve.extra_state_attributes["fan_coil_cooling"] is False
assert fan_coil.hvac_action == HVACAction.IDLE and fan_coil.current_humidity == 58.0
assert fan_coil.extra_state_attributes["fan_coil_cooling"] is True and fan_coil.extra_state_attributes["fan_coil_heating"] is False
assert fan_coil._actuator_states == {"2": False, "1": False, "valve": False}, "dimension 19 adds the valve key"

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 cross-PR break with #792 as in test_f461_actuators_live_2026_10_25_trace.py:507,517: on #792, Dimension 19 sets self._zone_active rather than inserting "valve" into self._actuator_states, so fan_coil._actuator_states is {"2": False, "1": False} and this assertion fails.

…; drop the OWNd#138 attribution

Review of OpenWebNet-HA#815 (5644290768): the replay tests asserted the whole
_actuator_states dict including the base branch's "valve" key, which OpenWebNet-HA#792
moves to _zone_active; they now assert the per-actuator keys only. The
'OpenWebNet-HA#20' write-form echoes are not covered by OpenWebNet-HA/OWNd#138 (dimension
14 values only), so that attribution is removed from the READMEs and tests.
The F461 tx check accepts is_dimension_request beside the archived
is_status_request value, so a later OWNd classification cannot fail the
conformance gate.

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

gdluck commented Oct 25, 2026

Copy link
Copy Markdown
Author

Thanks for the forensic pass; the three points are in 50c740e.

  1. Cross-PR break with feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792: the three assertions that pinned _actuator_states as a whole dict (with the base branch's "valve" key from dimension 19) now assert the per-actuator keys "1" / "2" only. Both modules were run against feat(climate): request actuator status (dimension 20) on add and after reconnect, expose actuators, derive hvac_action #792's head (4a41da6) with the fixtures copied in: 20 passed, and 20 passed on this branch, so they stay green on both sides of that merge.
  2. fix(message): parse '#'-prefixed dimension 14 setpoint values OWNd#138 only parses #-prefixed dimension values on dimension 14; the attribution is dropped from both READMEs, both test modules and the description, which now say that no OWNd change decodes the *#4*Z#N*#20*V## write-form echo yet. The forward-compatible event-decoding branch stays.
  3. is_status_request: the F461 module accepts cmd.is_dimension_request beside the archived value, so a later OWNd that classes dimension reads as status requests cannot fail its conformance gate; the archive row's own field is still pinned at line 401.

Privacy check 0 findings, ruff clean; the full suite is unchanged apart from these three modules' assertions.

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