Repository navigation
fix(message): parse '#'-prefixed dimension 14 setpoint values - #138
GreenGrassBlueOcean merged 2 commits into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
78948c9 to
c09238e
Compare
…; 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>
… (actuator status, fan speed and humidity per zone) Two physical captures behind #792 (actuator status at startup, hvac_action from dimension 20) and #793 (humidity gate), asked for in review 5642758778 of #792. f461_actuators_live_2026-10-25: a local Home Assistant at #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 *#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 (*#4*2#1*#20*1##, then *#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): *#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 *#4*Z*19## / *#4*Z*20## ACK on 34 zones and NACK on 54 / 56, *#4*Z*11## NACK on all 36 (the fan-speed dimension is refused on a valve zone), *#4*Z*60## ACK on the 17 zones with a probe and NACK on 19, *#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 *#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 '#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>
c09238e to
9991dd5
Compare
|
Sequencing note (no change to this PR): #138 and #139 both add |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Narrow allowlisting in OWNMessage instead of loosening _DIMENSION_REQUEST_REPLY globally is the right approach here — OpenWebNet-HA/MyHOME#139 shows a real plant emitting *#4*#1*14*#0230*1## / *#4*#5*14*#0230*1## every few seconds, and MyHOME (climate.py) consumes MESSAGE_TYPE_TARGET_TEMPERATURE / set_temperature directly.
Before approving:
-
Tighten the
_HASH_VALUE_REPLIESpattern to the actual Dimension 14 shape and ASCII digits (OWNd/message/base.py):re.compile( r"^\*#(?P<who>4)\*(?P<where>#?[0-9]+)(?P<where_param>(?:#[0-9]+)*)\*(?P<dimension>14)(?P<dimension_param>)(?P<dimension_value>\*#[01][0-9]{3}(?:\*[0-9]+)?)\#\#$", re.ASCII, )
Currently
r"^\*#(?P<who>4)\*(?P<where>#?\d+)(?P<where_param>(?:#\d+)*)\*(?P<dimension>14)(?P<dimension_param>)(?P<dimension_value>\*#\d+(?:\*\d*)*)##$"has two mismatches with the PR description ("limited here to WHO 4 Dimension 14 (*#4*WHERE*14*#TTTT*M##, one#, digits after it)"):(?:\*\d*)*accepts empty trailing fields (*#4*#1*14*#0230*##) and arbitrary value counts (*#4*#1*14*#0230*1*2*3##). Dimension 14 replies carry 1 or 2 values (*#TTTTor*#TTTT*M).- Without
re.ASCII/[0-9],\d+matches non-ASCII Unicode digits (*#4*#1*14*#٠٢٣٠*1##), producing anOWNHeatingEventwithset_temperature=None(since_WHO4_TEMP_REGEXis^[01][0-9]{3}$) instead of rejecting non-ASCII wire bytes asNone. Bounding the first value to[01][0-9]{3}also ensuresset_temperatureis neverNoneon matched frames (unlike*#4*#1*14*#99999*1##).
-
Pin
OWNEvent.parse(...)symmetry andhuman_readable_logintests/test_who4_hash_setpoint.py:- On
master,OWNEvent.parse("*#4*#1*14*#0230*1##")returned an invalidOWNHeatingEvent(is_valid=False,zone=0,message_type=None). SinceOWNMessage.__init__now populates the match fields, please pinOWNEvent.parse(...)alongsideOWNMessage.parse(...)(as in#62), and assertmessage.human_readable_log == "Zone 1's target temperature is set to 23.0°C."so we verify the#prefix never leaks into the log line.
- On
-
Rebase & sequencing:
- Please rebase onto current
master(12fef0c8, resolving theCHANGELOG.mdconflict) and drop the#144companion reference if#144is closed. Note that#139introduces the same_HASH_VALUE_REPLIESplumbing inOWNd/message/base.py, so let's land#138first and rebase#139onto it afterward.
- Please rebase onto current
…hape (review follow-up) Review 5684986239 on OpenWebNet-HA#138: the allowlisted pattern accepted empty trailing fields, any number of values and non-ASCII digits. It now matches exactly `*michnovka#4*WHERE*14*#TTTT*M##`: one '#', a four-digit temperature starting with 0 or 1 (as `_WHO4_TEMP_REGEX`), at most one mode value, ASCII digits only (`re.ASCII`), so a non-ASCII frame returns None instead of an event with `set_temperature None`. Tests pin `OWNEvent.parse` symmetry with `OWNMessage.parse`, the log line "Zone 1's target temperature is set to 23.0°C." (the '#' never leaks), and the refused shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Points 1 and 2 are in 6e0d2f2: the pattern is now exactly Point 3: the clean rebase onto |
…hape (review follow-up) Review 5684986239 on OpenWebNet-HA#138: the allowlisted pattern accepted empty trailing fields, any number of values and non-ASCII digits. It now matches exactly `*michnovka#4*WHERE*14*#TTTT*M##`: one '#', a four-digit temperature starting with 0 or 1 (as `_WHO4_TEMP_REGEX`), at most one mode value, ASCII digits only (`re.ASCII`), so a non-ASCII frame returns None instead of an event with `set_temperature None`. Tests pin `OWNEvent.parse` symmetry with `OWNMessage.parse`, the log line "Zone 1's target temperature is set to 23.0°C." (the '#' never leaks), and the refused shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6e0d2f2 to
55b5549
Compare
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Thanks @gdluck — all points from review #5684986239 are cleanly addressed in 55b5549:
- Exact Dimension 14 allowlist (
OWNd/message/base.py):_HASH_VALUE_REPLIES(^\*#(?P<who>4)\*(?P<where>#?[0-9]+)(?P<where_param>(?:#[0-9]+)*)\*(?P<dimension>14)(?P<dimension_param>)(?P<dimension_value>\*#[01][0-9]{3}(?:\*[0-9]+)?)##$,re.ASCII) matches only*#4*WHERE*14*#TTTT*M##with ASCII digits and a valid 4-digit WHO 4 temperature ([01][0-9]{3}), rejecting empty trailing fields, extra values, out-of-range codes, and non-ASCII bytes asNoneand ensuringset_temperatureis neverNoneon matched frames. - Parser symmetry & log line (
tests/test_who4_hash_setpoint.py): 17 cases now pinOWNEvent.parse==OWNMessage.parse,human_readable_log == "Zone 1's target temperature is set to 23.0°C."(no#leak), zone forms (#1,1,#0#2), plain Dimension 14 replies, and 11 rejected#shapes. MyHOMEcompatibility & CI: Verified downstream inMyHOME(climate.pyreadsmessage.set_temperatureand only checksdimension_value[1] == "3"in_dimension_3_is_protection, so keepingdimension_value == ["#0230", "1"]is completely safe). Authorship and closed#144companion notes are updated, and all 8 CI gates (Python 3.11–3.14, static analysis, build, golden corpus, andMyHOME test suite) are green.
LGTM — approving now. Only a 1-line CHANGELOG.md conflict under ### Fixed from #137 (4e1ddc2) needs to be rebased/resolved and the PR marked Ready for review to merge, after which #139 can rebase onto _HASH_VALUE_REPLIES.
`*michnovka#4*michnovka#1*14*#0230*1##` (MyHOME#139, "Data received is not a message") is a WHO 4 Dimension 14 setpoint reply whose value carries a '#' prefix. The general dimension-reply regex accepts digits only, so OWNMessage.parse returned None. A narrow allowlist (_HASH_VALUE_REPLIES, WHO 4 dimension 14 only) routes that shape to OWNHeatingEvent, which strips the '#' before decoding the temperature; dimension_value keeps the raw value and str(message) is the frame as received. A '#' in any other value still does not parse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hape (review follow-up) Review 5684986239 on OpenWebNet-HA#138: the allowlisted pattern accepted empty trailing fields, any number of values and non-ASCII digits. It now matches exactly `*michnovka#4*WHERE*14*#TTTT*M##`: one '#', a four-digit temperature starting with 0 or 1 (as `_WHO4_TEMP_REGEX`), at most one mode value, ASCII digits only (`re.ASCII`), so a non-ASCII frame returns None instead of an event with `set_temperature None`. Tests pin `OWNEvent.parse` symmetry with `OWNMessage.parse`, the log line "Zone 1's target temperature is set to 23.0°C." (the '#' never leaks), and the refused shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
55b5549 to
5ae277b
Compare
|
Rebased onto master |
GreenGrassBlueOcean
left a comment
There was a problem hiding this comment.
Rebase onto master (4e1ddc2) verified (55b5549 → 5ae277b):
CHANGELOG.md: Clean 1-line conflict resolution under[Unreleased]/### Fixed, retaining both the#137central-unit builders entry and this WHO 4 Dimension 14#-prefixed setpoint entry.- Code & tests (
OWNd/message/base.py,OWNd/message/heating.py,tests/test_who4_hash_setpoint.py): Identical to previously reviewed and approved55b5549— zero code or test diff across the rebase. - CI: All 8 gates (Python 3.11–3.14 tests & coverage, static analysis, package build, golden corpus & smoke gates, and downstream
MyHOME test suite) are green on5ae277b.
Re-approving for merge; #139 can rebase onto _HASH_VALUE_REPLIES once this lands.
Requested by A
Summary
*#4*#1*14*#0230*1##(OpenWebNet-HA/MyHOME#139, "Data received is not a message") is a WHO 4 Dimension 14 setpoint reply whose value carries a#prefix, sent by a user's central unit every few seconds. The dimension-reply regex accepts only digits in values, soOWNMessage.parsereturnedNoneand MyHOME logged every occurrence as an unparsable event instead of using the setpoint.Fixes
OpenWebNet-HA/MyHOME#139on the OWNd side (the MyHOME log rate limit,OpenWebNet-HA/MyHOME#802, was closed at the maintainer's request: this parser change is the fix, and the warning disappears once the OWNd pin includes it). The WHO 13 clock-echo timezone fix that this PR carried at first is now its own PR (see Companion); the two have different evidence and different reviewers' checks.Changes
OWNd/message/base.py:_HASH_VALUE_REPLIES, a narrow allowlist of reply shapes that carry a#inside a value, limited here to WHO 4 Dimension 14 (*#4*WHERE*14*#TTTT*M##, one#, digits after it)._match_hash_value_replyis tried after the general reply regex, which is unchanged, andparse()routes those shapes to events. The new branch fills the same fields as the general one (_who,_where,_where_param,_dimension,_dimension_param,_dimension_value). A#value on dimension 12, on WHO 1, on the mode field,##0230,#02#30or a bare#still returnsNone.OWNd/message/heating.py: the Dimension 14 decoder strips the#beforewho4_temperature, givingset_temperature == 23.0;dimension_valuekeeps["#0230", "1"]andstr(message)is the frame as received.CHANGELOG.md: entry under[Unreleased]/Fixed.State Diagram
stateDiagram-v2 direction TB state "OWNMessage.parse(frame) for a dimension reply" as Parse { [*] --> General General --> Event : *#4*#1*14*0230*1## , digits only\nOWNHeatingEvent, set_temperature 23.0 , unchanged General --> HashAllowlist : value with a # - general regex does not match HashAllowlist --> Event : *#4*WHERE*14*#TTTT*M## only , new\nOWNHeatingEvent, set_temperature 23.0, dimension_value [#0230, 1]\n, before - None, MyHOME Data received is not a message HashAllowlist --> NoneResult : any other # value\n*#4*#1*12*#0230*1##, *#1*11*1*#5##, *#4*#1*14*0230*#1##\nNone , unchanged }Where are the traces
No physical trace of ours applies: the
#-prefixed setpoint is in no oracle suite and in neither of our captures. The live F461 2.0.11 and MyHomeServer1 3.87.13 (trace PROpenWebNet-HA/MyHOME#799, merged) answer dimension 14 with plain values (*#4*2*14*T*3##,*#4*35*14*...##). The evidence for the shape is the user log inOpenWebNet-HA/MyHOME#139(frames*#4*#1*14*#0230*1##,*#4*#1*14*#0231*1##,*#4*#2*14*#0205*1##, replayed inOpenWebNet-HA/MyHOME#802's test).OpenWebNet-HA/MyHOME#799run 6 holds a#-prefixed dimension echo (*#4*2#1*#20*0##), a different shape that this PR does not touch.What does the Oracle say
No oracle stimulus exists for this frame: no suite sends or emits a
#-prefixed dimension 14 value.Revision 6e0d2f2 (review 5684986239).
_HASH_VALUE_REPLIESis bound to the dimension 14 shape:\*#[01][0-9]{3}(?:\*[0-9]+)?withre.ASCII(one#, a four-digit temperature as_WHO4_TEMP_REGEX, at most one mode value, ASCII digits), so*#4*#1*14*#0230*##,*#4*#1*14*#0230*1*2*3##,*#4*#1*14*#99999*1##and a non-ASCII-digit frame returnNoneinstead of an event. Tests pinOWNEvent.parsesymmetry withOWNMessage.parse(on master the event session got an invalid event withzone 0) and the log line "Zone 1's target temperature is set to 23.0°C." with no leaked#. The rebase onto master (CHANGELOG.md) follows once the force push is authorised; #139 waits for this PR to land, as agreed.Testing
tests/test_who4_hash_setpoint.py(new, 11 cases): theOpenWebNet-HA/MyHOME#139frame parses asOWNHeatingEvent/MESSAGE_TYPE_TARGET_TEMPERATURE/ 23.0 °C with the raw value kept anddimension_param == []; three zone forms (#1,1,#0#2); the plain setpoint is unchanged; six#shapes elsewhere still giveNone.master4 fail, 7 pass (OWNMessage.parse('*#4*#1*14*#0230*1##')isNone); on this branch all 11 pass.1918 passed, 1 skipped(master:1907 passed, 1 skipped); 100 % statement coverage,mypyclean,ruffclean,scripts/sync_documentation.py --checkandscripts/verify_library_standards.pypass.Live retest (2026-10-24)
Not exercised live: parser-only, and the F461 sends no
#-prefixed setpoint (no*#4*...*14*#TTTT##on this plant).Companion
MyHOME: none (
OpenWebNet-HA/MyHOME#802closed; the warning stops once this parser change is pinned). The companion OWNd PR #144 (clock echoes with timezone 0530 / 124, split from this one) was closed on 2026-10-28: those values are oracle stimuli no real gateway emits.🤖 Generated with Claude Code