Remove dependency on modbus for stiebel_eltron (#136482)

* Remove dependency on modbus for stiebel_eltron

The modbus integration changed its setup, so it is
not possible anymore to have an empty hub.

* Add config flow

* Update pystiebeleltron to v0.1.0

* Fix

* Fix

* Add test for non existing modbus hub

* Fix tests

* Add more tests

* Add missing translation string

* Add test for import failure

* Fix issues from review comments

* Fix issues from review comments

* Mock stiebel eltron client instead of setup_entry

* Update homeassistant/components/stiebel_eltron/__init__.py

* Update homeassistant/components/stiebel_eltron/__init__.py

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
pull/143881/head
Manuel Stahl 2025-04-29 14:57:01 +02:00 committed by GitHub
parent 87107c5a59
commit d7f43bddfa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 716 additions and 67 deletions

3
CODEOWNERS generated
View File

@ -1474,7 +1474,8 @@ build.json @home-assistant/supervisor
/tests/components/steam_online/ @tkdrob
/homeassistant/components/steamist/ @bdraco
/tests/components/steamist/ @bdraco
/homeassistant/components/stiebel_eltron/ @fucm
/homeassistant/components/stiebel_eltron/ @fucm @ThyMYthOS
/tests/components/stiebel_eltron/ @fucm @ThyMYthOS
/homeassistant/components/stookwijzer/ @fwestenberg
/tests/components/stookwijzer/ @fwestenberg
/homeassistant/components/stream/ @hunterjm @uvjustin @allenporter

View File

@ -1,22 +1,29 @@
"""The component for STIEBEL ELTRON heat pumps with ISGWeb Modbus module."""
from datetime import timedelta
import logging
from typing import Any
from pymodbus.client import ModbusTcpClient
from pystiebeleltron import pystiebeleltron
from pystiebeleltron.pystiebeleltron import StiebelEltronAPI
import voluptuous as vol
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME, Platform
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_NAME,
CONF_PORT,
DEVICE_DEFAULT_NAME,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import Throttle
CONF_HUB = "hub"
DEFAULT_HUB = "modbus_hub"
from .const import CONF_HUB, DEFAULT_HUB, DOMAIN
MODBUS_DOMAIN = "modbus"
DOMAIN = "stiebel_eltron"
CONFIG_SCHEMA = vol.Schema(
{
@ -31,39 +38,109 @@ CONFIG_SCHEMA = vol.Schema(
)
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
_PLATFORMS: list[Platform] = [Platform.CLIMATE]
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the STIEBEL ELTRON unit.
async def _async_import(hass: HomeAssistant, config: ConfigType) -> None:
"""Set up the STIEBEL ELTRON component."""
hub_config: dict[str, Any] | None = None
if MODBUS_DOMAIN in config:
for hub in config[MODBUS_DOMAIN]:
if hub[CONF_NAME] == config[DOMAIN][CONF_HUB]:
hub_config = hub
break
if hub_config is None:
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_issue_missing_hub",
breaks_in_ha_version="2025.11.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_missing_hub",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Stiebel Eltron",
},
)
return
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: hub_config[CONF_HOST],
CONF_PORT: hub_config[CONF_PORT],
CONF_NAME: config[DOMAIN][CONF_NAME],
},
)
if (
result.get("type") is FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2025.11.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": "Stiebel Eltron",
},
)
return
Will automatically load climate platform.
"""
name = config[DOMAIN][CONF_NAME]
modbus_client = hass.data[MODBUS_DOMAIN][config[DOMAIN][CONF_HUB]]
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_yaml",
breaks_in_ha_version="2025.9.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Stiebel Eltron",
},
)
hass.data[DOMAIN] = {
"name": name,
"ste_data": StiebelEltronData(name, modbus_client),
}
discovery.load_platform(hass, Platform.CLIMATE, DOMAIN, {}, config)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the STIEBEL ELTRON component."""
if DOMAIN in config:
hass.async_create_task(_async_import(hass, config))
return True
class StiebelEltronData:
"""Get the latest data and update the states."""
type StiebelEltronConfigEntry = ConfigEntry[StiebelEltronAPI]
def __init__(self, name: str, modbus_client: ModbusTcpClient) -> None:
"""Init the STIEBEL ELTRON data object."""
self.api = pystiebeleltron.StiebelEltronAPI(modbus_client, 1)
async def async_setup_entry(
hass: HomeAssistant, entry: StiebelEltronConfigEntry
) -> bool:
"""Set up STIEBEL ELTRON from a config entry."""
client = StiebelEltronAPI(
ModbusTcpClient(entry.data[CONF_HOST], port=entry.data[CONF_PORT]), 1
)
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self) -> None:
"""Update unit data."""
if not self.api.update():
_LOGGER.warning("Modbus read failed")
else:
_LOGGER.debug("Data updated successfully")
success = await hass.async_add_executor_job(client.update)
if not success:
raise ConfigEntryNotReady("Could not connect to device")
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(
hass: HomeAssistant, entry: StiebelEltronConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)

