Home Assistant Unofficial Reference 2024.12.1
views.py
Go to the documentation of this file.
1 """Onboarding views."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Coroutine
7 from http import HTTPStatus
8 from typing import TYPE_CHECKING, Any, cast
9 
10 from aiohttp import web
11 from aiohttp.web_exceptions import HTTPUnauthorized
12 import voluptuous as vol
13 
14 from homeassistant.auth.const import GROUP_ID_ADMIN
15 from homeassistant.auth.providers.homeassistant import HassAuthProvider
16 from homeassistant.components import person
17 from homeassistant.components.auth import indieauth
18 from homeassistant.components.http import KEY_HASS, KEY_HASS_REFRESH_TOKEN_ID
19 from homeassistant.components.http.data_validator import RequestDataValidator
20 from homeassistant.components.http.view import HomeAssistantView
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.helpers import area_registry as ar
23 from homeassistant.helpers.hassio import is_hassio
24 from homeassistant.helpers.system_info import async_get_system_info
25 from homeassistant.helpers.translation import async_get_translations
26 from homeassistant.setup import async_setup_component
27 from homeassistant.util.async_ import create_eager_task
28 
29 if TYPE_CHECKING:
30  from . import OnboardingData, OnboardingStorage, OnboardingStoreData
31 
32 from .const import (
33  DEFAULT_AREAS,
34  DOMAIN,
35  STEP_ANALYTICS,
36  STEP_CORE_CONFIG,
37  STEP_INTEGRATION,
38  STEP_USER,
39  STEPS,
40 )
41 
42 
43 async def async_setup(
44  hass: HomeAssistant, data: OnboardingStoreData, store: OnboardingStorage
45 ) -> None:
46  """Set up the onboarding view."""
47  hass.http.register_view(OnboardingView(data, store))
48  hass.http.register_view(InstallationTypeOnboardingView(data))
49  hass.http.register_view(UserOnboardingView(data, store))
50  hass.http.register_view(CoreConfigOnboardingView(data, store))
51  hass.http.register_view(IntegrationOnboardingView(data, store))
52  hass.http.register_view(AnalyticsOnboardingView(data, store))
53 
54 
55 class OnboardingView(HomeAssistantView):
56  """Return the onboarding status."""
57 
58  requires_auth = False
59  url = "/api/onboarding"
60  name = "api:onboarding"
61 
62  def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None:
63  """Initialize the onboarding view."""
64  self._store_store = store
65  self._data_data = data
66 
67  async def get(self, request: web.Request) -> web.Response:
68  """Return the onboarding status."""
69  return self.json(
70  [{"step": key, "done": key in self._data_data["done"]} for key in STEPS]
71  )
72 
73 
74 class InstallationTypeOnboardingView(HomeAssistantView):
75  """Return the installation type during onboarding."""
76 
77  requires_auth = False
78  url = "/api/onboarding/installation_type"
79  name = "api:onboarding:installation_type"
80 
81  def __init__(self, data: OnboardingStoreData) -> None:
82  """Initialize the onboarding installation type view."""
83  self._data_data = data
84 
85  async def get(self, request: web.Request) -> web.Response:
86  """Return the onboarding status."""
87  if self._data_data["done"]:
88  raise HTTPUnauthorized
89 
90  hass = request.app[KEY_HASS]
91  info = await async_get_system_info(hass)
92  return self.json({"installation_type": info["installation_type"]})
93 
94 
95 class _BaseOnboardingView(HomeAssistantView):
96  """Base class for onboarding."""
97 
98  step: str
99 
100  def __init__(self, data: OnboardingStoreData, store: OnboardingStorage) -> None:
101  """Initialize the onboarding view."""
102  self._store_store = store
103  self._data_data = data
104  self._lock_lock = asyncio.Lock()
105 
106  @callback
107  def _async_is_done(self) -> bool:
108  """Return if this step is done."""
109  return self.step in self._data_data["done"]
110 
111  async def _async_mark_done(self, hass: HomeAssistant) -> None:
112  """Mark step as done."""
113  self._data_data["done"].append(self.step)
114  await self._store_store.async_save(self._data_data)
115 
116  if set(self._data_data["done"]) == set(STEPS):
117  data: OnboardingData = hass.data[DOMAIN]
118  data.onboarded = True
119  for listener in data.listeners:
120  listener()
121 
122 
124  """View to handle create user onboarding step."""
125 
126  url = "/api/onboarding/users"
127  name = "api:onboarding:users"
128  requires_auth = False
129  step = STEP_USER
130 
131  @RequestDataValidator( vol.Schema( { vol.Required("name"): str,
132  vol.Required("username"): str,
133  vol.Required("password"): str,
134  vol.Required("client_id"): str,
135  vol.Required("language"): str,
136  }
137  )
138  )
139  async def post(self, request: web.Request, data: dict[str, str]) -> web.Response:
140  """Handle user creation, area creation."""
141  hass = request.app[KEY_HASS]
142 
143  async with self._lock_lock:
144  if self._async_is_done_async_is_done():
145  return self.json_message("User step already done", HTTPStatus.FORBIDDEN)
146 
147  provider = _async_get_hass_provider(hass)
148  await provider.async_initialize()
149 
150  user = await hass.auth.async_create_user(
151  data["name"], group_ids=[GROUP_ID_ADMIN]
152  )
153  await provider.async_add_auth(data["username"], data["password"])
154  credentials = await provider.async_get_or_create_credentials(
155  {"username": data["username"]}
156  )
157  await hass.auth.async_link_user(user, credentials)
158  if "person" in hass.config.components:
159  await person.async_create_person(hass, data["name"], user_id=user.id)
160 
161  # Create default areas using the users supplied language.
162  translations = await async_get_translations(
163  hass, data["language"], "area", {DOMAIN}
164  )
165 
166  area_registry = ar.async_get(hass)
167 
168  for area in DEFAULT_AREAS:
169  name = translations[f"component.onboarding.area.{area}"]
170  # Guard because area might have been created by an automatically
171  # set up integration.
172  if not area_registry.async_get_area_by_name(name):
173  area_registry.async_create(name)
174 
175  await self._async_mark_done_async_mark_done(hass)
176 
177  # Return authorization code for fetching tokens and connect
178  # during onboarding.
179  # pylint: disable-next=import-outside-toplevel
180  from homeassistant.components.auth import create_auth_code
181 
182  auth_code = create_auth_code(hass, data["client_id"], credentials)
183  return self.json({"auth_code": auth_code})
184 
185 
187  """View to finish core config onboarding step."""
188 
189  url = "/api/onboarding/core_config"
190  name = "api:onboarding:core_config"
191  step = STEP_CORE_CONFIG
192 
193  async def post(self, request: web.Request) -> web.Response:
194  """Handle finishing core config step."""
195  hass = request.app[KEY_HASS]
196 
197  async with self._lock_lock:
198  if self._async_is_done_async_is_done():
199  return self.json_message(
200  "Core config step already done", HTTPStatus.FORBIDDEN
201  )
202 
203  await self._async_mark_done_async_mark_done(hass)
204 
205  # Integrations to set up when finishing onboarding
206  onboard_integrations = [
207  "google_translate",
208  "met",
209  "radio_browser",
210  "shopping_list",
211  ]
212 
213  # pylint: disable-next=import-outside-toplevel
214  from homeassistant.components import hassio
215 
216  if (
217  is_hassio(hass)
218  and (core_info := hassio.get_core_info(hass))
219  and "raspberrypi" in core_info["machine"]
220  ):
221  onboard_integrations.append("rpi_power")
222 
223  coros: list[Coroutine[Any, Any, Any]] = [
224  hass.config_entries.flow.async_init(
225  domain, context={"source": "onboarding"}
226  )
227  for domain in onboard_integrations
228  ]
229 
230  if "analytics" not in hass.config.components:
231  # If by some chance that analytics has not finished
232  # setting up, wait for it here so its ready for the
233  # next step.
234  coros.append(async_setup_component(hass, "analytics", {}))
235 
236  # Set up integrations after onboarding and ensure
237  # analytics is ready for the next step.
238  await asyncio.gather(*(create_eager_task(coro) for coro in coros))
239 
240  return self.json({})
241 
242 
244  """View to finish integration onboarding step."""
245 
246  url = "/api/onboarding/integration"
247  name = "api:onboarding:integration"
248  step = STEP_INTEGRATION
249 
250  @RequestDataValidator( vol.Schema({vol.Required("client_id"): str, vol.Required("redirect_uri"): str})
251  )
252  async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
253  """Handle token creation."""
254  hass = request.app[KEY_HASS]
255  refresh_token_id = request[KEY_HASS_REFRESH_TOKEN_ID]
256 
257  async with self._lock_lock:
258  if self._async_is_done_async_is_done():
259  return self.json_message(
260  "Integration step already done", HTTPStatus.FORBIDDEN
261  )
262 
263  await self._async_mark_done_async_mark_done(hass)
264 
265  # Validate client ID and redirect uri
266  if not await indieauth.verify_redirect_uri(
267  request.app[KEY_HASS], data["client_id"], data["redirect_uri"]
268  ):
269  return self.json_message(
270  "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST
271  )
272 
273  refresh_token = hass.auth.async_get_refresh_token(refresh_token_id)
274  if refresh_token is None or refresh_token.credential is None:
275  return self.json_message(
276  "Credentials for user not available", HTTPStatus.FORBIDDEN
277  )
278 
279  # Return authorization code so we can redirect user and log them in
280  # pylint: disable-next=import-outside-toplevel
281  from homeassistant.components.auth import create_auth_code
282 
283  auth_code = create_auth_code(
284  hass, data["client_id"], refresh_token.credential
285  )
286  return self.json({"auth_code": auth_code})
287 
288 
290  """View to finish analytics onboarding step."""
291 
292  url = "/api/onboarding/analytics"
293  name = "api:onboarding:analytics"
294  step = STEP_ANALYTICS
295 
296  async def post(self, request: web.Request) -> web.Response:
297  """Handle finishing analytics step."""
298  hass = request.app[KEY_HASS]
299 
300  async with self._lock_lock:
301  if self._async_is_done_async_is_done():
302  return self.json_message(
303  "Analytics config step already done", HTTPStatus.FORBIDDEN
304  )
305 
306  await self._async_mark_done_async_mark_done(hass)
307 
308  return self.json({})
309 
310 
311 @callback
312 def _async_get_hass_provider(hass: HomeAssistant) -> HassAuthProvider:
313  """Get the Home Assistant auth provider."""
314  for prv in hass.auth.auth_providers:
315  if prv.type == "homeassistant":
316  return cast(HassAuthProvider, prv)
317 
318  raise RuntimeError("No Home Assistant provider found")
319 
web.Response post(self, web.Request request)
Definition: views.py:300
web.Response post(self, web.Request request)
Definition: views.py:196
web.Response post(self, web.Request request, dict[str, Any] data)
Definition: views.py:255
None __init__(self, OnboardingStoreData data, OnboardingStorage store)
Definition: views.py:62
web.Response get(self, web.Request request)
Definition: views.py:67
web.Response post(self, web.Request request, dict[str, str] data)
Definition: views.py:139
None __init__(self, OnboardingStoreData data, OnboardingStorage store)
Definition: views.py:100
str create_auth_code(HomeAssistant hass, str client_id, Credentials credential)
Definition: __init__.py:179
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
HassAuthProvider _async_get_hass_provider(HomeAssistant hass)
Definition: views.py:316
None async_setup(HomeAssistant hass, OnboardingStoreData data, OnboardingStorage store)
Definition: views.py:45
None async_save(self, _T data)
Definition: storage.py:424
dict[str, Any] async_get_system_info(HomeAssistant hass)
Definition: system_info.py:44
dict[str, str] async_get_translations(HomeAssistant hass, str language, str category, Iterable[str]|None integrations=None, bool|None config_flow=None)
Definition: translation.py:342
bool async_setup_component(core.HomeAssistant hass, str domain, ConfigType config)
Definition: setup.py:147