Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The devolo Home Network integration."""
2 
3 from __future__ import annotations
4 
5 from asyncio import Semaphore
6 from dataclasses import dataclass
7 import logging
8 from typing import Any
9 
10 from devolo_plc_api import Device
11 from devolo_plc_api.device_api import (
12  ConnectedStationInfo,
13  NeighborAPInfo,
14  UpdateFirmwareCheck,
15  WifiGuestAccessGet,
16 )
17 from devolo_plc_api.exceptions.device import (
18  DeviceNotFound,
19  DevicePasswordProtected,
20  DeviceUnavailable,
21 )
22 from devolo_plc_api.plcnet_api import LogicalNetwork
23 
24 from homeassistant.components import zeroconf
25 from homeassistant.config_entries import ConfigEntry
26 from homeassistant.const import (
27  CONF_IP_ADDRESS,
28  CONF_PASSWORD,
29  EVENT_HOMEASSISTANT_STOP,
30  Platform,
31 )
32 from homeassistant.core import Event, HomeAssistant, callback
33 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
34 from homeassistant.helpers import device_registry as dr
35 from homeassistant.helpers.httpx_client import get_async_client
36 from homeassistant.helpers.update_coordinator import UpdateFailed
37 
38 from .const import (
39  CONNECTED_PLC_DEVICES,
40  CONNECTED_WIFI_CLIENTS,
41  DOMAIN,
42  FIRMWARE_UPDATE_INTERVAL,
43  LAST_RESTART,
44  LONG_UPDATE_INTERVAL,
45  NEIGHBORING_WIFI_NETWORKS,
46  REGULAR_FIRMWARE,
47  SHORT_UPDATE_INTERVAL,
48  SWITCH_GUEST_WIFI,
49  SWITCH_LEDS,
50 )
51 from .coordinator import DevoloDataUpdateCoordinator
52 
53 _LOGGER = logging.getLogger(__name__)
54 
55 type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData]
56 
57 
58 @dataclass
60  """The devolo Home Network data."""
61 
62  device: Device
63  coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
64 
65 
67  hass: HomeAssistant, entry: DevoloHomeNetworkConfigEntry
68 ) -> bool:
69  """Set up devolo Home Network from a config entry."""
70  zeroconf_instance = await zeroconf.async_get_async_instance(hass)
71  async_client = get_async_client(hass)
72  device_registry = dr.async_get(hass)
73  semaphore = Semaphore(1)
74 
75  try:
76  device = Device(
77  ip=entry.data[CONF_IP_ADDRESS], zeroconf_instance=zeroconf_instance
78  )
79  await device.async_connect(session_instance=async_client)
80  device.password = entry.data.get(
81  CONF_PASSWORD,
82  "", # This key was added in HA Core 2022.6
83  )
84  except DeviceNotFound as err:
85  raise ConfigEntryNotReady(
86  translation_domain=DOMAIN,
87  translation_key="connection_failed",
88  translation_placeholders={"ip_address": entry.data[CONF_IP_ADDRESS]},
89  ) from err
90 
91  entry.runtime_data = DevoloHomeNetworkData(device=device, coordinators={})
92 
93  async def async_update_firmware_available() -> UpdateFirmwareCheck:
94  """Fetch data from API endpoint."""
95  assert device.device
96  update_sw_version(device_registry, device)
97  try:
98  return await device.device.async_check_firmware_available()
99  except DeviceUnavailable as err:
100  raise UpdateFailed(
101  translation_domain=DOMAIN,
102  translation_key="update_failed",
103  translation_placeholders={"error": str(err)},
104  ) from err
105 
106  async def async_update_connected_plc_devices() -> LogicalNetwork:
107  """Fetch data from API endpoint."""
108  assert device.plcnet
109  update_sw_version(device_registry, device)
110  try:
111  return await device.plcnet.async_get_network_overview()
112  except DeviceUnavailable as err:
113  raise UpdateFailed(
114  translation_domain=DOMAIN,
115  translation_key="update_failed",
116  translation_placeholders={"error": str(err)},
117  ) from err
118 
119  async def async_update_guest_wifi_status() -> WifiGuestAccessGet:
120  """Fetch data from API endpoint."""
121  assert device.device
122  update_sw_version(device_registry, device)
123  try:
124  return await device.device.async_get_wifi_guest_access()
125  except DeviceUnavailable as err:
126  raise UpdateFailed(
127  translation_domain=DOMAIN,
128  translation_key="update_failed",
129  translation_placeholders={"error": str(err)},
130  ) from err
131  except DevicePasswordProtected as err:
132  raise ConfigEntryAuthFailed(
133  translation_domain=DOMAIN, translation_key="password_wrong"
134  ) from err
135 
136  async def async_update_led_status() -> bool:
137  """Fetch data from API endpoint."""
138  assert device.device
139  update_sw_version(device_registry, device)
140  try:
141  return await device.device.async_get_led_setting()
142  except DeviceUnavailable as err:
143  raise UpdateFailed(
144  translation_domain=DOMAIN,
145  translation_key="update_failed",
146  translation_placeholders={"error": str(err)},
147  ) from err
148 
149  async def async_update_last_restart() -> int:
150  """Fetch data from API endpoint."""
151  assert device.device
152  update_sw_version(device_registry, device)
153  try:
154  return await device.device.async_uptime()
155  except DeviceUnavailable as err:
156  raise UpdateFailed(
157  translation_domain=DOMAIN,
158  translation_key="update_failed",
159  translation_placeholders={"error": str(err)},
160  ) from err
161  except DevicePasswordProtected as err:
162  raise ConfigEntryAuthFailed(
163  translation_domain=DOMAIN, translation_key="password_wrong"
164  ) from err
165 
166  async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]:
167  """Fetch data from API endpoint."""
168  assert device.device
169  update_sw_version(device_registry, device)
170  try:
171  return await device.device.async_get_wifi_connected_station()
172  except DeviceUnavailable as err:
173  raise UpdateFailed(
174  translation_domain=DOMAIN,
175  translation_key="update_failed",
176  translation_placeholders={"error": str(err)},
177  ) from err
178 
179  async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]:
180  """Fetch data from API endpoint."""
181  assert device.device
182  update_sw_version(device_registry, device)
183  try:
184  return await device.device.async_get_wifi_neighbor_access_points()
185  except DeviceUnavailable as err:
186  raise UpdateFailed(
187  translation_domain=DOMAIN,
188  translation_key="update_failed",
189  translation_placeholders={"error": str(err)},
190  ) from err
191 
192  async def disconnect(event: Event) -> None:
193  """Disconnect from device."""
194  await device.async_disconnect()
195 
196  coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {}
197  if device.plcnet:
198  coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator(
199  hass,
200  _LOGGER,
201  config_entry=entry,
202  name=CONNECTED_PLC_DEVICES,
203  semaphore=semaphore,
204  update_method=async_update_connected_plc_devices,
205  update_interval=LONG_UPDATE_INTERVAL,
206  )
207  if device.device and "led" in device.device.features:
208  coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator(
209  hass,
210  _LOGGER,
211  config_entry=entry,
212  name=SWITCH_LEDS,
213  semaphore=semaphore,
214  update_method=async_update_led_status,
215  update_interval=SHORT_UPDATE_INTERVAL,
216  )
217  if device.device and "restart" in device.device.features:
218  coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator(
219  hass,
220  _LOGGER,
221  config_entry=entry,
222  name=LAST_RESTART,
223  semaphore=semaphore,
224  update_method=async_update_last_restart,
225  update_interval=SHORT_UPDATE_INTERVAL,
226  )
227  if device.device and "update" in device.device.features:
228  coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator(
229  hass,
230  _LOGGER,
231  config_entry=entry,
232  name=REGULAR_FIRMWARE,
233  semaphore=semaphore,
234  update_method=async_update_firmware_available,
235  update_interval=FIRMWARE_UPDATE_INTERVAL,
236  )
237  if device.device and "wifi1" in device.device.features:
238  coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator(
239  hass,
240  _LOGGER,
241  config_entry=entry,
242  name=CONNECTED_WIFI_CLIENTS,
243  semaphore=semaphore,
244  update_method=async_update_wifi_connected_station,
245  update_interval=SHORT_UPDATE_INTERVAL,
246  )
247  coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator(
248  hass,
249  _LOGGER,
250  config_entry=entry,
251  name=NEIGHBORING_WIFI_NETWORKS,
252  semaphore=semaphore,
253  update_method=async_update_wifi_neighbor_access_points,
254  update_interval=LONG_UPDATE_INTERVAL,
255  )
256  coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator(
257  hass,
258  _LOGGER,
259  config_entry=entry,
260  name=SWITCH_GUEST_WIFI,
261  semaphore=semaphore,
262  update_method=async_update_guest_wifi_status,
263  update_interval=SHORT_UPDATE_INTERVAL,
264  )
265 
266  for coordinator in coordinators.values():
267  await coordinator.async_config_entry_first_refresh()
268 
269  entry.runtime_data.coordinators = coordinators
270 
271  await hass.config_entries.async_forward_entry_setups(entry, platforms(device))
272 
273  entry.async_on_unload(
274  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect)
275  )
276 
277  return True
278 
279 
281  hass: HomeAssistant, entry: DevoloHomeNetworkConfigEntry
282 ) -> bool:
283  """Unload a config entry."""
284  device = entry.runtime_data.device
285  unload_ok = await hass.config_entries.async_unload_platforms(
286  entry, platforms(device)
287  )
288  if unload_ok:
289  await device.async_disconnect()
290 
291  return unload_ok
292 
293 
294 @callback
295 def platforms(device: Device) -> set[Platform]:
296  """Assemble supported platforms."""
297  supported_platforms = {Platform.BUTTON, Platform.SENSOR, Platform.SWITCH}
298  if device.plcnet:
299  supported_platforms.add(Platform.BINARY_SENSOR)
300  if device.device and "wifi1" in device.device.features:
301  supported_platforms.add(Platform.DEVICE_TRACKER)
302  supported_platforms.add(Platform.IMAGE)
303  if device.device and "update" in device.device.features:
304  supported_platforms.add(Platform.UPDATE)
305  return supported_platforms
306 
307 
308 @callback
309 def update_sw_version(device_registry: dr.DeviceRegistry, device: Device) -> None:
310  """Update device registry with new firmware version."""
311  if (
312  device_entry := device_registry.async_get_device(
313  identifiers={(DOMAIN, str(device.serial_number))}
314  )
315  ) and device_entry.sw_version != device.firmware_version:
316  device_registry.async_update_device(
317  device_id=device_entry.id, sw_version=device.firmware_version
318  )
set[Platform] platforms(Device device)
Definition: __init__.py:295
None update_sw_version(dr.DeviceRegistry device_registry, Device device)
Definition: __init__.py:309
bool async_setup_entry(HomeAssistant hass, DevoloHomeNetworkConfigEntry entry)
Definition: __init__.py:68
bool async_unload_entry(HomeAssistant hass, DevoloHomeNetworkConfigEntry entry)
Definition: __init__.py:282
httpx.AsyncClient get_async_client(HomeAssistant hass, bool verify_ssl=True)
Definition: httpx_client.py:41