Cache zoneinfo for smart devices (#1156)

This commit is contained in:
Steven B. 2024-10-08 12:21:01 +01:00 committed by GitHub
parent 9641edcbc0
commit 7c1686d3ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 32 deletions

28
kasa/cachedzoneinfo.py Normal file
View File

@ -0,0 +1,28 @@
"""Module for caching ZoneInfos."""
from __future__ import annotations
import asyncio
from zoneinfo import ZoneInfo
class CachedZoneInfo(ZoneInfo):
"""Cache ZoneInfo objects."""
_cache: dict[str, ZoneInfo] = {}
@classmethod
async def get_cached_zone_info(cls, time_zone_str: str) -> ZoneInfo:
"""Get a cached zone info object."""
if cached := cls._cache.get(time_zone_str):
return cached
loop = asyncio.get_running_loop()
zinfo = await loop.run_in_executor(None, _get_zone_info, time_zone_str)
cls._cache[time_zone_str] = zinfo
return zinfo
def _get_zone_info(time_zone_str: str) -> ZoneInfo:
"""Get a time zone object for the given time zone string."""
return ZoneInfo(time_zone_str)

View File

@ -2,11 +2,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from datetime import datetime, tzinfo from datetime import datetime, tzinfo
from zoneinfo import ZoneInfo from ..cachedzoneinfo import CachedZoneInfo
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -17,10 +16,10 @@ async def get_timezone(index: int) -> tzinfo:
_LOGGER.error( _LOGGER.error(
"Unexpected index %s not configured as a timezone, defaulting to UTC", index "Unexpected index %s not configured as a timezone, defaulting to UTC", index
) )
return await _CachedZoneInfo.get_cached_zone_info("Etc/UTC") return await CachedZoneInfo.get_cached_zone_info("Etc/UTC")
name = TIMEZONE_INDEX[index] name = TIMEZONE_INDEX[index]
return await _CachedZoneInfo.get_cached_zone_info(name) return await CachedZoneInfo.get_cached_zone_info(name)
async def get_timezone_index(name: str) -> int: async def get_timezone_index(name: str) -> int:
@ -30,7 +29,7 @@ async def get_timezone_index(name: str) -> int:
return rev[name] return rev[name]
# Try to find a supported timezone matching dst true/false # Try to find a supported timezone matching dst true/false
zone = await _CachedZoneInfo.get_cached_zone_info(name) zone = await CachedZoneInfo.get_cached_zone_info(name)
now = datetime.now() now = datetime.now()
winter = datetime(now.year, 1, 1, 12) winter = datetime(now.year, 1, 1, 12)
summer = datetime(now.year, 7, 1, 12) summer = datetime(now.year, 7, 1, 12)
@ -43,27 +42,6 @@ async def get_timezone_index(name: str) -> int:
raise ValueError("Device does not support timezone %s", name) raise ValueError("Device does not support timezone %s", name)
class _CachedZoneInfo(ZoneInfo):
"""Cache zone info objects."""
_cache: dict[str, ZoneInfo] = {}
@classmethod
async def get_cached_zone_info(cls, time_zone_str: str) -> ZoneInfo:
"""Get a cached zone info object."""
if cached := cls._cache.get(time_zone_str):
return cached
loop = asyncio.get_running_loop()
zinfo = await loop.run_in_executor(None, _get_zone_info, time_zone_str)
cls._cache[time_zone_str] = zinfo
return zinfo
def _get_zone_info(time_zone_str: str) -> ZoneInfo:
"""Get a time zone object for the given time zone string."""
return ZoneInfo(time_zone_str)
TIMEZONE_INDEX = { TIMEZONE_INDEX = {
0: "Etc/GMT+12", 0: "Etc/GMT+12",
1: "Pacific/Samoa", 1: "Pacific/Samoa",

View File

@ -6,8 +6,9 @@ from datetime import datetime, timedelta, timezone, tzinfo
from time import mktime from time import mktime
from typing import cast from typing import cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfoNotFoundError
from ...cachedzoneinfo import CachedZoneInfo
from ...feature import Feature from ...feature import Feature
from ..smartmodule import SmartModule from ..smartmodule import SmartModule
@ -18,6 +19,8 @@ class Time(SmartModule):
REQUIRED_COMPONENT = "time" REQUIRED_COMPONENT = "time"
QUERY_GETTER_NAME = "get_device_time" QUERY_GETTER_NAME = "get_device_time"
_timezone: tzinfo = timezone.utc
def _initialize_features(self): def _initialize_features(self):
"""Initialize features after the initial update.""" """Initialize features after the initial update."""
self._add_feature( self._add_feature(
@ -32,21 +35,25 @@ class Time(SmartModule):
) )
) )
@property async def _post_update_hook(self):
def timezone(self) -> tzinfo: """Perform actions after a device update."""
"""Return current timezone."""
td = timedelta(minutes=cast(float, self.data.get("time_diff"))) td = timedelta(minutes=cast(float, self.data.get("time_diff")))
if region := self.data.get("region"): if region := self.data.get("region"):
try: try:
# Zoneinfo will return a DST aware object # Zoneinfo will return a DST aware object
tz: tzinfo = ZoneInfo(region) tz: tzinfo = await CachedZoneInfo.get_cached_zone_info(region)
except ZoneInfoNotFoundError: except ZoneInfoNotFoundError:
tz = timezone(td, region) tz = timezone(td, region)
else: else:
# in case the device returns a blank region this will result in the # in case the device returns a blank region this will result in the
# tzname being a UTC offset # tzname being a UTC offset
tz = timezone(td) tz = timezone(td)
return tz self._timezone = tz
@property
def timezone(self) -> tzinfo:
"""Return current timezone."""
return self._timezone
@property @property
def time(self) -> datetime: def time(self) -> datetime: