Skip to content

Add TimeService and DST attribute handlers to Time cluster - #1916

Open
ICEMANno1 wants to merge 1 commit into
zigpy:devfrom
ICEMANno1:add-time-cluster-dst
Open

ICEMANno1 wants to merge 1 commit into
zigpy:devfrom
ICEMANno1:add-time-cluster-dst

Conversation

@ICEMANno1

Copy link
Copy Markdown

Based on #1806 I created this PR.

Implement dst_start, dst_end, and dst_shift which reads with accurate timezone rules, including cross-year seasons and signed or non-hour shifts.

Changes

  • TimeService in zigpy/time.py): Add shared, cached DST calculations. Offload uncached discovery to avoid blocking the event loop.
  • zigpy/application.py: Add optional local_timezone configuration and invalidate cached rules when it changes.
  • zigpy/zcl/clusters/general.py: Add DST handlers and consistent per-request snapshots for specification-compliant attribute responses.
  • tests/test_application.py: Verify timezone initialization and updates.
  • tests/test_zcl_clusters.py: Cover DST boundaries, caching, concurrency, cancellation, and response handling.

Integration Note
ZHA would require it's own PR to provide the HA configured timezone instead of depending on OS.

@ICEMANno1
ICEMANno1 force-pushed the add-time-cluster-dst branch from 805c62c to 19bbca1 Compare October 15, 2026 18:30
@ICEMANno1

Copy link
Copy Markdown
Author

@TheJulianJES would be great if you could have a look at it.

@codecov

codecov Bot commented Oct 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.47644% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.51%. Comparing base (2fdc4ab) to head (19bbca1).

Files with missing lines Patch % Lines
zigpy/zcl/clusters/general.py 97.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #1916      +/-   ##
==========================================
- Coverage   99.51%   99.51%   -0.01%     
==========================================
  Files          61       62       +1     
  Lines       12777    12950     +173     
==========================================
+ Hits        12715    12887     +172     
- Misses         62       63       +1     

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

The DST computation is correct on every zone I tried, the tests are thorough, and zha and zha-quirks both pass against this branch. Most of what follows is about the ZHA wiring, which is smaller than the PR description suggests but has a trap in it. There is also one default-path behavior change to document, and a suggestion to simplify the async discovery path.

The ZHA follow-up

  • zha already has the value. ZHAConfiguration.local_timezone (zha/application/helpers.py:431) is set by HA from hass.config.time_zone and updated on core-config changes (the climate platform already uses it). Wiring it into ControllerApplication.new(local_timezone=...) plus the local_timezone setter on updates is close to a one-liner, and it does not depend on the OS timezone.
  • The trap: that field defaults to datetime.UTC. Passed through unchanged, a timezone instance takes the fixed-offset branch here (authoritative=True), so a non-HA zha user would advertise UTC with Master_for_Zone_and_DST set. That is worse than today's system-local values. The follow-up should pass None when nothing is configured.
  • Once wired, every HA install gets a configured timezone, so TimeZone and StandardTime change from the current offset (DST included, as on dev) to the standard offset. That is spec-correct (ZCL R8 §3.12.2.2.3) and matches zigbee-herdsman. But a device that reads only TimeZone/StandardTime and ignores the DST attributes will be an hour off while DST is active. This is the most visible change the series makes, so please call it out in this PR's description (and the ZHA one).
  • Europe/Dublin from the system tzdata comes out as TimeZone=+3600, DstShift=-3600, with the DST window over the winter (2026-10-25 → 2027-03-28). The spec allows a signed DstShift (§3.12.2.2.6) and the local-time arithmetic works out, but negative shifts are unusual for device firmware.

Default path (no timezone configured)

With local_timezone=None, TimeService._snapshot returns authoritative=False, so time_status drops Master_for_Zone_and_DST. On dev, handle_read_attribute_time_status always sets it. The other values are unchanged from dev. That is defensible by §3.12.2.2.2: in this path TimeZone is not the standard offset and the DST attributes are invalid. Still, it is a change from dev and from zigbee-herdsman, which always reports timeStatus: 0b1101. Please mention it in the PR description. Maintainers can decide whether to keep dev's bit for unconfigured setups.

Consider (maintainer call)

The async path looks heavier than the problem. For ZoneInfo zones, _discover_periods takes about 8–17 ms here, and the result stays cached until the UTC year rolls over. Only a synthetic permanent-DST tzinfo (as in test_time_service_unbounded_dst) widens the scan to 2000–2136, which takes seconds. Avoiding that cost costs a fair amount of machinery: the executor offload, the _pending futures under asyncio.shield, the generation counter, and moving every Time Read Attributes onto a task. A synchronous handle_cluster_general_request override could install the application's TimeService.snapshot(now) as _read_snapshot (the cached path the direct handlers already use), call super(), and restore. That keeps the one-instant-per-response property and drops async_snapshot, _pending and _generation, along with their concurrency tests. If the service stays, a public accessor would be cleaner than the cluster reaching into application._time_service.

Other

  • This overlaps #1806 (same feature, also open). Maintainers should pick one and close the other.
  • Nit: ZIGBEE_EPOCH in zigpy/zcl/clusters/general.py is no longer used there and is now defined again in zigpy/time.py. Import one from the other.
  • codecov/patch flags one uncovered line: the direct-call fallback in Time._get_time_snapshot (general.py:1143).
Verified (8 checks)
  • zigpy suite at 19bbca1: 1394 passed. zigpy/time.py has 100% coverage; general.py misses only line 1143.
  • Cross-repo: installed this branch editable into fresh zha and zha-device-handlers worktrees (current dev). zha: 1458 passed; quirks: 4843 passed, 2 xfailed.
  • Downstream subclasses: the only quirk subclassing Time is DanfossTimeCluster, which writes time to the device and overrides no read handler. No radio lib overrides ControllerApplication.new, so the new keyword-only local_timezone is additive.
  • zha's local_timezone field (default datetime.UTC) is set in HA Core's zha/helpers.py from hass.config.time_zone and refreshed in zha/__init__.py on config updates.
  • Probed TimeService.snapshot across Europe/Berlin, Australia/Sydney, America/Santiago (cross-year southern-hemisphere seasons), Europe/Dublin (negative DST), Antarctica/Troll (2 h shift), Asia/Tokyo, Europe/Moscow, America/Sao_Paulo and Pacific/Apia (no DST), at four instants in 2026. Start/end/shift were correct each time; DstEnd is the last DST second, as the inclusive §3.12.2.2.4 bound requires.
  • async_snapshot: the retry loop runs at most twice, since a second discovery uses the reader's own now. remove_pending runs before the shielded waiter resumes, and a timezone change during discovery doesn't publish to the cache. No issues found.
  • Within one scan window, zones whose next DST season is more than a year away (e.g. Africa/Cairo before its 2023 reinstatement) report "no DST". Local time stays correct, so this is not raised.
  • On dev, TimeZone for the system-local path already includes DST: datetime.now().astimezone() gives a fixed-offset tz whose dst() is None. The PR's zone is None path keeps that and clears the bit, confirmed by probe.

Comment thread zigpy/time.py
standard_time=zigbee_time(now + timedelta(seconds=standard_offset)),
local_time=zigbee_time(now + timedelta(seconds=offset)),
)
if zone is None or values.time == INVALID_TIME:

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.

With no timezone configured, this returns authoritative=False, so time_status drops Master_for_Zone_and_DST (set unconditionally on dev). §3.12.2.2.2 supports clearing it, but it's a change from dev and from herdsman's 0b1101. Please note it in the PR description. Also, the ZHA follow-up should pass None here rather than zha's datetime.UTC default, or unconfigured setups would advertise UTC as authoritative. See the review body.

Comment thread zigpy/time.py
self._cache = (zone, now.year, periods)
return self._snapshot(now, zone, periods)

async def async_snapshot(self, now: datetime) -> TimeSnapshot:

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.

For ZoneInfo zones, discovery took 8–17 ms per zone per year in my probes. This executor, _pending and generation machinery, plus putting every Time read on a task in general.py, seems a lot to avoid that. A synchronous cached snapshot installed in the handle_cluster_general_request override would keep the consistent-snapshot property with much less state. Maintainer call.

@ICEMANno1

Copy link
Copy Markdown
Author

@TheJulianJES thanks for the review. Would you mind looking at it and telling me if I shall resolve all or only parts of it as well as decide with regards to Consider (maintainer call)?

P.S. No pressure from my side! Whenever you have time it would be appreciated.

@zigpy-review-bot zigpy-review-bot added the new feature Adds new feature label Oct 17, 2026
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.

2 participants