Add support to consider device holiday and summer mode in AVM Fritz!Smarthome (#119862)

This commit is contained in:
Michael 2024-06-22 12:40:03 +02:00 committed by GitHub
parent ad1f0db5a4
commit 1a962b415e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 213 additions and 24 deletions

View File

@ -19,6 +19,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity from . import FritzBoxDeviceEntity
@ -27,18 +28,26 @@ from .const import (
ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_SUMMER_MODE, ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN, ATTR_STATE_WINDOW_OPEN,
DOMAIN,
LOGGER, LOGGER,
) )
from .coordinator import FritzboxConfigEntry from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator
from .model import ClimateExtraAttributes from .model import ClimateExtraAttributes
OPERATION_LIST = [HVACMode.HEAT, HVACMode.OFF] HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF]
PRESET_HOLIDAY = "holiday"
PRESET_SUMMER = "summer"
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
SUPPORTED_FEATURES = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
MIN_TEMPERATURE = 8 MIN_TEMPERATURE = 8
MAX_TEMPERATURE = 28 MAX_TEMPERATURE = 28
PRESET_MANUAL = "manual"
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome) # special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
ON_API_TEMPERATURE = 127.0 ON_API_TEMPERATURE = 127.0
OFF_API_TEMPERATURE = 126.5 OFF_API_TEMPERATURE = 126.5
@ -76,15 +85,38 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
"""The thermostat class for FRITZ!SmartHome thermostats.""" """The thermostat class for FRITZ!SmartHome thermostats."""
_attr_precision = PRECISION_HALVES _attr_precision = PRECISION_HALVES
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "thermostat"
_enable_turn_on_off_backwards_compatibility = False _enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
coordinator: FritzboxDataUpdateCoordinator,
ain: str,
) -> None:
"""Initialize the thermostat."""
self._attr_supported_features = SUPPORTED_FEATURES
self._attr_hvac_modes = HVAC_MODES
self._attr_preset_modes = PRESET_MODES
super().__init__(coordinator, ain)
@callback
def async_write_ha_state(self) -> None:
"""Write the state to the HASS state machine."""
if self.data.holiday_active:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
self._attr_hvac_modes = [HVACMode.HEAT]
self._attr_preset_modes = [PRESET_HOLIDAY]
elif self.data.summer_active:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
self._attr_hvac_modes = [HVACMode.OFF]
self._attr_preset_modes = [PRESET_SUMMER]
else:
self._attr_supported_features = SUPPORTED_FEATURES
self._attr_hvac_modes = HVAC_MODES
self._attr_preset_modes = PRESET_MODES
return super().async_write_ha_state()
@property @property
def current_temperature(self) -> float: def current_temperature(self) -> float:
"""Return the current temperature.""" """Return the current temperature."""
@ -116,6 +148,10 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""Return the current operation mode.""" """Return the current operation mode."""
if self.data.holiday_active:
return HVACMode.HEAT
if self.data.summer_active:
return HVACMode.OFF
if self.data.target_temperature in ( if self.data.target_temperature in (
OFF_REPORT_SET_TEMPERATURE, OFF_REPORT_SET_TEMPERATURE,
OFF_API_TEMPERATURE, OFF_API_TEMPERATURE,
@ -124,13 +160,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
return HVACMode.HEAT return HVACMode.HEAT
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available operation modes."""
return OPERATION_LIST
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new operation mode.""" """Set new operation mode."""
if self.data.holiday_active or self.data.summer_active:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_hvac_while_active_mode",
)
if self.hvac_mode == hvac_mode: if self.hvac_mode == hvac_mode:
LOGGER.debug( LOGGER.debug(
"%s is already in requested hvac mode %s", self.name, hvac_mode "%s is already in requested hvac mode %s", self.name, hvac_mode
@ -144,19 +180,23 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
@property @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:
"""Return current preset mode.""" """Return current preset mode."""
if self.data.holiday_active:
return PRESET_HOLIDAY
if self.data.summer_active:
return PRESET_SUMMER
if self.data.target_temperature == self.data.comfort_temperature: if self.data.target_temperature == self.data.comfort_temperature:
return PRESET_COMFORT return PRESET_COMFORT
if self.data.target_temperature == self.data.eco_temperature: if self.data.target_temperature == self.data.eco_temperature:
return PRESET_ECO return PRESET_ECO
return None return None
@property
def preset_modes(self) -> list[str]:
"""Return supported preset modes."""
return [PRESET_ECO, PRESET_COMFORT]
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode.""" """Set preset mode."""
if self.data.holiday_active or self.data.summer_active:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_preset_while_active_mode",
)
if preset_mode == PRESET_COMFORT: if preset_mode == PRESET_COMFORT:
await self.async_set_temperature(temperature=self.data.comfort_temperature) await self.async_set_temperature(temperature=self.data.comfort_temperature)
elif preset_mode == PRESET_ECO: elif preset_mode == PRESET_ECO:

View File

@ -0,0 +1,16 @@
{
"entity": {
"climate": {
"thermostat": {
"state_attributes": {
"preset_mode": {
"state": {
"holiday": "mdi:bag-suitcase-outline",
"summer": "mdi:radiator-off"
}
}
}
}
}
}
}

View File

@ -56,6 +56,21 @@
"device_lock": { "name": "Button lock via UI" }, "device_lock": { "name": "Button lock via UI" },
"lock": { "name": "Button lock on device" } "lock": { "name": "Button lock on device" }
}, },
"climate": {
"thermostat": {
"state_attributes": {
"preset_mode": {
"name": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::name%]",
"state": {
"eco": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
"holiday": "Holiday",
"summer": "Summer"
}
}
}
}
},
"sensor": { "sensor": {
"comfort_temperature": { "name": "Comfort temperature" }, "comfort_temperature": { "name": "Comfort temperature" },
"eco_temperature": { "name": "Eco temperature" }, "eco_temperature": { "name": "Eco temperature" },
@ -64,5 +79,13 @@
"nextchange_time": { "name": "Next scheduled change time" }, "nextchange_time": { "name": "Next scheduled change time" },
"scheduled_preset": { "name": "Current scheduled preset" } "scheduled_preset": { "name": "Current scheduled preset" }
} }
},
"exceptions": {
"change_preset_while_active_mode": {
"message": "Can't change preset while holiday or summer mode is active on the device."
},
"change_hvac_while_active_mode": {
"message": "Can't change hvac mode while holiday or summer mode is active on the device."
}
} }
} }

View File

@ -103,10 +103,10 @@ class FritzDeviceClimateMock(FritzEntityBaseMock):
has_temperature_sensor = True has_temperature_sensor = True
has_thermostat = True has_thermostat = True
has_blind = False has_blind = False
holiday_active = "fake_holiday" holiday_active = False
lock = "fake_locked" lock = "fake_locked"
present = True present = True
summer_active = "fake_summer" summer_active = False
target_temperature = 19.5 target_temperature = 19.5
window_open = "fake_window" window_open = "fake_window"
nextchange_temperature = 22.0 nextchange_temperature = 22.0

View File

@ -3,6 +3,8 @@
from datetime import timedelta from datetime import timedelta
from unittest.mock import Mock, call from unittest.mock import Mock, call
from freezegun.api import FrozenDateTimeFactory
import pytest
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from homeassistant.components.climate import ( from homeassistant.components.climate import (
@ -21,6 +23,7 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
HVACMode, HVACMode,
) )
from homeassistant.components.fritzbox.climate import PRESET_HOLIDAY, PRESET_SUMMER
from homeassistant.components.fritzbox.const import ( from homeassistant.components.fritzbox.const import (
ATTR_STATE_BATTERY_LOW, ATTR_STATE_BATTERY_LOW,
ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_HOLIDAY_MODE,
@ -40,6 +43,7 @@ from homeassistant.const import (
UnitOfTemperature, UnitOfTemperature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from . import FritzDeviceClimateMock, set_devices, setup_config_entry from . import FritzDeviceClimateMock, set_devices, setup_config_entry
@ -68,8 +72,8 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None:
assert state.attributes[ATTR_PRESET_MODE] is None assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT] assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT]
assert state.attributes[ATTR_STATE_BATTERY_LOW] is True assert state.attributes[ATTR_STATE_BATTERY_LOW] is True
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday" assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False
assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer" assert state.attributes[ATTR_STATE_SUMMER_MODE] is False
assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window" assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window"
assert state.attributes[ATTR_TEMPERATURE] == 19.5 assert state.attributes[ATTR_TEMPERATURE] == 19.5
assert ATTR_STATE_CLASS not in state.attributes assert ATTR_STATE_CLASS not in state.attributes
@ -444,3 +448,109 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
state = hass.states.get(f"{DOMAIN}.new_climate") state = hass.states.get(f"{DOMAIN}.new_climate")
assert state assert state
async def test_holidy_summer_mode(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock
) -> None:
"""Test holiday and summer mode."""
device = FritzDeviceClimateMock()
assert await setup_config_entry(
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
# initial state
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False
assert state.attributes[ATTR_STATE_SUMMER_MODE] is False
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF]
assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT]
# test holiday mode
device.holiday_active = True
device.summer_active = False
freezer.tick(timedelta(seconds=200))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_STATE_HOLIDAY_MODE]
assert state.attributes[ATTR_STATE_SUMMER_MODE] is False
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT]
assert state.attributes[ATTR_PRESET_MODE] == PRESET_HOLIDAY
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_HOLIDAY]
with pytest.raises(
HomeAssistantError,
match="Can't change hvac mode while holiday or summer mode is active on the device",
):
await hass.services.async_call(
"climate",
SERVICE_SET_HVAC_MODE,
{"entity_id": ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True,
)
with pytest.raises(
HomeAssistantError,
match="Can't change preset while holiday or summer mode is active on the device",
):
await hass.services.async_call(
"climate",
SERVICE_SET_PRESET_MODE,
{"entity_id": ENTITY_ID, ATTR_PRESET_MODE: PRESET_HOLIDAY},
blocking=True,
)
# test summer mode
device.holiday_active = False
device.summer_active = True
freezer.tick(timedelta(seconds=200))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False
assert state.attributes[ATTR_STATE_SUMMER_MODE]
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.OFF]
assert state.attributes[ATTR_PRESET_MODE] == PRESET_SUMMER
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_SUMMER]
with pytest.raises(
HomeAssistantError,
match="Can't change hvac mode while holiday or summer mode is active on the device",
):
await hass.services.async_call(
"climate",
SERVICE_SET_HVAC_MODE,
{"entity_id": ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True,
)
with pytest.raises(
HomeAssistantError,
match="Can't change preset while holiday or summer mode is active on the device",
):
await hass.services.async_call(
"climate",
SERVICE_SET_PRESET_MODE,
{"entity_id": ENTITY_ID, ATTR_PRESET_MODE: PRESET_SUMMER},
blocking=True,
)
# back to normal state
device.holiday_active = False
device.summer_active = False
freezer.tick(timedelta(seconds=200))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False
assert state.attributes[ATTR_STATE_SUMMER_MODE] is False
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.HEAT, HVACMode.OFF]
assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT]