mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Migrate Intergas InComfort/Intouch Lan2RF gateway YAML to config flow (#118642)
* Add config flow * Make sure the device is polled - refactor * Fix * Add tests config flow * Update test requirements * Ensure dispatcher has a unique signal per heater * Followup on review * Follow up comments * One more docstr * Make specific try blocks and refactoring * Handle import exceptions * Restore removed lines * Move initial heater update in try block * Raise issue failed import * Update test codeowners * Remove entity device info * Remove entity device info * Appy suggestions from code review * Remove broad exception handling from entry setup * Test coverage
This commit is contained in:
parent
aac31059b0
commit
dd1dd4c6a3
@ -596,7 +596,13 @@ omit =
|
||||
homeassistant/components/ifttt/alarm_control_panel.py
|
||||
homeassistant/components/iglo/light.py
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/incomfort/*
|
||||
homeassistant/components/incomfort/__init__.py
|
||||
homeassistant/components/incomfort/binary_sensor.py
|
||||
homeassistant/components/incomfort/climate.py
|
||||
homeassistant/components/incomfort/errors.py
|
||||
homeassistant/components/incomfort/models.py
|
||||
homeassistant/components/incomfort/sensor.py
|
||||
homeassistant/components/incomfort/water_heater.py
|
||||
homeassistant/components/insteon/binary_sensor.py
|
||||
homeassistant/components/insteon/climate.py
|
||||
homeassistant/components/insteon/cover.py
|
||||
|
@ -659,6 +659,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/improv_ble/ @emontnemery
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
/tests/components/incomfort/ @jbouwh
|
||||
/homeassistant/components/influxdb/ @mdegat01
|
||||
/tests/components/influxdb/ @mdegat01
|
||||
/homeassistant/components/inkbird/ @bdraco
|
||||
|
@ -2,24 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from incomfortclient import Gateway as InComfortGateway
|
||||
from incomfortclient import IncomfortError, InvalidHeaterList
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
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.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "incomfort"
|
||||
from .const import DOMAIN
|
||||
from .errors import InConfortTimeout, InConfortUnknownError, NoHeaters, NotFound
|
||||
from .models import DATA_INCOMFORT, async_connect_gateway
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@ -41,35 +40,87 @@ PLATFORMS = (
|
||||
Platform.CLIMATE,
|
||||
)
|
||||
|
||||
INTEGRATION_TITLE = "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
|
||||
|
||||
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Import config entry from configuration.yaml."""
|
||||
if not hass.config_entries.async_entries(DOMAIN):
|
||||
# Start import flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if result["type"] == FlowResultType.ABORT:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
breaks_in_ha_version="2025.1.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.1.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Create an Intergas InComfort/Intouch system."""
|
||||
incomfort_data = hass.data[DOMAIN] = {}
|
||||
|
||||
credentials = dict(hass_config[DOMAIN])
|
||||
hostname = credentials.pop(CONF_HOST)
|
||||
|
||||
client = incomfort_data["client"] = InComfortGateway(
|
||||
hostname, **credentials, session=async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
try:
|
||||
heaters = incomfort_data["heaters"] = list(await client.heaters())
|
||||
except ClientResponseError as err:
|
||||
_LOGGER.warning("Setup failed, check your configuration, message is: %s", err)
|
||||
return False
|
||||
|
||||
for heater in heaters:
|
||||
await heater.update()
|
||||
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, platform, DOMAIN, {}, hass_config)
|
||||
)
|
||||
|
||||
if config := hass_config.get(DOMAIN):
|
||||
hass.async_create_task(_async_import(hass, config))
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
try:
|
||||
data = await async_connect_gateway(hass, dict(entry.data))
|
||||
for heater in data.heaters:
|
||||
await heater.update()
|
||||
except InvalidHeaterList as exc:
|
||||
raise NoHeaters from exc
|
||||
except IncomfortError as exc:
|
||||
if isinstance(exc.message, ClientResponseError):
|
||||
if exc.message.status == 401:
|
||||
raise ConfigEntryAuthFailed("Incorrect credentials") from exc
|
||||
if exc.message.status == 404:
|
||||
raise NotFound from exc
|
||||
raise InConfortUnknownError from exc
|
||||
except TimeoutError as exc:
|
||||
raise InConfortTimeout from exc
|
||||
|
||||
hass.data.setdefault(DATA_INCOMFORT, {entry.entry_id: data})
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
|
||||
|
||||
class IncomfortEntity(Entity):
|
||||
"""Base class for all InComfort entities."""
|
||||
|
||||
@ -77,7 +128,11 @@ class IncomfortEntity(Entity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up a listener when this entity is added to HA."""
|
||||
self.async_on_remove(async_dispatcher_connect(self.hass, DOMAIN, self._refresh))
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, f"{DOMAIN}_{self.unique_id}", self._refresh
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _refresh(self) -> None:
|
||||
|
@ -7,27 +7,23 @@ from typing import Any
|
||||
from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, IncomfortEntity
|
||||
from . import DATA_INCOMFORT, IncomfortEntity
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an InComfort/InTouch binary_sensor device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
client = hass.data[DOMAIN]["client"]
|
||||
heaters = hass.data[DOMAIN]["heaters"]
|
||||
|
||||
async_add_entities([IncomfortFailed(client, h) for h in heaters])
|
||||
"""Set up an InComfort/InTouch binary_sensor entity."""
|
||||
incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id]
|
||||
async_add_entities(
|
||||
IncomfortFailed(incomfort_data.client, h) for h in incomfort_data.heaters
|
||||
)
|
||||
|
||||
|
||||
class IncomfortFailed(IncomfortEntity, BinarySensorEntity):
|
||||
|
@ -15,29 +15,25 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, IncomfortEntity
|
||||
from . import DATA_INCOMFORT, IncomfortEntity
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an InComfort/InTouch climate device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
client = hass.data[DOMAIN]["client"]
|
||||
heaters = hass.data[DOMAIN]["heaters"]
|
||||
|
||||
"""Set up InComfort/InTouch climate devices."""
|
||||
incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id]
|
||||
async_add_entities(
|
||||
[InComfortClimate(client, h, r) for h in heaters for r in h.rooms]
|
||||
InComfortClimate(incomfort_data.client, h, r)
|
||||
for h in incomfort_data.heaters
|
||||
for r in h.rooms
|
||||
)
|
||||
|
||||
|
||||
|
91
homeassistant/components/incomfort/config_flow.py
Normal file
91
homeassistant/components/incomfort/config_flow.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Config flow support for Intergas InComfort integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from incomfortclient import IncomfortError, InvalidHeaterList
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import async_connect_gateway
|
||||
|
||||
TITLE = "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.TEXT)
|
||||
),
|
||||
vol.Optional(CONF_USERNAME): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.TEXT, autocomplete="admin")
|
||||
),
|
||||
vol.Optional(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
ERROR_STATUS_MAPPING: dict[int, tuple[str, str]] = {
|
||||
401: (CONF_PASSWORD, "auth_error"),
|
||||
404: ("base", "not_found"),
|
||||
}
|
||||
|
||||
|
||||
async def async_try_connect_gateway(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> dict[str, str] | None:
|
||||
"""Try to connect to the Lan2RF gateway."""
|
||||
try:
|
||||
await async_connect_gateway(hass, config)
|
||||
except InvalidHeaterList:
|
||||
return {"base": "no_heaters"}
|
||||
except IncomfortError as exc:
|
||||
if isinstance(exc.message, ClientResponseError):
|
||||
scope, error = ERROR_STATUS_MAPPING.get(
|
||||
exc.message.status, ("base", "unknown")
|
||||
)
|
||||
return {scope: error}
|
||||
return {"base": "unknown"}
|
||||
except TimeoutError:
|
||||
return {"base": "timeout_error"}
|
||||
except Exception: # noqa: BLE001
|
||||
return {"base": "unknown"}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow to set up an Intergas InComfort boyler and thermostats."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] | None = None
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
if (
|
||||
errors := await async_try_connect_gateway(self.hass, user_input)
|
||||
) is None:
|
||||
return self.async_create_entry(title=TITLE, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import `incomfort` config entry from configuration.yaml."""
|
||||
errors: dict[str, str] | None = None
|
||||
if (errors := await async_try_connect_gateway(self.hass, import_data)) is None:
|
||||
return self.async_create_entry(title=TITLE, data=import_data)
|
||||
reason = next(iter(errors.items()))[1]
|
||||
return self.async_abort(reason=reason)
|
3
homeassistant/components/incomfort/const.py
Normal file
3
homeassistant/components/incomfort/const.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Constants for Intergas InComfort integration."""
|
||||
|
||||
DOMAIN = "incomfort"
|
32
homeassistant/components/incomfort/errors.py
Normal file
32
homeassistant/components/incomfort/errors.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Exceptions raised by Intergas InComfort integration."""
|
||||
|
||||
from homeassistant.core import DOMAIN
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
|
||||
|
||||
class NotFound(HomeAssistantError):
|
||||
"""Raise exception if no Lan2RF Gateway was found."""
|
||||
|
||||
translation_domain = DOMAIN
|
||||
translation_key = "not_found"
|
||||
|
||||
|
||||
class NoHeaters(ConfigEntryNotReady):
|
||||
"""Raise exception if no heaters are found."""
|
||||
|
||||
translation_domain = DOMAIN
|
||||
translation_key = "no_heaters"
|
||||
|
||||
|
||||
class InConfortTimeout(ConfigEntryNotReady):
|
||||
"""Raise exception if no heaters are found."""
|
||||
|
||||
translation_domain = DOMAIN
|
||||
translation_key = "timeout_error"
|
||||
|
||||
|
||||
class InConfortUnknownError(ConfigEntryNotReady):
|
||||
"""Raise exception if no heaters are found."""
|
||||
|
||||
translation_domain = DOMAIN
|
||||
translation_key = "unknown"
|
@ -2,6 +2,7 @@
|
||||
"domain": "incomfort",
|
||||
"name": "Intergas InComfort/Intouch Lan2RF gateway",
|
||||
"codeowners": ["@jbouwh"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/incomfort",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
|
40
homeassistant/components/incomfort/models.py
Normal file
40
homeassistant/components/incomfort/models.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Models for Intergas InComfort integration."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@dataclass
|
||||
class InComfortData:
|
||||
"""Keep the Intergas InComfort entry data."""
|
||||
|
||||
client: InComfortGateway
|
||||
heaters: list[InComfortHeater] = field(default_factory=list)
|
||||
|
||||
|
||||
DATA_INCOMFORT: HassKey[dict[str, InComfortData]] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_connect_gateway(
|
||||
hass: HomeAssistant,
|
||||
entry_data: dict[str, Any],
|
||||
) -> InComfortData:
|
||||
"""Validate the configuration."""
|
||||
credentials = dict(entry_data)
|
||||
hostname = credentials.pop(CONF_HOST)
|
||||
|
||||
client = InComfortGateway(
|
||||
hostname, **credentials, session=async_get_clientsession(hass)
|
||||
)
|
||||
heaters = await client.heaters()
|
||||
|
||||
return InComfortData(client=client, heaters=heaters)
|
@ -12,13 +12,13 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfPressure, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from . import DOMAIN, IncomfortEntity
|
||||
from . import DATA_INCOMFORT, IncomfortEntity
|
||||
|
||||
INCOMFORT_HEATER_TEMP = "CV Temp"
|
||||
INCOMFORT_PRESSURE = "CV Pressure"
|
||||
@ -59,26 +59,18 @@ SENSOR_TYPES: tuple[IncomfortSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an InComfort/InTouch sensor device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
client = hass.data[DOMAIN]["client"]
|
||||
heaters = hass.data[DOMAIN]["heaters"]
|
||||
|
||||
entities = [
|
||||
IncomfortSensor(client, heater, description)
|
||||
for heater in heaters
|
||||
"""Set up InComfort/InTouch sensor entities."""
|
||||
incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id]
|
||||
async_add_entities(
|
||||
IncomfortSensor(incomfort_data.client, heater, description)
|
||||
for heater in incomfort_data.heaters
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
)
|
||||
|
||||
|
||||
class IncomfortSensor(IncomfortEntity, SensorEntity):
|
||||
|
56
homeassistant/components/incomfort/strings.json
Normal file
56
homeassistant/components/incomfort/strings.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up new Intergas InComfort Lan2RF Gateway, some older systems might not need credentials to be set up. For newer devices authentication is required.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP-address of the Intergas InComfort Lan2RF Gateway.",
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the Lan2RF Gateway or is `intergas` for some older devices."
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"auth_error": "Invalid credentials.",
|
||||
"no_heaters": "No heaters found.",
|
||||
"not_found": "No Lan2RF gateway found.",
|
||||
"timeout_error": "Time out when connection to Lan2RF gateway.",
|
||||
"unknown": "Unknown error when connection to Lan2RF gateway."
|
||||
},
|
||||
"error": {
|
||||
"auth_error": "[%key:component::incomfort::config::abort::auth_error%]",
|
||||
"no_heaters": "[%key:component::incomfort::config::abort::no_heaters%]",
|
||||
"not_found": "[%key:component::incomfort::config::abort::not_found%]",
|
||||
"timeout_error": "[%key:component::incomfort::config::abort::timeout_error%]",
|
||||
"unknown": "[%key:component::incomfort::config::abort::unknown%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"title": "YAML import failed with unknown error",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_auth_error": {
|
||||
"title": "YAML import failed due to an authentication error",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_no_heaters": {
|
||||
"title": "YAML import failed because no heaters were found",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but no heaters were found while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_not_found": {
|
||||
"title": "YAML import failed because no gateway was found",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but no Lan2RF gateway was found while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
},
|
||||
"deprecated_yaml_import_issue_timeout_error": {
|
||||
"title": "YAML import failed because of timeout issues",
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a timeout while connecting to your {integration_title} while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually."
|
||||
}
|
||||
}
|
||||
}
|
@ -9,33 +9,29 @@ from aiohttp import ClientResponseError
|
||||
from incomfortclient import Gateway as InComfortGateway, Heater as InComfortHeater
|
||||
|
||||
from homeassistant.components.water_heater import WaterHeaterEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, IncomfortEntity
|
||||
from . import DATA_INCOMFORT, DOMAIN, IncomfortEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HEATER_ATTRS = ["display_code", "display_text", "is_burning"]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an InComfort/Intouch water_heater device."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
client = hass.data[DOMAIN]["client"]
|
||||
heaters = hass.data[DOMAIN]["heaters"]
|
||||
|
||||
async_add_entities([IncomfortWaterHeater(client, h) for h in heaters])
|
||||
"""Set up an InComfort/InTouch water_heater device."""
|
||||
incomfort_data = hass.data[DATA_INCOMFORT][entry.entry_id]
|
||||
async_add_entities(
|
||||
IncomfortWaterHeater(incomfort_data.client, h) for h in incomfort_data.heaters
|
||||
)
|
||||
|
||||
|
||||
class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity):
|
||||
@ -92,4 +88,4 @@ class IncomfortWaterHeater(IncomfortEntity, WaterHeaterEntity):
|
||||
_LOGGER.warning("Update failed, message is: %s", err)
|
||||
|
||||
else:
|
||||
async_dispatcher_send(self.hass, DOMAIN)
|
||||
async_dispatcher_send(self.hass, f"{DOMAIN}_{self.unique_id}")
|
||||
|
@ -256,6 +256,7 @@ FLOWS = {
|
||||
"imap",
|
||||
"imgw_pib",
|
||||
"improv_ble",
|
||||
"incomfort",
|
||||
"inkbird",
|
||||
"insteon",
|
||||
"intellifire",
|
||||
|
@ -2809,7 +2809,7 @@
|
||||
"incomfort": {
|
||||
"name": "Intergas InComfort/Intouch Lan2RF gateway",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"indianamichiganpower": {
|
||||
|
@ -935,6 +935,9 @@ ifaddr==0.2.0
|
||||
# homeassistant.components.imgw_pib
|
||||
imgw_pib==1.0.1
|
||||
|
||||
# homeassistant.components.incomfort
|
||||
incomfort-client==0.5.0
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb-client==1.24.0
|
||||
|
||||
|
1
tests/components/incomfort/__init__.py
Normal file
1
tests/components/incomfort/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Intergas InComfort integration."""
|
94
tests/components/incomfort/conftest.py
Normal file
94
tests/components/incomfort/conftest.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Fixtures for Intergas InComfort integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.incomfort.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_heater_status() -> dict[str, Any]:
|
||||
"""Mock heater status."""
|
||||
return {
|
||||
"display_code": 126,
|
||||
"display_text": "standby",
|
||||
"fault_code": None,
|
||||
"is_burning": False,
|
||||
"is_failed": False,
|
||||
"is_pumping": False,
|
||||
"is_tapping": False,
|
||||
"heater_temp": 35.34,
|
||||
"tap_temp": 30.21,
|
||||
"pressure": 1.86,
|
||||
"serial_no": "2404c08648",
|
||||
"nodenr": 249,
|
||||
"rf_message_rssi": 30,
|
||||
"rfstatus_cntr": 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_room_status() -> dict[str, Any]:
|
||||
"""Mock room status."""
|
||||
return {"room_temp": 21.42, "setpoint": 18.0, "override": 18.0}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_incomfort(
|
||||
hass: HomeAssistant,
|
||||
mock_heater_status: dict[str, Any],
|
||||
mock_room_status: dict[str, Any],
|
||||
) -> Generator[MagicMock, None]:
|
||||
"""Mock the InComfort gateway client."""
|
||||
|
||||
class MockRoom:
|
||||
"""Mocked InComfort room class."""
|
||||
|
||||
override: float
|
||||
room_no: int
|
||||
room_temp: float
|
||||
setpoint: float
|
||||
status: dict[str, Any]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize mocked room."""
|
||||
self.override = mock_room_status["override"]
|
||||
self.room_no = 1
|
||||
self.room_temp = mock_room_status["room_temp"]
|
||||
self.setpoint = mock_room_status["setpoint"]
|
||||
self.status = mock_room_status
|
||||
|
||||
class MockHeater:
|
||||
"""Mocked InComfort heater class."""
|
||||
|
||||
serial_no: str
|
||||
status: dict[str, Any]
|
||||
rooms: list[MockRoom]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize mocked heater."""
|
||||
self.serial_no = "c0ffeec0ffee"
|
||||
|
||||
async def update(self) -> None:
|
||||
self.status = mock_heater_status
|
||||
self.rooms = [MockRoom]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.incomfort.models.InComfortGateway", MagicMock()
|
||||
) as patch_gateway:
|
||||
patch_gateway().heaters = AsyncMock()
|
||||
patch_gateway().heaters.return_value = [MockHeater()]
|
||||
yield patch_gateway
|
163
tests/components/incomfort/test_config_flow.py
Normal file
163
tests/components/incomfort/test_config_flow.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""Tests for the Intergas InComfort config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from incomfortclient import IncomfortError, InvalidHeaterList
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.incomfort import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_CONFIG = {
|
||||
"host": "192.168.1.12",
|
||||
"username": "admin",
|
||||
"password": "verysecret",
|
||||
}
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock
|
||||
) -> None:
|
||||
"""Test we get the full form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_import(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_incomfort: MagicMock
|
||||
) -> None:
|
||||
"""Test we van import from YAML."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exc", "abort_reason"),
|
||||
[
|
||||
(IncomfortError(ClientResponseError(None, None, status=401)), "auth_error"),
|
||||
(IncomfortError(ClientResponseError(None, None, status=404)), "not_found"),
|
||||
(IncomfortError(ClientResponseError(None, None, status=500)), "unknown"),
|
||||
(IncomfortError, "unknown"),
|
||||
(InvalidHeaterList, "no_heaters"),
|
||||
(ValueError, "unknown"),
|
||||
(TimeoutError, "timeout_error"),
|
||||
],
|
||||
)
|
||||
async def test_import_fails(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_incomfort: MagicMock,
|
||||
exc: Exception,
|
||||
abort_reason: str,
|
||||
) -> None:
|
||||
"""Test YAML import fails."""
|
||||
mock_incomfort().heaters.side_effect = exc
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == abort_reason
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_entry_already_configured(hass: HomeAssistant) -> None:
|
||||
"""Test aborting if the entry is already configured."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: MOCK_CONFIG[CONF_HOST],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exc", "error", "base"),
|
||||
[
|
||||
(
|
||||
IncomfortError(ClientResponseError(None, None, status=401)),
|
||||
"auth_error",
|
||||
CONF_PASSWORD,
|
||||
),
|
||||
(
|
||||
IncomfortError(ClientResponseError(None, None, status=404)),
|
||||
"not_found",
|
||||
"base",
|
||||
),
|
||||
(
|
||||
IncomfortError(ClientResponseError(None, None, status=500)),
|
||||
"unknown",
|
||||
"base",
|
||||
),
|
||||
(IncomfortError, "unknown", "base"),
|
||||
(ValueError, "unknown", "base"),
|
||||
(TimeoutError, "timeout_error", "base"),
|
||||
(InvalidHeaterList, "no_heaters", "base"),
|
||||
],
|
||||
)
|
||||
async def test_form_validation(
|
||||
hass: HomeAssistant,
|
||||
mock_incomfort: MagicMock,
|
||||
exc: Exception,
|
||||
error: str,
|
||||
base: str,
|
||||
) -> None:
|
||||
"""Test form validation."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Simulate issue and retry
|
||||
mock_incomfort().heaters.side_effect = exc
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {
|
||||
base: error,
|
||||
}
|
||||
|
||||
# Fix the issue and retry
|
||||
mock_incomfort().heaters.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_CONFIG
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert "errors" not in result
|
Loading…
x
Reference in New Issue
Block a user