Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for HomeWizard."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any, NamedTuple
8 
9 from homewizard_energy import HomeWizardEnergyV1
10 from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError
11 from homewizard_energy.v1.models import Device
12 import voluptuous as vol
13 
14 from homeassistant.components import onboarding, zeroconf
15 from homeassistant.components.dhcp import DhcpServiceInfo
16 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
17 from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH
18 from homeassistant.data_entry_flow import AbortFlow
19 from homeassistant.exceptions import HomeAssistantError
20 from homeassistant.helpers.selector import TextSelector
21 
22 from .const import (
23  CONF_API_ENABLED,
24  CONF_PRODUCT_NAME,
25  CONF_PRODUCT_TYPE,
26  CONF_SERIAL,
27  DOMAIN,
28 )
29 
30 _LOGGER = logging.getLogger(__name__)
31 
32 
33 class DiscoveryData(NamedTuple):
34  """User metadata."""
35 
36  ip: str
37  product_name: str
38  product_type: str
39  serial: str
40 
41 
42 class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
43  """Handle a config flow for P1 meter."""
44 
45  VERSION = 1
46 
47  discovery: DiscoveryData
48 
49  async def async_step_user(
50  self, user_input: dict[str, Any] | None = None
51  ) -> ConfigFlowResult:
52  """Handle a flow initiated by the user."""
53  errors: dict[str, str] | None = None
54  if user_input is not None:
55  try:
56  device_info = await self._async_try_connect_async_try_connect(user_input[CONF_IP_ADDRESS])
57  except RecoverableError as ex:
58  _LOGGER.error(ex)
59  errors = {"base": ex.error_code}
60  else:
61  await self.async_set_unique_idasync_set_unique_id(
62  f"{device_info.product_type}_{device_info.serial}"
63  )
64  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates=user_input)
65  return self.async_create_entryasync_create_entryasync_create_entry(
66  title=f"{device_info.product_name}",
67  data=user_input,
68  )
69 
70  user_input = user_input or {}
71  return self.async_show_formasync_show_formasync_show_form(
72  step_id="user",
73  data_schema=vol.Schema(
74  {
75  vol.Required(
76  CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS)
77  ): TextSelector(),
78  }
79  ),
80  errors=errors,
81  )
82 
84  self, discovery_info: zeroconf.ZeroconfServiceInfo
85  ) -> ConfigFlowResult:
86  """Handle zeroconf discovery."""
87  if (
88  CONF_API_ENABLED not in discovery_info.properties
89  or CONF_PATH not in discovery_info.properties
90  or CONF_PRODUCT_NAME not in discovery_info.properties
91  or CONF_PRODUCT_TYPE not in discovery_info.properties
92  or CONF_SERIAL not in discovery_info.properties
93  ):
94  return self.async_abortasync_abortasync_abort(reason="invalid_discovery_parameters")
95 
96  if (discovery_info.properties[CONF_PATH]) != "/api/v1":
97  return self.async_abortasync_abortasync_abort(reason="unsupported_api_version")
98 
99  self.discoverydiscovery = DiscoveryData(
100  ip=discovery_info.host,
101  product_type=discovery_info.properties[CONF_PRODUCT_TYPE],
102  product_name=discovery_info.properties[CONF_PRODUCT_NAME],
103  serial=discovery_info.properties[CONF_SERIAL],
104  )
105 
106  await self.async_set_unique_idasync_set_unique_id(
107  f"{self.discovery.product_type}_{self.discovery.serial}"
108  )
109  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
110  updates={CONF_IP_ADDRESS: discovery_info.host}
111  )
112 
113  return await self.async_step_discovery_confirmasync_step_discovery_confirm()
114 
115  async def async_step_dhcp(
116  self, discovery_info: DhcpServiceInfo
117  ) -> ConfigFlowResult:
118  """Handle dhcp discovery to update existing entries.
119 
120  This flow is triggered only by DHCP discovery of known devices.
121  """
122  try:
123  device = await self._async_try_connect_async_try_connect(discovery_info.ip)
124  except RecoverableError as ex:
125  _LOGGER.error(ex)
126  return self.async_abortasync_abortasync_abort(reason="unknown")
127 
128  await self.async_set_unique_idasync_set_unique_id(
129  f"{device.product_type}_{discovery_info.macaddress}"
130  )
131 
132  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
133  updates={CONF_IP_ADDRESS: discovery_info.ip}
134  )
135 
136  # This situation should never happen, as Home Assistant will only
137  # send updates for existing entries. In case it does, we'll just
138  # abort the flow with an unknown error.
139  return self.async_abortasync_abortasync_abort(reason="unknown")
140 
142  self, user_input: dict[str, Any] | None = None
143  ) -> ConfigFlowResult:
144  """Confirm discovery."""
145  errors: dict[str, str] | None = None
146  if user_input is not None or not onboarding.async_is_onboarded(self.hass):
147  try:
148  await self._async_try_connect_async_try_connect(self.discoverydiscovery.ip)
149  except RecoverableError as ex:
150  _LOGGER.error(ex)
151  errors = {"base": ex.error_code}
152  else:
153  return self.async_create_entryasync_create_entryasync_create_entry(
154  title=self.discoverydiscovery.product_name,
155  data={CONF_IP_ADDRESS: self.discoverydiscovery.ip},
156  )
157 
158  self._set_confirm_only_set_confirm_only()
159 
160  # We won't be adding mac/serial to the title for devices
161  # that users generally don't have multiple of.
162  name = self.discoverydiscovery.product_name
163  if self.discoverydiscovery.product_type not in ["HWE-P1", "HWE-WTR"]:
164  name = f"{name} ({self.discovery.serial})"
165  self.context["title_placeholders"] = {"name": name}
166 
167  return self.async_show_formasync_show_formasync_show_form(
168  step_id="discovery_confirm",
169  description_placeholders={
170  CONF_PRODUCT_TYPE: self.discoverydiscovery.product_type,
171  CONF_SERIAL: self.discoverydiscovery.serial,
172  CONF_IP_ADDRESS: self.discoverydiscovery.ip,
173  },
174  errors=errors,
175  )
176 
177  async def async_step_reauth(
178  self, entry_data: Mapping[str, Any]
179  ) -> ConfigFlowResult:
180  """Handle re-auth if API was disabled."""
181  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
182 
184  self, user_input: dict[str, Any] | None = None
185  ) -> ConfigFlowResult:
186  """Confirm reauth dialog."""
187  errors: dict[str, str] | None = None
188  if user_input is not None:
189  reauth_entry = self._get_reauth_entry_get_reauth_entry()
190  try:
191  await self._async_try_connect_async_try_connect(reauth_entry.data[CONF_IP_ADDRESS])
192  except RecoverableError as ex:
193  _LOGGER.error(ex)
194  errors = {"base": ex.error_code}
195  else:
196  await self.hass.config_entries.async_reload(reauth_entry.entry_id)
197  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
198 
199  return self.async_show_formasync_show_formasync_show_form(step_id="reauth_confirm", errors=errors)
200 
202  self, user_input: dict[str, Any] | None = None
203  ) -> ConfigFlowResult:
204  """Handle reconfiguration of the integration."""
205  errors: dict[str, str] = {}
206  if user_input:
207  try:
208  device_info = await self._async_try_connect_async_try_connect(user_input[CONF_IP_ADDRESS])
209  except RecoverableError as ex:
210  _LOGGER.error(ex)
211  errors = {"base": ex.error_code}
212  else:
213  await self.async_set_unique_idasync_set_unique_id(
214  f"{device_info.product_type}_{device_info.serial}"
215  )
216  self._abort_if_unique_id_mismatch_abort_if_unique_id_mismatch(reason="wrong_device")
217  return self.async_update_reload_and_abortasync_update_reload_and_abort(
218  self._get_reconfigure_entry_get_reconfigure_entry(),
219  data_updates=user_input,
220  )
221  reconfigure_entry = self._get_reconfigure_entry_get_reconfigure_entry()
222  return self.async_show_formasync_show_formasync_show_form(
223  step_id="reconfigure",
224  data_schema=vol.Schema(
225  {
226  vol.Required(
227  CONF_IP_ADDRESS,
228  default=reconfigure_entry.data.get(CONF_IP_ADDRESS),
229  ): TextSelector(),
230  }
231  ),
232  description_placeholders={
233  "title": reconfigure_entry.title,
234  },
235  errors=errors,
236  )
237 
238  @staticmethod
239  async def _async_try_connect(ip_address: str) -> Device:
240  """Try to connect.
241 
242  Make connection with device to test the connection
243  and to get info for unique_id.
244  """
245  energy_api = HomeWizardEnergyV1(ip_address)
246  try:
247  return await energy_api.device()
248 
249  except DisabledError as ex:
250  raise RecoverableError(
251  "API disabled, API must be enabled in the app", "api_not_enabled"
252  ) from ex
253 
254  except UnsupportedError as ex:
255  _LOGGER.error("API version unsuppored")
256  raise AbortFlow("unsupported_api_version") from ex
257 
258  except RequestError as ex:
259  raise RecoverableError(
260  "Device unreachable or unexpected response", "network_error"
261  ) from ex
262 
263  except Exception as ex:
264  _LOGGER.exception("Unexpected exception")
265  raise AbortFlow("unknown_error") from ex
266 
267  finally:
268  await energy_api.close()
269 
270 
272  """Raised when a connection has been failed but can be retried."""
273 
274  def __init__(self, message: str, error_code: str) -> None:
275  """Init RecoverableError."""
276  super().__init__(message)
277  self.error_codeerror_code = error_code
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:185
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:85
ConfigFlowResult async_step_reconfigure(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:203
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
Definition: config_flow.py:117
ConfigFlowResult async_step_discovery_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:143
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:179
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:51
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_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
None _abort_if_unique_id_mismatch(self, *str reason="unique_id_mismatch", 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)
_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)