mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Change evohome to asyncio client (#26042)
* fully async now * add convergence (call update() 2 seconds after client API call) (issue#25400) * handle dead TRVs (e.g. flat battery)
This commit is contained in:
parent
298aafc79d
commit
f91dd4f5f8
@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
Such systems include evohome (multi-zone), and Round Thermostat (single zone).
|
Such systems include evohome (multi-zone), and Round Thermostat (single zone).
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional, Tuple
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
import requests.exceptions
|
import aiohttp.client_exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import evohomeclient2
|
import evohomeasync2
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
@ -21,17 +20,10 @@ from homeassistant.const import (
|
|||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.discovery import load_platform
|
from homeassistant.helpers.discovery import async_load_platform
|
||||||
from homeassistant.helpers.dispatcher import (
|
|
||||||
async_dispatcher_connect,
|
|
||||||
async_dispatcher_send,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity import Entity
|
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.helpers.typing import ConfigType, HomeAssistantType
|
||||||
from homeassistant.util.dt import parse_datetime, utcnow
|
from homeassistant.util.dt import parse_datetime, utcnow
|
||||||
|
|
||||||
@ -81,55 +73,60 @@ def _handle_exception(err) -> bool:
|
|||||||
try:
|
try:
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
except evohomeclient2.AuthenticationError:
|
except evohomeasync2.AuthenticationError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Failed to (re)authenticate with the vendor's server. "
|
"Failed to (re)authenticate with the vendor's server. "
|
||||||
|
"Check your network and the vendor's service status page. "
|
||||||
"Check that your username and password are correct. "
|
"Check that your username and password are correct. "
|
||||||
"Message is: %s",
|
"Message is: %s",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
except aiohttp.ClientConnectionError:
|
||||||
# this appears to be common with Honeywell's servers
|
# this appears to be common with Honeywell's servers
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Unable to connect with the vendor's server. "
|
"Unable to connect with the vendor's server. "
|
||||||
"Check your network and the vendor's status page."
|
"Check your network and the vendor's service status page. "
|
||||||
"Message is: %s",
|
"Message is: %s",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.exceptions.HTTPError:
|
except aiohttp.ClientResponseError:
|
||||||
if err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
|
if err.status == HTTP_SERVICE_UNAVAILABLE:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Vendor says their server is currently unavailable. "
|
"The vendor says their server is currently unavailable. "
|
||||||
"Check the vendor's status page."
|
"Check the vendor's service status page."
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
|
if err.status == HTTP_TOO_MANY_REQUESTS:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The vendor's API rate limit has been exceeded. "
|
"The vendor's API rate limit has been exceeded. "
|
||||||
"Consider increasing the %s.",
|
"If this message persists, consider increasing the %s.",
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
raise # we don't expect/handle any other HTTPErrors
|
raise # we don't expect/handle any other ClientResponseError
|
||||||
|
|
||||||
|
|
||||||
def setup(hass: HomeAssistantType, hass_config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
||||||
"""Create a (EMEA/EU-based) Honeywell evohome system."""
|
"""Create a (EMEA/EU-based) Honeywell evohome system."""
|
||||||
broker = EvoBroker(hass, hass_config[DOMAIN])
|
broker = EvoBroker(hass, config[DOMAIN])
|
||||||
if not broker.init_client():
|
if not await broker.init_client():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
load_platform(hass, "climate", DOMAIN, {}, hass_config)
|
hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config))
|
||||||
if broker.tcs.hotwater:
|
if broker.tcs.hotwater:
|
||||||
load_platform(hass, "water_heater", DOMAIN, {}, hass_config)
|
hass.async_create_task(
|
||||||
|
async_load_platform(hass, "water_heater", DOMAIN, {}, config)
|
||||||
|
)
|
||||||
|
|
||||||
track_time_interval(hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL])
|
hass.helpers.event.async_track_time_interval(
|
||||||
|
broker.update, config[DOMAIN][CONF_SCAN_INTERVAL]
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -141,8 +138,7 @@ class EvoBroker:
|
|||||||
"""Initialize the evohome client and data structure."""
|
"""Initialize the evohome client and data structure."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.params = params
|
self.params = params
|
||||||
|
self.config = {}
|
||||||
self.config = self.status = self.timers = {}
|
|
||||||
|
|
||||||
self.client = self.tcs = None
|
self.client = self.tcs = None
|
||||||
self._app_storage = {}
|
self._app_storage = {}
|
||||||
@ -150,32 +146,31 @@ class EvoBroker:
|
|||||||
hass.data[DOMAIN] = {}
|
hass.data[DOMAIN] = {}
|
||||||
hass.data[DOMAIN]["broker"] = self
|
hass.data[DOMAIN]["broker"] = self
|
||||||
|
|
||||||
def init_client(self) -> bool:
|
async def init_client(self) -> bool:
|
||||||
"""Initialse the evohome data broker.
|
"""Initialse the evohome data broker.
|
||||||
|
|
||||||
Return True if this is successful, otherwise return False.
|
Return True if this is successful, otherwise return False.
|
||||||
"""
|
"""
|
||||||
refresh_token, access_token, access_token_expires = asyncio.run_coroutine_threadsafe(
|
refresh_token, access_token, access_token_expires = (
|
||||||
self._load_auth_tokens(), self.hass.loop
|
await self._load_auth_tokens()
|
||||||
).result()
|
)
|
||||||
|
|
||||||
# evohomeclient2 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 = _utc_to_local_dt(access_token_expires)
|
||||||
|
|
||||||
try:
|
client = self.client = evohomeasync2.EvohomeClient(
|
||||||
client = self.client = evohomeclient2.EvohomeClient(
|
self.params[CONF_USERNAME],
|
||||||
self.params[CONF_USERNAME],
|
self.params[CONF_PASSWORD],
|
||||||
self.params[CONF_PASSWORD],
|
refresh_token=refresh_token,
|
||||||
refresh_token=refresh_token,
|
access_token=access_token,
|
||||||
access_token=access_token,
|
access_token_expires=access_token_expires,
|
||||||
access_token_expires=access_token_expires,
|
session=async_get_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
except (
|
try:
|
||||||
requests.exceptions.RequestException,
|
await client.login()
|
||||||
evohomeclient2.AuthenticationError,
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
) as err:
|
|
||||||
if not _handle_exception(err):
|
if not _handle_exception(err):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -200,17 +195,14 @@ class EvoBroker:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self.tcs = (
|
self.tcs = (
|
||||||
client.locations[loc_idx] # noqa: E501; pylint: disable=protected-access
|
client.locations[loc_idx] # pylint: disable=protected-access
|
||||||
._gateways[0]
|
._gateways[0]
|
||||||
._control_systems[0]
|
._control_systems[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.debug("Config = %s", self.config)
|
_LOGGER.debug("Config = %s", self.config)
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required
|
||||||
# don't do an I/O unless required
|
await self.update() # includes: _LOGGER.debug("Status = %s"...
|
||||||
_LOGGER.debug(
|
|
||||||
"Status = %s", client.locations[loc_idx].status()[GWS][0][TCS][0]
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -237,7 +229,7 @@ class EvoBroker:
|
|||||||
return (None, None, None) # account switched: so tokens wont be valid
|
return (None, None, None) # account switched: so tokens wont be valid
|
||||||
|
|
||||||
async def _save_auth_tokens(self, *args) -> None:
|
async def _save_auth_tokens(self, *args) -> None:
|
||||||
# evohomeclient2 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_utc(self.client.access_token_expires)
|
||||||
|
|
||||||
self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
|
self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME]
|
||||||
@ -248,13 +240,12 @@ class EvoBroker:
|
|||||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
await store.async_save(self._app_storage)
|
await store.async_save(self._app_storage)
|
||||||
|
|
||||||
async_track_point_in_utc_time(
|
self.hass.helpers.event.async_track_point_in_utc_time(
|
||||||
self.hass,
|
|
||||||
self._save_auth_tokens,
|
self._save_auth_tokens,
|
||||||
access_token_expires + self.params[CONF_SCAN_INTERVAL],
|
access_token_expires + self.params[CONF_SCAN_INTERVAL],
|
||||||
)
|
)
|
||||||
|
|
||||||
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 the entire evohome Location.
|
||||||
|
|
||||||
This includes state data for the Controller and all its child devices,
|
This includes state data for the Controller and all its child devices,
|
||||||
@ -264,19 +255,16 @@ class EvoBroker:
|
|||||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
status = self.client.locations[loc_idx].status()[GWS][0][TCS][0]
|
status = await self.client.locations[loc_idx].status()
|
||||||
except (
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
requests.exceptions.RequestException,
|
|
||||||
evohomeclient2.AuthenticationError,
|
|
||||||
) as err:
|
|
||||||
_handle_exception(err)
|
_handle_exception(err)
|
||||||
else:
|
else:
|
||||||
self.timers["statusUpdated"] = utcnow()
|
|
||||||
|
|
||||||
_LOGGER.debug("Status = %s", status)
|
|
||||||
|
|
||||||
# inform the evohome devices that state data has been updated
|
# inform the evohome devices that state data has been updated
|
||||||
async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"})
|
self.hass.helpers.dispatcher.async_dispatcher_send(
|
||||||
|
DOMAIN, {"signal": "refresh"}
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
||||||
|
|
||||||
|
|
||||||
class EvoDevice(Entity):
|
class EvoDevice(Entity):
|
||||||
@ -289,6 +277,7 @@ class EvoDevice(Entity):
|
|||||||
def __init__(self, evo_broker, evo_device) -> None:
|
def __init__(self, evo_broker, evo_device) -> None:
|
||||||
"""Initialize the evohome entity."""
|
"""Initialize the evohome entity."""
|
||||||
self._evo_device = evo_device
|
self._evo_device = evo_device
|
||||||
|
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._name = self._icon = self._precision = None
|
||||||
@ -387,7 +376,7 @@ class EvoDevice(Entity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Run when entity about to be added to hass."""
|
"""Run when entity about to be added to hass."""
|
||||||
async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
|
self.hass.helpers.dispatcher.async_dispatcher_connect(DOMAIN, self._refresh)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def precision(self) -> float:
|
def precision(self) -> float:
|
||||||
@ -399,14 +388,27 @@ 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
|
||||||
|
|
||||||
def _update_schedule(self) -> None:
|
async def _call_client_api(self, api_function) -> None:
|
||||||
|
try:
|
||||||
|
await api_function
|
||||||
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
|
_handle_exception(err)
|
||||||
|
|
||||||
|
self.hass.helpers.event.async_call_later(
|
||||||
|
2, self._evo_broker.update()
|
||||||
|
) # call update() in 2 seconds
|
||||||
|
|
||||||
|
async def _update_schedule(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest state data."""
|
||||||
if (
|
if (
|
||||||
not self._schedule.get("DailySchedules")
|
not self._schedule.get("DailySchedules")
|
||||||
or parse_datetime(self.setpoints["next"]["from"]) < utcnow()
|
or parse_datetime(self.setpoints["next"]["from"]) < utcnow()
|
||||||
):
|
):
|
||||||
self._schedule = self._evo_device.schedule()
|
try:
|
||||||
|
self._schedule = await self._evo_device.schedule()
|
||||||
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
|
_handle_exception(err)
|
||||||
|
|
||||||
def update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest state data."""
|
||||||
self._update_schedule()
|
await self._update_schedule()
|
||||||
|
@ -3,9 +3,6 @@ from datetime import datetime
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional, List
|
from typing import Any, Dict, Optional, List
|
||||||
|
|
||||||
import requests.exceptions
|
|
||||||
import evohomeclient2
|
|
||||||
|
|
||||||
from homeassistant.components.climate import ClimateDevice
|
from homeassistant.components.climate import ClimateDevice
|
||||||
from homeassistant.components.climate.const import (
|
from homeassistant.components.climate.const import (
|
||||||
CURRENT_HVAC_HEAT,
|
CURRENT_HVAC_HEAT,
|
||||||
@ -25,7 +22,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, _handle_exception, EvoDevice
|
from . import CONF_LOCATION_IDX, EvoDevice
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVO_RESET,
|
EVO_RESET,
|
||||||
@ -65,10 +62,13 @@ 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()}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistantType, hass_config: ConfigType, add_entities, discovery_info=None
|
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create the evohome Controller, and its Zones, if any."""
|
"""Create the evohome Controller, and its Zones, if any."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
broker = hass.data[DOMAIN]["broker"]
|
broker = hass.data[DOMAIN]["broker"]
|
||||||
loc_idx = broker.params[CONF_LOCATION_IDX]
|
loc_idx = broker.params[CONF_LOCATION_IDX]
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ def setup_platform(
|
|||||||
zone.name,
|
zone.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
add_entities([EvoThermostat(broker, zone)], update_before_add=True)
|
async_add_entities([EvoThermostat(broker, zone)], update_before_add=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
controller = EvoController(broker, broker.tcs)
|
controller = EvoController(broker, broker.tcs)
|
||||||
@ -107,7 +107,7 @@ def setup_platform(
|
|||||||
)
|
)
|
||||||
zones.append(EvoZone(broker, zone))
|
zones.append(EvoZone(broker, zone))
|
||||||
|
|
||||||
add_entities([controller] + zones, update_before_add=True)
|
async_add_entities([controller] + zones, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class EvoClimateDevice(EvoDevice, ClimateDevice):
|
class EvoClimateDevice(EvoDevice, ClimateDevice):
|
||||||
@ -119,22 +119,18 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||||||
|
|
||||||
self._preset_modes = None
|
self._preset_modes = None
|
||||||
|
|
||||||
def _set_temperature(
|
async def _set_temperature(
|
||||||
self, temperature: float, until: Optional[datetime] = None
|
self, temperature: float, until: Optional[datetime] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set a new target temperature for the Zone.
|
"""Set a new target temperature for the Zone.
|
||||||
|
|
||||||
until == None means indefinitely (i.e. PermanentOverride)
|
until == None means indefinitely (i.e. PermanentOverride)
|
||||||
"""
|
"""
|
||||||
try:
|
await self._call_client_api(
|
||||||
self._evo_device.set_temperature(temperature, until)
|
self._evo_device.set_temperature(temperature, until)
|
||||||
except (
|
)
|
||||||
requests.exceptions.RequestException,
|
|
||||||
evohomeclient2.AuthenticationError,
|
|
||||||
) as err:
|
|
||||||
_handle_exception(err)
|
|
||||||
|
|
||||||
def _set_zone_mode(self, op_mode: str) -> None:
|
async def _set_zone_mode(self, op_mode: str) -> None:
|
||||||
"""Set a Zone to one of its native EVO_* operating modes.
|
"""Set a Zone to one of its native EVO_* operating modes.
|
||||||
|
|
||||||
Zones inherit their _effective_ operating mode from the Controller.
|
Zones inherit their _effective_ operating mode from the Controller.
|
||||||
@ -153,35 +149,24 @@ class EvoClimateDevice(EvoDevice, ClimateDevice):
|
|||||||
(by default) 5C, and 'Away', Zones to (by default) 12C.
|
(by default) 5C, and 'Away', Zones to (by default) 12C.
|
||||||
"""
|
"""
|
||||||
if op_mode == EVO_FOLLOW:
|
if op_mode == EVO_FOLLOW:
|
||||||
try:
|
await self._call_client_api(self._evo_device.cancel_temp_override())
|
||||||
self._evo_device.cancel_temp_override()
|
|
||||||
except (
|
|
||||||
requests.exceptions.RequestException,
|
|
||||||
evohomeclient2.AuthenticationError,
|
|
||||||
) as err:
|
|
||||||
_handle_exception(err)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
temperature = self._evo_device.setpointStatus["targetHeatTemperature"]
|
||||||
until = None # EVO_PERMOVER
|
until = None # EVO_PERMOVER
|
||||||
|
|
||||||
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
||||||
self._update_schedule()
|
await self._update_schedule()
|
||||||
if self._schedule["DailySchedules"]:
|
if self._schedule["DailySchedules"]:
|
||||||
until = parse_datetime(self.setpoints["next"]["from"])
|
until = parse_datetime(self.setpoints["next"]["from"])
|
||||||
|
|
||||||
self._set_temperature(temperature, until=until)
|
await self._set_temperature(temperature, until=until)
|
||||||
|
|
||||||
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 the Controller to any of its native EVO_* operating modes."""
|
||||||
try:
|
await self._call_client_api(
|
||||||
# noqa: E501; pylint: disable=protected-access
|
self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access
|
||||||
self._evo_tcs._set_status(op_mode)
|
)
|
||||||
except (
|
|
||||||
requests.exceptions.RequestException,
|
|
||||||
evohomeclient2.AuthenticationError,
|
|
||||||
) as err:
|
|
||||||
_handle_exception(err)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_modes(self) -> List[str]:
|
def hvac_modes(self) -> List[str]:
|
||||||
@ -216,6 +201,11 @@ class EvoZone(EvoClimateDevice):
|
|||||||
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 the evohome Zone."""
|
||||||
@ -276,28 +266,28 @@ class EvoZone(EvoClimateDevice):
|
|||||||
"""
|
"""
|
||||||
return self._evo_device.setpointCapabilities["maxHeatSetpoint"]
|
return self._evo_device.setpointCapabilities["maxHeatSetpoint"]
|
||||||
|
|
||||||
def 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")
|
until = kwargs.get("until")
|
||||||
if until:
|
if until:
|
||||||
until = parse_datetime(until)
|
until = parse_datetime(until)
|
||||||
|
|
||||||
self._set_temperature(kwargs["temperature"], until)
|
await self._set_temperature(kwargs["temperature"], until)
|
||||||
|
|
||||||
def 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 an operating mode for the Zone."""
|
||||||
if hvac_mode == HVAC_MODE_OFF:
|
if hvac_mode == HVAC_MODE_OFF:
|
||||||
self._set_temperature(self.min_temp, until=None)
|
await self._set_temperature(self.min_temp, until=None)
|
||||||
|
|
||||||
else: # HVAC_MODE_HEAT
|
else: # HVAC_MODE_HEAT
|
||||||
self._set_zone_mode(EVO_FOLLOW)
|
await self._set_zone_mode(EVO_FOLLOW)
|
||||||
|
|
||||||
def 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 a new preset mode.
|
||||||
|
|
||||||
If preset_mode is None, then revert to following the schedule.
|
If preset_mode is None, then revert to following the schedule.
|
||||||
"""
|
"""
|
||||||
self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
||||||
|
|
||||||
|
|
||||||
class EvoController(EvoClimateDevice):
|
class EvoController(EvoClimateDevice):
|
||||||
@ -344,25 +334,25 @@ 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"])
|
||||||
|
|
||||||
def set_temperature(self, **kwargs) -> None:
|
async def async_set_temperature(self, **kwargs) -> None:
|
||||||
"""Do nothing.
|
"""Do nothing.
|
||||||
|
|
||||||
The evohome Controller doesn't have a target temperature.
|
The evohome Controller doesn't have a target temperature.
|
||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
def 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 the Controller."""
|
||||||
self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
||||||
|
|
||||||
def 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 a new preset mode.
|
||||||
|
|
||||||
If preset_mode is None, then revert to 'Auto' mode.
|
If preset_mode is None, then revert to 'Auto' mode.
|
||||||
"""
|
"""
|
||||||
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))
|
||||||
|
|
||||||
def update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Get the latest state data."""
|
"""Get the latest state data."""
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -409,16 +399,16 @@ class EvoThermostat(EvoZone):
|
|||||||
|
|
||||||
return super().preset_mode
|
return super().preset_mode
|
||||||
|
|
||||||
def set_hvac_mode(self, hvac_mode: str) -> None:
|
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||||
"""Set an operating mode."""
|
"""Set an operating mode."""
|
||||||
self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode))
|
||||||
|
|
||||||
def 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 a new preset mode.
|
||||||
|
|
||||||
If preset_mode is 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):
|
||||||
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:
|
||||||
self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW))
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Evohome",
|
"name": "Evohome",
|
||||||
"documentation": "https://www.home-assistant.io/components/evohome",
|
"documentation": "https://www.home-assistant.io/components/evohome",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"evohomeclient==0.3.3"
|
"evohome-async==0.3.3b4"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@zxdavb"]
|
"codeowners": ["@zxdavb"]
|
||||||
|
@ -2,9 +2,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import requests.exceptions
|
|
||||||
import evohomeclient2
|
|
||||||
|
|
||||||
from homeassistant.components.water_heater import (
|
from homeassistant.components.water_heater import (
|
||||||
SUPPORT_OPERATION_MODE,
|
SUPPORT_OPERATION_MODE,
|
||||||
WaterHeaterDevice,
|
WaterHeaterDevice,
|
||||||
@ -12,7 +9,7 @@ from homeassistant.components.water_heater import (
|
|||||||
from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
|
from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
|
||||||
from homeassistant.util.dt import parse_datetime
|
from homeassistant.util.dt import parse_datetime
|
||||||
|
|
||||||
from . import _handle_exception, EvoDevice
|
from . import EvoDevice
|
||||||
from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER
|
from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -23,8 +20,13 @@ 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_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None:
|
async def async_setup_platform(
|
||||||
|
hass, config, async_add_entities, discovery_info=None
|
||||||
|
) -> None:
|
||||||
"""Create the DHW controller."""
|
"""Create the DHW controller."""
|
||||||
|
if discovery_info is None:
|
||||||
|
return
|
||||||
|
|
||||||
broker = hass.data[DOMAIN]["broker"]
|
broker = hass.data[DOMAIN]["broker"]
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -33,7 +35,7 @@ def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None
|
|||||||
|
|
||||||
evo_dhw = EvoDHW(broker, broker.tcs.hotwater)
|
evo_dhw = EvoDHW(broker, broker.tcs.hotwater)
|
||||||
|
|
||||||
add_entities([evo_dhw], update_before_add=True)
|
async_add_entities([evo_dhw], update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class EvoDHW(EvoDevice, WaterHeaterDevice):
|
class EvoDHW(EvoDevice, WaterHeaterDevice):
|
||||||
@ -58,6 +60,11 @@ class EvoDHW(EvoDevice, WaterHeaterDevice):
|
|||||||
self._supported_features = SUPPORT_OPERATION_MODE
|
self._supported_features = SUPPORT_OPERATION_MODE
|
||||||
self._operation_list = list(HA_OPMODE_TO_DHW)
|
self._operation_list = list(HA_OPMODE_TO_DHW)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self._evo_device.temperatureStatus.get("isAvailable", False)
|
||||||
|
|
||||||
@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 (On, or Off)."""
|
||||||
@ -73,7 +80,7 @@ class EvoDHW(EvoDevice, WaterHeaterDevice):
|
|||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
return self._evo_device.temperatureStatus["temperature"]
|
return self._evo_device.temperatureStatus["temperature"]
|
||||||
|
|
||||||
def 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]
|
op_mode = HA_OPMODE_TO_DHW[operation_mode]
|
||||||
|
|
||||||
@ -81,17 +88,13 @@ class EvoDHW(EvoDevice, WaterHeaterDevice):
|
|||||||
until = None # EVO_FOLLOW, EVO_PERMOVER
|
until = None # EVO_FOLLOW, EVO_PERMOVER
|
||||||
|
|
||||||
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]:
|
||||||
self._update_schedule()
|
await self._update_schedule()
|
||||||
if self._schedule["DailySchedules"]:
|
if self._schedule["DailySchedules"]:
|
||||||
until = parse_datetime(self.setpoints["next"]["from"])
|
until = parse_datetime(self.setpoints["next"]["from"])
|
||||||
until = until.strftime(EVO_STRFTIME)
|
until = until.strftime(EVO_STRFTIME)
|
||||||
|
|
||||||
data = {"Mode": op_mode, "State": state, "UntilTime": until}
|
data = {"Mode": op_mode, "State": state, "UntilTime": until}
|
||||||
|
|
||||||
try:
|
await self._call_client_api(
|
||||||
self._evo_device._set_dhw(data) # pylint: disable=protected-access
|
self._evo_device._set_dhw(data) # pylint: disable=protected-access
|
||||||
except (
|
)
|
||||||
requests.exceptions.RequestException,
|
|
||||||
evohomeclient2.AuthenticationError,
|
|
||||||
) as err:
|
|
||||||
_handle_exception(err)
|
|
||||||
|
@ -461,7 +461,7 @@ eternalegypt==0.0.10
|
|||||||
# evdev==0.6.1
|
# evdev==0.6.1
|
||||||
|
|
||||||
# homeassistant.components.evohome
|
# homeassistant.components.evohome
|
||||||
evohomeclient==0.3.3
|
evohome-async==0.3.3b4
|
||||||
|
|
||||||
# homeassistant.components.dlib_face_detect
|
# homeassistant.components.dlib_face_detect
|
||||||
# homeassistant.components.dlib_face_identify
|
# homeassistant.components.dlib_face_identify
|
||||||
|
@ -123,9 +123,6 @@ enocean==0.50
|
|||||||
# homeassistant.components.season
|
# homeassistant.components.season
|
||||||
ephem==3.7.6.0
|
ephem==3.7.6.0
|
||||||
|
|
||||||
# homeassistant.components.evohome
|
|
||||||
evohomeclient==0.3.3
|
|
||||||
|
|
||||||
# homeassistant.components.feedreader
|
# homeassistant.components.feedreader
|
||||||
feedparser-homeassistant==5.2.2.dev1
|
feedparser-homeassistant==5.2.2.dev1
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user