From 90650429603cf379f3def8e6ecb8610b93321050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ethem=20Cem=20=C3=96zkan?= Date: Sat, 15 Jun 2024 18:16:10 +0200 Subject: [PATCH] Revert "Revert Use integration fallback configuration for tado water fallback" (#119526) * Revert "Revert Use integration fallback configuration for tado water heater fallback (#119466)" This reverts commit ade936e6d5088c4a4d809111417fb3c7080825d5. * add decide method for duration * add repair issue to let users know * test module for repairs * Update strings.json Co-authored-by: Franck Nijhof * repair issue should not be persistent * use issue_registery fixture instead of mocking * fix comment * parameterize repair issue created test case --------- Co-authored-by: Franck Nijhof --- homeassistant/components/tado/climate.py | 41 +++------ homeassistant/components/tado/const.py | 2 + homeassistant/components/tado/helper.py | 51 +++++++++++ homeassistant/components/tado/repairs.py | 34 ++++++++ homeassistant/components/tado/strings.json | 4 + homeassistant/components/tado/water_heater.py | 26 ++++-- tests/components/tado/test_helper.py | 87 +++++++++++++++++++ tests/components/tado/test_repairs.py | 64 ++++++++++++++ 8 files changed, 274 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/tado/helper.py create mode 100644 homeassistant/components/tado/repairs.py create mode 100644 tests/components/tado/test_helper.py create mode 100644 tests/components/tado/test_repairs.py diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6d298a80e79..3cb5d7fbce9 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -36,10 +36,7 @@ from .const import ( CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, - CONST_OVERLAY_TADO_DEFAULT, - CONST_OVERLAY_TADO_MODE, CONST_OVERLAY_TADO_OPTIONS, - CONST_OVERLAY_TIMER, DATA, DOMAIN, HA_TERMINATION_DURATION, @@ -67,6 +64,7 @@ from .const import ( TYPE_HEATING, ) from .entity import TadoZoneEntity +from .helper import decide_duration, decide_overlay_mode _LOGGER = logging.getLogger(__name__) @@ -598,31 +596,18 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado.reset_zone_overlay(self.zone_id) return - # If user gave duration then overlay mode needs to be timer - if duration: - overlay_mode = CONST_OVERLAY_TIMER - # If no duration or timer set to fallback setting - if overlay_mode is None: - overlay_mode = ( - self._tado.fallback - if self._tado.fallback is not None - else CONST_OVERLAY_TADO_MODE - ) - # If default is Tado default then look it up - if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: - overlay_mode = ( - self._tado_zone_data.default_overlay_termination_type - if self._tado_zone_data.default_overlay_termination_type is not None - else CONST_OVERLAY_TADO_MODE - ) - # If we ended up with a timer but no duration, set a default duration - if overlay_mode == CONST_OVERLAY_TIMER and duration is None: - duration = ( - int(self._tado_zone_data.default_overlay_termination_duration) - if self._tado_zone_data.default_overlay_termination_duration is not None - else 3600 - ) - + overlay_mode = decide_overlay_mode( + tado=self._tado, + duration=duration, + overlay_mode=overlay_mode, + zone_id=self.zone_id, + ) + duration = decide_duration( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + ) _LOGGER.debug( ( "Switching to %s for zone %s (%d) with temperature %s °C and duration" diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index c62352a6d95..be35bbb8e25 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -212,3 +212,5 @@ SERVICE_ADD_METER_READING = "add_meter_reading" CONF_CONFIG_ENTRY = "config_entry" CONF_READING = "reading" ATTR_MESSAGE = "message" + +WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback" diff --git a/homeassistant/components/tado/helper.py b/homeassistant/components/tado/helper.py new file mode 100644 index 00000000000..efcd3e7c4ea --- /dev/null +++ b/homeassistant/components/tado/helper.py @@ -0,0 +1,51 @@ +"""Helper methods for Tado.""" + +from . import TadoConnector +from .const import ( + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, +) + + +def decide_overlay_mode( + tado: TadoConnector, + duration: int | None, + zone_id: int, + overlay_mode: str | None = None, +) -> str: + """Return correct overlay mode based on the action and defaults.""" + # If user gave duration then overlay mode needs to be timer + if duration: + return CONST_OVERLAY_TIMER + # If no duration or timer set to fallback setting + if overlay_mode is None: + overlay_mode = tado.fallback or CONST_OVERLAY_TADO_MODE + # If default is Tado default then look it up + if overlay_mode == CONST_OVERLAY_TADO_DEFAULT: + overlay_mode = ( + tado.data["zone"][zone_id].default_overlay_termination_type + or CONST_OVERLAY_TADO_MODE + ) + + return overlay_mode + + +def decide_duration( + tado: TadoConnector, + duration: int | None, + zone_id: int, + overlay_mode: str | None = None, +) -> None | int: + """Return correct duration based on the selected overlay mode/duration and tado config.""" + # If we ended up with a timer but no duration, set a default duration + # If we ended up with a timer but no duration, set a default duration + if overlay_mode == CONST_OVERLAY_TIMER and duration is None: + duration = ( + int(tado.data["zone"][zone_id].default_overlay_termination_duration) + if tado.data["zone"][zone_id].default_overlay_termination_duration + is not None + else 3600 + ) + + return duration diff --git a/homeassistant/components/tado/repairs.py b/homeassistant/components/tado/repairs.py new file mode 100644 index 00000000000..5ffc3c76bf7 --- /dev/null +++ b/homeassistant/components/tado/repairs.py @@ -0,0 +1,34 @@ +"""Repair implementations.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from .const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + DOMAIN, + WATER_HEATER_FALLBACK_REPAIR, +) + + +def manage_water_heater_fallback_issue( + hass: HomeAssistant, + water_heater_entities: list, + integration_overlay_fallback: str | None, +) -> None: + """Notify users about water heater respecting fallback setting.""" + if ( + integration_overlay_fallback + in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL] + and len(water_heater_entities) > 0 + ): + for water_heater_entity in water_heater_entities: + ir.async_create_issue( + hass=hass, + domain=DOMAIN, + issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}", + is_fixable=False, + is_persistent=False, + severity=ir.IssueSeverity.WARNING, + translation_key=WATER_HEATER_FALLBACK_REPAIR, + ) diff --git a/homeassistant/components/tado/strings.json b/homeassistant/components/tado/strings.json index 51e36fe5355..d992befe112 100644 --- a/homeassistant/components/tado/strings.json +++ b/homeassistant/components/tado/strings.json @@ -165,6 +165,10 @@ "import_failed_invalid_auth": { "title": "Failed to import, invalid credentials", "description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful." + }, + "water_heater_fallback": { + "title": "Tado Water Heater entities now support fallback options", + "description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options." } } } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index f1257f097eb..a31b70a8f9a 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -32,6 +32,8 @@ from .const import ( TYPE_HOT_WATER, ) from .entity import TadoZoneEntity +from .helper import decide_duration, decide_overlay_mode +from .repairs import manage_water_heater_fallback_issue _LOGGER = logging.getLogger(__name__) @@ -79,6 +81,12 @@ async def async_setup_entry( async_add_entities(entities, True) + manage_water_heater_fallback_issue( + hass=hass, + water_heater_entities=entities, + integration_overlay_fallback=tado.fallback, + ) + def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]: """Create all water heater entities.""" @@ -277,13 +285,17 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) return - overlay_mode = CONST_OVERLAY_MANUAL - if duration: - overlay_mode = CONST_OVERLAY_TIMER - elif self._tado.fallback: - # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled - overlay_mode = CONST_OVERLAY_TADO_MODE - + overlay_mode = decide_overlay_mode( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + ) + duration = decide_duration( + tado=self._tado, + duration=duration, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + ) _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s", self._current_tado_hvac_mode, diff --git a/tests/components/tado/test_helper.py b/tests/components/tado/test_helper.py new file mode 100644 index 00000000000..bdd7977f858 --- /dev/null +++ b/tests/components/tado/test_helper.py @@ -0,0 +1,87 @@ +"""Helper method tests.""" + +from unittest.mock import patch + +from homeassistant.components.tado import TadoConnector +from homeassistant.components.tado.const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + CONST_OVERLAY_TIMER, +) +from homeassistant.components.tado.helper import decide_duration, decide_overlay_mode +from homeassistant.core import HomeAssistant + + +def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector: + """Return dummy tado connector.""" + return TadoConnector(hass, username="dummy", password="dummy", fallback=fallback) + + +async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None: + """Test overlay method selection when duration is set.""" + tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE) + overlay_mode = decide_overlay_mode(tado=tado, duration=3600, zone_id=1) + # Must select TIMER overlay + assert overlay_mode == CONST_OVERLAY_TIMER + + +async def test_overlay_mode_next_time_block_fallback(hass: HomeAssistant) -> None: + """Test overlay method selection when duration is not set.""" + integration_fallback = CONST_OVERLAY_TADO_MODE + tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) + overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=1) + # Must fallback to integration wide setting + assert overlay_mode == integration_fallback + + +async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None: + """Test overlay method selection when tado default is selected.""" + integration_fallback = CONST_OVERLAY_TADO_DEFAULT + zone_fallback = CONST_OVERLAY_MANUAL + tado = dummy_tado_connector(hass=hass, fallback=integration_fallback) + + class MockZoneData: + def __init__(self) -> None: + self.default_overlay_termination_type = zone_fallback + + zone_id = 1 + + zone_data = {"zone": {zone_id: MockZoneData()}} + with patch.dict(tado.data, zone_data): + overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id) + # Must fallback to zone setting + assert overlay_mode == zone_fallback + + +async def test_duration_enabled_without_tado_default(hass: HomeAssistant) -> None: + """Test duration decide method when overlay is timer and duration is set.""" + overlay = CONST_OVERLAY_TIMER + expected_duration = 600 + tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_MANUAL) + duration = decide_duration( + tado=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0 + ) + # Should return the same duration value + assert duration == expected_duration + + +async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None: + """Test overlay method selection when ended up with timer overlay and None duration.""" + zone_fallback = CONST_OVERLAY_TIMER + expected_duration = 45000 + tado = dummy_tado_connector(hass=hass, fallback=zone_fallback) + + class MockZoneData: + def __init__(self) -> None: + self.default_overlay_termination_duration = expected_duration + + zone_id = 1 + + zone_data = {"zone": {zone_id: MockZoneData()}} + with patch.dict(tado.data, zone_data): + duration = decide_duration( + tado=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback + ) + # Must fallback to zone timer setting + assert duration == expected_duration diff --git a/tests/components/tado/test_repairs.py b/tests/components/tado/test_repairs.py new file mode 100644 index 00000000000..2e055884272 --- /dev/null +++ b/tests/components/tado/test_repairs.py @@ -0,0 +1,64 @@ +"""Repair tests.""" + +import pytest + +from homeassistant.components.tado.const import ( + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_DEFAULT, + CONST_OVERLAY_TADO_MODE, + DOMAIN, + WATER_HEATER_FALLBACK_REPAIR, +) +from homeassistant.components.tado.repairs import manage_water_heater_fallback_issue +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + + +class MockWaterHeater: + """Mock Water heater entity.""" + + def __init__(self, zone_name) -> None: + """Init mock entity class.""" + self.zone_name = zone_name + + +async def test_manage_water_heater_fallback_issue_not_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, +) -> None: + """Test water heater fallback issue is not needed.""" + zone_name = "Hot Water" + expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" + water_heater_entities = [MockWaterHeater(zone_name)] + manage_water_heater_fallback_issue( + water_heater_entities=water_heater_entities, + integration_overlay_fallback=CONST_OVERLAY_TADO_MODE, + hass=hass, + ) + assert ( + issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN) + is None + ) + + +@pytest.mark.parametrize( + "integration_overlay_fallback", [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL] +) +async def test_manage_water_heater_fallback_issue_created( + hass: HomeAssistant, + issue_registry: ir.IssueRegistry, + integration_overlay_fallback: str, +) -> None: + """Test water heater fallback issue created cases.""" + zone_name = "Hot Water" + expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}" + water_heater_entities = [MockWaterHeater(zone_name)] + manage_water_heater_fallback_issue( + water_heater_entities=water_heater_entities, + integration_overlay_fallback=integration_overlay_fallback, + hass=hass, + ) + assert ( + issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN) + is not None + )