Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Hass.io."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from contextlib import suppress
7 from datetime import datetime
8 from functools import partial
9 import logging
10 import os
11 import re
12 from typing import Any, NamedTuple
13 
14 from aiohasupervisor import SupervisorError
15 import voluptuous as vol
16 
17 from homeassistant.auth.const import GROUP_ID_ADMIN
18 from homeassistant.components import panel_custom
19 from homeassistant.components.homeassistant import async_set_stop_handler
20 from homeassistant.components.http import StaticPathConfig
21 from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
22 from homeassistant.const import (
23  ATTR_NAME,
24  EVENT_CORE_CONFIG_UPDATE,
25  HASSIO_USER_NAME,
26  Platform,
27 )
28 from homeassistant.core import (
29  Event,
30  HassJob,
31  HomeAssistant,
32  ServiceCall,
33  async_get_hass_or_none,
34  callback,
35 )
36 from homeassistant.helpers import (
37  config_validation as cv,
38  device_registry as dr,
39  discovery_flow,
40 )
41 from homeassistant.helpers.aiohttp_client import async_get_clientsession
43  DeprecatedConstant,
44  all_with_deprecated_constants,
45  check_if_deprecated_constant,
46  deprecated_function,
47  dir_with_deprecated_constants,
48 )
49 from homeassistant.helpers.event import async_call_later
50 from homeassistant.helpers.hassio import (
51  get_supervisor_ip as _get_supervisor_ip,
52  is_hassio as _is_hassio,
53 )
54 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
56  HassioServiceInfo as _HassioServiceInfo,
57 )
58 from homeassistant.helpers.storage import Store
59 from homeassistant.helpers.typing import ConfigType
60 from homeassistant.loader import bind_hass
61 from homeassistant.util.async_ import create_eager_task
62 from homeassistant.util.dt import now
63 
64 # config_flow, diagnostics, system_health, and entity platforms are imported to
65 # ensure other dependencies that wait for hassio are not waiting
66 # for hassio to import its platforms
67 from . import ( # noqa: F401
68  binary_sensor,
69  config_flow,
70  diagnostics,
71  sensor,
72  system_health,
73  update,
74 )
75 from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401
76 from .addon_panel import async_setup_addon_panel
77 from .auth import async_setup_auth_view
78 from .const import (
79  ADDONS_COORDINATOR,
80  ATTR_ADDON,
81  ATTR_ADDONS,
82  ATTR_COMPRESSED,
83  ATTR_FOLDERS,
84  ATTR_HOMEASSISTANT,
85  ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
86  ATTR_INPUT,
87  ATTR_LOCATION,
88  ATTR_PASSWORD,
89  ATTR_SLUG,
90  DATA_CORE_INFO,
91  DATA_HOST_INFO,
92  DATA_INFO,
93  DATA_KEY_SUPERVISOR_ISSUES,
94  DATA_NETWORK_INFO,
95  DATA_OS_INFO,
96  DATA_STORE,
97  DATA_SUPERVISOR_INFO,
98  DOMAIN,
99  HASSIO_UPDATE_INTERVAL,
100 )
101 from .coordinator import (
102  HassioDataUpdateCoordinator,
103  get_addons_changelogs, # noqa: F401
104  get_addons_info,
105  get_addons_stats, # noqa: F401
106  get_core_info, # noqa: F401
107  get_core_stats, # noqa: F401
108  get_host_info, # noqa: F401
109  get_info, # noqa: F401
110  get_issues_info, # noqa: F401
111  get_os_info,
112  get_supervisor_info, # noqa: F401
113  get_supervisor_stats, # noqa: F401
114 )
115 from .discovery import async_setup_discovery_view # noqa: F401
116 from .handler import ( # noqa: F401
117  HassIO,
118  HassioAPIError,
119  async_create_backup,
120  async_get_green_settings,
121  async_get_yellow_settings,
122  async_reboot_host,
123  async_set_green_settings,
124  async_set_yellow_settings,
125  async_update_diagnostics,
126  get_supervisor_client,
127 )
128 from .http import HassIOView
129 from .ingress import async_setup_ingress_view
130 from .issues import SupervisorIssues
131 from .websocket_api import async_load_websocket_api
132 
133 _LOGGER = logging.getLogger(__name__)
134 
135 get_supervisor_ip = deprecated_function(
136  "homeassistant.helpers.hassio.get_supervisor_ip", breaks_in_ha_version="2025.11"
137 )(_get_supervisor_ip)
138 _DEPRECATED_HassioServiceInfo = DeprecatedConstant(
139  _HassioServiceInfo,
140  "homeassistant.helpers.service_info.hassio.HassioServiceInfo",
141  "2025.11",
142 )
143 
144 STORAGE_KEY = DOMAIN
145 STORAGE_VERSION = 1
146 # If new platforms are added, be sure to import them above
147 # so we do not make other components that depend on hassio
148 # wait for the import of the platforms
149 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE]
150 
151 CONF_FRONTEND_REPO = "development_repo"
152 
153 CONFIG_SCHEMA = vol.Schema(
154  {vol.Optional(DOMAIN): vol.Schema({vol.Optional(CONF_FRONTEND_REPO): cv.isdir})},
155  extra=vol.ALLOW_EXTRA,
156 )
157 
158 SERVICE_ADDON_START = "addon_start"
159 SERVICE_ADDON_STOP = "addon_stop"
160 SERVICE_ADDON_RESTART = "addon_restart"
161 SERVICE_ADDON_UPDATE = "addon_update"
162 SERVICE_ADDON_STDIN = "addon_stdin"
163 SERVICE_HOST_SHUTDOWN = "host_shutdown"
164 SERVICE_HOST_REBOOT = "host_reboot"
165 SERVICE_BACKUP_FULL = "backup_full"
166 SERVICE_BACKUP_PARTIAL = "backup_partial"
167 SERVICE_RESTORE_FULL = "restore_full"
168 SERVICE_RESTORE_PARTIAL = "restore_partial"
169 
170 VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
171 
172 
173 def valid_addon(value: Any) -> str:
174  """Validate value is a valid addon slug."""
175  value = VALID_ADDON_SLUG(value)
176  hass = async_get_hass_or_none()
177 
178  if hass and (addons := get_addons_info(hass)) is not None and value not in addons:
179  raise vol.Invalid("Not a valid add-on slug")
180  return value
181 
182 
183 SCHEMA_NO_DATA = vol.Schema({})
184 
185 SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): valid_addon})
186 
187 SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
188  {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
189 )
190 
191 SCHEMA_BACKUP_FULL = vol.Schema(
192  {
193  vol.Optional(
194  ATTR_NAME, default=lambda: now().strftime("%Y-%m-%d %H:%M:%S")
195  ): cv.string,
196  vol.Optional(ATTR_PASSWORD): cv.string,
197  vol.Optional(ATTR_COMPRESSED): cv.boolean,
198  vol.Optional(ATTR_LOCATION): vol.All(
199  cv.string, lambda v: None if v == "/backup" else v
200  ),
201  vol.Optional(ATTR_HOMEASSISTANT_EXCLUDE_DATABASE): cv.boolean,
202  }
203 )
204 
205 SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
206  {
207  vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
208  vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
209  vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]),
210  }
211 )
212 
213 SCHEMA_RESTORE_FULL = vol.Schema(
214  {
215  vol.Required(ATTR_SLUG): cv.slug,
216  vol.Optional(ATTR_PASSWORD): cv.string,
217  }
218 )
219 
220 SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
221  {
222  vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
223  vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
224  vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]),
225  }
226 )
227 
228 
229 class APIEndpointSettings(NamedTuple):
230  """Settings for API endpoint."""
231 
232  command: str
233  schema: vol.Schema
234  timeout: int | None = 60
235  pass_data: bool = False
236 
237 
238 MAP_SERVICE_API = {
239  SERVICE_ADDON_START: APIEndpointSettings("/addons/{addon}/start", SCHEMA_ADDON),
240  SERVICE_ADDON_STOP: APIEndpointSettings("/addons/{addon}/stop", SCHEMA_ADDON),
241  SERVICE_ADDON_RESTART: APIEndpointSettings("/addons/{addon}/restart", SCHEMA_ADDON),
242  SERVICE_ADDON_UPDATE: APIEndpointSettings("/addons/{addon}/update", SCHEMA_ADDON),
243  SERVICE_ADDON_STDIN: APIEndpointSettings(
244  "/addons/{addon}/stdin", SCHEMA_ADDON_STDIN
245  ),
246  SERVICE_HOST_SHUTDOWN: APIEndpointSettings("/host/shutdown", SCHEMA_NO_DATA),
247  SERVICE_HOST_REBOOT: APIEndpointSettings("/host/reboot", SCHEMA_NO_DATA),
248  SERVICE_BACKUP_FULL: APIEndpointSettings(
249  "/backups/new/full",
250  SCHEMA_BACKUP_FULL,
251  None,
252  True,
253  ),
254  SERVICE_BACKUP_PARTIAL: APIEndpointSettings(
255  "/backups/new/partial",
256  SCHEMA_BACKUP_PARTIAL,
257  None,
258  True,
259  ),
260  SERVICE_RESTORE_FULL: APIEndpointSettings(
261  "/backups/{slug}/restore/full",
262  SCHEMA_RESTORE_FULL,
263  None,
264  True,
265  ),
266  SERVICE_RESTORE_PARTIAL: APIEndpointSettings(
267  "/backups/{slug}/restore/partial",
268  SCHEMA_RESTORE_PARTIAL,
269  None,
270  True,
271  ),
272 }
273 
274 HARDWARE_INTEGRATIONS = {
275  "green": "homeassistant_green",
276  "odroid-c2": "hardkernel",
277  "odroid-c4": "hardkernel",
278  "odroid-m1": "hardkernel",
279  "odroid-m1s": "hardkernel",
280  "odroid-n2": "hardkernel",
281  "odroid-xu4": "hardkernel",
282  "rpi2": "raspberry_pi",
283  "rpi3": "raspberry_pi",
284  "rpi3-64": "raspberry_pi",
285  "rpi4": "raspberry_pi",
286  "rpi4-64": "raspberry_pi",
287  "rpi5-64": "raspberry_pi",
288  "yellow": "homeassistant_yellow",
289 }
290 
291 
292 def hostname_from_addon_slug(addon_slug: str) -> str:
293  """Return hostname of add-on."""
294  return addon_slug.replace("_", "-")
295 
296 
297 @callback
298 @deprecated_function( "homeassistant.helpers.hassio.is_hassio", breaks_in_ha_version="2025.11" )
299 @bind_hass
300 def is_hassio(hass: HomeAssistant) -> bool:
301  """Return true if Hass.io is loaded.
302 
303  Async friendly.
304  """
305  return _is_hassio(hass)
306 
307 
308 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
309  """Set up the Hass.io component."""
310  # Check local setup
311  for env in ("SUPERVISOR", "SUPERVISOR_TOKEN"):
312  if os.environ.get(env):
313  continue
314  _LOGGER.error("Missing %s environment variable", env)
315  if config_entries := hass.config_entries.async_entries(DOMAIN):
316  hass.async_create_task(
317  hass.config_entries.async_remove(config_entries[0].entry_id)
318  )
319  return False
320 
322 
323  host = os.environ["SUPERVISOR"]
324  websession = async_get_clientsession(hass)
325  hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
326  supervisor_client = get_supervisor_client(hass)
327 
328  try:
329  await supervisor_client.supervisor.ping()
330  except SupervisorError:
331  _LOGGER.warning("Not connected with the supervisor / system too busy!")
332 
333  store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY)
334  if (data := await store.async_load()) is None:
335  data = {}
336 
337  refresh_token = None
338  if "hassio_user" in data:
339  user = await hass.auth.async_get_user(data["hassio_user"])
340  if user and user.refresh_tokens:
341  refresh_token = list(user.refresh_tokens.values())[0]
342 
343  # Migrate old Hass.io users to be admin.
344  if not user.is_admin:
345  await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN])
346 
347  # Migrate old name
348  if user.name == "Hass.io":
349  await hass.auth.async_update_user(user, name=HASSIO_USER_NAME)
350 
351  if refresh_token is None:
352  user = await hass.auth.async_create_system_user(
353  HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN]
354  )
355  refresh_token = await hass.auth.async_create_refresh_token(user)
356  data["hassio_user"] = user.id
357  await store.async_save(data)
358 
359  # This overrides the normal API call that would be forwarded
360  development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO)
361  if development_repo is not None:
362  await hass.http.async_register_static_paths(
363  [
365  "/api/hassio/app",
366  os.path.join(development_repo, "hassio/build"),
367  False,
368  )
369  ]
370  )
371 
372  hass.http.register_view(HassIOView(host, websession))
373 
374  await panel_custom.async_register_panel(
375  hass,
376  frontend_url_path="hassio",
377  webcomponent_name="hassio-main",
378  js_url="/api/hassio/app/entrypoint.js",
379  embed_iframe=True,
380  require_admin=True,
381  )
382 
383  update_hass_api_task = hass.async_create_task(
384  hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True
385  )
386 
387  last_timezone = None
388 
389  async def push_config(_: Event | None) -> None:
390  """Push core config to Hass.io."""
391  nonlocal last_timezone
392 
393  new_timezone = str(hass.config.time_zone)
394 
395  if new_timezone == last_timezone:
396  return
397 
398  last_timezone = new_timezone
399  await hassio.update_hass_timezone(new_timezone)
400 
401  hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
402 
403  push_config_task = hass.async_create_task(push_config(None), eager_start=True)
404  # Start listening for problems with supervisor and making issues
405  hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
406  issues_task = hass.async_create_task(issues.setup(), eager_start=True)
407 
408  async def async_service_handler(service: ServiceCall) -> None:
409  """Handle service calls for Hass.io."""
410  if service.service == SERVICE_ADDON_UPDATE:
412  hass,
413  DOMAIN,
414  "update_service_deprecated",
415  breaks_in_ha_version="2025.5",
416  is_fixable=False,
417  severity=IssueSeverity.WARNING,
418  translation_key="update_service_deprecated",
419  )
420  api_endpoint = MAP_SERVICE_API[service.service]
421 
422  data = service.data.copy()
423  addon = data.pop(ATTR_ADDON, None)
424  slug = data.pop(ATTR_SLUG, None)
425  payload = None
426 
427  # Pass data to Hass.io API
428  if service.service == SERVICE_ADDON_STDIN:
429  payload = data[ATTR_INPUT]
430  elif api_endpoint.pass_data:
431  payload = data
432 
433  # Call API
434  # The exceptions are logged properly in hassio.send_command
435  with suppress(HassioAPIError):
436  await hassio.send_command(
437  api_endpoint.command.format(addon=addon, slug=slug),
438  payload=payload,
439  timeout=api_endpoint.timeout,
440  )
441 
442  for service, settings in MAP_SERVICE_API.items():
443  hass.services.async_register(
444  DOMAIN, service, async_service_handler, schema=settings.schema
445  )
446 
447  async def update_info_data(_: datetime | None = None) -> None:
448  """Update last available supervisor information."""
449  supervisor_client = get_supervisor_client(hass)
450 
451  try:
452  (
453  hass.data[DATA_INFO],
454  hass.data[DATA_HOST_INFO],
455  store_info,
456  hass.data[DATA_CORE_INFO],
457  hass.data[DATA_SUPERVISOR_INFO],
458  hass.data[DATA_OS_INFO],
459  hass.data[DATA_NETWORK_INFO],
460  ) = await asyncio.gather(
461  create_eager_task(hassio.get_info()),
462  create_eager_task(hassio.get_host_info()),
463  create_eager_task(supervisor_client.store.info()),
464  create_eager_task(hassio.get_core_info()),
465  create_eager_task(hassio.get_supervisor_info()),
466  create_eager_task(hassio.get_os_info()),
467  create_eager_task(hassio.get_network_info()),
468  )
469 
470  except HassioAPIError as err:
471  _LOGGER.warning("Can't read Supervisor data: %s", err)
472  else:
473  hass.data[DATA_STORE] = store_info.to_dict()
474 
476  hass,
477  HASSIO_UPDATE_INTERVAL,
478  HassJob(update_info_data, cancel_on_shutdown=True),
479  )
480 
481  # Fetch data
482  update_info_task = hass.async_create_task(update_info_data(), eager_start=True)
483 
484  async def _async_stop(hass: HomeAssistant, restart: bool) -> None:
485  """Stop or restart home assistant."""
486  if restart:
487  await supervisor_client.homeassistant.restart()
488  else:
489  await supervisor_client.homeassistant.stop()
490 
491  # Set a custom handler for the homeassistant.restart and homeassistant.stop services
492  async_set_stop_handler(hass, _async_stop)
493 
494  # Init discovery Hass.io feature
495  async_setup_discovery_view(hass, hassio)
496 
497  # Init auth Hass.io feature
498  assert user is not None
499  async_setup_auth_view(hass, user)
500 
501  # Init ingress Hass.io feature
502  async_setup_ingress_view(hass, host)
503 
504  # Init add-on ingress panels
505  panels_task = hass.async_create_task(
506  async_setup_addon_panel(hass, hassio), eager_start=True
507  )
508 
509  # Make sure to await the update_info task before
510  # _async_setup_hardware_integration is called
511  # so the hardware integration can be set up
512  # and does not fallback to calling later
513  await update_hass_api_task
514  await panels_task
515  await update_info_task
516  await push_config_task
517  await issues_task
518 
519  # Setup hardware integration for the detected board type
520  @callback
521  def _async_setup_hardware_integration(_: datetime | None = None) -> None:
522  """Set up hardware integration for the detected board type."""
523  if (os_info := get_os_info(hass)) is None:
524  # os info not yet fetched from supervisor, retry later
526  hass,
527  HASSIO_UPDATE_INTERVAL,
528  async_setup_hardware_integration_job,
529  )
530  return
531  if (board := os_info.get("board")) is None:
532  return
533  if (hw_integration := HARDWARE_INTEGRATIONS.get(board)) is None:
534  return
535  discovery_flow.async_create_flow(
536  hass, hw_integration, context={"source": SOURCE_SYSTEM}, data={}
537  )
538 
539  async_setup_hardware_integration_job = HassJob(
540  _async_setup_hardware_integration, cancel_on_shutdown=True
541  )
542 
543  _async_setup_hardware_integration()
544  discovery_flow.async_create_flow(
545  hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
546  )
547  return True
548 
549 
550 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
551  """Set up a config entry."""
552  dev_reg = dr.async_get(hass)
553  coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
554  await coordinator.async_config_entry_first_refresh()
555  hass.data[ADDONS_COORDINATOR] = coordinator
556 
557  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
558 
559  return True
560 
561 
562 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
563  """Unload a config entry."""
564  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
565 
566  # Pop add-on data
567  hass.data.pop(ADDONS_COORDINATOR, None)
568 
569  return unload_ok
570 
571 
572 # These can be removed if no deprecated constant are in this module anymore
573 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
574 __dir__ = partial(
575  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
576 )
577 __all__ = all_with_deprecated_constants(globals())
578 
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_addon_panel(HomeAssistant hass, HassIO hassio)
Definition: addon_panel.py:20
None async_setup_auth_view(HomeAssistant hass, User user)
Definition: auth.py:25
dict[str, dict[str, Any]]|None get_addons_info(HomeAssistant hass)
Definition: coordinator.py:119
dict[str, Any]|None get_os_info(HomeAssistant hass)
Definition: coordinator.py:169
None async_setup_discovery_view(HomeAssistant hass, HassIO hassio)
Definition: discovery.py:30
SupervisorClient get_supervisor_client(HomeAssistant hass)
Definition: handler.py:344
None async_setup_ingress_view(HomeAssistant hass, str host)
Definition: ingress.py:51
None async_load_websocket_api(HomeAssistant hass)
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:310
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:552
bool is_hassio(HomeAssistant hass)
Definition: __init__.py:302
str hostname_from_addon_slug(str addon_slug)
Definition: __init__.py:292
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:564
None async_set_stop_handler(HomeAssistant hass, Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]] stop_handler)
Definition: __init__.py:403
None _async_stop(HomeAssistant hass, bool restart)
Definition: __init__.py:392
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
HomeAssistant|None async_get_hass_or_none()
Definition: core.py:299
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)
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597
datetime now(HomeAssistant hass)
Definition: template.py:1890