diff --git a/kasa/device.py b/kasa/device.py index 05a4f767..4397e2ff 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -435,7 +435,11 @@ class Device(ABC): @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. + + This returns a cached value if the device reported value difference is under + five seconds to avoid device-caused jitter. + """ @abstractmethod async def wifi_scan(self) -> list[WifiNetwork]: diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 2959612f..f0d14e10 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -181,6 +181,7 @@ class IotDevice(Device): 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]: @@ -594,18 +595,25 @@ class IotDevice(Device): @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: + 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 return None on_time = self._sys_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 diff --git a/kasa/iot/iotstrip.py b/kasa/iot/iotstrip.py index 61017228..0bdfc1cb 100755 --- a/kasa/iot/iotstrip.py +++ b/kasa/iot/iotstrip.py @@ -318,6 +318,7 @@ class IotStripPlug(IotPlug): 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.""" @@ -438,14 +439,20 @@ class IotStripPlug(IotPlug): 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() 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 diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 04a9608a..8d373f58 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -66,6 +66,7 @@ class SmartDevice(Device): 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.""" @@ -494,15 +495,25 @@ class SmartDevice(Device): @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. + + 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") or (on_time := self._info.get("on_time")) is None ): + self._on_since = None return None on_time = cast(float, on_time) - return self.time - 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 def timezone(self) -> dict: