2022-10-26 19:34:44 +00:00
|
|
|
"""UPnP/IGD integration."""
|
2021-08-13 16:13:25 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-08-13 10:11:58 +00:00
|
|
|
import asyncio
|
2021-08-17 18:23:41 +00:00
|
|
|
from datetime import timedelta
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2022-10-26 19:34:44 +00:00
|
|
|
from async_upnp_client.exceptions import UpnpConnectionError
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2021-06-08 20:32:06 +00:00
|
|
|
from homeassistant.components import ssdp
|
2018-08-29 19:19:04 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
2021-12-04 13:10:01 +00:00
|
|
|
from homeassistant.const import Platform
|
2021-09-11 23:38:16 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2020-04-10 22:24:03 +00:00
|
|
|
from homeassistant.exceptions import ConfigEntryNotReady
|
2022-10-26 19:34:44 +00:00
|
|
|
from homeassistant.helpers import config_validation, device_registry
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-09-01 19:20:15 +00:00
|
|
|
from .const import (
|
2022-09-06 15:33:16 +00:00
|
|
|
CONFIG_ENTRY_HOST,
|
2022-04-20 21:01:43 +00:00
|
|
|
CONFIG_ENTRY_MAC_ADDRESS,
|
|
|
|
CONFIG_ENTRY_ORIGINAL_UDN,
|
2020-05-03 01:03:54 +00:00
|
|
|
CONFIG_ENTRY_ST,
|
|
|
|
CONFIG_ENTRY_UDN,
|
2021-08-17 18:23:41 +00:00
|
|
|
DEFAULT_SCAN_INTERVAL,
|
2019-11-25 23:37:03 +00:00
|
|
|
DOMAIN,
|
2022-09-06 15:33:16 +00:00
|
|
|
IDENTIFIER_HOST,
|
|
|
|
IDENTIFIER_SERIAL_NUMBER,
|
2021-08-17 18:23:41 +00:00
|
|
|
LOGGER,
|
2018-09-01 19:20:15 +00:00
|
|
|
)
|
2022-10-26 19:34:44 +00:00
|
|
|
from .coordinator import UpnpDataUpdateCoordinator
|
|
|
|
from .device import async_create_device
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
NOTIFICATION_ID = "upnp_notification"
|
|
|
|
NOTIFICATION_TITLE = "UPnP/IGD Setup"
|
|
|
|
|
2021-12-04 13:10:01 +00:00
|
|
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
2021-04-27 20:19:57 +00:00
|
|
|
|
2022-10-26 19:34:44 +00:00
|
|
|
CONFIG_SCHEMA = config_validation.removed(DOMAIN, raise_if_present=False)
|
2018-08-17 19:28:29 +00:00
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2021-05-27 13:56:20 +00:00
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
2018-12-21 17:25:23 +00:00
|
|
|
"""Set up UPnP/IGD device from a config entry."""
|
2022-04-20 21:01:43 +00:00
|
|
|
LOGGER.debug("Setting up config entry: %s", entry.entry_id)
|
2018-12-21 17:25:23 +00:00
|
|
|
|
2022-05-30 15:07:18 +00:00
|
|
|
hass.data.setdefault(DOMAIN, {})
|
|
|
|
|
2021-05-27 13:56:20 +00:00
|
|
|
udn = entry.data[CONFIG_ENTRY_UDN]
|
|
|
|
st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name
|
2021-08-13 16:13:25 +00:00
|
|
|
usn = f"{udn}::{st}"
|
|
|
|
|
|
|
|
# Register device discovered-callback.
|
|
|
|
device_discovered_event = asyncio.Event()
|
2021-12-03 07:32:42 +00:00
|
|
|
discovery_info: ssdp.SsdpServiceInfo | None = None
|
2021-08-13 16:13:25 +00:00
|
|
|
|
2021-12-03 07:32:42 +00:00
|
|
|
async def device_discovered(
|
|
|
|
headers: ssdp.SsdpServiceInfo, change: ssdp.SsdpChange
|
|
|
|
) -> None:
|
|
|
|
if change == ssdp.SsdpChange.BYEBYE:
|
2021-09-11 23:38:16 +00:00
|
|
|
return
|
|
|
|
|
2021-08-13 16:13:25 +00:00
|
|
|
nonlocal discovery_info
|
2021-12-03 07:32:42 +00:00
|
|
|
LOGGER.debug("Device discovered: %s, at: %s", usn, headers.ssdp_location)
|
2021-09-11 23:38:16 +00:00
|
|
|
discovery_info = headers
|
2021-08-13 16:13:25 +00:00
|
|
|
device_discovered_event.set()
|
|
|
|
|
2021-09-11 23:38:16 +00:00
|
|
|
cancel_discovered_callback = await ssdp.async_register_callback(
|
2021-08-13 16:13:25 +00:00
|
|
|
hass,
|
|
|
|
device_discovered,
|
|
|
|
{
|
|
|
|
"usn": usn,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2020-08-13 10:11:58 +00:00
|
|
|
try:
|
2021-08-13 16:13:25 +00:00
|
|
|
await asyncio.wait_for(device_discovered_event.wait(), timeout=10)
|
2020-08-28 11:50:32 +00:00
|
|
|
except asyncio.TimeoutError as err:
|
2022-05-24 19:37:37 +00:00
|
|
|
raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err
|
2021-08-13 16:13:25 +00:00
|
|
|
finally:
|
|
|
|
cancel_discovered_callback()
|
2020-08-13 10:11:58 +00:00
|
|
|
|
2021-08-13 16:13:25 +00:00
|
|
|
# Create device.
|
2022-03-08 06:51:23 +00:00
|
|
|
assert discovery_info is not None
|
|
|
|
assert discovery_info.ssdp_location is not None
|
2021-12-03 07:32:42 +00:00
|
|
|
location = discovery_info.ssdp_location
|
2021-10-06 04:31:11 +00:00
|
|
|
try:
|
2022-05-24 19:37:37 +00:00
|
|
|
device = await async_create_device(hass, location)
|
2021-10-06 04:31:11 +00:00
|
|
|
except UpnpConnectionError as err:
|
2022-05-24 19:37:37 +00:00
|
|
|
raise ConfigEntryNotReady(
|
|
|
|
f"Error connecting to device at location: {location}, err: {err}"
|
|
|
|
) from err
|
2022-04-20 21:01:43 +00:00
|
|
|
|
|
|
|
# Track the original UDN such that existing sensors do not change their unique_id.
|
|
|
|
if CONFIG_ENTRY_ORIGINAL_UDN not in entry.data:
|
2020-05-03 01:03:54 +00:00
|
|
|
hass.config_entries.async_update_entry(
|
2021-05-27 13:56:20 +00:00
|
|
|
entry=entry,
|
2022-04-20 21:01:43 +00:00
|
|
|
data={
|
|
|
|
**entry.data,
|
|
|
|
CONFIG_ENTRY_ORIGINAL_UDN: device.udn,
|
|
|
|
},
|
2020-05-03 01:03:54 +00:00
|
|
|
)
|
2022-04-20 21:01:43 +00:00
|
|
|
device.original_udn = entry.data[CONFIG_ENTRY_ORIGINAL_UDN]
|
2018-12-21 17:25:23 +00:00
|
|
|
|
2022-04-20 21:01:43 +00:00
|
|
|
# Store mac address for changed UDN matching.
|
2022-09-06 15:33:16 +00:00
|
|
|
device_mac_address = await device.async_get_mac_address()
|
|
|
|
if device_mac_address and not entry.data.get(CONFIG_ENTRY_MAC_ADDRESS):
|
2021-02-21 02:26:17 +00:00
|
|
|
hass.config_entries.async_update_entry(
|
2021-05-27 13:56:20 +00:00
|
|
|
entry=entry,
|
2022-04-20 21:01:43 +00:00
|
|
|
data={
|
|
|
|
**entry.data,
|
2022-09-06 15:33:16 +00:00
|
|
|
CONFIG_ENTRY_MAC_ADDRESS: device_mac_address,
|
|
|
|
CONFIG_ENTRY_HOST: device.host,
|
2022-04-20 21:01:43 +00:00
|
|
|
},
|
2021-02-21 02:26:17 +00:00
|
|
|
)
|
|
|
|
|
2022-09-06 15:33:16 +00:00
|
|
|
identifiers = {(DOMAIN, device.usn)}
|
|
|
|
if device.host:
|
|
|
|
identifiers.add((IDENTIFIER_HOST, device.host))
|
|
|
|
if device.serial_number:
|
|
|
|
identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number))
|
|
|
|
|
2022-10-26 19:34:44 +00:00
|
|
|
connections = {(device_registry.CONNECTION_UPNP, device.udn)}
|
2022-09-06 15:33:16 +00:00
|
|
|
if device_mac_address:
|
2022-10-26 19:34:44 +00:00
|
|
|
connections.add((device_registry.CONNECTION_NETWORK_MAC, device_mac_address))
|
2022-04-20 21:01:43 +00:00
|
|
|
|
2022-10-26 19:34:44 +00:00
|
|
|
dev_registry = device_registry.async_get(hass)
|
|
|
|
device_entry = dev_registry.async_get_device(
|
2022-09-06 15:33:16 +00:00
|
|
|
identifiers=identifiers, connections=connections
|
2018-12-21 17:25:23 +00:00
|
|
|
)
|
2022-04-20 21:01:43 +00:00
|
|
|
if device_entry:
|
|
|
|
LOGGER.debug(
|
|
|
|
"Found device using connections: %s, device_entry: %s",
|
|
|
|
connections,
|
|
|
|
device_entry,
|
|
|
|
)
|
|
|
|
if not device_entry:
|
|
|
|
# No device found, create new device entry.
|
2022-10-26 19:34:44 +00:00
|
|
|
device_entry = dev_registry.async_get_or_create(
|
2022-04-20 21:01:43 +00:00
|
|
|
config_entry_id=entry.entry_id,
|
|
|
|
connections=connections,
|
2022-09-06 15:33:16 +00:00
|
|
|
identifiers=identifiers,
|
2022-04-20 21:01:43 +00:00
|
|
|
name=device.name,
|
|
|
|
manufacturer=device.manufacturer,
|
|
|
|
model=device.model_name,
|
|
|
|
)
|
|
|
|
LOGGER.debug(
|
|
|
|
"Created device using UDN '%s', device_entry: %s", device.udn, device_entry
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
# Update identifier.
|
2022-10-26 19:34:44 +00:00
|
|
|
device_entry = dev_registry.async_update_device(
|
2022-04-20 21:01:43 +00:00
|
|
|
device_entry.id,
|
2022-09-06 15:33:16 +00:00
|
|
|
new_identifiers=identifiers,
|
2022-04-20 21:01:43 +00:00
|
|
|
)
|
2018-12-21 17:25:23 +00:00
|
|
|
|
2022-04-20 21:01:43 +00:00
|
|
|
assert device_entry
|
2022-04-12 21:10:54 +00:00
|
|
|
update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
2021-08-17 18:23:41 +00:00
|
|
|
coordinator = UpnpDataUpdateCoordinator(
|
|
|
|
hass,
|
|
|
|
device=device,
|
2022-04-20 21:01:43 +00:00
|
|
|
device_entry=device_entry,
|
2021-08-17 18:23:41 +00:00
|
|
|
update_interval=update_interval,
|
|
|
|
)
|
|
|
|
|
2022-04-20 21:01:43 +00:00
|
|
|
# Try an initial refresh.
|
|
|
|
await coordinator.async_config_entry_first_refresh()
|
|
|
|
|
2021-08-17 18:23:41 +00:00
|
|
|
# Save coordinator.
|
|
|
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
|
|
|
|
2022-02-27 18:29:29 +00:00
|
|
|
# Setup platforms, creating sensors/binary_sensors.
|
2022-07-09 15:27:42 +00:00
|
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
2018-08-17 19:28:29 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
2018-08-29 19:19:04 +00:00
|
|
|
|
2022-04-20 21:01:43 +00:00
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
2018-12-21 17:25:23 +00:00
|
|
|
"""Unload a UPnP/IGD device from a config entry."""
|
2022-04-20 21:01:43 +00:00
|
|
|
LOGGER.debug("Unloading config entry: %s", entry.entry_id)
|
2021-04-14 21:39:44 +00:00
|
|
|
|
2022-02-27 18:29:29 +00:00
|
|
|
# Unload platforms.
|
2022-04-20 21:01:43 +00:00
|
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
if unload_ok:
|
|
|
|
del hass.data[DOMAIN][entry.entry_id]
|
|
|
|
|
|
|
|
return unload_ok
|