Add config flow to evohome

This commit is contained in:
G Johansson
2026-07-08 19:09:04 +00:00
parent e563c567a2
commit 56f6164b07
11 changed files with 186 additions and 103 deletions
+39 -33
View File
@@ -10,26 +10,23 @@ from dataclasses import dataclass
import logging
from typing import Final
from config_entries import ConfigEntry
import evohomeasync as ec1
import evohomeasync2 as ec2
import voluptuous as vol
from homeassistant.const import (
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
Platform,
)
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_LOCATION_IDX,
DOMAIN,
EVOHOME_DATA,
PLATFORMS,
SCAN_INTERVAL_DEFAULT,
SCAN_INTERVAL_MINIMUM,
)
@@ -65,13 +62,34 @@ class EvoData:
tcs: ec2.ControlSystem
type EvohomeConfigEntry = ConfigEntry[EvoData]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Evohome integration."""
if DOMAIN in config:
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config[DOMAIN],
)
if result["type"] is FlowResultType.CREATE_ENTRY:
# issue success
pass
else:
pass
# issue failure
return True
async def async_setup_entry(hass: HomeAssistant, entry: EvohomeConfigEntry) -> bool:
"""Set up Evohome from a config entry."""
token_manager = TokenManager(
hass,
config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
entry.data[CONF_USERNAME],
entry.data[CONF_PASSWORD],
async_get_clientsession(hass),
)
coordinator = EvoDataUpdateCoordinator(
@@ -79,37 +97,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_LOGGER,
ec2.EvohomeClient(token_manager),
name=f"{DOMAIN}_coordinator",
update_interval=config[DOMAIN][CONF_SCAN_INTERVAL],
location_idx=config[DOMAIN][CONF_LOCATION_IDX],
update_interval=SCAN_INTERVAL_DEFAULT,
location_idx=entry.data[CONF_LOCATION_IDX],
client_v1=ec1.EvohomeClient(token_manager),
)
await coordinator.async_register_shutdown()
await coordinator.async_first_refresh()
if not coordinator.last_update_success:
_LOGGER.error(f"Failed to fetch initial data: {coordinator.last_exception}") # noqa: G004
return False
assert coordinator.tcs is not None # mypy
hass.data[EVOHOME_DATA] = EvoData(
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = EvoData(
coordinator=coordinator,
loc_idx=coordinator.loc_idx,
tcs=coordinator.tcs,
)
hass.async_create_task(
async_load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
)
hass.async_create_task(
async_load_platform(hass, Platform.BUTTON, DOMAIN, {}, config)
)
if coordinator.tcs.hotwater:
hass.async_create_task(
async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config)
)
setup_service_functions(hass, coordinator)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: EvohomeConfigEntry) -> bool:
"""Unload Evohome config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+8 -13
View File
@@ -7,28 +7,23 @@ import evohomeasync2 as evo
from homeassistant.components.button import ButtonEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import EVOHOME_DATA
from . import EvohomeConfigEntry
from .coordinator import EvoDataUpdateCoordinator
from .entity import is_valid_zone, unique_zone_id
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
_: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: EvohomeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the button platform for Evohome."""
"""Set up Evohome button platform."""
if discovery_info is None:
return
coordinator = hass.data[EVOHOME_DATA].coordinator
tcs = hass.data[EVOHOME_DATA].tcs
coordinator = entry.runtime_data.coordinator
tcs = entry.runtime_data.tcs
entities: list[EvoResetButtonBase] = [EvoResetSystemButton(coordinator, tcs)]
+9 -21
View File
@@ -31,11 +31,11 @@ from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemper
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN, EVOHOME_DATA, RESET_BREAKS_IN_HA_VERSION, EvoService
from . import EvohomeConfigEntry
from .const import DOMAIN, RESET_BREAKS_IN_HA_VERSION, EvoService
from .coordinator import EvoDataUpdateCoordinator
from .entity import EvoChild, EvoEntity, is_valid_zone, unique_zone_id
from .helpers import async_create_deprecation_issue_once
@@ -63,27 +63,15 @@ EVO_PRESET_TO_HA = {
HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()}
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
_: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: EvohomeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the climate platform for Evohome."""
"""Set up Evohome climate platform."""
if discovery_info is None:
return
coordinator = hass.data[EVOHOME_DATA].coordinator
tcs = hass.data[EVOHOME_DATA].tcs
_LOGGER.debug(
"Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)",
tcs.model,
tcs.id,
tcs.location.name,
coordinator.loc_idx,
)
coordinator = entry.runtime_data.coordinator
tcs = entry.runtime_data.tcs
entities: list[EvoController | EvoZone] = [EvoController(coordinator, tcs)]
@@ -0,0 +1,89 @@
"""Adds config flow for Evohome integration."""
from typing import Any, override
import evohomeasync2 as ec2
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import CONF_LOCATION_IDX, DOMAIN
from .storage import TokenManager
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(type=TextSelectorType.EMAIL, autocomplete="username")
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD, autocomplete="current-password"
)
),
vol.Optional(CONF_LOCATION_IDX, default=0): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
),
}
)
async def validate_api(
hass: HomeAssistant, username: str, password: str
) -> dict[str, Any]:
"""Validate the API key."""
errors: dict[str, str] = {}
token_manager = TokenManager(
hass,
username,
password,
async_get_clientsession(hass),
)
client_v2 = ec2.EvohomeClient(token_manager)
try:
await client_v2.update(dont_update_status=True) # only config for now
except ec2.EvohomeError:
errors["base"] = "cannot_connect"
return errors
class EvohomeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Evohome integration."""
VERSION = 1
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step."""
errors: dict[str, str] = {}
if user_input:
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
errors = await validate_api(
self.hass, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
if not errors:
return self.async_create_entry(
title=user_input[CONF_USERNAME],
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
errors=errors,
)
@@ -4,11 +4,18 @@ from datetime import timedelta
from enum import StrEnum, unique
from typing import TYPE_CHECKING, Final
from homeassistant.const import Platform
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from . import EvoData
PLATFORMS = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.WATER_HEATER,
]
DOMAIN: Final = "evohome"
EVOHOME_DATA: HassKey[EvoData] = HassKey(DOMAIN)
@@ -65,22 +65,6 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
self._first_refresh_done = False # get schedules only after first refresh
# our version of async_config_entry_first_refresh()...
async def async_first_refresh(self) -> None:
"""Refresh data for the first time when integration is setup.
This integration does not have config flow, so it is inappropriate to
invoke `async_config_entry_first_refresh()`.
"""
# can't replicate `if not await self.__wrap_async_setup():` (is mangled), so...
if not await self._DataUpdateCoordinator__wrap_async_setup(): # type: ignore[attr-defined]
return
await self._async_refresh(
log_failures=False, raise_on_auth_failed=True, raise_on_entry_error=True
)
@override
async def _async_setup(self) -> None:
"""Set up the coordinator.
@@ -2,7 +2,9 @@
"domain": "evohome",
"name": "Honeywell Total Connect Comfort (Europe)",
"codeowners": ["@zxdavb"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/evohome",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
@@ -1,4 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Evohome account already configured"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
"data": {
"location_idx": "Location",
"password": "Password",
"username": "Username"
},
"data_description": {
"location_idx": "Which location to use if your login has access to more than one location.",
"password": "Your Evohome account password.",
"username": "Your Evohome account username."
}
}
}
},
"exceptions": {
"controller_only_service": {
"message": "Only Evohome controllers support the `{service}` action"
@@ -24,11 +24,10 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import EVOHOME_DATA
from . import EvohomeConfigEntry
from .coordinator import EvoDataUpdateCoordinator
from .entity import EvoChild
@@ -40,28 +39,18 @@ HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: EvoDhwState.ON, STATE_OFF: EvoDhwSt
EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""}
async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
_: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: EvohomeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the water heater platform for Evohome."""
"""Set up Evohome water heater platform."""
if discovery_info is None:
return
coordinator = hass.data[EVOHOME_DATA].coordinator
tcs = hass.data[EVOHOME_DATA].tcs
coordinator = entry.runtime_data.coordinator
tcs = entry.runtime_data.tcs
assert tcs.hotwater is not None # mypy check
_LOGGER.debug(
"Adding: DhwController (%s), id=%s",
tcs.hotwater.type,
tcs.hotwater.id,
)
entity = EvoDHW(coordinator, tcs.hotwater)
async_add_entities([entity])
+1
View File
@@ -223,6 +223,7 @@ FLOWS = {
"eufylife_ble",
"eurotronic_cometblue",
"evil_genius_labs",
"evohome",
"ezviz",
"faa_delays",
"fastdotcom",
+1 -1
View File
@@ -2993,7 +2993,7 @@
},
"evohome": {
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Honeywell Total Connect Comfort (Europe)"
},