core/homeassistant/components/upnp/config_flow.py

245 lines
8.9 KiB
Python
Raw Normal View History

"""Config flow for UPNP."""
2021-03-18 13:43:52 +00:00
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, cast
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import ssdp
from homeassistant.components.ssdp import SsdpServiceInfo
2022-04-12 21:10:54 +00:00
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
2021-01-29 09:23:34 +00:00
from .const import (
CONFIG_ENTRY_LOCATION,
CONFIG_ENTRY_MAC_ADDRESS,
CONFIG_ENTRY_ORIGINAL_UDN,
CONFIG_ENTRY_ST,
CONFIG_ENTRY_UDN,
DOMAIN,
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER,
2021-08-13 16:13:25 +00:00
ST_IGD_V1,
ST_IGD_V2,
)
from .device import async_get_mac_address_from_host
2021-08-13 16:13:25 +00:00
def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str:
2021-08-13 16:13:25 +00:00
"""Extract user-friendly name from discovery."""
return cast(
str,
discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)
or discovery_info.ssdp_headers.get("_host", ""),
2021-08-13 16:13:25 +00:00
)
def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
"""Test if discovery is complete and usable."""
return bool(
ssdp.ATTR_UPNP_UDN in discovery_info.upnp
and discovery_info.ssdp_st
and discovery_info.ssdp_location
and discovery_info.ssdp_usn
)
async def _async_discover_igd_devices(
hass: HomeAssistant,
) -> list[ssdp.SsdpServiceInfo]:
2021-08-13 16:13:25 +00:00
"""Discovery IGD devices."""
return await ssdp.async_get_discovery_info_by_st(
2021-08-13 16:13:25 +00:00
hass, ST_IGD_V1
) + await ssdp.async_get_discovery_info_by_st(hass, ST_IGD_V2)
2021-01-29 09:23:34 +00:00
async def _async_mac_address_from_discovery(
hass: HomeAssistant, discovery: SsdpServiceInfo
) -> str | None:
"""Get the mac address from a discovery."""
host = discovery.ssdp_headers["_host"]
return await async_get_mac_address_from_host(hass, host)
def _is_igd_device(discovery_info: ssdp.SsdpServiceInfo) -> bool:
"""Test if discovery is a complete IGD device."""
root_device_info = discovery_info.upnp
return root_device_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) in {ST_IGD_V1, ST_IGD_V2}
class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a UPnP/IGD config flow."""
VERSION = 1
# Paths:
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
# - user(None): scan --> user({...}) --> create_entry()
# - import(None) --> create_entry()
2021-01-29 09:23:34 +00:00
def __init__(self) -> None:
"""Initialize the UPnP/IGD config flow."""
self._discoveries: list[SsdpServiceInfo] | None = None
async def async_step_user(
self, user_input: Mapping[str, Any] | None = None
) -> FlowResult:
"""Handle a flow start."""
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER.debug("async_step_user: user_input: %s", user_input)
if user_input is not None:
# Ensure wanted device was discovered.
assert self._discoveries
discovery = next(
iter(
discovery
for discovery in self._discoveries
if discovery.ssdp_usn == user_input["unique_id"]
)
)
await self.async_set_unique_id(discovery.ssdp_usn, raise_on_progress=False)
return await self._async_create_entry_from_discovery(discovery)
# Discover devices.
discoveries = await _async_discover_igd_devices(self.hass)
2021-01-29 09:23:34 +00:00
# Store discoveries which have not been configured.
current_unique_ids = {
entry.unique_id for entry in self._async_current_entries()
}
self._discoveries = [
2021-01-29 09:23:34 +00:00
discovery
for discovery in discoveries
if (
_is_complete_discovery(discovery)
and _is_igd_device(discovery)
and discovery.ssdp_usn not in current_unique_ids
)
]
# Ensure anything to add.
if not self._discoveries:
return self.async_abort(reason="no_devices_found")
data_schema = vol.Schema(
{
2021-01-29 09:23:34 +00:00
vol.Required("unique_id"): vol.In(
{
discovery.ssdp_usn: _friendly_name_from_discovery(discovery)
for discovery in self._discoveries
}
),
}
)
2020-08-27 11:56:20 +00:00
return self.async_show_form(
step_id="user",
data_schema=data_schema,
)
async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult:
"""Handle a discovered UPnP/IGD device.
This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not.
"""
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info)
# Ensure complete discovery.
if not _is_complete_discovery(discovery_info):
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER.debug("Incomplete discovery, ignoring")
return self.async_abort(reason="incomplete_discovery")
# Ensure device is usable. Ideally we would use IgdDevice.is_profile_device,
# but that requires constructing the device completely.
if not _is_igd_device(discovery_info):
LOGGER.debug("Non IGD device, ignoring")
return self.async_abort(reason="non_igd_device")
# Ensure not already configuring/configured.
unique_id = discovery_info.ssdp_usn
2021-01-29 09:23:34 +00:00
await self.async_set_unique_id(unique_id)
mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
self._abort_if_unique_id_configured(
# Store mac address for older entries.
# The location is stored in the config entry such that when the location changes, the entry is reloaded.
updates={
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
},
)
2021-08-13 16:13:25 +00:00
# Handle devices changing their UDN, only allow a single host.
for entry in self._async_current_entries(include_ignore=True):
entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS)
entry_st = entry.data.get(CONFIG_ENTRY_ST)
if entry_mac_address != mac_address:
continue
if discovery_info.ssdp_st != entry_st:
# Check ssdp_st to prevent swapping between IGDv1 and IGDv2.
continue
if entry.source == config_entries.SOURCE_IGNORE:
# Host was already ignored. Don't update ignored entries.
return self.async_abort(reason="discovery_ignored")
LOGGER.debug("Updating entry: %s", entry.entry_id)
self.hass.config_entries.async_update_entry(
entry,
unique_id=unique_id,
data={**entry.data, CONFIG_ENTRY_UDN: discovery_info.ssdp_udn},
)
if entry.state == config_entries.ConfigEntryState.LOADED:
# Only reload when entry has state LOADED; when entry has state SETUP_RETRY,
# another load is started, causing the entry to be loaded twice.
LOGGER.debug("Reloading entry: %s", entry.entry_id)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
return self.async_abort(reason="config_entry_updated")
# Store discovery.
2021-08-13 16:13:25 +00:00
self._discoveries = [discovery_info]
# Ensure user recognizable.
self.context["title_placeholders"] = {
2021-08-13 16:13:25 +00:00
"name": _friendly_name_from_discovery(discovery_info),
}
return await self.async_step_ssdp_confirm()
2021-01-29 09:23:34 +00:00
async def async_step_ssdp_confirm(
self, user_input: Mapping[str, Any] | None = None
) -> FlowResult:
"""Confirm integration via SSDP."""
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input)
if user_input is None:
return self.async_show_form(step_id="ssdp_confirm")
assert self._discoveries
discovery = self._discoveries[0]
return await self._async_create_entry_from_discovery(discovery)
async def _async_create_entry_from_discovery(
2020-08-27 11:56:20 +00:00
self,
discovery: SsdpServiceInfo,
) -> FlowResult:
"""Create an entry from discovery."""
Add upnp binary sensor for connectivity status (#54489) * New binary sensor for connectivity * Add binary_sensor * New binary sensor for connectivity * Add binary_sensor * Handle values returned as None * Small text update for Uptime * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Updates based on review * Update homeassistant/components/upnp/binary_sensor.py Co-authored-by: Joakim Sørensen <hi@ludeeus.dev> * Further updates based on review * Set device_class as a class atribute * Create 1 combined data coordinator and UpnpEntity class * Updates on coordinator * Update comment * Fix in async_step_init for coordinator * Add async_get_status to mocked device and set times polled for each call seperately * Updated to get device through coordinator Check polling for each status call seperately * Use collections.abc instead of Typing for Mapping * Remove adding device to hass.data as coordinator is now saved * Removed setting _coordinator * Added myself as codeowner * Update type in __init__ * Removed attributes from binary sensor * Fix async_unload_entry * Add expected return value to is_on Co-authored-by: Joakim Sørensen <hi@ludeeus.dev>
2021-08-17 18:23:41 +00:00
LOGGER.debug(
"_async_create_entry_from_discovery: discovery: %s",
2020-08-27 11:56:20 +00:00
discovery,
)
2021-08-13 16:13:25 +00:00
title = _friendly_name_from_discovery(discovery)
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
data = {
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_ST: discovery.ssdp_st,
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
}
return self.async_create_entry(title=title, data=data)