mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 00:37:13 +00:00
Bugfix evohome (#26810)
* address issues #25984, #25985 * small tweak * refactor - fix bugs, coding erros, consolidate * some zones don't have schedules * some zones don't have schedules 2 * some zones don't have schedules 3 * fix water_heater, add away mode * readbility tweak * bugfix: no refesh after state change * bugfix: no refesh after state change 2 * temove dodgy wrappers (protected-access), fix until logic * remove dodgy _set_zone_mode wrapper * tweak * tweak docstrings * refactor as per PR review * refactor as per PR review 3 * refactor to use dt_util * small tweak * tweak doc strings * remove packet from _refresh * set_temp() don't have until * add unique_id * add unique_id 2
This commit is contained in:
parent
e2d7a01d65
commit
a1997ee891
@ -4,6 +4,7 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone).
|
|||||||
"""
|
"""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
import aiohttp.client_exceptions
|
import aiohttp.client_exceptions
|
||||||
@ -25,9 +26,9 @@ import homeassistant.helpers.config_validation as cv
|
|||||||
from homeassistant.helpers.discovery import async_load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
from homeassistant.util.dt import parse_datetime, utcnow
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS
|
from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -55,20 +56,45 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _local_dt_to_utc(dt_naive: datetime) -> datetime:
|
def _local_dt_to_aware(dt_naive: datetime) -> datetime:
|
||||||
dt_aware = utcnow() + (dt_naive - datetime.now())
|
dt_aware = dt_util.now() + (dt_naive - datetime.now())
|
||||||
if dt_aware.microsecond >= 500000:
|
if dt_aware.microsecond >= 500000:
|
||||||
dt_aware += timedelta(seconds=1)
|
dt_aware += timedelta(seconds=1)
|
||||||
return dt_aware.replace(microsecond=0)
|
return dt_aware.replace(microsecond=0)
|
||||||
|
|
||||||
|
|
||||||
def _utc_to_local_dt(dt_aware: datetime) -> datetime:
|
def _dt_to_local_naive(dt_aware: datetime) -> datetime:
|
||||||
dt_naive = datetime.now() + (dt_aware - utcnow())
|
dt_naive = datetime.now() + (dt_aware - dt_util.now())
|
||||||
if dt_naive.microsecond >= 500000:
|
if dt_naive.microsecond >= 500000:
|
||||||
dt_naive += timedelta(seconds=1)
|
dt_naive += timedelta(seconds=1)
|
||||||
return dt_naive.replace(microsecond=0)
|
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."""
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Recursively convert a dict's keys to snake_case."""
|
||||||
|
|
||||||
|
def convert_key(key: str) -> str:
|
||||||
|
"""Convert a string to snake_case."""
|
||||||
|
string = re.sub(r"[\-\.\s]", "_", str(key))
|
||||||
|
return (string[0]).lower() + re.sub(
|
||||||
|
r"[A-Z]", lambda matched: "_" + matched.group(0).lower(), string[1:]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
(convert_key(k) if isinstance(k, str) else k): (
|
||||||
|
convert_dict(v) if isinstance(v, dict) else v
|
||||||
|
)
|
||||||
|
for k, v in dictionary.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _handle_exception(err) -> bool:
|
def _handle_exception(err) -> bool:
|
||||||
try:
|
try:
|
||||||
raise err
|
raise err
|
||||||
@ -135,7 +161,7 @@ class EvoBroker:
|
|||||||
"""Container for evohome client and data."""
|
"""Container for evohome client and data."""
|
||||||
|
|
||||||
def __init__(self, hass, params) -> None:
|
def __init__(self, hass, params) -> None:
|
||||||
"""Initialize the evohome client and data structure."""
|
"""Initialize the evohome client and its data structure."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.params = params
|
self.params = params
|
||||||
self.config = {}
|
self.config = {}
|
||||||
@ -157,7 +183,7 @@ class EvoBroker:
|
|||||||
|
|
||||||
# evohomeasync2 uses naive/local datetimes
|
# evohomeasync2 uses naive/local datetimes
|
||||||
if access_token_expires is not None:
|
if access_token_expires is not None:
|
||||||
access_token_expires = _utc_to_local_dt(access_token_expires)
|
access_token_expires = _dt_to_local_naive(access_token_expires)
|
||||||
|
|
||||||
client = self.client = evohomeasync2.EvohomeClient(
|
client = self.client = evohomeasync2.EvohomeClient(
|
||||||
self.params[CONF_USERNAME],
|
self.params[CONF_USERNAME],
|
||||||
@ -220,7 +246,7 @@ class EvoBroker:
|
|||||||
access_token = app_storage.get(CONF_ACCESS_TOKEN)
|
access_token = app_storage.get(CONF_ACCESS_TOKEN)
|
||||||
at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
|
at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
|
||||||
if at_expires_str:
|
if at_expires_str:
|
||||||
at_expires_dt = parse_datetime(at_expires_str)
|
at_expires_dt = dt_util.parse_datetime(at_expires_str)
|
||||||
else:
|
else:
|
||||||
at_expires_dt = None
|
at_expires_dt = None
|
||||||
|
|
||||||
@ -230,7 +256,7 @@ class EvoBroker:
|
|||||||
|
|
||||||
async def _save_auth_tokens(self, *args) -> None:
|
async def _save_auth_tokens(self, *args) -> None:
|
||||||
# evohomeasync2 uses naive/local datetimes
|
# evohomeasync2 uses naive/local datetimes
|
||||||
access_token_expires = _local_dt_to_utc(self.client.access_token_expires)
|
access_token_expires = _local_dt_to_aware(self.client.access_token_expires)
|
||||||
|
|
||||||
self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
|
self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
|
||||||
self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
|
self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
|
||||||
@ -246,11 +272,11 @@ class EvoBroker:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def update(self, *args, **kwargs) -> None:
|
async def update(self, *args, **kwargs) -> None:
|
||||||
"""Get the latest state data of the entire evohome Location.
|
"""Get the latest state data of an entire evohome Location.
|
||||||
|
|
||||||
This includes state data for the Controller and all its child devices,
|
This includes state data for a Controller and all its child devices, such as the
|
||||||
such as the operating mode of the Controller and the current temp of
|
operating mode of the Controller and the current temp of its children (e.g.
|
||||||
its children (e.g. Zones, DHW controller).
|
Zones, DHW controller).
|
||||||
"""
|
"""
|
||||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||||
|
|
||||||
@ -260,9 +286,7 @@ class EvoBroker:
|
|||||||
_handle_exception(err)
|
_handle_exception(err)
|
||||||
else:
|
else:
|
||||||
# inform the evohome devices that state data has been updated
|
# inform the evohome devices that state data has been updated
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_send(
|
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
||||||
DOMAIN, {"signal": "refresh"}
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
||||||
|
|
||||||
@ -270,8 +294,8 @@ class EvoBroker:
|
|||||||
class EvoDevice(Entity):
|
class EvoDevice(Entity):
|
||||||
"""Base for any evohome device.
|
"""Base for any evohome device.
|
||||||
|
|
||||||
This includes the Controller, (up to 12) Heating Zones and
|
This includes the Controller, (up to 12) Heating Zones and (optionally) a
|
||||||
(optionally) a DHW controller.
|
DHW controller.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
@ -280,72 +304,26 @@ class EvoDevice(Entity):
|
|||||||
self._evo_broker = evo_broker
|
self._evo_broker = evo_broker
|
||||||
self._evo_tcs = evo_broker.tcs
|
self._evo_tcs = evo_broker.tcs
|
||||||
|
|
||||||
self._name = self._icon = self._precision = None
|
self._unique_id = self._name = self._icon = self._precision = None
|
||||||
self._state_attributes = []
|
|
||||||
|
|
||||||
|
self._device_state_attrs = {}
|
||||||
|
self._state_attributes = []
|
||||||
self._supported_features = None
|
self._supported_features = None
|
||||||
self._schedule = {}
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _refresh(self, packet):
|
def _refresh(self) -> None:
|
||||||
if packet["signal"] == "refresh":
|
|
||||||
self.async_schedule_update_ha_state(force_refresh=True)
|
self.async_schedule_update_ha_state(force_refresh=True)
|
||||||
|
|
||||||
@property
|
|
||||||
def setpoints(self) -> Dict[str, Any]:
|
|
||||||
"""Return the current/next setpoints from the schedule.
|
|
||||||
|
|
||||||
Only Zones & DHW controllers (but not the TCS) can have schedules.
|
|
||||||
"""
|
|
||||||
if not self._schedule["DailySchedules"]:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
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 = 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"]:
|
|
||||||
sp_idx = i # current setpoint
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Did the current SP start yesterday? Does the next start SP tomorrow?
|
|
||||||
current_sp_day = -1 if sp_idx == -1 else 0
|
|
||||||
next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0
|
|
||||||
|
|
||||||
for key, offset, idx in [
|
|
||||||
("current", current_sp_day, sp_idx),
|
|
||||||
("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)),
|
|
||||||
]:
|
|
||||||
|
|
||||||
spt = switchpoints[key] = {}
|
|
||||||
|
|
||||||
sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d")
|
|
||||||
day = self._schedule["DailySchedules"][(day_of_week + offset) % 7]
|
|
||||||
switchpoint = day["Switchpoints"][idx]
|
|
||||||
|
|
||||||
dt_naive = datetime.strptime(
|
|
||||||
f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S"
|
|
||||||
)
|
|
||||||
|
|
||||||
spt["from"] = _local_dt_to_utc(dt_naive).isoformat()
|
|
||||||
try:
|
|
||||||
spt["temperature"] = switchpoint["heatSetpoint"]
|
|
||||||
except KeyError:
|
|
||||||
spt["state"] = switchpoint["DhwState"]
|
|
||||||
|
|
||||||
return switchpoints
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
"""Evohome entities should not be polled."""
|
"""Evohome entities should not be polled."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> Optional[str]:
|
||||||
|
"""Return a unique ID."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the Evohome entity."""
|
"""Return the name of the Evohome entity."""
|
||||||
@ -354,15 +332,15 @@ class EvoDevice(Entity):
|
|||||||
@property
|
@property
|
||||||
def device_state_attributes(self) -> Dict[str, Any]:
|
def device_state_attributes(self) -> Dict[str, Any]:
|
||||||
"""Return the Evohome-specific state attributes."""
|
"""Return the Evohome-specific state attributes."""
|
||||||
status = {}
|
status = self._device_state_attrs
|
||||||
for attr in self._state_attributes:
|
if "systemModeStatus" in status:
|
||||||
if attr != "setpoints":
|
convert_until(status["systemModeStatus"], "timeUntil")
|
||||||
status[attr] = getattr(self._evo_device, attr)
|
if "setpointStatus" in status:
|
||||||
|
convert_until(status["setpointStatus"], "until")
|
||||||
|
if "stateStatus" in status:
|
||||||
|
convert_until(status["stateStatus"], "until")
|
||||||
|
|
||||||
if "setpoints" in self._state_attributes:
|
return {"status": convert_dict(status)}
|
||||||
status["setpoints"] = self.setpoints
|
|
||||||
|
|
||||||
return {"status": status}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
@ -388,27 +366,98 @@ class EvoDevice(Entity):
|
|||||||
"""Return the temperature unit to use in the frontend UI."""
|
"""Return the temperature unit to use in the frontend UI."""
|
||||||
return TEMP_CELSIUS
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
async def _call_client_api(self, api_function) -> None:
|
async def _call_client_api(self, api_function, refresh=True) -> Any:
|
||||||
try:
|
try:
|
||||||
await api_function
|
result = await api_function
|
||||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
_handle_exception(err)
|
if not _handle_exception(err):
|
||||||
|
return
|
||||||
|
|
||||||
self.hass.helpers.event.async_call_later(
|
if refresh is True:
|
||||||
2, self._evo_broker.update()
|
self.hass.helpers.event.async_call_later(1, self._evo_broker.update())
|
||||||
) # call update() in 2 seconds
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class EvoChild(EvoDevice):
|
||||||
|
"""Base for any evohome child.
|
||||||
|
|
||||||
|
This includes (up to 12) Heating Zones and (optionally) a DHW controller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
|
"""Initialize a evohome Controller (hub)."""
|
||||||
|
super().__init__(evo_broker, evo_device)
|
||||||
|
self._schedule = {}
|
||||||
|
self._setpoints = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_temperature(self) -> Optional[float]:
|
||||||
|
"""Return the current temperature of a Zone."""
|
||||||
|
if self._evo_device.temperatureStatus["isAvailable"]:
|
||||||
|
return self._evo_device.temperatureStatus["temperature"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def setpoints(self) -> Dict[str, Any]:
|
||||||
|
"""Return the current/next setpoints from the schedule.
|
||||||
|
|
||||||
|
Only Zones & DHW controllers (but not the TCS) can have schedules.
|
||||||
|
"""
|
||||||
|
if not self._schedule["DailySchedules"]:
|
||||||
|
return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints
|
||||||
|
|
||||||
|
day_time = dt_util.now()
|
||||||
|
day_of_week = int(day_time.strftime("%w")) # 0 is Sunday
|
||||||
|
time_of_day = day_time.strftime("%H:%M:%S")
|
||||||
|
|
||||||
|
# Iterate today's switchpoints until past the current time of day...
|
||||||
|
day = self._schedule["DailySchedules"][day_of_week]
|
||||||
|
sp_idx = -1 # last switchpoint of the day before
|
||||||
|
for i, tmp in enumerate(day["Switchpoints"]):
|
||||||
|
if time_of_day > tmp["TimeOfDay"]:
|
||||||
|
sp_idx = i # current setpoint
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Did the current SP start yesterday? Does the next start SP tomorrow?
|
||||||
|
this_sp_day = -1 if sp_idx == -1 else 0
|
||||||
|
next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0
|
||||||
|
|
||||||
|
for key, offset, idx in [
|
||||||
|
("this", this_sp_day, sp_idx),
|
||||||
|
("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)),
|
||||||
|
]:
|
||||||
|
sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d")
|
||||||
|
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']}")
|
||||||
|
)
|
||||||
|
|
||||||
|
self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat()
|
||||||
|
try:
|
||||||
|
self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"]
|
||||||
|
except KeyError:
|
||||||
|
self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"]
|
||||||
|
|
||||||
|
return self._setpoints
|
||||||
|
|
||||||
async def _update_schedule(self) -> None:
|
async def _update_schedule(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest schedule."""
|
||||||
if (
|
if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]:
|
||||||
not self._schedule.get("DailySchedules")
|
if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||||
or parse_datetime(self.setpoints["next"]["from"]) < utcnow()
|
return # avoid unnecessary I/O - there's nothing to update
|
||||||
):
|
|
||||||
try:
|
self._schedule = await self._call_client_api(
|
||||||
self._schedule = await self._evo_device.schedule()
|
self._evo_device.schedule(), refresh=False
|
||||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
)
|
||||||
_handle_exception(err)
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest state data."""
|
||||||
await self._update_schedule()
|
next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00")
|
||||||
|
if dt_util.now() >= dt_util.parse_datetime(next_sp_from):
|
||||||
|
await self._update_schedule() # no schedule, or it's out-of-date
|
||||||
|
|
||||||
|
self._device_state_attrs = {"setpoints": self.setpoints}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
@ -22,7 +21,7 @@ from homeassistant.const import PRECISION_TENTHS
|
|||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
from homeassistant.util.dt import parse_datetime
|
from homeassistant.util.dt import parse_datetime
|
||||||
|
|
||||||
from . import CONF_LOCATION_IDX, EvoDevice
|
from . import CONF_LOCATION_IDX, EvoDevice, EvoChild
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVO_RESET,
|
EVO_RESET,
|
||||||
@ -61,6 +60,9 @@ EVO_PRESET_TO_HA = {
|
|||||||
}
|
}
|
||||||
HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()}
|
HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()}
|
||||||
|
|
||||||
|
STATE_ATTRS_TCS = ["systemId", "activeFaults", "systemModeStatus"]
|
||||||
|
STATE_ATTRS_ZONES = ["zoneId", "activeFaults", "setpointStatus", "temperatureStatus"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
|
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
|
||||||
@ -114,63 +116,20 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||||||
"""Base for a Honeywell evohome Climate device."""
|
"""Base for a Honeywell evohome Climate device."""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the evohome Climate device."""
|
"""Initialize a Climate device."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
self._preset_modes = None
|
self._preset_modes = None
|
||||||
|
|
||||||
async def _set_temperature(
|
|
||||||
self, temperature: float, until: Optional[datetime] = None
|
|
||||||
) -> None:
|
|
||||||
"""Set a new target temperature for the Zone.
|
|
||||||
|
|
||||||
until == None means indefinitely (i.e. PermanentOverride)
|
|
||||||
"""
|
|
||||||
await self._call_client_api(
|
|
||||||
self._evo_device.set_temperature(temperature, until)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _set_zone_mode(self, op_mode: str) -> None:
|
|
||||||
"""Set a Zone to one of its native EVO_* operating modes.
|
|
||||||
|
|
||||||
Zones inherit their _effective_ operating mode from the Controller.
|
|
||||||
|
|
||||||
Usually, Zones are in 'FollowSchedule' mode, where their setpoints are
|
|
||||||
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 indefinitely,
|
|
||||||
'PermanentOverride' mode, or for a period of time, 'TemporaryOverride',
|
|
||||||
after which they will revert back to 'FollowSchedule'.
|
|
||||||
|
|
||||||
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:
|
|
||||||
await self._call_client_api(self._evo_device.cancel_temp_override())
|
|
||||||
return
|
|
||||||
|
|
||||||
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
|
||||||
until = None # EVO_PERMOVER
|
|
||||||
|
|
||||||
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
|
||||||
await self._update_schedule()
|
|
||||||
if self._schedule["DailySchedules"]:
|
|
||||||
until = parse_datetime(self.setpoints["next"]["from"])
|
|
||||||
|
|
||||||
await self._set_temperature(temperature, until=until)
|
|
||||||
|
|
||||||
async def _set_tcs_mode(self, op_mode: str) -> None:
|
async def _set_tcs_mode(self, op_mode: str) -> None:
|
||||||
"""Set the Controller to any of its native EVO_* operating modes."""
|
"""Set a Controller to any of its native EVO_* operating modes."""
|
||||||
await self._call_client_api(
|
await self._call_client_api(
|
||||||
self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access
|
self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self) -> List[str]:
|
def hvac_modes(self) -> List[str]:
|
||||||
"""Return the list of available hvac operation modes."""
|
"""Return a list of available hvac operation modes."""
|
||||||
return list(HA_HVAC_TO_TCS)
|
return list(HA_HVAC_TO_TCS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -179,36 +138,24 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||||||
return self._preset_modes
|
return self._preset_modes
|
||||||
|
|
||||||
|
|
||||||
class EvoZone(EvoClimateDevice):
|
class EvoZone(EvoChild, EvoClimateDevice):
|
||||||
"""Base for a Honeywell evohome Zone."""
|
"""Base for a Honeywell evohome Zone."""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the evohome Zone."""
|
"""Initialize a Zone."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
|
self._unique_id = evo_device.zoneId
|
||||||
self._name = evo_device.name
|
self._name = evo_device.name
|
||||||
self._icon = "mdi:radiator"
|
self._icon = "mdi:radiator"
|
||||||
|
|
||||||
self._precision = self._evo_device.setpointCapabilities["valueResolution"]
|
self._precision = self._evo_device.setpointCapabilities["valueResolution"]
|
||||||
self._state_attributes = [
|
|
||||||
"zoneId",
|
|
||||||
"activeFaults",
|
|
||||||
"setpointStatus",
|
|
||||||
"temperatureStatus",
|
|
||||||
"setpoints",
|
|
||||||
]
|
|
||||||
|
|
||||||
self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
|
self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE
|
||||||
self._preset_modes = list(HA_PRESET_TO_EVO)
|
self._preset_modes = list(HA_PRESET_TO_EVO)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return True if entity is available."""
|
|
||||||
return self._evo_device.temperatureStatus["isAvailable"]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
"""Return the current operating mode of the evohome Zone."""
|
"""Return the current operating mode of a Zone."""
|
||||||
if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]:
|
if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]:
|
||||||
return HVAC_MODE_AUTO
|
return HVAC_MODE_AUTO
|
||||||
is_off = self.target_temperature <= self.min_temp
|
is_off = self.target_temperature <= self.min_temp
|
||||||
@ -221,24 +168,15 @@ class EvoZone(EvoClimateDevice):
|
|||||||
return CURRENT_HVAC_OFF
|
return CURRENT_HVAC_OFF
|
||||||
if self.target_temperature <= self.min_temp:
|
if self.target_temperature <= self.min_temp:
|
||||||
return CURRENT_HVAC_OFF
|
return CURRENT_HVAC_OFF
|
||||||
if self.target_temperature < self.current_temperature:
|
if not self._evo_device.temperatureStatus["isAvailable"]:
|
||||||
|
return None
|
||||||
|
if self.target_temperature <= self.current_temperature:
|
||||||
return CURRENT_HVAC_IDLE
|
return CURRENT_HVAC_IDLE
|
||||||
return CURRENT_HVAC_HEAT
|
return CURRENT_HVAC_HEAT
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> Optional[float]:
|
|
||||||
"""Return the current temperature of the evohome Zone."""
|
|
||||||
return (
|
|
||||||
self._evo_device.temperatureStatus["temperature"]
|
|
||||||
if self._evo_device.temperatureStatus["isAvailable"]
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float:
|
def target_temperature(self) -> float:
|
||||||
"""Return the target temperature of the evohome Zone."""
|
"""Return the target temperature of a Zone."""
|
||||||
if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF:
|
|
||||||
return self._evo_device.setpointCapabilities["minHeatSetpoint"]
|
|
||||||
return self._evo_device.setpointStatus["targetHeatTemperature"]
|
return self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -252,7 +190,7 @@ class EvoZone(EvoClimateDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def min_temp(self) -> float:
|
def min_temp(self) -> float:
|
||||||
"""Return the minimum target temperature of a evohome Zone.
|
"""Return the minimum target temperature of a Zone.
|
||||||
|
|
||||||
The default is 5, but is user-configurable within 5-35 (in Celsius).
|
The default is 5, but is user-configurable within 5-35 (in Celsius).
|
||||||
"""
|
"""
|
||||||
@ -260,7 +198,7 @@ class EvoZone(EvoClimateDevice):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def max_temp(self) -> float:
|
def max_temp(self) -> float:
|
||||||
"""Return the maximum target temperature of a evohome Zone.
|
"""Return the maximum target temperature of a Zone.
|
||||||
|
|
||||||
The default is 35, but is user-configurable within 5-35 (in Celsius).
|
The default is 35, but is user-configurable within 5-35 (in Celsius).
|
||||||
"""
|
"""
|
||||||
@ -268,26 +206,70 @@ class EvoZone(EvoClimateDevice):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs) -> None:
|
async def async_set_temperature(self, **kwargs) -> None:
|
||||||
"""Set a new target temperature."""
|
"""Set a new target temperature."""
|
||||||
until = kwargs.get("until")
|
temperature = kwargs["temperature"]
|
||||||
if until:
|
|
||||||
until = parse_datetime(until)
|
|
||||||
|
|
||||||
await self._set_temperature(kwargs["temperature"], until)
|
if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW:
|
||||||
|
await self._update_schedule()
|
||||||
|
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||||
|
elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER:
|
||||||
|
until = parse_datetime(self._evo_device.setpointStatus["until"])
|
||||||
|
else: # EVO_PERMOVER
|
||||||
|
until = None
|
||||||
|
|
||||||
|
await self._call_client_api(
|
||||||
|
self._evo_device.set_temperature(temperature, until)
|
||||||
|
)
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||||
"""Set an operating mode for the Zone."""
|
"""Set a Zone to one of its native EVO_* operating modes.
|
||||||
if hvac_mode == HVAC_MODE_OFF:
|
|
||||||
await self._set_temperature(self.min_temp, until=None)
|
|
||||||
|
|
||||||
|
Zones inherit their _effective_ operating mode from their Controller.
|
||||||
|
|
||||||
|
Usually, Zones are in 'FollowSchedule' mode, where their setpoints are 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 indefinitely,
|
||||||
|
'PermanentOverride' mode, or for a set period of time, 'TemporaryOverride' mode
|
||||||
|
(after which they will revert back to 'FollowSchedule' mode).
|
||||||
|
|
||||||
|
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 hvac_mode == HVAC_MODE_OFF:
|
||||||
|
await self._call_client_api(
|
||||||
|
self._evo_device.set_temperature(self.min_temp, until=None)
|
||||||
|
)
|
||||||
else: # HVAC_MODE_HEAT
|
else: # HVAC_MODE_HEAT
|
||||||
await self._set_zone_mode(EVO_FOLLOW)
|
await self._call_client_api(self._evo_device.cancel_temp_override())
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
||||||
"""Set a new preset mode.
|
"""Set the preset mode; if None, then revert to following the schedule."""
|
||||||
|
evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)
|
||||||
|
|
||||||
If preset_mode is None, then revert to following the schedule.
|
if evo_preset_mode == EVO_FOLLOW:
|
||||||
"""
|
await self._call_client_api(self._evo_device.cancel_temp_override())
|
||||||
await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
return
|
||||||
|
|
||||||
|
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||||
|
|
||||||
|
if evo_preset_mode == EVO_TEMPOVER:
|
||||||
|
await self._update_schedule()
|
||||||
|
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||||
|
else: # EVO_PERMOVER
|
||||||
|
until = None
|
||||||
|
|
||||||
|
await self._call_client_api(
|
||||||
|
self._evo_device.set_temperature(temperature, until)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Get the latest state data for a Zone."""
|
||||||
|
await super().async_update()
|
||||||
|
|
||||||
|
for attr in STATE_ATTRS_ZONES:
|
||||||
|
self._device_state_attrs[attr] = getattr(self._evo_device, attr)
|
||||||
|
|
||||||
|
|
||||||
class EvoController(EvoClimateDevice):
|
class EvoController(EvoClimateDevice):
|
||||||
@ -298,21 +280,20 @@ class EvoController(EvoClimateDevice):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the evohome Controller (hub)."""
|
"""Initialize a evohome Controller (hub)."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
|
self._unique_id = evo_device.systemId
|
||||||
self._name = evo_device.location.name
|
self._name = evo_device.location.name
|
||||||
self._icon = "mdi:thermostat"
|
self._icon = "mdi:thermostat"
|
||||||
|
|
||||||
self._precision = PRECISION_TENTHS
|
self._precision = PRECISION_TENTHS
|
||||||
self._state_attributes = ["systemId", "activeFaults", "systemModeStatus"]
|
|
||||||
|
|
||||||
self._supported_features = SUPPORT_PRESET_MODE
|
self._supported_features = SUPPORT_PRESET_MODE
|
||||||
self._preset_modes = list(HA_PRESET_TO_TCS)
|
self._preset_modes = list(HA_PRESET_TO_TCS)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
"""Return the current operating mode of the evohome Controller."""
|
"""Return the current operating mode of a Controller."""
|
||||||
tcs_mode = self._evo_tcs.systemModeStatus["mode"]
|
tcs_mode = self._evo_tcs.systemModeStatus["mode"]
|
||||||
return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT
|
return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT
|
||||||
|
|
||||||
@ -334,52 +315,53 @@ class EvoController(EvoClimateDevice):
|
|||||||
"""Return the current preset mode, e.g., home, away, temp."""
|
"""Return the current preset mode, e.g., home, away, temp."""
|
||||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
|
return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"])
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs) -> None:
|
@property
|
||||||
"""Do nothing.
|
def min_temp(self) -> float:
|
||||||
|
"""Return None as Controllers don't have a target temperature."""
|
||||||
|
return None
|
||||||
|
|
||||||
The evohome Controller doesn't have a target temperature.
|
@property
|
||||||
"""
|
def max_temp(self) -> float:
|
||||||
return
|
"""Return None as Controllers don't have a target temperature."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_set_temperature(self, **kwargs) -> None:
|
||||||
|
"""Raise exception as Controllers don't have a target temperature."""
|
||||||
|
raise NotImplementedError("Evohome Controllers don't have target temperatures.")
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||||
"""Set an operating mode for the Controller."""
|
"""Set an operating mode for a Controller."""
|
||||||
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
||||||
"""Set a new preset mode.
|
"""Set the preset mode; if None, then revert to 'Auto' mode."""
|
||||||
|
|
||||||
If preset_mode is None, then revert to 'Auto' mode.
|
|
||||||
"""
|
|
||||||
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO))
|
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO))
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest state data for a Controller."""
|
||||||
return
|
self._device_state_attrs = {}
|
||||||
|
|
||||||
|
attrs = self._device_state_attrs
|
||||||
|
for attr in STATE_ATTRS_TCS:
|
||||||
|
if attr == "activeFaults":
|
||||||
|
attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr)
|
||||||
|
else:
|
||||||
|
attrs[attr] = getattr(self._evo_tcs, attr)
|
||||||
|
|
||||||
|
|
||||||
class EvoThermostat(EvoZone):
|
class EvoThermostat(EvoZone):
|
||||||
"""Base for a Honeywell Round Thermostat.
|
"""Base for a Honeywell Round Thermostat.
|
||||||
|
|
||||||
Implemented as a combined Controller/Zone.
|
These are implemented as a combined Controller/Zone.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the Round Thermostat."""
|
"""Initialize the Thermostat."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
self._name = evo_broker.tcs.location.name
|
self._name = evo_broker.tcs.location.name
|
||||||
self._preset_modes = [PRESET_AWAY, PRESET_ECO]
|
self._preset_modes = [PRESET_AWAY, PRESET_ECO]
|
||||||
|
|
||||||
@property
|
|
||||||
def device_state_attributes(self) -> Dict[str, Any]:
|
|
||||||
"""Return the device-specific state attributes."""
|
|
||||||
status = super().device_state_attributes["status"]
|
|
||||||
|
|
||||||
status["systemModeStatus"] = self._evo_tcs.systemModeStatus
|
|
||||||
status["activeFaults"] += self._evo_tcs.activeFaults
|
|
||||||
|
|
||||||
return {"status": status}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
"""Return the current operating mode."""
|
"""Return the current operating mode."""
|
||||||
@ -404,11 +386,19 @@ class EvoThermostat(EvoZone):
|
|||||||
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None:
|
||||||
"""Set a new preset mode.
|
"""Set the preset mode; if None, then revert to following the schedule."""
|
||||||
|
|
||||||
If preset_mode is None, then revert to following the schedule.
|
|
||||||
"""
|
|
||||||
if preset_mode in list(HA_PRESET_TO_TCS):
|
if preset_mode in list(HA_PRESET_TO_TCS):
|
||||||
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode))
|
await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode))
|
||||||
else:
|
else:
|
||||||
await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
await super().async_set_hvac_mode(preset_mode)
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Get the latest state data for the Thermostat."""
|
||||||
|
await super().async_update()
|
||||||
|
|
||||||
|
attrs = self._device_state_attrs
|
||||||
|
for attr in STATE_ATTRS_TCS:
|
||||||
|
if attr == "activeFaults": # self._evo_device also has "activeFaults"
|
||||||
|
attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr)
|
||||||
|
else:
|
||||||
|
attrs[attr] = getattr(self._evo_tcs, attr)
|
||||||
|
@ -21,5 +21,3 @@ EVO_PERMOVER = "PermanentOverride"
|
|||||||
# These are used only to help prevent E501 (line too long) violations
|
# These are used only to help prevent E501 (line too long) violations
|
||||||
GWS = "gateways"
|
GWS = "gateways"
|
||||||
TCS = "temperatureControlSystems"
|
TCS = "temperatureControlSystems"
|
||||||
|
|
||||||
EVO_STRFTIME = "%Y-%m-%dT%H:%M:%SZ"
|
|
||||||
|
@ -3,27 +3,31 @@ import logging
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
|
SUPPORT_AWAY_MODE,
|
||||||
SUPPORT_OPERATION_MODE,
|
SUPPORT_OPERATION_MODE,
|
||||||
WaterHeaterDevice,
|
WaterHeaterDevice,
|
||||||
)
|
)
|
||||||
from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
|
from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
from homeassistant.util.dt import parse_datetime
|
from homeassistant.util.dt import parse_datetime
|
||||||
|
|
||||||
from . import EvoDevice
|
from . import EvoChild
|
||||||
from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER
|
from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HA_STATE_TO_EVO = {STATE_ON: "On", STATE_OFF: "Off"}
|
STATE_AUTO = "auto"
|
||||||
EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()}
|
|
||||||
|
|
||||||
HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER}
|
HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: "On", STATE_OFF: "Off"}
|
||||||
|
EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""}
|
||||||
|
|
||||||
|
STATE_ATTRS_DHW = ["dhwId", "activeFaults", "stateStatus", "temperatureStatus"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_platform(
|
||||||
hass, config, async_add_entities, discovery_info=None
|
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create the DHW controller."""
|
"""Create a DHW controller."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -38,63 +42,71 @@ async def async_setup_platform(
|
|||||||
async_add_entities([evo_dhw], update_before_add=True)
|
async_add_entities([evo_dhw], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class EvoDHW(EvoDevice, WaterHeaterDevice):
|
class EvoDHW(EvoChild, WaterHeaterDevice):
|
||||||
"""Base for a Honeywell evohome DHW controller (aka boiler)."""
|
"""Base for a Honeywell evohome DHW controller (aka boiler)."""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the evohome DHW controller."""
|
"""Initialize a evohome DHW controller."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
|
self._unique_id = evo_device.dhwId
|
||||||
self._name = "DHW controller"
|
self._name = "DHW controller"
|
||||||
self._icon = "mdi:thermometer-lines"
|
self._icon = "mdi:thermometer-lines"
|
||||||
|
|
||||||
self._precision = PRECISION_WHOLE
|
self._precision = PRECISION_WHOLE
|
||||||
self._state_attributes = [
|
self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE
|
||||||
"dhwId",
|
|
||||||
"activeFaults",
|
|
||||||
"stateStatus",
|
|
||||||
"temperatureStatus",
|
|
||||||
"setpoints",
|
|
||||||
]
|
|
||||||
|
|
||||||
self._supported_features = SUPPORT_OPERATION_MODE
|
|
||||||
self._operation_list = list(HA_OPMODE_TO_DHW)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def state(self):
|
||||||
"""Return True if entity is available."""
|
"""Return the current state."""
|
||||||
return self._evo_device.temperatureStatus.get("isAvailable", False)
|
return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self) -> str:
|
def current_operation(self) -> str:
|
||||||
"""Return the current operating mode (On, or Off)."""
|
"""Return the current operating mode (Auto, On, or Off)."""
|
||||||
|
if self._evo_device.stateStatus["mode"] == EVO_FOLLOW:
|
||||||
|
return STATE_AUTO
|
||||||
return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]]
|
return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def operation_list(self) -> List[str]:
|
def operation_list(self) -> List[str]:
|
||||||
"""Return the list of available operations."""
|
"""Return the list of available operations."""
|
||||||
return self._operation_list
|
return list(HA_STATE_TO_EVO)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float:
|
def is_away_mode_on(self):
|
||||||
"""Return the current temperature."""
|
"""Return True if away mode is on."""
|
||||||
return self._evo_device.temperatureStatus["temperature"]
|
is_off = EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] == STATE_OFF
|
||||||
|
is_permanent = self._evo_device.stateStatus["mode"] == EVO_PERMOVER
|
||||||
|
return is_off and is_permanent
|
||||||
|
|
||||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||||
"""Set new operation mode for a DHW controller."""
|
"""Set new operation mode for a DHW controller.
|
||||||
op_mode = HA_OPMODE_TO_DHW[operation_mode]
|
|
||||||
|
|
||||||
state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF]
|
Except for Auto, the mode is only until the next SetPoint.
|
||||||
until = None # EVO_FOLLOW, EVO_PERMOVER
|
"""
|
||||||
|
if operation_mode == STATE_AUTO:
|
||||||
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
await self._call_client_api(self._evo_device.set_dhw_auto())
|
||||||
|
else:
|
||||||
await self._update_schedule()
|
await self._update_schedule()
|
||||||
if self._schedule["DailySchedules"]:
|
until = parse_datetime(str(self.setpoints.get("next_sp_from")))
|
||||||
until = parse_datetime(self.setpoints["next"]["from"])
|
|
||||||
until = until.strftime(EVO_STRFTIME)
|
|
||||||
|
|
||||||
data = {"Mode": op_mode, "State": state, "UntilTime": until}
|
if operation_mode == STATE_ON:
|
||||||
|
await self._call_client_api(self._evo_device.set_dhw_on(until))
|
||||||
|
else: # STATE_OFF
|
||||||
|
await self._call_client_api(self._evo_device.set_dhw_off(until))
|
||||||
|
|
||||||
await self._call_client_api(
|
async def async_turn_away_mode_on(self):
|
||||||
self._evo_device._set_dhw(data) # pylint: disable=protected-access
|
"""Turn away mode on."""
|
||||||
)
|
await self._call_client_api(self._evo_device.set_dhw_off())
|
||||||
|
|
||||||
|
async def async_turn_away_mode_off(self):
|
||||||
|
"""Turn away mode off."""
|
||||||
|
await self._call_client_api(self._evo_device.set_dhw_auto())
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Get the latest state data for a DHW controller."""
|
||||||
|
await super().async_update()
|
||||||
|
|
||||||
|
for attr in STATE_ATTRS_DHW:
|
||||||
|
self._device_state_attrs[attr] = getattr(self._evo_device, attr)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user