Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """UPnP/IGD integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 
8 from async_upnp_client.exceptions import UpnpConnectionError
9 
10 from homeassistant.components import ssdp
11 from homeassistant.config_entries import ConfigEntry
12 from homeassistant.const import Platform
13 from homeassistant.core import HomeAssistant
14 from homeassistant.exceptions import ConfigEntryNotReady
15 from homeassistant.helpers import device_registry as dr
16 
17 from .const import (
18  CONFIG_ENTRY_FORCE_POLL,
19  CONFIG_ENTRY_HOST,
20  CONFIG_ENTRY_MAC_ADDRESS,
21  CONFIG_ENTRY_ORIGINAL_UDN,
22  CONFIG_ENTRY_ST,
23  CONFIG_ENTRY_UDN,
24  DEFAULT_SCAN_INTERVAL,
25  DOMAIN,
26  IDENTIFIER_HOST,
27  IDENTIFIER_SERIAL_NUMBER,
28  LOGGER,
29 )
30 from .coordinator import UpnpDataUpdateCoordinator
31 from .device import async_create_device, get_preferred_location
32 
33 NOTIFICATION_ID = "upnp_notification"
34 NOTIFICATION_TITLE = "UPnP/IGD Setup"
35 
36 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
37 
38 
39 type UpnpConfigEntry = ConfigEntry[UpnpDataUpdateCoordinator]
40 
41 
42 async def async_setup_entry(hass: HomeAssistant, entry: UpnpConfigEntry) -> bool:
43  """Set up UPnP/IGD device from a config entry."""
44  LOGGER.debug("Setting up config entry: %s", entry.entry_id)
45 
46  udn = entry.data[CONFIG_ENTRY_UDN]
47  st = entry.data[CONFIG_ENTRY_ST]
48  usn = f"{udn}::{st}"
49 
50  # Register device discovered-callback.
51  device_discovered_event = asyncio.Event()
52  discovery_info: ssdp.SsdpServiceInfo | None = None
53 
54  async def device_discovered(
55  headers: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange
56  ) -> None:
57  if change == ssdp.SsdpChange.BYEBYE:
58  return
59 
60  nonlocal discovery_info
61  LOGGER.debug("Device discovered: %s, at: %s", usn, headers.ssdp_all_locations)
62  discovery_info = headers
63  device_discovered_event.set()
64 
65  cancel_discovered_callback = await ssdp.async_register_callback(
66  hass,
67  device_discovered,
68  {
69  "usn": usn,
70  },
71  )
72 
73  try:
74  async with asyncio.timeout(10):
75  await device_discovered_event.wait()
76  except TimeoutError as err:
77  raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err
78  finally:
79  cancel_discovered_callback()
80 
81  # Create device.
82  assert discovery_info is not None
83  assert discovery_info.ssdp_udn
84  assert discovery_info.ssdp_all_locations
85  force_poll = entry.options.get(CONFIG_ENTRY_FORCE_POLL, False)
86  location = get_preferred_location(discovery_info.ssdp_all_locations)
87  try:
88  device = await async_create_device(hass, location, force_poll)
89  except UpnpConnectionError as err:
90  raise ConfigEntryNotReady(
91  f"Error connecting to device at location: {location}, err: {err}"
92  ) from err
93 
94  # Try to subscribe, if configured.
95  if not force_poll:
96  await device.async_subscribe_services()
97 
98  # Unsubscribe services on unload.
99  entry.async_on_unload(device.async_unsubscribe_services)
100 
101  # Update force_poll on options update.
102  async def update_listener(hass: HomeAssistant, entry: UpnpConfigEntry):
103  """Handle options update."""
104  force_poll = entry.options.get(CONFIG_ENTRY_FORCE_POLL, False)
105  await device.async_set_force_poll(force_poll)
106 
107  entry.async_on_unload(entry.add_update_listener(update_listener))
108 
109  # Track the original UDN such that existing sensors do not change their unique_id.
110  if CONFIG_ENTRY_ORIGINAL_UDN not in entry.data:
111  hass.config_entries.async_update_entry(
112  entry=entry,
113  data={
114  **entry.data,
115  CONFIG_ENTRY_ORIGINAL_UDN: device.udn,
116  },
117  )
118  device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN]
119 
120  # Store mac address for changed UDN matching.
121  device_mac_address = await device.async_get_mac_address()
122  if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS):
123  hass.config_entries.async_update_entry(
124  entry=entry,
125  data={
126  **entry.data,
127  CONFIG_ENTRY_MAC_ADDRESS: device_mac_address,
128  CONFIG_ENTRY_HOST: device.host,
129  },
130  )
131 
132  identifiers = {(DOMAIN, device.usn)}
133  if device.host:
134  identifiers.add((IDENTIFIER_HOST, device.host))
135  if device.serial_number:
136  identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
137 
138  connections = {(dr.CONNECTION_UPNP, discovery_info.ssdp_udn)}
139  if discovery_info.ssdp_udn != device.udn:
140  connections.add((dr.CONNECTION_UPNP, device.udn))
141  if device_mac_address:
142  connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address))
143 
144  dev_registry = dr.async_get(hass)
145  device_entry = dev_registry.async_get_device(
146  identifiers=identifiers, connections=connections
147  )
148  if device_entry:
149  LOGGER.debug(
150  "Found device using connections: %s, device_entry: %s",
151  connections,
152  device_entry,
153  )
154  if not device_entry:
155  # No device found, create new device entry.
156  device_entry = dev_registry.async_get_or_create(
157  config_entry_id=entry.entry_id,
158  connections=connections,
159  identifiers=identifiers,
160  name=device.name,
161  manufacturer=device.manufacturer,
162  model=device.model_name,
163  )
164  LOGGER.debug(
165  "Created device using UDN '%s', device_entry: %s", device.udn, device_entry
166  )
167  else:
168  # Update identifier.
169  device_entry = dev_registry.async_update_device(
170  device_entry.id,
171  new_identifiers=identifiers,
172  )
173 
174  assert device_entry
175  update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
176  coordinator = UpnpDataUpdateCoordinator(
177  hass,
178  device=device,
179  device_entry=device_entry,
180  update_interval=update_interval,
181  )
182 
183  # Try an initial refresh.
184  await coordinator.async_config_entry_first_refresh()
185 
186  # Save coordinator.
187  entry.runtime_data = coordinator
188 
189  # Setup platforms, creating sensors/binary_sensors.
190  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
191 
192  return True
193 
194 
195 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
196  """Unload a UPnP/IGD device from a config entry."""
197  LOGGER.debug("Unloading config entry: %s", entry.entry_id)
198  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
dr.DeviceEntry async_create_device(HomeAssistant hass, str config_entry_id, str|None device_name, str|None device_translation_key, dict[str, str]|None device_translation_placeholders, str unique_id)
Definition: device.py:18
str get_preferred_location(set[str] locations)
Definition: device.py:56
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:195
bool async_setup_entry(HomeAssistant hass, UpnpConfigEntry entry)
Definition: __init__.py:42
None update_listener(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:30