From bf37cc8371737048425712307e72a105fbb36389 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 22 Jul 2019 09:45:31 +0100 Subject: [PATCH] Tweak evohome migration (#25281) * Initial commit add hvac_action to zones, remove target_temp from controller fix incorrect hvac_action de-lint Initial commit de-lint & minor refactor tweak docstrings, and type hints tweak docstrings * refactor setpoints property * tweak docstring * tweak docstring * avoid a unnecessary I/O * avoid unnecessary I/O * refactor schedule/setpoints * remove type hint * remove type hint 2 * tweak code * delint type hints * fix regression --- homeassistant/components/evohome/__init__.py | 33 ++++++----- homeassistant/components/evohome/climate.py | 57 ++++++++++--------- .../components/evohome/water_heater.py | 8 +-- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 49ddbdde156..b81d32a20f4 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( async_track_point_in_utc_time, track_time_interval) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime, utcnow from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS @@ -101,7 +102,7 @@ def _handle_exception(err) -> bool: raise # we don't expect/handle any other HTTPErrors -def setup(hass, hass_config) -> bool: +def setup(hass: HomeAssistantType, hass_config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" broker = EvoBroker(hass, hass_config[DOMAIN]) if not broker.init_client(): @@ -270,29 +271,29 @@ class EvoDevice(Entity): self._state_attributes = [] self._supported_features = None - self._setpoints = None + self._schedule = {} @callback def _refresh(self, packet): if packet['signal'] == 'refresh': self.async_schedule_update_ha_state(force_refresh=True) - def get_setpoints(self) -> Optional[Dict[str, Any]]: - """Return the current/next scheduled switchpoints. + @property + def setpoints(self) -> Dict[str, Any]: + """Return the current/next setpoints from the schedule. - Only Zones & DHW controllers (but not the TCS) have schedules. + Only Zones & DHW controllers (but not the TCS) can have schedules. """ - switchpoints = {} - schedule = self._evo_device.schedule() + if not self._schedule['DailySchedules']: + return {} - if not schedule['DailySchedules']: - return None + switchpoints = {} day_time = datetime.now() day_of_week = int(day_time.strftime('%w')) # 0 is Sunday # Iterate today's switchpoints until past the current time of day... - day = schedule['DailySchedules'][day_of_week] + day = self._schedule['DailySchedules'][day_of_week] sp_idx = -1 # last switchpoint of the day before for i, tmp in enumerate(day['Switchpoints']): if day_time.strftime('%H:%M:%S') > tmp['TimeOfDay']: @@ -311,7 +312,7 @@ class EvoDevice(Entity): spt = switchpoints[key] = {} sp_date = (day_time + timedelta(days=offset)).strftime('%Y-%m-%d') - day = schedule['DailySchedules'][(day_of_week + offset) % 7] + day = self._schedule['DailySchedules'][(day_of_week + offset) % 7] switchpoint = day['Switchpoints'][idx] dt_naive = datetime.strptime( @@ -345,7 +346,7 @@ class EvoDevice(Entity): status[attr] = getattr(self._evo_device, attr) if 'setpoints' in self._state_attributes: - status['setpoints'] = self._setpoints + status['setpoints'] = self.setpoints return {'status': status} @@ -373,6 +374,12 @@ class EvoDevice(Entity): """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS + def _update_schedule(self) -> None: + """Get the latest state data.""" + if not self._schedule.get('DailySchedules') or \ + parse_datetime(self.setpoints['next']['from']) < utcnow(): + self._schedule = self._evo_device.schedule() + def update(self) -> None: """Get the latest state data.""" - self._setpoints = self.get_setpoints() + self._update_schedule() diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index bdd846a35b2..2070938f439 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -8,16 +8,17 @@ import evohomeclient2 from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, PRESET_HOME, PRESET_NONE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) from homeassistant.const import PRECISION_TENTHS +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice from .const import ( - DOMAIN, EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_DAYOFF, EVO_CUSTOM, + DOMAIN, EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_CUSTOM, EVO_DAYOFF, EVO_HEATOFF, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER) _LOGGER = logging.getLogger(__name__) @@ -30,14 +31,15 @@ HA_HVAC_TO_TCS = { HVAC_MODE_HEAT: EVO_AUTO, } -HA_PRESET_TO_TCS = { - PRESET_AWAY: EVO_AWAY, - PRESET_CUSTOM: EVO_CUSTOM, - PRESET_ECO: EVO_AUTOECO, - PRESET_HOME: EVO_DAYOFF, - PRESET_RESET: EVO_RESET, -} -TCS_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_TCS.items()} +TCS_PRESET_TO_HA = { + EVO_AWAY: PRESET_AWAY, + EVO_CUSTOM: PRESET_CUSTOM, + EVO_AUTOECO: PRESET_ECO, + EVO_DAYOFF: PRESET_HOME, + EVO_RESET: PRESET_RESET, +} # EVO_AUTO: None, + +HA_PRESET_TO_TCS = {v: k for k, v in TCS_PRESET_TO_HA.items()} EVO_PRESET_TO_HA = { EVO_FOLLOW: PRESET_NONE, @@ -47,8 +49,8 @@ EVO_PRESET_TO_HA = { HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} -def setup_platform(hass, hass_config, add_entities, - discovery_info=None) -> None: +def setup_platform(hass: HomeAssistantType, hass_config: ConfigType, + add_entities, discovery_info=None) -> None: """Create the evohome Controller, and its Zones, if any.""" broker = hass.data[DOMAIN]['broker'] loc_idx = broker.params[CONF_LOCATION_IDX] @@ -102,21 +104,22 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): _handle_exception(err) def _set_zone_mode(self, op_mode: str) -> None: - """Set the Zone to one of its native EVO_* operating modes. + """Set a Zone to one of its native EVO_* operating modes. - NB: evohome Zones 'inherit' their operating mode from the Controller. + Zones inherit their _effective_ operating mode from the Controller. Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their schedule, and the Controller's operating_mode, e.g. - Economy mode is their scheduled setpoint less (usually) 3C. + a function of their own schedule and the Controller's operating mode, + e.g. 'AutoWithEco' mode means their setpoint is (by default) 3C less + than scheduled. - However, Zones can override these setpoints, either for a specified - period of time, 'TemporaryOverride', after which they will revert back - to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. + However, Zones can _override_ these setpoints, either indefinitely, + 'PermanentOverride' mode, or for a period of time, 'TemporaryOverride', + after which they will revert back to 'FollowSchedule'. - Some of the Controller's operating_mode are 'forced' upon the Zone, - regardless of its override state, e.g. 'HeatingOff' (Zones to min_temp) - and 'Away' (Zones to 12C). + Finally, some of the Controller's operating modes are _forced_ upon the + Zones, regardless of any override mode, e.g. 'HeatingOff', Zones to + (by default) 5C, and 'Away', Zones to (by default) 12C. """ if op_mode == EVO_FOLLOW: try: @@ -129,10 +132,10 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): temperature = self._evo_device.setpointStatus['targetHeatTemperature'] until = None # EVO_PERMOVER - if op_mode == EVO_TEMPOVER: - self._setpoints = self.get_setpoints() - if self._setpoints: - until = parse_datetime(self._setpoints['next']['from']) + if op_mode == EVO_TEMPOVER and self._schedule['DailySchedules']: + self._update_schedule() + if self._schedule['DailySchedules']: + until = parse_datetime(self.setpoints['next']['from']) self._set_temperature(temperature, until=until) @@ -147,7 +150,7 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): @property def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" - return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + return list(HA_HVAC_TO_TCS) @property def preset_modes(self) -> Optional[List[str]]: diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 4706269e1cf..6f5952bdfb0 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -75,10 +75,10 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): state = '' if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] until = None # EVO_FOLLOW, EVO_PERMOVER - if op_mode == EVO_TEMPOVER: - self._setpoints = self.get_setpoints() - if self._setpoints: - until = parse_datetime(self._setpoints['next']['from']) + if op_mode == EVO_TEMPOVER and self._schedule['DailySchedules']: + self._update_schedule() + if self._schedule['DailySchedules']: + until = parse_datetime(self.setpoints['next']['from']) until = until.strftime(EVO_STRFTIME) data = {'Mode': op_mode, 'State': state, 'UntilTime': until}