10000 Add Time module to SmartCamera devices by sdb9696 · Pull Request #1182 · python-kasa/python-kasa · GitHub
[go: up one dir, main page]

Skip to content

Add Time module to SmartCamera devices #1182

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 41 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
0df0f4a
Add initial test framework changes for SmartCamera
sdb9696 Oct 18, 2024
2fab6dc
Add basic child initialization
sdb9696 Oct 19, 2024
2b80075
Provide https to get_device_family
sdb9696 Oct 19, 2024
1c684d1
Add call to initialise children
sdb9696 Oct 19, 2024
71d7537
Log stack trace on child init
sdb9696 Oct 20, 2024
5f62f83
Fix device_type error
sdb9696 Oct 20, 2024
5ec48de
Do not double wrap child device protocol
sdb9696 Oct 20, 2024
d6740fe
Enable sending empty params
sdb9696 Oct 21, 2024
2eebb53
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 21, 2024
fb174cf
Add extra params to control child request
sdb9696 Oct 21, 2024
039ccea
Revert "Add extra params to control child request"
sdb9696 Oct 21, 2024
9591521
Add time and camera modules
sdb9696 Oct 21, 2024
52a8219
Improve REGISTERED_MODULES restriction
sdb9696 Oct 21, 2024
7f958ff
Handle child devices not supporting time
sdb9696 Oct 21, 2024
56a5e33
Fix tests
sdb9696 Oct 21, 2024
7a4a4c6
Add modules to init
sdb9696 Oct 21, 2024
e66c13d
Fix clock status
sdb9696 Oct 22, 2024
8dbe65c
Fix camera disabled
sdb9696 Oct 22, 2024
84fc93c
Fix cloud_connect missing error
sdb9696 Oct 22, 2024
72deb4c
Fix intentionally broken camera getter name
sdb9696 Oct 22, 2024
972e0d2
Enable experimental as default
sdb9696 Oct 22, 2024
8115554
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 22, 2024
58bfa57
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 22, 2024
99a4315
Update post review
sdb9696 Oct 22, 2024
67db948
Simplify module auto registration
sdb9696 Oct 23, 2024
4fd38bb
Apply suggestions from code review
sdb9696 Oct 23, 2024
d3dd5cb
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 23, 2024
4e7ef06
Update docstring for SmartChildDevice.create
sdb9696 Oct 23, 2024
2410c43
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 23, 2024
a19784b
Revert fixture change
sdb9696 Oct 23, 2024
e3513f0
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 23, 2024
777fe74
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 24, 2024
45b59b0
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 24, 2024
779e468
Remove seperate SmartErrorCode handling
sdb9696 Oct 24, 2024
dd5dc95
Raise errors on missing module data
sdb9696 Oct 24, 2024
1cbbab6
Merge remote-tracking branch 'upstream/master' into feat/camera_hub
sdb9696 Oct 24, 2024
ee32c40
Add test for set_time
sdb9696 Oct 24, 2024
edf9a96
Fix whitespace and disable experimental
sdb9696 Oct 24, 2024
16cdd8c
Include local time in time set
sdb9696 Oct 24, 2024
2445eb7
Remove unnecessary NAME
sdb9696 Oct 24, 2024
205e890
Fix tests
sdb9696 Oct 24, 2024
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
2 changes: 2 additions & 0 deletions kasa/experimental/modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
from .camera import Camera
from .childdevice import ChildDevice
from .device import DeviceModule
from .time import Time

__all__ = [
"Camera",
"ChildDevice",
"DeviceModule",
"Time",
]
91 changes: 91 additions & 0 deletions kasa/experimental/modules/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Implementation of time module."""

from __future__ import annotations

from datetime import datetime, timezone, tzinfo
from typing import cast

from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from ...cachedzoneinfo import CachedZoneInfo
from ...feature import Feature
from ...interfaces import Time as TimeInterface
from ..smartcameramodule import SmartCameraModule


class Time(SmartCameraModule, TimeInterface):
"""Implementation of device_local_time."""

QUERY_GETTER_NAME = "getTimezone"
QUERY_MODULE_NAME = "system"
QUERY_SECTION_NAMES = "basic"

_timezone: tzinfo = timezone.utc
_time: datetime

def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
self._add_feature(
Feature(
device=self._device,
id="device_time",
name="Device time",
attribute_getter="time",
container=self,
category=Feature.Category.Debug,
type=Feature.Type.Sensor,
)
)

def query(self) -> dict:
"""Query to execute during the update cycle."""
q = super().query()
q["getClockStatus"] = {self.QUERY_MODULE_NAME: {"name": "clock_status"}}

return q

async def _post_update_hook(self) -> None:
"""Perform actions after a device update."""
time_data = self.data["getClockStatus"]["system"]["clock_status"]
timezone_data = self.data["getTimezone"]["system"]["basic"]
zone_id = timezone_data["zone_id"]
timestamp = time_data["seconds_from_1970"]
try:
# Zoneinfo will return a DST aware object
tz: tzinfo = await CachedZoneInfo.get_cached_zone_info(zone_id)
except ZoneInfoNotFoundError:
# timezone string like: UTC+10:00
timezone_str = timezone_data["timezone"]
tz = cast(tzinfo, datetime.strptime(timezone_str[-6:], "%z").tzinfo)

self._timezone = tz
self._time = datetime.fromtimestamp(
cast(float, timestamp),
tz=tz,
)

@property
def timezone(self) -> tzinfo:
"""Return current timezone."""
return self._timezone

@property
def time(self) -> datetime:
"""Return device's current datetime."""
return self._time

