Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Frontier Silicon Media Player integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 from urllib.parse import urlparse
9 
10 from afsapi import (
11  AFSAPI,
12  ConnectionError as FSConnectionError,
13  InvalidPinException,
14  NotImplementedException,
15 )
16 import voluptuous as vol
17 
18 from homeassistant.components import ssdp
19 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
20 from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT
21 
22 from .const import (
23  CONF_WEBFSAPI_URL,
24  DEFAULT_PIN,
25  DEFAULT_PORT,
26  DOMAIN,
27  SSDP_ATTR_SPEAKER_NAME,
28 )
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 STEP_USER_DATA_SCHEMA = vol.Schema(
33  {
34  vol.Required(CONF_HOST): str,
35  vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
36  }
37 )
38 
39 STEP_DEVICE_CONFIG_DATA_SCHEMA = vol.Schema(
40  {
41  vol.Required(
42  CONF_PIN,
43  default=DEFAULT_PIN,
44  ): str,
45  }
46 )
47 
48 
49 def hostname_from_url(url: str) -> str:
50  """Return the hostname from a url."""
51  return str(urlparse(url).hostname)
52 
53 
54 class FrontierSiliconConfigFlow(ConfigFlow, domain=DOMAIN):
55  """Handle a config flow for Frontier Silicon Media Player."""
56 
57  VERSION = 1
58 
59  _name: str
60  _webfsapi_url: str
61 
62  async def async_step_user(
63  self, user_input: dict[str, Any] | None = None
64  ) -> ConfigFlowResult:
65  """Handle the initial step of manual configuration."""
66  errors = {}
67 
68  if user_input:
69  device_url = (
70  f"http://{user_input[CONF_HOST]}:{user_input[CONF_PORT]}/device"
71  )
72  try:
73  self._webfsapi_url_webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url)
74  except FSConnectionError:
75  errors["base"] = "cannot_connect"
76  except Exception:
77  _LOGGER.exception("Unexpected exception")
78  errors["base"] = "unknown"
79  else:
80  return await self._async_step_device_config_if_needed_async_step_device_config_if_needed()
81 
82  data_schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
83  STEP_USER_DATA_SCHEMA, user_input
84  )
85  return self.async_show_formasync_show_formasync_show_form(
86  step_id="user", data_schema=data_schema, errors=errors
87  )
88 
89  async def async_step_ssdp(
90  self, discovery_info: ssdp.SsdpServiceInfo
91  ) -> ConfigFlowResult:
92  """Process entity discovered via SSDP."""
93 
94  device_url = discovery_info.ssdp_location
95  if device_url is None:
96  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
97 
98  device_hostname = hostname_from_url(device_url)
99  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
100  if device_hostname == hostname_from_url(entry.data[CONF_WEBFSAPI_URL]):
101  return self.async_abortasync_abortasync_abort(reason="already_configured")
102 
103  if speaker_name := discovery_info.ssdp_headers.get(SSDP_ATTR_SPEAKER_NAME):
104  # If we have a name, use it as flow title
105  self.context["title_placeholders"] = {"name": speaker_name}
106 
107  try:
108  self._webfsapi_url_webfsapi_url = await AFSAPI.get_webfsapi_endpoint(device_url)
109  except FSConnectionError:
110  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
111  except Exception as exception: # noqa: BLE001
112  _LOGGER.debug(exception)
113  return self.async_abortasync_abortasync_abort(reason="unknown")
114 
115  # try to login with default pin
116  afsapi = AFSAPI(self._webfsapi_url_webfsapi_url, DEFAULT_PIN)
117  try:
118  await afsapi.get_friendly_name()
119  except InvalidPinException:
120  return self.async_abortasync_abortasync_abort(reason="invalid_auth")
121 
122  try:
123  unique_id = await afsapi.get_radio_id()
124  except NotImplementedException:
125  unique_id = None
126 
127  await self.async_set_unique_idasync_set_unique_id(unique_id)
128  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
129  updates={CONF_WEBFSAPI_URL: self._webfsapi_url_webfsapi_url}, reload_on_update=True
130  )
131 
132  self._name_name = await afsapi.get_friendly_name()
133 
134  return await self.async_step_confirmasync_step_confirm()
135 
136  async def _async_step_device_config_if_needed(self) -> ConfigFlowResult:
137  """Most users will not have changed the default PIN on their radio.
138 
139  We try to use this default PIN, and only if this fails ask for it via `async_step_device_config`
140  """
141 
142  try:
143  # try to login with default pin
144  afsapi = AFSAPI(self._webfsapi_url_webfsapi_url, DEFAULT_PIN)
145 
146  self._name_name = await afsapi.get_friendly_name()
147  except InvalidPinException:
148  # Ask for a PIN
149  return await self.async_step_device_configasync_step_device_config()
150 
151  self.context["title_placeholders"] = {"name": self._name_name}
152 
153  try:
154  unique_id = await afsapi.get_radio_id()
155  except NotImplementedException:
156  unique_id = None
157  await self.async_set_unique_idasync_set_unique_id(unique_id)
158  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
159 
160  return await self._async_create_entry_async_create_entry()
161 
163  self, user_input: dict[str, Any] | None = None
164  ) -> ConfigFlowResult:
165  """Allow the user to confirm adding the device. Used when the default PIN could successfully be used."""
166 
167  if user_input is not None:
168  return await self._async_create_entry_async_create_entry()
169 
170  self._set_confirm_only_set_confirm_only()
171  return self.async_show_formasync_show_formasync_show_form(
172  step_id="confirm", description_placeholders={"name": self._name_name}
173  )
174 
175  async def async_step_reauth(
176  self, entry_data: Mapping[str, Any]
177  ) -> ConfigFlowResult:
178  """Perform reauth upon an API authentication error."""
179  self._webfsapi_url_webfsapi_url = entry_data[CONF_WEBFSAPI_URL]
180  return await self.async_step_device_configasync_step_device_config()
181 
183  self, user_input: dict[str, Any] | None = None
184  ) -> ConfigFlowResult:
185  """Handle device configuration step.
186 
187  We ask for the PIN in this step.
188  """
189 
190  if user_input is None:
191  return self.async_show_formasync_show_formasync_show_form(
192  step_id="device_config", data_schema=STEP_DEVICE_CONFIG_DATA_SCHEMA
193  )
194 
195  errors = {}
196 
197  try:
198  afsapi = AFSAPI(self._webfsapi_url_webfsapi_url, user_input[CONF_PIN])
199 
200  self._name_name = await afsapi.get_friendly_name()
201 
202  except FSConnectionError:
203  errors["base"] = "cannot_connect"
204  except InvalidPinException:
205  errors["base"] = "invalid_auth"
206  except Exception:
207  _LOGGER.exception("Unexpected exception")
208  errors["base"] = "unknown"
209  else:
210  if self.sourcesourcesourcesource == SOURCE_REAUTH:
211  return self.async_update_reload_and_abortasync_update_reload_and_abort(
212  self._get_reauth_entry_get_reauth_entry(),
213  data_updates={CONF_PIN: user_input[CONF_PIN]},
214  )
215 
216  try:
217  unique_id = await afsapi.get_radio_id()
218  except NotImplementedException:
219  unique_id = None
220  await self.async_set_unique_idasync_set_unique_id(unique_id, raise_on_progress=False)
221  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
222  return await self._async_create_entry_async_create_entry(user_input[CONF_PIN])
223 
224  data_schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
225  STEP_DEVICE_CONFIG_DATA_SCHEMA, user_input
226  )
227  return self.async_show_formasync_show_formasync_show_form(
228  step_id="device_config",
229  data_schema=data_schema,
230  errors=errors,
231  )
232 
233  async def _async_create_entry(self, pin: str | None = None):
234  """Create the entry."""
235 
236  return self.async_create_entryasync_create_entryasync_create_entry(
237  title=self._name_name,
238  data={CONF_WEBFSAPI_URL: self._webfsapi_url_webfsapi_url, CONF_PIN: pin or DEFAULT_PIN},
239  )
ConfigFlowResult async_step_device_config(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:184
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:177
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:164
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:91
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:64
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)
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
_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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)