1 """Support for Hass.io."""
3 from __future__
import annotations
6 from contextlib
import suppress
7 from datetime
import datetime
8 from functools
import partial
12 from typing
import Any, NamedTuple
14 from aiohasupervisor
import SupervisorError
15 import voluptuous
as vol
24 EVENT_CORE_CONFIG_UPDATE,
33 async_get_hass_or_none,
37 config_validation
as cv,
38 device_registry
as dr,
44 all_with_deprecated_constants,
45 check_if_deprecated_constant,
47 dir_with_deprecated_constants,
51 get_supervisor_ip
as _get_supervisor_ip,
52 is_hassio
as _is_hassio,
56 HassioServiceInfo
as _HassioServiceInfo,
75 from .addon_manager
import AddonError, AddonInfo, AddonManager, AddonState
76 from .addon_panel
import async_setup_addon_panel
77 from .auth
import async_setup_auth_view
85 ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
93 DATA_KEY_SUPERVISOR_ISSUES,
99 HASSIO_UPDATE_INTERVAL,
101 from .coordinator
import (
102 HassioDataUpdateCoordinator,
103 get_addons_changelogs,
113 get_supervisor_stats,
115 from .discovery
import async_setup_discovery_view
116 from .handler
import (
120 async_get_green_settings,
121 async_get_yellow_settings,
123 async_set_green_settings,
124 async_set_yellow_settings,
125 async_update_diagnostics,
126 get_supervisor_client,
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
133 _LOGGER = logging.getLogger(__name__)
135 get_supervisor_ip = deprecated_function(
136 "homeassistant.helpers.hassio.get_supervisor_ip", breaks_in_ha_version=
"2025.11"
137 )(_get_supervisor_ip)
140 "homeassistant.helpers.service_info.hassio.HassioServiceInfo",
149 PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.UPDATE]
151 CONF_FRONTEND_REPO =
"development_repo"
153 CONFIG_SCHEMA = vol.Schema(
154 {vol.Optional(DOMAIN): vol.Schema({vol.Optional(CONF_FRONTEND_REPO): cv.isdir})},
155 extra=vol.ALLOW_EXTRA,
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"
170 VALID_ADDON_SLUG = vol.Match(re.compile(
r"^[-_.A-Za-z0-9]+$"))
174 """Validate value is a valid addon slug."""
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")
183 SCHEMA_NO_DATA = vol.Schema({})
185 SCHEMA_ADDON = vol.Schema({vol.Required(ATTR_ADDON): valid_addon})
187 SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
188 {vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
191 SCHEMA_BACKUP_FULL = vol.Schema(
194 ATTR_NAME, default=
lambda:
now().strftime(
"%Y-%m-%d %H:%M:%S")
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
201 vol.Optional(ATTR_HOMEASSISTANT_EXCLUDE_DATABASE): cv.boolean,
205 SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
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]),
213 SCHEMA_RESTORE_FULL = vol.Schema(
215 vol.Required(ATTR_SLUG): cv.slug,
216 vol.Optional(ATTR_PASSWORD): cv.string,
220 SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
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]),
230 """Settings for API endpoint."""
234 timeout: int |
None = 60
235 pass_data: bool =
False
244 "/addons/{addon}/stdin", SCHEMA_ADDON_STDIN
255 "/backups/new/partial",
256 SCHEMA_BACKUP_PARTIAL,
261 "/backups/{slug}/restore/full",
267 "/backups/{slug}/restore/partial",
268 SCHEMA_RESTORE_PARTIAL,
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",
293 """Return hostname of add-on."""
294 return addon_slug.replace(
"_",
"-")
298 @deprecated_function(
"homeassistant.helpers.hassio.is_hassio", breaks_in_ha_version="2025.11"
)
300 def is_hassio(hass: HomeAssistant) -> bool:
301 """Return true if Hass.io is loaded.
305 return _is_hassio(hass)
308 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
309 """Set up the Hass.io component."""
311 for env
in (
"SUPERVISOR",
"SUPERVISOR_TOKEN"):
312 if os.environ.get(env):
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)
323 host = os.environ[
"SUPERVISOR"]
325 hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
329 await supervisor_client.supervisor.ping()
330 except SupervisorError:
331 _LOGGER.warning(
"Not connected with the supervisor / system too busy!")
333 store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY)
334 if (data := await store.async_load())
is 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]
344 if not user.is_admin:
345 await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN])
348 if user.name ==
"Hass.io":
349 await hass.auth.async_update_user(user, name=HASSIO_USER_NAME)
351 if refresh_token
is None:
352 user = await hass.auth.async_create_system_user(
353 HASSIO_USER_NAME, group_ids=[GROUP_ID_ADMIN]
355 refresh_token = await hass.auth.async_create_refresh_token(user)
356 data[
"hassio_user"] = user.id
357 await store.async_save(data)
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(
366 os.path.join(development_repo,
"hassio/build"),
372 hass.http.register_view(
HassIOView(host, websession))
374 await panel_custom.async_register_panel(
376 frontend_url_path=
"hassio",
377 webcomponent_name=
"hassio-main",
378 js_url=
"/api/hassio/app/entrypoint.js",
383 update_hass_api_task = hass.async_create_task(
384 hassio.update_hass_api(config.get(
"http", {}), refresh_token), eager_start=
True
389 async
def push_config(_: Event |
None) ->
None:
390 """Push core config to Hass.io."""
391 nonlocal last_timezone
393 new_timezone =
str(hass.config.time_zone)
395 if new_timezone == last_timezone:
398 last_timezone = new_timezone
399 await hassio.update_hass_timezone(new_timezone)
401 hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
403 push_config_task = hass.async_create_task(push_config(
None), eager_start=
True)
405 hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues =
SupervisorIssues(hass, hassio)
406 issues_task = hass.async_create_task(issues.setup(), eager_start=
True)
408 async
def async_service_handler(service: ServiceCall) ->
None:
409 """Handle service calls for Hass.io."""
410 if service.service == SERVICE_ADDON_UPDATE:
414 "update_service_deprecated",
415 breaks_in_ha_version=
"2025.5",
417 severity=IssueSeverity.WARNING,
418 translation_key=
"update_service_deprecated",
420 api_endpoint = MAP_SERVICE_API[service.service]
422 data = service.data.copy()
423 addon = data.pop(ATTR_ADDON,
None)
424 slug = data.pop(ATTR_SLUG,
None)
428 if service.service == SERVICE_ADDON_STDIN:
429 payload = data[ATTR_INPUT]
430 elif api_endpoint.pass_data:
435 with suppress(HassioAPIError):
436 await hassio.send_command(
437 api_endpoint.command.format(addon=addon, slug=slug),
439 timeout=api_endpoint.timeout,
442 for service, settings
in MAP_SERVICE_API.items():
443 hass.services.async_register(
444 DOMAIN, service, async_service_handler, schema=settings.schema
447 async
def update_info_data(_: datetime |
None =
None) ->
None:
448 """Update last available supervisor information."""
453 hass.data[DATA_INFO],
454 hass.data[DATA_HOST_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()),
470 except HassioAPIError
as err:
471 _LOGGER.warning(
"Can't read Supervisor data: %s", err)
473 hass.data[DATA_STORE] = store_info.to_dict()
477 HASSIO_UPDATE_INTERVAL,
478 HassJob(update_info_data, cancel_on_shutdown=
True),
482 update_info_task = hass.async_create_task(update_info_data(), eager_start=
True)
484 async
def _async_stop(hass: HomeAssistant, restart: bool) ->
None:
485 """Stop or restart home assistant."""
487 await supervisor_client.homeassistant.restart()
489 await supervisor_client.homeassistant.stop()
498 assert user
is not None
505 panels_task = hass.async_create_task(
513 await update_hass_api_task
515 await update_info_task
516 await push_config_task
521 def _async_setup_hardware_integration(_: datetime |
None =
None) ->
None:
522 """Set up hardware integration for the detected board type."""
527 HASSIO_UPDATE_INTERVAL,
528 async_setup_hardware_integration_job,
531 if (board := os_info.get(
"board"))
is None:
533 if (hw_integration := HARDWARE_INTEGRATIONS.get(board))
is None:
535 discovery_flow.async_create_flow(
536 hass, hw_integration, context={
"source": SOURCE_SYSTEM}, data={}
539 async_setup_hardware_integration_job =
HassJob(
540 _async_setup_hardware_integration, cancel_on_shutdown=
True
543 _async_setup_hardware_integration()
544 discovery_flow.async_create_flow(
545 hass, DOMAIN, context={
"source": SOURCE_SYSTEM}, data={}
551 """Set up a config entry."""
552 dev_reg = dr.async_get(hass)
554 await coordinator.async_config_entry_first_refresh()
555 hass.data[ADDONS_COORDINATOR] = coordinator
557 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
563 """Unload a config entry."""
564 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
567 hass.data.pop(ADDONS_COORDINATOR,
None)
573 __getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
575 dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
578
web.Response get(self, web.Request request, str config_key)
None async_setup_addon_panel(HomeAssistant hass, HassIO hassio)
None async_setup_auth_view(HomeAssistant hass, User user)
dict[str, dict[str, Any]]|None get_addons_info(HomeAssistant hass)
dict[str, Any]|None get_os_info(HomeAssistant hass)
None async_setup_discovery_view(HomeAssistant hass, HassIO hassio)
SupervisorClient get_supervisor_client(HomeAssistant hass)
None async_setup_ingress_view(HomeAssistant hass, str host)
None async_load_websocket_api(HomeAssistant hass)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
bool is_hassio(HomeAssistant hass)
str hostname_from_addon_slug(str addon_slug)
str valid_addon(Any value)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None async_set_stop_handler(HomeAssistant hass, Callable[[HomeAssistant, bool], Coroutine[Any, Any, None]] stop_handler)
None _async_stop(HomeAssistant hass, bool restart)
None async_create_issue(HomeAssistant hass, str entry_id)
HomeAssistant|None async_get_hass_or_none()
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)
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)
datetime now(HomeAssistant hass)