Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for sia integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from copy import deepcopy
7 import logging
8 from typing import Any
9 
10 from pysiaalarm import (
11  InvalidAccountFormatError,
12  InvalidAccountLengthError,
13  InvalidKeyFormatError,
14  InvalidKeyLengthError,
15  SIAAccount,
16 )
17 import voluptuous as vol
18 
19 from homeassistant.config_entries import (
20  ConfigEntry,
21  ConfigFlow,
22  ConfigFlowResult,
23  OptionsFlow,
24 )
25 from homeassistant.const import CONF_PORT, CONF_PROTOCOL
26 from homeassistant.core import callback
27 
28 from .const import (
29  CONF_ACCOUNT,
30  CONF_ACCOUNTS,
31  CONF_ADDITIONAL_ACCOUNTS,
32  CONF_ENCRYPTION_KEY,
33  CONF_IGNORE_TIMESTAMPS,
34  CONF_PING_INTERVAL,
35  CONF_ZONES,
36  DOMAIN,
37  TITLE,
38 )
39 from .hub import SIAHub
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 HUB_SCHEMA = vol.Schema(
44  {
45  vol.Required(CONF_PORT): int,
46  vol.Optional(CONF_PROTOCOL, default="TCP"): vol.In(["TCP", "UDP"]),
47  vol.Required(CONF_ACCOUNT): str,
48  vol.Optional(CONF_ENCRYPTION_KEY): str,
49  vol.Required(CONF_PING_INTERVAL, default=1): int,
50  vol.Required(CONF_ZONES, default=1): int,
51  vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool,
52  }
53 )
54 
55 ACCOUNT_SCHEMA = vol.Schema(
56  {
57  vol.Required(CONF_ACCOUNT): str,
58  vol.Optional(CONF_ENCRYPTION_KEY): str,
59  vol.Required(CONF_PING_INTERVAL, default=1): int,
60  vol.Required(CONF_ZONES, default=1): int,
61  vol.Optional(CONF_ADDITIONAL_ACCOUNTS, default=False): bool,
62  }
63 )
64 
65 DEFAULT_OPTIONS = {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: None}
66 
67 
68 def validate_input(data: dict[str, Any]) -> dict[str, str] | None:
69  """Validate the input by the user."""
70  try:
71  SIAAccount.validate_account(data[CONF_ACCOUNT], data.get(CONF_ENCRYPTION_KEY))
72  except InvalidKeyFormatError:
73  return {"base": "invalid_key_format"}
74  except InvalidKeyLengthError:
75  return {"base": "invalid_key_length"}
76  except InvalidAccountFormatError:
77  return {"base": "invalid_account_format"}
78  except InvalidAccountLengthError:
79  return {"base": "invalid_account_length"}
80  except Exception:
81  _LOGGER.exception("Unexpected exception from SIAAccount")
82  return {"base": "unknown"}
83  if not 1 <= data[CONF_PING_INTERVAL] <= 1440:
84  return {"base": "invalid_ping"}
85  return validate_zones(data)
86 
87 
88 def validate_zones(data: dict[str, Any]) -> dict[str, str] | None:
89  """Validate the zones field."""
90  if data[CONF_ZONES] == 0:
91  return {"base": "invalid_zones"}
92  return None
93 
94 
95 class SIAConfigFlow(ConfigFlow, domain=DOMAIN):
96  """Handle a config flow for sia."""
97 
98  VERSION: int = 1
99 
100  @staticmethod
101  @callback
103  config_entry: ConfigEntry,
104  ) -> SIAOptionsFlowHandler:
105  """Get the options flow for this handler."""
106  return SIAOptionsFlowHandler(config_entry)
107 
108  def __init__(self) -> None:
109  """Initialize the config flow."""
110  self._data_data: dict[str, Any] = {}
111  self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}}
112 
113  async def async_step_user(
114  self, user_input: dict[str, Any] | None = None
115  ) -> ConfigFlowResult:
116  """Handle the initial user step."""
117  errors: dict[str, str] | None = None
118  if user_input is not None:
119  errors = validate_input(user_input)
120  if user_input is None or errors is not None:
121  return self.async_show_formasync_show_formasync_show_form(
122  step_id="user", data_schema=HUB_SCHEMA, errors=errors
123  )
124  return await self.async_handle_data_and_routeasync_handle_data_and_route(user_input)
125 
127  self, user_input: dict[str, Any] | None = None
128  ) -> ConfigFlowResult:
129  """Handle the additional accounts steps."""
130  errors: dict[str, str] | None = None
131  if user_input is not None:
132  errors = validate_input(user_input)
133  if user_input is None or errors is not None:
134  return self.async_show_formasync_show_formasync_show_form(
135  step_id="add_account", data_schema=ACCOUNT_SCHEMA, errors=errors
136  )
137  return await self.async_handle_data_and_routeasync_handle_data_and_route(user_input)
138 
140  self, user_input: dict[str, Any]
141  ) -> ConfigFlowResult:
142  """Handle the user_input, check if configured and route to the right next step or create entry."""
143  self._update_data_update_data(user_input)
144 
145  self._async_abort_entries_match_async_abort_entries_match({CONF_PORT: self._data_data[CONF_PORT]})
146 
147  if user_input[CONF_ADDITIONAL_ACCOUNTS]:
148  return await self.async_step_add_accountasync_step_add_account()
149  return self.async_create_entryasync_create_entryasync_create_entry(
150  title=TITLE.format(self._data_data[CONF_PORT]),
151  data=self._data_data,
152  options=self._options,
153  )
154 
155  def _update_data(self, user_input: dict[str, Any]) -> None:
156  """Parse the user_input and store in data and options attributes.
157 
158  If there is a port in the input or no data, assume it is fully new and overwrite.
159  Add the default options and overwrite the zones in options.
160  """
161  if not self._data_data or user_input.get(CONF_PORT):
162  self._data_data = {
163  CONF_PORT: user_input[CONF_PORT],
164  CONF_PROTOCOL: user_input[CONF_PROTOCOL],
165  CONF_ACCOUNTS: [],
166  }
167  account = user_input[CONF_ACCOUNT]
168  self._data_data[CONF_ACCOUNTS].append(
169  {
170  CONF_ACCOUNT: account,
171  CONF_ENCRYPTION_KEY: user_input.get(CONF_ENCRYPTION_KEY),
172  CONF_PING_INTERVAL: user_input[CONF_PING_INTERVAL],
173  }
174  )
175  self._options[CONF_ACCOUNTS].setdefault(account, deepcopy(DEFAULT_OPTIONS))
176  self._options[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES]
177 
178 
180  """Handle SIA options."""
181 
182  def __init__(self, config_entry: ConfigEntry) -> None:
183  """Initialize SIA options flow."""
184  self.optionsoptions = deepcopy(dict(config_entry.options))
185  self.hubhub: SIAHub | None = None
186  self.accounts_todoaccounts_todo: list = []
187 
188  async def async_step_init(
189  self, user_input: dict[str, Any] | None = None
190  ) -> ConfigFlowResult:
191  """Manage the SIA options."""
192  self.hubhub = self.hass.data[DOMAIN][self.config_entryconfig_entryconfig_entry.entry_id]
193  assert self.hubhub is not None
194  assert self.hubhub.sia_accounts is not None
195  self.accounts_todoaccounts_todo = [a.account_id for a in self.hubhub.sia_accounts]
196  return await self.async_step_optionsasync_step_options()
197 
199  self, user_input: dict[str, Any] | None = None
200  ) -> ConfigFlowResult:
201  """Create the options step for a account."""
202  errors: dict[str, str] | None = None
203  if user_input is not None:
204  errors = validate_zones(user_input)
205  if user_input is None or errors is not None:
206  account = self.accounts_todoaccounts_todo[0]
207  return self.async_show_formasync_show_form(
208  step_id="options",
209  description_placeholders={"account": account},
210  data_schema=vol.Schema(
211  {
212  vol.Optional(
213  CONF_ZONES,
214  default=self.optionsoptions[CONF_ACCOUNTS][account][CONF_ZONES],
215  ): int,
216  vol.Optional(
217  CONF_IGNORE_TIMESTAMPS,
218  default=self.optionsoptions[CONF_ACCOUNTS][account][
219  CONF_IGNORE_TIMESTAMPS
220  ],
221  ): bool,
222  }
223  ),
224  errors=errors,
225  last_step=self.last_steplast_step,
226  )
227 
228  account = self.accounts_todoaccounts_todo.pop(0)
229  self.optionsoptions[CONF_ACCOUNTS][account][CONF_IGNORE_TIMESTAMPS] = user_input[
230  CONF_IGNORE_TIMESTAMPS
231  ]
232  self.optionsoptions[CONF_ACCOUNTS][account][CONF_ZONES] = user_input[CONF_ZONES]
233  if self.accounts_todoaccounts_todo:
234  return await self.async_step_optionsasync_step_options()
235  return self.async_create_entryasync_create_entry(title="", data=self.optionsoptions)
236 
237  @property
238  def last_step(self) -> bool:
239  """Return if this is the last step."""
240  return len(self.accounts_todoaccounts_todo) <= 1
ConfigFlowResult async_handle_data_and_route(self, dict[str, Any] user_input)
Definition: config_flow.py:141
ConfigFlowResult async_step_add_account(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:128
SIAOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:104
None _update_data(self, dict[str, Any] user_input)
Definition: config_flow.py:155
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:115
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:190
ConfigFlowResult async_step_options(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:200
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)
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)
_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)
dict[str, str]|None validate_input(dict[str, Any] data)
Definition: config_flow.py:68
dict[str, str]|None validate_zones(dict[str, Any] data)
Definition: config_flow.py:88