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 <frenck@frenck.nl>

* 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 <frenck@frenck.nl>
This commit is contained in:
Ethem Cem Özkan 2024-06-15 18:16:10 +02:00 committed by GitHub
parent 410ef8ce14
commit 9065042960
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 274 additions and 35 deletions

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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,
)

View File

@ -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."
}
}
}

View File

@ -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,

View File

@ -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

View File

@ -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
)