Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for AlarmDecoder."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from adext import AdExt
9 from alarmdecoder.devices import SerialDevice, SocketDevice
10 from alarmdecoder.util import NoDeviceError
11 import voluptuous as vol
12 
14  DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
15 )
16 from homeassistant.config_entries import (
17  ConfigEntry,
18  ConfigFlow,
19  ConfigFlowResult,
20  OptionsFlow,
21 )
22 from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL
23 from homeassistant.core import callback
24 
25 from .const import (
26  CONF_ALT_NIGHT_MODE,
27  CONF_AUTO_BYPASS,
28  CONF_CODE_ARM_REQUIRED,
29  CONF_DEVICE_BAUD,
30  CONF_DEVICE_PATH,
31  CONF_RELAY_ADDR,
32  CONF_RELAY_CHAN,
33  CONF_ZONE_LOOP,
34  CONF_ZONE_NAME,
35  CONF_ZONE_NUMBER,
36  CONF_ZONE_RFID,
37  CONF_ZONE_TYPE,
38  DEFAULT_ARM_OPTIONS,
39  DEFAULT_DEVICE_BAUD,
40  DEFAULT_DEVICE_HOST,
41  DEFAULT_DEVICE_PATH,
42  DEFAULT_DEVICE_PORT,
43  DEFAULT_ZONE_OPTIONS,
44  DEFAULT_ZONE_TYPE,
45  DOMAIN,
46  OPTIONS_ARM,
47  OPTIONS_ZONES,
48  PROTOCOL_SERIAL,
49  PROTOCOL_SOCKET,
50 )
51 
52 EDIT_KEY = "edit_selection"
53 EDIT_ZONES = "Zones"
54 EDIT_SETTINGS = "Arming Settings"
55 
56 _LOGGER = logging.getLogger(__name__)
57 
58 
59 class AlarmDecoderFlowHandler(ConfigFlow, domain=DOMAIN):
60  """Handle a AlarmDecoder config flow."""
61 
62  VERSION = 1
63 
64  def __init__(self) -> None:
65  """Initialize AlarmDecoder ConfigFlow."""
66  self.protocolprotocol = None
67 
68  @staticmethod
69  @callback
71  config_entry: ConfigEntry,
72  ) -> AlarmDecoderOptionsFlowHandler:
73  """Get the options flow for AlarmDecoder."""
74  return AlarmDecoderOptionsFlowHandler(config_entry)
75 
76  async def async_step_user(
77  self, user_input: dict[str, Any] | None = None
78  ) -> ConfigFlowResult:
79  """Handle a flow initialized by the user."""
80  if user_input is not None:
81  self.protocolprotocol = user_input[CONF_PROTOCOL]
82  return await self.async_step_protocolasync_step_protocol()
83 
84  return self.async_show_formasync_show_formasync_show_form(
85  step_id="user",
86  data_schema=vol.Schema(
87  {
88  vol.Required(CONF_PROTOCOL): vol.In(
89  [PROTOCOL_SOCKET, PROTOCOL_SERIAL]
90  ),
91  }
92  ),
93  )
94 
96  self, user_input: dict[str, Any] | None = None
97  ) -> ConfigFlowResult:
98  """Handle AlarmDecoder protocol setup."""
99  errors = {}
100  if user_input is not None:
102  self._async_current_entries_async_current_entries(), user_input, self.protocolprotocol
103  ):
104  return self.async_abortasync_abortasync_abort(reason="already_configured")
105  connection = {}
106  baud = None
107  if self.protocolprotocol == PROTOCOL_SOCKET:
108  host = connection[CONF_HOST] = user_input[CONF_HOST]
109  port = connection[CONF_PORT] = user_input[CONF_PORT]
110  title = f"{host}:{port}"
111  device = SocketDevice(interface=(host, port))
112  if self.protocolprotocol == PROTOCOL_SERIAL:
113  path = connection[CONF_DEVICE_PATH] = user_input[CONF_DEVICE_PATH]
114  baud = connection[CONF_DEVICE_BAUD] = user_input[CONF_DEVICE_BAUD]
115  title = path
116  device = SerialDevice(interface=path)
117 
118  controller = AdExt(device)
119 
120  def test_connection():
121  controller.open(baud)
122  controller.close()
123 
124  try:
125  await self.hass.async_add_executor_job(test_connection)
126  return self.async_create_entryasync_create_entryasync_create_entry(
127  title=title, data={CONF_PROTOCOL: self.protocolprotocol, **connection}
128  )
129  except NoDeviceError:
130  errors["base"] = "cannot_connect"
131  except Exception:
132  _LOGGER.exception("Unexpected exception during AlarmDecoder setup")
133  errors["base"] = "unknown"
134 
135  if self.protocolprotocol == PROTOCOL_SOCKET:
136  schema = vol.Schema(
137  {
138  vol.Required(CONF_HOST, default=DEFAULT_DEVICE_HOST): str,
139  vol.Required(CONF_PORT, default=DEFAULT_DEVICE_PORT): int,
140  }
141  )
142  if self.protocolprotocol == PROTOCOL_SERIAL:
143  schema = vol.Schema(
144  {
145  vol.Required(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): str,
146  vol.Required(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): int,
147  }
148  )
149 
150  return self.async_show_formasync_show_formasync_show_form(
151  step_id="protocol",
152  data_schema=schema,
153  errors=errors,
154  )
155 
156 
158  """Handle AlarmDecoder options."""
159 
160  selected_zone: str
161 
162  def __init__(self, config_entry: ConfigEntry) -> None:
163  """Initialize AlarmDecoder options flow."""
164  self.arm_optionsarm_options = config_entry.options.get(OPTIONS_ARM, DEFAULT_ARM_OPTIONS)
165  self.zone_optionszone_options = config_entry.options.get(
166  OPTIONS_ZONES, DEFAULT_ZONE_OPTIONS
167  )
168 
169  async def async_step_init(
170  self, user_input: dict[str, Any] | None = None
171  ) -> ConfigFlowResult:
172  """Manage the options."""
173  if user_input is not None:
174  if user_input[EDIT_KEY] == EDIT_SETTINGS:
175  return await self.async_step_arm_settingsasync_step_arm_settings()
176  if user_input[EDIT_KEY] == EDIT_ZONES:
177  return await self.async_step_zone_selectasync_step_zone_select()
178 
179  return self.async_show_formasync_show_form(
180  step_id="init",
181  data_schema=vol.Schema(
182  {
183  vol.Required(EDIT_KEY, default=EDIT_SETTINGS): vol.In(
184  [EDIT_SETTINGS, EDIT_ZONES]
185  )
186  },
187  ),
188  )
189 
191  self, user_input: dict[str, Any] | None = None
192  ) -> ConfigFlowResult:
193  """Arming options form."""
194  if user_input is not None:
195  return self.async_create_entryasync_create_entry(
196  title="",
197  data={OPTIONS_ARM: user_input, OPTIONS_ZONES: self.zone_optionszone_options},
198  )
199 
200  return self.async_show_formasync_show_form(
201  step_id="arm_settings",
202  data_schema=vol.Schema(
203  {
204  vol.Optional(
205  CONF_ALT_NIGHT_MODE,
206  default=self.arm_optionsarm_options[CONF_ALT_NIGHT_MODE],
207  ): bool,
208  vol.Optional(
209  CONF_AUTO_BYPASS, default=self.arm_optionsarm_options[CONF_AUTO_BYPASS]
210  ): bool,
211  vol.Optional(
212  CONF_CODE_ARM_REQUIRED,
213  default=self.arm_optionsarm_options[CONF_CODE_ARM_REQUIRED],
214  ): bool,
215  },
216  ),
217  )
218 
220  self, user_input: dict[str, Any] | None = None
221  ) -> ConfigFlowResult:
222  """Zone selection form."""
223  errors = _validate_zone_input(user_input)
224 
225  if user_input is not None and not errors:
226  self.selected_zoneselected_zone = str(
227  int(user_input[CONF_ZONE_NUMBER])
228  ) # remove leading zeros
229  return await self.async_step_zone_detailsasync_step_zone_details()
230 
231  return self.async_show_formasync_show_form(
232  step_id="zone_select",
233  data_schema=vol.Schema({vol.Required(CONF_ZONE_NUMBER): str}),
234  errors=errors,
235  )
236 
238  self, user_input: dict[str, Any] | None = None
239  ) -> ConfigFlowResult:
240  """Zone details form."""
241  errors = _validate_zone_input(user_input)
242 
243  if user_input is not None and not errors:
244  zone_options = self.zone_optionszone_options.copy()
245  zone_id = self.selected_zoneselected_zone
246  zone_options[zone_id] = _fix_input_types(user_input)
247 
248  # Delete zone entry if zone_name is omitted
249  if CONF_ZONE_NAME not in zone_options[zone_id]:
250  zone_options.pop(zone_id)
251 
252  return self.async_create_entryasync_create_entry(
253  title="",
254  data={OPTIONS_ARM: self.arm_optionsarm_options, OPTIONS_ZONES: zone_options},
255  )
256 
257  existing_zone_settings = self.zone_optionszone_options.get(self.selected_zoneselected_zone, {})
258 
259  return self.async_show_formasync_show_form(
260  step_id="zone_details",
261  description_placeholders={CONF_ZONE_NUMBER: self.selected_zoneselected_zone},
262  data_schema=vol.Schema(
263  {
264  vol.Optional(
265  CONF_ZONE_NAME,
266  description={
267  "suggested_value": existing_zone_settings.get(
268  CONF_ZONE_NAME
269  )
270  },
271  ): str,
272  vol.Optional(
273  CONF_ZONE_TYPE,
274  default=existing_zone_settings.get(
275  CONF_ZONE_TYPE, DEFAULT_ZONE_TYPE
276  ),
277  ): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
278  vol.Optional(
279  CONF_ZONE_RFID,
280  description={
281  "suggested_value": existing_zone_settings.get(
282  CONF_ZONE_RFID
283  )
284  },
285  ): str,
286  vol.Optional(
287  CONF_ZONE_LOOP,
288  description={
289  "suggested_value": existing_zone_settings.get(
290  CONF_ZONE_LOOP
291  )
292  },
293  ): str,
294  vol.Optional(
295  CONF_RELAY_ADDR,
296  description={
297  "suggested_value": existing_zone_settings.get(
298  CONF_RELAY_ADDR
299  )
300  },
301  ): str,
302  vol.Optional(
303  CONF_RELAY_CHAN,
304  description={
305  "suggested_value": existing_zone_settings.get(
306  CONF_RELAY_CHAN
307  )
308  },
309  ): str,
310  }
311  ),
312  errors=errors,
313  )
314 
315 
316 def _validate_zone_input(zone_input: dict[str, Any] | None) -> dict[str, str]:
317  if not zone_input:
318  return {}
319  errors = {}
320 
321  # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive
322  if (CONF_RELAY_ADDR in zone_input and CONF_RELAY_CHAN not in zone_input) or (
323  CONF_RELAY_ADDR not in zone_input and CONF_RELAY_CHAN in zone_input
324  ):
325  errors["base"] = "relay_inclusive"
326 
327  # The following keys must be int
328  for key in (CONF_ZONE_NUMBER, CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN):
329  if key in zone_input:
330  try:
331  int(zone_input[key])
332  except ValueError:
333  errors[key] = "int"
334 
335  # CONF_ZONE_LOOP depends on CONF_ZONE_RFID
336  if CONF_ZONE_LOOP in zone_input and CONF_ZONE_RFID not in zone_input:
337  errors[CONF_ZONE_LOOP] = "loop_rfid"
338 
339  # CONF_ZONE_LOOP must be 1-4
340  if (
341  CONF_ZONE_LOOP in zone_input
342  and zone_input[CONF_ZONE_LOOP].isdigit()
343  and int(zone_input[CONF_ZONE_LOOP]) not in list(range(1, 5))
344  ):
345  errors[CONF_ZONE_LOOP] = "loop_range"
346 
347  return errors
348 
349 
350 def _fix_input_types(zone_input: dict[str, Any]) -> dict[str, Any]:
351  """Convert necessary keys to int.
352 
353  Since ConfigFlow inputs of type int cannot default to an empty string, we collect the values below as
354  strings and then convert them to ints.
355  """
356 
357  for key in (CONF_ZONE_LOOP, CONF_RELAY_ADDR, CONF_RELAY_CHAN):
358  if key in zone_input:
359  zone_input[key] = int(zone_input[key])
360 
361  return zone_input
362 
363 
365  current_entries: list[ConfigEntry], user_input: dict[str, Any], protocol: str | None
366 ) -> bool:
367  """Determine if entry has already been added to HA."""
368  user_host = user_input.get(CONF_HOST)
369  user_port = user_input.get(CONF_PORT)
370  user_path = user_input.get(CONF_DEVICE_PATH)
371  user_baud = user_input.get(CONF_DEVICE_BAUD)
372 
373  for entry in current_entries:
374  entry_host = entry.data.get(CONF_HOST)
375  entry_port = entry.data.get(CONF_PORT)
376  entry_path = entry.data.get(CONF_DEVICE_PATH)
377  entry_baud = entry.data.get(CONF_DEVICE_BAUD)
378 
379  if (
380  protocol == PROTOCOL_SOCKET
381  and user_host == entry_host
382  and user_port == entry_port
383  ):
384  return True
385 
386  if (
387  protocol == PROTOCOL_SERIAL
388  and user_baud == entry_baud
389  and user_path == entry_path
390  ):
391  return True
392 
393  return False
AlarmDecoderOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:72
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:78
ConfigFlowResult async_step_protocol(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:97
ConfigFlowResult async_step_zone_select(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:221
ConfigFlowResult async_step_zone_details(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:239
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:171
ConfigFlowResult async_step_arm_settings(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:192
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)
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)
str
_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)
dict[str, str] _validate_zone_input(dict[str, Any]|None zone_input)
Definition: config_flow.py:316
dict[str, Any] _fix_input_types(dict[str, Any] zone_input)
Definition: config_flow.py:350
bool _device_already_added(list[ConfigEntry] current_entries, dict[str, Any] user_input, str|None protocol)
Definition: config_flow.py:366
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str test_connection(HomeAssistant hass, str host, int port)
Definition: config_flow.py:100