Skip to content

Pass the configured timezone to zigpy for Time cluster DST support - #974

Merged
puddly merged 3 commits into
zigpy:devfrom
lowside:pr/pass-timezone-to-controller
Oct 27, 2026
Merged

puddly merged 3 commits into
zigpy:devfrom
lowside:pr/pass-timezone-to-controller

Conversation

@lowside

@lowside lowside commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Closes #967

Passes the gateway's configured timezone to ControllerApplication.new() so zigpy's ZCL Time cluster server can report the correct UTC offset and DST attributes (dst_start, dst_end, dst_shift) to Zigbee devices. Without this, zigpy reported a zero UTC offset and no DST, causing devices to display UTC rather than local time.


Changes

zha/application/helpers.py

  • ZHAData.local_timezone default changed from datetime.UTC to None. None means "no timezone configured" and is passed through to zigpy as-is, correctly suppressing Master_for_Zone_and_DST claims for a timezone the user never set. The previous datetime.UTC default was only ever used by the Sinopé climate entity — it was never forwarded to zigpy — so there is no change in existing behavior.

zha/application/gateway.py

  • local_timezone=self.config.local_timezone added to the ControllerApplication.new() call in _async_initialize, so zigpy's Time cluster receives the correct timezone at startup.
  • Gateway.update_local_timezone(tz) added as the canonical way to update the timezone at runtime. Direct writes to gateway.config.local_timezone only update the ZHAData copy; this method keeps ControllerApplication.local_timezone in sync as well. If called before initialization or after shutdown, ZHAData is still updated and the controller will receive the correct value on the next async_initialize.

zha/application/platforms/climate/__init__.py

  • SinopeTechnologiesThermostat._async_update_time now falls back to datetime.UTC when local_timezone is None, preserving existing behavior for unconfigured setups. datetime.now(None) returns a naive system-local datetime and would silently produce wrong secs_since_2k values on non-UTC hosts.

tests/test_gateway.py

  • test_configured_timezone_passed_to_controller — parametrized over None and a real ZoneInfo; verifies both that the kwarg is forwarded to ControllerApplication.new() and that it lands on the controller instance.
  • test_update_local_timezone_syncs_controller — verifies both ZHAData and application_controller are updated together, including clearing back to None.
  • test_update_local_timezone_before_init — verifies no crash when called before async_initialize or after shutdown, and that ZHAData is still updated.

tests/test_climate.py

  • Explicit assertion that local_timezone is None before the UTC fallback case, making the precondition clear.
  • Updated to use gateway.update_local_timezone() for the runtime timezone change, consistent with the documented API.

Notes

A companion change is recommended in homeassistant-core: the EVENT_CORE_CONFIG_UPDATE handler currently writes zha_gateway.config.local_timezone = ZoneInfo(hass.config.time_zone) directly, which bypasses ControllerApplication.local_timezone. Swapping that to zha_gateway.update_local_timezone(ZoneInfo(hass.config.time_zone)) will keep both copies in sync on runtime timezone changes without requiring a ZHA restart.

Comment thread zha/application/gateway.py Outdated
@lowside
lowside force-pushed the pr/pass-timezone-to-controller branch from 28919dc to 3115e51 Compare October 25, 2026 03:49
@lowside
lowside requested a review from puddly October 25, 2026 03:52
@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.23%. Comparing base (6decce1) to head (dad4e57).
⚠️ Report is 4 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #974   +/-   ##
=======================================
  Coverage   97.23%   97.23%           
=======================================
  Files          61       61           
  Lines       10704    10710    +6     
=======================================
+ Hits        10408    10414    +6     
  Misses        296      296           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good. The change is small and does what the description says: local_timezone now reaches ControllerApplication.new(), which zigpy 2.4.0 (the pinned version) accepts as a keyword-only argument and stores on the controller, where the Time cluster reads it.

Verified (6 checks)
  • The local_timezone kwarg on ControllerApplication.new exists in the pinned zigpy==2.4.0. The Time cluster reads application.local_timezone in _handle_read_attributes, and None means no zone authority (Master_for_Zone_and_DST is not set).
  • The only runtime consumer of ZHAData.local_timezone in zha is the Sinopé _async_update_time. The None to UTC fallback keeps its behaviour identical to the old dt.UTC default, and datetime.now(None) would indeed have returned a naive local time.
  • Home Assistant core already passes a ZoneInfo when it builds ZHAData (homeassistant/components/zha/helpers.py), so HA setups get the zone at startup with this change. The runtime EVENT_CORE_CONFIG_UPDATE path in __init__.py still writes config.local_timezone directly, which is the companion core change the description already calls out.
  • update_local_timezone is safe before init and after shutdown, since application_controller is None in both cases.
  • tests/test_gateway.py and tests/test_climate.py pass (118 passed). ruff check, ruff format --check and mypy zha/ are clean in the worktree venv, and CI is green on all three Python versions.
  • The earlier docstring thread from puddly is resolved and the docstring is now a single line.

One optional nit is inline.

Comment thread tests/test_gateway.py Outdated

Copilot AI 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.

🟡 Changes recommended

A timezone update during asynchronous controller creation can leave the controller reporting the previous timezone.

1 open issue
What changed in this PR

This PR forwards ZHA’s configured timezone to zigpy so its Time cluster can report local offsets and daylight-saving attributes, addressing #967.

Changes:

  • Pass the configured timezone at controller creation and add a runtime update method.
  • Represent an unconfigured timezone as None, preserving the Sinopé thermostat’s UTC fallback.
  • Add timezone forwarding and lifecycle tests, and update climate tests.
File Description
zha/​application/​platforms/​climate/​__init__.py Preserve UTC fallback when no timezone is configured.
zha/​application/​helpers.py Default the optional timezone to None.
zha/​application/​gateway.py Forward timezone configuration and support runtime updates.
tests/​test_gateway.py Test timezone forwarding, synchronization, and lifecycle handling.
tests/​test_climate.py Verify the fallback and use the runtime update method.

Review effort: Balanced

Comment thread zha/application/gateway.py
@puddly
puddly merged commit b7c5424 into zigpy:dev Oct 27, 2026
10 checks passed
@lowside
lowside deleted the pr/pass-timezone-to-controller branch October 27, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature Adds new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pass the configured timezone to zigpy for Time cluster DST support

5 participants