Skip to content

feat(message,profiles): add declarative expose frame builders, query helpers, and profile flags - #129

Merged
GreenGrassBlueOcean merged 2 commits into
masterfrom
feat/declarative-expose
Oct 20, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
masterfrom
feat/declarative-expose

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What is the problem

Downstream virtual-device emulation and bus-state injection (the expose: engine in OpenWebNet-HA/MyHOME#750, verified on physical hardware in OpenWebNet-HA/MyHOME#753 and issuecomment-6277050571) require four protocol-layer capabilities that were previously missing or incomplete in OWNd:

  1. Missing dimension-response (report_*) and dimension-write (write_* / set_*) frame builders:
    • OWNd previously provided actuator command and request builders (switch_on, set_brightness, raise_shutter, set_temperature, get_total_consumption, request_datetime), but lacked builders for dimension response frames (*#WHO*WHERE*DIM*VAL...##) and WHO=4 Dimension #0 temperature writes (*#4*WHERE*#0*TTTT##) needed when acting as a virtual responder or injecting external state onto the SCS bus across WHO=1 (Lighting), WHO=2 (Automation), WHO=4 (Thermoregulation), WHO=13 (Gateway Clock), and WHO=18 (Energy Management).
  2. Missing incoming query / translation classification helpers on OWNMessage and OWN*Command:
    • A virtual responder listening on the event session must distinguish status requests (*#WHO*WHERE##), dimension requests (*#WHO*WHERE*DIM##), dimension writes (*#WHO*WHERE*#DIM*VAL##), dimension replies (*#WHO*WHERE*DIM*VAL##), and pushbutton command-translation frames (*WHO*1000#WHAT*WHERE##) without ad-hoc string regexes in downstream application code.
  3. Hardware-specific gateway injection capabilities (Report Form vs. #DIM Write Form):
    • Real BTicino/Legrand gateways treat incoming un-hashed dimension response frames (*#WHO*WHERE*DIM*VAL##) vs. #DIM write frames (*#WHO*WHERE*#DIM*VAL##) differently across subsystems and hardware families:
      • WHO=1, WHO=2, WHO=4, WHO=13: On physical F461 (2.0.11), un-hashed response frames on the TCP command session are either NACKed (*#1*08*1*150*0##, *#2*25*10*10*50*0*0##) or answered as dimension reads (*#4*2*0*0999## returns Zone 2's measured *#4*2*0*0249##; *#13**0*22*51*00*999## returns the gateway's own time *#13**0*22*51*02*999##). The #DIM write frames (*#1*08*#1*150*0##, *#2*25*#11#001*50##, *#4*50*#0*0215##, *#13**#0*…##) are ACKed. Timings and sources are in the trace section below.
      • WHO=18: Report-form frames (*#18*51*113*450##, *#18*51*51*12345##, *#18*51*54*320##) are ACKed on physical F461 in 1.27–1.40 s, with no event after the ACK, so the value is not shown to reach the bus. The plain read *#18*51*113## is ACKed in 0.91 s without a reply too.
      • No other gateway family has a capture or a published oracle verdict for these frames.
  4. Infeasible parameter and address bounds on expose builders (extending OpenWebNet-HA/OWNd#128):
    • New dimension-reply and dimension-write builders must enforce the same strict input boundaries established in OWNd#128: rejecting empty or *-polluted WHERE addresses, bool values (True/False disguised as int/float), non-finite floats (NaN, ±inf, raising ValueError rather than OverflowError), finite fractional floats on protocol-integer fields (e.g. 50.9 rather than silently truncating via int()), naive datetime/time inputs lacking an explicit time_zone, and out-of-range hardware parameters before constructing frames.

(Note: The command-session post-write retry guard for is_status_request=True was split into standalone PR OpenWebNet-HA/OWNd#131.)


How is it solved

  1. Base Message Classification (OWNd/message/base.py):
    • Added is_status_request (*#WHO*WHERE##), is_dimension_request (*#WHO*WHERE*DIM##), is_dimension_writing (*#WHO*WHERE*#DIM*...##), is_dimension_reply (*#WHO*WHERE*DIM*VAL...##), and effective_what (unwrapping 1000#WHAT translation frames to integer WHAT) on OWNMessage.
  2. Declarative Expose Builders & Query Properties (OWNd/message/):
    • WHO=1 (OWNLightingCommand):
      • is_status_query (*#1*WHERE##) and is_brightness_query (*#1*WHERE*1## or *#1*WHERE*4##).
      • report_brightness(where, level, transition=0) $\to$ *#1*WHERE*1*LEVEL*SPEED## (encoding 0..100% into OpenWebNet 100..200, where 100 = OFF).
    • WHO=2 (OWNAutomationCommand):
      • is_status_query (*#2*WHERE##) and is_shutter_status_query (*#2*WHERE*10##).
      • report_shutter_level(where, level, state=10, priority=0, info=0) $\to$ *#2*WHERE*10*STATE*LEVEL*PRIORITY*INFO## (supporting 0..100% or 255 unknown, states 10..15, and both integer 0..255 and $\le 3$-digit priority bitfield strings such as "001" / "000").
    • WHO=4 (OWNHeatingCommand & OWNHeatingEvent):
      • is_temperature_query (*#4*WHERE*0##).
      • encode_temperature(temperature) $\to$ 4-digit signed tenths (0TTT for $\ge 0^\circ\text{C}$, 1TTT for $< 0^\circ\text{C}$, validated to finite [-99.9, +99.9]°C, using Decimal with ROUND_HALF_UP and avoiding -0.0 sign artifacts).
      • report_temperature(where, temperature) $\to$ *#4*WHERE*0*TTTT## and write_temperature(where, temperature) $\to$ *#4*WHERE*#0*TTTT##.
      • OWNHeatingEvent: adds probe_temperature property (returning the scalar float temperature for 3-digit external probes 101–899 alongside secondary_temperature == [sensor, temp]) while preserving existing unique_id semantics ("4-#0" for WHERE="000" / zone == 0, "4-101" for WHERE="101").
    • WHO=13 (OWNGatewayCommand):
      • is_clock_query (*#13**0##, *#13**1##, *#13**22##).
      • set_datetime(dt, time_zone=None) (*#13**#22*...##), report_datetime(dt, time_zone=None) (*#13**22*...##), set_date(d, time_zone=None) (*#13**#1*...##), report_date(d, time_zone=None) (*#13**1*...##), set_time(t, time_zone=None) (*#13**#0*...##), report_time(t, time_zone=None) (*#13**0*...##), accepting datetime.tzinfo | str | None for time_zone, rejecting naive datetime/time inputs when time_zone is None, and rejecting UTC offsets that are not whole hours (Asia/Kolkata, Asia/Kathmandu, America/St_Johns), which the 0HH/1HH field cannot carry. The report_* forms are for answering on the event side. On the F461 command session they are answered as reads, so injection uses set_*.
    • WHO=18 (OWNEnergyCommand):
      • is_dimension_query (*#18*WHERE*DIM##), is_auto_update_command (*#18*WHERE*#1200#1*TIME##), and stream_duration (0..255 minutes).
      • report_active_power(where, watts) (*#18*WHERE*113*W##), report_total_energy(where, wh) (*#18*WHERE*51*Wh##), report_current_month_energy(where, wh, year=None, month=None) (*#18*WHERE*52[#YY#M]*Wh##), report_partial_month_energy(where, wh, month=None) (*#18*WHERE*53[#M]*Wh##), report_partial_day_energy(where, wh, day=None) (*#18*WHERE*54[#D]*Wh##), and report_streaming_state(where, duration, energy_type=1) (*#18*WHERE*1200#TYPE*TIME##).
  3. Gateway Profile Capability Flags (OWNd/profiles.py):
    • Three tri-state flags on GatewayProfile, all bool | None = None. None means unverified: no capture or published oracle verdict exists for that family. Callers decide what to do with None.
      • supports_dimension_response_injection: whether reply-form frames (*#1*W*1*…##, *#2*W*10*…##, *#4*W*0*T##, *#13**D*…##) inject state on the command session.
      • supports_who4_command_injection: whether *#4*W*#0*TTTT## is accepted.
      • supports_who18_response_injection: whether *#18*W*DIM*VAL## is acknowledged. This is not proof that the value reaches the bus.
    • Only F461Profile sets them (False, True, True), from the live captures cited in its docstring. Every other profile, MH201Profile included, stays None.
  4. Infeasible Input & Boundary Validation Across All Builders (OWNd/message/{lighting,automation,heating,energy,gateway}.py):
    • Enforces require_non_empty_where(where) across report_brightness, report_shutter_level, report_temperature, write_temperature, and all OWNEnergyCommand.report_* builders.
    • Rejects bool, non-finite floats (NaN, ±inf), and finite fractional floats (isinstance(x, float) and x % 1 != 0, e.g. 50.9) with ValueError rather than silently truncating via int() across all protocol-integer builder parameters, and enforces domain bounds: report_brightness (0 <= level <= 100, 0 <= transition <= 255), set_shutter_level (0 <= level <= 100), report_shutter_level (level in 0..100 or 255, 10 <= state <= 15, 0 <= priority, info <= 255, preserving $\le 3$-digit string priority), encode_temperature (math.isfinite, -99.9 <= temperature <= 99.9, ROUND_HALF_UP), OWNEnergyCommand.report_* (_validate_non_negative_energy, paired integral 2000 <= year <= 2099 and 1 <= month <= 12, integral 1 <= day <= 31, integral 1 <= energy_type <= 4, integral 0 <= duration <= 255), and OWNGatewayCommand (datetime.datetime / datetime.date / datetime.time type checks, explicit time_zone required for naive datetime/time, and 2000 <= year <= 2099).

State diagram

stateDiagram-v2
    direction TB

    state "Bus Query / Virtual-Device Injection Lifecycle" as ExposeFlow {
        [*] --> EventMonitor: Frame received on Event Session
        EventMonitor --> QueryDetected: is_status_query / is_brightness_query / is_shutter_status_query / is_temperature_query / is_clock_query / is_dimension_query
        EventMonitor --> StreamDetected: is_auto_update_command (*#18*W*#1200#1*T##)
        EventMonitor --> TranslationDetected: is_translation (*WHO*1000#WHAT*WHERE##) -> effective_what
        QueryDetected --> ProfileGate: Check GatewayProfile capabilities
        StreamDetected --> ProfileGate
        ProfileGate --> InjectDimWrite: WHO in {1, 2, 4} & supports_dimension_response_injection is not True\n-> set_brightness (*#1*W*#1*L*S##) / set_shutter_level (*#2*W*#11#001*P##) / write_temperature (*#4*W*#0*TTTT##)
        ProfileGate --> InjectWho18Reply: WHO == 18 & supports_who18_response_injection is not False\n-> report_active_power / report_total_energy / report_streaming_state (*#18*W*DIM*VAL##), ACK only
        ProfileGate --> InjectClockWrite: WHO == 13\n-> set_datetime (*#13**#22*...##) / set_time / set_date (report_* forms are answered as reads on F461)
        ProfileGate --> SkipUnsupported: flag is False (none set today)
        InjectDimWrite --> [*]
        InjectWho18Reply --> [*]
        InjectClockWrite --> [*]
        SkipUnsupported --> [*]
    }
Loading

How is it tested

  • Unit & Boundary Tests (tests/test_message_protocol.py):
    • test_own_message_helper_properties_and_effective_what: verifies is_status_request, is_dimension_request, is_dimension_writing, is_dimension_reply, is_translation, effective_what, and all WHO=1, WHO=2, WHO=4, WHO=13, and WHO=18 builders and query detectors (including 3-digit priority bitfield strings like priority="001" on OWNAutomationCommand.report_shutter_level, datetime.tzinfo and IANA string timezones, and aware vs naive datetime.time).
    • test_expose_builders_infeasible_inputs: exhaustively tests rejection (ValueError) of bool inputs (True/False), NaN, ±inf, finite fractional floats (50.9, 0.5, 10.5, 2026.5, 18.5, 60.5, 1.5) across all integer-valued builder parameters, out-of-range brightness/shutter levels, out-of-range temperatures (< -99.9 or > 99.9), ROUND_HALF_UP half-way rounding (20.25 -> 0203, 20.35 -> 0204), negative energy values, invalid years/months/days, naive datetime/time without time_zone, invalid time_zone types, half- and quarter-hour UTC offsets, and empty/malformed WHERE addresses.
  • Profile Capability Tests (tests/test_profiles.py):
    • test_profile_injection_capability_flags: F461 is False / True / True; every other profile and the generic fallback are None on all three flags.
  • Golden Corpus Conformance & Builder Parity (tests/test_golden_conformance.py, tests/golden/frames/*.yaml):
    • Added 32 community-plant-capture fixtures across who01_lighting.yaml, who02_automation.yaml, who04_thermo.yaml, who13_gateway.yaml, and who18_energy.yaml (bringing the corpus to 164 validated frames, including the 2026-10-18 live F461 capture from MyHOME#753 and the 2026-10-19 F461 plant-check table in MyHOME#750 issuecomment-6277050571) with builder: blocks pinning OWNLightingCommand.report_brightness, OWNAutomationCommand.set_shutter_level/report_shutter_level, OWNHeatingCommand.report_temperature/write_temperature, OWNGatewayCommand.report_time/set_time/report_date/set_date/report_datetime/set_datetime, and OWNEnergyCommand.report_active_power/report_total_energy/report_partial_month_energy/report_partial_day_energy/report_streaming_state byte-for-byte against real hardware captures.
  • Full Suite & Static Analysis:
    • 1,894 passed, 1 skipped locally on Python 3.14 (CI covers 3.11–3.14) with 100.0% statement and branch coverage on all modified modules (automation.py, base.py, energy.py, gateway.py, heating.py, lighting.py, profiles.py), mypy OWNd tests/typing clean (0 issues), ruff check clean.

Where are the traces

1. Real Physical Bus Captures by Community Contributors (community-plant-capture)

Every frame shape built or classified in this PR is backed by authentic physical bus captures contributed by the community and pinned with builder: blocks in tests/golden/frames/ (provenance recorded in tests/golden/SOURCE.yaml):

Subsystem Real Community Capture Files (MyHOME/tests/fixtures/traces/ & OWNd/tests/golden/frames/) Contributor & Hardware Plant Captured Wire Frames Verified by This PR
Live F461 (2.0.11) Expose (WHO=1, 2, 4, 13, 18), F411/4 Lights & F411/2 5 s Shutter (2026-10-18 capture, 2026-10-19 plant check) f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json (OpenWebNet-HA/MyHOME#753)
Plant-check table in MyHOME#750 issuecomment-6277050571 (text only, no capture file)
OWNd/tests/golden/frames/who02_automation.yaml
OWNd/tests/golden/frames/who04_thermo.yaml
@gdluck & @GreenGrassBlueOcean (MyHOME#753 & MyHOME#750) — BTicino F461 (FW 2.0.11, WHO=13 code 200, WHO=1013 134.15.5.0) + BT-F411/4 (06, 07), BT-F411/2 5 s shutter (05), KW4691 + F430/2 + F430/4 (Zone 2) Physical shutter 05 (*2*1000#0*05## $\to$ *2*0*05## $\to$ *2*1000#1/2*05## $\to$ *2*1/2*05## $\to$ 5 s auto-stop *2*0*05##); virtual cover 25 event-session translation bursts before NACK on cmd.
2026-10-18 trace: *#1*08*1*150*0## NACK 0.055 s vs *#1*08*#1*150*0## ACK 0.126 s; *#2*25*10*10*50*0*0## (priority 0) NACK 0.055 s; *#4*2*0*0999## answered as a read with *#4*2*0*0249## (0.22 s); *#4*50*0*0215## NACK after 2.13 s vs *#4*50*#0*0215## ACK 0.155 s; *#13**0*22*51*00*999## answered as a read with *#13**0*22*51*02*999## (0.116 s) vs *#13**#0*22*51*00*999## ACK 0.071 s; *#18*51*113*450## / *#18*51*51*12345## / *#18*51*54*320## ACK 1.27–1.40 s, *#18*51*#1200#1*1/0## ACK 0.14–0.15 s, no events after.
2026-10-19 plant check (comment table): *#1*08*1*150*0## NACK 6–10 ms vs #1 write ACK 83 ms; *#2*25*10*10*50*001*0## NACK 10 ms vs *#2*25*#11#001*50## ACK 83 ms, echoed *#2*25*#11#001#1*50##; *#4*50*0*0215## read, NACK after 4.2 s vs *#4*50*#0*0215## ACK 88 ms; *#18*51*113*450## ACK 1.2–1.7 s; the WHO 13 write frame sets the gateway clock.
WHO=1 Lighting (Dim 1 report_brightness & 1000#WHAT translation) issue_466/myhome_trace_MH202_all_2026-09-26T16-59-17.json
issue_466/myhome_trace_F454_f418u2_dimmer_2026-09-27T08-58-54.json
issue_466/myhome_trace_MH202_f418u2_dimmer_2026-09-27T09-43-45.json
OWNd/tests/golden/frames/who01_lighting.yaml
@anotherjulien (MyHOME#466 comments 5849027587, 5854469497, 5854805415) — BTicino MH202 (FW 2.6.13) + F454 (FW 2.0.5) + F418U2 2-ch dimmer (WHERE 14, 32) Pushbutton translations *1*1000#1*14##, *1*1000#0*14##, *1*1000#30*14##, *1*1000#31*14## (effective_what) and Dim 1 replies *#1*32*1*100*0## (OFF), *#1*32*1*130*5##, *#1*14*1*130*5##..*#1*14*1*200*5## (report_brightness)
WHO=2 Automation (Dim 10 report_shutter_level, Dim 11 set_shutter_level & 1000#WHAT translation) issue_304/myhome_trace_MyHomeServer1_who2_2026-10-08T10-49-50.json
issue_304/myhome_trace_MyHomeServer1_who2_2026-10-08T10-49-12.json
issue_466/myhome_trace_MH202_all_2026-09-26T16-59-17.json
issue_466/myhome_trace_MyHomeServer1_scenarios_cover_diagnostic_2026-09-27T13-47-33.json
f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json
OWNd/tests/golden/frames/who02_automation.yaml
@f18m (MyHOME#304 comment 6060208135 — MyHomeServer1 FW 2.53.16 + LN-4660M2 shutters 91..95), @anotherjulien (MH202 + F454, shutters 15, 22, 31), @TheDarkWizard (MyHOME#466 comment 5857411555), @gdluck & @GreenGrassBlueOcean (F461 FW 2.0.11 + BT-F411/2 05, MyHOME#753 & MyHOME#750) *#2*95*#11#001*29## $\to$ *2*1000#0*95## $\to$ *#2*95*10*12*40*000*0## (closing) $\to$ *#2*95*10*10*29*000*0## (stopped at 29%); *#2*25*#11#001*50## $\to$ *#2*25*#11#001#1*50## (F461 monitor translation); *#2*31*10*10*40*001*0## (priority="001" preserved); *#2*15*10*11*0*000*0## (opening); *#2*44*10*10*100*000*0##; physical F411/2 05 (*2*1000#0*05##, *2*1000#1*05##, *2*1000#2*05##)
WHO=4 Heating (Dim 0 report_temperature, write_temperature & 3-digit external probes) issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json
issue_466/myhome_trace_MyHomeServer1_all_2026-09-29T17-57-37.json
issue_466/myhome_trace_MyHomeServer1_scenarios_cover_diagnostic_2026-09-27T13-47-33.json
f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json
OWNd/tests/golden/frames/who04_thermo.yaml
@caiosweet (MH200N FW 1.1.8, MyHOME#466 comment 5834435606), @gdluck (MyHomeServer1 FW 3.87.13 & F461 FW 2.0.11, MyHOME#753), @TheDarkWizard (MyHomeServer1 FW 2.38.9) External 3-digit probes *#4*105*0*0296## (29.6 °C on probe 105) and *#4*169*0*...## (probe 169), zone temperatures *#4*2*0*0271## (27.1 °C) and *#4*2*0*0249## (24.9 °C on F461 KW4691), and Dimension #0 write *#4*50*#0*0215## (write_temperature("50", 21.5) ACKed in 88 ms / 155 ms on F461)
WHO=13 Gateway Clock (Dims 0, 1, 22 set_* & report_*) issue_466/config_entry-myhome_F455.json
issue_466/myhome_trace_MH202_all_2026-09-26T16-59-17.json
issue_466/config_entry-myhome_F454_step_commands.json
f461_live_2026-10-15/myhome_trace_F461_live_2026-10-15.json
f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json
issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json
OWNd/tests/golden/frames/who13_gateway.yaml
@lionelser (F455), @anotherjulien (F454 + MH202), @caiosweet (MH200N), @gdluck & @GreenGrassBlueOcean (F461 FW 2.0.11 & MH200 FW 2.1.0, MyHOME#753) *#13**0## $\to$ *#13**0*20*54*00*002##, *#13**#0*11*30*00*002##, *#13**1*04*15*10*2026##, *#13**#1*03*07*10*2026##, *#13**22*16*37*03*001*05*25*09*2026##, *#13**#22*17*33*54*002*05*11*09*2026##
WHO=18 Energy (Dim 1200 stream lifecycle + Dims 113, 51, 53, 54) issue_466/myhome_trace_F454_who18_2026-10-08T19-30-16.json
issue_466/myhome_trace_MyHomeServer1_all_2026-10-01T07-59-11.json
issue_466/myhome_sweep_F454_all_2026-09-26T16-59-13.json
issue_466/myhome_sweep_MH200N_all_2026-09-25T14-40-10.json
f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json
OWNd/tests/golden/frames/who18_energy.yaml
OWNd/tests/test_who18_auto_update_reply.py
@xtimmy86x (MyHOME#661 comment 6067730589, F454 FW 2.0 + F520 51..511), @Interstellar0verdrive (MyHomeServer1 + F520 51, 52), @anotherjulien (F454 + F520 52), @caiosweet (MH200N + F520 51..57), @gdluck & @GreenGrassBlueOcean (F461 FW 2.0.11, MyHOME#753), MyHOME#669 (7X#0) TX *#18*56*#1200#1*2## $\to$ RX *#18*56*1200#1*2## (~235 ms) $\to$ *#18*56*113*9##..*#18*51*113*572## $\to$ expiry RX *#18*56*1200#1*0## (~126.18 s); *#18*51*113*450##, *#18*51*51*12345##, *#18*51*54*320##, *#18*51*#1200#1*1##, *#18*51*#1200#1*0## ACKed on live F461; *#18*51*51*23791364## (Dim 51); *#18*52*53*1## (Dim 53); *#18*51*54*13165## (Dim 54); *#18*77#0*113*161##

2. Emulated Gateway Firmware Oracle Traces (own-firmware-oracle)

tests/golden/firmware_oracle.json pins only these frames related to this PR: *#2*31*10##, *#2*31*#11#001*40##, *#4*1*0##, *#18*51*113##, *#18*51*51##, *#18*51*53##, *#18*51*54##, *#18*51*#1200#1*255##, *#18*51*#1200#1*0##. These are queries and writes, not report-form injection. No published oracle verdict exists for *#4*W*#0*TTTT##, *#4*W*0*TTTT## or any WHO 18 report frame, so no profile flag rests on the oracle. An earlier revision of this PR set F453/MH200N/MH200 overrides from an unpublished local replay; those overrides were removed in e374f3c.


What does the Encyclopedia / Firmware Oracle / Live F461 Plant say

  1. Grammar & Meaning (OpenWebNet-Encyclopedia + Community Hardware Captures):
    • WHO=1 Dimension 1 (*#1*WHERE*1*LEVEL*SPEED## vs *#1*WHERE*#1*LEVEL*SPEED##): LEVEL on the wire is 100..200 corresponding to 0%..100% (100 = OFF, 200 = 100%). On physical F461 (2.0.11), *#1*08*1*150*0## (report form) is NACKed (0.055 s in the 2026-10-18 trace, 6–10 ms in the 2026-10-19 plant check), whereas *#1*08*#1*150*0## (set_brightness #1 write form) is ACKed (0.126 s / 83 ms).
    • WHO=2 Dimension 10 (*#2*WHERE*10*STATE*LEVEL*PRIORITY*INFO##), Dimension 11 (*#2*WHERE*#11#001*LEVEL##) & Command Translation (*2*1000#0/1/2*WHERE##): STATE is 10 (stopped), 11 (opening), 12 (closing), 13 (step-up), 14 (step-down), 15 (PUL); LEVEL is 0..100 (%) or 255 (unknown position); PRIORITY is a 3-digit bitfield (001 / 000) on physical actuators or integer 0..255. On physical F461 (2.0.11), the report form is NACKed: *#2*25*10*10*50*0*0## in 0.055 s (2026-10-18 trace) and *#2*25*10*10*50*001*0## in 10 ms (2026-10-19 plant check). By contrast, *#2*25*#11#001*50## (set_shutter_level #11#001 write form) is ACKed in 83 ms and echoed on evt as *#2*25*#11#001#1*50## (@gdluck, MyHOME#750 issuecomment-6277050571).
    • WHO=4 Dimension 0 (*#4*WHERE*0*TTTT## vs *#4*WHERE*#0*TTTT##): 4-digit signed tenths of a degree Celsius (0TTT for $\ge 0^\circ\text{C}$, 1TTT for $&lt; 0^\circ\text{C}$). External probes use WHERE = 000 (mapped to 4-#0) or 3-digit pZZ (101–899). On physical F461 (2.0.11), bt_termo parses *#4*W*0*TTTT## without #0 as a Dimension 0 read request returning Zone 2's *#4*2*0*0249##, while OWNHeatingCommand.write_temperature("50", 21.5) (*#4*50*#0*0215##) is ACKed (0.155 s / 88 ms).
    • WHO=13 Dimensions 0, 1, 22: Time (H*M*S*TZ), Date (0W*D*M*Y), and Combined Datetime (H*M*S*TZ*0W*D*M*Y), where TZ is 0HH ($\text{UTC}+$) or 1HH ($\text{UTC}-$). What the captures show for that field:
      • Filled with 0HH: F453AV (001), F454 (001, 002), MH200 (000, 001, 002), MH200N (001) and MH202 (000, 002).
      • Empty: MH201 (3.6.27 *#13**0*21*31*40*## and *#13**22*21*31*43**03*14*10*2026##, the mh201_physical_plant fixture *#13**22*00*22*43**04*17*09*2026##, and the 3.3.6 issue_466 trace *#13**0*21*07*30*##), and F455.
      • 999 (unconfigured): F461 in the 2026-10-15 and 2026-10-18 traces, and one F454 (issue_297).
      • Not captured: MyHomeServer1 has no clock frame in the fixtures, and no plant capture shows a 1HH value; the only one is the openwebnet4j fixture *#13**22*09*37*30*102*03*01*05*2019##.
      • Command session: on the F461, *#13**0*22*51*00*999## is answered as a read with the gateway's real time, like WHO 4, while *#13**#0*…## is ACKed and sets the clock.
    • WHO=18 Dimensions 51, 52, 53, 54, 113, 1200: On physical F461, report-form frames (*#18*51*113*450##, *#18*51*51*12345##, *#18*51*54*320##) are ACKed in 1.27–1.40 s (1.2–1.7 s in the plant check) with no event after, and the plain read *#18*51*113## is ACKed in 0.91 s without a reply. So the ACK does not show the value reaching the bus (supports_who18_response_injection = True on F461 means acknowledged).
  2. Firmware Oracle Verdicts (own-firmware-oracle):
    • None of the three profile flags rests on an oracle verdict. Only F461Profile sets them, from the live captures; every other profile is None (unverified) until a capture or a published verdict exists.

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

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

I found one validation gap: finite fractional values are silently truncated by int() instead of being rejected. For example, report_brightness(..., 50.9) builds a frame with level 50; the same issue affects integer-valued parameters in the other builders. Since these fields represent protocol integers, please reject non-integral values and add regression tests before approval.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Thanks for catching that, @fedem95!

Fixed in d478495:

  • All integer-valued builder parameters across OWNLightingCommand.report_brightness (level, transition), OWNAutomationCommand.set_shutter_level (level), OWNAutomationCommand.report_shutter_level (level, state, priority, info), OWNEnergyCommand.report_current_month_energy (year, month), OWNEnergyCommand.report_partial_month_energy (month), OWNEnergyCommand.report_partial_day_energy (day), and OWNEnergyCommand.report_streaming_state (duration, energy_type) now guard against isinstance(x, float) and x % 1 != 0, rejecting both non-finite floats (NaN, ±inf) and finite fractional floats (e.g. 50.9) with ValueError instead of silently truncating via int().
  • Added regression tests covering finite fractional float inputs across all affected builders in tests/test_message_protocol.py::test_expose_builders_infeasible_inputs.

@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Follow-up update (@fedem95):

Also pushed 42280af on this branch to address a few additional review findings:

  • Split out the OWNCommandSession retry-guard fix into standalone PR fix(connection): only retry idempotent queries after mid-send transport loss #131 ( ix/non-query-no-retry) so this PR stays focused on the message builders and profile capability flag.
  • Timezone & naive datetime/ ime handling (OWNd/message/gateway.py): OWNGatewayCommand clock builders (set_datetime,
    eport_datetime, set_date,
    eport_date, set_time,
    eport_time, set_datetime_to_now, set_date_to_today, set_time_to_now) now accept datetime.tzinfo | str | None (avoiding synchronous ZoneInfo(str) disk loads inside the event loop when a zinfo is passed) and reject naive datetime/ ime inputs with ValueError when ime_zone is None instead of silently encoding UTC

@GreenGrassBlueOcean
GreenGrassBlueOcean removed the request for review from fedem95 October 19, 2026 21:25
@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as draft October 19, 2026 21:25
GreenGrassBlueOcean added a commit that referenced this pull request Oct 20, 2026
@GreenGrassBlueOcean
GreenGrassBlueOcean marked this pull request as ready for review October 20, 2026 06:43

@fedem95 fedem95 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 d478495 and the rebase: report_brightness('21', 50.9) and True now raise, 50.0 still builds *#1*21*1*150*0##. Tested 30410c6: 1893 passed, CI green. The only change from c68ffd5 is the gateway.py conflict resolution, which keeps master's _resolve_tzinfo, and the set_*_to_now builders work with both str and tzinfo. The F461 2026-10-18 trace backs the main points: *#1*08*#1*150*0## ACK in 0.126 s, *#4*2*0*0999## answered as a read with *#4*2*0*0249##, *#4*50*#0*0215## ACK in 0.155 s, WHO 18 reports ACKed in 0.14-1.40 s. Before approving:

  1. The F453/MH200N supports_who4_command_injection=False and MH200/MH200N supports_who18_response_injection=False flags are credited to the oracle, but I can't find the evidence. *#4*1*#0*0215## isn't in tests/golden/firmware_oracle.json, and neither is any *#4*W*#0*TTTT##, *#4*W*0*TTTT## or WHO 18 report frame. The 88 F453/MH200/MH200N/F461 TSVs on oracle main have none either (the only 0215 is *#4*#23#1*#14*0215*1##). Where are those verdicts? If they aren't published yet, I'd keep the defaults and leave the overrides out until they are.
  2. Figures that don't match the 2026-10-18 trace: *#1*08*1*150*0## is NACKed in 0.055 s (not 5 ms), and the WHO 2 report there is *#2*25*10*10*50*0*0## (priority 0, NACK in 0.055 s), not …*001*0## in 82 ms. If those come from the 2026-10-20 capture, could it be added to the corpus?
  3. On the F461, WHO 13 report frames are answered as reads too (*#13**0*22*51*00*999## → ACK plus the real time), like WHO 4; the diagram lists report_datetime as a valid injection.
  4. MH201Profile inherits supports_who4_command_injection=True and supports_who18_response_injection=True with no MH201 evidence. Maybe say "unverified" for profiles without data rather than defaulting to True?

Non-blocking: a half-hour offset is still truncated silently (Asia/Kolkata → TZ 005); rejecting it might be safer.

…t sub-hour TZ offsets

Review follow-up (#129, pullrequestreview-5586521653):

- supports_dimension_response_injection, supports_who4_command_injection
  and supports_who18_response_injection are now bool | None, None meaning
  no capture or published oracle verdict exists. Only F461Profile sets
  them, from the live 2026-10-18 capture and the MyHOME#750 plant check:
  reply forms not injected (WHO 1/2 NACK, WHO 4/13 answered as reads),
  *#4*W*#0*TTTT## ACKed, WHO 18 reply forms ACKed.
- Drop the F453/MH200N WHO 4 and MH200/MH200N WHO 18 overrides: no
  published oracle verdict backs them.
- _encode_tz_offset raises ValueError for offsets that are not whole hours
  (Asia/Kolkata, Asia/Kathmandu, America/St_Johns) instead of truncating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

Thanks, all five points checked against the F461 trace and the oracle checkout; fixed in e374f3c and in the description.

  1. You're right: there is no published verdict. The F453/MH200N WHO 4 and MH200/MH200N WHO 18 overrides came from a local replay that never made it into own-firmware-oracle or firmware_oracle.json. Both overrides are gone.
  2. Confirmed. The 2026-10-18 trace has *#1*08*1*150*0## NACK in 0.055 s and *#2*25*10*10*50*0*0## (priority 0) NACK in 0.055 s. The 5 ms / …*001*0## / 82 ms figures were a mix-up with the 2026-10-19 plant-check table in MyHOME#750 (issuecomment-6277050571), which reports 6–10 ms and *001*0## at 10 ms. That table is text only, with no capture file, so there is nothing to add to the corpus. The description now cites each figure with its source, and the "2026-10-20 capture" reference is gone.
  3. Confirmed: *#13**0*22*51*00*999## → ACK plus *#13**0*22*51*02*999##. The diagram now injects WHO 13 through set_* only, and the description says report_* is answered as a read on the F461. I also noted that the WHO 18 report ACKs (1.27–1.40 s) come with no event after, and that the plain *#18*51*113## read is ACKed in 0.91 s too, so supports_who18_response_injection means "acknowledged", not "reaches the bus".
  4. The three flags are now bool | None, defaulting to None (unverified). Only F461Profile sets them (False / True / True), with the trace frames in its docstring. MH201 and every other profile stay None.

Non-blocking: done. _encode_tz_offset now raises ValueError for offsets that are not whole hours (Asia/Kolkata, Asia/Kathmandu, America/St_Johns), including through set_*_to_now.

1894 passed locally; ruff and mypy clean.

@fedem95 fedem95 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, all five points are addressed in e374f3c. Merged with current master (a2ce7fb): no conflicts, 1893 passed, CI green. The flags now default to None, only F461Profile sets them, and its docstring matches the 2026-10-18 trace (*#4*50*#0*0215## ACK in 0.155 s, the WHO 18 reports ACKed in 1.27-1.40 s with no event after). MH201Profile stays None on all three. Non-whole-hour offsets now raise, also through set_time_to_now. Approving.

One last fix to the description, no code change needed: it says the 0HH/1HH TZ field is "confirmed across physical … MH201 … captures", but every MH201 capture we have leaves it empty: my 3.6.27 answers *#13**0*21*31*40*## and *#13**22*21*31*43**03*14*10*2026##, the mh201_physical_plant fixture has *#13**22*00*22*43**04*17*09*2026##, and the 3.3.6 trace in issue_466 has *#13**0*21*07*30*##. The F461 in the same list answers 999 (unconfigured) in the 2026-10-18 trace.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 0a82296 into master Oct 20, 2026
16 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the feat/declarative-expose branch October 20, 2026 16:50
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Oct 20, 2026
…penWebNet-HA#132, OpenWebNet-HA#133 to the changelog

Brings in OpenWebNet-HA#132 (typed exceptions, fast send reconnect), OpenWebNet-HA#133 (discovery
shim removal, MH201 retry notes) and OpenWebNet-HA#129 (expose builders, query helpers,
tri-state profile flags). OWNd/__init__.py keeps master's exception
re-exports with __version__ = "2.0.0b11". Changelog dated 2026-10-20.

Co-Authored-By: Claude Opus 5.5 <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