Skip to content

fix(message): parse '#'-prefixed dimension 14 setpoint values - #138

Merged
GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/parse-hash-setpoint-and-clock-echoes
Oct 29, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
OpenWebNet-HA:masterfrom
gdluck:fix/parse-hash-setpoint-and-clock-echoes

Conversation

@gdluck

@gdluck gdluck commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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, so OWNMessage.parse returned None and MyHOME logged every occurrence as an unparsable event instead of using the setpoint.

Fixes OpenWebNet-HA/MyHOME#139 on 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_reply is tried after the general reply regex, which is unchanged, and parse() 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#30 or a bare # still returns None.
  • OWNd/message/heating.py: the Dimension 14 decoder strips the # before who4_temperature, giving set_temperature == 23.0; dimension_value keeps ["#0230", "1"] and str(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
    }
Loading

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 PR OpenWebNet-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 in OpenWebNet-HA/MyHOME#139 (frames *#4*#1*14*#0230*1##, *#4*#1*14*#0231*1##, *#4*#2*14*#0205*1##, replayed in OpenWebNet-HA/MyHOME#802's test). OpenWebNet-HA/MyHOME#799 run 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_REPLIES is bound to the dimension 14 shape: \*#[01][0-9]{3}(?:\*[0-9]+)? with re.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 return None instead of an event. Tests pin OWNEvent.parse symmetry with OWNMessage.parse (on master the event session got an invalid event with zone 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): the OpenWebNet-HA/MyHOME#139 frame parses as OWNHeatingEvent / MESSAGE_TYPE_TARGET_TEMPERATURE / 23.0 °C with the raw value kept and dimension_param == []; three zone forms (#1, 1, #0#2); the plain setpoint is unchanged; six # shapes elsewhere still give None.
  • Fail before / pass after: on master 4 fail, 7 pass (OWNMessage.parse('*#4*#1*14*#0230*1##') is None); on this branch all 11 pass.
  • Full suite: 1918 passed, 1 skipped (master: 1907 passed, 1 skipped); 100 % statement coverage, mypy clean, ruff clean, scripts/sync_documentation.py --check and scripts/verify_library_standards.py pass.

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#802 closed; 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

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

