1 """Config flow for UPNP."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from typing
import Any, cast
7 from urllib.parse
import urlparse
9 import voluptuous
as vol
23 CONFIG_ENTRY_FORCE_POLL,
25 CONFIG_ENTRY_LOCATION,
26 CONFIG_ENTRY_MAC_ADDRESS,
27 CONFIG_ENTRY_ORIGINAL_UDN,
30 DEFAULT_CONFIG_ENTRY_FORCE_POLL,
37 from .device
import async_get_mac_address_from_host, get_preferred_location
41 """Extract user-friendly name from discovery."""
44 discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
45 or discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME)
46 or discovery_info.ssdp_headers.get(
"_host",
""),
51 """Test if discovery is complete and usable."""
53 discovery_info.ssdp_udn
54 and discovery_info.ssdp_st
55 and discovery_info.ssdp_all_locations
56 and discovery_info.ssdp_usn
63 """Discovery IGD devices."""
64 return await ssdp.async_get_discovery_info_by_st(
66 ) + await ssdp.async_get_discovery_info_by_st(hass, ST_IGD_V2)
70 hass: HomeAssistant, discovery: SsdpServiceInfo
72 """Get the mac address from a discovery."""
74 host = urlparse(location).hostname
75 assert host
is not None
80 """Test if discovery is a complete IGD device."""
81 root_device_info = discovery_info.upnp
82 return root_device_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE)
in {ST_IGD_V1, ST_IGD_V2}
86 """Handle a UPnP/IGD config flow."""
97 config_entry: ConfigEntry,
98 ) -> UpnpOptionsFlowHandler:
99 """Get the options flow for this handler."""
104 """Get current discoveries."""
105 domain_data: dict = self.hass.data.setdefault(DOMAIN, {})
106 return domain_data.setdefault(DOMAIN_DISCOVERIES, {})
109 """Add a discovery."""
110 self.
_discoveries_discoveries[discovery.ssdp_usn] = discovery
113 """Remove a discovery by its USN/unique_id."""
117 self, user_input: Mapping[str, Any] |
None =
None
118 ) -> ConfigFlowResult:
119 """Handle a flow start."""
120 LOGGER.debug(
"async_step_user: user_input: %s", user_input)
122 if user_input
is not None:
129 if discovery.ssdp_usn == user_input[
"unique_id"]
132 await self.
async_set_unique_idasync_set_unique_id(discovery.ssdp_usn, raise_on_progress=
False)
139 current_unique_ids = {
142 for discovery
in discoveries:
146 and discovery.ssdp_usn
not in current_unique_ids
154 data_schema = vol.Schema(
156 vol.Required(
"unique_id"): vol.In(
166 data_schema=data_schema,
170 self, discovery_info: ssdp.SsdpServiceInfo
171 ) -> ConfigFlowResult:
172 """Handle a discovered UPnP/IGD device.
174 This flow is triggered by the SSDP component. It will check if the
175 host is already configured and delegate to the import step if not.
177 LOGGER.debug(
"async_step_ssdp: discovery_info: %s", discovery_info)
181 LOGGER.debug(
"Incomplete discovery, ignoring")
187 LOGGER.debug(
"Non IGD device, ignoring")
191 unique_id = discovery_info.ssdp_usn
194 host = discovery_info.ssdp_headers[
"_host"]
200 CONFIG_ENTRY_MAC_ADDRESS: mac_address,
202 discovery_info.ssdp_all_locations
204 CONFIG_ENTRY_HOST: host,
205 CONFIG_ENTRY_ST: discovery_info.ssdp_st,
211 entry_mac_address = entry.data.get(CONFIG_ENTRY_MAC_ADDRESS)
212 entry_host = entry.data.get(CONFIG_ENTRY_HOST)
213 if entry_mac_address != mac_address
and entry_host != host:
216 entry_st = entry.data.get(CONFIG_ENTRY_ST)
217 if discovery_info.ssdp_st != entry_st:
221 if entry.source == SOURCE_IGNORE:
225 LOGGER.debug(
"Updating entry: %s", entry.entry_id)
229 data={**entry.data, CONFIG_ENTRY_UDN: discovery_info.ssdp_udn},
230 reason=
"config_entry_updated",
237 self.context[
"title_placeholders"] = {
244 self, user_input: Mapping[str, Any] |
None =
None
245 ) -> ConfigFlowResult:
246 """Confirm integration via SSDP."""
247 LOGGER.debug(
"async_step_ssdp_confirm: user_input: %s", user_input)
248 if user_input
is None:
256 """Ignore this config flow."""
257 usn = user_input[
"unique_id"]
261 CONFIG_ENTRY_UDN: discovery.ssdp_udn,
262 CONFIG_ENTRY_ST: discovery.ssdp_st,
263 CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn,
264 CONFIG_ENTRY_MAC_ADDRESS: mac_address,
265 CONFIG_ENTRY_HOST: discovery.ssdp_headers[
"_host"],
269 CONFIG_ENTRY_FORCE_POLL:
False,
272 await self.
async_set_unique_idasync_set_unique_id(user_input[
"unique_id"], raise_on_progress=
False)
274 title=user_input[
"title"], data=data, options=options
279 discovery: SsdpServiceInfo,
280 ) -> ConfigFlowResult:
281 """Create an entry from discovery."""
283 "_async_create_entry_from_discovery: discovery: %s",
290 CONFIG_ENTRY_UDN: discovery.ssdp_udn,
291 CONFIG_ENTRY_ST: discovery.ssdp_st,
292 CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn,
294 CONFIG_ENTRY_MAC_ADDRESS: mac_address,
295 CONFIG_ENTRY_HOST: discovery.ssdp_headers[
"_host"],
298 CONFIG_ENTRY_FORCE_POLL:
False,
304 """Handle an options flow."""
307 self, user_input: dict[str, Any] |
None =
None
308 ) -> ConfigFlowResult:
309 """Handle options flow."""
310 if user_input
is not None:
313 data_schema = vol.Schema(
316 CONFIG_ENTRY_FORCE_POLL,
318 CONFIG_ENTRY_FORCE_POLL, DEFAULT_CONFIG_ENTRY_FORCE_POLL
323 return self.
async_show_formasync_show_form(step_id=
"init", data_schema=data_schema)
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
ConfigFlowResult async_step_user(self, Mapping[str, Any]|None user_input=None)
None _add_discovery(self, SsdpServiceInfo discovery)
dict[str, SsdpServiceInfo] _discoveries(self)
ConfigFlowResult async_step_ssdp_confirm(self, Mapping[str, Any]|None user_input=None)
ConfigFlowResult async_step_ignore(self, dict[str, Any] user_input)
UpnpOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
SsdpServiceInfo _remove_discovery(self, str usn)
ConfigFlowResult _async_create_entry_from_discovery(self, SsdpServiceInfo discovery)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
list[ssdp.SsdpServiceInfo] _async_discovered_igd_devices(HomeAssistant hass)
str _friendly_name_from_discovery(ssdp.SsdpServiceInfo discovery_info)
str|None _async_mac_address_from_discovery(HomeAssistant hass, SsdpServiceInfo discovery)
bool _is_complete_discovery(ssdp.SsdpServiceInfo discovery_info)
bool _is_igd_device(ssdp.SsdpServiceInfo discovery_info)
str get_preferred_location(set[str] locations)
str|None async_get_mac_address_from_host(HomeAssistant hass, str host)