Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for WattTime integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from typing import TYPE_CHECKING, Any
7 
8 from aiowatttime import Client
9 from aiowatttime.errors import CoordinatesNotFoundError, InvalidCredentialsError
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import (
13  ConfigEntry,
14  ConfigFlow,
15  ConfigFlowResult,
16  OptionsFlow,
17 )
18 from homeassistant.const import (
19  CONF_LATITUDE,
20  CONF_LONGITUDE,
21  CONF_PASSWORD,
22  CONF_SHOW_ON_MAP,
23  CONF_USERNAME,
24 )
25 from homeassistant.core import callback
26 from homeassistant.helpers import aiohttp_client, config_validation as cv
27 
28 from .const import (
29  CONF_BALANCING_AUTHORITY,
30  CONF_BALANCING_AUTHORITY_ABBREV,
31  DOMAIN,
32  LOGGER,
33 )
34 
35 CONF_LOCATION_TYPE = "location_type"
36 
37 LOCATION_TYPE_COORDINATES = "Specify coordinates"
38 LOCATION_TYPE_HOME = "Use home location"
39 
40 STEP_COORDINATES_DATA_SCHEMA = vol.Schema(
41  {
42  vol.Required(CONF_LATITUDE): cv.latitude,
43  vol.Required(CONF_LONGITUDE): cv.longitude,
44  }
45 )
46 
47 STEP_LOCATION_DATA_SCHEMA = vol.Schema(
48  {
49  vol.Required(CONF_LOCATION_TYPE): vol.In(
50  [LOCATION_TYPE_HOME, LOCATION_TYPE_COORDINATES]
51  ),
52  }
53 )
54 
55 STEP_REAUTH_CONFIRM_DATA_SCHEMA = vol.Schema(
56  {
57  vol.Required(CONF_PASSWORD): str,
58  }
59 )
60 
61 STEP_USER_DATA_SCHEMA = vol.Schema(
62  {
63  vol.Required(CONF_USERNAME): str,
64  vol.Required(CONF_PASSWORD): str,
65  }
66 )
67 
68 
69 @callback
70 def get_unique_id(data: dict[str, Any]) -> str:
71  """Get a unique ID from a data payload."""
72  return f"{data[CONF_LATITUDE]}, {data[CONF_LONGITUDE]}"
73 
74 
75 class WattTimeConfigFlow(ConfigFlow, domain=DOMAIN):
76  """Handle a config flow for WattTime."""
77 
78  VERSION = 1
79 
80  def __init__(self) -> None:
81  """Initialize."""
82  self._client_client: Client | None = None
83  self._data_data: dict[str, Any] = {}
84 
86  self, username: str, password: str, error_step_id: str, error_schema: vol.Schema
87  ) -> ConfigFlowResult:
88  """Validate input credentials and proceed accordingly."""
89  session = aiohttp_client.async_get_clientsession(self.hass)
90 
91  try:
92  self._client_client = await Client.async_login(username, password, session=session)
93  except InvalidCredentialsError:
94  return self.async_show_formasync_show_formasync_show_form(
95  step_id=error_step_id,
96  data_schema=error_schema,
97  errors={"base": "invalid_auth"},
98  description_placeholders={CONF_USERNAME: username},
99  )
100  except Exception as err: # noqa: BLE001
101  LOGGER.exception("Unexpected exception while logging in: %s", err)
102  return self.async_show_formasync_show_formasync_show_form(
103  step_id=error_step_id,
104  data_schema=error_schema,
105  errors={"base": "unknown"},
106  description_placeholders={CONF_USERNAME: username},
107  )
108 
109  if CONF_LATITUDE in self._data_data:
110  # If coordinates already exist at this stage, we're in an existing flow and
111  # should reauth:
112  entry_unique_id = get_unique_id(self._data_data)
113  if existing_entry := await self.async_set_unique_idasync_set_unique_id(entry_unique_id):
114  self.hass.config_entries.async_update_entry(
115  existing_entry, data=self._data_data
116  )
117  self.hass.async_create_task(
118  self.hass.config_entries.async_reload(existing_entry.entry_id)
119  )
120  return self.async_abortasync_abortasync_abort(reason="reauth_successful")
121 
122  # ...otherwise, we're in a new flow:
123  self._data_data[CONF_USERNAME] = username
124  self._data_data[CONF_PASSWORD] = password
125  return await self.async_step_locationasync_step_location()
126 
127  @staticmethod
128  @callback
130  config_entry: ConfigEntry,
131  ) -> WattTimeOptionsFlowHandler:
132  """Define the config flow to handle options."""
134 
136  self, user_input: dict[str, Any] | None = None
137  ) -> ConfigFlowResult:
138  """Handle the coordinates step."""
139  if not user_input:
140  return self.async_show_formasync_show_formasync_show_form(
141  step_id="coordinates", data_schema=STEP_COORDINATES_DATA_SCHEMA
142  )
143 
144  if TYPE_CHECKING:
145  assert self._client_client
146 
147  unique_id = get_unique_id(user_input)
148  await self.async_set_unique_idasync_set_unique_id(unique_id)
149  self._abort_if_unique_id_configured_abort_if_unique_id_configured()
150 
151  try:
152  grid_region = await self._client_client.emissions.async_get_grid_region(
153  user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE]
154  )
155  except CoordinatesNotFoundError:
156  return self.async_show_formasync_show_formasync_show_form(
157  step_id="coordinates",
158  data_schema=STEP_COORDINATES_DATA_SCHEMA,
159  errors={CONF_LATITUDE: "unknown_coordinates"},
160  )
161  except Exception as err: # noqa: BLE001
162  LOGGER.exception("Unexpected exception while getting region: %s", err)
163  return self.async_show_formasync_show_formasync_show_form(
164  step_id="coordinates",
165  data_schema=STEP_COORDINATES_DATA_SCHEMA,
166  errors={"base": "unknown"},
167  )
168 
169  return self.async_create_entryasync_create_entryasync_create_entry(
170  title=unique_id,
171  data={
172  CONF_USERNAME: self._data_data[CONF_USERNAME],
173  CONF_PASSWORD: self._data_data[CONF_PASSWORD],
174  CONF_LATITUDE: user_input[CONF_LATITUDE],
175  CONF_LONGITUDE: user_input[CONF_LONGITUDE],
176  CONF_BALANCING_AUTHORITY: grid_region["name"],
177  CONF_BALANCING_AUTHORITY_ABBREV: grid_region["abbrev"],
178  },
179  )
180 
182  self, user_input: dict[str, Any] | None = None
183  ) -> ConfigFlowResult:
184  """Handle the "pick a location" step."""
185  if not user_input:
186  return self.async_show_formasync_show_formasync_show_form(
187  step_id="location", data_schema=STEP_LOCATION_DATA_SCHEMA
188  )
189 
190  if user_input[CONF_LOCATION_TYPE] == LOCATION_TYPE_HOME:
191  return await self.async_step_coordinatesasync_step_coordinates(
192  {
193  CONF_LATITUDE: self.hass.config.latitude,
194  CONF_LONGITUDE: self.hass.config.longitude,
195  }
196  )
197  return await self.async_step_coordinatesasync_step_coordinates()
198 
199  async def async_step_reauth(
200  self, entry_data: Mapping[str, Any]
201  ) -> ConfigFlowResult:
202  """Handle configuration by re-auth."""
203  self._data_data = {**entry_data}
204  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
205 
207  self, user_input: dict[str, Any] | None = None
208  ) -> ConfigFlowResult:
209  """Handle re-auth completion."""
210  if not user_input:
211  return self.async_show_formasync_show_formasync_show_form(
212  step_id="reauth_confirm",
213  data_schema=STEP_REAUTH_CONFIRM_DATA_SCHEMA,
214  description_placeholders={CONF_USERNAME: self._data_data[CONF_USERNAME]},
215  )
216 
217  self._data_data[CONF_PASSWORD] = user_input[CONF_PASSWORD]
218 
219  return await self._async_validate_credentials_async_validate_credentials(
220  self._data_data[CONF_USERNAME],
221  self._data_data[CONF_PASSWORD],
222  "reauth_confirm",
223  STEP_REAUTH_CONFIRM_DATA_SCHEMA,
224  )
225 
226  async def async_step_user(
227  self, user_input: dict[str, Any] | None = None
228  ) -> ConfigFlowResult:
229  """Handle the initial step."""
230  if not user_input:
231  return self.async_show_formasync_show_formasync_show_form(
232  step_id="user", data_schema=STEP_USER_DATA_SCHEMA
233  )
234 
235  return await self._async_validate_credentials_async_validate_credentials(
236  user_input[CONF_USERNAME],
237  user_input[CONF_PASSWORD],
238  "user",
239  STEP_USER_DATA_SCHEMA,
240  )
241 
242 
244  """Handle a WattTime options flow."""
245 
246  async def async_step_init(
247  self, user_input: dict[str, Any] | None = None
248  ) -> ConfigFlowResult:
249  """Manage the options."""
250  if user_input is not None:
251  return self.async_create_entryasync_create_entry(data=user_input)
252 
253  return self.async_show_formasync_show_form(
254  step_id="init",
255  data_schema=vol.Schema(
256  {
257  vol.Required(
258  CONF_SHOW_ON_MAP,
259  default=self.config_entryconfig_entryconfig_entry.options.get(CONF_SHOW_ON_MAP, True),
260  ): bool
261  }
262  ),
263  )
ConfigFlowResult async_step_location(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:183
ConfigFlowResult async_step_coordinates(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:137
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:228
ConfigFlowResult async_step_reauth_confirm(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:208
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:201
ConfigFlowResult _async_validate_credentials(self, str username, str password, str error_step_id, vol.Schema error_schema)
Definition: config_flow.py:87
WattTimeOptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:131
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:248
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)
None config_entry(self, ConfigEntry value)
_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)