Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Tesla Powerwall integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 import logging
8 from typing import Any
9 
10 from aiohttp import CookieJar
11 from tesla_powerwall import (
12  AccessDeniedError,
13  MissingAttributeError,
14  Powerwall,
15  PowerwallUnreachableError,
16  SiteInfoResponse,
17 )
18 import voluptuous as vol
19 
20 from homeassistant.components import dhcp
21 from homeassistant.config_entries import (
22  ConfigEntry,
23  ConfigEntryState,
24  ConfigFlow,
25  ConfigFlowResult,
26 )
27 from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
28 from homeassistant.core import HomeAssistant
29 from homeassistant.exceptions import HomeAssistantError
30 from homeassistant.helpers.aiohttp_client import async_create_clientsession
31 from homeassistant.util.network import is_ip_address
32 
33 from . import async_last_update_was_successful
34 from .const import DOMAIN
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 
39 ENTRY_FAILURE_STATES = {
40  ConfigEntryState.SETUP_ERROR,
41  ConfigEntryState.SETUP_RETRY,
42 }
43 
44 
46  power_wall: Powerwall, password: str
47 ) -> tuple[SiteInfoResponse, str]:
48  """Login to the powerwall and fetch the base info."""
49  if password is not None:
50  await power_wall.login(password)
51 
52  return await asyncio.gather(
53  power_wall.get_site_info(), power_wall.get_gateway_din()
54  )
55 
56 
57 async def _powerwall_is_reachable(ip_address: str, password: str) -> bool:
58  """Check if the powerwall is reachable."""
59  try:
60  async with Powerwall(ip_address) as power_wall:
61  await power_wall.login(password)
62  except AccessDeniedError:
63  return True
64  except PowerwallUnreachableError:
65  return False
66  return True
67 
68 
69 async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]:
70  """Validate the user input allows us to connect.
71 
72  Data has the keys from schema with values provided by the user.
73  """
75  hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
76  )
77  async with Powerwall(data[CONF_IP_ADDRESS], http_session=session) as power_wall:
78  password = data[CONF_PASSWORD]
79 
80  try:
81  site_info, gateway_din = await _login_and_fetch_site_info(
82  power_wall, password
83  )
84  except MissingAttributeError as err:
85  # Only log the exception without the traceback
86  _LOGGER.error(str(err))
87  raise WrongVersion from err
88 
89  # Return info that you want to store in the config entry.
90  return {"title": site_info.site_name, "unique_id": gateway_din.upper()}
91 
92 
93 class PowerwallConfigFlow(ConfigFlow, domain=DOMAIN):
94  """Handle a config flow for Tesla Powerwall."""
95 
96  VERSION = 1
97 
98  def __init__(self) -> None:
99  """Initialize the powerwall flow."""
100  self.ip_addressip_address: str | None = None
101  self.titletitle: str | None = None
102 
103  async def _async_powerwall_is_offline(self, entry: ConfigEntry) -> bool:
104  """Check if the power wall is offline.
105 
106  We define offline by the config entry
107  is in a failure/retry state or the updates
108  are failing and the powerwall is unreachable
109  since device may be updating.
110  """
111  ip_address = entry.data[CONF_IP_ADDRESS]
112  password = entry.data[CONF_PASSWORD]
113  return bool(
114  entry.state in ENTRY_FAILURE_STATES
115  or not async_last_update_was_successful(self.hass, entry)
116  ) and not await _powerwall_is_reachable(ip_address, password)
117 
118  async def async_step_dhcp(
119  self, discovery_info: dhcp.DhcpServiceInfo
120  ) -> ConfigFlowResult:
121  """Handle dhcp discovery."""
122  self.ip_addressip_address = discovery_info.ip
123  gateway_din = discovery_info.hostname.upper()
124  # The hostname is the gateway_din (unique_id)
125  await self.async_set_unique_idasync_set_unique_id(gateway_din)
126  for entry in self._async_current_entries_async_current_entries(include_ignore=False):
127  if entry.data[CONF_IP_ADDRESS] == discovery_info.ip:
128  if entry.unique_id is not None and is_ip_address(entry.unique_id):
129  if self.hass.config_entries.async_update_entry(
130  entry, unique_id=gateway_din
131  ):
132  self.hass.config_entries.async_schedule_reload(entry.entry_id)
133  return self.async_abortasync_abortasync_abort(reason="already_configured")
134  if entry.unique_id == gateway_din:
135  if await self._async_powerwall_is_offline_async_powerwall_is_offline(entry):
136  if self.hass.config_entries.async_update_entry(
137  entry, data={**entry.data, CONF_IP_ADDRESS: self.ip_addressip_address}
138  ):
139  self.hass.config_entries.async_schedule_reload(entry.entry_id)
140  return self.async_abortasync_abortasync_abort(reason="already_configured")
141  # Still need to abort for ignored entries
142  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
143  self.context["title_placeholders"] = {
144  "name": gateway_din,
145  "ip_address": self.ip_addressip_address,
146  }
147  errors, info, _ = await self._async_try_connect_async_try_connect(
148  {CONF_IP_ADDRESS: self.ip_addressip_address, CONF_PASSWORD: gateway_din[-5:]}
149  )
150  if errors:
151  if CONF_PASSWORD in errors:
152  # The default password is the gateway din last 5
153  # if it does not work, we have to ask
154  return await self.async_step_userasync_step_userasync_step_user()
155  return self.async_abortasync_abortasync_abort(reason="cannot_connect")
156  assert info is not None
157  self.titletitle = info["title"]
158  return await self.async_step_confirm_discoveryasync_step_confirm_discovery()
159 
161  self, user_input: dict[str, Any]
162  ) -> tuple[dict[str, Any] | None, dict[str, str] | None, dict[str, str]]:
163  """Try to connect to the powerwall."""
164  info = None
165  errors: dict[str, str] = {}
166  description_placeholders: dict[str, str] = {}
167  try:
168  info = await validate_input(self.hass, user_input)
169  except (PowerwallUnreachableError, TimeoutError) as ex:
170  errors[CONF_IP_ADDRESS] = "cannot_connect"
171  description_placeholders = {"error": str(ex)}
172  except WrongVersion as ex:
173  errors["base"] = "wrong_version"
174  description_placeholders = {"error": str(ex)}
175  except AccessDeniedError as ex:
176  errors[CONF_PASSWORD] = "invalid_auth"
177  description_placeholders = {"error": str(ex)}
178  except Exception as ex:
179  _LOGGER.exception("Unexpected exception")
180  errors["base"] = "unknown"
181  description_placeholders = {"error": str(ex)}
182 
183  return errors, info, description_placeholders
184 
186  self, user_input: dict[str, Any] | None = None
187  ) -> ConfigFlowResult:
188  """Confirm a discovered powerwall."""
189  assert self.ip_addressip_address is not None
190  assert self.titletitle is not None
191  assert self.unique_idunique_id is not None
192  if user_input is not None:
193  return self.async_create_entryasync_create_entryasync_create_entry(
194  title=self.titletitle,
195  data={
196  CONF_IP_ADDRESS: self.ip_addressip_address,
197  CONF_PASSWORD: self.unique_idunique_id[-5:],
198  },
199  )
200 
201  self._set_confirm_only_set_confirm_only()
202  self.context["title_placeholders"] = {
203  "name": self.titletitle,
204  "ip_address": self.ip_addressip_address,
205  }
206  return self.async_show_formasync_show_formasync_show_form(
207  step_id="confirm_discovery",
208  description_placeholders={
209  "name": self.titletitle,
210  "ip_address": self.ip_addressip_address,
211  },
212  )
213 
214  async def async_step_user(
215  self, user_input: dict[str, Any] | None = None
216  ) -> ConfigFlowResult:
217  """Handle the initial step."""
218  errors: dict[str, str] | None = {}
219  description_placeholders: dict[str, str] = {}
220  if user_input is not None:
221  errors, info, description_placeholders = await self._async_try_connect_async_try_connect(
222  user_input
223  )
224  if not errors:
225  assert info is not None
226  if info["unique_id"]:
227  await self.async_set_unique_idasync_set_unique_id(
228  info["unique_id"], raise_on_progress=False
229  )
230  self._abort_if_unique_id_configured_abort_if_unique_id_configured(
231  updates={CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
232  )
233  self._async_abort_entries_match_async_abort_entries_match({CONF_IP_ADDRESS: self.ip_addressip_address})
234  return self.async_create_entryasync_create_entryasync_create_entry(title=info["title"], data=user_input)
235 
236  return self.async_show_formasync_show_formasync_show_form(
237  step_id="user",
238  data_schema=vol.Schema(
239  {
240  vol.Required(CONF_IP_ADDRESS, default=self.ip_addressip_address): str,
241  vol.Optional(CONF_PASSWORD): str,
242  }
243  ),
244  errors=errors,
245  description_placeholders=description_placeholders,
246  )
247 
249  self, user_input: dict[str, Any] | None = None
250  ) -> ConfigFlowResult:
251  """Handle reauth confirmation."""
252  errors: dict[str, str] | None = {}
253  description_placeholders: dict[str, str] = {}
254  reauth_entry = self._get_reauth_entry_get_reauth_entry()
255  if user_input is not None:
256  errors, _, description_placeholders = await self._async_try_connect_async_try_connect(
257  {CONF_IP_ADDRESS: reauth_entry.data[CONF_IP_ADDRESS], **user_input}
258  )
259  if not errors:
260  return self.async_update_reload_and_abortasync_update_reload_and_abort(
261  reauth_entry, data_updates=user_input
262  )
263 
264  self.context["title_placeholders"] = {
265  "name": reauth_entry.title,
266  "ip_address": reauth_entry.data[CONF_IP_ADDRESS],
267  }
268  return self.async_show_formasync_show_formasync_show_form(
269  step_id="reauth_confirm",
270  data_schema=vol.Schema({vol.Optional(CONF_PASSWORD): str}),
271  errors=errors,
272  description_placeholders=description_placeholders,
273  )
274 
275  async def async_step_reauth(
276  self, entry_data: Mapping[str, Any]
277  ) -> ConfigFlowResult:
278  """Handle configuration by re-auth."""
279  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
280 
281 
283  """Error indicating we cannot interact with the powerwall software version."""
ConfigFlowResult async_step_confirm_discovery(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:187
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:250
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:216
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:277
ConfigFlowResult async_step_dhcp(self, dhcp.DhcpServiceInfo discovery_info)
Definition: config_flow.py:120
tuple[dict[str, Any]|None, dict[str, str]|None, dict[str, str]] _async_try_connect(self, dict[str, Any] user_input)
Definition: config_flow.py:162
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)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=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)
str
_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)
bool _powerwall_is_reachable(str ip_address, str password)
Definition: config_flow.py:57
tuple[SiteInfoResponse, str] _login_and_fetch_site_info(Powerwall power_wall, str password)
Definition: config_flow.py:47
dict[str, str] validate_input(HomeAssistant hass, dict[str, str] data)
Definition: config_flow.py:69
bool async_last_update_was_successful(HomeAssistant hass, PowerwallConfigEntry entry)
Definition: __init__.py:284
aiohttp.ClientSession async_create_clientsession()
Definition: coordinator.py:51
bool is_ip_address(str address)
Definition: network.py:63