async def set_time(self, dt: datetime) -> dict:
"""Set device time."""
if not dt.tzinfo:
timestamp = dt.replace(tzinfo=self.timezone).timestamp()
else:
timestamp = dt.timestamp()

lt = datetime.fromtimestamp(timestamp).isoformat().replace("T", " ")
params = {"seconds_from_1970": int(timestamp), "local_time": lt}
# Doesn't seem to update the time, perhaps because timing_mode is ntp
res = await self.call("setTimezone", {"system": {"clock_status": params}})
if (zinfo := dt.tzinfo) and isinstance(zinfo, ZoneInfo):
tz_params = {"zone_id": zinfo.key}
res = await self.call("setTimezone", {"system": {"basic": tz_params}})
return res
8 changes: 6 additions & 2 deletions kasa/experimental/smartcameramodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, cast

from ..exceptions import DeviceError, KasaException, SmartErrorCode
from ..smart.smartmodule import SmartModule
Expand Down Expand Up @@ -54,7 +54,11 @@ async def call(self, method: str, params: dict | None = None) -> dict:
if method[:3] == "get":
return await self._device._query_getter_helper(method, module, section)

return await self._device._query_setter_helper(method, module, section, params)
if TYPE_CHECKING:
params = cast(dict[str, dict[str, Any]], params)
return await self._device._query_setter_helper(
method, module, section, params[module][section]
)

@property
def data(self) -> dict:
Expand Down
30 changes: 25 additions & 5 deletions kasa/tests/fakeprotocol_smartcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ def _get_param_set_value(info: dict, set_keys: list[str], value):
"lens_mask_info",
"enabled",
],
("system", "clock_status", "seconds_from_1970"): [
"getClockStatus",
"system",
"clock_status",
"seconds_from_1970",
],
("system", "clock_status", "local_time"): [
"getClockStatus",
"system",
"clock_status",
"local_time",
],
("system", "basic", "zone_id"): [
"getTimezone",
"system",
"basic",
"zone_id",
],
}

async def _send_request(self, request_dict: dict):
Expand All @@ -188,12 +206,14 @@ async def _send_request(self, request_dict: dict):
for skey, sval in skey_val.items():
section_key = skey
section_value = sval
if setter_keys := self.SETTERS.get(
(module, section, section_key)
):
self._get_param_set_value(info, setter_keys, section_value)
else:
return {"error_code": -1}
break
if setter_keys := self.SETTERS.get((module, section, section_key)):
self._get_param_set_value(info, setter_keys, section_value)
return {"error_code": 0}
else:
return {"error_code": -1}
return {"error_code": 0}
elif method[:3] == "get":
params = request_dict.get("params")
if method in info:
Expand Down
16 changes: 15 additions & 1 deletion kasa/tests/smartcamera/test_smartcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

from __future__ import annotations

from datetime import datetime, timezone

import pytest
from freezegun.api import FrozenDateTimeFactory

from kasa import Device, DeviceType
from kasa import Device, DeviceType, Module

from ..conftest import device_smartcamera, hub_smartcamera

Expand Down Expand Up @@ -45,3 +48,14 @@ async def test_hub(dev):
await child.update()
assert "Time" not in child.modules
assert child.time


@device_smartcamera
async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
"""Test a child device gets the time from it's parent module."""
fallback_time = datetime.now(timezone.utc).astimezone().replace(microsecond=0)
assert dev.time != fallback_time
module = dev.modules[Module.Time]
await module.set_time(fallback_time)
await dev.update()
assert dev.time == fallback_time
Loading
0