Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for konnected.io integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import copy
7 import logging
8 import random
9 import string
10 from typing import Any
11 from urllib.parse import urlparse
12 
13 import voluptuous as vol
14 
15 from homeassistant.components import ssdp
17  DEVICE_CLASSES_SCHEMA,
18  BinarySensorDeviceClass,
19 )
20 from homeassistant.config_entries import (
21  ConfigEntry,
22  ConfigFlow,
23  ConfigFlowResult,
24  OptionsFlow,
25 )
26 from homeassistant.const import (
27  CONF_ACCESS_TOKEN,
28  CONF_BINARY_SENSORS,
29  CONF_DISCOVERY,
30  CONF_HOST,
31  CONF_ID,
32  CONF_MODEL,
33  CONF_NAME,
34  CONF_PORT,
35  CONF_REPEAT,
36  CONF_SENSORS,
37  CONF_SWITCHES,
38  CONF_TYPE,
39  CONF_ZONE,
40 )
41 from homeassistant.core import callback
42 from homeassistant.helpers import config_validation as cv
43 
44 from .const import (
45  CONF_ACTIVATION,
46  CONF_API_HOST,
47  CONF_BLINK,
48  CONF_DEFAULT_OPTIONS,
49  CONF_INVERSE,
50  CONF_MOMENTARY,
51  CONF_PAUSE,
52  CONF_POLL_INTERVAL,
53  DOMAIN,
54  STATE_HIGH,
55  STATE_LOW,
56  ZONES,
57 )
58 from .errors import CannotConnect
59 from .panel import KONN_MODEL, KONN_MODEL_PRO, get_status
60 
61 _LOGGER = logging.getLogger(__name__)
62 
63 ATTR_KONN_UPNP_MODEL_NAME = "model_name" # standard upnp is modelName
64 CONF_IO = "io"
65 CONF_IO_DIS = "Disabled"
66 CONF_IO_BIN = "Binary Sensor"
67 CONF_IO_DIG = "Digital Sensor"
68 CONF_IO_SWI = "Switchable Output"
69 
70 CONF_MORE_STATES = "more_states"
71 CONF_YES = "Yes"
72 CONF_NO = "No"
73 
74 CONF_OVERRIDE_API_HOST = "override_api_host"
75 
76 KONN_MANUFACTURER = "konnected.io"
77 KONN_PANEL_MODEL_NAMES = {
78  KONN_MODEL: "Konnected Alarm Panel",
79  KONN_MODEL_PRO: "Konnected Alarm Panel Pro",
80 }
81 
82 OPTIONS_IO_ANY = vol.In([CONF_IO_DIS, CONF_IO_BIN, CONF_IO_DIG, CONF_IO_SWI])
83 OPTIONS_IO_INPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_BIN])
84 OPTIONS_IO_OUTPUT_ONLY = vol.In([CONF_IO_DIS, CONF_IO_SWI])
85 
86 
87 # Config entry schemas
88 IO_SCHEMA = vol.Schema(
89  {
90  vol.Optional("1", default=CONF_IO_DIS): OPTIONS_IO_ANY,
91  vol.Optional("2", default=CONF_IO_DIS): OPTIONS_IO_ANY,
92  vol.Optional("3", default=CONF_IO_DIS): OPTIONS_IO_ANY,
93  vol.Optional("4", default=CONF_IO_DIS): OPTIONS_IO_ANY,
94  vol.Optional("5", default=CONF_IO_DIS): OPTIONS_IO_ANY,
95  vol.Optional("6", default=CONF_IO_DIS): OPTIONS_IO_ANY,
96  vol.Optional("7", default=CONF_IO_DIS): OPTIONS_IO_ANY,
97  vol.Optional("8", default=CONF_IO_DIS): OPTIONS_IO_ANY,
98  vol.Optional("9", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
99  vol.Optional("10", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
100  vol.Optional("11", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
101  vol.Optional("12", default=CONF_IO_DIS): OPTIONS_IO_INPUT_ONLY,
102  vol.Optional("out", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
103  vol.Optional("alarm1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
104  vol.Optional("out1", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
105  vol.Optional("alarm2_out2", default=CONF_IO_DIS): OPTIONS_IO_OUTPUT_ONLY,
106  }
107 )
108 
109 BINARY_SENSOR_SCHEMA = vol.Schema(
110  {
111  vol.Required(CONF_ZONE): vol.In(ZONES),
112  vol.Required(
113  CONF_TYPE, default=BinarySensorDeviceClass.DOOR
114  ): DEVICE_CLASSES_SCHEMA,
115  vol.Optional(CONF_NAME): cv.string,
116  vol.Optional(CONF_INVERSE, default=False): cv.boolean,
117  }
118 )
119 
120 SENSOR_SCHEMA = vol.Schema(
121  {
122  vol.Required(CONF_ZONE): vol.In(ZONES),
123  vol.Required(CONF_TYPE, default="dht"): vol.All(
124  vol.Lower, vol.In(["dht", "ds18b20"])
125  ),
126  vol.Optional(CONF_NAME): cv.string,
127  vol.Optional(CONF_POLL_INTERVAL, default=3): vol.All(
128  vol.Coerce(int), vol.Range(min=1)
129  ),
130  }
131 )
132 
133 SWITCH_SCHEMA = vol.Schema(
134  {
135  vol.Required(CONF_ZONE): vol.In(ZONES),
136  vol.Optional(CONF_NAME): cv.string,
137  vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All(
138  vol.Lower, vol.In([STATE_HIGH, STATE_LOW])
139  ),
140  vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)),
141  vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)),
142  vol.Optional(CONF_REPEAT): vol.All(vol.Coerce(int), vol.Range(min=-1)),
143  }
144 )
145 
146 OPTIONS_SCHEMA = vol.Schema(
147  {
148  vol.Required(CONF_IO): IO_SCHEMA,
149  vol.Optional(CONF_BINARY_SENSORS): vol.All(
150  cv.ensure_list, [BINARY_SENSOR_SCHEMA]
151  ),
152  vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]),
153  vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]),
154  vol.Optional(CONF_BLINK, default=True): cv.boolean,
155  vol.Optional(CONF_API_HOST, default=""): vol.Any("", cv.url),
156  vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
157  },
158  extra=vol.REMOVE_EXTRA,
159 )
160 
161 CONFIG_ENTRY_SCHEMA = vol.Schema(
162  {
163  vol.Required(CONF_ID): cv.matches_regex("[0-9a-f]{12}"),
164  vol.Required(CONF_HOST): cv.string,
165  vol.Required(CONF_PORT): cv.port,
166  vol.Required(CONF_MODEL): vol.Any(*KONN_PANEL_MODEL_NAMES),
167  vol.Required(CONF_ACCESS_TOKEN): cv.matches_regex("[a-zA-Z0-9]+"),
168  vol.Required(CONF_DEFAULT_OPTIONS): OPTIONS_SCHEMA,
169  },
170  extra=vol.REMOVE_EXTRA,
171 )
172 
173 
174 class KonnectedFlowHandler(ConfigFlow, domain=DOMAIN):
175  """Handle a config flow for Konnected Panels."""
176 
177  VERSION = 1
178 
179  # class variable to store/share discovered host information
180  DISCOVERED_HOSTS: dict[str, dict[str, Any]] = {}
181 
182  unique_id: str
183 
184  def __init__(self) -> None:
185  """Initialize the Konnected flow."""
186  self.data: dict[str, Any] = {}
187  self.optionsoptions = OPTIONS_SCHEMA({CONF_IO: {}})
188 
189  async def async_gen_config(self, host, port):
190  """Populate self.data based on panel status.
191 
192  This will raise CannotConnect if an error occurs
193  """
194  self.data[CONF_HOST] = host
195  self.data[CONF_PORT] = port
196  try:
197  status = await get_status(self.hass, host, port)
198  self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", ""))
199  except (CannotConnect, KeyError) as err:
200  raise CannotConnect from err
201 
202  self.data[CONF_MODEL] = status.get("model", KONN_MODEL)
203  self.data[CONF_ACCESS_TOKEN] = "".join(
204  random.choices(f"{string.ascii_uppercase}{string.digits}", k=20)
205  )
206 
207  async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
208  """Import a configuration.yaml config.
209 
210  This flow is triggered by `async_setup` for configured panels.
211  """
212  _LOGGER.debug(import_data)
213 
214  # save the data and confirm connection via user step
215  await self.async_set_unique_idasync_set_unique_id(import_data["id"])
216  self.optionsoptions = import_data[CONF_DEFAULT_OPTIONS]
217 
218  # config schema ensures we have port if we have host
219  if import_data.get(CONF_HOST):
220  # automatically connect if we have host info
221  return await self.async_step_userasync_step_userasync_step_user(
222  user_input={
223  CONF_HOST: import_data[CONF_HOST],
224  CONF_PORT: import_data[CONF_PORT],
225  }
226  )
227 
228  # if we have no host info wait for it or abort if previously configured
229  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
230  return await self.async_step_import_confirmasync_step_import_confirm()
231 
233  self, user_input: dict[str, Any] | None = None
234  ) -> ConfigFlowResult:
235  """Confirm the user wants to import the config entry."""
236  if user_input is None:
237  return self.async_show_formasync_show_formasync_show_form(
238  step_id="import_confirm",
239  description_placeholders={"id": self.unique_idunique_id},
240  )
241 
242  # if we have ssdp discovered applicable host info use it
243  if KonnectedFlowHandler.DISCOVERED_HOSTS.get(self.unique_idunique_id):
244  return await self.async_step_userasync_step_userasync_step_user(
245  user_input={
246  CONF_HOST: KonnectedFlowHandler.DISCOVERED_HOSTS[self.unique_idunique_id][
247  CONF_HOST
248  ],
249  CONF_PORT: KonnectedFlowHandler.DISCOVERED_HOSTS[self.unique_idunique_id][
250  CONF_PORT
251  ],
252  }
253  )
254  return await self.async_step_userasync_step_userasync_step_user()
255 
256  async def async_step_ssdp(
257  self, discovery_info: ssdp.SsdpServiceInfo
258  ) -> ConfigFlowResult:
259  """Handle a discovered konnected panel.
260 
261  This flow is triggered by the SSDP component. It will check if the
262  device is already configured and attempt to finish the config if not.
263  """
264  _LOGGER.debug(discovery_info)
265 
266  try:
267  if discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER] != KONN_MANUFACTURER:
268  return self.async_abortasync_abortasync_abort(reason="not_konn_panel")
269 
270  if not any(
271  name in discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME]
272  for name in KONN_PANEL_MODEL_NAMES
273  ):
274  _LOGGER.warning(
275  "Discovered unrecognized Konnected device %s",
276  discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME, "Unknown"),
277  )
278  return self.async_abortasync_abortasync_abort(reason="not_konn_panel")
279 
280  # If MAC is missing it is a bug in the device fw but we'll guard
281  # against it since the field is so vital
282  except KeyError:
283  _LOGGER.error("Malformed Konnected SSDP info")
284  else:
285  # extract host/port from ssdp_location
286  assert discovery_info.ssdp_location
287  netloc = urlparse(discovery_info.ssdp_location).netloc.split(":")
288  self._async_abort_entries_match_async_abort_entries_match(
289  {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])}
290  )
291 
292  try:
293  status = await get_status(self.hass, netloc[0], int(netloc[1]))
294  except CannotConnect:
295  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
296 
297  self.data[CONF_HOST] = netloc[0]
298  self.data[CONF_PORT] = int(netloc[1])
299  self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", ""))
300  self.data[CONF_MODEL] = status.get("model", KONN_MODEL)
301 
302  KonnectedFlowHandler.DISCOVERED_HOSTS[self.data[CONF_ID]] = {
303  CONF_HOST: self.data[CONF_HOST],
304  CONF_PORT: self.data[CONF_PORT],
305  }
306  return await self.async_step_confirmasync_step_confirm()
307 
308  return self.async_abortasync_abortasync_abort(reason="unknown")
309 
310  async def async_step_user(
311  self, user_input: dict[str, Any] | None = None
312  ) -> ConfigFlowResult:
313  """Connect to panel and get config."""
314  errors = {}
315  if user_input:
316  # build config info and wait for user confirmation
317  self.data[CONF_HOST] = user_input[CONF_HOST]
318  self.data[CONF_PORT] = user_input[CONF_PORT]
319 
320  # brief delay to allow processing of recent status req
321  await asyncio.sleep(0.1)
322  try:
323  status = await get_status(
324  self.hass, self.data[CONF_HOST], self.data[CONF_PORT]
325  )
326  except CannotConnect:
327  errors["base"] = "cannot_connect"
328  else:
329  self.data[CONF_ID] = status.get(
330  "chipId", status["mac"].replace(":", "")
331  )
332  self.data[CONF_MODEL] = status.get("model", KONN_MODEL)
333 
334  # save off our discovered host info
335  KonnectedFlowHandler.DISCOVERED_HOSTS[self.data[CONF_ID]] = {
336  CONF_HOST: self.data[CONF_HOST],
337  CONF_PORT: self.data[CONF_PORT],
338  }
339  return await self.async_step_confirmasync_step_confirm()
340 
341  return self.async_show_formasync_show_formasync_show_form(
342  step_id="user",
343  description_placeholders={
344  "host": self.data.get(CONF_HOST, "Unknown"),
345  "port": self.data.get(CONF_PORT, "Unknown"),
346  },
347  data_schema=vol.Schema(
348  {
349  vol.Required(CONF_HOST, default=self.data.get(CONF_HOST)): str,
350  vol.Required(CONF_PORT, default=self.data.get(CONF_PORT)): int,
351  }
352  ),
353  errors=errors,
354  )
355 
357  self, user_input: dict[str, Any] | None = None
358  ) -> ConfigFlowResult:
359  """Attempt to link with the Konnected panel.
360 
361  Given a configured host, will ask the user to confirm and finalize
362  the connection.
363  """
364  if user_input is None:
365  # abort and update an existing config entry if host info changes
366  await self.async_set_unique_idasync_set_unique_id(self.data[CONF_ID])
367  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
368  updates=self.data, reload_on_update=False
369  )
370  return self.async_show_formasync_show_formasync_show_form(
371  step_id="confirm",
372  description_placeholders={
373  "model": KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]],
374  "id": self.unique_idunique_id,
375  "host": self.data[CONF_HOST],
376  "port": self.data[CONF_PORT],
377  },
378  )
379 
380  # Create access token, attach default options and create entry
381  self.data[CONF_DEFAULT_OPTIONS] = self.optionsoptions
382  self.data[CONF_ACCESS_TOKEN] = self.hass.data.get(DOMAIN, {}).get(
383  CONF_ACCESS_TOKEN
384  ) or "".join(random.choices(f"{string.ascii_uppercase}{string.digits}", k=20))
385 
386  return self.async_create_entryasync_create_entryasync_create_entry(
387  title=KONN_PANEL_MODEL_NAMES[self.data[CONF_MODEL]],
388  data=self.data,
389  )
390 
391  @staticmethod
392  @callback
394  config_entry: ConfigEntry,
395  ) -> OptionsFlowHandler:
396  """Return the Options Flow."""
397  return OptionsFlowHandler(config_entry)
398 
399 
401  """Handle a option flow for a Konnected Panel."""
402 
403  def __init__(self, config_entry: ConfigEntry) -> None:
404  """Initialize options flow."""
405  self.modelmodel = config_entry.data[CONF_MODEL]
406  self.current_optcurrent_opt = (
407  config_entry.options or config_entry.data[CONF_DEFAULT_OPTIONS]
408  )
409 
410  # as config proceeds we'll build up new options and then replace what's in the config entry
411  self.new_opt: dict[str, Any] = {CONF_IO: {}}
412  self.active_cfgactive_cfg: str | None = None
413  self.io_cfgio_cfg: dict[str, Any] = {}
414  self.current_statescurrent_states: list[dict[str, Any]] = []
415  self.current_statecurrent_state = 1
416 
417  @callback
418  def get_current_cfg(self, io_type, zone):
419  """Get the current zone config."""
420  return next(
421  (
422  cfg
423  for cfg in self.current_optcurrent_opt.get(io_type, [])
424  if cfg[CONF_ZONE] == zone
425  ),
426  {},
427  )
428 
429  async def async_step_init(
430  self, user_input: dict[str, Any] | None = None
431  ) -> ConfigFlowResult:
432  """Handle options flow."""
433  return await self.async_step_options_ioasync_step_options_io()
434 
436  self, user_input: dict[str, Any] | None = None
437  ) -> ConfigFlowResult:
438  """Configure legacy panel IO or first half of pro IO."""
439  errors: dict[str, str] = {}
440  current_io = self.current_optcurrent_opt.get(CONF_IO, {})
441 
442  if user_input is not None:
443  # strip out disabled io and save for options cfg
444  for key, value in user_input.items():
445  if value != CONF_IO_DIS:
446  self.new_opt[CONF_IO][key] = value
447  return await self.async_step_options_io_extasync_step_options_io_ext()
448 
449  if self.modelmodel == KONN_MODEL:
450  return self.async_show_formasync_show_form(
451  step_id="options_io",
452  data_schema=vol.Schema(
453  {
454  vol.Required(
455  "1", default=current_io.get("1", CONF_IO_DIS)
456  ): OPTIONS_IO_ANY,
457  vol.Required(
458  "2", default=current_io.get("2", CONF_IO_DIS)
459  ): OPTIONS_IO_ANY,
460  vol.Required(
461  "3", default=current_io.get("3", CONF_IO_DIS)
462  ): OPTIONS_IO_ANY,
463  vol.Required(
464  "4", default=current_io.get("4", CONF_IO_DIS)
465  ): OPTIONS_IO_ANY,
466  vol.Required(
467  "5", default=current_io.get("5", CONF_IO_DIS)
468  ): OPTIONS_IO_ANY,
469  vol.Required(
470  "6", default=current_io.get("6", CONF_IO_DIS)
471  ): OPTIONS_IO_ANY,
472  vol.Required(
473  "out", default=current_io.get("out", CONF_IO_DIS)
474  ): OPTIONS_IO_OUTPUT_ONLY,
475  }
476  ),
477  description_placeholders={
478  "model": KONN_PANEL_MODEL_NAMES[self.modelmodel],
479  "host": self.config_entryconfig_entryconfig_entry.data[CONF_HOST],
480  },
481  errors=errors,
482  )
483 
484  # configure the first half of the pro board io
485  if self.modelmodel == KONN_MODEL_PRO:
486  return self.async_show_formasync_show_form(
487  step_id="options_io",
488  data_schema=vol.Schema(
489  {
490  vol.Required(
491  "1", default=current_io.get("1", CONF_IO_DIS)
492  ): OPTIONS_IO_ANY,
493  vol.Required(
494  "2", default=current_io.get("2", CONF_IO_DIS)
495  ): OPTIONS_IO_ANY,
496  vol.Required(
497  "3", default=current_io.get("3", CONF_IO_DIS)
498  ): OPTIONS_IO_ANY,
499  vol.Required(
500  "4", default=current_io.get("4", CONF_IO_DIS)
501  ): OPTIONS_IO_ANY,
502  vol.Required(
503  "5", default=current_io.get("5", CONF_IO_DIS)
504  ): OPTIONS_IO_ANY,
505  vol.Required(
506  "6", default=current_io.get("6", CONF_IO_DIS)
507  ): OPTIONS_IO_ANY,
508  vol.Required(
509  "7", default=current_io.get("7", CONF_IO_DIS)
510  ): OPTIONS_IO_ANY,
511  }
512  ),
513  description_placeholders={
514  "model": KONN_PANEL_MODEL_NAMES[self.modelmodel],
515  "host": self.config_entryconfig_entryconfig_entry.data[CONF_HOST],
516  },
517  errors=errors,
518  )
519 
520  return self.async_abortasync_abort(reason="not_konn_panel")
521 
523  self, user_input: dict[str, Any] | None = None
524  ) -> ConfigFlowResult:
525  """Allow the user to configure the extended IO for pro."""
526  errors: dict[str, str] = {}
527  current_io = self.current_optcurrent_opt.get(CONF_IO, {})
528 
529  if user_input is not None:
530  # strip out disabled io and save for options cfg
531  for key, value in user_input.items():
532  if value != CONF_IO_DIS:
533  self.new_opt[CONF_IO].update({key: value})
534  self.io_cfgio_cfg = copy.deepcopy(self.new_opt[CONF_IO])
535  return await self.async_step_options_binaryasync_step_options_binary()
536 
537  if self.modelmodel == KONN_MODEL:
538  self.io_cfgio_cfg = copy.deepcopy(self.new_opt[CONF_IO])
539  return await self.async_step_options_binaryasync_step_options_binary()
540 
541  if self.modelmodel == KONN_MODEL_PRO:
542  return self.async_show_formasync_show_form(
543  step_id="options_io_ext",
544  data_schema=vol.Schema(
545  {
546  vol.Required(
547  "8", default=current_io.get("8", CONF_IO_DIS)
548  ): OPTIONS_IO_ANY,
549  vol.Required(
550  "9", default=current_io.get("9", CONF_IO_DIS)
551  ): OPTIONS_IO_INPUT_ONLY,
552  vol.Required(
553  "10", default=current_io.get("10", CONF_IO_DIS)
554  ): OPTIONS_IO_INPUT_ONLY,
555  vol.Required(
556  "11", default=current_io.get("11", CONF_IO_DIS)
557  ): OPTIONS_IO_INPUT_ONLY,
558  vol.Required(
559  "12", default=current_io.get("12", CONF_IO_DIS)
560  ): OPTIONS_IO_INPUT_ONLY,
561  vol.Required(
562  "alarm1", default=current_io.get("alarm1", CONF_IO_DIS)
563  ): OPTIONS_IO_OUTPUT_ONLY,
564  vol.Required(
565  "out1", default=current_io.get("out1", CONF_IO_DIS)
566  ): OPTIONS_IO_OUTPUT_ONLY,
567  vol.Required(
568  "alarm2_out2",
569  default=current_io.get("alarm2_out2", CONF_IO_DIS),
570  ): OPTIONS_IO_OUTPUT_ONLY,
571  }
572  ),
573  description_placeholders={
574  "model": KONN_PANEL_MODEL_NAMES[self.modelmodel],
575  "host": self.config_entryconfig_entryconfig_entry.data[CONF_HOST],
576  },
577  errors=errors,
578  )
579 
580  return self.async_abortasync_abort(reason="not_konn_panel")
581 
583  self, user_input: dict[str, Any] | None = None
584  ) -> ConfigFlowResult:
585  """Allow the user to configure the IO options for binary sensors."""
586  errors: dict[str, str] = {}
587  if user_input is not None and self.active_cfgactive_cfg is not None:
588  zone = {"zone": self.active_cfgactive_cfg}
589  zone.update(user_input)
590  self.new_opt[CONF_BINARY_SENSORS] = [
591  *self.new_opt.get(CONF_BINARY_SENSORS, []),
592  zone,
593  ]
594  self.io_cfgio_cfg.pop(self.active_cfgactive_cfg)
595  self.active_cfgactive_cfg = None
596 
597  if self.active_cfgactive_cfg:
598  current_cfg = self.get_current_cfgget_current_cfg(CONF_BINARY_SENSORS, self.active_cfgactive_cfg)
599  return self.async_show_formasync_show_form(
600  step_id="options_binary",
601  data_schema=vol.Schema(
602  {
603  vol.Required(
604  CONF_TYPE,
605  default=current_cfg.get(
606  CONF_TYPE, BinarySensorDeviceClass.DOOR
607  ),
608  ): DEVICE_CLASSES_SCHEMA,
609  vol.Optional(
610  CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
611  ): str,
612  vol.Optional(
613  CONF_INVERSE, default=current_cfg.get(CONF_INVERSE, False)
614  ): bool,
615  }
616  ),
617  description_placeholders={
618  "zone": f"Zone {self.active_cfg}"
619  if len(self.active_cfgactive_cfg) < 3
620  else self.active_cfgactive_cfg.upper()
621  },
622  errors=errors,
623  )
624 
625  # find the next unconfigured binary sensor
626  for key, value in self.io_cfgio_cfg.items():
627  if value == CONF_IO_BIN:
628  self.active_cfgactive_cfg = key
629  current_cfg = self.get_current_cfgget_current_cfg(CONF_BINARY_SENSORS, self.active_cfgactive_cfg)
630  return self.async_show_formasync_show_form(
631  step_id="options_binary",
632  data_schema=vol.Schema(
633  {
634  vol.Required(
635  CONF_TYPE,
636  default=current_cfg.get(
637  CONF_TYPE, BinarySensorDeviceClass.DOOR
638  ),
639  ): DEVICE_CLASSES_SCHEMA,
640  vol.Optional(
641  CONF_NAME,
642  default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
643  ): str,
644  vol.Optional(
645  CONF_INVERSE,
646  default=current_cfg.get(CONF_INVERSE, False),
647  ): bool,
648  }
649  ),
650  description_placeholders={
651  "zone": f"Zone {self.active_cfg}"
652  if len(self.active_cfgactive_cfg) < 3
653  else self.active_cfgactive_cfg.upper()
654  },
655  errors=errors,
656  )
657 
658  return await self.async_step_options_digitalasync_step_options_digital()
659 
661  self, user_input: dict[str, Any] | None = None
662  ) -> ConfigFlowResult:
663  """Allow the user to configure the IO options for digital sensors."""
664  errors: dict[str, str] = {}
665  if user_input is not None and self.active_cfgactive_cfg is not None:
666  zone = {"zone": self.active_cfgactive_cfg}
667  zone.update(user_input)
668  self.new_opt[CONF_SENSORS] = [*self.new_opt.get(CONF_SENSORS, []), zone]
669  self.io_cfgio_cfg.pop(self.active_cfgactive_cfg)
670  self.active_cfgactive_cfg = None
671 
672  if self.active_cfgactive_cfg:
673  current_cfg = self.get_current_cfgget_current_cfg(CONF_SENSORS, self.active_cfgactive_cfg)
674  return self.async_show_formasync_show_form(
675  step_id="options_digital",
676  data_schema=vol.Schema(
677  {
678  vol.Required(
679  CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht")
680  ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
681  vol.Optional(
682  CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
683  ): str,
684  vol.Optional(
685  CONF_POLL_INTERVAL,
686  default=current_cfg.get(CONF_POLL_INTERVAL, 3),
687  ): vol.All(vol.Coerce(int), vol.Range(min=1)),
688  }
689  ),
690  description_placeholders={
691  "zone": f"Zone {self.active_cfg}"
692  if len(self.active_cfgactive_cfg) < 3
693  else self.active_cfgactive_cfg.upper()
694  },
695  errors=errors,
696  )
697 
698  # find the next unconfigured digital sensor
699  for key, value in self.io_cfgio_cfg.items():
700  if value == CONF_IO_DIG:
701  self.active_cfgactive_cfg = key
702  current_cfg = self.get_current_cfgget_current_cfg(CONF_SENSORS, self.active_cfgactive_cfg)
703  return self.async_show_formasync_show_form(
704  step_id="options_digital",
705  data_schema=vol.Schema(
706  {
707  vol.Required(
708  CONF_TYPE, default=current_cfg.get(CONF_TYPE, "dht")
709  ): vol.All(vol.Lower, vol.In(["dht", "ds18b20"])),
710  vol.Optional(
711  CONF_NAME,
712  default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
713  ): str,
714  vol.Optional(
715  CONF_POLL_INTERVAL,
716  default=current_cfg.get(CONF_POLL_INTERVAL, 3),
717  ): vol.All(vol.Coerce(int), vol.Range(min=1)),
718  }
719  ),
720  description_placeholders={
721  "zone": f"Zone {self.active_cfg}"
722  if len(self.active_cfgactive_cfg) < 3
723  else self.active_cfgactive_cfg.upper()
724  },
725  errors=errors,
726  )
727 
728  return await self.async_step_options_switchasync_step_options_switch()
729 
731  self, user_input: dict[str, Any] | None = None
732  ) -> ConfigFlowResult:
733  """Allow the user to configure the IO options for switches."""
734  errors: dict[str, str] = {}
735  if user_input is not None and self.active_cfgactive_cfg is not None:
736  zone = {"zone": self.active_cfgactive_cfg}
737  zone.update(user_input)
738  del zone[CONF_MORE_STATES]
739  self.new_opt[CONF_SWITCHES] = [*self.new_opt.get(CONF_SWITCHES, []), zone]
740 
741  # iterate through multiple switch states
742  if self.current_statescurrent_states:
743  self.current_statescurrent_states.pop(0)
744 
745  # only go to next zone if all states are entered
746  self.current_statecurrent_state += 1
747  if user_input[CONF_MORE_STATES] == CONF_NO:
748  self.io_cfgio_cfg.pop(self.active_cfgactive_cfg)
749  self.active_cfgactive_cfg = None
750 
751  if self.active_cfgactive_cfg:
752  current_cfg = next(iter(self.current_statescurrent_states), {})
753  return self.async_show_formasync_show_form(
754  step_id="options_switch",
755  data_schema=vol.Schema(
756  {
757  vol.Optional(
758  CONF_NAME, default=current_cfg.get(CONF_NAME, vol.UNDEFINED)
759  ): str,
760  vol.Optional(
761  CONF_ACTIVATION,
762  default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH),
763  ): vol.All(vol.Lower, vol.In([STATE_HIGH, STATE_LOW])),
764  vol.Optional(
765  CONF_MOMENTARY,
766  default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED),
767  ): vol.All(vol.Coerce(int), vol.Range(min=10)),
768  vol.Optional(
769  CONF_PAUSE,
770  default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED),
771  ): vol.All(vol.Coerce(int), vol.Range(min=10)),
772  vol.Optional(
773  CONF_REPEAT,
774  default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
775  ): vol.All(vol.Coerce(int), vol.Range(min=-1)),
776  vol.Required(
777  CONF_MORE_STATES,
778  default=CONF_YES
779  if len(self.current_statescurrent_states) > 1
780  else CONF_NO,
781  ): vol.In([CONF_YES, CONF_NO]),
782  }
783  ),
784  description_placeholders={
785  "zone": f"Zone {self.active_cfg}"
786  if len(self.active_cfgactive_cfg) < 3
787  else self.active_cfgactive_cfg.upper(),
788  "state": str(self.current_statecurrent_state),
789  },
790  errors=errors,
791  )
792 
793  # find the next unconfigured switch
794  for key, value in self.io_cfgio_cfg.items():
795  if value == CONF_IO_SWI:
796  self.active_cfgactive_cfg = key
797  self.current_statescurrent_states = [
798  cfg
799  for cfg in self.current_optcurrent_opt.get(CONF_SWITCHES, [])
800  if cfg[CONF_ZONE] == self.active_cfgactive_cfg
801  ]
802  current_cfg = next(iter(self.current_statescurrent_states), {})
803  self.current_statecurrent_state = 1
804  return self.async_show_formasync_show_form(
805  step_id="options_switch",
806  data_schema=vol.Schema(
807  {
808  vol.Optional(
809  CONF_NAME,
810  default=current_cfg.get(CONF_NAME, vol.UNDEFINED),
811  ): str,
812  vol.Optional(
813  CONF_ACTIVATION,
814  default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH),
815  ): vol.In(["low", "high"]),
816  vol.Optional(
817  CONF_MOMENTARY,
818  default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED),
819  ): vol.All(vol.Coerce(int), vol.Range(min=10)),
820  vol.Optional(
821  CONF_PAUSE,
822  default=current_cfg.get(CONF_PAUSE, vol.UNDEFINED),
823  ): vol.All(vol.Coerce(int), vol.Range(min=10)),
824  vol.Optional(
825  CONF_REPEAT,
826  default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED),
827  ): vol.All(vol.Coerce(int), vol.Range(min=-1)),
828  vol.Required(
829  CONF_MORE_STATES,
830  default=CONF_YES
831  if len(self.current_statescurrent_states) > 1
832  else CONF_NO,
833  ): vol.In([CONF_YES, CONF_NO]),
834  }
835  ),
836  description_placeholders={
837  "zone": f"Zone {self.active_cfg}"
838  if len(self.active_cfgactive_cfg) < 3
839  else self.active_cfgactive_cfg.upper(),
840  "state": str(self.current_statecurrent_state),
841  },
842  errors=errors,
843  )
844 
845  return await self.async_step_options_miscasync_step_options_misc()
846 
848  self, user_input: dict[str, Any] | None = None
849  ) -> ConfigFlowResult:
850  """Allow the user to configure the LED behavior."""
851  errors = {}
852  if user_input is not None:
853  # config schema only does basic schema val so check url here
854  try:
855  if user_input[CONF_OVERRIDE_API_HOST]:
856  cv.url(user_input.get(CONF_API_HOST, ""))
857  else:
858  user_input[CONF_API_HOST] = ""
859  except vol.Invalid:
860  errors["base"] = "bad_host"
861  else:
862  # no need to store the override - can infer
863  del user_input[CONF_OVERRIDE_API_HOST]
864  self.new_opt.update(user_input)
865  return self.async_create_entryasync_create_entry(title="", data=self.new_opt)
866 
867  return self.async_show_formasync_show_form(
868  step_id="options_misc",
869  data_schema=vol.Schema(
870  {
871  vol.Required(
872  CONF_DISCOVERY,
873  default=self.current_optcurrent_opt.get(CONF_DISCOVERY, True),
874  ): bool,
875  vol.Required(
876  CONF_BLINK, default=self.current_optcurrent_opt.get(CONF_BLINK, True)
877  ): bool,
878  vol.Required(
879  CONF_OVERRIDE_API_HOST,
880  default=bool(self.current_optcurrent_opt.get(CONF_API_HOST)),
881  ): bool,
882  vol.Optional(
883  CONF_API_HOST, default=self.current_optcurrent_opt.get(CONF_API_HOST, "")
884  ): str,
885  }
886  ),
887  errors=errors,
888  )
ConfigFlowResult async_step_import_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:234
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:358
ConfigFlowResult async_step_import(self, dict[str, Any] import_data)
Definition: config_flow.py:207
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:312
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:395
ConfigFlowResult async_step_ssdp(self, ssdp.SsdpServiceInfo discovery_info)
Definition: config_flow.py:258
ConfigFlowResult async_step_options_binary(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:584
ConfigFlowResult async_step_options_io(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:437
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:431
ConfigFlowResult async_step_options_digital(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:662
ConfigFlowResult async_step_options_io_ext(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:524
ConfigFlowResult async_step_options_misc(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:849
ConfigFlowResult async_step_options_switch(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:732
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_step_user(self, dict[str, Any]|None user_input=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)
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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
def get_status(hass, host, port)
Definition: panel.py:387