@gdluck
gdluck marked this pull request as draft October 24, 2026 13:39
@gdluck
gdluck force-pushed the fix/parse-hash-setpoint-and-clock-echoes branch from 78948c9 to c09238e Compare October 24, 2026 15:44
@gdluck gdluck changed the title fix(message): parse '#'-prefixed dimension 14 setpoints and WHO 13 clock echoes with 0530/124 timezones fix(message): parse '#'-prefixed dimension 14 setpoint values Oct 24, 2026
gdluck pushed a commit to gdluck/HomeAssistant-MyHOME that referenced this pull request Oct 25, 2026
…; 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>
GreenGrassBlueOcean pushed a commit to OpenWebNet-HA/MyHOME that referenced this pull request Oct 25, 2026
… (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>
@gdluck
gdluck force-pushed the fix/parse-hash-setpoint-and-clock-echoes branch from c09238e to 9991dd5 Compare October 26, 2026 08:56
@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on master 87cafc3 (after #136): the CHANGELOG [Unreleased] entry and the heating.py regex block now sit beside the merged dimension-22 guard; content unchanged, suite and 100 % coverage green.

@gdluck

gdluck commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Sequencing note (no change to this PR): #138 and #139 both add _HASH_VALUE_REPLIES and _match_hash_value_reply at the same lines of OWNd/message/base.py (the model-name pattern there, the #-prefixed dimension 14 pattern here). Each merges clean on master alone; whichever lands second I will rebase at once into one tuple with both patterns, so a conflict resolution does not silently drop the other PR's pattern. @GreenGrassBlueOcean merge order is yours; just say which goes first.

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

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:

  1. Tighten the _HASH_VALUE_REPLIES pattern 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 (*#TTTT or *#TTTT*M).
    • Without re.ASCII / [0-9], \d+ matches non-ASCII Unicode digits (*#4*#1*14*#٠٢٣٠*1##), producing an OWNHeatingEvent with set_temperature=None (since _WHO4_TEMP_REGEX is ^[01][0-9]{3}$) instead of rejecting non-ASCII wire bytes as None. Bounding the first value to [01][0-9]{3} also ensures set_temperature is never None on matched frames (unlike *#4*#1*14*#99999*1##).
  2. Pin OWNEvent.parse(...) symmetry and human_readable_log in tests/test_who4_hash_setpoint.py:

    • On master, OWNEvent.parse("*#4*#1*14*#0230*1##") returned an invalid OWNHeatingEvent (is_valid=False, zone=0, message_type=None). Since OWNMessage.__init__ now populates the match fields, please pin OWNEvent.parse(...) alongside OWNMessage.parse(...) (as in #62), and assert message.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.
  3. Rebase & sequencing:

    • Please rebase onto current master (12fef0c8, resolving the CHANGELOG.md conflict) and drop the #144 companion reference if #144 is closed. Note that #139 introduces the same _HASH_VALUE_REPLIES plumbing in OWNd/message/base.py, so let's land #138 first and rebase #139 onto it afterward.

gdluck pushed a commit to gdluck/OWNd that referenced this pull request Oct 28, 2026
…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>
@gdluck

gdluck commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Points 1 and 2 are in 6e0d2f2: the pattern is now exactly *#4*WHERE*14*#TTTT*M## (\*#[01][0-9]{3}(?:\*[0-9]+)?, re.ASCII), so empty trailing fields, extra values, a non-temperature and non-ASCII digits all return None; tests/test_who4_hash_setpoint.py pins OWNEvent.parse symmetry with OWNMessage.parse and the log line "Zone 1's target temperature is set to 23.0°C.". Suite 2058 passed, ruff / mypy / docs check clean. The description drops the #144 companion (closed today, as you suggested).

Point 3: the clean rebase onto master (CHANGELOG) and the authorship reset need a force push, which I will do as soon as the branch owner authorises it; the content is final. #139 holds until this lands, then gets rebased onto it with the + quantifier and re.ASCII pattern you wrote.

gdluck pushed a commit to gdluck/OWNd that referenced this pull request Oct 28, 2026
…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>
@gdluck
gdluck force-pushed the fix/parse-hash-setpoint-and-clock-echoes branch from 6e0d2f2 to 55b5549 Compare October 28, 2026 16:00
@gdluck

gdluck commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto master 4213cab (two commits af28ed5 + 55b5549, authorship reset, no merge commit); 2143 passed, ruff / mypy / docs check clean. With #150 in, CI should run through; #139 follows once this lands.

@GreenGrassBlueOcean GreenGrassBlueOcean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gdluck — all points from review #5684986239 are cleanly addressed in 55b5549:

  1. 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 as None and ensuring set_temperature is never None on matched frames.
  2. Parser symmetry & log line (tests/test_who4_hash_setpoint.py): 17 cases now pin OWNEvent.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.
  3. MyHOME compatibility & CI: Verified downstream in MyHOME (climate.py reads message.set_temperature and only checks dimension_value[1] == "3" in _dimension_3_is_protection, so keeping dimension_value == ["#0230", "1"] is completely safe). Authorship and closed #144 companion notes are updated, and all 8 CI gates (Python 3.11–3.14, static analysis, build, golden corpus, and MyHOME 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.

gdluck and others added 2 commits October 28, 2026 21:23
`*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>
@gdluck
gdluck marked this pull request as ready for review October 28, 2026 19:27
@gdluck
gdluck force-pushed the fix/parse-hash-setpoint-and-clock-echoes branch from 55b5549 to 5ae277b Compare October 28, 2026 19:27
@gdluck

gdluck commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto master 4e1ddc2 (after #137): the one-line CHANGELOG conflict is resolved by keeping both ### Fixed bullets, no other change (55b5549 -> 5ae277b, both commits authored by gdluck). Full suite 2340 passed locally; marked ready for review.

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

Rebase onto master (4e1ddc2) verified (55b5549 → 5ae277b):

  • CHANGELOG.md: Clean 1-line conflict resolution under [Unreleased] / ### Fixed, retaining both the #137 central-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 approved 55b5549 — 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 on 5ae277b.

Re-approving for merge; #139 can rebase onto _HASH_VALUE_REPLIES once this lands.

@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit f3efc1d into OpenWebNet-HA:master Oct 29, 2026
8 checks passed
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