Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure Denon AVR receivers using their HTTP interface."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 from urllib.parse import urlparse
8 
9 import denonavr
10 from denonavr.exceptions import AvrNetworkError, AvrTimoutError
11 import voluptuous as vol
12 
13 from homeassistant.components import ssdp
14 from homeassistant.config_entries import (
15  ConfigEntry,
16  ConfigFlow,
17  ConfigFlowResult,
18  OptionsFlow,
19 )
20 from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TYPE
21 from homeassistant.core import callback
22 from homeassistant.helpers.httpx_client import get_async_client
23 
24 from .receiver import ConnectDenonAVR
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 DOMAIN = "denonavr"
29 
30 SUPPORTED_MANUFACTURERS = ["Denon", "DENON", "DENON PROFESSIONAL", "Marantz"]
31 IGNORED_MODELS = ["HEOS 1", "HEOS 3", "HEOS 5", "HEOS 7"]
32 
33 CONF_SHOW_ALL_SOURCES = "show_all_sources"
34 CONF_ZONE2 = "zone2"
35 CONF_ZONE3 = "zone3"
36 CONF_MANUFACTURER = "manufacturer"
37 CONF_SERIAL_NUMBER = "serial_number"
38 CONF_UPDATE_AUDYSSEY = "update_audyssey"
39 CONF_USE_TELNET = "use_telnet"
40 
41 DEFAULT_SHOW_SOURCES = False
42 DEFAULT_TIMEOUT = 5
43 DEFAULT_ZONE2 = False
44 DEFAULT_ZONE3 = False
45 DEFAULT_UPDATE_AUDYSSEY = False
46 DEFAULT_USE_TELNET = False
47 DEFAULT_USE_TELNET_NEW_INSTALL = True
48 
49 CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
50 
51 
53  """Options for the component."""
54 
55  async def async_step_init(
56  self, user_input: dict[str, Any] | None = None
57  ) -> ConfigFlowResult:
58  """Manage the options."""
59  if user_input is not None:
60  return self.async_create_entryasync_create_entry(title="", data=user_input)
61 
62  settings_schema = vol.Schema(
63  {
64  vol.Optional(
65  CONF_SHOW_ALL_SOURCES,
66  default=self.config_entryconfig_entryconfig_entry.options.get(
67  CONF_SHOW_ALL_SOURCES, DEFAULT_SHOW_SOURCES
68  ),
69  ): bool,
70  vol.Optional(
71  CONF_ZONE2,
72  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_ZONE2, DEFAULT_ZONE2),
73  ): bool,
74  vol.Optional(
75  CONF_ZONE3,
76  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_ZONE3, DEFAULT_ZONE3),
77  ): bool,
78  vol.Optional(
79  CONF_UPDATE_AUDYSSEY,
80  default=self.config_entryconfig_entryconfig_entry.options.get(
81  CONF_UPDATE_AUDYSSEY, DEFAULT_UPDATE_AUDYSSEY
82  ),
83  ): bool,
84  vol.Optional(
85  CONF_USE_TELNET,
86  default=self.config_entryconfig_entryconfig_entry.options.get(
87  CONF_USE_TELNET, DEFAULT_USE_TELNET
88  ),
89  ): bool,
90  }
91  )
92 
93  return self.async_show_formasync_show_form(step_id="init", data_schema=settings_schema)
94 
95 
96 class DenonAvrFlowHandler(ConfigFlow, domain=DOMAIN):
97  """Handle a Denon AVR config flow."""
98 
99  VERSION = 1
100 
101  def __init__(self) -> None:
102  """Initialize the Denon AVR flow."""
103  self.hosthost: str | None = None
104  self.serial_numberserial_number: str | None = None
105  self.model_namemodel_name: str | None = None
106  self.timeouttimeout = DEFAULT_TIMEOUT
107  self.show_all_sourcesshow_all_sources = DEFAULT_SHOW_SOURCES
108  self.zone2zone2 = DEFAULT_ZONE2
109  self.zone3zone3 = DEFAULT_ZONE3
110  self.d_receiversd_receivers: list[dict[str, Any]] = []
111 
112  @staticmethod
113  @callback
115  config_entry: ConfigEntry,
116  ) -> OptionsFlowHandler:
117  """Get the options flow."""
118  return OptionsFlowHandler()
119 
120  async def async_step_user(
121  self, user_input: dict[str, Any] | None = None
122  ) -> ConfigFlowResult:
123  """Handle a flow initialized by the user."""
124  errors = {}
125  if user_input is not None:
126  # check if IP address is set manually
127  if host := user_input.get(CONF_HOST):
128  self.hosthost = host
129  return await self.async_step_connectasync_step_connect()
130 
131  # discovery using denonavr library
132  self.d_receiversd_receivers = await denonavr.async_discover()
133  # More than one receiver could be discovered by that method
134  if len(self.d_receiversd_receivers) == 1:
135  self.hosthost = self.d_receiversd_receivers[0]["host"]
136  return await self.async_step_connectasync_step_connect()
137  if len(self.d_receiversd_receivers) > 1:
138  # show selection form
139  return await self.async_step_selectasync_step_select()
140 
141  errors["base"] = "discovery_error"
142 
143  return self.async_show_formasync_show_formasync_show_form(
144  step_id="user", data_schema=CONFIG_SCHEMA, errors=errors
145  )
146 
147  async def async_step_select(
148  self, user_input: dict[str, Any] | None = None
149  ) -> ConfigFlowResult:
150  """Handle multiple receivers found."""
151  errors: dict[str, str] = {}
152  if user_input is not None:
153  self.hosthost = user_input["select_host"]
154  return await self.async_step_connectasync_step_connect()
155 
156  select_scheme = vol.Schema(
157  {
158  vol.Required("select_host"): vol.In(
159  [d_receiver["host"] for d_receiver in self.d_receiversd_receivers]
160  )
161  }
162  )
163 
164  return self.async_show_formasync_show_formasync_show_form(
165  step_id="select", data_schema=select_scheme, errors=errors
166  )
167 
169  self, user_input: dict[str, Any] | None = None
170  ) -> ConfigFlowResult:
171  """Allow the user to confirm adding the device."""
172  if user_input is not None:
173  return await self.async_step_connectasync_step_connect()
174 
175  self._set_confirm_only_set_confirm_only()
176  return self.async_show_formasync_show_formasync_show_form(step_id="confirm")
177 
179  self, user_input: dict[str, Any] | None = None
180  ) -> ConfigFlowResult:
181  """Connect to the receiver."""
182  assert self.hosthost
183  connect_denonavr = ConnectDenonAVR(
184  self.hosthost,
185  self.timeouttimeout,
186  self.show_all_sourcesshow_all_sources,
187  self.zone2zone2,
188  self.zone3zone3,
189  use_telnet=False,
190  update_audyssey=False,
191  async_client_getter=lambda: get_async_client(self.hass),
192  )
193 
194  try:
195  success = await connect_denonavr.async_connect_receiver()
196  except (AvrNetworkError, AvrTimoutError):
197  success = False
198  if not success:
199  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
200  receiver = connect_denonavr.receiver
201  assert receiver
202 
203  if not self.serial_numberserial_number:
204  self.serial_numberserial_number = receiver.serial_number
205  if not self.model_namemodel_name:
206  self.model_namemodel_name = (receiver.model_name).replace("*", "")
207 
208  if self.serial_numberserial_number is not None:
209  unique_id = self.construct_unique_idconstruct_unique_id(self.model_namemodel_name, self.serial_numberserial_number)
210  await self.async_set_unique_idasync_set_unique_id(unique_id)
211  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
212  else:
213  _LOGGER.error(
214  (
215  "Could not get serial number of host %s, "
216  "unique_id's will not be available"
217  ),
218  self.hosthost,
219  )
220  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: self.hosthost})
221 
222  return self.async_create_entryasync_create_entryasync_create_entry(
223  title=receiver.name,
224  data={
225  CONF_HOST: self.hosthost,
226  CONF_TYPE: receiver.receiver_type,
227  CONF_MODEL: self.model_namemodel_name,
228  CONF_MANUFACTURER: receiver.manufacturer,
229  CONF_SERIAL_NUMBER: self.serial_numberserial_number,
230  },
231  options={CONF_USE_TELNET: DEFAULT_USE_TELNET_NEW_INSTALL},
232  )
233 
234  async def async_step_ssdp(
235  self, discovery_info: ssdp.SsdpServiceInfo
236  ) -> ConfigFlowResult:
237  """Handle a discovered Denon AVR.
238 
239  This flow is triggered by the SSDP component. It will check if the
240  host is already configured and delegate to the import step if not.
241  """
242  # Filter out non-Denon AVRs#1
243  if (
244  discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER)
245  not in SUPPORTED_MANUFACTURERS
246  ):
247  return self.async_abortasync_abortasync_abort(reason="not_denonavr_manufacturer")
248 
249  # Check if required information is present to set the unique_id
250  if (
251  ssdp.ATTR_UPNP_MODEL_NAME not in discovery_info.upnp
252  or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp
253  ):
254  return self.async_abortasync_abortasync_abort(reason="not_denonavr_missing")
255 
256  self.model_namemodel_name = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME].replace(
257  "*", ""
258  )
259  self.serial_numberserial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]
260  assert discovery_info.ssdp_location is not None
261  self.hosthost = urlparse(discovery_info.ssdp_location).hostname
262 
263  if self.model_namemodel_name in IGNORED_MODELS:
264  return self.async_abortasync_abortasync_abort(reason="not_denonavr_manufacturer")
265 
266  unique_id = self.construct_unique_idconstruct_unique_id(self.model_namemodel_name, self.serial_numberserial_number)
267  await self.async_set_unique_idasync_set_unique_id(unique_id)
268  self._abort_if_unique_id_configured_abort_if_unique_id_configured({CONF_HOST: self.hosthost})
269 
270  self.context.update(
271  {
272  "title_placeholders": {
273  "name": discovery_info.upnp.get(
274  ssdp.ATTR_UPNP_FRIENDLY_NAME, self.hosthost
275  )
276  }
277  }
278  )
279 
280  return await self.async_step_confirmasync_step_confirm()
281 
282  @staticmethod
283  def construct_unique_id(model_name: str | None, serial_number: str | None) -> str:
284  """Construct the unique id from the ssdp discovery or user_step."""
285  return f"{model_name}-{serial_number}"
ConfigFlowResult async_step_connect(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:180
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:236
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:116
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:170
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:122
str construct_unique_id(str|None model_name, str|None serial_number)
Definition: config_flow.py:283
ConfigFlowResult async_step_select(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:149
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:57
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)
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)
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)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
httpx.AsyncClient get_async_client(HomeAssistant hass, bool verify_ssl=True)
Definition: httpx_client.py:41