Add support to consider device holiday and summer mode in AVM Fritz!Smarthome (#119862)
parent
ad1f0db5a4
commit
1a962b415e
|
@ -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:
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"holiday": "mdi:bag-suitcase-outline",
|
||||
"summer": "mdi:radiator-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue