From ccad6a8f07067aa938d5822e1804b3af5bfbeb3a Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 31 Oct 2021 20:12:25 +0100 Subject: [PATCH] Add configuration url to AVM Fritz!Smarthome (#57711) * add configuration url * extend data update coordinator * improve exception handling during data update * store coordinator after first refresh * fix light init --- homeassistant/components/fritzbox/__init__.py | 62 +++-------------- .../components/fritzbox/binary_sensor.py | 4 +- .../components/fritzbox/coordinator.py | 68 +++++++++++++++++++ homeassistant/components/fritzbox/light.py | 6 +- tests/components/fritzbox/test_init.py | 18 ++++- 5 files changed, 98 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/fritzbox/coordinator.py diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 0ddd0b8d417..e72e1d86fc1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,10 +1,7 @@ """Support for AVM FRITZ!SmartHome devices.""" from __future__ import annotations -from datetime import timedelta - from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError -import requests from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -18,10 +15,7 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_STATE_DEVICE_LOCKED, @@ -32,6 +26,7 @@ from .const import ( LOGGER, PLATFORMS, ) +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzExtraAttributes @@ -53,52 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_CONNECTIONS: fritz, } - def _update_fritz_devices() -> dict[str, FritzhomeDevice]: - """Update all fritzbox device data.""" - try: - devices = fritz.get_devices() - except requests.exceptions.HTTPError: - # If the device rebooted, login again - try: - fritz.login() - except requests.exceptions.HTTPError as ex: - raise ConfigEntryAuthFailed from ex - devices = fritz.get_devices() - - data = {} - fritz.update_devices() - for device in devices: - # assume device as unavailable, see #55799 - if ( - device.has_powermeter - and device.present - and hasattr(device, "voltage") - and device.voltage <= 0 - and device.power <= 0 - and device.energy <= 0 - ): - LOGGER.debug("Assume device %s as unavailable", device.name) - device.present = False - - data[device.ain] = device - return data - - async def async_update_coordinator() -> dict[str, FritzhomeDevice]: - """Fetch all device data.""" - return await hass.async_add_executor_job(_update_fritz_devices) - - hass.data[DOMAIN][entry.entry_id][ - CONF_COORDINATOR - ] = coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{entry.entry_id}", - update_method=async_update_coordinator, - update_interval=timedelta(seconds=30), - ) + coordinator = FritzboxDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() + hass.data[DOMAIN][entry.entry_id][CONF_COORDINATOR] = coordinator + def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None: """Update unique ID of entity entry.""" if ( @@ -142,9 +97,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class FritzBoxEntity(CoordinatorEntity): """Basis FritzBox entity.""" + coordinator: FritzboxDataUpdateCoordinator + def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: EntityDescription | None = None, ) -> None: @@ -174,11 +131,12 @@ class FritzBoxEntity(CoordinatorEntity): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( + name=self.device.name, identifiers={(DOMAIN, self.ain)}, manufacturer=self.device.manufacturer, model=self.device.productname, - name=self.device.name, sw_version=self.device.fw_version, + configuration_url=self.coordinator.configuration_url, ) @property diff --git a/homeassistant/components/fritzbox/binary_sensor.py b/homeassistant/components/fritzbox/binary_sensor.py index 1317710c570..b0f5e63d424 100644 --- a/homeassistant/components/fritzbox/binary_sensor.py +++ b/homeassistant/components/fritzbox/binary_sensor.py @@ -15,10 +15,10 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import FritzBoxEntity from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN +from .coordinator import FritzboxDataUpdateCoordinator from .model import FritzEntityDescriptionMixinBase @@ -70,7 +70,7 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, entity_description: FritzBinarySensorEntityDescription, ) -> None: diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py new file mode 100644 index 00000000000..69ab0b4c274 --- /dev/null +++ b/homeassistant/components/fritzbox/coordinator.py @@ -0,0 +1,68 @@ +"""Data update coordinator for AVM FRITZ!SmartHome devices.""" +from __future__ import annotations + +from datetime import timedelta + +from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError +import requests + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, DOMAIN, LOGGER + + +class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): + """Fritzbox Smarthome device data update coordinator.""" + + configuration_url: str + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the Fritzbox Smarthome device coordinator.""" + self.entry = entry + self.fritz: Fritzhome = hass.data[DOMAIN][self.entry.entry_id][CONF_CONNECTIONS] + self.configuration_url = self.fritz.get_prefixed_host() + super().__init__( + hass, + LOGGER, + name=entry.entry_id, + update_interval=timedelta(seconds=30), + ) + + def _update_fritz_devices(self) -> dict[str, FritzhomeDevice]: + """Update all fritzbox device data.""" + try: + devices = self.fritz.get_devices() + except requests.exceptions.ConnectionError as ex: + raise ConfigEntryNotReady from ex + except requests.exceptions.HTTPError: + # If the device rebooted, login again + try: + self.fritz.login() + except LoginError as ex: + raise ConfigEntryAuthFailed from ex + devices = self.fritz.get_devices() + + data = {} + self.fritz.update_devices() + for device in devices: + # assume device as unavailable, see #55799 + if ( + device.has_powermeter + and device.present + and hasattr(device, "voltage") + and device.voltage <= 0 + and device.power <= 0 + and device.energy <= 0 + ): + LOGGER.debug("Assume device %s as unavailable", device.name) + device.present = False + + data[device.ain] = device + return data + + async def _async_update_data(self) -> dict[str, FritzhomeDevice]: + """Fetch all device data.""" + return await self.hass.async_add_executor_job(self._update_fritz_devices) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 3f9e3cabfa2..272d170e13d 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Any -from pyfritzhome.fritzhomedevice import FritzhomeDevice - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -16,7 +14,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import color from . import FritzBoxEntity @@ -26,6 +23,7 @@ from .const import ( CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, ) +from .coordinator import FritzboxDataUpdateCoordinator SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} @@ -64,7 +62,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity): def __init__( self, - coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], + coordinator: FritzboxDataUpdateCoordinator, ain: str, supported_colors: dict, supported_color_temps: list[str], diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index ea0356c6af1..60828e83801 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import Mock, call, patch from pyfritzhome import LoginError -from requests.exceptions import HTTPError +from requests.exceptions import ConnectionError, HTTPError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -120,13 +120,27 @@ async def test_coordinator_update_after_password_change( ) entry.add_to_hass(hass) fritz().get_devices.side_effect = HTTPError() - fritz().login.side_effect = ["", HTTPError()] + fritz().login.side_effect = ["", LoginError("some_user")] assert not await hass.config_entries.async_setup(entry.entry_id) assert fritz().get_devices.call_count == 1 assert fritz().login.call_count == 2 +async def test_coordinator_update_when_unreachable(hass: HomeAssistant, fritz: Mock): + """Test coordinator after reboot.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], + unique_id="any", + ) + entry.add_to_hass(hass) + fritz().get_devices.side_effect = [ConnectionError(), ""] + + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + async def test_unload_remove(hass: HomeAssistant, fritz: Mock): """Test unload and remove of integration.""" fritz().get_devices.return_value = [FritzDeviceSwitchMock()]