mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 08:17:08 +00:00
Make evohome strictly typed (#106012)
* initial commit * return to conventional approach * add type hint for wrapper * use walrus operator
This commit is contained in:
parent
2b65fb22d3
commit
aa9f00099d
@ -126,6 +126,7 @@ homeassistant.components.energy.*
|
|||||||
homeassistant.components.esphome.*
|
homeassistant.components.esphome.*
|
||||||
homeassistant.components.event.*
|
homeassistant.components.event.*
|
||||||
homeassistant.components.evil_genius_labs.*
|
homeassistant.components.evil_genius_labs.*
|
||||||
|
homeassistant.components.evohome.*
|
||||||
homeassistant.components.faa_delays.*
|
homeassistant.components.faa_delays.*
|
||||||
homeassistant.components.fan.*
|
homeassistant.components.fan.*
|
||||||
homeassistant.components.fastdotcom.*
|
homeassistant.components.fastdotcom.*
|
||||||
|
@ -4,15 +4,16 @@ Such systems include evohome, Round Thermostat, and others.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime as dt, timedelta
|
from collections.abc import Awaitable
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import evohomeasync
|
import evohomeasync as ev1
|
||||||
from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP
|
from evohomeasync.schema import SZ_ID, SZ_SESSION_ID, SZ_TEMP
|
||||||
import evohomeasync2
|
import evohomeasync2 as evo
|
||||||
from evohomeasync2.schema.const import (
|
from evohomeasync2.schema.const import (
|
||||||
SZ_ALLOWED_SYSTEM_MODES,
|
SZ_ALLOWED_SYSTEM_MODES,
|
||||||
SZ_AUTO_WITH_RESET,
|
SZ_AUTO_WITH_RESET,
|
||||||
@ -112,15 +113,15 @@ SET_ZONE_OVERRIDE_SCHEMA = vol.Schema(
|
|||||||
# system mode schemas are built dynamically, below
|
# system mode schemas are built dynamically, below
|
||||||
|
|
||||||
|
|
||||||
def _dt_local_to_aware(dt_naive: dt) -> dt:
|
def _dt_local_to_aware(dt_naive: datetime) -> datetime:
|
||||||
dt_aware = dt_util.now() + (dt_naive - dt.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 _dt_aware_to_naive(dt_aware: dt) -> dt:
|
def _dt_aware_to_naive(dt_aware: datetime) -> datetime:
|
||||||
dt_naive = dt.now() + (dt_aware - dt_util.now())
|
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)
|
||||||
@ -157,12 +158,12 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _handle_exception(err) -> None:
|
def _handle_exception(err: evo.RequestFailed) -> None:
|
||||||
"""Return False if the exception can't be ignored."""
|
"""Return False if the exception can't be ignored."""
|
||||||
try:
|
try:
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
except evohomeasync2.AuthenticationFailed:
|
except evo.AuthenticationFailed:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
(
|
(
|
||||||
"Failed to authenticate with the vendor's server. Check your username"
|
"Failed to authenticate with the vendor's server. Check your username"
|
||||||
@ -173,7 +174,7 @@ def _handle_exception(err) -> None:
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
except evohomeasync2.RequestFailed:
|
except evo.RequestFailed:
|
||||||
if err.status is None:
|
if err.status is None:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
(
|
(
|
||||||
@ -206,7 +207,7 @@ def _handle_exception(err) -> None:
|
|||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Create a (EMEA/EU-based) Honeywell TCC system."""
|
"""Create a (EMEA/EU-based) Honeywell TCC system."""
|
||||||
|
|
||||||
async def load_auth_tokens(store) -> tuple[dict[str, str | dt], dict[str, str]]:
|
async def load_auth_tokens(store: Store) -> tuple[dict, dict | None]:
|
||||||
app_storage = await store.async_load()
|
app_storage = await store.async_load()
|
||||||
tokens = dict(app_storage or {})
|
tokens = dict(app_storage or {})
|
||||||
|
|
||||||
@ -227,16 +228,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
|
store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY)
|
||||||
tokens, user_data = await load_auth_tokens(store)
|
tokens, user_data = await load_auth_tokens(store)
|
||||||
|
|
||||||
client_v2 = evohomeasync2.EvohomeClient(
|
client_v2 = evo.EvohomeClient(
|
||||||
config[DOMAIN][CONF_USERNAME],
|
config[DOMAIN][CONF_USERNAME],
|
||||||
config[DOMAIN][CONF_PASSWORD],
|
config[DOMAIN][CONF_PASSWORD],
|
||||||
**tokens, # type: ignore[arg-type]
|
**tokens,
|
||||||
session=async_get_clientsession(hass),
|
session=async_get_clientsession(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client_v2.login()
|
await client_v2.login()
|
||||||
except evohomeasync2.AuthenticationFailed as err:
|
except evo.AuthenticationFailed as err:
|
||||||
_handle_exception(err)
|
_handle_exception(err)
|
||||||
return False
|
return False
|
||||||
finally:
|
finally:
|
||||||
@ -268,7 +269,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
_config[GWS][0][TCS] = loc_config[GWS][0][TCS]
|
_config[GWS][0][TCS] = loc_config[GWS][0][TCS]
|
||||||
_LOGGER.debug("Config = %s", _config)
|
_LOGGER.debug("Config = %s", _config)
|
||||||
|
|
||||||
client_v1 = evohomeasync.EvohomeClient(
|
client_v1 = ev1.EvohomeClient(
|
||||||
client_v2.username,
|
client_v2.username,
|
||||||
client_v2.password,
|
client_v2.password,
|
||||||
session_id=user_data.get(SZ_SESSION_ID) if user_data else None, # STORAGE_VER 1
|
session_id=user_data.get(SZ_SESSION_ID) if user_data else None, # STORAGE_VER 1
|
||||||
@ -301,7 +302,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def setup_service_functions(hass: HomeAssistant, broker):
|
def setup_service_functions(hass: HomeAssistant, broker: EvoBroker) -> None:
|
||||||
"""Set up the service handlers for the system/zone operating modes.
|
"""Set up the service handlers for the system/zone operating modes.
|
||||||
|
|
||||||
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
|
Not all Honeywell TCC-compatible systems support all operating modes. In addition,
|
||||||
@ -401,7 +402,7 @@ def setup_service_functions(hass: HomeAssistant, broker):
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SVC_SET_SYSTEM_MODE,
|
SVC_SET_SYSTEM_MODE,
|
||||||
set_system_mode,
|
set_system_mode,
|
||||||
schema=vol.Any(*system_mode_schemas),
|
schema=vol.Schema(vol.Any(*system_mode_schemas)),
|
||||||
)
|
)
|
||||||
|
|
||||||
# The zone modes are consistent across all systems and use the same schema
|
# The zone modes are consistent across all systems and use the same schema
|
||||||
@ -425,8 +426,8 @@ class EvoBroker:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
client: evohomeasync2.EvohomeClient,
|
client: evo.EvohomeClient,
|
||||||
client_v1: evohomeasync.EvohomeClient | None,
|
client_v1: ev1.EvohomeClient | None,
|
||||||
store: Store[dict[str, Any]],
|
store: Store[dict[str, Any]],
|
||||||
params: ConfigType,
|
params: ConfigType,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -438,11 +439,11 @@ class EvoBroker:
|
|||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
loc_idx = params[CONF_LOCATION_IDX]
|
loc_idx = params[CONF_LOCATION_IDX]
|
||||||
|
self._location: evo.Location = client.locations[loc_idx]
|
||||||
|
|
||||||
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
self.config = client.installation_info[loc_idx][GWS][0][TCS][0]
|
||||||
self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0]
|
self.tcs: evo.ControlSystem = self._location._gateways[0]._control_systems[0]
|
||||||
self.tcs_utc_offset = timedelta(
|
self.tcs_utc_offset = timedelta(minutes=self._location.timeZone[UTC_OFFSET])
|
||||||
minutes=client.locations[loc_idx].timeZone[UTC_OFFSET]
|
|
||||||
)
|
|
||||||
self.temps: dict[str, float | None] = {}
|
self.temps: dict[str, float | None] = {}
|
||||||
|
|
||||||
async def save_auth_tokens(self) -> None:
|
async def save_auth_tokens(self) -> None:
|
||||||
@ -461,38 +462,46 @@ class EvoBroker:
|
|||||||
|
|
||||||
if self.client_v1:
|
if self.client_v1:
|
||||||
app_storage[USER_DATA] = { # type: ignore[assignment]
|
app_storage[USER_DATA] = { # type: ignore[assignment]
|
||||||
"sessionId": self.client_v1.broker.session_id,
|
SZ_SESSION_ID: self.client_v1.broker.session_id,
|
||||||
} # this is the schema for STORAGE_VER == 1
|
} # this is the schema for STORAGE_VER == 1
|
||||||
else:
|
else:
|
||||||
app_storage[USER_DATA] = {} # type: ignore[assignment]
|
app_storage[USER_DATA] = {} # type: ignore[assignment]
|
||||||
|
|
||||||
await self._store.async_save(app_storage)
|
await self._store.async_save(app_storage)
|
||||||
|
|
||||||
async def call_client_api(self, api_function, update_state=True) -> Any:
|
async def call_client_api(
|
||||||
|
self,
|
||||||
|
api_function: Awaitable[dict[str, Any] | None],
|
||||||
|
update_state: bool = True,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
"""Call a client API and update the broker state if required."""
|
"""Call a client API and update the broker state if required."""
|
||||||
try:
|
try:
|
||||||
result = await api_function
|
result = await api_function
|
||||||
except evohomeasync2.EvohomeError as err:
|
except evo.RequestFailed as err:
|
||||||
_handle_exception(err)
|
_handle_exception(err)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if update_state: # wait a moment for system to quiesce before updating state
|
if update_state: # wait a moment for system to quiesce before updating state
|
||||||
async_call_later(self.hass, 1, self._update_v2_api_state)
|
async_call_later(self.hass, 1, self._update_v2_api_state)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _update_v1_api_temps(self, *args, **kwargs) -> None:
|
async def _update_v1_api_temps(self) -> None:
|
||||||
"""Get the latest high-precision temperatures of the default Location."""
|
"""Get the latest high-precision temperatures of the default Location."""
|
||||||
|
|
||||||
assert self.client_v1 # mypy check
|
assert self.client_v1 is not None # mypy check
|
||||||
|
|
||||||
session_id = self.client_v1.broker.session_id # maybe receive a new session_id?
|
def get_session_id(client_v1: ev1.EvohomeClient) -> str | None:
|
||||||
|
user_data = client_v1.user_data if client_v1 else None
|
||||||
|
return user_data.get(SZ_SESSION_ID) if user_data else None # type: ignore[return-value]
|
||||||
|
|
||||||
|
session_id = get_session_id(self.client_v1)
|
||||||
|
|
||||||
self.temps = {} # these are now stale, will fall back to v2 temps
|
self.temps = {} # these are now stale, will fall back to v2 temps
|
||||||
try:
|
try:
|
||||||
temps = await self.client_v1.get_temperatures()
|
temps = await self.client_v1.get_temperatures()
|
||||||
|
|
||||||
except evohomeasync.InvalidSchema as exc:
|
except ev1.InvalidSchema as err:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
(
|
(
|
||||||
"Unable to obtain high-precision temperatures. "
|
"Unable to obtain high-precision temperatures. "
|
||||||
@ -500,11 +509,11 @@ class EvoBroker:
|
|||||||
"so the high-precision feature will be disabled until next restart."
|
"so the high-precision feature will be disabled until next restart."
|
||||||
"Message is: %s"
|
"Message is: %s"
|
||||||
),
|
),
|
||||||
exc,
|
err,
|
||||||
)
|
)
|
||||||
self.client_v1 = None
|
self.client_v1 = None
|
||||||
|
|
||||||
except evohomeasync.EvohomeError as exc:
|
except ev1.RequestFailed as err:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
(
|
(
|
||||||
"Unable to obtain the latest high-precision temperatures. "
|
"Unable to obtain the latest high-precision temperatures. "
|
||||||
@ -512,14 +521,11 @@ class EvoBroker:
|
|||||||
"Proceeding without high-precision temperatures for now. "
|
"Proceeding without high-precision temperatures for now. "
|
||||||
"Message is: %s"
|
"Message is: %s"
|
||||||
),
|
),
|
||||||
exc,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if (
|
if str(self.client_v1.location_id) != self._location.locationId:
|
||||||
str(self.client_v1.location_id)
|
|
||||||
!= self.client.locations[self.params[CONF_LOCATION_IDX]].locationId
|
|
||||||
):
|
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"The v2 API's configured location doesn't match "
|
"The v2 API's configured location doesn't match "
|
||||||
"the v1 API's default location (there is more than one location), "
|
"the v1 API's default location (there is more than one location), "
|
||||||
@ -535,15 +541,14 @@ class EvoBroker:
|
|||||||
|
|
||||||
_LOGGER.debug("Temperatures = %s", self.temps)
|
_LOGGER.debug("Temperatures = %s", self.temps)
|
||||||
|
|
||||||
async def _update_v2_api_state(self, *args, **kwargs) -> None:
|
async def _update_v2_api_state(self, *args: Any) -> None:
|
||||||
"""Get the latest modes, temperatures, setpoints of a Location."""
|
"""Get the latest modes, temperatures, setpoints of a Location."""
|
||||||
|
|
||||||
access_token = self.client.access_token # maybe receive a new token?
|
access_token = self.client.access_token # maybe receive a new token?
|
||||||
|
|
||||||
loc_idx = self.params[CONF_LOCATION_IDX]
|
|
||||||
try:
|
try:
|
||||||
status = await self.client.locations[loc_idx].refresh_status()
|
status = await self._location.refresh_status()
|
||||||
except evohomeasync2.EvohomeError as err:
|
except evo.RequestFailed as err:
|
||||||
_handle_exception(err)
|
_handle_exception(err)
|
||||||
else:
|
else:
|
||||||
async_dispatcher_send(self.hass, DOMAIN)
|
async_dispatcher_send(self.hass, DOMAIN)
|
||||||
@ -553,7 +558,7 @@ class EvoBroker:
|
|||||||
if access_token != self.client.access_token:
|
if access_token != self.client.access_token:
|
||||||
await self.save_auth_tokens()
|
await self.save_auth_tokens()
|
||||||
|
|
||||||
async def async_update(self, *args, **kwargs) -> None:
|
async def async_update(self, *args: Any) -> None:
|
||||||
"""Get the latest state data of an entire Honeywell TCC Location.
|
"""Get the latest state data of an entire Honeywell TCC Location.
|
||||||
|
|
||||||
This includes state data for a Controller and all its child devices, such as the
|
This includes state data for a Controller and all its child devices, such as the
|
||||||
@ -575,9 +580,11 @@ class EvoDevice(Entity):
|
|||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
_evo_id: str
|
def __init__(
|
||||||
|
self,
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
evo_broker: EvoBroker,
|
||||||
|
evo_device: evo.ControlSystem | evo.HotWater | evo.Zone,
|
||||||
|
) -> 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_broker = evo_broker
|
||||||
@ -629,9 +636,14 @@ class EvoChild(EvoDevice):
|
|||||||
This includes (up to 12) Heating Zones and (optionally) a DHW controller.
|
This includes (up to 12) Heating Zones and (optionally) a DHW controller.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
_evo_id: str # mypy hint
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, evo_broker: EvoBroker, evo_device: evo.HotWater | evo.Zone
|
||||||
|
) -> None:
|
||||||
"""Initialize a evohome Controller (hub)."""
|
"""Initialize a evohome Controller (hub)."""
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
|
||||||
self._schedule: dict[str, Any] = {}
|
self._schedule: dict[str, Any] = {}
|
||||||
self._setpoints: dict[str, Any] = {}
|
self._setpoints: dict[str, Any] = {}
|
||||||
|
|
||||||
@ -639,6 +651,8 @@ class EvoChild(EvoDevice):
|
|||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature of a Zone."""
|
"""Return the current temperature of a Zone."""
|
||||||
|
|
||||||
|
assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check
|
||||||
|
|
||||||
if self._evo_broker.temps.get(self._evo_id) is not None:
|
if self._evo_broker.temps.get(self._evo_id) is not None:
|
||||||
return self._evo_broker.temps[self._evo_id]
|
return self._evo_broker.temps[self._evo_id]
|
||||||
return self._evo_device.temperature
|
return self._evo_device.temperature
|
||||||
@ -650,7 +664,7 @@ class EvoChild(EvoDevice):
|
|||||||
Only Zones & DHW controllers (but not the TCS) can have schedules.
|
Only Zones & DHW controllers (but not the TCS) can have schedules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _dt_evo_to_aware(dt_naive: dt, utc_offset: timedelta) -> dt:
|
def _dt_evo_to_aware(dt_naive: datetime, utc_offset: timedelta) -> datetime:
|
||||||
dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset
|
dt_aware = dt_naive.replace(tzinfo=dt_util.UTC) - utc_offset
|
||||||
return dt_util.as_local(dt_aware)
|
return dt_util.as_local(dt_aware)
|
||||||
|
|
||||||
@ -686,7 +700,7 @@ class EvoChild(EvoDevice):
|
|||||||
switchpoint_time_of_day = dt_util.parse_datetime(
|
switchpoint_time_of_day = dt_util.parse_datetime(
|
||||||
f"{sp_date}T{switchpoint['TimeOfDay']}"
|
f"{sp_date}T{switchpoint['TimeOfDay']}"
|
||||||
)
|
)
|
||||||
assert switchpoint_time_of_day # mypy check
|
assert switchpoint_time_of_day is not None # mypy check
|
||||||
dt_aware = _dt_evo_to_aware(
|
dt_aware = _dt_evo_to_aware(
|
||||||
switchpoint_time_of_day, self._evo_broker.tcs_utc_offset
|
switchpoint_time_of_day, self._evo_broker.tcs_utc_offset
|
||||||
)
|
)
|
||||||
@ -708,7 +722,10 @@ class EvoChild(EvoDevice):
|
|||||||
|
|
||||||
async def _update_schedule(self) -> None:
|
async def _update_schedule(self) -> None:
|
||||||
"""Get the latest schedule, if any."""
|
"""Get the latest schedule, if any."""
|
||||||
self._schedule = await self._evo_broker.call_client_api(
|
|
||||||
|
assert isinstance(self._evo_device, evo.HotWater | evo.Zone) # mypy check
|
||||||
|
|
||||||
|
self._schedule = await self._evo_broker.call_client_api( # type: ignore[assignment]
|
||||||
self._evo_device.get_schedule(), update_state=False
|
self._evo_device.get_schedule(), update_state=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
"""Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime as dt
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import evohomeasync2 as evo
|
||||||
from evohomeasync2.schema.const import (
|
from evohomeasync2.schema.const import (
|
||||||
SZ_ACTIVE_FAULTS,
|
SZ_ACTIVE_FAULTS,
|
||||||
SZ_ALLOWED_SYSTEM_MODES,
|
SZ_ALLOWED_SYSTEM_MODES,
|
||||||
@ -61,6 +62,10 @@ from .const import (
|
|||||||
EVO_TEMPOVER,
|
EVO_TEMPOVER,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import EvoBroker
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW
|
PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW
|
||||||
@ -104,7 +109,7 @@ async def async_setup_platform(
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
broker = hass.data[DOMAIN]["broker"]
|
broker: EvoBroker = hass.data[DOMAIN]["broker"]
|
||||||
|
|
||||||
_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)",
|
||||||
@ -163,16 +168,19 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
|
|
||||||
_attr_preset_modes = list(HA_PRESET_TO_EVO)
|
_attr_preset_modes = list(HA_PRESET_TO_EVO)
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
_evo_device: evo.Zone # mypy hint
|
||||||
|
|
||||||
|
def __init__(self, evo_broker: EvoBroker, evo_device: evo.Zone) -> None:
|
||||||
"""Initialize a Honeywell TCC Zone."""
|
"""Initialize a Honeywell TCC Zone."""
|
||||||
|
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
self._evo_id = evo_device.zoneId
|
||||||
|
|
||||||
if evo_device.modelType.startswith("VisionProWifi"):
|
if evo_device.modelType.startswith("VisionProWifi"):
|
||||||
# this system does not have a distinct ID for the zone
|
# this system does not have a distinct ID for the zone
|
||||||
self._attr_unique_id = f"{evo_device.zoneId}z"
|
self._attr_unique_id = f"{evo_device.zoneId}z"
|
||||||
else:
|
else:
|
||||||
self._attr_unique_id = evo_device.zoneId
|
self._attr_unique_id = evo_device.zoneId
|
||||||
self._evo_id = evo_device.zoneId
|
|
||||||
|
|
||||||
self._attr_name = evo_device.name
|
self._attr_name = evo_device.name
|
||||||
|
|
||||||
@ -197,7 +205,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp)
|
temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp)
|
||||||
|
|
||||||
if ATTR_DURATION_UNTIL in data:
|
if ATTR_DURATION_UNTIL in data:
|
||||||
duration = data[ATTR_DURATION_UNTIL]
|
duration: timedelta = data[ATTR_DURATION_UNTIL]
|
||||||
if duration.total_seconds() == 0:
|
if duration.total_seconds() == 0:
|
||||||
await self._update_schedule()
|
await self._update_schedule()
|
||||||
until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", ""))
|
until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", ""))
|
||||||
@ -232,6 +240,8 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
"""Return the current preset mode, e.g., home, away, temp."""
|
"""Return the current preset mode, e.g., home, away, temp."""
|
||||||
if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF):
|
if self._evo_tcs.system_mode in (EVO_AWAY, EVO_HEATOFF):
|
||||||
return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
|
return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
|
||||||
|
if self._evo_device.mode is None:
|
||||||
|
return None
|
||||||
return EVO_PRESET_TO_HA.get(self._evo_device.mode)
|
return EVO_PRESET_TO_HA.get(self._evo_device.mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -252,6 +262,9 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set a new target temperature."""
|
"""Set a new target temperature."""
|
||||||
|
|
||||||
|
assert self._evo_device.setpointStatus is not None # mypy check
|
||||||
|
|
||||||
temperature = kwargs["temperature"]
|
temperature = kwargs["temperature"]
|
||||||
|
|
||||||
if (until := kwargs.get("until")) is None:
|
if (until := kwargs.get("until")) is None:
|
||||||
@ -300,14 +313,15 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
|||||||
await self._evo_broker.call_client_api(self._evo_device.reset_mode())
|
await self._evo_broker.call_client_api(self._evo_device.reset_mode())
|
||||||
return
|
return
|
||||||
|
|
||||||
temperature = self._evo_device.target_heat_temperature
|
|
||||||
|
|
||||||
if evo_preset_mode == EVO_TEMPOVER:
|
if evo_preset_mode == EVO_TEMPOVER:
|
||||||
await self._update_schedule()
|
await self._update_schedule()
|
||||||
until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", ""))
|
until = dt_util.parse_datetime(self.setpoints.get("next_sp_from", ""))
|
||||||
else: # EVO_PERMOVER
|
else: # EVO_PERMOVER
|
||||||
until = None
|
until = None
|
||||||
|
|
||||||
|
temperature = self._evo_device.target_heat_temperature
|
||||||
|
assert temperature is not None # mypy check
|
||||||
|
|
||||||
until = dt_util.as_utc(until) if until else None
|
until = dt_util.as_utc(until) if until else None
|
||||||
await self._evo_broker.call_client_api(
|
await self._evo_broker.call_client_api(
|
||||||
self._evo_device.set_temperature(temperature, until=until)
|
self._evo_device.set_temperature(temperature, until=until)
|
||||||
@ -334,12 +348,15 @@ class EvoController(EvoClimateEntity):
|
|||||||
_attr_icon = "mdi:thermostat"
|
_attr_icon = "mdi:thermostat"
|
||||||
_attr_precision = PRECISION_TENTHS
|
_attr_precision = PRECISION_TENTHS
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
_evo_device: evo.ControlSystem # mypy hint
|
||||||
|
|
||||||
|
def __init__(self, evo_broker: EvoBroker, evo_device: evo.ControlSystem) -> None:
|
||||||
"""Initialize a Honeywell TCC Controller/Location."""
|
"""Initialize a Honeywell TCC Controller/Location."""
|
||||||
|
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
self._evo_id = evo_device.systemId
|
||||||
|
|
||||||
self._attr_unique_id = evo_device.systemId
|
self._attr_unique_id = evo_device.systemId
|
||||||
self._evo_id = evo_device.systemId
|
|
||||||
self._attr_name = evo_device.location.name
|
self._attr_name = evo_device.location.name
|
||||||
|
|
||||||
modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.config[SZ_ALLOWED_SYSTEM_MODES]]
|
modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.config[SZ_ALLOWED_SYSTEM_MODES]]
|
||||||
@ -371,11 +388,11 @@ class EvoController(EvoClimateEntity):
|
|||||||
|
|
||||||
await self._set_tcs_mode(mode, until=until)
|
await self._set_tcs_mode(mode, until=until)
|
||||||
|
|
||||||
async def _set_tcs_mode(self, mode: str, until: dt | None = None) -> None:
|
async def _set_tcs_mode(self, mode: str, until: datetime | None = None) -> None:
|
||||||
"""Set a Controller to any of its native EVO_* operating modes."""
|
"""Set a Controller to any of its native EVO_* operating modes."""
|
||||||
until = dt_util.as_utc(until) if until else None
|
until = dt_util.as_utc(until) if until else None
|
||||||
await self._evo_broker.call_client_api(
|
await self._evo_broker.call_client_api(
|
||||||
self._evo_tcs.set_mode(mode, until=until)
|
self._evo_tcs.set_mode(mode, until=until) # type: ignore[arg-type]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import evohomeasync2 as evo
|
||||||
from evohomeasync2.schema.const import (
|
from evohomeasync2.schema.const import (
|
||||||
SZ_ACTIVE_FAULTS,
|
SZ_ACTIVE_FAULTS,
|
||||||
SZ_DHW_ID,
|
SZ_DHW_ID,
|
||||||
@ -31,6 +33,10 @@ import homeassistant.util.dt as dt_util
|
|||||||
from . import EvoChild
|
from . import EvoChild
|
||||||
from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER
|
from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import EvoBroker
|
||||||
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATE_AUTO = "auto"
|
STATE_AUTO = "auto"
|
||||||
@ -51,7 +57,9 @@ async def async_setup_platform(
|
|||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
broker = hass.data[DOMAIN]["broker"]
|
broker: EvoBroker = hass.data[DOMAIN]["broker"]
|
||||||
|
|
||||||
|
assert broker.tcs.hotwater is not None # mypy check
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Adding: DhwController (%s), id=%s",
|
"Adding: DhwController (%s), id=%s",
|
||||||
@ -72,12 +80,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
|||||||
_attr_operation_list = list(HA_STATE_TO_EVO)
|
_attr_operation_list = list(HA_STATE_TO_EVO)
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||||
|
|
||||||
def __init__(self, evo_broker, evo_device) -> None:
|
_evo_device: evo.HotWater # mypy hint
|
||||||
|
|
||||||
|
def __init__(self, evo_broker: EvoBroker, evo_device: evo.HotWater) -> None:
|
||||||
"""Initialize an evohome DHW controller."""
|
"""Initialize an evohome DHW controller."""
|
||||||
|
|
||||||
super().__init__(evo_broker, evo_device)
|
super().__init__(evo_broker, evo_device)
|
||||||
|
self._evo_id = evo_device.dhwId
|
||||||
|
|
||||||
self._attr_unique_id = evo_device.dhwId
|
self._attr_unique_id = evo_device.dhwId
|
||||||
self._evo_id = evo_device.dhwId
|
|
||||||
|
|
||||||
self._attr_precision = (
|
self._attr_precision = (
|
||||||
PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE
|
PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE
|
||||||
@ -87,15 +98,19 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self) -> str:
|
def current_operation(self) -> str | None:
|
||||||
"""Return the current operating mode (Auto, On, or Off)."""
|
"""Return the current operating mode (Auto, On, or Off)."""
|
||||||
if self._evo_device.mode == EVO_FOLLOW:
|
if self._evo_device.mode == EVO_FOLLOW:
|
||||||
return STATE_AUTO
|
return STATE_AUTO
|
||||||
return EVO_STATE_TO_HA[self._evo_device.state]
|
if (device_state := self._evo_device.state) is None:
|
||||||
|
return None
|
||||||
|
return EVO_STATE_TO_HA[device_state]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_away_mode_on(self):
|
def is_away_mode_on(self) -> bool | None:
|
||||||
"""Return True if away mode is on."""
|
"""Return True if away mode is on."""
|
||||||
|
if self._evo_device.state is None:
|
||||||
|
return None
|
||||||
is_off = EVO_STATE_TO_HA[self._evo_device.state] == STATE_OFF
|
is_off = EVO_STATE_TO_HA[self._evo_device.state] == STATE_OFF
|
||||||
is_permanent = self._evo_device.mode == EVO_PERMOVER
|
is_permanent = self._evo_device.mode == EVO_PERMOVER
|
||||||
return is_off and is_permanent
|
return is_off and is_permanent
|
||||||
@ -129,11 +144,11 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
|||||||
"""Turn away mode off."""
|
"""Turn away mode off."""
|
||||||
await self._evo_broker.call_client_api(self._evo_device.reset_mode())
|
await self._evo_broker.call_client_api(self._evo_device.reset_mode())
|
||||||
|
|
||||||
async def async_turn_on(self):
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn on."""
|
"""Turn on."""
|
||||||
await self._evo_broker.call_client_api(self._evo_device.set_on())
|
await self._evo_broker.call_client_api(self._evo_device.set_on())
|
||||||
|
|
||||||
async def async_turn_off(self):
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off."""
|
"""Turn off."""
|
||||||
await self._evo_broker.call_client_api(self._evo_device.set_off())
|
await self._evo_broker.call_client_api(self._evo_device.set_off())
|
||||||
|
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -1021,6 +1021,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.evohome.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.faa_delays.*]
|
[mypy-homeassistant.components.faa_delays.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user