From 38c36fa3e3cf93412b2c861dfa1469ec41f0dcb6 Mon Sep 17 00:00:00 2001 From: Patrik Lindgren <21142447+ggravlingen@users.noreply.github.com> Date: Tue, 15 Mar 2022 07:25:18 +0100 Subject: [PATCH] Add type hints to smart_task.py (#456) * Add type hints StartActionItemController * Fix typo * Add type hints * Convert to int * Add type hints * FIx test * Make transition time optional * Implement calibration method * Improve setter * Remove setter * Refactor * Update pytradfri/smart_task.py Co-authored-by: Martin Hjelmare * Remove type annotation * Fix? * Remove type hints Co-authored-by: Martin Hjelmare --- mypy.ini | 11 ++ pytradfri/resource.py | 4 +- pytradfri/smart_task.py | 307 ++++++++++++++++++++++++++------------- tests/test_smart_task.py | 2 + 4 files changed, 218 insertions(+), 106 deletions(-) diff --git a/mypy.ini b/mypy.ini index ec7cd747..185b167e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -278,3 +278,14 @@ disallow_untyped_defs = true no_implicit_optional = true warn_return_any = true warn_unreachable = true + +[mypy-pytradfri.smart_task] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true diff --git a/pytradfri/resource.py b/pytradfri/resource.py index a83ae897..96d9c14d 100644 --- a/pytradfri/resource.py +++ b/pytradfri/resource.py @@ -23,7 +23,7 @@ class BaseResponse(BaseModel): class ApiResourceResponse(BaseResponse): """Represent a resource response.""" - name: str = Field(alias=ATTR_NAME) + name: Optional[str] = Field(alias=ATTR_NAME) created_at: Optional[int] = Field(alias=ATTR_CREATED_AT) ota_update_state: Optional[int] = Field(alias=ATTR_OTA_UPDATE_STATE) @@ -51,7 +51,7 @@ def id(self) -> int: return resource_id @property - def name(self) -> str: + def name(self) -> str | None: """Name.""" if self._model_class: name = self.raw.name # type: ignore[union-attr] diff --git a/pytradfri/smart_task.py b/pytradfri/smart_task.py index 7a532bdb..29ec1cd6 100644 --- a/pytradfri/smart_task.py +++ b/pytradfri/smart_task.py @@ -8,13 +8,17 @@ StartActionItem # Get info on specific device in task StartActionItemController # change values for task """ +from __future__ import annotations -import datetime -from datetime import datetime as dt +from datetime import datetime as dt, time, timedelta +from typing import TYPE_CHECKING, Any, List, Optional + +from pydantic import BaseModel, Field from .command import Command from .const import ( ATTR_DEVICE_STATE, + ATTR_GATEWAY_INFO, ATTR_ID, ATTR_LIGHT_DIMMER, ATTR_REPEAT_DAYS, @@ -26,14 +30,20 @@ ATTR_SMART_TASK_TYPE, ATTR_SMART_TASK_WAKE_UP, ATTR_START_ACTION, + ATTR_TIME_START_TIME_MINUTE, ATTR_TRANSITION_TIME, + ROOT_GATEWAY, ROOT_SMART_TASKS, ROOT_START_ACTION, ) -from .resource import ApiResource +from .resource import ApiResource, ApiResourceResponse, BaseResponse, TypeRaw from .util import BitChoices -WEEKDAYS = BitChoices( +if TYPE_CHECKING: + from .gateway import Gateway, GatewayInfo + + +WEEKDAYS: BitChoices = BitChoices( ( ("mon", "Monday"), ("tue", "Tuesday"), @@ -46,31 +56,72 @@ ) +class SmartTaskMixin(BaseModel): + """Represent common task attributes.""" + + state: int = Field(alias=ATTR_DEVICE_STATE) + + +class StartActionResponse(BaseResponse): + """Represent a start action response.""" + + transition_time: Optional[int] = Field(alias=ATTR_TRANSITION_TIME) + dimmer: int = Field(alias=ATTR_LIGHT_DIMMER) + + +class TimeIntervalResponse(BaseModel): + """Represent a time interval response.""" + + hour_start: int = Field(alias=ATTR_SMART_TASK_TRIGGER_TIME_START_HOUR) + minute_start: int = Field(alias=ATTR_TIME_START_TIME_MINUTE) + + +class RootStartActionResponse(SmartTaskMixin, BaseModel): + """Represent a smart action response.""" + + root_start_action: List[StartActionResponse] = Field(alias=ROOT_START_ACTION) + + +class SmartTaskResponse(SmartTaskMixin, ApiResourceResponse): + """Represent a smart task response.""" + + smart_task_type: int = Field(alias=ATTR_SMART_TASK_TYPE) + repeat_days: int = Field(alias=ATTR_REPEAT_DAYS) + start_action: RootStartActionResponse = Field(alias=ATTR_START_ACTION) + time_interval: List[TimeIntervalResponse] = Field( + alias=ATTR_SMART_TASK_TRIGGER_TIME_INTERVAL + ) + + class SmartTask(ApiResource): """Represent a smart task.""" - def __init__(self, gateway, raw): + _model_class: type[SmartTaskResponse] = SmartTaskResponse + raw: SmartTaskResponse + + def __init__(self, gateway: Gateway, raw: TypeRaw) -> None: """Initialize the class.""" super().__init__(raw) self._gateway = gateway + self.delta_time_gateway_local = timedelta(0) @property - def path(self): + def path(self) -> list[str]: """Return gateway path.""" return [ROOT_SMART_TASKS, str(self.id)] @property - def state(self): + def state(self) -> bool: """Boolean representing the light state of the transition.""" - return self.raw.get(ATTR_DEVICE_STATE) == 1 + return self.raw.state == 1 @property - def task_type_id(self): + def task_type_id(self) -> int: """Return type of task.""" - return self.raw.get(ATTR_SMART_TASK_TYPE) + return self.raw.smart_task_type @property - def task_type_name(self): + def task_type_name(self) -> str | None: """Return the task type in plain text. (Own interpretation of names.) @@ -84,57 +135,57 @@ def task_type_name(self): return None @property - def is_wake_up(self): + def is_wake_up(self) -> bool: """Boolean representing if this is a wake up task.""" - return self.raw.get(ATTR_SMART_TASK_TYPE) == ATTR_SMART_TASK_WAKE_UP + return self.raw.smart_task_type == ATTR_SMART_TASK_WAKE_UP @property - def is_not_at_home(self): + def is_not_at_home(self) -> bool: """Boolean representing if this is a not home task.""" - return self.raw.get(ATTR_SMART_TASK_TYPE) == ATTR_SMART_TASK_NOT_AT_HOME + return self.raw.smart_task_type == ATTR_SMART_TASK_NOT_AT_HOME @property - def is_lights_off(self): + def is_lights_off(self) -> bool: """Boolean representing if this is a lights off task.""" - return self.raw.get(ATTR_SMART_TASK_TYPE) == ATTR_SMART_TASK_LIGHTS_OFF + return self.raw.smart_task_type == ATTR_SMART_TASK_LIGHTS_OFF @property - def repeat_days(self): + def repeat_days(self) -> int: """Return int (bit) for enabled weekdays.""" - return self.raw.get(ATTR_REPEAT_DAYS) + return self.raw.repeat_days @property - def repeat_days_list(self): + def repeat_days_list(self) -> list[str]: """Binary representation of weekdays the event takes place.""" - return WEEKDAYS.get_selected_values(self.raw.get(ATTR_REPEAT_DAYS)) + return WEEKDAYS.get_selected_values(self.repeat_days) @property - def task_start_parameters(self): + def task_start_parameters(self) -> TimeIntervalResponse: """Return hour and minute that task starts.""" - return self.raw.get(ATTR_SMART_TASK_TRIGGER_TIME_INTERVAL)[0] + return self.raw.time_interval[0] @property - def task_start_time(self): + def task_start_time(self) -> time: """Return the time the task starts. Time is set according to iso8601. """ - return datetime.time( - self.task_start_parameters[ATTR_SMART_TASK_TRIGGER_TIME_START_HOUR], - self.task_start_parameters[ATTR_SMART_TASK_TRIGGER_TIME_START_MIN], + return time( + self.task_start_parameters.hour_start, + self.task_start_parameters.minute_start, ) @property - def task_control(self): + def task_control(self) -> TaskControl: """Control a task.""" return TaskControl(self, self.state, self.path, self._gateway) @property - def start_action(self): + def start_action(self) -> StartAction: """Return start action object.""" return StartAction(self, self.path) - def __repr__(self): + def __repr__(self) -> str: """Return a readable name for smart task.""" state = "on" if self.state else "off" return f"" @@ -143,7 +194,9 @@ def __repr__(self): class TaskControl: """Class to control the tasks.""" - def __init__(self, task, state, path, gateway): + def __init__( + self, task: SmartTask, state: bool, path: list[str], gateway: Gateway + ) -> None: """Initialize TaskControl.""" self._task = task self.state = state @@ -151,72 +204,94 @@ def __init__(self, task, state, path, gateway): self._gateway = gateway @property - def tasks(self): + def tasks(self) -> list[StartActionItem]: """Return task objects of the task control.""" return [ - StartActionItem(self._task, i, self.state, self.path, self.raw) - for i in range(len(self.raw)) + StartActionItem(self._task, idx, self.state, self.path, self.raw) + for idx in range(len(self.raw.root_start_action)) ] - def set_dimmer_start_time(self, hour, minute): + def calibrate_time(self) -> Command[None]: + """Calibrate difference between local time and gateway time.""" + + def process_result(result: TypeRaw) -> None: + gateway_info: GatewayInfo = GatewayInfo(result) + if not gateway_info.current_time: + return + + d_now = gateway_info.current_time + d_utcnow = dt.utcnow() + diff = d_now - d_utcnow + + self._task.delta_time_gateway_local = diff + + return Command( + "get", [ROOT_GATEWAY, ATTR_GATEWAY_INFO], process_result=process_result + ) + + def set_dimmer_start_time(self, hour: int, minute: int) -> Command[None]: """Set start time for task (hh:mm) in iso8601. NB: dimmer starts 30 mins before time in app """ - # This is to calculate the difference between local time - # and the time in the gateway - d_now = self._gateway.get_gateway_info().current_time - d_utcnow = dt.utcnow() - diff = d_now - d_utcnow - newtime = dt(100, 1, 1, hour, minute, 00) - diff - - command = { + new_time: dt = ( + dt(100, 1, 1, hour, minute, 00) - self._task.delta_time_gateway_local + ) + + command: dict[str, list[dict[str, int]]] = { ATTR_SMART_TASK_TRIGGER_TIME_INTERVAL: [ { - ATTR_SMART_TASK_TRIGGER_TIME_START_HOUR: newtime.hour, - ATTR_SMART_TASK_TRIGGER_TIME_START_MIN: newtime.minute, + ATTR_SMART_TASK_TRIGGER_TIME_START_HOUR: new_time.hour, + ATTR_SMART_TASK_TRIGGER_TIME_START_MIN: new_time.minute, } ] } return self._task.set_values(command) @property - def raw(self): + def raw(self) -> RootStartActionResponse: """Return raw data that it represents.""" - return self._task.raw[ATTR_START_ACTION] + return self._task.raw.start_action class StartAction: """Class to control the start action-node.""" - def __init__(self, start_action, path): + def __init__(self, smart_task: SmartTask, path: list[str]) -> None: """Initialize StartAction class.""" - self.start_action = start_action + self.smart_task = smart_task self.path = path @property - def state(self): + def state(self) -> bool: """Return state of start action task.""" - return self.raw.get(ATTR_DEVICE_STATE) + return self.raw.state == 1 @property - def devices(self): + def devices(self) -> list[StartActionItem]: """Return state of start action task.""" return [ - StartActionItem(self.start_action, i, self.state, self.path, self.raw) - for i in range(len(self.raw[ROOT_START_ACTION])) + StartActionItem(self.smart_task, i, self.state, self.path, self.raw) + for i in range(len(self.raw.root_start_action)) ] @property - def raw(self): + def raw(self) -> RootStartActionResponse: """Return raw data that it represents.""" - return self.start_action.raw[ATTR_START_ACTION] + return self.smart_task.raw.start_action class StartActionItem: """Class to show settings for a task.""" - def __init__(self, task, index, state, path, raw): + def __init__( + self, + task: SmartTask, + index: int, + state: bool, + path: list[str], + raw: RootStartActionResponse, + ): """Initialize TaskInfo.""" self.task = task self.index = index @@ -225,47 +300,57 @@ def __init__(self, task, index, state, path, raw): self._raw = raw @property - def devices_dict(self): - """Return state of start action task.""" - json_list = {} - index = 0 - for x_entry in self._raw[ROOT_START_ACTION]: - if index != self.index: - json_list.update(x_entry) - index = index + 1 - return json_list + def devices_list(self) -> list[dict[str, int]]: + """Store task data for all tasks but the one we want to update.""" + output_list: list[dict[str, int]] = [] + current_data_list: list[StartActionResponse] = self._raw.root_start_action + for idx, record in enumerate(current_data_list): + if idx != self.index: + list_record: dict[str, int] = {} + list_record[ATTR_ID] = record.id + list_record[ATTR_LIGHT_DIMMER] = record.dimmer + + if record.transition_time is not None: + list_record[ATTR_TRANSITION_TIME] = record.transition_time + + output_list.append(list_record) + + return output_list @property - def id(self): + def id(self) -> int: """Return ID (device id) of task.""" - return self.raw.get(ATTR_ID) + return self.raw.id @property - def item_controller(self): + def item_controller(self) -> StartActionItemController: """Control a task.""" return StartActionItemController( - self, self.raw, self.state, self.path, self.devices_dict + self, self.raw, self.state, self.path, self.devices_list ) @property - def transition_time(self): + def transition_time(self) -> int | None: """Transition runs for this long from the time in task_start. Value is in seconds x 10. Default to 0 if transition is missing. """ - return self.raw.get(ATTR_TRANSITION_TIME, 0) / 60 / 10 + if self.raw.transition_time is not None: + return round(self.raw.transition_time / 60 / 10) + + return None @property - def dimmer(self): + def dimmer(self) -> int: """Return dimmer level.""" - return self.raw.get(ATTR_LIGHT_DIMMER) + return self.raw.dimmer @property - def raw(self): + def raw(self) -> StartActionResponse: """Return raw data that it represents.""" - return self._raw[ROOT_START_ACTION][self.index] + return self._raw.root_start_action[self.index] - def __repr__(self): + def __repr__(self) -> str: """Return a readable name for this class.""" return f"" @@ -273,52 +358,66 @@ def __repr__(self): class StartActionItemController: """Class to edit settings for a task.""" - def __init__(self, item, raw, state, path, devices_dict): - """Initialize TaskControl.""" + def __init__( + self, + item: StartActionItem, + raw: StartActionResponse, + state: bool, + path: list[str], + devices_list: list[dict[str, int]], + ): + """Initialize StartActionItemController.""" self._item = item self.raw = raw self.state = state self.path = path - self.devices_dict = devices_dict + self.devices_list = devices_list - def set_dimmer(self, dimmer): + def set_dimmer(self, dimmer: int) -> Command[None]: """Set final dimmer value for task.""" - command = { + root_start_action_list: list[dict[str, int]] = [ + { + ATTR_ID: self.raw.id, + ATTR_LIGHT_DIMMER: dimmer, + } + ] + + if self.raw.transition_time is not None: + root_start_action_list[0][ATTR_TRANSITION_TIME] = self.raw.transition_time + + root_start_action_list.extend(self.devices_list) + + command: dict[str, dict[str, Any]] = { ATTR_START_ACTION: { - ATTR_DEVICE_STATE: self.state, - ROOT_START_ACTION: [ - { - ATTR_ID: self.raw[ATTR_ID], - ATTR_LIGHT_DIMMER: dimmer, - ATTR_TRANSITION_TIME: self.raw.get(ATTR_TRANSITION_TIME, 0), - }, - self.devices_dict, - ], + ATTR_DEVICE_STATE: int(self.state), + ROOT_START_ACTION: root_start_action_list, } } return self.set_values(command) - def set_transition_time(self, transition_time): + def set_transition_time(self, transition_time: int) -> Command[None]: """Set time (mins) for light transition.""" - command = { + root_start_action_list: list[dict[str, int]] = [ + { + ATTR_ID: self.raw.id, + ATTR_LIGHT_DIMMER: self.raw.dimmer, + ATTR_TRANSITION_TIME: transition_time * 10 * 60, + } + ] + root_start_action_list.extend(self.devices_list) + + command: dict[str, dict[str, Any]] = { ATTR_START_ACTION: { - ATTR_DEVICE_STATE: self.state, - ROOT_START_ACTION: [ - { - ATTR_ID: self.raw[ATTR_ID], - ATTR_LIGHT_DIMMER: self.raw[ATTR_LIGHT_DIMMER], - ATTR_TRANSITION_TIME: transition_time * 10 * 60, - }, - self.devices_dict, - ], + ATTR_DEVICE_STATE: int(self.state), + ROOT_START_ACTION: root_start_action_list, } } return self.set_values(command) - def set_values(self, command): + def set_values(self, values: dict[str, dict[str, Any]]) -> Command[None]: """ Set values on task control. Returns a Command. """ - return Command("put", self._item.path, command) + return Command("put", self._item.path, values) diff --git a/tests/test_smart_task.py b/tests/test_smart_task.py index ec6df81c..775f7c69 100644 --- a/tests/test_smart_task.py +++ b/tests/test_smart_task.py @@ -14,6 +14,7 @@ "15013": [ {"5712": 18000, "5851": 254, "9003": 65537}, {"5712": 18000, "5851": 254, "9003": 65538}, + {"5712": 19000, "5851": 230, "9003": 65539}, ], "5850": 1, }, @@ -112,6 +113,7 @@ def test_smart_task_set_start_action_dimmer(): "15013": [ {"5712": 18000, "5851": 30, "9003": 65537}, {"5712": 18000, "5851": 254, "9003": 65538}, + {"5712": 19000, "5851": 230, "9003": 65539}, ], "5850": 1, }