Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow to configure the Tailwind integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from typing import Any
7 
8 from gotailwind import (
9  MIN_REQUIRED_FIRMWARE_VERSION,
10  Tailwind,
11  TailwindAuthenticationError,
12  TailwindConnectionError,
13  TailwindUnsupportedFirmwareVersionError,
14  tailwind_device_id_to_mac_address,
15 )
16 import voluptuous as vol
17 
18 from homeassistant.components import zeroconf
19 from homeassistant.components.dhcp import DhcpServiceInfo
20 from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
21 from homeassistant.const import CONF_HOST, CONF_TOKEN
22 from homeassistant.data_entry_flow import AbortFlow
23 from homeassistant.helpers.aiohttp_client import async_get_clientsession
24 from homeassistant.helpers.device_registry import format_mac
26  TextSelector,
27  TextSelectorConfig,
28  TextSelectorType,
29 )
30 
31 from .const import DOMAIN, LOGGER
32 
33 LOCAL_CONTROL_KEY_URL = (
34  "https://web.gotailwind.com/client/integration/local-control-key"
35 )
36 
37 
38 class TailwindFlowHandler(ConfigFlow, domain=DOMAIN):
39  """Handle a Tailwind config flow."""
40 
41  VERSION = 1
42 
43  host: str
44 
45  async def async_step_user(
46  self, user_input: dict[str, Any] | None = None
47  ) -> ConfigFlowResult:
48  """Handle a flow initiated by the user."""
49  errors = {}
50 
51  if user_input is not None:
52  try:
53  return await self._async_step_create_entry_async_step_create_entry(
54  host=user_input[CONF_HOST],
55  token=user_input[CONF_TOKEN],
56  )
57  except AbortFlow:
58  raise
59  except TailwindAuthenticationError:
60  errors[CONF_TOKEN] = "invalid_auth"
61  except TailwindConnectionError:
62  errors[CONF_HOST] = "cannot_connect"
63  except Exception: # noqa: BLE001
64  LOGGER.exception("Unexpected exception")
65  errors["base"] = "unknown"
66  else:
67  user_input = {}
68 
69  return self.async_show_formasync_show_formasync_show_form(
70  step_id="user",
71  data_schema=vol.Schema(
72  {
73  vol.Required(
74  CONF_HOST, default=user_input.get(CONF_HOST)
75  ): TextSelector(TextSelectorConfig(autocomplete="off")),
76  vol.Required(CONF_TOKEN): TextSelector(
77  TextSelectorConfig(type=TextSelectorType.PASSWORD)
78  ),
79  }
80  ),
81  description_placeholders={"url": LOCAL_CONTROL_KEY_URL},
82  errors=errors,
83  )
84 
86  self, discovery_info: zeroconf.ZeroconfServiceInfo
87  ) -> ConfigFlowResult:
88  """Handle zeroconf discovery of a Tailwind device."""
89  if not (device_id := discovery_info.properties.get("device_id")):
90  return self.async_abortasync_abortasync_abort(reason="no_device_id")
91 
92  if (
93  version := discovery_info.properties.get("SW ver")
94  ) and version < MIN_REQUIRED_FIRMWARE_VERSION:
95  return self.async_abortasync_abortasync_abort(reason="unsupported_firmware")
96 
97  await self.async_set_unique_idasync_set_unique_id(
98  format_mac(tailwind_device_id_to_mac_address(device_id))
99  )
100  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
101 
102  self.hosthost = discovery_info.host
103  self.context.update(
104  {
105  "title_placeholders": {
106  "name": f"Tailwind {discovery_info.properties.get('product')}"
107  },
108  "configuration_url": LOCAL_CONTROL_KEY_URL,
109  }
110  )
111  return await self.async_step_zeroconf_confirmasync_step_zeroconf_confirm()
112 
114  self, user_input: dict[str, Any] | None = None
115  ) -> ConfigFlowResult:
116  """Handle a flow initiated by zeroconf."""
117  errors = {}
118 
119  if user_input is not None:
120  try:
121  return await self._async_step_create_entry_async_step_create_entry(
122  host=self.hosthost,
123  token=user_input[CONF_TOKEN],
124  )
125  except TailwindAuthenticationError:
126  errors[CONF_TOKEN] = "invalid_auth"
127  except TailwindConnectionError:
128  errors["base"] = "cannot_connect"
129  except Exception: # noqa: BLE001
130  LOGGER.exception("Unexpected exception")
131  errors["base"] = "unknown"
132 
133  return self.async_show_formasync_show_formasync_show_form(
134  step_id="zeroconf_confirm",
135  data_schema=vol.Schema(
136  {
137  vol.Required(CONF_TOKEN): TextSelector(
138  TextSelectorConfig(type=TextSelectorType.PASSWORD)
139  ),
140  }
141  ),
142  description_placeholders={"url": LOCAL_CONTROL_KEY_URL},
143  errors=errors,
144  )
145 
146  async def async_step_reauth(
147  self, entry_data: Mapping[str, Any]
148  ) -> ConfigFlowResult:
149  """Handle initiation of re-authentication with a Tailwind device."""
150  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
151 
153  self, user_input: dict[str, Any] | None = None
154  ) -> ConfigFlowResult:
155  """Handle re-authentication with a Tailwind device."""
156  errors = {}
157 
158  if user_input is not None:
159  try:
160  return await self._async_step_create_entry_async_step_create_entry(
161  host=self._get_reauth_entry_get_reauth_entry().data[CONF_HOST],
162  token=user_input[CONF_TOKEN],
163  )
164  except TailwindAuthenticationError:
165  errors[CONF_TOKEN] = "invalid_auth"
166  except TailwindConnectionError:
167  errors["base"] = "cannot_connect"
168  except Exception: # noqa: BLE001
169  LOGGER.exception("Unexpected exception")
170  errors["base"] = "unknown"
171 
172  return self.async_show_formasync_show_formasync_show_form(
173  step_id="reauth_confirm",
174  data_schema=vol.Schema(
175  {
176  vol.Required(CONF_TOKEN): TextSelector(
177  TextSelectorConfig(type=TextSelectorType.PASSWORD)
178  ),
179  }
180  ),
181  description_placeholders={"url": LOCAL_CONTROL_KEY_URL},
182  errors=errors,
183  )
184 
185  async def async_step_dhcp(
186  self, discovery_info: DhcpServiceInfo
187  ) -> ConfigFlowResult:
188  """Handle dhcp discovery to update existing entries.
189 
190  This flow is triggered only by DHCP discovery of known devices.
191  """
192  await self.async_set_unique_idasync_set_unique_id(format_mac(discovery_info.macaddress))
193  self._abort_if_unique_id_configured_abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
194 
195  # This situation should never happen, as Home Assistant will only
196  # send updates for existing entries. In case it does, we'll just
197  # abort the flow with an unknown error.
198  return self.async_abortasync_abortasync_abort(reason="unknown")
199 
201  self, *, host: str, token: str
202  ) -> ConfigFlowResult:
203  """Create entry."""
204  tailwind = Tailwind(
205  host=host, token=token, session=async_get_clientsession(self.hass)
206  )
207 
208  try:
209  status = await tailwind.status()
210  except TailwindUnsupportedFirmwareVersionError:
211  return self.async_abortasync_abortasync_abort(reason="unsupported_firmware")
212 
213  if self.sourcesourcesourcesource == SOURCE_REAUTH:
214  return self.async_update_reload_and_abortasync_update_reload_and_abort(
215  self._get_reauth_entry_get_reauth_entry(),
216  data={
217  CONF_HOST: host,
218  CONF_TOKEN: token,
219  },
220  )
221 
222  await self.async_set_unique_idasync_set_unique_id(
223  format_mac(status.mac_address), raise_on_progress=False
224  )
225  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
226  updates={
227  CONF_HOST: host,
228  CONF_TOKEN: token,
229  }
230  )
231 
232  return self.async_create_entryasync_create_entryasync_create_entry(
233  title=f"Tailwind {status.product}",
234  data={CONF_HOST: host, CONF_TOKEN: token},
235  )
ConfigFlowResult _async_step_create_entry(self, *str host, str token)
Definition: config_flow.py:202
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
Definition: config_flow.py:87
ConfigFlowResult async_step_zeroconf_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:115
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:47
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:154
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:148
ConfigFlowResult async_step_dhcp(self, DhcpServiceInfo discovery_info)
Definition: config_flow.py:187
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)
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)
str|None source(self)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
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)