diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 1a408c0a660..f56c92d6572 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -34,7 +34,7 @@ from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VERSION, TCS +from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VER, TCS, UTC_OFFSET _LOGGER = logging.getLogger(__name__) @@ -93,22 +93,22 @@ SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( # system mode schemas are built dynamically, below -def _local_dt_to_aware(dt_naive: dt) -> dt: +def _dt_local_to_aware(dt_naive: dt) -> dt: dt_aware = dt_util.now() + (dt_naive - dt.now()) if dt_aware.microsecond >= 500000: dt_aware += timedelta(seconds=1) return dt_aware.replace(microsecond=0) -def _dt_to_local_naive(dt_aware: dt) -> dt: +def _dt_aware_to_naive(dt_aware: dt) -> dt: dt_naive = dt.now() + (dt_aware - dt_util.now()) if dt_naive.microsecond >= 500000: dt_naive += timedelta(seconds=1) return dt_naive.replace(microsecond=0) -def convert_until(status_dict, until_key) -> str: - """Convert datetime string from "%Y-%m-%dT%H:%M:%SZ" to local/aware/isoformat.""" +def convert_until(status_dict: dict, until_key: str) -> str: + """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" if until_key in status_dict: # only present for certain modes dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() @@ -190,14 +190,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: # evohomeasync2 requires naive/local datetimes as strings if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: - tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_naive( + tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive( dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) ) user_data = tokens.pop(USER_DATA, None) return (tokens, user_data) - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = hass.helpers.storage.Store(STORAGE_VER, STORAGE_KEY) tokens, user_data = await load_auth_tokens(store) client_v2 = evohomeasync2.EvohomeClient( @@ -217,7 +217,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: loc_idx = config[DOMAIN][CONF_LOCATION_IDX] try: - loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0] + loc_config = client_v2.installation_info[loc_idx] except IndexError: _LOGGER.error( "Config error: '%s' = %s, but the valid range is 0-%s. " @@ -228,7 +228,11 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) return False - _LOGGER.debug("Config = %s", loc_config) + if _LOGGER.isEnabledFor(logging.DEBUG): + _config = {"locationInfo": {"timeZone": None}, GWS: [{TCS: None}]} + _config["locationInfo"]["timeZone"] = loc_config["locationInfo"]["timeZone"] + _config[GWS][0][TCS] = loc_config[GWS][0][TCS] + _LOGGER.debug("Config = %s", _config) client_v1 = evohomeasync.EvohomeClient( client_v2.username, @@ -393,12 +397,15 @@ class EvoBroker: loc_idx = params[CONF_LOCATION_IDX] self.config = client.installation_info[loc_idx][GWS][0][TCS][0] self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] + self.tcs_utc_offset = timedelta( + minutes=client.locations[loc_idx].timeZone[UTC_OFFSET] + ) self.temps = {} async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" # evohomeasync2 uses naive/local datetimes - access_token_expires = _local_dt_to_aware(self.client.access_token_expires) + access_token_expires = _dt_local_to_aware(self.client.access_token_expires) app_storage = {CONF_USERNAME: self.client.username} app_storage[REFRESH_TOKEN] = self.client.refresh_token @@ -481,7 +488,7 @@ class EvoBroker: else: async_dispatcher_send(self.hass, DOMAIN) - _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + _LOGGER.debug("Status = %s", status) if access_token != self.client.access_token: await self.save_auth_tokens() @@ -621,6 +628,11 @@ class EvoChild(EvoDevice): Only Zones & DHW controllers (but not the TCS) can have schedules. """ + + def _dt_evo_to_aware(dt_naive: dt, utc_offset: timedelta) -> dt: + dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset + return dt_util.as_local(dt_aware) + if not self._schedule["DailySchedules"]: return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints @@ -650,11 +662,12 @@ class EvoChild(EvoDevice): day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] switchpoint = day["Switchpoints"][idx] - dt_local_aware = _local_dt_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + dt_aware = _dt_evo_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}"), + self._evo_broker.tcs_utc_offset, ) - self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() + self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() try: self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] except KeyError: diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index aece0f0ec0d..8b65d837171 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -20,7 +20,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util.dt import parse_datetime +import homeassistant.util.dt as dt_util from . import ( ATTR_DURATION_DAYS, @@ -170,21 +170,21 @@ class EvoZone(EvoChild, EvoClimateDevice): return # otherwise it is SVC_SET_ZONE_OVERRIDE - temp = round(data[ATTR_ZONE_TEMP] * self.precision) / self.precision - temp = max(min(temp, self.max_temp), self.min_temp) + temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp) if ATTR_DURATION_UNTIL in data: duration = data[ATTR_DURATION_UNTIL] if duration.total_seconds() == 0: await self._update_schedule() - until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) else: - until = dt.now() + data[ATTR_DURATION_UNTIL] + until = dt_util.now() + data[ATTR_DURATION_UNTIL] else: until = None # indefinitely + until = dt_util.as_utc(until) if until else None await self._evo_broker.call_client_api( - self._evo_device.set_temperature(temperature=temp, until=until) + self._evo_device.set_temperature(temperature, until=until) ) @property @@ -244,12 +244,13 @@ class EvoZone(EvoChild, EvoClimateDevice): if until is None: if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: await self._update_schedule() - until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: - until = parse_datetime(self._evo_device.setpointStatus["until"]) + until = dt_util.parse_datetime(self._evo_device.setpointStatus["until"]) + until = dt_util.as_utc(until) if until else None await self._evo_broker.call_client_api( - self._evo_device.set_temperature(temperature, until) + self._evo_device.set_temperature(temperature, until=until) ) async def async_set_hvac_mode(self, hvac_mode: str) -> None: @@ -292,12 +293,13 @@ class EvoZone(EvoChild, EvoClimateDevice): if evo_preset_mode == EVO_TEMPOVER: await self._update_schedule() - until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) else: # EVO_PERMOVER until = None + until = dt_util.as_utc(until) if until else None await self._evo_broker.call_client_api( - self._evo_device.set_temperature(temperature, until) + self._evo_device.set_temperature(temperature, until=until) ) async def async_update(self) -> None: @@ -345,11 +347,11 @@ class EvoController(EvoClimateDevice): mode = EVO_RESET if ATTR_DURATION_DAYS in data: - until = dt.combine(dt.now().date(), dt.min.time()) + until = dt_util.start_of_local_day() until += data[ATTR_DURATION_DAYS] elif ATTR_DURATION_HOURS in data: - until = dt.now() + data[ATTR_DURATION_HOURS] + until = dt_util.now() + data[ATTR_DURATION_HOURS] else: until = None @@ -358,7 +360,10 @@ class EvoController(EvoClimateDevice): async def _set_tcs_mode(self, mode: str, until: Optional[dt] = None) -> None: """Set a Controller to any of its native EVO_* operating modes.""" - await self._evo_broker.call_client_api(self._evo_tcs.set_status(mode)) + until = dt_util.as_utc(until) if until else None + await self._evo_broker.call_client_api( + self._evo_tcs.set_status(mode, until=until) + ) @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index eaa7048e53b..6bd3a59c225 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -1,7 +1,7 @@ """Support for (EMEA/EU-based) Honeywell TCC climate systems.""" DOMAIN = "evohome" -STORAGE_VERSION = 1 +STORAGE_VER = 1 STORAGE_KEY = DOMAIN # The Parent's (i.e. TCS, Controller's) operating mode is one of: @@ -21,3 +21,5 @@ EVO_PERMOVER = "PermanentOverride" # These are used only to help prevent E501 (line too long) violations GWS = "gateways" TCS = "temperatureControlSystems" + +UTC_OFFSET = "currentOffsetMinutes" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index cc282534f1b..20aa0710d0d 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -9,7 +9,7 @@ from homeassistant.components.water_heater import ( ) from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util.dt import parse_datetime +import homeassistant.util.dt as dt_util from . import EvoChild from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER @@ -90,15 +90,16 @@ class EvoDHW(EvoChild, WaterHeaterDevice): await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) else: await self._update_schedule() - until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", "")) + until = dt_util.as_utc(until) if until else None if operation_mode == STATE_ON: await self._evo_broker.call_client_api( - self._evo_device.set_dhw_on(until) + self._evo_device.set_dhw_on(until=until) ) else: # STATE_OFF await self._evo_broker.call_client_api( - self._evo_device.set_dhw_off(until) + self._evo_device.set_dhw_off(until=until) ) async def async_turn_away_mode_on(self):