1 """UPnP/IGD integration."""
3 from __future__
import annotations
6 from datetime
import timedelta
8 from async_upnp_client.exceptions
import UpnpConnectionError
18 CONFIG_ENTRY_FORCE_POLL,
20 CONFIG_ENTRY_MAC_ADDRESS,
21 CONFIG_ENTRY_ORIGINAL_UDN,
24 DEFAULT_SCAN_INTERVAL,
27 IDENTIFIER_SERIAL_NUMBER,
30 from .coordinator
import UpnpDataUpdateCoordinator
31 from .device
import async_create_device, get_preferred_location
33 NOTIFICATION_ID =
"upnp_notification"
34 NOTIFICATION_TITLE =
"UPnP/IGD Setup"
36 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
39 type UpnpConfigEntry = ConfigEntry[UpnpDataUpdateCoordinator]
43 """Set up UPnP/IGD device from a config entry."""
44 LOGGER.debug(
"Setting up config entry: %s", entry.entry_id)
46 udn = entry.data[CONFIG_ENTRY_UDN]
47 st = entry.data[CONFIG_ENTRY_ST]
51 device_discovered_event = asyncio.Event()
54 async
def device_discovered(
55 headers: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange
57 if change == ssdp.SsdpChange.BYEBYE:
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()
65 cancel_discovered_callback = await ssdp.async_register_callback(
74 async
with asyncio.timeout(10):
75 await device_discovered_event.wait()
76 except TimeoutError
as err:
79 cancel_discovered_callback()
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)
89 except UpnpConnectionError
as err:
91 f
"Error connecting to device at location: {location}, err: {err}"
96 await device.async_subscribe_services()
99 entry.async_on_unload(device.async_unsubscribe_services)
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)
107 entry.async_on_unload(entry.add_update_listener(update_listener))
110 if CONFIG_ENTRY_ORIGINAL_UDN
not in entry.data:
111 hass.config_entries.async_update_entry(
115 CONFIG_ENTRY_ORIGINAL_UDN: device.udn,
118 device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN]
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(
127 CONFIG_ENTRY_MAC_ADDRESS: device_mac_address,
128 CONFIG_ENTRY_HOST: device.host,
132 identifiers = {(DOMAIN, device.usn)}
134 identifiers.add((IDENTIFIER_HOST, device.host))
135 if device.serial_number:
136 identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
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))
144 dev_registry = dr.async_get(hass)
145 device_entry = dev_registry.async_get_device(
146 identifiers=identifiers, connections=connections
150 "Found device using connections: %s, device_entry: %s",
156 device_entry = dev_registry.async_get_or_create(
157 config_entry_id=entry.entry_id,
158 connections=connections,
159 identifiers=identifiers,
161 manufacturer=device.manufacturer,
162 model=device.model_name,
165 "Created device using UDN '%s', device_entry: %s", device.udn, device_entry
169 device_entry = dev_registry.async_update_device(
171 new_identifiers=identifiers,
175 update_interval =
timedelta(seconds=DEFAULT_SCAN_INTERVAL)
179 device_entry=device_entry,
180 update_interval=update_interval,
184 await coordinator.async_config_entry_first_refresh()
187 entry.runtime_data = coordinator
190 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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)
str get_preferred_location(set[str] locations)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, UpnpConfigEntry entry)
None update_listener(HomeAssistant hass, ConfigEntry entry)