Skip to content

Commit

Permalink
Make last activity timezone aware
Browse files Browse the repository at this point in the history
  • Loading branch information
Shutgun committed May 7, 2023
1 parent f474e90 commit 2374edf
Show file tree
Hide file tree
Showing 27 changed files with 428 additions and 297 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
pylint --exit-zero --score=n --disable=E --enable=useless-suppression devolo_home_control_api
- name: Lint with mypy
run: |
pip install mypy types-requests types-setuptools
pip install mypy types-python-dateutil types-requests
mypy --ignore-missing-imports devolo_home_control_api
test:
Expand Down
8 changes: 8 additions & 0 deletions devolo_home_control_api/devices/gateway.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""The devolo Home Control Central Unit."""
import logging
from datetime import timezone
from typing import Dict, Optional

from dateutil import tz

from devolo_home_control_api.exceptions import GatewayOfflineError
from devolo_home_control_api.mydevolo import Mydevolo

Expand Down Expand Up @@ -30,6 +33,11 @@ def __init__(self, gateway_id: str, mydevolo_instance: Mydevolo) -> None:
self.external_access = details.get("externalAccess")
self.firmware_version = details.get("firmwareVersion")

if details["location"]:
self.timezone = tz.gettz(details["location"]["timezone"]) or timezone.utc
else:
self.timezone = tz.gettz(self._mydevolo.get_timezone()) or timezone.utc

try:
self.full_url: Optional[str] = self._mydevolo.get_full_url(self.id)
except GatewayOfflineError:
Expand Down
59 changes: 49 additions & 10 deletions devolo_home_control_api/homecontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def _binary_sensor(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding binary sensor property to %s.", device_uid)
self.devices[device_uid].binary_sensor_property[uid_info["UID"]] = BinarySensorProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
state=bool(uid_info["properties"]["state"]),
sensor_type=uid_info["properties"]["sensorType"],
sub_type=uid_info["properties"]["subType"],
Expand All @@ -165,6 +166,7 @@ def _binary_switch(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding binary switch property to %s.", device_uid)
self.devices[device_uid].binary_switch_property[uid_info["UID"]] = BinarySwitchProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_binary_switch,
state=bool(uid_info["properties"]["state"]),
enabled=uid_info["properties"]["guiEnabled"],
Expand All @@ -176,6 +178,7 @@ def _general_device(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding general device settings to %s.", device_uid)
self.devices[device_uid].settings_property["general_device_settings"] = SettingsProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
events_enabled=uid_info["properties"]["settings"]["eventsEnabled"],
name=uid_info["properties"]["settings"]["name"],
Expand All @@ -201,7 +204,9 @@ def _humidity_bar(self, uid_info: Dict[str, Any]) -> None:
self.devices[device_uid].humidity_bar_property = {}
if self.devices[device_uid].humidity_bar_property.get(fake_element_uid) is None:
self.devices[device_uid].humidity_bar_property[fake_element_uid] = HumidityBarProperty(
element_uid=fake_element_uid, sensorType="humidityBar"
element_uid=fake_element_uid,
tz=self.gateway.timezone,
sensorType="humidityBar",
)
if uid_info["properties"]["sensorType"] == "humidityBarZone":
self._logger.debug("Adding humidity bar zone property to %s.", device_uid)
Expand Down Expand Up @@ -252,6 +257,7 @@ def _automatic_calibration(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding automatic calibration setting to %s.", device_uid)
self.devices[device_uid].settings_property["automatic_calibration"] = SettingsProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
calibration_status=bool(uid_info["properties"]["calibrationStatus"]),
)
Expand All @@ -264,15 +270,21 @@ def _binary_sync(self, uid_info: Dict[str, Any]) -> None:
device_uid = get_device_uid_from_setting_uid(uid_info["UID"])
self._logger.debug("Adding binary sync setting to %s.", device_uid)
self.devices[device_uid].settings_property["movement_direction"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, inverted=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
inverted=uid_info["properties"]["value"],
)

def _binary_async(self, uid_info: Dict[str, Any]) -> None:
"""Process binary async setting (bas) properties."""
device_uid = get_device_uid_from_setting_uid(uid_info["UID"])
self._logger.debug("Adding binary async settings to %s.", device_uid)
settings_property = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, value=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
value=uid_info["properties"]["value"],
)

# The siren needs to be handled differently, as otherwise their binary async setting will not be named nicely
Expand Down Expand Up @@ -309,7 +321,10 @@ def _led(self, uid_info: Dict[str, Any]) -> None:
except KeyError:
led_setting = uid_info["properties"]["feedback"]
self.devices[device_uid].settings_property["led"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, led_setting=led_setting
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
led_setting=led_setting,
)

def _meter(self, uid_info: Dict[str, Any]) -> None:
Expand All @@ -320,6 +335,7 @@ def _meter(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding consumption property to %s.", device_uid)
self.devices[device_uid].consumption_property[uid_info["UID"]] = ConsumptionProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
current=uid_info["properties"]["currentValue"],
total=uid_info["properties"]["totalValue"],
total_since=uid_info["properties"]["sinceTime"],
Expand All @@ -339,7 +355,10 @@ def _multilevel_async(self, uid_info: Dict[str, Any]) -> None:

self._logger.debug("Adding %s setting to %s.", name, device_uid)
self.devices[device_uid].settings_property[name] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, value=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
value=uid_info["properties"]["value"],
)

def _multilevel_sync(self, uid_info: Dict[str, Any]) -> None:
Expand All @@ -350,21 +369,30 @@ def _multilevel_sync(self, uid_info: Dict[str, Any]) -> None:
if self.devices[device_uid].device_model_uid == "devolo.model.Siren":
self._logger.debug("Adding tone settings to %s.", device_uid)
self.devices[device_uid].settings_property["tone"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, tone=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
tone=uid_info["properties"]["value"],
)

# The shutter needs to be handled differently, as otherwise their multilevel sync setting will not be named nicely.
elif self.devices[device_uid].device_model_uid in ("devolo.model.OldShutter", "devolo.model.Shutter"):
self._logger.debug("Adding shutter duration settings to %s.", device_uid)
self.devices[device_uid].settings_property["shutter_duration"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, shutter_duration=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
shutter_duration=uid_info["properties"]["value"],
)

# Other devices are up to now always motion sensors.
else:
self._logger.debug("Adding motion sensitivity settings to %s.", device_uid)
self.devices[device_uid].settings_property["motion_sensitivity"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, motion_sensitivity=uid_info["properties"]["value"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
motion_sensitivity=uid_info["properties"]["value"],
)

def _multi_level_sensor(self, uid_info: Dict[str, Any]) -> None:
Expand All @@ -375,6 +403,7 @@ def _multi_level_sensor(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding multi level sensor property %s to %s.", uid_info["UID"], device_uid)
self.devices[device_uid].multi_level_sensor_property[uid_info["UID"]] = MultiLevelSensorProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
value=uid_info["properties"]["value"],
unit=uid_info["properties"]["unit"],
sensor_type=uid_info["properties"]["sensorType"],
Expand All @@ -388,6 +417,7 @@ def _multi_level_switch(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding multi level switch property %s to %s.", uid_info["UID"], device_uid)
self.devices[device_uid].multi_level_switch_property[uid_info["UID"]] = MultiLevelSwitchProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_multi_level_switch,
value=uid_info["properties"]["value"],
switch_type=uid_info["properties"]["switchType"],
Expand All @@ -400,7 +430,10 @@ def _parameter(self, uid_info: Dict[str, Any]) -> None:
device_uid = get_device_uid_from_setting_uid(uid_info["UID"])
self._logger.debug("Adding parameter settings to %s.", device_uid)
self.devices[device_uid].settings_property["param_changed"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, param_changed=uid_info["properties"]["paramChanged"]
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
param_changed=uid_info["properties"]["paramChanged"],
)

def _protection(self, uid_info: Dict[str, Any]) -> None:
Expand All @@ -409,6 +442,7 @@ def _protection(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding protection settings to %s.", device_uid)
self.devices[device_uid].settings_property["protection"] = SettingsProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
local_switching=uid_info["properties"]["localSwitch"],
remote_switching=uid_info["properties"]["remoteSwitch"],
Expand All @@ -422,6 +456,7 @@ def _remote_control(self, uid_info: Dict[str, Any]) -> None:
self.devices[device_uid].remote_control_property = {}
self.devices[device_uid].remote_control_property[uid_info["UID"]] = RemoteControlProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_remote_control,
key_count=uid_info["properties"]["keyCount"],
key_pressed=uid_info["properties"]["keyPressed"],
Expand All @@ -436,7 +471,10 @@ def _switch_type(self, uid_info: Dict[str, Any]) -> None:
device_uid = get_device_uid_from_setting_uid(uid_info["UID"])
self._logger.debug("Adding switch type setting to %s.", device_uid)
self.devices[device_uid].settings_property["switch_type"] = SettingsProperty(
element_uid=uid_info["UID"], setter=self.set_setting, value=uid_info["properties"]["switchType"] * 2
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
value=uid_info["properties"]["switchType"] * 2,
)

def _temperature_report(self, uid_info: Dict[str, Any]) -> None:
Expand All @@ -445,6 +483,7 @@ def _temperature_report(self, uid_info: Dict[str, Any]) -> None:
self._logger.debug("Adding temperature report settings to %s.", device_uid)
self.devices[device_uid].settings_property["temperature_report"] = SettingsProperty(
element_uid=uid_info["UID"],
tz=self.gateway.timezone,
setter=self.set_setting,
temp_report=uid_info["properties"]["tempReport"],
target_temp_report=uid_info["properties"]["targetTempReport"],
Expand Down
11 changes: 11 additions & 0 deletions devolo_home_control_api/mydevolo.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def get_gateway(self, gateway_id: str) -> Dict[str, Any]:
except WrongUrlError:
self._logger.error("Could not get full URL. Wrong gateway ID used?")
raise
if details["location"]:
details["location"] = self._call(details["location"])
return details

def get_full_url(self, gateway_id: str) -> str:
Expand All @@ -92,6 +94,15 @@ def get_full_url(self, gateway_id: str) -> str:
self._logger.debug("Getting full URL of gateway.")
return self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/{gateway_id}/fullURL")["url"]

def get_timezone(self) -> str:
"""
Get user's standard timezone.
:return: Standard timezone of the user based on his country settings.
"""
self._logger.debug("Getting the user's standard timezone.")
return self._call(f"{self.url}/v1/users/{self.uuid()}/standardTimezone")["timezone"]

def get_zwave_products(self, manufacturer: str, product_type: str, product: str) -> Dict[str, Any]:
"""
Get information about a Z-Wave device.
Expand Down
11 changes: 6 additions & 5 deletions devolo_home_control_api/properties/binary_sensor_property.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Binary Sensors."""
from datetime import datetime
from datetime import datetime, timezone, tzinfo
from typing import Any

from devolo_home_control_api.exceptions import WrongElementError
Expand All @@ -12,18 +12,19 @@ class BinarySensorProperty(SensorProperty):
Object for binary sensors. It stores the binary sensor state.
:param element_uid: Element UID, something like devolo.BinarySensor:hdm:ZWave:CBC56091/24
:param tz: Timezone the last activity is recorded in
:key state: State of the binary sensor
:type state: bool
"""

