mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add option to allow to use setpoint instead of override for legacy incomfort RF gateway (#135143)
* Add option to allow to use setpoint in stead of override for legacy incomfort RF gateway * Add test to assert state with legacy_setpoint_status option * Use selector * Update homeassistant/components/incomfort/strings.json Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Follow up on code review * Rephrase data_description * Rephrase * Use async_schedule_reload helper * Move option flow after config flow --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
1c053485a9
commit
984c380e13
@ -25,7 +25,7 @@ INTEGRATION_TITLE = "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
type InComfortConfigEntry = ConfigEntry[InComfortDataCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
try:
|
||||
data = await async_connect_gateway(hass, dict(entry.data))
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import InComfortConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_LEGACY_SETPOINT_STATUS, DOMAIN
|
||||
from .coordinator import InComfortDataCoordinator
|
||||
from .entity import IncomfortEntity
|
||||
|
||||
@ -32,9 +32,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up InComfort/InTouch climate devices."""
|
||||
incomfort_coordinator = entry.runtime_data
|
||||
legacy_setpoint_status = entry.options.get(CONF_LEGACY_SETPOINT_STATUS, False)
|
||||
heaters = incomfort_coordinator.data.heaters
|
||||
async_add_entities(
|
||||
InComfortClimate(incomfort_coordinator, h, r) for h in heaters for r in h.rooms
|
||||
InComfortClimate(incomfort_coordinator, h, r, legacy_setpoint_status)
|
||||
for h in heaters
|
||||
for r in h.rooms
|
||||
)
|
||||
|
||||
|
||||
@ -54,12 +57,14 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
||||
coordinator: InComfortDataCoordinator,
|
||||
heater: InComfortHeater,
|
||||
room: InComfortRoom,
|
||||
legacy_setpoint_status: bool,
|
||||
) -> None:
|
||||
"""Initialize the climate device."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._heater = heater
|
||||
self._room = room
|
||||
self._legacy_setpoint_status = legacy_setpoint_status
|
||||
|
||||
self._attr_unique_id = f"{heater.serial_no}_{room.room_no}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@ -91,9 +96,11 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
||||
|
||||
As we set the override, we report back the override. The actual set point is
|
||||
is returned at a later time.
|
||||
Some older thermostats return 0.0 as override, in that case we fallback to
|
||||
the actual setpoint.
|
||||
Some older thermostats do not clear the override setting in that case, in that case
|
||||
we fallback to the returning actual setpoint.
|
||||
"""
|
||||
if self._legacy_setpoint_status:
|
||||
return self._room.setpoint
|
||||
return self._room.override or self._room.setpoint
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
|
@ -1,21 +1,30 @@
|
||||
"""Config flow support for Intergas InComfort integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
BooleanSelectorConfig,
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_LEGACY_SETPOINT_STATUS, DOMAIN
|
||||
from .coordinator import async_connect_gateway
|
||||
|
||||
TITLE = "Intergas InComfort/Intouch Lan2RF gateway"
|
||||
@ -34,6 +43,14 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_LEGACY_SETPOINT_STATUS, default=False): BooleanSelector(
|
||||
BooleanSelectorConfig()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ERROR_STATUS_MAPPING: dict[int, tuple[str, str]] = {
|
||||
401: (CONF_PASSWORD, "auth_error"),
|
||||
404: ("base", "not_found"),
|
||||
@ -66,6 +83,14 @@ async def async_try_connect_gateway(
|
||||
class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow to set up an Intergas InComfort boyler and thermostats."""
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> InComfortOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return InComfortOptionsFlowHandler()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@ -81,3 +106,29 @@ class InComfortConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
class InComfortOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle InComfort Lan2RF gateway options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
errors: dict[str, str] | None = None
|
||||
if user_input is not None:
|
||||
new_options: dict[str, Any] = self.config_entry.options | user_input
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, options=new_options
|
||||
)
|
||||
self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id)
|
||||
return self.async_create_entry(data=new_options)
|
||||
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
OPTIONS_SCHEMA, self.config_entry.options
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""Constants for Intergas InComfort integration."""
|
||||
|
||||
DOMAIN = "incomfort"
|
||||
|
||||
CONF_LEGACY_SETPOINT_STATUS = "legacy_setpoint_status"
|
||||
|
@ -31,6 +31,19 @@
|
||||
"unknown": "[%key:component::incomfort::config::abort::unknown%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Intergas InComfort Lan2RF Gateway options",
|
||||
"data": {
|
||||
"legacy_setpoint_status": "Legacy setpoint handling"
|
||||
},
|
||||
"data_description": {
|
||||
"legacy_setpoint_status": "Some older gateway models with an older firmware versions might not update the thermostat setpoint and override settings correctly. Enable this option if you experience issues in updating the setpoint for your thermostat. It will use the actual setpoint of the thermostat instead of the override. As side effect is that it might take a few minutes before the setpoint is updated."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"title": "YAML import failed with unknown error",
|
||||
|
@ -53,12 +53,22 @@ def mock_entry_data() -> dict[str, Any]:
|
||||
return MOCK_CONFIG
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_entry_options() -> dict[str, Any] | None:
|
||||
"""Mock config entry options for fixture."""
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(
|
||||
hass: HomeAssistant, mock_entry_data: dict[str, Any]
|
||||
hass: HomeAssistant,
|
||||
mock_entry_data: dict[str, Any],
|
||||
mock_entry_options: dict[str, Any],
|
||||
) -> ConfigEntry:
|
||||
"""Mock a config entry setup for incomfort integration."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=mock_entry_data)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=mock_entry_data, options=mock_entry_options
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-entry]
|
||||
# name: test_setup_platform[legacy-override][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@ -38,7 +38,73 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[legacy_thermostat][climate.thermostat_1-state]
|
||||
# name: test_setup_platform[legacy-override][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
'friendly_name': 'Thermostat 1',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'status': dict({
|
||||
'override': 19.0,
|
||||
'room_temp': 21.42,
|
||||
'setpoint': 18.0,
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'temperature': 18.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[legacy-zero_override][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'incomfort',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'c0ffeec0ffee_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[legacy-zero_override][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
@ -65,7 +131,7 @@
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[new_thermostat][climate.thermostat_1-entry]
|
||||
# name: test_setup_platform[modern-override][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@ -104,7 +170,7 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[new_thermostat][climate.thermostat_1-state]
|
||||
# name: test_setup_platform[modern-override][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
@ -116,7 +182,73 @@
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'status': dict({
|
||||
'override': 18.0,
|
||||
'override': 19.0,
|
||||
'room_temp': 21.42,
|
||||
'setpoint': 18.0,
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'temperature': 19.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[modern-zero_override][climate.thermostat_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.thermostat_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'incomfort',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'c0ffeec0ffee_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[modern-zero_override][climate.thermostat_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 21.4,
|
||||
'friendly_name': 'Thermostat 1',
|
||||
'hvac_action': <HVACAction.IDLE: 'idle'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 30.0,
|
||||
'min_temp': 5.0,
|
||||
'status': dict({
|
||||
'override': 0.0,
|
||||
'room_temp': 21.42,
|
||||
'setpoint': 18.0,
|
||||
}),
|
||||
|
@ -17,10 +17,15 @@ from tests.common import snapshot_platform
|
||||
@pytest.mark.parametrize(
|
||||
"mock_room_status",
|
||||
[
|
||||
{"room_temp": 21.42, "setpoint": 18.0, "override": 18.0},
|
||||
{"room_temp": 21.42, "setpoint": 18.0, "override": 19.0},
|
||||
{"room_temp": 21.42, "setpoint": 18.0, "override": 0.0},
|
||||
],
|
||||
ids=["new_thermostat", "legacy_thermostat"],
|
||||
ids=["override", "zero_override"],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"mock_entry_options",
|
||||
[None, {"legacy_setpoint_status": True}],
|
||||
ids=["modern", "legacy"],
|
||||
)
|
||||
async def test_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
@ -31,8 +36,9 @@ async def test_setup_platform(
|
||||
) -> None:
|
||||
"""Test the incomfort entities are set up correctly.
|
||||
|
||||
Legacy thermostats report 0.0 as override if no override is set,
|
||||
but new thermostat sync the override with the actual setpoint instead.
|
||||
Thermostats report 0.0 as override if no override is set
|
||||
or when the setpoint has been changed manually,
|
||||
Some older thermostats do not reset the override setpoint has been changed manually.
|
||||
"""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Tests for the Intergas InComfort config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from incomfortclient import IncomfortError, InvalidHeaterList
|
||||
@ -113,3 +114,39 @@ async def test_form_validation(
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert "errors" not in result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_input", "legacy_setpoint_status"),
|
||||
[
|
||||
({}, False),
|
||||
({"legacy_setpoint_status": False}, False),
|
||||
({"legacy_setpoint_status": True}, True),
|
||||
],
|
||||
)
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_incomfort: MagicMock,
|
||||
user_input: dict[str, Any],
|
||||
legacy_setpoint_status: bool,
|
||||
) -> None:
|
||||
"""Test options flow."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
with patch("homeassistant.components.incomfort.async_setup_entry") as restart_mock:
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert restart_mock.call_count == 1
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["data"] == {"legacy_setpoint_status": legacy_setpoint_status}
|
||||
assert entry.options.get("legacy_setpoint_status", False) is legacy_setpoint_status
|
||||
|
Loading…
x
Reference in New Issue
Block a user