AVM Fritz!Box SmartHome: Integrate Templates (#81885)

This commit is contained in:
Ayk Borstelmann 2022-11-18 17:37:56 +01:00 committed by GitHub
parent f7badfe441
commit 3b783a85c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 235 additions and 95 deletions

View file

@ -1,7 +1,10 @@
"""Support for AVM FRITZ!SmartHome devices."""
from __future__ import annotations
from abc import ABC, abstractmethod
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
from pyfritzhome.devicetypes.fritzhomeentitybase import FritzhomeEntityBase
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.config_entries import ConfigEntry
@ -93,7 +96,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok
class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]):
class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator], ABC):
"""Basis FritzBox entity."""
def __init__(
@ -108,30 +111,39 @@ class FritzBoxEntity(CoordinatorEntity[FritzboxDataUpdateCoordinator]):
self.ain = ain
if entity_description is not None:
self.entity_description = entity_description
self._attr_name = f"{self.device.name} {entity_description.name}"
self._attr_name = f"{self.entity.name} {entity_description.name}"
self._attr_unique_id = f"{ain}_{entity_description.key}"
else:
self._attr_name = self.device.name
self._attr_name = self.entity.name
self._attr_unique_id = ain
@property
@abstractmethod
def entity(self) -> FritzhomeEntityBase:
"""Return entity object from coordinator."""
class FritzBoxDeviceEntity(FritzBoxEntity):
"""Reflects FritzhomeDevice and uses its attributes to construct FritzBoxDeviceEntity."""
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.device.present
return super().available and self.entity.present
@property
def device(self) -> FritzhomeDevice:
def entity(self) -> FritzhomeDevice:
"""Return device object from coordinator."""
return self.coordinator.data[self.ain]
return self.coordinator.data.devices[self.ain]
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
return DeviceInfo(
name=self.device.name,
name=self.entity.name,
identifiers={(DOMAIN, self.ain)},
manufacturer=self.device.manufacturer,
model=self.device.productname,
sw_version=self.device.fw_version,
manufacturer=self.entity.manufacturer,
model=self.entity.productname,
sw_version=self.entity.fw_version,
configuration_url=self.coordinator.configuration_url,
)

View file

@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from . import FritzBoxDeviceEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
from .coordinator import FritzboxDataUpdateCoordinator
from .model import FritzEntityDescriptionMixinBase
@ -73,14 +73,14 @@ async def async_setup_entry(
async_add_entities(
[
FritzboxBinarySensor(coordinator, ain, description)
for ain, device in coordinator.data.items()
for ain, device in coordinator.data.devices.items()
for description in BINARY_SENSOR_TYPES
if description.suitable(device)
]
)
class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
class FritzboxBinarySensor(FritzBoxDeviceEntity, BinarySensorEntity):
"""Representation of a binary FRITZ!SmartHome device."""
entity_description: FritzBinarySensorEntityDescription
@ -93,10 +93,10 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
) -> None:
"""Initialize the FritzBox entity."""
super().__init__(coordinator, ain, entity_description)
self._attr_name = f"{self.device.name} {entity_description.name}"
self._attr_name = f"{self.entity.name} {entity_description.name}"
self._attr_unique_id = f"{ain}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return true if sensor is on."""
return self.entity_description.is_on(self.device)
return self.entity_description.is_on(self.entity)

View file

@ -0,0 +1,56 @@
"""Support for AVM FRITZ!SmartHome templates."""
from pyfritzhome.devicetypes import FritzhomeTemplate
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzboxDataUpdateCoordinator, FritzBoxEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the FRITZ!SmartHome template from ConfigEntry."""
coordinator: FritzboxDataUpdateCoordinator = hass.data[FRITZBOX_DOMAIN][
entry.entry_id
][CONF_COORDINATOR]
async_add_entities(
[
FritzBoxTemplate(coordinator, ain)
for ain in coordinator.data.templates.keys()
]
)
class FritzBoxTemplate(FritzBoxEntity, ButtonEntity):
"""Interface between FritzhomeTemplate and hass."""
@property
def entity(self) -> FritzhomeTemplate:
"""Return the template entity."""
return self.coordinator.data.templates[self.ain]
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
return DeviceInfo(
name=self.entity.name,
identifiers={(FRITZBOX_DOMAIN, self.ain)},
configuration_url=self.coordinator.configuration_url,
manufacturer="AVM",
model="SmartHome Template",
)
async def async_press(self) -> None:
"""Apply template and refresh."""
await self.hass.async_add_executor_job(self.apply_template)
await self.coordinator.async_refresh()
def apply_template(self) -> None:
"""Use Fritzhome to apply the template via ain."""
self.coordinator.fritz.apply_template(self.ain)

View file

@ -21,7 +21,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from . import FritzBoxDeviceEntity
from .const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_HOLIDAY_MODE,
@ -55,13 +55,13 @@ async def async_setup_entry(
async_add_entities(
[
FritzboxThermostat(coordinator, ain)
for ain, device in coordinator.data.items()
for ain, device in coordinator.data.devices.items()
if device.has_thermostat
]
)
class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
"""The thermostat class for FRITZ!SmartHome thermostats."""
_attr_precision = PRECISION_HALVES
@ -73,18 +73,18 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
@property
def current_temperature(self) -> float:
"""Return the current temperature."""
if self.device.has_temperature_sensor and self.device.temperature is not None:
return self.device.temperature # type: ignore [no-any-return]
return self.device.actual_temperature # type: ignore [no-any-return]
if self.entity.has_temperature_sensor and self.entity.temperature is not None:
return self.entity.temperature # type: ignore [no-any-return]
return self.entity.actual_temperature # type: ignore [no-any-return]
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
if self.device.target_temperature == ON_API_TEMPERATURE:
if self.entity.target_temperature == ON_API_TEMPERATURE:
return ON_REPORT_SET_TEMPERATURE
if self.device.target_temperature == OFF_API_TEMPERATURE:
if self.entity.target_temperature == OFF_API_TEMPERATURE:
return OFF_REPORT_SET_TEMPERATURE
return self.device.target_temperature # type: ignore [no-any-return]
return self.entity.target_temperature # type: ignore [no-any-return]
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
@ -94,14 +94,14 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
elif kwargs.get(ATTR_TEMPERATURE) is not None:
temperature = kwargs[ATTR_TEMPERATURE]
await self.hass.async_add_executor_job(
self.device.set_target_temperature, temperature
self.entity.set_target_temperature, temperature
)
await self.coordinator.async_refresh()
@property
def hvac_mode(self) -> str:
"""Return the current operation mode."""
if self.device.target_temperature in (
if self.entity.target_temperature in (
OFF_REPORT_SET_TEMPERATURE,
OFF_API_TEMPERATURE,
):
@ -120,15 +120,15 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
else:
await self.async_set_temperature(
temperature=self.device.comfort_temperature
temperature=self.entity.comfort_temperature
)
@property
def preset_mode(self) -> str | None:
"""Return current preset mode."""
if self.device.target_temperature == self.device.comfort_temperature:
if self.entity.target_temperature == self.entity.comfort_temperature:
return PRESET_COMFORT
if self.device.target_temperature == self.device.eco_temperature:
if self.entity.target_temperature == self.entity.eco_temperature:
return PRESET_ECO
return None
@ -141,10 +141,10 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
"""Set preset mode."""
if preset_mode == PRESET_COMFORT:
await self.async_set_temperature(
temperature=self.device.comfort_temperature
temperature=self.entity.comfort_temperature
)
elif preset_mode == PRESET_ECO:
await self.async_set_temperature(temperature=self.device.eco_temperature)
await self.async_set_temperature(temperature=self.entity.eco_temperature)
@property
def min_temp(self) -> int:
@ -160,17 +160,17 @@ class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
def extra_state_attributes(self) -> ClimateExtraAttributes:
"""Return the device specific state attributes."""
attrs: ClimateExtraAttributes = {
ATTR_STATE_BATTERY_LOW: self.device.battery_low,
ATTR_STATE_BATTERY_LOW: self.entity.battery_low,
}
# the following attributes are available since fritzos 7
if self.device.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self.device.battery_level
if self.device.holiday_active is not None:
attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active
if self.device.summer_active is not None:
attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active
if self.device.window_open is not None:
attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open
if self.entity.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self.entity.battery_level
if self.entity.holiday_active is not None:
attrs[ATTR_STATE_HOLIDAY_MODE] = self.entity.holiday_active
if self.entity.summer_active is not None:
attrs[ATTR_STATE_SUMMER_MODE] = self.entity.summer_active
if self.entity.window_open is not None:
attrs[ATTR_STATE_WINDOW_OPEN] = self.entity.window_open
return attrs

View file

@ -26,6 +26,7 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__package__)
PLATFORMS: Final[list[Platform]] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.LIGHT,

View file

@ -1,9 +1,11 @@
"""Data update coordinator for AVM FRITZ!SmartHome devices."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
from pyfritzhome.devicetypes import FritzhomeTemplate
import requests
from homeassistant.config_entries import ConfigEntry
@ -14,7 +16,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import CONF_CONNECTIONS, DOMAIN, LOGGER
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator):
@dataclass
class FritzboxCoordinatorData:
"""Data Type of FritzboxDataUpdateCoordinator's data."""
devices: dict[str, FritzhomeDevice]
templates: dict[str, FritzhomeTemplate]
class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
"""Fritzbox Smarthome device data update coordinator."""
configuration_url: str
@ -31,10 +41,11 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=30),
)
def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]:
def _update_fritz_devices(self) -> FritzboxCoordinatorData:
"""Update all fritzbox device data."""
try:
self.fritz.update_devices()
self.fritz.update_templates()
except requests.exceptions.ConnectionError as ex:
raise UpdateFailed from ex
except requests.exceptions.HTTPError:
@ -44,9 +55,10 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator):
except LoginError as ex:
raise ConfigEntryAuthFailed from ex
self.fritz.update_devices()
self.fritz.update_templates()
devices = self.fritz.get_devices()
data = {}
device_data = {}
for device in devices:
# assume device as unavailable, see #55799
if (
@ -61,9 +73,15 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator):
LOGGER.debug("Assume device %s as unavailable", device.name)
device.present = False
data[device.ain] = device
return data
device_data[device.ain] = device
async def _async_update_data(self) -> dict[str, FritzhomeDevice]:
templates = self.fritz.get_templates()
template_data = {}
for template in templates:
template_data[template.ain] = template
return FritzboxCoordinatorData(devices=device_data, templates=template_data)
async def _async_update_data(self) -> FritzboxCoordinatorData:
"""Fetch all device data."""
return await self.hass.async_add_executor_job(self._update_fritz_devices)

