Add "nextchange" sensors to AVM FRITZ!Smarthome devices (#58274)

This commit is contained in:
refinedcranberry 2021-11-25 12:34:04 +01:00 committed by GitHub
parent 995f01cb68
commit a78f0eae39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 128 additions and 4 deletions

View file

@ -185,7 +185,7 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active
if self.device.summer_active is not None: if self.device.summer_active is not None:
attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active
if ATTR_STATE_WINDOW_OPEN is not None: if self.device.window_open is not None:
attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open
return attrs return attrs

View file

@ -18,8 +18,8 @@ class FritzExtraAttributes(TypedDict):
class ClimateExtraAttributes(FritzExtraAttributes, total=False): class ClimateExtraAttributes(FritzExtraAttributes, total=False):
"""TypedDict for climates extra attributes.""" """TypedDict for climates extra attributes."""
battery_low: bool
battery_level: int battery_level: int
battery_low: bool
holiday_mode: bool holiday_mode: bool
summer_mode: bool summer_mode: bool
window_open: bool window_open: bool

View file

@ -3,10 +3,12 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from typing import Final from typing import Final
from pyfritzhome.fritzhomedevice import FritzhomeDevice from pyfritzhome.fritzhomedevice import FritzhomeDevice
from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING, STATE_CLASS_TOTAL_INCREASING,
@ -20,6 +22,7 @@ from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_TIMESTAMP,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
PERCENTAGE, PERCENTAGE,
@ -28,6 +31,8 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utc_from_timestamp
from . import FritzBoxEntity from . import FritzBoxEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
@ -38,7 +43,7 @@ from .model import FritzEntityDescriptionMixinBase
class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase): class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase):
"""Sensor description mixin for Fritz!Smarthome entities.""" """Sensor description mixin for Fritz!Smarthome entities."""
native_value: Callable[[FritzhomeDevice], float | int | None] native_value: Callable[[FritzhomeDevice], StateType | datetime]
@dataclass @dataclass
@ -97,6 +102,60 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return]
native_value=lambda device: device.energy / 1000 if device.energy else 0.0, native_value=lambda device: device.energy / 1000 if device.energy else 0.0,
), ),
# Thermostat Sensors
FritzSensorEntityDescription(
key="comfort_temperature",
name="Comfort Temperature",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
suitable=lambda device: device.has_thermostat
and device.comfort_temperature is not None,
native_value=lambda device: device.comfort_temperature, # type: ignore[no-any-return]
),
FritzSensorEntityDescription(
key="eco_temperature",
name="Eco Temperature",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
suitable=lambda device: device.has_thermostat
and device.eco_temperature is not None,
native_value=lambda device: device.eco_temperature, # type: ignore[no-any-return]
),
FritzSensorEntityDescription(
key="nextchange_temperature",
name="Next Scheduled Temperature",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
suitable=lambda device: device.has_thermostat
and device.nextchange_temperature is not None,
native_value=lambda device: device.nextchange_temperature, # type: ignore[no-any-return]
),
FritzSensorEntityDescription(
key="nextchange_time",
name="Next Scheduled Change Time",
device_class=DEVICE_CLASS_TIMESTAMP,
suitable=lambda device: device.has_thermostat
and device.nextchange_endperiod is not None,
native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod),
),
FritzSensorEntityDescription(
key="nextchange_preset",
name="Next Scheduled Preset",
suitable=lambda device: device.has_thermostat
and device.nextchange_temperature is not None,
native_value=lambda device: PRESET_ECO
if device.nextchange_temperature == device.eco_temperature
else PRESET_COMFORT,
),
FritzSensorEntityDescription(
key="scheduled_preset",
name="Current Scheduled Preset",
suitable=lambda device: device.has_thermostat
and device.nextchange_temperature is not None,
native_value=lambda device: PRESET_COMFORT
if device.nextchange_temperature == device.eco_temperature
else PRESET_ECO,
),
) )
@ -122,6 +181,6 @@ class FritzBoxSensor(FritzBoxEntity, SensorEntity):
entity_description: FritzSensorEntityDescription entity_description: FritzSensorEntityDescription
@property @property
def native_value(self) -> float | int | None: def native_value(self) -> StateType | datetime:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.entity_description.native_value(self.device) return self.entity_description.native_value(self.device)

View file

@ -4,6 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from unittest.mock import Mock from unittest.mock import Mock
from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO
from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.components.fritzbox.const import DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -86,6 +87,10 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock):
summer_active = "fake_summer" summer_active = "fake_summer"
target_temperature = 19.5 target_temperature = 19.5
window_open = "fake_window" window_open = "fake_window"
nextchange_temperature = 22.0
nextchange_endperiod = 0
nextchange_preset = PRESET_COMFORT
scheduled_preset = PRESET_ECO
class FritzDeviceSensorMock(FritzDeviceBaseMock): class FritzDeviceSensorMock(FritzDeviceBaseMock):

View file

@ -39,6 +39,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICES, CONF_DEVICES,
PERCENTAGE, PERCENTAGE,
TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -85,6 +86,65 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert ATTR_STATE_CLASS not in state.attributes assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_comfort_temperature")
assert state
assert state.state == "22.0"
assert (
state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Comfort Temperature"
)
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_eco_temperature")
assert state
assert state.state == "16.0"
assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Eco Temperature"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(
f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_temperature"
)
assert state
assert state.state == "22.0"
assert (
state.attributes[ATTR_FRIENDLY_NAME]
== f"{CONF_FAKE_NAME} Next Scheduled Temperature"
)
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(
f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_change_time"
)
assert state
assert state.state == "1970-01-01T00:00:00+00:00"
assert (
state.attributes[ATTR_FRIENDLY_NAME]
== f"{CONF_FAKE_NAME} Next Scheduled Change Time"
)
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset")
assert state
assert state.state == PRESET_COMFORT
assert (
state.attributes[ATTR_FRIENDLY_NAME]
== f"{CONF_FAKE_NAME} Next Scheduled Preset"
)
assert ATTR_STATE_CLASS not in state.attributes
state = hass.states.get(
f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_current_scheduled_preset"
)
assert state
assert state.state == PRESET_ECO
assert (
state.attributes[ATTR_FRIENDLY_NAME]
== f"{CONF_FAKE_NAME} Current Scheduled Preset"
)
assert ATTR_STATE_CLASS not in state.attributes
async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock): async def test_target_temperature_on(hass: HomeAssistant, fritz: Mock):
"""Test turn device on.""" """Test turn device on."""