Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for Netatmo."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import logging
7 from typing import Any
8 import uuid
9 
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import (
13  SOURCE_REAUTH,
14  ConfigEntry,
15  ConfigFlowResult,
16  OptionsFlow,
17 )
18 from homeassistant.const import CONF_SHOW_ON_MAP, CONF_UUID
19 from homeassistant.core import callback
20 from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
21 
22 from .api import get_api_scopes
23 from .const import (
24  CONF_AREA_NAME,
25  CONF_LAT_NE,
26  CONF_LAT_SW,
27  CONF_LON_NE,
28  CONF_LON_SW,
29  CONF_NEW_AREA,
30  CONF_PUBLIC_MODE,
31  CONF_WEATHER_AREAS,
32  DOMAIN,
33 )
34 
35 _LOGGER = logging.getLogger(__name__)
36 
37 
39  config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
40 ):
41  """Config flow to handle Netatmo OAuth2 authentication."""
42 
43  DOMAIN = DOMAIN
44 
45  @staticmethod
46  @callback
48  config_entry: ConfigEntry,
49  ) -> OptionsFlow:
50  """Get the options flow for this handler."""
51  return NetatmoOptionsFlowHandler(config_entry)
52 
53  @property
54  def logger(self) -> logging.Logger:
55  """Return logger."""
56  return logging.getLogger(__name__)
57 
58  @property
59  def extra_authorize_data(self) -> dict:
60  """Extra data that needs to be appended to the authorize url."""
61  scopes = get_api_scopes(self.flow_impl.domain)
62  return {"scope": " ".join(scopes)}
63 
64  async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult:
65  """Handle a flow start."""
66  await self.async_set_unique_id(DOMAIN)
67 
68  if self.source != SOURCE_REAUTH and self._async_current_entries():
69  return self.async_abort(reason="single_instance_allowed")
70 
71  return await super().async_step_user(user_input)
72 
73  async def async_step_reauth(
74  self, entry_data: Mapping[str, Any]
75  ) -> ConfigFlowResult:
76  """Perform reauth upon an API authentication error."""
77  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
78 
80  self, user_input: dict | None = None
81  ) -> ConfigFlowResult:
82  """Dialog that informs the user that reauth is required."""
83  if user_input is None:
84  return self.async_show_form(step_id="reauth_confirm")
85 
86  return await self.async_step_userasync_step_user()
87 
88  async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
89  """Create an oauth config entry or update existing entry for reauth."""
90  existing_entry = await self.async_set_unique_id(DOMAIN)
91  if existing_entry:
92  self.hass.config_entries.async_update_entry(existing_entry, data=data)
93  await self.hass.config_entries.async_reload(existing_entry.entry_id)
94  return self.async_abort(reason="reauth_successful")
95 
96  return await super().async_oauth_create_entry(data)
97 
98 
100  """Handle Netatmo options."""
101 
102  def __init__(self, config_entry: ConfigEntry) -> None:
103  """Initialize Netatmo options flow."""
104  self.optionsoptions = dict(config_entry.options)
105  self.optionsoptions.setdefault(CONF_WEATHER_AREAS, {})
106 
107  async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
108  """Manage the Netatmo options."""
109  return await self.async_step_public_weather_areasasync_step_public_weather_areas()
110 
112  self, user_input: dict | None = None
113  ) -> ConfigFlowResult:
114  """Manage configuration of Netatmo public weather areas."""
115  errors: dict = {}
116 
117  if user_input is not None:
118  new_client = user_input.pop(CONF_NEW_AREA, None)
119  areas = user_input.pop(CONF_WEATHER_AREAS, [])
120  user_input[CONF_WEATHER_AREAS] = {
121  area: self.optionsoptions[CONF_WEATHER_AREAS][area] for area in areas
122  }
123  self.optionsoptions.update(user_input)
124  if new_client:
125  return await self.async_step_public_weatherasync_step_public_weather(
126  user_input={CONF_NEW_AREA: new_client}
127  )
128 
129  return self._create_options_entry_create_options_entry()
130 
131  weather_areas = list(self.optionsoptions[CONF_WEATHER_AREAS])
132 
133  data_schema = vol.Schema(
134  {
135  vol.Optional(
136  CONF_WEATHER_AREAS,
137  default=weather_areas,
138  ): cv.multi_select({wa: None for wa in weather_areas}),
139  vol.Optional(CONF_NEW_AREA): str,
140  }
141  )
142  return self.async_show_formasync_show_form(
143  step_id="public_weather_areas",
144  data_schema=data_schema,
145  errors=errors,
146  )
147 
148  async def async_step_public_weather(self, user_input: dict) -> ConfigFlowResult:
149  """Manage configuration of Netatmo public weather sensors."""
150  if user_input is not None and CONF_NEW_AREA not in user_input:
151  self.optionsoptions[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]] = (
152  fix_coordinates(user_input)
153  )
154 
155  self.optionsoptions[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]][CONF_UUID] = (
156  str(uuid.uuid4())
157  )
158 
159  return await self.async_step_public_weather_areasasync_step_public_weather_areas()
160 
161  orig_options = self.config_entryconfig_entryconfig_entry.options.get(CONF_WEATHER_AREAS, {}).get(
162  user_input[CONF_NEW_AREA], {}
163  )
164 
165  default_longitude = self.hass.config.longitude
166  default_latitude = self.hass.config.latitude
167  default_size = 0.04
168 
169  data_schema = vol.Schema(
170  {
171  vol.Optional(CONF_AREA_NAME, default=user_input[CONF_NEW_AREA]): str,
172  vol.Optional(
173  CONF_LAT_NE,
174  default=orig_options.get(
175  CONF_LAT_NE, default_latitude + default_size
176  ),
177  ): cv.latitude,
178  vol.Optional(
179  CONF_LON_NE,
180  default=orig_options.get(
181  CONF_LON_NE, default_longitude + default_size
182  ),
183  ): cv.longitude,
184  vol.Optional(
185  CONF_LAT_SW,
186  default=orig_options.get(
187  CONF_LAT_SW, default_latitude - default_size
188  ),
189  ): cv.latitude,
190  vol.Optional(
191  CONF_LON_SW,
192  default=orig_options.get(
193  CONF_LON_SW, default_longitude - default_size
194  ),
195  ): cv.longitude,
196  vol.Required(
197  CONF_PUBLIC_MODE,
198  default=orig_options.get(CONF_PUBLIC_MODE, "avg"),
199  ): vol.In(["avg", "max", "min"]),
200  vol.Required(
201  CONF_SHOW_ON_MAP,
202  default=orig_options.get(CONF_SHOW_ON_MAP, False),
203  ): bool,
204  }
205  )
206 
207  return self.async_show_formasync_show_form(step_id="public_weather", data_schema=data_schema)
208 
209  def _create_options_entry(self) -> ConfigFlowResult:
210  """Update config entry options."""
211  return self.async_create_entryasync_create_entry(
212  title="Netatmo Public Weather", data=self.optionsoptions
213  )
214 
215 
216 def fix_coordinates(user_input: dict) -> dict:
217  """Fix coordinates if they don't comply with the Netatmo API."""
218  # Ensure coordinates have acceptable length for the Netatmo API
219  for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW):
220  if len(str(user_input[coordinate]).split(".")[1]) < 7:
221  user_input[coordinate] = user_input[coordinate] + 0.0000001
222 
223  # Swap coordinates if entered in wrong order
224  if user_input[CONF_LAT_NE] < user_input[CONF_LAT_SW]:
225  user_input[CONF_LAT_NE], user_input[CONF_LAT_SW] = (
226  user_input[CONF_LAT_SW],
227  user_input[CONF_LAT_NE],
228  )
229  if user_input[CONF_LON_NE] < user_input[CONF_LON_SW]:
230  user_input[CONF_LON_NE], user_input[CONF_LON_SW] = (
231  user_input[CONF_LON_SW],
232  user_input[CONF_LON_NE],
233  )
234 
235  return user_input
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:49
ConfigFlowResult async_oauth_create_entry(self, dict data)
Definition: config_flow.py:88
ConfigFlowResult async_step_user(self, dict|None user_input=None)
Definition: config_flow.py:64
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:75
ConfigFlowResult async_step_reauth_confirm(self, dict|None user_input=None)
Definition: config_flow.py:81
ConfigFlowResult async_step_public_weather(self, dict user_input)
Definition: config_flow.py:148
ConfigFlowResult async_step_init(self, dict|None user_input=None)
Definition: config_flow.py:107
ConfigFlowResult async_step_public_weather_areas(self, dict|None user_input=None)
Definition: config_flow.py:113
None config_entry(self, ConfigEntry value)
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)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
Iterable[str] get_api_scopes(str auth_implementation)
Definition: api.py:15