From 1526827e120575b85315baedf56c08115032faa9 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:42:44 +0100 Subject: [PATCH 1/7] Stabilise on_since value for smart devices --- kasa/smart/smartdevice.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 04a9608a6..f3bbdb765 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -66,6 +66,7 @@ def __init__( self._children: Mapping[str, SmartDevice] = {} self._last_update = {} self._last_update_time: float | None = None + self._on_since: datetime | None = None async def _initialize_children(self): """Initialize children for power strips.""" @@ -502,7 +503,14 @@ def on_since(self) -> datetime | None: return None on_time = cast(float, on_time) - return self.time - timedelta(seconds=on_time) + on_since = self.time - timedelta(seconds=on_time) + # Ensure slight variations between time module and on_time do not cause the + # on_since time to change. More likely to happen with child devices. + if not self._on_since or timedelta( + seconds=0 + ) < on_since - self._on_since > timedelta(seconds=2): + self._on_since = on_since + return self._on_since @property def timezone(self) -> dict: From 5b95e3eda723134a37546c8df036be9b91861b2e Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:12:14 +0100 Subject: [PATCH 2/7] Update threshold to 5s --- kasa/smart/smartdevice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index f3bbdb765..4b0865f7a 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -504,11 +504,12 @@ def on_since(self) -> datetime | None: on_time = cast(float, on_time) on_since = self.time - timedelta(seconds=on_time) - # Ensure slight variations between time module and on_time do not cause the - # on_since time to change. More likely to happen with child devices. + # Ensure slight variations between time module and on_time do not cause + # the on_since time to change. More likely to happen with child devices + # or P100s that do not do multi-queries. if not self._on_since or timedelta( seconds=0 - ) < on_since - self._on_since > timedelta(seconds=2): + ) < on_since - self._on_since > timedelta(seconds=5): self._on_since = on_since return self._on_since From de9c84b93dae5a3ae23a17ef728309799ed15158 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:58:49 +0100 Subject: [PATCH 3/7] Add same fix to iot devices and update docstring --- kasa/device.py | 6 +++++- kasa/iot/iotdevice.py | 22 ++++++++++++++-------- kasa/smart/smartdevice.py | 1 + 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index 05a4f7675..46f859f78 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -435,7 +435,11 @@ def has_emeter(self) -> bool: @property @abstractmethod def on_since(self) -> datetime | None: - """Return the time that the device was turned on or None if turned off.""" + """Return the time that the device was turned on or None if turned off. + + Could be inprecise by up to 5 seconds due to device jitter between + the device reporting time and on_time. + """ @abstractmethod async def wifi_scan(self) -> list[WifiNetwork]: diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 3986c001d..928faa566 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -18,7 +18,7 @@ import inspect import logging from collections.abc import Mapping, Sequence -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any, cast from ..device import Device, WifiNetwork @@ -181,6 +181,7 @@ def __init__( self._legacy_features: set[str] = set() self._children: Mapping[str, IotDevice] = {} self._modules: dict[str | ModuleName[Module], IotModule] = {} + self._on_since: datetime | None = None @property def children(self) -> Sequence[IotDevice]: @@ -592,18 +593,23 @@ async def set_state(self, on: bool): @property # type: ignore @requires_update def on_since(self) -> datetime | None: - """Return pretty-printed on-time, or None if not available.""" - if "on_time" not in self._sys_info: - return None + """Return the time that the device was turned on or None if turned off. - if self.is_off: + Could be inprecise by up to 5 seconds due to device jitter between + the device reporting time and on_time. + """ + if self.is_off or "on_time" not in self._sys_info: + self._on_since = None return None on_time = self._sys_info["on_time"] - return datetime.now(timezone.utc).astimezone().replace( - microsecond=0 - ) - timedelta(seconds=on_time) + on_since = self.time - timedelta(seconds=on_time) + if not self._on_since or timedelta( + seconds=0 + ) < on_since - self._on_since > timedelta(seconds=5): + self._on_since = on_since + return self._on_since @property # type: ignore @requires_update diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 4b0865f7a..4dfce0aa1 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -500,6 +500,7 @@ def on_since(self) -> datetime | None: not self._info.get("device_on") or (on_time := self._info.get("on_time")) is None ): + self._on_since = None return None on_time = cast(float, on_time) From 4ee53a594fd984f0aa4d669fb4b3a47926965123 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:22:55 +0100 Subject: [PATCH 4/7] Revert use of time property --- kasa/iot/iotdevice.py | 6 ++++-- kasa/smart/smartdevice.py | 9 +++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 928faa566..32263446e 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -18,7 +18,7 @@ import inspect import logging from collections.abc import Mapping, Sequence -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, cast from ..device import Device, WifiNetwork @@ -604,7 +604,9 @@ def on_since(self) -> datetime | None: on_time = self._sys_info["on_time"] - on_since = self.time - timedelta(seconds=on_time) + time = datetime.now(timezone.utc).astimezone().replace(microsecond=0) + + on_since = time - timedelta(seconds=on_time) if not self._on_since or timedelta( seconds=0 ) < on_since - self._on_since > timedelta(seconds=5): diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 4dfce0aa1..f033d229f 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -495,7 +495,11 @@ def time(self) -> datetime: @property def on_since(self) -> datetime | None: - """Return the time that the device was turned on or None if turned off.""" + """Return the time that the device was turned on or None if turned off. + + Could be inprecise by up to 5 seconds due to device jitter between + the device reporting time and on_time. + """ if ( not self._info.get("device_on") or (on_time := self._info.get("on_time")) is None @@ -505,9 +509,6 @@ def on_since(self) -> datetime | None: on_time = cast(float, on_time) on_since = self.time - timedelta(seconds=on_time) - # Ensure slight variations between time module and on_time do not cause - # the on_since time to change. More likely to happen with child devices - # or P100s that do not do multi-queries. if not self._on_since or timedelta( seconds=0 ) < on_since - self._on_since > timedelta(seconds=5): From a88219d0d8db7fd1ea59cfcf4150bffa5f65d058 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:43:57 +0100 Subject: [PATCH 5/7] Add fix to iot strip plug --- kasa/iot/iotstrip.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index 61017228d..732962532 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -318,6 +318,7 @@ def __init__(self, host: str, parent: IotStrip, child_id: str) -> None: self._set_sys_info(parent.sys_info) self._device_type = DeviceType.StripSocket self.protocol = parent.protocol # Must use the same connection as the parent + self._on_since: datetime | None = None async def _initialize_modules(self): """Initialize modules not added in init.""" @@ -443,9 +444,14 @@ def on_since(self) -> datetime | None: info = self._get_child_info() on_time = info["on_time"] - return datetime.now(timezone.utc).astimezone().replace( - microsecond=0 - ) - timedelta(seconds=on_time) + time = datetime.now(timezone.utc).astimezone().replace(microsecond=0) + + on_since = time - timedelta(seconds=on_time) + if not self._on_since or timedelta( + seconds=0 + ) < on_since - self._on_since > timedelta(seconds=5): + self._on_since = on_since + return self._on_since @property # type: ignore @requires_update From b47b981ea8d61f4e3851c4eb4f09f09ad5f46bce Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:50:53 +0100 Subject: [PATCH 6/7] Clear cache on strip plug --- kasa/iot/iotstrip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index 732962532..0bdfc1cb6 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -439,6 +439,7 @@ def next_action(self) -> dict: def on_since(self) -> datetime | None: """Return on-time, if available.""" if self.is_off: + self._on_since = None return None info = self._get_child_info() From 993fc510c065c82c7db8de49e9b57f21b143b432 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:58:30 +0100 Subject: [PATCH 7/7] Tweak docstring --- kasa/device.py | 4 ++-- kasa/iot/iotdevice.py | 4 ++-- kasa/smart/smartdevice.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kasa/device.py b/kasa/device.py index 46f859f78..4397e2ffd 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -437,8 +437,8 @@ def has_emeter(self) -> bool: def on_since(self) -> datetime | None: """Return the time that the device was turned on or None if turned off. - Could be inprecise by up to 5 seconds due to device jitter between - the device reporting time and on_time. + This returns a cached value if the device reported value difference is under + five seconds to avoid device-caused jitter. """ @abstractmethod diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 003c30b69..f0d14e10b 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -597,8 +597,8 @@ async def set_state(self, on: bool): def on_since(self) -> datetime | None: """Return the time that the device was turned on or None if turned off. - Could be inprecise by up to 5 seconds due to device jitter between - the device reporting time and on_time. + This returns a cached value if the device reported value difference is under + five seconds to avoid device-caused jitter. """ if self.is_off or "on_time" not in self._sys_info: self._on_since = None diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index f033d229f..8d373f580 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -497,8 +497,8 @@ def time(self) -> datetime: def on_since(self) -> datetime | None: """Return the time that the device was turned on or None if turned off. - Could be inprecise by up to 5 seconds due to device jitter between - the device reporting time and on_time. + This returns a cached value if the device reported value difference is under + five seconds to avoid device-caused jitter. """ if ( not self._info.get("device_on")