-
Notifications
You must be signed in to change notification settings - Fork 63
/
Copy pathtime.py
168 lines (130 loc) · 7.32 KB
/
time.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
from __future__ import annotations
import logging
import datetime
from homeassistant.components.time import TimeEntity, time, timedelta
from home_connect_async import Appliance, HomeConnect, HomeConnectError, Events, ConditionalLogger as CL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.entity_registry import async_get
from .common import InteractiveEntityBase, EntityManager, is_boolean_enum, Configuration
from .const import CONF_DELAYED_OPS, CONF_DELAYED_OPS_ABSOLUTE_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass:HomeAssistant , config_entry:ConfigType, async_add_entities:AddEntitiesCallback) -> None:
"""Add Selects for passed config_entry in HA."""
entry_conf:Configuration = hass.data[DOMAIN][config_entry.entry_id]
homeconnect:HomeConnect = entry_conf["homeconnect"]
entity_manager = EntityManager(async_add_entities, "Time")
def add_appliance(appliance:Appliance) -> None:
conf = entry_conf.get_config()
if appliance.available_programs:
for program in appliance.available_programs.values():
if program.options:
for option in program.options.values():
if conf.get_entity_setting(option.key, "type") == "DelayedOperation" \
and entry_conf[CONF_DELAYED_OPS]==CONF_DELAYED_OPS_ABSOLUTE_TIME \
and DelayedOperationTime.has_program_run_time(appliance):
device = DelayedOperationTime(appliance, option.key, conf, option)
# remove the SELECT delayed operation entity if it exists
reg = async_get(hass)
select_entity = reg.async_get_entity_id("select", DOMAIN, device.unique_id)
if select_entity:
reg.async_remove(select_entity)
entity_manager.add(device)
entity_manager.register()
def remove_appliance(appliance:Appliance) -> None:
entity_manager.remove_appliance(appliance)
homeconnect.register_callback(add_appliance, [Events.PAIRED, Events.DATA_CHANGED, Events.PROGRAM_STARTED, Events.PROGRAM_SELECTED])
homeconnect.register_callback(remove_appliance, Events.DEPAIRED)
for appliance in homeconnect.appliances.values():
add_appliance(appliance)
class DelayedOperationTime(InteractiveEntityBase, TimeEntity):
""" Class for setting delayed start by the program end time """
should_poll = True
def __init__(self, appliance: Appliance, key: str = None, conf: dict = None, hc_obj = None) -> None:
super().__init__(appliance, key, conf, hc_obj)
self._current:time = None
@property
def name_ext(self) -> str|None:
return self._hc_obj.name if self._hc_obj.name else "Delayed operation"
@property
def icon(self) -> str:
return self.get_entity_setting('icon', 'mdi:clock-outline')
@property
def available(self) -> bool:
# We must have the program run time for this entity to work
available = super().program_option_available and self.get_program_run_time(self._appliance) is not None
if not available:
self._appliance.clear_startonly_option(self._key)
return available
async def async_set_value(self, value: time) -> None:
"""Update the current value."""
self._current = self.adjust_time(value, True)
#self.async_write_ha_state()
@property
def native_value(self) -> time:
"""Return the entity value to represent the entity state."""
if self._current is None:
self._current = self.init_time()
if self._appliance.startonly_options and self._key in self._appliance.startonly_options:
self._current = self.adjust_time(self._current, True)
else:
self._current = self.adjust_time(self._current, False)
return self._current
def adjust_time(self, t:time, set_option:bool) -> time|None:
""" Adjust the time state when required """
now = datetime.datetime.now()
endtime = datetime.datetime(year=now.year, month=now.month, day=now.day, hour=t.hour, minute=t.minute)
if (now.hour > endtime.hour) or (now.hour == endtime.hour and now.minute > endtime.minute):
# if the specified time is smaller than now then it means tomorrow
endtime += datetime.timedelta(days=1)
program_run_time = self.get_program_run_time(self._appliance)
if not program_run_time:
return None
if endtime < now + timedelta(seconds=program_run_time):
# the set end time is closer then the program run time so change it to the expected end of the program
# and cancel the set delay option
endtime = now + timedelta(seconds=program_run_time)
#self._current = time(hour=endtime.hour, minute=endtime.minute)
if self._appliance.startonly_options and self._key in self._appliance.startonly_options:
_LOGGER.debug("Clearing startonly option %s", self._key)
self._appliance.clear_startonly_option(self._key)
elif set_option:
delay = (endtime-now).total_seconds()
if "StartInRelative" in self._key:
delay -= program_run_time
# round the delay to the stepsize
stepsize_option = self._appliance.get_applied_program_available_option(self._key)
stepsize = stepsize_option.stepsize if stepsize_option and stepsize_option.stepsize and stepsize_option.stepsize != 0 else 60
delay = int(delay/stepsize)*stepsize
_LOGGER.debug("Setting startonly option %s to: %i", self._key, delay)
self._appliance.set_startonly_option(self._key, delay)
return time(hour=endtime.hour, minute=endtime.minute)
def init_time(self) -> time:
""" Initialize the time state """
inittime = datetime.datetime.now() + timedelta(minutes=1)
t = time(hour=inittime.hour, minute=inittime.minute)
return self.adjust_time(t, False)
@classmethod
def get_program_run_time(cls, appliance:Appliance) -> int|None:
""" Try to get the expected run time of the selected program or the remaining time of the running program """
time_option_keys = [
"BSH.Common.Option.RemainingProgramTime",
"BSH.Common.Option.FinishInRelative",
"BSH.Common.Option.EstimatedTotalProgramTime",
]
for key in time_option_keys:
o = appliance.get_applied_program_option(key)
if o:
return o.value
return None
@classmethod
def has_program_run_time(cls, appliance:Appliance) ->bool:
""" Check if it's possible to get a program run time estimate """
return cls.get_program_run_time(appliance) is not None
async def async_on_update(self, appliance:Appliance, key:str, value) -> None:
# reset the end time clock when a different program is selected
if key == Events.PROGRAM_SELECTED or "RemoteControlStartAllowed" in key:
self._current = self.init_time()
self.async_write_ha_state()