Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Data update coordinator for AVM FRITZ!SmartHome devices."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from datetime import timedelta
7 
8 from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
9 from pyfritzhome.devicetypes import FritzhomeTemplate
10 from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
14 from homeassistant.core import HomeAssistant
15 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
16 from homeassistant.helpers import device_registry as dr, entity_registry as er
17 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
18 
19 from .const import DOMAIN, LOGGER
20 
21 type FritzboxConfigEntry = ConfigEntry[FritzboxDataUpdateCoordinator]
22 
23 
24 @dataclass
26  """Data Type of FritzboxDataUpdateCoordinator's data."""
27 
28  devices: dict[str, FritzhomeDevice]
29  templates: dict[str, FritzhomeTemplate]
30 
31 
32 class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorData]):
33  """Fritzbox Smarthome device data update coordinator."""
34 
35  config_entry: FritzboxConfigEntry
36  configuration_url: str
37  fritz: Fritzhome
38  has_templates: bool
39 
40  def __init__(self, hass: HomeAssistant, name: str) -> None:
41  """Initialize the Fritzbox Smarthome device coordinator."""
42  super().__init__(
43  hass,
44  LOGGER,
45  name=name,
46  update_interval=timedelta(seconds=30),
47  )
48 
49  self.new_devicesnew_devices: set[str] = set()
50  self.new_templatesnew_templates: set[str] = set()
51 
52  self.datadatadata = FritzboxCoordinatorData({}, {})
53 
54  async def async_setup(self) -> None:
55  """Set up the coordinator."""
56 
57  self.fritzfritz = Fritzhome(
58  host=self.config_entryconfig_entry.data[CONF_HOST],
59  user=self.config_entryconfig_entry.data[CONF_USERNAME],
60  password=self.config_entryconfig_entry.data[CONF_PASSWORD],
61  )
62 
63  try:
64  await self.hasshass.async_add_executor_job(self.fritzfritz.login)
65  except RequestConnectionError as err:
66  raise ConfigEntryNotReady from err
67  except LoginError as err:
68  raise ConfigEntryAuthFailed from err
69 
70  self.has_templateshas_templates = await self.hasshass.async_add_executor_job(
71  self.fritzfritz.has_templates
72  )
73  LOGGER.debug("enable smarthome templates: %s", self.has_templateshas_templates)
74 
75  self.configuration_urlconfiguration_url = self.fritzfritz.get_prefixed_host()
76 
77  await self.async_config_entry_first_refreshasync_config_entry_first_refresh()
78  self.cleanup_removed_devicescleanup_removed_devices(
79  list(self.datadatadata.devices) + list(self.datadatadata.templates)
80  )
81 
82  def cleanup_removed_devices(self, available_ains: list[str]) -> None:
83  """Cleanup entity and device registry from removed devices."""
84  entity_reg = er.async_get(self.hasshass)
85  for entity in er.async_entries_for_config_entry(
86  entity_reg, self.config_entryconfig_entry.entry_id
87  ):
88  if entity.unique_id.split("_")[0] not in available_ains:
89  LOGGER.debug("Removing obsolete entity entry %s", entity.entity_id)
90  entity_reg.async_remove(entity.entity_id)
91 
92  device_reg = dr.async_get(self.hasshass)
93  identifiers = {(DOMAIN, ain) for ain in available_ains}
94  for device in dr.async_entries_for_config_entry(
95  device_reg, self.config_entryconfig_entry.entry_id
96  ):
97  if not set(device.identifiers) & identifiers:
98  LOGGER.debug("Removing obsolete device entry %s", device.name)
99  device_reg.async_update_device(
100  device.id, remove_config_entry_id=self.config_entryconfig_entry.entry_id
101  )
102 
103  def _update_fritz_devices(self) -> FritzboxCoordinatorData:
104  """Update all fritzbox device data."""
105  try:
106  self.fritzfritz.update_devices(ignore_removed=False)
107  if self.has_templateshas_templates:
108  self.fritzfritz.update_templates(ignore_removed=False)
109  except RequestConnectionError as ex:
110  raise UpdateFailed from ex
111  except HTTPError:
112  # If the device rebooted, login again
113  try:
114  self.fritzfritz.login()
115  except LoginError as ex:
116  raise ConfigEntryAuthFailed from ex
117  self.fritzfritz.update_devices(ignore_removed=False)
118  if self.has_templateshas_templates:
119  self.fritzfritz.update_templates(ignore_removed=False)
120 
121  devices = self.fritzfritz.get_devices()
122  device_data = {}
123  for device in devices:
124  # assume device as unavailable, see #55799
125  if (
126  device.has_powermeter
127  and device.present
128  and isinstance(device.voltage, int)
129  and device.voltage <= 0
130  and isinstance(device.power, int)
131  and device.power <= 0
132  and device.energy <= 0
133  ):
134  LOGGER.debug("Assume device %s as unavailable", device.name)
135  device.present = False
136 
137  device_data[device.ain] = device
138 
139  template_data = {}
140  if self.has_templateshas_templates:
141  templates = self.fritzfritz.get_templates()
142  for template in templates:
143  template_data[template.ain] = template
144 
145  self.new_devicesnew_devices = device_data.keys() - self.datadatadata.devices.keys()
146  self.new_templatesnew_templates = template_data.keys() - self.datadatadata.templates.keys()
147 
148  return FritzboxCoordinatorData(devices=device_data, templates=template_data)
149 
150  async def _async_update_data(self) -> FritzboxCoordinatorData:
151  """Fetch all device data."""
152  new_data = await self.hasshass.async_add_executor_job(self._update_fritz_devices_update_fritz_devices)
153 
154  if (
155  self.datadatadata.devices.keys() - new_data.devices.keys()
156  or self.datadatadata.templates.keys() - new_data.templates.keys()
157  ):
158  self.cleanup_removed_devicescleanup_removed_devices(
159  list(new_data.devices) + list(new_data.templates)
160  )
161 
162  return new_data
None update_devices(HomeAssistant hass, ConfigEntry config_entry, dict[int, Roller] api)
Definition: helpers.py:47