8000 Add Time module to SmartCamera devices (#1182) · python-kasa/python-kasa@e3610cf · GitHub
[go: up one dir, main page]

Skip to content

Commit e3610cf

Browse files
authored
Add Time module to SmartCamera devices (#1182)
1 parent 28361c1 commit e3610cf

File tree

5 files changed

+139
-8
lines changed

5 files changed

+139
-8
lines changed

kasa/experimental/modules/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from .camera import Camera
44
from .childdevice import ChildDevice
55
from .device import DeviceModule
6+
from .time import Time
67

78
__all__ = [
89
"Camera",
910
"ChildDevice",
1011
"DeviceModule",
12+
"Time",
1113
]

kasa/experimental/modules/time.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Implementation of time module."""
2+
3+
from __future__ import annotations
4+
5+
from datetime import datetime, timezone, tzinfo
6+
from typing import cast
7+
8+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
9+
10+
from ...cachedzoneinfo import CachedZoneInfo
11+
from ...feature import Feature
12+
from ...interfaces import Time as TimeInterface
13+
from ..smartcameramodule import SmartCameraModule
14+
15+
16+
class Time(SmartCameraModule, TimeInterface):
17+
"""Implementation of device_local_time."""
18+
19+
QUERY_GETTER_NAME = "getTimezone"
20+
QUERY_MODULE_NAME = "system"
21+
QUERY_SECTION_NAMES = "basic"
22+
23+
_timezone: tzinfo = timezone.utc
24+
_time: datetime
25+
26+
def _initialize_features(self) -> None:
27+
"""Initialize features after the initial update."""
28+
self._add_feature(
29+
Feature(
30+
device=self._device,
31+
id="device_time",
32+
name="Device time",
33+
attribute_getter="time",
34+
container=self,
35+
category=Feature.Category.Debug,
36+
type=Feature.Type.Sensor,
37+
)
38+
)
39+
40+
def query(self) -> dict:
41+
"""Query to execute during the update cycle."""
42+
q = super().query()
43+
q["getClockStatus"] = {self.QUERY_MODULE_NAME: {"name": "clock_status"}}
44+
45+
return q
46+
47+
async def _post_update_hook(self) -> None:
48+
"""Perform actions after a device update."""
49+
time_data = self.data["getClockStatus"]["system"]["clock_status"]
50+
timezone_data = self.data["getTimezone"]["system"]["basic"]
51+
zone_id = timezone_data["zone_id"]
52+
timestamp = time_data["seconds_from_1970"]
53+
try:
54+
# Zoneinfo will return a DST aware object
55+
tz: tzinfo = await CachedZoneInfo.get_cached_zone_info(zone_id)
56+
except ZoneInfoNotFoundError:
57+
# timezone string like: UTC+10:00
58+
timezone_str = timezone_data["timezone"]
59+
tz = cast(tzinfo, datetime.strptime(timezone_str[-6:], "%z").tzinfo)
60+
61+
self._timezone = tz
62+
self._time = datetime.fromtimestamp(
63+
cast(float, timestamp),
64+
tz=tz,
65+
)
66+
67+
@property
68+
def timezone(self) -> tzinfo:
69+
"""Return current timezone."""
70+
return self._timezone
71+
72+
@property
73+
def time(self) -> datetime:
74+
"""Return device's current datetime."""
75+
return self._time
76+
77+
async def set_time(self, dt: datetime) -> dict:
78+
"""Set device time."""
79+
if not dt.tzinfo:
80+
timestamp = dt.replace(tzinfo=self.timezone).timestamp()
81+
else:
82+
timestamp = dt.timestamp()
83+
84+
lt = datetime.fromtimestamp(timestamp).isoformat().replace("T", " ")
85+
params = {"seconds_from_1970": int(timestamp), "local_time": lt}
86+
# Doesn't seem to update the time, perhaps because timing_mode is ntp
87+
res = await self.call("setTimezone", {"system": {"clock_status": params}})
88+
if (zinfo := dt.tzinfo) and isinstance(zinfo, ZoneInfo):
89+
tz_params = {"zone_id": zinfo.key}
90+
res = await self.call("setTimezone", {"system": {"basic": tz_params}})
91+
return res

kasa/experimental/smartcameramodule.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, Any, cast
77

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

57-
return await self._device._query_setter_helper(method, module, section, params)
57+
if TYPE_CHECKING:
58+
params = cast(dict[str, dict[str, Any]], params)
59+
return await self._device._query_setter_helper(
60+
method, module, section, params[module][section]
61+
)
5862

5963
@property
6064
def data(self) -> dict:

kasa/tests/fakeprotocol_smartcamera.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,24 @@ def _get_param_set_value(info: dict, set_keys: list[str], value):
162162
"lens_mask_info",
163163
"enabled",
164164
],
165+
("system", "clock_status", "seconds_from_1970"): [
166+
"getClockStatus",
167+
"system",
168+
"clock_status",
169+
"seconds_from_1970",
170+
],
171+
("system", "clock_status", "local_time"): [
172+
"getClockStatus",
173+
"system",
174+
"clock_status",
175+
"local_time",
176+
],
177+
("system", "basic", "zone_id"): [
178+
"getTimezone",
179+
"system",
180+
"basic",
181+
"zone_id",
182+
],
165183
}
166184

167185
async def _send_request(self, request_dict: dict):
@@ -188,12 +206,14 @@ async def _send_request(self, request_dict: dict):
188206
for skey, sval in skey_val.items():
189207
section_key = skey
190208
section_value = sval
209+
if setter_keys := self.SETTERS.get(
210+
(module, section, section_key)
211+
):
212+
self._get_param_set_value(info, setter_keys, section_value)
213+
else:
214+
return {"error_code": -1}
191215
break
192-
if setter_keys := self.SETTERS.get((module, section, section_key)):
193-
self._get_param_set_value(info, setter_keys, section_value)
194-
return {"error_code": 0}
195-
else:
196-
return {"error_code": -1}
216+
return {"error_code": 0}
197217
elif method[:3] == "get":
198218
params = request_dict.get("params")
199219
if method in info:

kasa/tests/smartcamera/test_smartcamera.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
from __future__ import annotations
44

5+
from datetime import datetime, timezone
6+
57
import pytest
8+
from freezegun.api import FrozenDateTimeFactory
69

7-
from kasa import Device, DeviceType
10+
from kasa import Device, DeviceType, Module
811

912
from ..conftest import device_smartcamera, hub_smartcamera
1013

@@ -45,3 +48,14 @@ async def test_hub(dev):
4548
await child.update()
4649
assert "Time" not in child.modules
4750
assert child.time
51+
52+
53+
@device_smartcamera
54+
async def test_device_time(dev: Device, freezer: FrozenDateTimeFactory):
55+
"""Test a child device gets the time from it's parent module."""
56+
fallback_time = datetime.now(timezone.utc).astimezone().replace(microsecond=0)
57+
assert dev.time != fallback_time
58+
module = dev.modules[Module.Time]
59+
await module.set_time(fallback_time)
60+
await dev.update()
61+
assert dev.time == fallback_time

0 commit comments

Comments
 (0)
0