Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for the Bang & Olufsen integration."""
2 
3 from __future__ import annotations
4 
5 from ipaddress import AddressValueError, IPv4Address
6 from typing import Any, TypedDict
7 
8 from aiohttp.client_exceptions import ClientConnectorError
9 from mozart_api.exceptions import ApiException
10 from mozart_api.mozart_client import MozartClient
11 import voluptuous as vol
12 
13 from homeassistant.components.zeroconf import ZeroconfServiceInfo
14 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
15 from homeassistant.const import CONF_HOST, CONF_MODEL
16 from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
17 from homeassistant.util.ssl import get_default_context
18 
19 from .const import (
20  ATTR_FRIENDLY_NAME,
21  ATTR_ITEM_NUMBER,
22  ATTR_SERIAL_NUMBER,
23  ATTR_TYPE_NUMBER,
24  COMPATIBLE_MODELS,
25  CONF_SERIAL_NUMBER,
26  DEFAULT_MODEL,
27  DOMAIN,
28 )
29 from .util import get_serial_number_from_jid
30 
31 
32 class EntryData(TypedDict, total=False):
33  """TypedDict for config_entry data."""
34 
35  host: str
36  jid: str
37  model: str
38  name: str
39 
40 
41 # Map exception types to strings
42 _exception_map = {
43  ApiException: "api_exception",
44  ClientConnectorError: "client_connector_error",
45  TimeoutError: "timeout_error",
46  AddressValueError: "invalid_ip",
47 }
48 
49 
51  """Handle a config flow."""
52 
53  _beolink_jid = ""
54  _client: MozartClient
55  _host = ""
56  _model = ""
57  _name = ""
58  _serial_number = ""
59 
60  def __init__(self) -> None:
61  """Init the config flow."""
62 
63  VERSION = 1
64 
65  async def async_step_user(
66  self, user_input: dict[str, Any] | None = None
67  ) -> ConfigFlowResult:
68  """Handle the initial step."""
69  data_schema = vol.Schema(
70  {
71  vol.Required(CONF_HOST): str,
72  vol.Required(CONF_MODEL, default=DEFAULT_MODEL): SelectSelector(
73  SelectSelectorConfig(options=COMPATIBLE_MODELS)
74  ),
75  }
76  )
77 
78  if user_input is not None:
79  self._host_host_host = user_input[CONF_HOST]
80  self._model_model_model = user_input[CONF_MODEL]
81 
82  # Check if the IP address is a valid IPv4 address.
83  try:
84  IPv4Address(self._host_host_host)
85  except AddressValueError as error:
86  return self.async_show_formasync_show_formasync_show_form(
87  step_id="user",
88  data_schema=data_schema,
89  errors={"base": _exception_map[type(error)]},
90  )
91 
92  self._client_client = MozartClient(
93  host=self._host_host_host, ssl_context=get_default_context()
94  )
95 
96  # Try to get information from Beolink self method.
97  async with self._client_client:
98  try:
99  beolink_self = await self._client_client.get_beolink_self(
100  _request_timeout=3
101  )
102  except (
103  ApiException,
104  ClientConnectorError,
105  TimeoutError,
106  ) as error:
107  return self.async_show_formasync_show_formasync_show_form(
108  step_id="user",
109  data_schema=data_schema,
110  errors={"base": _exception_map[type(error)]},
111  )
112 
113  self._beolink_jid_beolink_jid_beolink_jid = beolink_self.jid
114  self._serial_number_serial_number_serial_number = get_serial_number_from_jid(beolink_self.jid)
115 
116  await self.async_set_unique_idasync_set_unique_id(self._serial_number_serial_number_serial_number)
117  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
118 
119  return await self._create_entry_create_entry()
120 
121  return self.async_show_formasync_show_formasync_show_form(
122  step_id="user",
123  data_schema=data_schema,
124  )
125 
127  self, discovery_info: ZeroconfServiceInfo
128  ) -> ConfigFlowResult:
129  """Handle discovery using Zeroconf."""
130 
131  # Check if the discovered device is a Mozart device
132  if ATTR_FRIENDLY_NAME not in discovery_info.properties:
133  return self.async_abortasync_abortasync_abort(reason="not_mozart_device")
134 
135  # Ensure that an IPv4 address is received
136  self._host_host_host = discovery_info.host
137  try:
138  IPv4Address(self._host_host_host)
139  except AddressValueError:
140  return self.async_abortasync_abortasync_abort(reason="ipv6_address")
141 
142  # Check connection to ensure valid address is received
143  self._client_client = MozartClient(self._host_host_host, ssl_context=get_default_context())
144 
145  async with self._client_client:
146  try:
147  await self._client_client.get_beolink_self(_request_timeout=3)
148  except (ClientConnectorError, TimeoutError):
149  return self.async_abortasync_abortasync_abort(reason="invalid_address")
150 
151  self._model_model_model = discovery_info.hostname[:-16].replace("-", " ")
152  self._serial_number_serial_number_serial_number = discovery_info.properties[ATTR_SERIAL_NUMBER]
153  self._beolink_jid_beolink_jid_beolink_jid = f"{discovery_info.properties[ATTR_TYPE_NUMBER]}.{discovery_info.properties[ATTR_ITEM_NUMBER]}.{self._serial_number}@products.bang-olufsen.com"
154 
155  await self.async_set_unique_idasync_set_unique_id(self._serial_number_serial_number_serial_number)
156  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: self._host_host_host})
157 
158  # Set the discovered device title
159  self.context["title_placeholders"] = {
160  "name": discovery_info.properties[ATTR_FRIENDLY_NAME]
161  }
162 
163  return await self.async_step_zeroconf_confirmasync_step_zeroconf_confirm()
164 
165  async def _create_entry(self) -> ConfigFlowResult:
166  """Create the config entry for a discovered or manually configured Bang & Olufsen device."""
167  # Ensure that created entities have a unique and easily identifiable id and not a "friendly name"
168  self._name_name_name = f"{self._model}-{self._serial_number}"
169 
170  return self.async_create_entryasync_create_entryasync_create_entry(
171  title=self._name_name_name,
172  data=EntryData(
173  host=self._host_host_host,
174  jid=self._beolink_jid_beolink_jid_beolink_jid,
175  model=self._model_model_model,
176  name=self._name_name_name,
177  ),
178  )
179 
181  self, user_input: dict[str, Any] | None = None
182  ) -> ConfigFlowResult:
183  """Confirm the configuration of the device."""
184  if user_input is not None:
185  return await self._create_entry_create_entry()
186 
187  self._set_confirm_only_set_confirm_only()
188 
189  return self.async_show_formasync_show_formasync_show_form(
190  step_id="zeroconf_confirm",
191  description_placeholders={
192  CONF_HOST: self._host_host_host,
193  CONF_MODEL: self._model_model_model,
194  CONF_SERIAL_NUMBER: self._serial_number_serial_number_serial_number,
195  },
196  last_step=True,
197  )
ConfigFlowResult async_step_zeroconf_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:182
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:128
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:67
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)
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)
ssl.SSLContext get_default_context()
Definition: ssl.py:118