View file

@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from . import FritzBoxDeviceEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
@ -25,12 +25,12 @@ async def async_setup_entry(
async_add_entities(
FritzboxCover(coordinator, ain)
for ain, device in coordinator.data.items()
for ain, device in coordinator.data.devices.items()
if device.has_blind
)
class FritzboxCover(FritzBoxEntity, CoverEntity):
class FritzboxCover(FritzBoxDeviceEntity, CoverEntity):
"""The cover class for FRITZ!SmartHome covers."""
_attr_device_class = CoverDeviceClass.BLIND
@ -45,34 +45,34 @@ class FritzboxCover(FritzBoxEntity, CoverEntity):
def current_cover_position(self) -> int | None:
"""Return the current position."""
position = None
if self.device.levelpercentage is not None:
position = 100 - self.device.levelpercentage
if self.entity.levelpercentage is not None:
position = 100 - self.entity.levelpercentage
return position
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self.device.levelpercentage is None:
if self.entity.levelpercentage is None:
return None
return self.device.levelpercentage == 100 # type: ignore [no-any-return]
return self.entity.levelpercentage == 100 # type: ignore [no-any-return]
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self.hass.async_add_executor_job(self.device.set_blind_open)
await self.hass.async_add_executor_job(self.entity.set_blind_open)
await self.coordinator.async_refresh()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self.hass.async_add_executor_job(self.device.set_blind_close)
await self.hass.async_add_executor_job(self.entity.set_blind_close)
await self.coordinator.async_refresh()
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
await self.hass.async_add_executor_job(
self.device.set_level_percentage, 100 - kwargs[ATTR_POSITION]
self.entity.set_level_percentage, 100 - kwargs[ATTR_POSITION]
)
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self.hass.async_add_executor_job(self.device.set_blind_stop)
await self.hass.async_add_executor_job(self.entity.set_blind_stop)
await self.coordinator.async_refresh()

View file

@ -23,11 +23,13 @@ async def async_get_config_entry_diagnostics(
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"data": {},
}
if not isinstance(coordinator.data, dict):
return diag_data
entities: dict[str, dict] = {
**coordinator.data.devices,
**coordinator.data.templates,
}
diag_data["data"] = {
ain: {k: v for k, v in vars(dev).items() if not k.startswith("_")}
for ain, dev in coordinator.data.items()
ain: {k: v for k, v in vars(entity).items() if not k.startswith("_")}
for ain, entity in entities.items()
}
return diag_data

View file

@ -16,7 +16,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from . import FritzBoxDeviceEntity
from .const import (
COLOR_MODE,
COLOR_TEMP_MODE,
@ -36,7 +36,7 @@ async def async_setup_entry(
entities: list[FritzboxLight] = []
coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
for ain, device in coordinator.data.items():
for ain, device in coordinator.data.devices.items():
if not device.has_lightbulb:
continue
@ -58,7 +58,7 @@ async def async_setup_entry(
async_add_entities(entities)
class FritzboxLight(FritzBoxEntity, LightEntity):
class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
"""The light class for FRITZ!SmartHome lightbulbs."""
def __init__(
@ -88,36 +88,36 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
@property
def is_on(self) -> bool:
"""If the light is currently on or off."""
return self.device.state # type: ignore [no-any-return]
return self.entity.state # type: ignore [no-any-return]
@property
def brightness(self) -> int:
"""Return the current Brightness."""
return self.device.level # type: ignore [no-any-return]
return self.entity.level # type: ignore [no-any-return]
@property
def hs_color(self) -> tuple[float, float] | None:
"""Return the hs color value."""
if self.device.color_mode != COLOR_MODE:
if self.entity.color_mode != COLOR_MODE:
return None
hue = self.device.hue
saturation = self.device.saturation
hue = self.entity.hue
saturation = self.entity.saturation
return (hue, float(saturation) * 100.0 / 255.0)
@property
def color_temp_kelvin(self) -> int | None:
"""Return the CT color value."""
if self.device.color_mode != COLOR_TEMP_MODE:
if self.entity.color_mode != COLOR_TEMP_MODE:
return None
return self.device.color_temp # type: ignore [no-any-return]
return self.entity.color_temp # type: ignore [no-any-return]
@property
def color_mode(self) -> ColorMode:
"""Return the color mode of the light."""
if self.device.color_mode == COLOR_MODE:
if self.entity.color_mode == COLOR_MODE:
return ColorMode.HS
return ColorMode.COLOR_TEMP
@ -130,7 +130,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
"""Turn the light on."""
if kwargs.get(ATTR_BRIGHTNESS) is not None:
level = kwargs[ATTR_BRIGHTNESS]
await self.hass.async_add_executor_job(self.device.set_level, level)
await self.hass.async_add_executor_job(self.entity.set_level, level)
if kwargs.get(ATTR_HS_COLOR) is not None:
# Try setunmappedcolor first. This allows free color selection,
# but we don't know if its supported by all devices.
@ -139,7 +139,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
unmapped_hue = int(kwargs[ATTR_HS_COLOR][0] % 360)
unmapped_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0)
await self.hass.async_add_executor_job(
self.device.set_unmapped_color, (unmapped_hue, unmapped_saturation)
self.entity.set_unmapped_color, (unmapped_hue, unmapped_saturation)
)
# This will raise 400 BAD REQUEST if the setunmappedcolor is not available
except HTTPError as err:
@ -157,18 +157,18 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
key=lambda x: abs(x - unmapped_saturation),
)
await self.hass.async_add_executor_job(
self.device.set_color, (hue, saturation)
self.entity.set_color, (hue, saturation)
)
if kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
await self.hass.async_add_executor_job(
self.device.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN]
self.entity.set_color_temp, kwargs[ATTR_COLOR_TEMP_KELVIN]
)
await self.hass.async_add_executor_job(self.device.set_state_on)
await self.hass.async_add_executor_job(self.entity.set_state_on)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
await self.hass.async_add_executor_job(self.device.set_state_off)
await self.hass.async_add_executor_job(self.entity.set_state_off)
await self.coordinator.async_refresh()

View file

@ -30,7 +30,7 @@ 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 FritzBoxDeviceEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
from .model import FritzEntityDescriptionMixinBase
@ -220,14 +220,14 @@ async def async_setup_entry(
async_add_entities(
[
FritzBoxSensor(coordinator, ain, description)
for ain, device in coordinator.data.items()
for ain, device in coordinator.data.devices.items()
for description in SENSOR_TYPES
if description.suitable(device)
]
)
class FritzBoxSensor(FritzBoxEntity, SensorEntity):
class FritzBoxSensor(FritzBoxDeviceEntity, SensorEntity):
"""The entity class for FRITZ!SmartHome sensors."""
entity_description: FritzSensorEntityDescription
@ -235,4 +235,4 @@ class FritzBoxSensor(FritzBoxEntity, SensorEntity):
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.native_value(self.device)
return self.entity_description.native_value(self.entity)

View file

@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxEntity
from . import FritzBoxDeviceEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
@ -21,26 +21,26 @@ async def async_setup_entry(
async_add_entities(
[
FritzboxSwitch(coordinator, ain)
for ain, device in coordinator.data.items()
for ain, device in coordinator.data.devices.items()
if device.has_switch
]
)
class FritzboxSwitch(FritzBoxEntity, SwitchEntity):
class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
"""The switch class for FRITZ!SmartHome switches."""
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self.device.switch_state # type: ignore [no-any-return]
return self.entity.switch_state # type: ignore [no-any-return]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.hass.async_add_executor_job(self.device.set_switch_state_on)
await self.hass.async_add_executor_job(self.entity.set_switch_state_on)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.hass.async_add_executor_job(self.device.set_switch_state_off)
await self.hass.async_add_executor_job(self.entity.set_switch_state_off)
await self.coordinator.async_refresh()

View file

@ -24,6 +24,7 @@ async def setup_config_entry(
unique_id: str = "any",
device: Mock = None,
fritz: Mock = None,
template: Mock = None,
) -> bool:
"""Do setup of a MockConfigEntry."""
entry = MockConfigEntry(
@ -34,13 +35,17 @@ async def setup_config_entry(
entry.add_to_hass(hass)
if device is not None and fritz is not None:
fritz().get_devices.return_value = [device]
if template is not None and fritz is not None:
fritz().get_templates.return_value = [template]
result = await hass.config_entries.async_setup(entry.entry_id)
if device is not None:
await hass.async_block_till_done()
return result
class FritzDeviceBaseMock(Mock):
class FritzEntityBaseMock(Mock):
"""base mock of a AVM Fritz!Box binary sensor device."""
ain = CONF_FAKE_AIN
@ -49,7 +54,7 @@ class FritzDeviceBaseMock(Mock):
productname = CONF_FAKE_PRODUCTNAME
class FritzDeviceBinarySensorMock(FritzDeviceBaseMock):
class FritzDeviceBinarySensorMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box binary sensor device."""
alert_state = "fake_state"
@ -65,7 +70,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock):
present = True
class FritzDeviceClimateMock(FritzDeviceBaseMock):
class FritzDeviceClimateMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box climate device."""
actual_temperature = 18.0
@ -96,7 +101,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock):
scheduled_preset = PRESET_ECO
class FritzDeviceSensorMock(FritzDeviceBaseMock):
class FritzDeviceSensorMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box sensor device."""
battery_level = 23
@ -115,7 +120,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock):
rel_humidity = 42
class FritzDeviceSwitchMock(FritzDeviceBaseMock):
class FritzDeviceSwitchMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box switch device."""
battery_level = None
@ -137,7 +142,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock):
temperature = 1.23
class FritzDeviceLightMock(FritzDeviceBaseMock):
class FritzDeviceLightMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box light device."""
fw_version = "1.2.3"
@ -153,7 +158,7 @@ class FritzDeviceLightMock(FritzDeviceBaseMock):
state = True
class FritzDeviceCoverMock(FritzDeviceBaseMock):
class FritzDeviceCoverMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box cover device."""
fw_version = "1.2.3"

View file

@ -0,0 +1,43 @@
"""Tests for AVM Fritz!Box templates."""
from unittest.mock import Mock
from homeassistant.components.button import DOMAIN, SERVICE_PRESS
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
CONF_DEVICES,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from . import FritzEntityBaseMock, setup_config_entry
from .const import CONF_FAKE_NAME, MOCK_CONFIG
ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}"
async def test_setup(hass: HomeAssistant, fritz: Mock):
"""Test if is initialized correctly."""
template = FritzEntityBaseMock()
assert await setup_config_entry(
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], fritz=fritz, template=template
)
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME
assert state.state == STATE_UNKNOWN
async def test_apply_template(hass: HomeAssistant, fritz: Mock):
"""Test if applies works."""
template = FritzEntityBaseMock()
assert await setup_config_entry(
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], fritz=fritz, template=template
)
assert await hass.services.async_call(
DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert fritz().apply_template.call_count == 1

View file

@ -167,7 +167,9 @@ async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock)
assert await hass.config_entries.async_setup(entry.entry_id)
assert fritz().update_devices.call_count == 2
assert fritz().update_templates.call_count == 1
assert fritz().get_devices.call_count == 1
assert fritz().get_templates.call_count == 1
assert fritz().login.call_count == 2
@ -187,6 +189,7 @@ async def test_coordinator_update_after_password_change(
assert not await hass.config_entries.async_setup(entry.entry_id)
assert fritz().update_devices.call_count == 1
assert fritz().get_devices.call_count == 0
assert fritz().get_templates.call_count == 0
assert fritz().login.call_count == 2