From 9030179aad86effaf63f8543fa506f2c70396e63 Mon Sep 17 00:00:00 2001 From: Ahmed Hassan Date: Sun, 4 Oct 2026 18:52:54 +0500 Subject: [PATCH] Fall back to UTC for iot time when the device clock is not set (#1768) A new or factory-reset iot device can report a clock far in the past, e.g. year 2000, which causes a failure in timezone autodetection logic as the difference can be in years. This causes `update()` to fail, so every cli command fails, including `wifi scan` and `wifi join`, which are the commands needed to provision the device. To fix this, this changes the logic fall back to UTC if the device clock is not set, detected by checking if the timedelta is too large. Co-authored-by: Claude Opus 5.5 (1M context) --- kasa/iot/modules/time.py | 7 ++++++- tests/test_common_modules.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/kasa/iot/modules/time.py b/kasa/iot/modules/time.py index a9f3704b..f1e55661 100644 --- a/kasa/iot/modules/time.py +++ b/kasa/iot/modules/time.py @@ -35,7 +35,7 @@ class Time(IotModule, TimeInterface): If the configured zone is not available on this host, compute the device's current UTC offset and choose a best-match available zone, preferring DST- observing candidates when the original index implies DST. As a last resort, - use a fixed-offset timezone. + use a fixed-offset timezone. If the device clock is not set, use UTC. """ if res := self.data.get("get_timezone"): idx = res.get("index") @@ -58,6 +58,11 @@ class Time(IotModule, TimeInterface): now_utc = datetime.now(UTC) delta = device_local - now_utc.replace(tzinfo=None) rounded = timedelta(seconds=60 * round(delta.total_seconds() / 60)) + # UTC offsets range from -12h to +14h, anything beyond that means the + # device clock is not set (e.g. before provisioning). + if abs(rounded) > timedelta(hours=14): + self._timezone = UTC + return dst_expected = None if res := self.data.get("get_timezone"): diff --git a/tests/test_common_modules.py b/tests/test_common_modules.py index 4eb63ac9..917a70c7 100644 --- a/tests/test_common_modules.py +++ b/tests/test_common_modules.py @@ -513,6 +513,40 @@ async def test_time_post_update_uses_offset_when_index_missing_unit( assert inst.timezone.utcoffset(now) == timedelta(0) +@device_iot +async def test_time_post_update_unsynced_clock_uses_utc( + dev: Device, mocker: MockerFixture +): + """Fall back to UTC when the device clock is not set. + + An unprovisioned device can report e.g. year 2000, which is no valid UTC offset + away from the host time, so the offset-based guess must not be attempted. + """ + from zoneinfo import ZoneInfoNotFoundError + + proto = dev.protocol._transport.proto # type: ignore[attr-defined] + for target in ("time", "smartlife.iot.common.timesetting"): + if target in proto: + proto[target]["get_time"] = { + "year": 2000, + "month": 1, + "mday": 1, + "hour": 2, + "min": 45, + "sec": 0, + } + # Force the offset-based path, as when the zone is not available on the host + mocker.patch( + "kasa.iot.modules.time.get_timezone", + new=AsyncMock(side_effect=ZoneInfoNotFoundError("missing on host")), + ) + + await dev.update() + time_mod = dev.modules[Module.Time] + assert time_mod.timezone is UTC + assert time_mod.time.year == 2000 + + async def test_time_get_time_exception_returns_none_unit(mocker: MockerFixture): """Cover Time.get_time exception path (unit test of iot Time).""" from kasa.iot.modules.time import Time as TimeModule