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
This commit is contained in:
Michael 2021-10-31 20:12:25 +01:00 committed by GitHub
parent 8f51192cf0
commit ccad6a8f07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 98 additions and 60 deletions

View File

@ -1,10 +1,7 @@
"""Support for AVM FRITZ!SmartHome devices.""" """Support for AVM FRITZ!SmartHome devices."""
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
import requests
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@ -18,10 +15,7 @@ from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import CoordinatorEntity
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import ( from .const import (
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_DEVICE_LOCKED,
@ -32,6 +26,7 @@ from .const import (
LOGGER, LOGGER,
PLATFORMS, PLATFORMS,
) )
from .coordinator import FritzboxDataUpdateCoordinator
from .model import FritzExtraAttributes from .model import FritzExtraAttributes
@ -53,52 +48,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
CONF_CONNECTIONS: fritz, CONF_CONNECTIONS: fritz,
} }
def _update_fritz_devices() -> dict[str, FritzhomeDevice]: coordinator = FritzboxDataUpdateCoordinator(hass, entry)
"""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),
)
await coordinator.async_config_entry_first_refresh() 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: def _update_unique_id(entry: RegistryEntry) -> dict[str, str] | None:
"""Update unique ID of entity entry.""" """Update unique ID of entity entry."""
if ( if (
@ -142,9 +97,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
class FritzBoxEntity(CoordinatorEntity): class FritzBoxEntity(CoordinatorEntity):
"""Basis FritzBox entity.""" """Basis FritzBox entity."""
coordinator: FritzboxDataUpdateCoordinator
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], coordinator: FritzboxDataUpdateCoordinator,
ain: str, ain: str,
entity_description: EntityDescription | None = None, entity_description: EntityDescription | None = None,
) -> None: ) -> None:
@ -174,11 +131,12 @@ class FritzBoxEntity(CoordinatorEntity):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return device specific attributes.""" """Return device specific attributes."""
return DeviceInfo( return DeviceInfo(
name=self.device.name,
identifiers={(DOMAIN, self.ain)}, identifiers={(DOMAIN, self.ain)},
manufacturer=self.device.manufacturer, manufacturer=self.device.manufacturer,
model=self.device.productname, model=self.device.productname,
name=self.device.name,
sw_version=self.device.fw_version, sw_version=self.device.fw_version,
configuration_url=self.coordinator.configuration_url,
) )
@property @property

View File

@ -15,10 +15,10 @@ from homeassistant.components.binary_sensor import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
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.update_coordinator import DataUpdateCoordinator
from . import FritzBoxEntity from . import FritzBoxEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
from .coordinator import FritzboxDataUpdateCoordinator
from .model import FritzEntityDescriptionMixinBase from .model import FritzEntityDescriptionMixinBase
@ -70,7 +70,7 @@ class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], coordinator: FritzboxDataUpdateCoordinator,
ain: str, ain: str,
entity_description: FritzBinarySensorEntityDescription, entity_description: FritzBinarySensorEntityDescription,
) -> None: ) -> None:

View File

@ -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)

View File

@ -3,8 +3,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from pyfritzhome.fritzhomedevice import FritzhomeDevice
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP, ATTR_COLOR_TEMP,
@ -16,7 +14,6 @@ from homeassistant.components.light import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
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.update_coordinator import DataUpdateCoordinator
from homeassistant.util import color from homeassistant.util import color
from . import FritzBoxEntity from . import FritzBoxEntity
@ -26,6 +23,7 @@ from .const import (
CONF_COORDINATOR, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
) )
from .coordinator import FritzboxDataUpdateCoordinator
SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} SUPPORTED_COLOR_MODES = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS}
@ -64,7 +62,7 @@ class FritzboxLight(FritzBoxEntity, LightEntity):
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator[dict[str, FritzhomeDevice]], coordinator: FritzboxDataUpdateCoordinator,
ain: str, ain: str,
supported_colors: dict, supported_colors: dict,
supported_color_temps: list[str], supported_color_temps: list[str],

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
from pyfritzhome import LoginError 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.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_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) entry.add_to_hass(hass)
fritz().get_devices.side_effect = HTTPError() 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 not await hass.config_entries.async_setup(entry.entry_id)
assert fritz().get_devices.call_count == 1 assert fritz().get_devices.call_count == 1
assert fritz().login.call_count == 2 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): async def test_unload_remove(hass: HomeAssistant, fritz: Mock):
"""Test unload and remove of integration.""" """Test unload and remove of integration."""
fritz().get_devices.return_value = [FritzDeviceSwitchMock()] fritz().get_devices.return_value = [FritzDeviceSwitchMock()]