Skip to content

Stabilise on_since value for smart devices #1144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion kasa/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

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]:
Expand Down
22 changes: 15 additions & 7 deletions kasa/iot/iotdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -594,18 +595,25 @@ 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:
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
Expand Down
13 changes: 10 additions & 3 deletions kasa/iot/iotstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -438,14 +439,20 @@ 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()
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
Expand Down
15 changes: 13 additions & 2 deletions kasa/smart/smartdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -494,15 +495,25 @@ 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.

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:
Expand Down
Loading