Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for DLNA DMS."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from pprint import pformat
7 from typing import TYPE_CHECKING, Any, cast
8 from urllib.parse import urlparse
9 
10 from async_upnp_client.profiles.dlna import DmsDevice
11 import voluptuous as vol
12 
13 from homeassistant.components import ssdp
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_DEVICE_ID, CONF_HOST, CONF_URL
16 from homeassistant.data_entry_flow import AbortFlow
17 
18 from .const import CONF_SOURCE_ID, CONFIG_VERSION, DEFAULT_NAME, DOMAIN
19 from .util import generate_source_id
20 
21 LOGGER = logging.getLogger(__name__)
22 
23 
24 class DlnaDmsFlowHandler(ConfigFlow, domain=DOMAIN):
25  """Handle a DLNA DMS config flow.
26 
27  The Unique Service Name (USN) of the DMS device is used as the unique_id for
28  config entries and for entities. This USN may differ from the root USN if
29  the DMS is an embedded device.
30  """
31 
32  VERSION = CONFIG_VERSION
33 
34  def __init__(self) -> None:
35  """Initialize flow."""
36  self._discoveries_discoveries: dict[str, ssdp.SsdpServiceInfo] = {}
37  self._location_location: str | None = None
38  self._usn_usn: str | None = None
39  self._name_name: str | None = None
40 
41  async def async_step_user(
42  self, user_input: dict[str, Any] | None = None
43  ) -> ConfigFlowResult:
44  """Handle a flow initialized by the user by listing unconfigured devices."""
45  LOGGER.debug("async_step_user: user_input: %s", user_input)
46 
47  if user_input is not None and (host := user_input.get(CONF_HOST)):
48  # User has chosen a device
49  discovery = self._discoveries_discoveries[host]
50  await self._async_parse_discovery_async_parse_discovery(discovery, raise_on_progress=False)
51  return self._create_entry_create_entry()
52 
53  if not (discoveries := await self._async_get_discoveries_async_get_discoveries()):
54  # Nothing found, abort configuration
55  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
56 
57  self._discoveries_discoveries = {
58  cast(str, urlparse(discovery.ssdp_location).hostname): discovery
59  for discovery in discoveries
60  }
61 
62  discovery_choices = {
63  host: f"{discovery.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)} ({host})"
64  for host, discovery in self._discoveries_discoveries.items()
65  }
66  data_schema = vol.Schema({vol.Optional(CONF_HOST): vol.In(discovery_choices)})
67  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=data_schema)
68 
69  async def async_step_ssdp(
70  self, discovery_info: ssdp.SsdpServiceInfo
71  ) -> ConfigFlowResult:
72  """Handle a flow initialized by SSDP discovery."""
73  if LOGGER.isEnabledFor(logging.DEBUG):
74  LOGGER.debug("async_step_ssdp: discovery_info %s", pformat(discovery_info))
75 
76  await self._async_parse_discovery_async_parse_discovery(discovery_info)
77  if TYPE_CHECKING:
78  # _async_parse_discovery unconditionally sets self._name
79  assert self._name_name is not None
80 
81  # Abort if the device doesn't support all services required for a DmsDevice.
82  # Use the discovery_info instead of DmsDevice.is_profile_device to avoid
83  # contacting the device again.
84  discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
85  if not discovery_service_list:
86  return self.async_abortasync_abortasync_abort(reason="not_dms")
87 
88  services = discovery_service_list.get("service")
89  if not services:
90  discovery_service_ids: set[str] = set()
91  elif isinstance(services, list):
92  discovery_service_ids = {service.get("serviceId") for service in services}
93  else:
94  # Only one service defined (etree_to_dict failed to make a list)
95  discovery_service_ids = {services.get("serviceId")}
96 
97  if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids):
98  return self.async_abortasync_abortasync_abort(reason="not_dms")
99 
100  # Abort if another config entry has the same location, in case the
101  # device doesn't have a static and unique UDN (breaking the UPnP spec).
102  self._async_abort_entries_match_async_abort_entries_match({CONF_URL: self._location_location})
103 
104  self.context["title_placeholders"] = {"name": self._name_name}
105 
106  return await self.async_step_confirmasync_step_confirm()
107 
109  self, user_input: dict[str, Any] | None = None
110  ) -> ConfigFlowResult:
111  """Allow the user to confirm adding the device."""
112  if user_input is not None:
113  return self._create_entry_create_entry()
114 
115  self._set_confirm_only_set_confirm_only()
116  return self.async_show_formasync_show_formasync_show_form(step_id="confirm")
117 
118  def _create_entry(self) -> ConfigFlowResult:
119  """Create a config entry, assuming all required information is now known."""
120  LOGGER.debug(
121  "_create_entry: name: %s, location: %s, USN: %s",
122  self._name_name,
123  self._location_location,
124  self._usn_usn,
125  )
126  assert self._name_name
127  assert self._location_location
128  assert self._usn_usn
129 
130  data = {
131  CONF_URL: self._location_location,
132  CONF_DEVICE_ID: self._usn_usn,
133  CONF_SOURCE_ID: generate_source_id(self.hass, self._name_name),
134  }
135  return self.async_create_entryasync_create_entryasync_create_entry(title=self._name_name, data=data)
136 
138  self, discovery_info: ssdp.SsdpServiceInfo, raise_on_progress: bool = True
139  ) -> None:
140  """Get required details from an SSDP discovery.
141 
142  Aborts if a device matching the SSDP USN has already been configured.
143  """
144  LOGGER.debug(
145  "_async_parse_discovery: location: %s, USN: %s",
146  discovery_info.ssdp_location,
147  discovery_info.ssdp_usn,
148  )
149 
150  if not discovery_info.ssdp_location or not discovery_info.ssdp_usn:
151  raise AbortFlow("bad_ssdp")
152 
153  if not self._location_location:
154  self._location_location = discovery_info.ssdp_location
155 
156  self._usn_usn = discovery_info.ssdp_usn
157  await self.async_set_unique_idasync_set_unique_id(self._usn_usn, raise_on_progress=raise_on_progress)
158 
159  # Abort if already configured, but update the last-known location
160  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
161  updates={CONF_URL: self._location_location}, reload_on_update=False
162  )
163 
164  self._name_name = (
165  discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME)
166  or urlparse(self._location_location).hostname
167  or DEFAULT_NAME
168  )
169 
170  async def _async_get_discoveries(self) -> list[ssdp.SsdpServiceInfo]:
171  """Get list of unconfigured DLNA devices discovered by SSDP."""
172  # Get all compatible devices from ssdp's cache
173  discoveries: list[ssdp.SsdpServiceInfo] = []
174  for udn_st in DmsDevice.DEVICE_TYPES:
175  st_discoveries = await ssdp.async_get_discovery_info_by_st(
176  self.hass, udn_st
177  )
178  discoveries.extend(st_discoveries)
179 
180  # Filter out devices already configured
181  current_unique_ids = {
182  entry.unique_id
183  for entry in self._async_current_entries_async_current_entries(include_ignore=False)
184  }
185  return [disc for disc in discoveries if disc.ssdp_udn not in current_unique_ids]
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:43
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:110
None _async_parse_discovery(self, ssdp.SsdpServiceInfo discovery_info, bool raise_on_progress=True)
Definition: config_flow.py:139
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:71
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_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=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)
_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)
str generate_source_id(HomeAssistant hass, str name)
Definition: util.py:11