Make iot time timezone aware (#1147)

Also makes on_since for iot devices use device time.
Changes the return value for device.timezone to be tzinfo instead of a dict.
This commit is contained in:
Steven B.
2024-10-08 08:16:51 +01:00
committed by GitHub
parent 8bb2cca7cf
commit 9641edcbc0
22 changed files with 289 additions and 45 deletions

View File

@@ -10,7 +10,7 @@ class DeviceModule(SmartModule):
REQUIRED_COMPONENT = "device"
def _post_update_hook(self):
async def _post_update_hook(self):
"""Perform actions after a device update.
Overrides the default behaviour to disable a module if the query returns

View File

@@ -152,7 +152,7 @@ class Light(SmartModule, LightInterface):
"""Return the current light state."""
return self._light_state
def _post_update_hook(self) -> None:
async def _post_update_hook(self) -> None:
if self._device.is_on is False:
state = LightState(light_on=False)
else:

View File

@@ -28,7 +28,7 @@ class LightEffect(SmartModule, SmartLightEffect):
_effect_list: list[str]
_scenes_names_to_id: dict[str, str]
def _post_update_hook(self) -> None:
async def _post_update_hook(self) -> None:
"""Update internal effect state."""
# Copy the effects so scene name updates do not update the underlying dict.
effects = copy.deepcopy(

View File

@@ -34,7 +34,7 @@ class LightPreset(SmartModule, LightPresetInterface):
self._state_in_sysinfo = self.SYS_INFO_STATE_KEY in device.sys_info
self._brightness_only: bool = False
def _post_update_hook(self):
async def _post_update_hook(self):
"""Update the internal presets."""
index = 0
self._presets = {}

View File

@@ -90,7 +90,7 @@ class LightTransition(SmartModule):
)
)
def _post_update_hook(self) -> None:
async def _post_update_hook(self) -> None:
"""Update the states."""
# Assumes any device with state in sysinfo supports on and off and
# has maximum values for both.

View File

@@ -2,10 +2,12 @@
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta, timezone, tzinfo
from time import mktime
from typing import cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from ...feature import Feature
from ..smartmodule import SmartModule
@@ -31,18 +33,27 @@ class Time(SmartModule):
)
@property
def time(self) -> datetime:
"""Return device's current datetime."""
def timezone(self) -> tzinfo:
"""Return current timezone."""
td = timedelta(minutes=cast(float, self.data.get("time_diff")))
if self.data.get("region"):
tz = timezone(td, str(self.data.get("region")))
if region := self.data.get("region"):
try:
# Zoneinfo will return a DST aware object
tz: tzinfo = ZoneInfo(region)
except ZoneInfoNotFoundError:
tz = timezone(td, region)
else:
# in case the device returns a blank region this will result in the
# tzname being a UTC offset
tz = timezone(td)
return tz
@property
def time(self) -> datetime:
"""Return device's current datetime."""
return datetime.fromtimestamp(
cast(float, self.data.get("timestamp")),
tz=tz,
tz=self.timezone,
)
async def set_time(self, dt: datetime):

View File

@@ -61,7 +61,7 @@ class SmartChildDevice(SmartDevice):
self._last_update = await self.protocol.query(req)
for module in self.modules.values():
self._handle_module_post_update(
await self._handle_module_post_update(
module, now, had_query=module in module_queries
)
self._last_update_time = now

View File

@@ -6,8 +6,8 @@ import base64
import logging
import time
from collections.abc import Mapping, Sequence
from datetime import datetime, timedelta, timezone
from typing import Any, cast
from datetime import datetime, timedelta, timezone, tzinfo
from typing import TYPE_CHECKING, Any, cast
from ..aestransport import AesTransport
from ..device import Device, WifiNetwork
@@ -168,7 +168,7 @@ class SmartDevice(Device):
await self._initialize_modules()
# Run post update for the cloud module
if cloud_mod := self.modules.get(Module.Cloud):
self._handle_module_post_update(cloud_mod, now, had_query=True)
await self._handle_module_post_update(cloud_mod, now, had_query=True)
resp = await self._modular_update(first_update, now)
@@ -195,7 +195,7 @@ class SmartDevice(Device):
updated = self._last_update if first_update else resp
_LOGGER.debug("Update completed %s: %s", self.host, list(updated.keys()))
def _handle_module_post_update(
async def _handle_module_post_update(
self, module: SmartModule, update_time: float, had_query: bool
):
if module.disabled:
@@ -203,7 +203,7 @@ class SmartDevice(Device):
if had_query:
module._last_update_time = update_time
try:
module._post_update_hook()
await module._post_update_hook()
module._set_error(None)
except Exception as ex:
# Only set the error if a query happened.
@@ -260,7 +260,7 @@ class SmartDevice(Device):
# Call handle update for modules that want to update internal data
for module in self._modules.values():
self._handle_module_post_update(
await self._handle_module_post_update(
module, update_time, had_query=module in module_queries
)
@@ -516,10 +516,11 @@ class SmartDevice(Device):
return self._on_since
@property
def timezone(self) -> dict:
def timezone(self) -> tzinfo:
"""Return the timezone and time_difference."""
ti = self.time
return {"timezone": ti.tzname()}
if TYPE_CHECKING:
assert self.time.tzinfo
return self.time.tzinfo
@property
def hw_info(self) -> dict:

View File

@@ -121,7 +121,7 @@ class SmartModule(Module):
"""Name of the module."""
return getattr(self, "NAME", self.__class__.__name__)
def _post_update_hook(self): # noqa: B027
async def _post_update_hook(self): # noqa: B027
"""Perform actions after a device update.
Any modules overriding this should ensure that self.data is