Add "nextchange" sensors to AVM FRITZ!Smarthome devices (#58274)
This commit is contained in:
parent
995f01cb68
commit
a78f0eae39
5 changed files with 128 additions and 4 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue