Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure Axis devices."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from ipaddress import ip_address
7 from types import MappingProxyType
8 from typing import Any
9 from urllib.parse import urlsplit
10 
11 import voluptuous as vol
12 
13 from homeassistant.components import dhcp, ssdp, zeroconf
14 from homeassistant.config_entries import (
15  SOURCE_IGNORE,
16  SOURCE_REAUTH,
17  SOURCE_RECONFIGURE,
18  ConfigEntry,
19  ConfigFlow,
20  ConfigFlowResult,
21  OptionsFlow,
22 )
23 from homeassistant.const import (
24  CONF_HOST,
25  CONF_MAC,
26  CONF_MODEL,
27  CONF_NAME,
28  CONF_PASSWORD,
29  CONF_PORT,
30  CONF_PROTOCOL,
31  CONF_USERNAME,
32 )
33 from homeassistant.core import callback
34 from homeassistant.helpers.device_registry import format_mac
35 from homeassistant.helpers.typing import VolDictType
36 from homeassistant.util.network import is_link_local
37 
38 from . import AxisConfigEntry
39 from .const import (
40  CONF_STREAM_PROFILE,
41  CONF_VIDEO_SOURCE,
42  DEFAULT_STREAM_PROFILE,
43  DEFAULT_VIDEO_SOURCE,
44  DOMAIN as AXIS_DOMAIN,
45 )
46 from .errors import AuthenticationRequired, CannotConnect
47 from .hub import AxisHub, get_axis_api
48 
49 AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"}
50 DEFAULT_PORT = 443
51 DEFAULT_PROTOCOL = "https"
52 PROTOCOL_CHOICES = ["https", "http"]
53 
54 
55 class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
56  """Handle a Axis config flow."""
57 
58  VERSION = 3
59 
60  @staticmethod
61  @callback
63  config_entry: ConfigEntry,
64  ) -> AxisOptionsFlowHandler:
65  """Get the options flow for this handler."""
66  return AxisOptionsFlowHandler()
67 
68  def __init__(self) -> None:
69  """Initialize the Axis config flow."""
70  self.configconfig: dict[str, Any] = {}
71  self.discovery_schemadiscovery_schema: VolDictType | None = None
72 
73  async def async_step_user(
74  self, user_input: dict[str, Any] | None = None
75  ) -> ConfigFlowResult:
76  """Handle a Axis config flow start.
77 
78  Manage device specific parameters.
79  """
80  errors = {}
81 
82  if user_input is not None:
83  try:
84  api = await get_axis_api(self.hass, MappingProxyType(user_input))
85 
86  except AuthenticationRequired:
87  errors["base"] = "invalid_auth"
88 
89  except CannotConnect:
90  errors["base"] = "cannot_connect"
91 
92  else:
93  serial = api.vapix.serial_number
94  config = {
95  CONF_PROTOCOL: user_input[CONF_PROTOCOL],
96  CONF_HOST: user_input[CONF_HOST],
97  CONF_PORT: user_input[CONF_PORT],
98  CONF_USERNAME: user_input[CONF_USERNAME],
99  CONF_PASSWORD: user_input[CONF_PASSWORD],
100  }
101 
102  await self.async_set_unique_idasync_set_unique_id(format_mac(serial))
103 
104  if self.sourcesourcesourcesource == SOURCE_REAUTH:
105  self._abort_if_unique_id_mismatch_abort_if_unique_id_mismatch()
106  return self.async_update_reload_and_abortasync_update_reload_and_abort(
107  self._get_reauth_entry_get_reauth_entry(), data_updates=config
108  )
109  if self.sourcesourcesourcesource == SOURCE_RECONFIGURE:
110  self._abort_if_unique_id_mismatch_abort_if_unique_id_mismatch()
111  return self.async_update_reload_and_abortasync_update_reload_and_abort(
112  self._get_reconfigure_entry_get_reconfigure_entry(), data_updates=config
113  )
114  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
115 
116  self.configconfig = config | {CONF_MODEL: api.vapix.product_number}
117 
118  return await self._create_entry_create_entry(serial)
119 
120  data = self.discovery_schemadiscovery_schema or {
121  vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),
122  vol.Required(CONF_HOST): str,
123  vol.Required(CONF_USERNAME): str,
124  vol.Required(CONF_PASSWORD): str,
125  vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
126  }
127 
128  return self.async_show_formasync_show_formasync_show_form(
129  step_id="user",
130  description_placeholders=self.configconfig,
131  data_schema=vol.Schema(data),
132  errors=errors,
133  )
134 
135  async def _create_entry(self, serial: str) -> ConfigFlowResult:
136  """Create entry for device.
137 
138  Generate a name to be used as a prefix for device entities.
139  """
140  model = self.configconfig[CONF_MODEL]
141  same_model = [
142  entry.data[CONF_NAME]
143  for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
144  if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
145  ]
146 
147  name = model
148  for idx in range(len(same_model) + 1):
149  name = f"{model} {idx}"
150  if name not in same_model:
151  break
152 
153  self.configconfig[CONF_NAME] = name
154 
155  title = f"{model} - {serial}"
156  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=self.configconfig)
157 
159  self, user_input: dict[str, Any] | None = None
160  ) -> ConfigFlowResult:
161  """Trigger a reconfiguration flow."""
162  return await self._redo_configuration_redo_configuration(
163  self._get_reconfigure_entry_get_reconfigure_entry().data, keep_password=True
164  )
165 
166  async def async_step_reauth(
167  self, entry_data: Mapping[str, Any]
168  ) -> ConfigFlowResult:
169  """Trigger a reauthentication flow."""
170  self.context["title_placeholders"] = {
171  CONF_NAME: entry_data[CONF_NAME],
172  CONF_HOST: entry_data[CONF_HOST],
173  }
174  return await self._redo_configuration_redo_configuration(entry_data, keep_password=False)
175 
177  self, entry_data: Mapping[str, Any], keep_password: bool
178  ) -> ConfigFlowResult:
179  """Re-run configuration step."""
180  protocol = entry_data.get(CONF_PROTOCOL, "http")
181  password = entry_data[CONF_PASSWORD] if keep_password else ""
182  self.discovery_schemadiscovery_schema = {
183  vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES),
184  vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str,
185  vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str,
186  vol.Required(CONF_PASSWORD, default=password): str,
187  vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int,
188  }
189 
190  return await self.async_step_userasync_step_userasync_step_user()
191 
192  async def async_step_dhcp(
193  self, discovery_info: dhcp.DhcpServiceInfo
194  ) -> ConfigFlowResult:
195  """Prepare configuration for a DHCP discovered Axis device."""
196  return await self._process_discovered_device_process_discovered_device(
197  {
198  CONF_HOST: discovery_info.ip,
199  CONF_MAC: format_mac(discovery_info.macaddress),
200  CONF_NAME: discovery_info.hostname,
201  CONF_PORT: 80,
202  }
203  )
204 
205  async def async_step_ssdp(
206  self, discovery_info: ssdp.SsdpServiceInfo
207  ) -> ConfigFlowResult:
208  """Prepare configuration for a SSDP discovered Axis device."""
209  url = urlsplit(discovery_info.upnp[ssdp.ATTR_UPNP_PRESENTATION_URL])
210  return await self._process_discovered_device_process_discovered_device(
211  {
212  CONF_HOST: url.hostname,
213  CONF_MAC: format_mac(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]),
214  CONF_NAME: f"{discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME]}",
215  CONF_PORT: url.port,
216  }
217  )
218 
220  self, discovery_info: zeroconf.ZeroconfServiceInfo
221  ) -> ConfigFlowResult:
222  """Prepare configuration for a Zeroconf discovered Axis device."""
223  return await self._process_discovered_device_process_discovered_device(
224  {
225  CONF_HOST: discovery_info.host,
226  CONF_MAC: format_mac(discovery_info.properties["macaddress"]),
227  CONF_NAME: discovery_info.name.split(".", 1)[0],
228  CONF_PORT: discovery_info.port,
229  }
230  )
231 
233  self, discovery_info: dict[str, Any]
234  ) -> ConfigFlowResult:
235  """Prepare configuration for a discovered Axis device."""
236  if discovery_info[CONF_MAC][:8] not in AXIS_OUI:
237  return self.async_abortasync_abortasync_abort(reason="not_axis_device")
238 
239  if is_link_local(ip_address(discovery_info[CONF_HOST])):
240  return self.async_abortasync_abortasync_abort(reason="link_local_address")
241 
242  await self.async_set_unique_idasync_set_unique_id(discovery_info[CONF_MAC])
243 
244  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
245  updates={CONF_HOST: discovery_info[CONF_HOST]}
246  )
247 
248  self.context.update(
249  {
250  "title_placeholders": {
251  CONF_NAME: discovery_info[CONF_NAME],
252  CONF_HOST: discovery_info[CONF_HOST],
253  },
254  "configuration_url": f"http://{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}",
255  }
256  )
257 
258  self.discovery_schemadiscovery_schema = {
259  vol.Required(CONF_PROTOCOL): vol.In(PROTOCOL_CHOICES),
260  vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str,
261  vol.Required(CONF_USERNAME): str,
262  vol.Required(CONF_PASSWORD): str,
263  vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
264  }
265 
266  return await self.async_step_userasync_step_userasync_step_user()
267 
268 
270  """Handle Axis device options."""
271 
272  config_entry: AxisConfigEntry
273  hub: AxisHub
274 
275  async def async_step_init(
276  self, user_input: dict[str, Any] | None = None
277  ) -> ConfigFlowResult:
278  """Manage the Axis device options."""
279  self.hubhub = self.config_entryconfig_entryconfig_entry.runtime_data
280  return await self.async_step_configure_streamasync_step_configure_stream()
281 
283  self, user_input: dict[str, Any] | None = None
284  ) -> ConfigFlowResult:
285  """Manage the Axis device stream options."""
286  if user_input is not None:
287  return self.async_create_entryasync_create_entry(data=self.config_entryconfig_entryconfig_entry.options | user_input)
288 
289  schema = {}
290 
291  vapix = self.hubhub.api.vapix
292 
293  # Stream profiles
294 
295  if vapix.stream_profiles or (
296  (profiles := vapix.params.stream_profile_handler.get("0"))
297  and profiles.max_groups > 0
298  ):
299  stream_profiles = [DEFAULT_STREAM_PROFILE]
300  stream_profiles.extend(profile.name for profile in vapix.streaming_profiles)
301 
302  schema[
303  vol.Optional(
304  CONF_STREAM_PROFILE, default=self.hubhub.config.stream_profile
305  )
306  ] = vol.In(stream_profiles)
307 
308  # Video sources
309 
310  if (
311  properties := vapix.params.property_handler.get("0")
312  ) and properties.image_number_of_views > 0:
313  await vapix.params.image_handler.update()
314  video_sources: dict[int | str, str] = {
315  DEFAULT_VIDEO_SOURCE: DEFAULT_VIDEO_SOURCE
316  }
317  for idx, video_source in vapix.params.image_handler.items():
318  if not video_source.enabled:
319  continue
320  video_sources[int(idx) + 1] = video_source.name
321 
322  schema[
323  vol.Optional(CONF_VIDEO_SOURCE, default=self.hubhub.config.video_source)
324  ] = vol.In(video_sources)
325 
326  return self.async_show_formasync_show_form(
327  step_id="configure_stream", data_schema=vol.Schema(schema)
328  )
ConfigFlowResult _redo_configuration(self, Mapping[str, Any] entry_data, bool keep_password)
Definition: config_flow.py:178
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:75
ConfigFlowResult async_step_reconfigure(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:160
ConfigFlowResult _process_discovered_device(self, dict[str, Any] discovery_info)
Definition: config_flow.py:234
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:168
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:194
AxisOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:64
ConfigFlowResult _create_entry(self, str serial)
Definition: config_flow.py:135
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:207
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:221
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:277
ConfigFlowResult async_step_configure_stream(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:284
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_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_step_user(self, dict[str, Any]|None user_input=None)
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)
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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
axis.AxisDevice get_axis_api(HomeAssistant hass, MappingProxyType[str, Any] config)
Definition: api.py:27
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
bool is_link_local(IPv4Address|IPv6Address address)
Definition: network.py:48