Skip to content

fix(services): reject half-hour time zones in sync_time with a translated error - #764

Merged
GreenGrassBlueOcean merged 2 commits into
v2-phase1-architecturefrom
fix/sync-time-half-hour-offset
Oct 21, 2026
Merged

GreenGrassBlueOcean merged 2 commits into
v2-phase1-architecturefrom
fix/sync-time-half-hour-offset

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to OWNd 2.0.0b11 (OpenWebNet-HA/OWNd#129), which manifest.json on v2-phase1-architecture already pins.


1. What is the problem?

  1. myhome.sync_time raises an uncaught ValueError in half-hour time zones (custom_components/myhome/services.py):
    • The WHO 13 time-zone field T is three digits: a sign digit plus a two-digit hour offset (001 = GMT+1, 102 = GMT-2). It cannot carry +05:30, +10:30 or -02:30.

    • Up to OWNd 2.0.0b10, OWNGatewayCommand.set_datetime_to_now() / set_time_to_now() truncated such offsets, so the gateway clock was set with the wrong offset and nothing was reported. Since 2.0.0b11 (feat(message,profiles): add declarative expose frame builders, query helpers, and profile flags OWNd#129) the builders raise ValueError: UTC offset +05:30 is not a whole number of hours instead.

    • handle_sync_time() called the builders without handling that error. A Home Assistant instance configured for Asia/Kolkata, Australia/Adelaide, America/St_Johns, and so on got a raw traceback from the service call instead of a readable error.

    • Reproduced against the PyPI wheel:

      Time zone OWNGatewayCommand.set_datetime_to_now(tz) on 2.0.0b11
      Europe/Brussels *#13**#22*09*57*37*002*03*21*10*2026##
      Asia/Kolkata ValueError: UTC offset +05:30 is not a whole number of hours
      Australia/Adelaide ValueError: UTC offset +10:30 is not a whole number of hours
      America/St_Johns ValueError: UTC offset -02:30 is not a whole number of hours

2. How is it solved?

  1. Build both frames up front in _build_time_sync_commands(timezone) (custom_components/myhome/services.py):
    • Builds the DIMENSION 22 datetime frame and the DIMENSION 0 time frame together, before anything is sent, so a failure can never leave the gateway with only one of the two frames.
    • Converts OWNd's ValueError into a ServiceValidationError with translation_key="sync_time_unsupported_offset" and the timezone and error placeholders.
    • Both service paths use it: the per-gateway call (gateway: <mac>) and the all-gateways call (one sync per bus, skipping followers).
  2. Add the translated message (strings.json, translations/en.json):
    • sync_time_unsupported_offset: Cannot synchronise the gateway clock for time zone "{timezone}": {error}. OpenWebNet only carries whole-hour UTC offsets.
  3. Test the half-hour case (tests/test_init.py::test_services):
    • Switches hass.config to Asia/Kolkata and checks that both the per-gateway and all-gateways calls raise ServiceValidationError with the right translation key and timezone placeholder, and that gateway.send is never called.
    • Switches back to Europe/Brussels and checks that the call still sends exactly two frames, each with a sign + whole-hour T field (001/002).

The expose engine in #750 calls the same WHO 13 builders and already catches the ValueError there; this PR does not touch that branch.


3. State Diagram

flowchart TD
    Call["myhome.sync_time\n(gateway: mac | all)"] --> TZ["timezone = hass.config time_zone"]
    TZ --> Build["_build_time_sync_commands(timezone)\nset_datetime_to_now + set_time_to_now"]
    Build -->|whole-hour offset| Send["send *#13**#22*H*M*S*T*W*D*M*Y##\nthen *#13**#0*H*M*S*T##"]
    Build -->|"ValueError (+05:30, +10:30, -02:30 …)"| Err["ServiceValidationError\nsync_time_unsupported_offset\n(nothing sent)"]
Loading

4. Where are the traces (real ones)?

Hardware Gateway Real Trace File Observed WHO 13 T field
BTicino MH200N tests/fixtures/traces/issue_624/myhome_trace_MH200N_all_2026-10-05T20-50-47.json *#13**22*22*50*32*001*01*05*10*2026##: T = 001, GMT+1 as a sign plus whole hours.
BTicino F461 (2.0.11) tests/fixtures/traces/f461_live_2026-10-18/myhome_trace_F461_live_2026-10-18.json Replies *#13**0*22*51*00*999## and *#13**22*22*51*02*999*00*18*10*2026##: T = 999 (offset not set). There are no minute digits in any captured T field.

No capture from a gateway configured for a half-hour zone exists. The failure is on the HA side, before any frame is built.


5. What does the Firmware Oracle / Encyclopedia say?

  1. OpenWebNet Encyclopedia (functional/who-13-integration-gateway/dimensions.md, capabilities.md):
    • DIMENSION 0 payload H*M*S*T: "T | Time zone | three digits, sign + hour offset". The first digit is 0 for positive and 1 for negative, and the other two are the hour offset (001 = GMT+1, 102 = GMT-2).
    • DIMENSION 22 payload H*M*S*T*W*D*M*Y carries the same T field.
    • capabilities.md describes the time-zone encoding as an hour-offset representation, not a named time-zone identifier.
  2. Empirical Firmware Oracle (openwebnet-mcp — parse_and_validate_frame, lookup_firmware_verdict):
    • *#13**#22*09*57*37*002*03*21*10*2026## is grammar-valid (DIMENSION_WRITING, WHO 13, DIMENSION 22). Neither it nor *#13**0## has been replayed against the 12 emulated gateways yet, so there are no firmware verdicts for WHO 13 writes either way.

Verification

  • Full suite against the released OWNd==2.0.0b11 wheel (PYTHONPATH = scratch install, not the editable ../OWNd) with --cov=custom_components/myhome: 4358 passed, 5 snapshots passed, TOTAL 14468 statements, 0 missed, 100.0%

  • tests/test_init.py::test_services covers both directions: Asia/Kolkata raises sync_time_unsupported_offset and sends nothing, and Europe/Brussels still sends both frames with T = 001|002.

  • scripts/typing_ratchet.py: 0 error(s); baseline 0 as CI runs it (without OWNd installed). With the b11 wheel visible to mypy it reports 14 errors in 7 modules. None are in lines this PR changes; they are the MyHOME/OWNd boundary errors that fix(config_flow): inject the HA websession into OWNd discovery and correct the OWNd licence #757 resolves.

  • ruff check custom_components tests scripts (All checks passed!)

  • scripts/verify_ha_standards.py & scripts/sync_documentation.py --check (both pass)

  • V2 Architecture: New entities implement handle_event() and do not manually subscribe via async_dispatcher_connect. (No entities are added or changed.)

🤖 Generated with Claude Code

…ated error

OWNd 2.0.0b11 raises ValueError from set_datetime_to_now() and
set_time_to_now() when the UTC offset is not a whole number of hours,
because the WHO 13 time-zone field only carries a sign and an hour
offset. myhome.sync_time called the builders unguarded, so Home
Assistant instances in Asia/Kolkata, Australia/Adelaide or
America/St_Johns got a raw traceback.

Build both frames up front in _build_time_sync_commands() so a failure
never sends only half of the datetime/time pair, and raise a
ServiceValidationError with the sync_time_unsupported_offset
translation key instead.

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

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…est the whole-hour path

The ServiceValidationError fallback message now equals the
sync_time_unsupported_offset translation, and test_services checks
that Europe/Brussels still sends both WHO 13 frames with a sign +
whole-hour T field after the half-hour rejection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit 7f6ea17 into v2-phase1-architecture Oct 21, 2026
17 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the fix/sync-time-half-hour-offset branch October 21, 2026 08:55
GreenGrassBlueOcean added a commit that referenced this pull request Oct 21, 2026
- Count and summarize every pull request merged since 2.0.0b15, grouped by
  subsystem, with the expose engine (#750), slat tilt (#508) and impulse
  covers (#667) as the headline features.
- Covers: the start-up floor is the per-cover min_motor_pulse (#708), timed
  runs end at the stop status (#707) and full-travel keypad runs report the
  end (#760); drop the "lift only" and open follow-up lines, both shipped.
- New behaviour-change section: sync_time refuses half-hour time zones
  (#764) and advanced: true on an F411 now warns (#762).

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.

2 participants