Skip to content

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

Open
GreenGrassBlueOcean wants to merge 1 commit into
masterfrom
feat/declarative-expose
Open

GreenGrassBlueOcean wants to merge 1 commit 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 (bt_luci & bt_termo): On physical F461 (2.0.11), incoming un-hashed response frames (*#1*08*1*150*0##, *#2*25*10*10*50*001*0##, *#4*50*0*0215##) on the TCP command session are either rejected with NACK (*#*0##) or parsed as dimension reads (*#4*2*0*0999## returns Zone 2's measured *#4*2*0*0249##), whereas #DIM write frames (*#1*08*#1*150*0##, *#2*25*#11#001*50##, *#4*50*#0*0215##) are ACKed in ~83–126 ms and forwarded to the SCS bus. Furthermore, F453 and MH200N reject WHO=4 Dimension 0 / #0 temperature injection entirely (nack + silent).
      • WHO=18 (bt_energy / bt_supervisione): Conversely, WHO=18 does accept un-hashed dimension response frames (*#18*51*113*450##, 51..54, 1200#1), which are ACKed in 140 ms – 1.7 s on physical F461 (and supported on all gateways with WHO=18 except MH200 / MH200N).
  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 and rejecting naive datetime/time inputs when time_zone is None.
    • 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):
    • Added supports_dimension_response_injection: bool = False on GatewayProfile (since real BTicino gateways reject un-hashed *#1*W*1*...##, *#2*W*10*...##, and *#4*W*0*...## frames on the TCP command session or parse them as dimension reads, requiring #DIM write frames set_brightness, set_shutter_level, and write_temperature by default).
    • Added supports_who4_command_injection: bool = True on GatewayProfile, overridden to False on F453Profile and MH200NProfile (the two gateway families whose bt_dev firmware rejects *#4*WHERE*0*TTTT## and *#4*WHERE*#0*TTTT## with nack + silent).
    • Added supports_who18_response_injection: bool = True on GatewayProfile, overridden to False on MH200Profile and MH200NProfile (since bt_energy / bt_supervisione accepts un-hashed *#18*WHERE*DIM*VAL## response frames).
  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 == False\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 == True\n-> report_active_power / report_total_energy / report_streaming_state (*#18*W*DIM*VAL##)
        ProfileGate --> InjectClockReply: WHO == 13\n-> set_datetime (*#13**#22*...##) or report_datetime (*#13**22*...##)
        ProfileGate --> SkipUnsupported: WHO=4 on F453 / MH200N (supports_who4_command_injection == False)\nor WHO=18 on MH200 / MH200N (supports_who18_response_injection == False)
        InjectDimWrite --> [*]
        InjectWho18Reply --> [*]
        InjectClockReply --> [*]
        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, and empty/malformed WHERE addresses.
  • Profile Capability Tests (tests/test_profiles.py):
    • test_profile_injection_capability_flags: verifies supports_dimension_response_injection (False across all profiles), supports_who4_command_injection (False on F453 and MH200N; True on all others), and supports_who18_response_injection (False on MH200 and MH200N; True on all others).
  • 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 and 2026-10-20 live physical BTicino F461 captures in MyHOME#753 and 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,828 passed across Python 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 & 2026-10-20) f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json (OpenWebNet-HA/MyHOME#753 & MyHOME#750 issuecomment-6277050571)
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 (['*2*1000#0*25##', '*2*1000#1/2*25##'] before NACK on cmd), set_shutter_level("25", 50) (*#2*25*#11#001*50## ACKed in 83 ms + echoed on evt as *#2*25*#11#001#1*50##) vs report_shutter_level (*#2*25*10*10*50*001*0## NACKed in 82 ms); WHO=4 write_temperature("50", 21.5) (*#4*50*#0*0215## ACKed in 84–155 ms) vs report_temperature (*#4*2*0*0999## parsed by F461 bt_termo as read returning *#4*2*0*0249##); WHO=1 virtual 08 (*#1*08*#1*150*0## ACKed in 88–126 ms vs *#1*08*1*150*0## NACKed in 5 ms, *1*1/0/5*08## NACK after 2.06 s) and Group #1 Dim 15 (*#1*#1*15*1/0##); WHO=13 (*#13**#0*...## ACKed); WHO=18 (*#18*51*113*450##, 51..54, #1200#1*1/0 ACKed)
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 84–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)

In addition to the community plant captures above, command-session injection behavior across all 12 emulated gateway firmwares is pinned in tests/golden/firmware_oracle.json (sourced from own-firmware-oracle/results/<PRODUCT>/<VERSION>/oracle/full/*.tsv):

  • WHO=1 Lighting (lighting-ts10.tsv): *#1*21*1##, *#1*21*4##, *#1*21*#1*150*3## across all 12 gateways.
  • WHO=2 Automation (automation-ts10.tsv, trace-corpus.tsv): *#2*31*10##, *#2*31*#11#001*40## $\to$ *#2*31*#11#001#1*40##, *#2*31*10*10*50*0*0##, *#2*31*10*11*100*0*0##, *#2*31*10*12*0*0*0## across all 12 gateways.
  • WHO=4 Thermoregulation (thermo-ts10.tsv, thermo-probes.tsv): *#4*1*0##, *#4*1*0*0215##, *#4*1*#0*0215##, *#4*000*0##, *#4*101*0## across all 12 gateways.
  • WHO=13 Gateway Clock (gateway-ts10.tsv): *#13**0##, *#13**1##, *#13**22##, *#13**#0*...##, *#13**#1*...##, *#13**#22*...## across all 12 gateways.
  • WHO=18 Energy Management (energy-ts10.tsv): *#18*51*113##, *#18*51*51##, *#18*51*52#24#9##, *#18*51*53##, *#18*51*54##, *#18*51*#1200#1*1##, *#18*51*#1200#1*255##, *#18*51*#1200#1*0## across all 12 gateways.

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 rejected with NACK in 5 ms, whereas *#1*08*#1*150*0## (set_brightness #1 write form) is ACKed in 88–126 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), *#2*25*10*10*50*001*0## (report form) is rejected with NACK in 82 ms, whereas *#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 in 84–155 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}-$), confirmed across physical F454, F455, F461 (MyHOME#753), MH200, MH200N, MH201, MH202, and MyHomeServer1 captures.
    • WHO=18 Dimensions 51, 52, 53, 54, 113, 1200: Unlike bt_luci and bt_termo, bt_energy / bt_supervisione does accept un-hashed dimension response frames (*#18*51*113*450##, 51..54, 1200#1), which are ACKed in 140 ms – 1.7 s on physical F461 (supports_who18_response_injection = True).
  2. Firmware Oracle Verdicts (own-firmware-oracle):
    • Profile Capability Flags (supports_dimension_response_injection, supports_who4_command_injection, supports_who18_response_injection):
      • supports_dimension_response_injection defaults to False across all profiles so WHO=1, WHO=2, and WHO=4 use #DIM write frames (set_brightness, set_shutter_level, write_temperature) by default.
      • Sending *#4*1*#0*0215## on a command session produces verdict: out (bus frame $120001600000D7\r) on F453AV, F454, F459, F460, F461, H4684, MH200, MH202, and MyHomeServer1, but produces reply: nack, verdict: silent (0 bus frames) on F453 (010100) and MH200N (010108). Hence F453Profile and MH200NProfile set supports_who4_command_injection = False.
      • supports_who18_response_injection defaults to True and is set to False on MH200Profile and MH200NProfile.

@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

This branch has not been deployed

No deployments
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