Bugfix evohome converting non-UTC timezones ()

* bugfix: correctly handle non-UTC TZs
* bugfix: system mode is always permanent
* bugfix: handle where until is none
* tweak: improve logging to support above fixes
This commit is contained in:
David Bonnes 2020-03-05 20:42:52 +00:00 committed by GitHub
parent 4717d072c9
commit ae0ea0f088
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 54 additions and 33 deletions
homeassistant/components/evohome

View file

@ -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:

View file

@ -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:

View file

@ -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"

View file

@ -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):