mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +00:00
Add evohome high_precision temperatures (#27513)
* add high_precision (current) temperatures * bump client to use aiohttp for v1 client * token saving now event-driven rather than scheduled * protection against invalid tokens that cause issues * tweak error message
This commit is contained in:
parent
5a35e52adf
commit
44b6258e48
@ -10,9 +10,9 @@ from typing import Any, Dict, Optional, Tuple
|
|||||||
import aiohttp.client_exceptions
|
import aiohttp.client_exceptions
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
import evohomeasync2
|
import evohomeasync2
|
||||||
|
import evohomeasync
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
@ -32,10 +32,13 @@ from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires"
|
ACCESS_TOKEN = "access_token"
|
||||||
CONF_REFRESH_TOKEN = "refresh_token"
|
ACCESS_TOKEN_EXPIRES = "access_token_expires"
|
||||||
|
REFRESH_TOKEN = "refresh_token"
|
||||||
|
USER_DATA = "user_data"
|
||||||
|
|
||||||
CONF_LOCATION_IDX = "location_idx"
|
CONF_LOCATION_IDX = "location_idx"
|
||||||
|
|
||||||
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
||||||
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)
|
SCAN_INTERVAL_MINIMUM = timedelta(seconds=60)
|
||||||
|
|
||||||
@ -96,14 +99,15 @@ def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _handle_exception(err) -> bool:
|
def _handle_exception(err) -> bool:
|
||||||
|
"""Return False if the exception can't be ignored."""
|
||||||
try:
|
try:
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
except evohomeasync2.AuthenticationError:
|
except evohomeasync2.AuthenticationError:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Failed to (re)authenticate with the vendor's server. "
|
"Failed to authenticate with the vendor's server. "
|
||||||
"Check your network and the vendor's service status page. "
|
"Check your network and the vendor's service status page. "
|
||||||
"Check that your username and password are correct. "
|
"Also check that your username and password are correct. "
|
||||||
"Message is: %s",
|
"Message is: %s",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
@ -135,14 +139,77 @@ def _handle_exception(err) -> bool:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
raise # we don't expect/handle any other ClientResponseError
|
raise # we don't expect/handle any other Exceptions
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistantType, 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, config[DOMAIN])
|
|
||||||
if not await broker.init_client():
|
async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]:
|
||||||
|
app_storage = await store.async_load()
|
||||||
|
tokens = dict(app_storage if app_storage else {})
|
||||||
|
|
||||||
|
if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]:
|
||||||
|
# any tokens wont be valid, and store might be be corrupt
|
||||||
|
await store.async_save({})
|
||||||
|
return ({}, None)
|
||||||
|
|
||||||
|
# evohomeasync2 requires naive/local datetimes as strings
|
||||||
|
if tokens.get(ACCESS_TOKEN_EXPIRES) is not None:
|
||||||
|
tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_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)
|
||||||
|
tokens, user_data = await load_auth_tokens(store)
|
||||||
|
|
||||||
|
client_v2 = evohomeasync2.EvohomeClient(
|
||||||
|
config[DOMAIN][CONF_USERNAME],
|
||||||
|
config[DOMAIN][CONF_PASSWORD],
|
||||||
|
**tokens,
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client_v2.login()
|
||||||
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
|
_handle_exception(err)
|
||||||
return False
|
return False
|
||||||
|
finally:
|
||||||
|
config[DOMAIN][CONF_PASSWORD] = "REDACTED"
|
||||||
|
|
||||||
|
loc_idx = config[DOMAIN][CONF_LOCATION_IDX]
|
||||||
|
try:
|
||||||
|
loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0]
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Config error: '%s' = %s, but the valid range is 0-%s. "
|
||||||
|
"Unable to continue. Fix any configuration errors and restart HA.",
|
||||||
|
CONF_LOCATION_IDX,
|
||||||
|
loc_idx,
|
||||||
|
len(client_v2.installation_info) - 1,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
_LOGGER.debug("Config = %s", loc_config)
|
||||||
|
|
||||||
|
client_v1 = evohomeasync.EvohomeClient(
|
||||||
|
client_v2.username,
|
||||||
|
client_v2.password,
|
||||||
|
user_data=user_data,
|
||||||
|
session=async_get_clientsession(hass),
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN]["broker"] = broker = EvoBroker(
|
||||||
|
hass, client_v2, client_v1, store, config[DOMAIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
await broker.save_auth_tokens()
|
||||||
|
await broker.update() # get initial state
|
||||||
|
|
||||||
hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config))
|
hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config))
|
||||||
if broker.tcs.hotwater:
|
if broker.tcs.hotwater:
|
||||||
@ -160,116 +227,100 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
|
|||||||
class EvoBroker:
|
class EvoBroker:
|
||||||
"""Container for evohome client and data."""
|
"""Container for evohome client and data."""
|
||||||
|
|
||||||
def __init__(self, hass, params) -> None:
|
def __init__(self, hass, client, client_v1, store, params) -> None:
|
||||||
"""Initialize the evohome client and its data structure."""
|
"""Initialize the evohome client and its data structure."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self.client = client
|
||||||
|
self.client_v1 = client_v1
|
||||||
|
self._store = store
|
||||||
self.params = params
|
self.params = params
|
||||||
self.config = {}
|
|
||||||
|
|
||||||
self.client = self.tcs = None
|
|
||||||
self._app_storage = {}
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
|
||||||
hass.data[DOMAIN]["broker"] = self
|
|
||||||
|
|
||||||
async def init_client(self) -> bool:
|
|
||||||
"""Initialse the evohome data broker.
|
|
||||||
|
|
||||||
Return True if this is successful, otherwise return False.
|
|
||||||
"""
|
|
||||||
refresh_token, access_token, access_token_expires = (
|
|
||||||
await self._load_auth_tokens()
|
|
||||||
)
|
|
||||||
|
|
||||||
# evohomeasync2 uses naive/local datetimes
|
|
||||||
if access_token_expires is not None:
|
|
||||||
access_token_expires = _dt_to_local_naive(access_token_expires)
|
|
||||||
|
|
||||||
client = self.client = evohomeasync2.EvohomeClient(
|
|
||||||
self.params[CONF_USERNAME],
|
|
||||||
self.params[CONF_PASSWORD],
|
|
||||||
refresh_token=refresh_token,
|
|
||||||
access_token=access_token,
|
|
||||||
access_token_expires=access_token_expires,
|
|
||||||
session=async_get_clientsession(self.hass),
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
await client.login()
|
|
||||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
|
||||||
if not _handle_exception(err):
|
|
||||||
return False
|
|
||||||
|
|
||||||
finally:
|
|
||||||
self.params[CONF_PASSWORD] = "REDACTED"
|
|
||||||
|
|
||||||
self.hass.add_job(self._save_auth_tokens())
|
|
||||||
|
|
||||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
|
||||||
try:
|
|
||||||
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
|
||||||
|
|
||||||
except IndexError:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Config error: '%s' = %s, but its valid range is 0-%s. "
|
|
||||||
"Unable to continue. "
|
|
||||||
"Fix any configuration errors and restart HA.",
|
|
||||||
CONF_LOCATION_IDX,
|
|
||||||
loc_idx,
|
|
||||||
len(client.installation_info) - 1,
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
loc_idx = params[CONF_LOCATION_IDX]
|
||||||
|
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
||||||
self.tcs = (
|
self.tcs = (
|
||||||
client.locations[loc_idx] # pylint: disable=protected-access
|
client.locations[loc_idx] # pylint: disable=protected-access
|
||||||
._gateways[0]
|
._gateways[0]
|
||||||
._control_systems[0]
|
._control_systems[0]
|
||||||
)
|
)
|
||||||
|
self.temps = None
|
||||||
|
|
||||||
_LOGGER.debug("Config = %s", self.config)
|
async def save_auth_tokens(self) -> None:
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required
|
"""Save access tokens and session IDs to the store for later use."""
|
||||||
await self.update() # includes: _LOGGER.debug("Status = %s"...
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def _load_auth_tokens(
|
|
||||||
self
|
|
||||||
) -> Tuple[Optional[str], Optional[str], Optional[datetime]]:
|
|
||||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
|
||||||
app_storage = self._app_storage = await store.async_load()
|
|
||||||
|
|
||||||
if app_storage is None:
|
|
||||||
app_storage = self._app_storage = {}
|
|
||||||
|
|
||||||
if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]:
|
|
||||||
refresh_token = app_storage.get(CONF_REFRESH_TOKEN)
|
|
||||||
access_token = app_storage.get(CONF_ACCESS_TOKEN)
|
|
||||||
at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES)
|
|
||||||
if at_expires_str:
|
|
||||||
at_expires_dt = dt_util.parse_datetime(at_expires_str)
|
|
||||||
else:
|
|
||||||
at_expires_dt = None
|
|
||||||
|
|
||||||
return (refresh_token, access_token, at_expires_dt)
|
|
||||||
|
|
||||||
return (None, None, None) # account switched: so tokens wont be valid
|
|
||||||
|
|
||||||
async def _save_auth_tokens(self, *args) -> None:
|
|
||||||
# evohomeasync2 uses naive/local datetimes
|
# evohomeasync2 uses naive/local datetimes
|
||||||
access_token_expires = _local_dt_to_aware(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]
|
app_storage = {CONF_USERNAME: self.client.username}
|
||||||
self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token
|
app_storage[REFRESH_TOKEN] = self.client.refresh_token
|
||||||
self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token
|
app_storage[ACCESS_TOKEN] = self.client.access_token
|
||||||
self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat()
|
app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat()
|
||||||
|
|
||||||
store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
if self.client_v1 and self.client_v1.user_data:
|
||||||
await store.async_save(self._app_storage)
|
app_storage[USER_DATA] = {
|
||||||
|
"userInfo": {"userID": self.client_v1.user_data["userInfo"]["userID"]},
|
||||||
|
"sessionId": self.client_v1.user_data["sessionId"],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
app_storage[USER_DATA] = None
|
||||||
|
|
||||||
self.hass.helpers.event.async_track_point_in_utc_time(
|
await self._store.async_save(app_storage)
|
||||||
self._save_auth_tokens,
|
|
||||||
access_token_expires + self.params[CONF_SCAN_INTERVAL],
|
async def _update_v1(self, *args, **kwargs) -> None:
|
||||||
)
|
"""Get the latest high-precision temperatures of the default Location."""
|
||||||
|
|
||||||
|
def get_session_id(client_v1) -> Optional[str]:
|
||||||
|
user_data = client_v1.user_data if client_v1 else None
|
||||||
|
return user_data.get("sessionId") if user_data else None
|
||||||
|
|
||||||
|
session_id = get_session_id(self.client_v1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
temps = list(await self.client_v1.temperatures(force_refresh=True))
|
||||||
|
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Unable to obtain the latest high-precision temperatures. "
|
||||||
|
"Check your network and the vendor's service status page. "
|
||||||
|
"Proceeding with low-precision temperatures. "
|
||||||
|
"Message is: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
self.temps = None # these are now stale, will fall back to v2 temps
|
||||||
|
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
str(self.client_v1.location_id)
|
||||||
|
!= self.client.locations[self.params[CONF_LOCATION_IDX]].locationId
|
||||||
|
):
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The v2 API's configured location doesn't match "
|
||||||
|
"the v1 API's default location (there is more than one location), "
|
||||||
|
"so the high-precision feature will be disabled"
|
||||||
|
)
|
||||||
|
self.client_v1 = self.temps = None
|
||||||
|
else:
|
||||||
|
self.temps = {str(i["id"]): i["temp"] for i in temps}
|
||||||
|
|
||||||
|
_LOGGER.debug("Temperatures = %s", self.temps)
|
||||||
|
|
||||||
|
if session_id != get_session_id(self.client_v1):
|
||||||
|
await self.save_auth_tokens()
|
||||||
|
|
||||||
|
async def _update_v2(self, *args, **kwargs) -> None:
|
||||||
|
"""Get the latest modes, temperatures, setpoints of a Location."""
|
||||||
|
access_token = self.client.access_token
|
||||||
|
|
||||||
|
loc_idx = self.params[CONF_LOCATION_IDX]
|
||||||
|
try:
|
||||||
|
status = await self.client.locations[loc_idx].status()
|
||||||
|
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
||||||
|
_handle_exception(err)
|
||||||
|
else:
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
||||||
|
|
||||||
|
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
||||||
|
|
||||||
|
if access_token != self.client.access_token:
|
||||||
|
await self.save_auth_tokens()
|
||||||
|
|
||||||
async def update(self, *args, **kwargs) -> None:
|
async def update(self, *args, **kwargs) -> None:
|
||||||
"""Get the latest state data of an entire evohome Location.
|
"""Get the latest state data of an entire evohome Location.
|
||||||
@ -278,17 +329,13 @@ class EvoBroker:
|
|||||||
operating mode of the Controller and the current temp of its children (e.g.
|
operating mode of the Controller and the current temp of its children (e.g.
|
||||||
Zones, DHW controller).
|
Zones, DHW controller).
|
||||||
"""
|
"""
|
||||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
await self._update_v2()
|
||||||
|
|
||||||
try:
|
if self.client_v1:
|
||||||
status = await self.client.locations[loc_idx].status()
|
await self._update_v1()
|
||||||
except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err:
|
|
||||||
_handle_exception(err)
|
|
||||||
else:
|
|
||||||
# inform the evohome devices that state data has been updated
|
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
|
||||||
|
|
||||||
_LOGGER.debug("Status = %s", status[GWS][0][TCS][0])
|
# inform the evohome devices that state data has been updated
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
class EvoDevice(Entity):
|
class EvoDevice(Entity):
|
||||||
@ -305,10 +352,8 @@ class EvoDevice(Entity):
|
|||||||
self._evo_tcs = evo_broker.tcs
|
self._evo_tcs = evo_broker.tcs
|
||||||
|
|
||||||
self._unique_id = self._name = self._icon = self._precision = None
|
self._unique_id = self._name = self._icon = self._precision = None
|
||||||
|
|
||||||
self._device_state_attrs = {}
|
|
||||||
self._state_attributes = []
|
|
||||||
self._supported_features = None
|
self._supported_features = None
|
||||||
|
self._device_state_attrs = {}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _refresh(self) -> None:
|
def _refresh(self) -> None:
|
||||||
@ -394,9 +439,13 @@ class EvoChild(EvoDevice):
|
|||||||
@property
|
@property
|
||||||
def current_temperature(self) -> Optional[float]:
|
def current_temperature(self) -> Optional[float]:
|
||||||
"""Return the current temperature of a Zone."""
|
"""Return the current temperature of a Zone."""
|
||||||
if self._evo_device.temperatureStatus["isAvailable"]:
|
if not self._evo_device.temperatureStatus["isAvailable"]:
|
||||||
return self._evo_device.temperatureStatus["temperature"]
|
return None
|
||||||
return None
|
|
||||||
|
if self._evo_broker.temps:
|
||||||
|
return self._evo_broker.temps[self._evo_device.zoneId]
|
||||||
|
|
||||||
|
return self._evo_device.temperatureStatus["temperature"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def setpoints(self) -> Dict[str, Any]:
|
def setpoints(self) -> Dict[str, Any]:
|
||||||
|
@ -72,14 +72,13 @@ async def async_setup_platform(
|
|||||||
return
|
return
|
||||||
|
|
||||||
broker = hass.data[DOMAIN]["broker"]
|
broker = hass.data[DOMAIN]["broker"]
|
||||||
loc_idx = broker.params[CONF_LOCATION_IDX]
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
|
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
|
||||||
broker.tcs.modelType,
|
broker.tcs.modelType,
|
||||||
broker.tcs.systemId,
|
broker.tcs.systemId,
|
||||||
broker.tcs.location.name,
|
broker.tcs.location.name,
|
||||||
loc_idx,
|
broker.params[CONF_LOCATION_IDX],
|
||||||
)
|
)
|
||||||
|
|
||||||
# special case of RoundModulation/RoundWireless (is a single zone system)
|
# special case of RoundModulation/RoundWireless (is a single zone system)
|
||||||
@ -148,9 +147,12 @@ class EvoZone(EvoChild, EvoClimateDevice):
|
|||||||
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._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)
|
||||||
|
if evo_broker.client_v1:
|
||||||
|
self._precision = PRECISION_TENTHS
|
||||||
|
else:
|
||||||
|
self._precision = self._evo_device.setpointCapabilities["valueResolution"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Evohome",
|
"name": "Evohome",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"evohome-async==0.3.3b4"
|
"evohome-async==0.3.3b5"
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@zxdavb"]
|
"codeowners": ["@zxdavb"]
|
||||||
|
@ -7,7 +7,7 @@ from homeassistant.components.water_heater import (
|
|||||||
SUPPORT_OPERATION_MODE,
|
SUPPORT_OPERATION_MODE,
|
||||||
WaterHeaterDevice,
|
WaterHeaterDevice,
|
||||||
)
|
)
|
||||||
from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON
|
from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON
|
||||||
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
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ class EvoDHW(EvoChild, WaterHeaterDevice):
|
|||||||
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_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE
|
||||||
self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE
|
self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -477,7 +477,7 @@ eternalegypt==0.0.10
|
|||||||
# evdev==0.6.1
|
# evdev==0.6.1
|
||||||
|
|
||||||
# homeassistant.components.evohome
|
# homeassistant.components.evohome
|
||||||
evohome-async==0.3.3b4
|
evohome-async==0.3.3b5
|
||||||
|
|
||||||
# homeassistant.components.dlib_face_detect
|
# homeassistant.components.dlib_face_detect
|
||||||
# homeassistant.components.dlib_face_identify
|
# homeassistant.components.dlib_face_identify
|
||||||
|
Loading…
x
Reference in New Issue
Block a user