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

pull/121623/head
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,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
@ -27,18 +28,26 @@ from .const import (
ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN,
DOMAIN,
LOGGER,
)
from .coordinator import FritzboxConfigEntry
from .coordinator import FritzboxConfigEntry, FritzboxDataUpdateCoordinator
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
MAX_TEMPERATURE = 28
PRESET_MANUAL = "manual"
# special temperatures for on/off in Fritz!Box API (modified by pyfritzhome)
ON_API_TEMPERATURE = 127.0
OFF_API_TEMPERATURE = 126.5
@ -76,15 +85,38 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
"""The thermostat class for FRITZ!SmartHome thermostats."""
_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_translation_key = "thermostat"
_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
def current_temperature(self) -> float:
"""Return the current temperature."""
@ -116,6 +148,10 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
@property
def hvac_mode(self) -> HVACMode:
"""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 (
OFF_REPORT_SET_TEMPERATURE,
OFF_API_TEMPERATURE,
@ -124,13 +160,13 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
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:
"""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:
LOGGER.debug(
"%s is already in requested hvac mode %s", self.name, hvac_mode
@ -144,19 +180,23 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""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:
return PRESET_COMFORT
if self.data.target_temperature == self.data.eco_temperature:
return PRESET_ECO
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:
"""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:
await self.async_set_temperature(temperature=self.data.comfort_temperature)
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" },
"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": {
"comfort_temperature": { "name": "Comfort temperature" },
"eco_temperature": { "name": "Eco temperature" },
@ -64,5 +79,13 @@
"nextchange_time": { "name": "Next scheduled change time" },
"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_thermostat = True
has_blind = False
holiday_active = "fake_holiday"
holiday_active = False
lock = "fake_locked"
present = True
summer_active = "fake_summer"
summer_active = False
target_temperature = 19.5
window_open = "fake_window"
nextchange_temperature = 22.0

View File

@ -3,6 +3,8 @@
from datetime import timedelta
from unittest.mock import Mock, call
from freezegun.api import FrozenDateTimeFactory
import pytest
from requests.exceptions import HTTPError
from homeassistant.components.climate import (
@ -21,6 +23,7 @@ from homeassistant.components.climate import (
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.components.fritzbox.climate import PRESET_HOLIDAY, PRESET_SUMMER
from homeassistant.components.fritzbox.const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_HOLIDAY_MODE,
@ -40,6 +43,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.dt as dt_util
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_MODES] == [PRESET_ECO, PRESET_COMFORT]
assert state.attributes[ATTR_STATE_BATTERY_LOW] is True
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday"
assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer"
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] is False
assert state.attributes[ATTR_STATE_SUMMER_MODE] is False
assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window"
assert state.attributes[ATTR_TEMPERATURE] == 19.5
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")
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]