def __init__(self, element_uid: str, **kwargs: Any) -> None:
def __init__(self, element_uid: str, tz: tzinfo, **kwargs: Any) -> None:
"""Initialize the binary sensor."""
if not element_uid.startswith(
("devolo.BinarySensor:", "devolo.MildewSensor:", "devolo.ShutterMovementFI:", "devolo.WarningBinaryFI:")
):
raise WrongElementError(element_uid, self.__class__.__name__)

super().__init__(element_uid=element_uid, **kwargs)
super().__init__(element_uid, tz, **kwargs)

self._state: bool = kwargs.pop("state", False)

Expand All @@ -39,7 +40,7 @@ def last_activity(self, timestamp: int) -> None:
They can be initialized with that value. The others stay with a default timestamp until first update.
"""
if timestamp != -1:
self._last_activity = datetime.utcfromtimestamp(timestamp / 1000)
self._last_activity = datetime.fromtimestamp(timestamp / 1000, tz=timezone.utc).replace(tzinfo=self._timezone)
self._logger.debug("last_activity of element_uid %s set to %s.", self.element_uid, self._last_activity)

@property
Expand All @@ -51,5 +52,5 @@ def state(self) -> bool:
def state(self, state: bool) -> None:
"""Update state of the binary sensor and set point in time of the last_activity."""
self._state = state
self._last_activity = datetime.now()
self._last_activity = datetime.now(tz=self._timezone)
self._logger.debug("state of element_uid %s set to %s.", self.element_uid, state)
9 changes: 5 additions & 4 deletions devolo_home_control_api/properties/binary_switch_property.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Binary Switches."""
from datetime import datetime
from datetime import datetime, tzinfo
from typing import Callable

from devolo_home_control_api.exceptions import SwitchingProtected, WrongElementError
Expand All @@ -12,19 +12,20 @@ class BinarySwitchProperty(Property):
Object for binary switches. It stores the binary switch state.
:param element_uid: Element UID, something like devolo.BinarySwitch:hdm:ZWave:CBC56091/24#2
:param tz: Timezone the last activity is recorded in
:param setter: Method to call on setting the state
:key enabled: State of the remote protection setting
:type enabled: bool
:key state: State the switch has at time of creating this instance
:type state: bool
"""

def __init__(self, element_uid: str, setter: Callable[[str, bool], bool], **kwargs: bool) -> None:
def __init__(self, element_uid: str, tz: tzinfo, setter: Callable[[str, bool], bool], **kwargs: bool) -> None:
"""Initialize the binary switch."""
if not element_uid.startswith("devolo.BinarySwitch:"):
raise WrongElementError(element_uid, self.__class__.__name__)

super().__init__(element_uid=element_uid)
super().__init__(element_uid, tz)
self._setter = setter

self._state: bool = kwargs.pop("state", False)
Expand All @@ -39,7 +40,7 @@ def state(self) -> bool:
def state(self, state: bool) -> None:
"""Update state of the binary sensor and set point in time of the last_activity."""
self._state = state
self._last_activity = datetime.now()
self._last_activity = datetime.now(tz=self._timezone)
self._logger.debug("State of %s set to %s.", self.element_uid, state)

def set(self, state: bool) -> bool:
Expand Down
Loading

0 comments on commit 2374edf

Please sign in to comment.