diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 4bb2eee45b1..3351372421b 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -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, ) diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index b80d853e562..10ffd0b9104 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -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) diff --git a/homeassistant/components/fritzbox/button.py b/homeassistant/components/fritzbox/button.py new file mode 100644 index 00000000000..f8e5db9fcef --- /dev/null +++ b/homeassistant/components/fritzbox/button.py @@ -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) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 806f8b2303e..364a96e8d2f 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -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 diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 4831b0f6ab2..791da4540a4 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -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, diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index 47d2cdca005..c16751c8c9f 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -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) diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py index d8fc8d4f3c3..78389fe3c99 100644 --- a/homeassistant/components/fritzbox/cover.py +++ b/homeassistant/components/fritzbox/cover.py @@ -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() diff --git a/homeassistant/components/fritzbox/diagnostics.py b/homeassistant/components/fritzbox/diagnostics.py index 581fe4c6f01..403082c4f90 100644 --- a/homeassistant/components/fritzbox/diagnostics.py +++ b/homeassistant/components/fritzbox/diagnostics.py @@ -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 diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 0f7037843d8..a63d3e06008 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -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() diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index ab341fb1520..9a86ffae5ed 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -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) diff --git a/homeassistant/components/fritzbox/switch.py b/homeassistant/components/fritzbox/switch.py index 79f256bded0..029627e191c 100644 --- a/homeassistant/components/fritzbox/switch.py +++ b/homeassistant/components/fritzbox/switch.py @@ -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() diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index acafd19c924..fe7d11068fd 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -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" diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py new file mode 100644 index 00000000000..b362e7dcfb1 --- /dev/null +++ b/tests/components/fritzbox/test_button.py @@ -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 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index fdf787d4cf2..d68d9e1679c 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -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