Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Vogel's MotionMount."""
2 
3 import logging
4 import socket
5 from typing import Any
6 
7 import motionmount
8 import voluptuous as vol
9 
10 from homeassistant.components import zeroconf
11 from homeassistant.config_entries import (
12  DEFAULT_DISCOVERY_UNIQUE_ID,
13  ConfigFlow,
14  ConfigFlowResult,
15 )
16 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_UUID
17 from homeassistant.helpers.device_registry import format_mac
18 
19 from .const import DOMAIN, EMPTY_MAC
20 
21 _LOGGER = logging.getLogger(__name__)
22 
23 
24 # A MotionMount can be in four states:
25 # 1. Old CE and old Pro FW -> It doesn't supply any kind of mac
26 # 2. Old CE but new Pro FW -> It supplies its mac using DNS-SD, but a read of the mac fails
27 # 3. New CE but old Pro FW -> It doesn't supply the mac using DNS-SD but we can read it (returning the EMPTY_MAC)
28 # 4. New CE and new Pro FW -> Both DNS-SD and a read gives us the mac
29 # If we can't get the mac, we use DEFAULT_DISCOVERY_UNIQUE_ID as an ID, so we can always configure a single MotionMount. Most households will only have a single MotionMount
30 class MotionMountFlowHandler(ConfigFlow, domain=DOMAIN):
31  """Handle a Vogel's MotionMount config flow."""
32 
33  VERSION = 1
34 
35  def __init__(self) -> None:
36  """Set up the instance."""
37  self.discovery_info: dict[str, Any] = {}
38 
39  async def async_step_user(
40  self, user_input: dict[str, Any] | None = None
41  ) -> ConfigFlowResult:
42  """Handle a flow initiated by the user."""
43  if user_input is None:
44  return self._show_setup_form_show_setup_form()
45 
46  info = {}
47  try:
48  info = await self._validate_input_validate_input(user_input)
49  except (ConnectionError, socket.gaierror):
50  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
51  except TimeoutError:
52  return self.async_abortasync_abortasync_abort(reason="time_out")
53  except motionmount.NotConnectedError:
54  return self.async_abortasync_abortasync_abort(reason="not_connected")
55  except motionmount.MotionMountResponseError:
56  # This is most likely due to missing support for the mac address property
57  # Abort if the handler has config entries already
58  if self._async_current_entries_async_current_entries():
59  return self.async_abortasync_abortasync_abort(reason="already_configured")
60 
61  # Otherwise we try to continue with the generic uid
62  info[CONF_UUID] = DEFAULT_DISCOVERY_UNIQUE_ID
63 
64  # If the device mac is valid we use it, otherwise we use the default id
65  if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
66  unique_id = info[CONF_UUID]
67  else:
68  unique_id = DEFAULT_DISCOVERY_UNIQUE_ID
69 
70  name = info.get(CONF_NAME, user_input[CONF_HOST])
71 
72  await self.async_set_unique_idasync_set_unique_id(unique_id)
73  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
74  updates={
75  CONF_HOST: user_input[CONF_HOST],
76  CONF_PORT: user_input[CONF_PORT],
77  }
78  )
79 
80  return self.async_create_entryasync_create_entryasync_create_entry(title=name, data=user_input)
81 
83  self, discovery_info: zeroconf.ZeroconfServiceInfo
84  ) -> ConfigFlowResult:
85  """Handle zeroconf discovery."""
86 
87  # Extract information from discovery
88  host = discovery_info.hostname
89  port = discovery_info.port
90  zctype = discovery_info.type
91  name = discovery_info.name.removesuffix(f".{zctype}")
92  unique_id = discovery_info.properties.get("mac")
93 
94  self.discovery_info.update(
95  {
96  CONF_HOST: host,
97  CONF_PORT: port,
98  CONF_NAME: name,
99  }
100  )
101 
102  if unique_id:
103  # If we already have the unique id, try to set it now
104  # so we can avoid probing the device if its already
105  # configured or ignored
106  await self.async_set_unique_idasync_set_unique_id(unique_id)
107  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
108  updates={CONF_HOST: host, CONF_PORT: port}
109  )
110  else:
111  # Avoid probing devices that already have an entry
112  self._async_abort_entries_match_async_abort_entries_match({CONF_HOST: host})
113 
114  self.context.update({"title_placeholders": {"name": name}})
115 
116  try:
117  info = await self._validate_input_validate_input(self.discovery_info)
118  except (ConnectionError, socket.gaierror):
119  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
120  except TimeoutError:
121  return self.async_abortasync_abortasync_abort(reason="time_out")
122  except motionmount.NotConnectedError:
123  return self.async_abortasync_abortasync_abort(reason="not_connected")
124  except motionmount.MotionMountResponseError:
125  info = {}
126  # We continue as we want to be able to connect with older FW that does not support MAC address
127 
128  # If the device supplied as with a valid MAC we use that
129  if info.get(CONF_UUID, EMPTY_MAC) != EMPTY_MAC:
130  unique_id = info[CONF_UUID]
131 
132  if unique_id:
133  await self.async_set_unique_idasync_set_unique_id(unique_id)
134  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
135  updates={CONF_HOST: host, CONF_PORT: port}
136  )
137  else:
138  await self._async_handle_discovery_without_unique_id_async_handle_discovery_without_unique_id()
139 
140  return await self.async_step_zeroconf_confirmasync_step_zeroconf_confirm()
141 
143  self, user_input: dict[str, Any] | None = None
144  ) -> ConfigFlowResult:
145  """Handle a confirmation flow initiated by zeroconf."""
146  if user_input is None:
147  return self.async_show_formasync_show_formasync_show_form(
148  step_id="zeroconf_confirm",
149  description_placeholders={CONF_NAME: self.discovery_info[CONF_NAME]},
150  errors={},
151  )
152 
153  return self.async_create_entryasync_create_entryasync_create_entry(
154  title=self.discovery_info[CONF_NAME],
155  data=self.discovery_info,
156  )
157 
158  async def _validate_input(self, data: dict) -> dict[str, Any]:
159  """Validate the user input allows us to connect."""
160 
161  mm = motionmount.MotionMount(data[CONF_HOST], data[CONF_PORT])
162  try:
163  await mm.connect()
164  finally:
165  await mm.disconnect()
166 
167  return {CONF_UUID: format_mac(mm.mac.hex()), CONF_NAME: mm.name}
168 
170  self, errors: dict[str, str] | None = None
171  ) -> ConfigFlowResult:
172  """Show the setup form to the user."""
173  return self.async_show_formasync_show_formasync_show_form(
174  step_id="user",
175  data_schema=vol.Schema(
176  {
177  vol.Required(CONF_HOST): str,
178  vol.Required(CONF_PORT, default=23): int,
179  }
180  ),
181  errors=errors or {},
182  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:41
ConfigFlowResult _show_setup_form(self, dict[str, str]|None errors=None)
Definition: config_flow.py:171
ConfigFlowResult async_step_zeroconf_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:144
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:84
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_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)
_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