mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 02:24:46 -05:00
Add config flow to evohome
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
Generated
+1
@@ -223,6 +223,7 @@ FLOWS = {
|
||||
"eufylife_ble",
|
||||
"eurotronic_cometblue",
|
||||
"evil_genius_labs",
|
||||
"evohome",
|
||||
"ezviz",
|
||||
"faa_delays",
|
||||
"fastdotcom",
|
||||
|
||||
@@ -2993,7 +2993,7 @@
|
||||
},
|
||||
"evohome": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Honeywell Total Connect Comfort (Europe)"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user