View File

@ -5,6 +5,8 @@ from __future__ import annotations
import logging
from typing import Any
from pystiebeleltron.pystiebeleltron import StiebelEltronAPI
from homeassistant.components.climate import (
PRESET_ECO,
ClimateEntity,
@ -13,10 +15,9 @@ from homeassistant.components.climate import (
)
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 homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN as STE_DOMAIN, StiebelEltronData
from . import StiebelEltronConfigEntry
DEPENDENCIES = ["stiebel_eltron"]
@ -56,17 +57,14 @@ HA_TO_STE_HVAC = {
HA_TO_STE_PRESET = {k: i for i, k in STE_TO_HA_PRESET.items()}
def setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
entry: StiebelEltronConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the StiebelEltron platform."""
name = hass.data[STE_DOMAIN]["name"]
ste_data = hass.data[STE_DOMAIN]["ste_data"]
"""Set up STIEBEL ELTRON climate platform."""
add_entities([StiebelEltron(name, ste_data)], True)
async_add_entities([StiebelEltron(entry.title, entry.runtime_data)], True)
class StiebelEltron(ClimateEntity):
@ -81,7 +79,7 @@ class StiebelEltron(ClimateEntity):
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
def __init__(self, name: str, ste_data: StiebelEltronData) -> None:
def __init__(self, name: str, client: StiebelEltronAPI) -> None:
"""Initialize the unit."""
self._name = name
self._target_temperature: float | int | None = None
@ -89,19 +87,17 @@ class StiebelEltron(ClimateEntity):
self._current_humidity: float | int | None = None
self._operation: str | None = None
self._filter_alarm: bool | None = None
self._force_update: bool = False
self._ste_data = ste_data
self._client = client
def update(self) -> None:
"""Update unit attributes."""
self._ste_data.update(no_throttle=self._force_update)
self._force_update = False
self._client.update()
self._target_temperature = self._ste_data.api.get_target_temp()
self._current_temperature = self._ste_data.api.get_current_temp()
self._current_humidity = self._ste_data.api.get_current_humidity()
self._filter_alarm = self._ste_data.api.get_filter_alarm_status()
self._operation = self._ste_data.api.get_operation()
self._target_temperature = self._client.get_target_temp()
self._current_temperature = self._client.get_current_temp()
self._current_humidity = self._client.get_current_humidity()
self._filter_alarm = self._client.get_filter_alarm_status()
self._operation = self._client.get_operation()
_LOGGER.debug(
"Update %s, current temp: %s", self._name, self._current_temperature
@ -170,20 +166,17 @@ class StiebelEltron(ClimateEntity):
return
new_mode = HA_TO_STE_HVAC.get(hvac_mode)
_LOGGER.debug("set_hvac_mode: %s -> %s", self._operation, new_mode)
self._ste_data.api.set_operation(new_mode)
self._force_update = True
self._client.set_operation(new_mode)
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temperature = kwargs.get(ATTR_TEMPERATURE)
if target_temperature is not None:
_LOGGER.debug("set_temperature: %s", target_temperature)
self._ste_data.api.set_target_temp(target_temperature)
self._force_update = True
self._client.set_target_temp(target_temperature)
def set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
new_mode = HA_TO_STE_PRESET.get(preset_mode)
_LOGGER.debug("set_hvac_mode: %s -> %s", self._operation, new_mode)
self._ste_data.api.set_operation(new_mode)
self._force_update = True
self._client.set_operation(new_mode)

View File

@ -0,0 +1,82 @@
"""Config flow for the STIEBEL ELTRON integration."""
from __future__ import annotations
import logging
from typing import Any
from pymodbus.client import ModbusTcpClient
from pystiebeleltron.pystiebeleltron import StiebelEltronAPI
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from .const import DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
class StiebelEltronConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for STIEBEL ELTRON."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
client = StiebelEltronAPI(
ModbusTcpClient(user_input[CONF_HOST], port=user_input[CONF_PORT]), 1
)
try:
success = await self.hass.async_add_executor_job(client.update)
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
if not success:
errors["base"] = "cannot_connect"
if not errors:
return self.async_create_entry(title="Stiebel Eltron", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
}
),
errors=errors,
)
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle import."""
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
client = StiebelEltronAPI(
ModbusTcpClient(user_input[CONF_HOST], port=user_input[CONF_PORT]), 1
)
try:
success = await self.hass.async_add_executor_job(client.update)
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
if not success:
return self.async_abort(reason="cannot_connect")
return self.async_create_entry(
title=user_input[CONF_NAME],
data={
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
},
)

View File

@ -0,0 +1,8 @@
"""Constants for the STIEBEL ELTRON integration."""
DOMAIN = "stiebel_eltron"
CONF_HUB = "hub"
DEFAULT_HUB = "modbus_hub"
DEFAULT_PORT = 502

View File

@ -1,11 +1,10 @@
{
"domain": "stiebel_eltron",
"name": "STIEBEL ELTRON",
"codeowners": ["@fucm"],
"dependencies": ["modbus"],
"codeowners": ["@fucm", "@ThyMYthOS"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/stiebel_eltron",
"iot_class": "local_polling",
"loggers": ["pymodbus", "pystiebeleltron"],
"quality_scale": "legacy",
"requirements": ["pystiebeleltron==0.0.1.dev2"]
"requirements": ["pystiebeleltron==0.1.0"]
}

View File

@ -0,0 +1,43 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Stiebel Eltron device.",
"port": "The port of your Stiebel Eltron device."
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"issues": {
"deprecated_yaml": {
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove both the `{domain}` and the relevant Modbus configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_yaml_import_issue_cannot_connect": {
"title": "YAML import failed due to a connection error",
"description": "Configuring {integration_title} using YAML is being removed but there was a connect 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_missing_hub": {
"title": "YAML import failed due to incomplete config",
"description": "Configuring {integration_title} using YAML is being removed but the configuration was not complete, thus we could not import your 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_unknown": {
"title": "YAML import failed due to an 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."
}
}
}

View File

@ -603,6 +603,7 @@ FLOWS = {
"starlink",
"steam_online",
"steamist",
"stiebel_eltron",
"stookwijzer",
"streamlabswater",
"subaru",

View File

@ -6269,7 +6269,7 @@
"stiebel_eltron": {
"name": "STIEBEL ELTRON",
"integration_type": "hub",
"config_flow": false,
"config_flow": true,
"iot_class": "local_polling"
},
"stookwijzer": {

2
requirements_all.txt generated
View File

@ -2359,7 +2359,7 @@ pyspeex-noise==1.0.2
pysqueezebox==0.12.0
# homeassistant.components.stiebel_eltron
pystiebeleltron==0.0.1.dev2
pystiebeleltron==0.1.0
# homeassistant.components.suez_water
pysuezV2==2.0.4

View File

@ -1931,6 +1931,9 @@ pyspeex-noise==1.0.2
# homeassistant.components.squeezebox
pysqueezebox==0.12.0
# homeassistant.components.stiebel_eltron
pystiebeleltron==0.1.0
# homeassistant.components.suez_water
pysuezV2==2.0.4

View File

@ -0,0 +1 @@
"""Tests for the STIEBEL ELTRON integration."""

View File

@ -0,0 +1,55 @@
"""Common fixtures for the STIEBEL ELTRON tests."""
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.stiebel_eltron import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from tests.common import MockConfigEntry
@pytest.fixture
def mock_stiebel_eltron_client() -> Generator[MagicMock]:
"""Mock a stiebel eltron client."""
with (
patch(
"homeassistant.components.stiebel_eltron.StiebelEltronAPI",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.stiebel_eltron.config_flow.StiebelEltronAPI",
new=mock_client,
),
):
client = mock_client.return_value
client.update.return_value = True
yield client
@pytest.fixture(autouse=True)
def mock_modbus() -> Generator[MagicMock]:
"""Mock a modbus client."""
with (
patch(
"homeassistant.components.stiebel_eltron.ModbusTcpClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.stiebel_eltron.config_flow.ModbusTcpClient",
new=mock_client,
),
):
yield mock_client
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="Stiebel Eltron",
data={CONF_HOST: "1.1.1.1", CONF_PORT: 502},
)

View File

@ -0,0 +1,209 @@
"""Test the STIEBEL ELTRON config flow."""
from unittest.mock import MagicMock
import pytest
from homeassistant.components.stiebel_eltron.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_stiebel_eltron_client")
async def test_full_flow(hass: HomeAssistant) -> None:
"""Test the full flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Stiebel Eltron"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
}
async def test_form_cannot_connect(
hass: HomeAssistant,
mock_stiebel_eltron_client: MagicMock,
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_stiebel_eltron_client.update.return_value = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_stiebel_eltron_client.update.return_value = True
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_form_unknown_exception(
hass: HomeAssistant,
mock_stiebel_eltron_client: MagicMock,
) -> None:
"""Test we handle cannot connect error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
mock_stiebel_eltron_client.update.side_effect = Exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "unknown"}
mock_stiebel_eltron_client.update.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we handle already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("mock_stiebel_eltron_client")
async def test_import(hass: HomeAssistant) -> None:
"""Test import step."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
CONF_NAME: "Stiebel Eltron",
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Stiebel Eltron"
assert result["data"] == {
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
}
async def test_import_cannot_connect(
hass: HomeAssistant,
mock_stiebel_eltron_client: MagicMock,
) -> None:
"""Test we handle cannot connect error."""
mock_stiebel_eltron_client.update.return_value = False
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
CONF_NAME: "Stiebel Eltron",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_import_unknown_exception(
hass: HomeAssistant,
mock_stiebel_eltron_client: MagicMock,
) -> None:
"""Test we handle cannot connect error."""
mock_stiebel_eltron_client.update.side_effect = Exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
CONF_NAME: "Stiebel Eltron",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "unknown"
async def test_import_already_configured(
hass: HomeAssistant, mock_config_entry: MockConfigEntry
) -> None:
"""Test we handle already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
CONF_NAME: "Stiebel Eltron",
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,177 @@
"""Tests for the STIEBEL ELTRON integration."""
from unittest.mock import AsyncMock
import pytest
from homeassistant.components.stiebel_eltron.const import CONF_HUB, DEFAULT_HUB, DOMAIN
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.setup import async_setup_component
@pytest.mark.usefixtures("mock_stiebel_eltron_client")
async def test_async_setup_success(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test successful async_setup."""
config = {
DOMAIN: {
CONF_NAME: "Stiebel Eltron",
CONF_HUB: DEFAULT_HUB,
},
"modbus": [
{
CONF_NAME: DEFAULT_HUB,
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
}
],
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Verify the issue is created
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml")
assert issue
assert issue.active is True
assert issue.severity == ir.IssueSeverity.WARNING
@pytest.mark.usefixtures("mock_stiebel_eltron_client")
async def test_async_setup_already_configured(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
mock_config_entry,
) -> None:
"""Test we handle already configured."""
mock_config_entry.add_to_hass(hass)
config = {
DOMAIN: {
CONF_NAME: "Stiebel Eltron",
CONF_HUB: DEFAULT_HUB,
},
"modbus": [
{
CONF_NAME: DEFAULT_HUB,
CONF_HOST: "1.1.1.1",
CONF_PORT: 502,
}
],
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Verify the issue is created
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_yaml")
assert issue
assert issue.active is True
assert issue.severity == ir.IssueSeverity.WARNING
async def test_async_setup_with_non_existing_hub(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test async_setup with non-existing modbus hub."""
config = {
DOMAIN: {
CONF_NAME: "Stiebel Eltron",
CONF_HUB: "non_existing_hub",
},
}
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Verify the issue is created
issue = issue_registry.async_get_issue(
DOMAIN, "deprecated_yaml_import_issue_missing_hub"
)
assert issue
assert issue.active is True
assert issue.is_fixable is False
assert issue.is_persistent is False
assert issue.translation_key == "deprecated_yaml_import_issue_missing_hub"
assert issue.severity == ir.IssueSeverity.WARNING
async def test_async_setup_import_failure(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
mock_stiebel_eltron_client: AsyncMock,
) -> None:
"""Test async_setup with import failure."""
config = {
DOMAIN: {
CONF_NAME: "Stiebel Eltron",
CONF_HUB: DEFAULT_HUB,
},
"modbus": [
{
CONF_NAME: DEFAULT_HUB,
CONF_HOST: "invalid_host",
CONF_PORT: 502,
}
],
}
# Simulate an import failure
mock_stiebel_eltron_client.update.side_effect = Exception("Import failure")
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Verify the issue is created
issue = issue_registry.async_get_issue(
DOMAIN, "deprecated_yaml_import_issue_unknown"
)
assert issue
assert issue.active is True
assert issue.is_fixable is False
assert issue.is_persistent is False
assert issue.translation_key == "deprecated_yaml_import_issue_unknown"
assert issue.severity == ir.IssueSeverity.WARNING
@pytest.mark.usefixtures("mock_modbus")
async def test_async_setup_cannot_connect(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
mock_stiebel_eltron_client: AsyncMock,
) -> None:
"""Test async_setup with import failure."""
config = {
DOMAIN: {
CONF_NAME: "Stiebel Eltron",
CONF_HUB: DEFAULT_HUB,
},
"modbus": [
{
CONF_NAME: DEFAULT_HUB,
CONF_HOST: "invalid_host",
CONF_PORT: 502,
}
],
}
# Simulate a cannot connect error
mock_stiebel_eltron_client.update.return_value = False
assert await async_setup_component(hass, DOMAIN, config)
await hass.async_block_till_done()
# Verify the issue is created
issue = issue_registry.async_get_issue(
DOMAIN, "deprecated_yaml_import_issue_cannot_connect"
)
assert issue
assert issue.active is True
assert issue.is_fixable is False
assert issue.is_persistent is False
assert issue.translation_key == "deprecated_yaml_import_issue_cannot_connect"
assert issue.severity == ir.IssueSeverity.WARNING