Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for iskra integration."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from pyiskra.adapters import Modbus, RestAPI
9 from pyiskra.exceptions import (
10  DeviceConnectionError,
11  DeviceTimeoutError,
12  InvalidResponseCode,
13  NotAuthorised,
14 )
15 from pyiskra.helper import BasicInfo
16 import voluptuous as vol
17 
18 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
19 from homeassistant.const import (
20  CONF_ADDRESS,
21  CONF_HOST,
22  CONF_PASSWORD,
23  CONF_PORT,
24  CONF_PROTOCOL,
25  CONF_USERNAME,
26 )
27 from homeassistant.exceptions import HomeAssistantError
29  NumberSelector,
30  NumberSelectorConfig,
31  NumberSelectorMode,
32  SelectSelector,
33  SelectSelectorConfig,
34  SelectSelectorMode,
35 )
36 
37 from .const import DOMAIN
38 
39 _LOGGER = logging.getLogger(__name__)
40 
41 
42 STEP_USER_DATA_SCHEMA = vol.Schema(
43  {
44  vol.Required(CONF_HOST): str,
45  vol.Required(CONF_PROTOCOL, default="rest_api"): SelectSelector(
47  options=["rest_api", "modbus_tcp"],
48  mode=SelectSelectorMode.LIST,
49  translation_key="protocol",
50  ),
51  ),
52  }
53 )
54 
55 STEP_AUTHENTICATION_DATA_SCHEMA = vol.Schema(
56  {
57  vol.Required(CONF_USERNAME): str,
58  vol.Required(CONF_PASSWORD): str,
59  }
60 )
61 
62 # CONF_ADDRESS validation is done later in code, as if ranges are set in voluptuous it turns into a slider
63 STEP_MODBUS_TCP_DATA_SCHEMA = vol.Schema(
64  {
65  vol.Required(CONF_PORT, default=10001): vol.All(
66  vol.Coerce(int), vol.Range(min=0, max=65535)
67  ),
68  vol.Required(CONF_ADDRESS, default=33): NumberSelector(
69  NumberSelectorConfig(min=1, max=255, mode=NumberSelectorMode.BOX)
70  ),
71  }
72 )
73 
74 
75 async def test_rest_api_connection(host: str, user_input: dict[str, Any]) -> BasicInfo:
76  """Check if the RestAPI requires authentication."""
77 
78  rest_api = RestAPI(ip_address=host, authentication=user_input)
79  try:
80  basic_info = await rest_api.get_basic_info()
81  except NotAuthorised as e:
82  raise NotAuthorised from e
83  except (DeviceConnectionError, DeviceTimeoutError, InvalidResponseCode) as e:
84  raise CannotConnect from e
85  except Exception as e:
86  _LOGGER.error("Unexpected exception: %s", e)
87  raise UnknownException from e
88 
89  return basic_info
90 
91 
92 async def test_modbus_connection(host: str, user_input: dict[str, Any]) -> BasicInfo:
93  """Test the Modbus connection."""
94  modbus_api = Modbus(
95  ip_address=host,
96  protocol="tcp",
97  port=user_input[CONF_PORT],
98  modbus_address=user_input[CONF_ADDRESS],
99  )
100 
101  try:
102  basic_info = await modbus_api.get_basic_info()
103  except NotAuthorised as e:
104  raise NotAuthorised from e
105  except (DeviceConnectionError, DeviceTimeoutError, InvalidResponseCode) as e:
106  raise CannotConnect from e
107  except Exception as e:
108  _LOGGER.error("Unexpected exception: %s", e)
109  raise UnknownException from e
110 
111  return basic_info
112 
113 
114 class IskraConfigFlowFlow(ConfigFlow, domain=DOMAIN):
115  """Handle a config flow for iskra."""
116 
117  VERSION = 1
118  host: str
119  protocol: str
120 
121  async def async_step_user(
122  self, user_input: dict[str, Any] | None = None
123  ) -> ConfigFlowResult:
124  """Handle a flow initiated by the user."""
125  errors: dict[str, str] = {}
126  if user_input is not None:
127  self.hosthost = user_input[CONF_HOST]
128  self.protocolprotocol = user_input[CONF_PROTOCOL]
129  if self.protocolprotocol == "rest_api":
130  # Check if authentication is required.
131  try:
132  device_info = await test_rest_api_connection(self.hosthost, user_input)
133  except CannotConnect:
134  errors["base"] = "cannot_connect"
135  except NotAuthorised:
136  # Proceed to authentication step.
137  return await self.async_step_authenticationasync_step_authentication()
138  except UnknownException:
139  errors["base"] = "unknown"
140  # If the connection was not successful, show an error.
141 
142  # If the connection was successful, create the device.
143  if not errors:
144  return await self._create_entry_create_entry(
145  host=self.hosthost,
146  protocol=self.protocolprotocol,
147  device_info=device_info,
148  user_input=user_input,
149  )
150 
151  if self.protocolprotocol == "modbus_tcp":
152  # Proceed to modbus step.
153  return await self.async_step_modbus_tcpasync_step_modbus_tcp()
154 
155  return self.async_show_formasync_show_formasync_show_form(
156  step_id="user",
157  data_schema=STEP_USER_DATA_SCHEMA,
158  errors=errors,
159  )
160 
162  self, user_input: dict[str, Any] | None = None
163  ) -> ConfigFlowResult:
164  """Handle the authentication step."""
165  errors: dict[str, str] = {}
166  if user_input is not None:
167  try:
168  device_info = await test_rest_api_connection(self.hosthost, user_input)
169  # If the connection failed, abort.
170  except CannotConnect:
171  errors["base"] = "cannot_connect"
172  # If the authentication failed, show an error and authentication form again.
173  except NotAuthorised:
174  errors["base"] = "invalid_auth"
175  except UnknownException:
176  errors["base"] = "unknown"
177 
178  # if the connection was successful, create the device.
179  if not errors:
180  return await self._create_entry_create_entry(
181  self.hosthost,
182  self.protocolprotocol,
183  device_info=device_info,
184  user_input=user_input,
185  )
186 
187  # If there's no user_input or there was an error, show the authentication form again.
188  return self.async_show_formasync_show_formasync_show_form(
189  step_id="authentication",
190  data_schema=STEP_AUTHENTICATION_DATA_SCHEMA,
191  errors=errors,
192  )
193 
195  self, user_input: dict[str, Any] | None = None
196  ) -> ConfigFlowResult:
197  """Handle the Modbus TCP step."""
198  errors: dict[str, str] = {}
199 
200  # If there's user_input, check the connection.
201  if user_input is not None:
202  # convert to integer
203  user_input[CONF_ADDRESS] = int(user_input[CONF_ADDRESS])
204 
205  try:
206  device_info = await test_modbus_connection(self.hosthost, user_input)
207 
208  # If the connection failed, show an error.
209  except CannotConnect:
210  errors["base"] = "cannot_connect"
211  except UnknownException:
212  errors["base"] = "unknown"
213 
214  # If the connection was successful, create the device.
215  if not errors:
216  return await self._create_entry_create_entry(
217  host=self.hosthost,
218  protocol=self.protocolprotocol,
219  device_info=device_info,
220  user_input=user_input,
221  )
222 
223  # If there's no user_input or there was an error, show the modbus form again.
224  return self.async_show_formasync_show_formasync_show_form(
225  step_id="modbus_tcp",
226  data_schema=STEP_MODBUS_TCP_DATA_SCHEMA,
227  errors=errors,
228  )
229 
230  async def _create_entry(
231  self,
232  host: str,
233  protocol: str,
234  device_info: BasicInfo,
235  user_input: dict[str, Any],
236  ) -> ConfigFlowResult:
237  """Create the config entry."""
238 
239  await self.async_set_unique_idasync_set_unique_id(device_info.serial)
240  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
241 
242  return self.async_create_entryasync_create_entryasync_create_entry(
243  title=device_info.model,
244  data={CONF_HOST: host, CONF_PROTOCOL: protocol, **user_input},
245  )
246 
247 
249  """Error to indicate we cannot connect."""
250 
251 
253  """Error to indicate an unknown exception occurred."""
ConfigFlowResult async_step_authentication(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:163
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:123
ConfigFlowResult _create_entry(self, str host, str protocol, BasicInfo device_info, dict[str, Any] user_input)
Definition: config_flow.py:236
ConfigFlowResult async_step_modbus_tcp(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:196
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_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)
BasicInfo test_modbus_connection(str host, dict[str, Any] user_input)
Definition: config_flow.py:92
BasicInfo test_rest_api_connection(str host, dict[str, Any] user_input)
Definition: config_flow.py:75