Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Risco integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 
9 from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError
10 import voluptuous as vol
11 
12 from homeassistant.components.alarm_control_panel import AlarmControlPanelState
13 from homeassistant.config_entries import (
14  ConfigEntry,
15  ConfigFlow,
16  ConfigFlowResult,
17  OptionsFlow,
18 )
19 from homeassistant.const import (
20  CONF_HOST,
21  CONF_PASSWORD,
22  CONF_PIN,
23  CONF_PORT,
24  CONF_SCAN_INTERVAL,
25  CONF_TYPE,
26  CONF_USERNAME,
27 )
28 from homeassistant.core import HomeAssistant, callback
29 from homeassistant.helpers.aiohttp_client import async_get_clientsession
30 
31 from .const import (
32  CONF_CODE_ARM_REQUIRED,
33  CONF_CODE_DISARM_REQUIRED,
34  CONF_COMMUNICATION_DELAY,
35  CONF_CONCURRENCY,
36  CONF_HA_STATES_TO_RISCO,
37  CONF_RISCO_STATES_TO_HA,
38  DEFAULT_ADVANCED_OPTIONS,
39  DEFAULT_OPTIONS,
40  DOMAIN,
41  MAX_COMMUNICATION_DELAY,
42  RISCO_STATES,
43  TYPE_LOCAL,
44 )
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 
49 CLOUD_SCHEMA = vol.Schema(
50  {
51  vol.Required(CONF_USERNAME): str,
52  vol.Required(CONF_PASSWORD): str,
53  vol.Required(CONF_PIN): str,
54  }
55 )
56 LOCAL_SCHEMA = vol.Schema(
57  {
58  vol.Required(CONF_HOST): str,
59  vol.Required(CONF_PORT, default=1000): int,
60  vol.Required(CONF_PIN): str,
61  }
62 )
63 HA_STATES = [
64  AlarmControlPanelState.ARMED_AWAY.value,
65  AlarmControlPanelState.ARMED_HOME.value,
66  AlarmControlPanelState.ARMED_NIGHT.value,
67  AlarmControlPanelState.ARMED_CUSTOM_BYPASS.value,
68 ]
69 
70 
72  hass: HomeAssistant, data: dict[str, Any]
73 ) -> dict[str, str]:
74  """Validate the user input allows us to connect to Risco Cloud.
75 
76  Data has the keys from CLOUD_SCHEMA with values provided by the user.
77  """
78  risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN])
79 
80  try:
81  await risco.login(async_get_clientsession(hass))
82  finally:
83  await risco.close()
84 
85  return {"title": risco.site_name}
86 
87 
89  hass: HomeAssistant, data: Mapping[str, str]
90 ) -> dict[str, Any]:
91  """Validate the user input allows us to connect to a local panel.
92 
93  Data has the keys from LOCAL_SCHEMA with values provided by the user.
94  """
95  comm_delay = 0
96  while True:
97  risco = RiscoLocal(
98  data[CONF_HOST],
99  data[CONF_PORT],
100  data[CONF_PIN],
101  communication_delay=comm_delay,
102  )
103  try:
104  await risco.connect()
105  except CannotConnectError:
106  if comm_delay >= MAX_COMMUNICATION_DELAY:
107  raise
108  comm_delay += 1
109  else:
110  break
111 
112  site_id = risco.id
113  await risco.disconnect()
114  return {"title": site_id, "comm_delay": comm_delay}
115 
116 
117 class RiscoConfigFlow(ConfigFlow, domain=DOMAIN):
118  """Handle a config flow for Risco."""
119 
120  VERSION = 1
121 
122  def __init__(self) -> None:
123  """Init the config flow."""
124  self._reauth_entry_reauth_entry: ConfigEntry | None = None
125 
126  @staticmethod
127  @callback
129  config_entry: ConfigEntry,
130  ) -> RiscoOptionsFlowHandler:
131  """Define the config flow to handle options."""
132  return RiscoOptionsFlowHandler(config_entry)
133 
134  async def async_step_user(
135  self, user_input: dict[str, Any] | None = None
136  ) -> ConfigFlowResult:
137  """Handle the initial step."""
138  return self.async_show_menuasync_show_menu(
139  step_id="user",
140  menu_options=["cloud", "local"],
141  )
142 
143  async def async_step_cloud(
144  self, user_input: dict[str, Any] | None = None
145  ) -> ConfigFlowResult:
146  """Configure a cloud based alarm."""
147  errors: dict[str, str] = {}
148  if user_input is not None:
149  if not self._reauth_entry_reauth_entry:
150  await self.async_set_unique_idasync_set_unique_id(user_input[CONF_USERNAME])
151  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
152 
153  try:
154  info = await validate_cloud_input(self.hass, user_input)
155  except CannotConnectError:
156  errors["base"] = "cannot_connect"
157  except UnauthorizedError:
158  errors["base"] = "invalid_auth"
159  except Exception:
160  _LOGGER.exception("Unexpected exception")
161  errors["base"] = "unknown"
162  else:
163  if not self._reauth_entry_reauth_entry:
164  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=user_input)
165  self.hass.config_entries.async_update_entry(
166  self._reauth_entry_reauth_entry,
167  data=user_input,
168  unique_id=user_input[CONF_USERNAME],
169  )
170  await self.hass.config_entries.async_reload(self._reauth_entry_reauth_entry.entry_id)
171  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
172 
173  return self.async_show_formasync_show_formasync_show_form(
174  step_id="cloud", data_schema=CLOUD_SCHEMA, errors=errors
175  )
176 
177  async def async_step_reauth(
178  self, entry_data: Mapping[str, Any]
179  ) -> ConfigFlowResult:
180  """Handle configuration by re-auth."""
181  self._reauth_entry_reauth_entry = await self.async_set_unique_idasync_set_unique_id(entry_data[CONF_USERNAME])
182  return await self.async_step_cloudasync_step_cloud()
183 
184  async def async_step_local(
185  self, user_input: dict[str, Any] | None = None
186  ) -> ConfigFlowResult:
187  """Configure a local based alarm."""
188  errors: dict[str, str] = {}
189  if user_input is not None:
190  try:
191  info = await validate_local_input(self.hass, user_input)
192  except CannotConnectError as ex:
193  _LOGGER.debug("Cannot connect", exc_info=ex)
194  errors["base"] = "cannot_connect"
195  except UnauthorizedError:
196  errors["base"] = "invalid_auth"
197  except Exception:
198  _LOGGER.exception("Unexpected exception")
199  errors["base"] = "unknown"
200  else:
201  await self.async_set_unique_idasync_set_unique_id(info["title"])
202  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
203 
204  return self.async_create_entryasync_create_entryasync_create_entry(
205  title=info["title"],
206  data={
207  **user_input,
208  CONF_TYPE: TYPE_LOCAL,
209  CONF_COMMUNICATION_DELAY: info["comm_delay"],
210  },
211  )
212 
213  return self.async_show_formasync_show_formasync_show_form(
214  step_id="local", data_schema=LOCAL_SCHEMA, errors=errors
215  )
216 
217 
219  """Handle a Risco options flow."""
220 
221  def __init__(self, config_entry: ConfigEntry) -> None:
222  """Initialize."""
223  self._data_data = {**DEFAULT_OPTIONS, **config_entry.options}
224 
225  def _options_schema(self) -> vol.Schema:
226  schema = vol.Schema(
227  {
228  vol.Required(
229  CONF_CODE_ARM_REQUIRED, default=self._data_data[CONF_CODE_ARM_REQUIRED]
230  ): bool,
231  vol.Required(
232  CONF_CODE_DISARM_REQUIRED,
233  default=self._data_data[CONF_CODE_DISARM_REQUIRED],
234  ): bool,
235  }
236  )
237  if self.show_advanced_optionsshow_advanced_options:
238  self._data_data = {**DEFAULT_ADVANCED_OPTIONS, **self._data_data}
239  schema = schema.extend(
240  {
241  vol.Required(
242  CONF_SCAN_INTERVAL, default=self._data_data[CONF_SCAN_INTERVAL]
243  ): int,
244  vol.Required(
245  CONF_CONCURRENCY, default=self._data_data[CONF_CONCURRENCY]
246  ): int,
247  }
248  )
249  return schema
250 
251  async def async_step_init(
252  self, user_input: dict[str, Any] | None = None
253  ) -> ConfigFlowResult:
254  """Manage the options."""
255  if user_input is not None:
256  self._data_data = {**self._data_data, **user_input}
257  return await self.async_step_risco_to_haasync_step_risco_to_ha()
258 
259  return self.async_show_formasync_show_form(step_id="init", data_schema=self._options_schema_options_schema())
260 
262  self, user_input: dict[str, Any] | None = None
263  ) -> ConfigFlowResult:
264  """Map Risco states to HA states."""
265  if user_input is not None:
266  self._data_data[CONF_RISCO_STATES_TO_HA] = user_input
267  return await self.async_step_ha_to_riscoasync_step_ha_to_risco()
268 
269  risco_to_ha = self._data_data[CONF_RISCO_STATES_TO_HA]
270  options = vol.Schema(
271  {
272  vol.Required(risco_state, default=risco_to_ha[risco_state]): vol.In(
273  HA_STATES
274  )
275  for risco_state in RISCO_STATES
276  }
277  )
278 
279  return self.async_show_formasync_show_form(step_id="risco_to_ha", data_schema=options)
280 
282  self, user_input: dict[str, Any] | None = None
283  ) -> ConfigFlowResult:
284  """Map HA states to Risco states."""
285  if user_input is not None:
286  self._data_data[CONF_HA_STATES_TO_RISCO] = user_input
287  return self.async_create_entryasync_create_entry(title="", data=self._data_data)
288 
289  options = {}
290  risco_to_ha = self._data_data[CONF_RISCO_STATES_TO_HA]
291  # we iterate over HA_STATES, instead of set(self._risco_to_ha.values())
292  # to ensure a consistent order
293  for ha_state in HA_STATES:
294  if ha_state not in risco_to_ha.values():
295  continue
296 
297  values = [
298  risco_state
299  for risco_state in RISCO_STATES
300  if risco_to_ha[risco_state] == ha_state
301  ]
302  current = self._data_data[CONF_HA_STATES_TO_RISCO].get(ha_state)
303  if current not in values:
304  current = values[0]
305  options[vol.Required(ha_state, default=current)] = vol.In(values)
306 
307  return self.async_show_formasync_show_form(
308  step_id="ha_to_risco", data_schema=vol.Schema(options)
309  )
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:136
RiscoOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:130
ConfigFlowResult async_step_local(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:186
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:179
ConfigFlowResult async_step_cloud(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:145
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:253
ConfigFlowResult async_step_risco_to_ha(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:263
ConfigFlowResult async_step_ha_to_risco(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:283
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_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)
bool show_advanced_options(self)
_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_show_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=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
dict[str, Any] validate_local_input(HomeAssistant hass, Mapping[str, str] data)
Definition: config_flow.py:90
dict[str, str] validate_cloud_input(HomeAssistant hass, dict[str, Any] data)
Definition: config_flow.py:73
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)