Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Onkyo."""
2 
3 import logging
4 from typing import Any
5 
6 import voluptuous as vol
7 
9  SOURCE_RECONFIGURE,
10  ConfigEntry,
11  ConfigFlow,
12  ConfigFlowResult,
13  OptionsFlow,
14 )
15 from homeassistant.const import CONF_HOST, CONF_NAME
16 from homeassistant.core import callback
18  NumberSelector,
19  NumberSelectorConfig,
20  NumberSelectorMode,
21  Selector,
22  SelectSelector,
23  SelectSelectorConfig,
24  SelectSelectorMode,
25  TextSelector,
26 )
27 
28 from .const import (
29  CONF_RECEIVER_MAX_VOLUME,
30  CONF_SOURCES,
31  DOMAIN,
32  OPTION_INPUT_SOURCES,
33  OPTION_MAX_VOLUME,
34  OPTION_MAX_VOLUME_DEFAULT,
35  OPTION_VOLUME_RESOLUTION,
36  OPTION_VOLUME_RESOLUTION_DEFAULT,
37  VOLUME_RESOLUTION_ALLOWED,
38  InputSource,
39 )
40 from .receiver import ReceiverInfo, async_discover, async_interview
41 
42 _LOGGER = logging.getLogger(__name__)
43 
44 CONF_DEVICE = "device"
45 
46 INPUT_SOURCES_ALL_MEANINGS = [
47  input_source.value_meaning for input_source in InputSource
48 ]
49 STEP_MANUAL_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
50 STEP_CONFIGURE_SCHEMA = vol.Schema(
51  {
52  vol.Required(OPTION_VOLUME_RESOLUTION): vol.In(VOLUME_RESOLUTION_ALLOWED),
53  vol.Required(OPTION_INPUT_SOURCES): SelectSelector(
55  options=INPUT_SOURCES_ALL_MEANINGS,
56  multiple=True,
57  mode=SelectSelectorMode.DROPDOWN,
58  )
59  ),
60  }
61 )
62 
63 
64 class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
65  """Onkyo config flow."""
66 
67  _receiver_info: ReceiverInfo
68  _discovered_infos: dict[str, ReceiverInfo]
69 
70  async def async_step_user(
71  self, user_input: dict[str, Any] | None = None
72  ) -> ConfigFlowResult:
73  """Handle a flow initialized by the user."""
74  return self.async_show_menuasync_show_menu(
75  step_id="user", menu_options=["manual", "eiscp_discovery"]
76  )
77 
78  async def async_step_manual(
79  self, user_input: dict[str, Any] | None = None
80  ) -> ConfigFlowResult:
81  """Handle manual device entry."""
82  errors = {}
83 
84  if user_input is not None:
85  host = user_input[CONF_HOST]
86  _LOGGER.debug("Config flow start manual: %s", host)
87  try:
88  info = await async_interview(host)
89  except Exception:
90  _LOGGER.exception("Unexpected exception")
91  errors["base"] = "unknown"
92  else:
93  if info is None:
94  errors["base"] = "cannot_connect"
95  else:
96  self._receiver_info_receiver_info = info
97 
98  await self.async_set_unique_idasync_set_unique_id(
99  info.identifier, raise_on_progress=False
100  )
101  if self.sourcesourcesourcesource == SOURCE_RECONFIGURE:
102  self._abort_if_unique_id_mismatch_abort_if_unique_id_mismatch()
103  else:
104  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
105 
106  return await self.async_step_configure_receiver()
107 
108  suggested_values = user_input
109  if suggested_values is None and self.sourcesourcesourcesource == SOURCE_RECONFIGURE:
110  suggested_values = {
111  CONF_HOST: self._get_reconfigure_entry_get_reconfigure_entry().data[CONF_HOST]
112  }
113 
114  return self.async_show_formasync_show_formasync_show_form(
115  step_id="manual",
116  data_schema=self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
117  STEP_MANUAL_SCHEMA, suggested_values
118  ),
119  errors=errors,
120  )
121 
122  async def async_step_eiscp_discovery(
123  self, user_input: dict[str, Any] | None = None
124  ) -> ConfigFlowResult:
125  """Start eiscp discovery and handle user device selection."""
126  if user_input is not None:
127  self._receiver_info_receiver_info = self._discovered_infos_discovered_infos[user_input[CONF_DEVICE]]
128  await self.async_set_unique_idasync_set_unique_id(
129  self._receiver_info_receiver_info.identifier, raise_on_progress=False
130  )
131  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
132  updates={CONF_HOST: self._receiver_info_receiver_info.host}
133  )
134  return await self.async_step_configure_receiver()
135 
136  _LOGGER.debug("Config flow start eiscp discovery")
137 
138  try:
139  infos = await async_discover()
140  except Exception:
141  _LOGGER.exception("Unexpected exception")
142  return self.async_abortasync_abortasync_abort(reason="unknown")
143 
144  _LOGGER.debug("Discovered devices: %s", infos)
145 
146  self._discovered_infos_discovered_infos = {}
147  discovered_names = {}
148  current_unique_ids = self._async_current_ids_async_current_ids()
149  for info in infos:
150  if info.identifier in current_unique_ids:
151  continue
152  self._discovered_infos_discovered_infos[info.identifier] = info
153  device_name = f"{info.model_name} ({info.host})"
154  discovered_names[info.identifier] = device_name
155 
156  _LOGGER.debug("Discovered new devices: %s", self._discovered_infos_discovered_infos)
157 
158  if not discovered_names:
159  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
160 
161  return self.async_show_formasync_show_formasync_show_form(
162  step_id="eiscp_discovery",
163  data_schema=vol.Schema(
164  {vol.Required(CONF_DEVICE): vol.In(discovered_names)}
165  ),
166  )
167 
168  async def async_step_configure_receiver(
169  self, user_input: dict[str, Any] | None = None
170  ) -> ConfigFlowResult:
171  """Handle the configuration of a single receiver."""
172  errors = {}
173 
174  entry = None
175  entry_options = None
176  if self.sourcesourcesourcesource == SOURCE_RECONFIGURE:
177  entry = self._get_reconfigure_entry_get_reconfigure_entry()
178  entry_options = entry.options
179 
180  if user_input is not None:
181  source_meanings: list[str] = user_input[OPTION_INPUT_SOURCES]
182  if not source_meanings:
183  errors[OPTION_INPUT_SOURCES] = "empty_input_source_list"
184  else:
185  sources_store: dict[str, str] = {}
186  for source_meaning in source_meanings:
187  source = InputSource.from_meaning(source_meaning)
188 
189  source_name = source_meaning
190  if entry_options is not None:
191  source_name = entry_options[OPTION_INPUT_SOURCES].get(
192  source.value, source_name
193  )
194  sources_store[source.value] = source_name
195 
196  volume_resolution = user_input[OPTION_VOLUME_RESOLUTION]
197 
198  if entry_options is None:
199  result = self.async_create_entryasync_create_entryasync_create_entry(
200  title=self._receiver_info_receiver_info.model_name,
201  data={
202  CONF_HOST: self._receiver_info_receiver_info.host,
203  },
204  options={
205  OPTION_VOLUME_RESOLUTION: volume_resolution,
206  OPTION_MAX_VOLUME: OPTION_MAX_VOLUME_DEFAULT,
207  OPTION_INPUT_SOURCES: sources_store,
208  },
209  )
210  else:
211  assert entry is not None
212  result = self.async_update_reload_and_abortasync_update_reload_and_abort(
213  entry,
214  data={
215  CONF_HOST: self._receiver_info_receiver_info.host,
216  },
217  options={
218  OPTION_VOLUME_RESOLUTION: volume_resolution,
219  OPTION_MAX_VOLUME: entry_options[OPTION_MAX_VOLUME],
220  OPTION_INPUT_SOURCES: sources_store,
221  },
222  )
223 
224  _LOGGER.debug("Configured receiver, result: %s", result)
225  return result
226 
227  _LOGGER.debug("Configuring receiver, info: %s", self._receiver_info_receiver_info)
228 
229  suggested_values = user_input
230  if suggested_values is None:
231  if entry_options is None:
232  suggested_values = {
233  OPTION_VOLUME_RESOLUTION: OPTION_VOLUME_RESOLUTION_DEFAULT,
234  OPTION_INPUT_SOURCES: [],
235  }
236  else:
237  suggested_values = {
238  OPTION_VOLUME_RESOLUTION: entry_options[OPTION_VOLUME_RESOLUTION],
239  OPTION_INPUT_SOURCES: [
240  InputSource(input_source).value_meaning
241  for input_source in entry_options[OPTION_INPUT_SOURCES]
242  ],
243  }
244 
245  return self.async_show_formasync_show_formasync_show_form(
246  step_id="configure_receiver",
247  data_schema=self.add_suggested_values_to_schemaadd_suggested_values_to_schema(
248  STEP_CONFIGURE_SCHEMA, suggested_values
249  ),
250  errors=errors,
251  description_placeholders={
252  "name": f"{self._receiver_info.model_name} ({self._receiver_info.host})"
253  },
254  )
255 
256  async def async_step_reconfigure(
257  self, user_input: dict[str, Any] | None = None
258  ) -> ConfigFlowResult:
259  """Handle reconfiguration of the receiver."""
260  return await self.async_step_manual()
261 
262  async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
263  """Import the yaml config."""
264  _LOGGER.debug("Import flow user input: %s", user_input)
265 
266  host: str = user_input[CONF_HOST]
267  name: str | None = user_input.get(CONF_NAME)
268  user_max_volume: int = user_input[OPTION_MAX_VOLUME]
269  user_volume_resolution: int = user_input[CONF_RECEIVER_MAX_VOLUME]
270  user_sources: dict[InputSource, str] = user_input[CONF_SOURCES]
271 
272  info: ReceiverInfo | None = user_input.get("info")
273  if info is None:
274  try:
275  info = await async_interview(host)
276  except Exception:
277  _LOGGER.exception("Import flow interview error for host %s", host)
278  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
279 
280  if info is None:
281  _LOGGER.error("Import flow interview error for host %s", host)
282  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
283 
284  unique_id = info.identifier
285  await self.async_set_unique_idasync_set_unique_id(unique_id)
286  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
287 
288  name = name or info.model_name
289 
290  volume_resolution = VOLUME_RESOLUTION_ALLOWED[-1]
291  for volume_resolution_allowed in VOLUME_RESOLUTION_ALLOWED:
292  if user_volume_resolution <= volume_resolution_allowed:
293  volume_resolution = volume_resolution_allowed
294  break
295 
296  max_volume = min(
297  100, user_max_volume * user_volume_resolution / volume_resolution
298  )
299 
300  sources_store: dict[str, str] = {}
301  for source, source_name in user_sources.items():
302  sources_store[source.value] = source_name
303 
304  return self.async_create_entryasync_create_entryasync_create_entry(
305  title=name,
306  data={
307  CONF_HOST: host,
308  },
309  options={
310  OPTION_VOLUME_RESOLUTION: volume_resolution,
311  OPTION_MAX_VOLUME: max_volume,
312  OPTION_INPUT_SOURCES: sources_store,
313  },
314  )
315 
316  @staticmethod
317  @callback
319  config_entry: ConfigEntry,
320  ) -> OptionsFlow:
321  """Return the options flow."""
322  return OnkyoOptionsFlowHandler(config_entry)
323 
324 
326  """Handle an options flow for Onkyo."""
327 
328  def __init__(self, config_entry: ConfigEntry) -> None:
329  """Initialize options flow."""
330  sources_store: dict[str, str] = config_entry.options[OPTION_INPUT_SOURCES]
331  self._input_sources_input_sources = {InputSource(k): v for k, v in sources_store.items()}
332 
333  async def async_step_init(
334  self, user_input: dict[str, Any] | None = None
335  ) -> ConfigFlowResult:
336  """Manage the options."""
337  if user_input is not None:
338  sources_store: dict[str, str] = {}
339  for source_meaning, source_name in user_input.items():
340  if source_meaning in INPUT_SOURCES_ALL_MEANINGS:
341  source = InputSource.from_meaning(source_meaning)
342  sources_store[source.value] = source_name
343 
344  return self.async_create_entryasync_create_entry(
345  data={
346  OPTION_VOLUME_RESOLUTION: self.config_entryconfig_entryconfig_entry.options[
347  OPTION_VOLUME_RESOLUTION
348  ],
349  OPTION_MAX_VOLUME: user_input[OPTION_MAX_VOLUME],
350  OPTION_INPUT_SOURCES: sources_store,
351  }
352  )
353 
354  schema_dict: dict[Any, Selector] = {}
355 
356  max_volume: float = self.config_entryconfig_entryconfig_entry.options[OPTION_MAX_VOLUME]
357  schema_dict[vol.Required(OPTION_MAX_VOLUME, default=max_volume)] = (
359  NumberSelectorConfig(min=1, max=100, mode=NumberSelectorMode.BOX)
360  )
361  )
362 
363  for source, source_name in self._input_sources_input_sources.items():
364  schema_dict[vol.Required(source.value_meaning, default=source_name)] = (
365  TextSelector()
366  )
367 
368  return self.async_show_formasync_show_form(
369  step_id="init",
370  data_schema=vol.Schema(schema_dict),
371  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:72
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:335
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
set[str|None] _async_current_ids(self, bool include_ignore=True)
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)
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)
None _abort_if_unique_id_mismatch(self, *str reason="unique_id_mismatch", 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)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
None config_entry(self, ConfigEntry value)
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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
ReceiverInfo|None async_interview(str host)
Definition: receiver.py:104
None async_discover(HomeAssistant hass)
Definition: config_flow.py:82
config_entries.ConfigFlowResult async_step_import(self, dict[str, Any]|None _)