mirror of
https://github.com/home-assistant/core.git
synced 2026-05-06 08:36:42 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 308f214eaf | |||
| 648dfe832f | |||
| 0fa542b806 | |||
| 8e437ce39d |
@@ -6,20 +6,12 @@ from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
GreenOptions,
|
||||
HomeAssistantInfo,
|
||||
HomeAssistantOptions,
|
||||
HostInfo,
|
||||
InstalledAddon,
|
||||
NetworkInfo,
|
||||
OSInfo,
|
||||
RootInfo,
|
||||
StoreInfo,
|
||||
SupervisorInfo,
|
||||
SupervisorOptions,
|
||||
YellowOptions,
|
||||
)
|
||||
@@ -28,6 +20,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.homeassistant.const import DATA_STOP_HANDLER
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
@@ -41,6 +34,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
@@ -51,7 +45,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
|
||||
# config_flow, diagnostics, system_health, and entity platforms are imported to
|
||||
# ensure other dependencies that wait for hassio are not waiting
|
||||
@@ -74,17 +67,12 @@ from .auth import async_setup_auth_view
|
||||
from .config import HassioConfig
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
DATA_ADDONS_LIST,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DATA_CORE_INFO,
|
||||
DATA_HOST_INFO,
|
||||
DATA_INFO,
|
||||
DATA_HASSIO_HOST,
|
||||
DATA_HASSIO_HTTP_CONFIG,
|
||||
DATA_HASSIO_SUPERVISOR_USER,
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
DATA_NETWORK_INFO,
|
||||
DATA_OS_INFO,
|
||||
DATA_STORE,
|
||||
DATA_SUPERVISOR_INFO,
|
||||
DOMAIN,
|
||||
HASSIO_MAIN_UPDATE_INTERVAL,
|
||||
MAIN_COORDINATOR,
|
||||
@@ -188,6 +176,61 @@ def hostname_from_addon_slug(addon_slug: str) -> str:
|
||||
return addon_slug.replace("_", "-")
|
||||
|
||||
|
||||
@callback
|
||||
def _check_deprecated_setup(hass: HomeAssistant) -> None:
|
||||
"""Create issues for deprecated installation types and architectures."""
|
||||
os_info = get_os_info(hass)
|
||||
info = get_info(hass)
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
board = os_info.get("board")
|
||||
arch = info.get("arch", "unknown")
|
||||
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
|
||||
unsupported_os_on_board = board in {"rpi3", "rpi4"}
|
||||
if is_haos and (unsupported_board or unsupported_os_on_board):
|
||||
issue_id = "deprecated_os_"
|
||||
if unsupported_os_on_board:
|
||||
issue_id += "aarch64"
|
||||
elif unsupported_board:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
bit32 = _is_32_bit()
|
||||
deprecated_architecture = bit32 and not (
|
||||
unsupported_board or unsupported_os_on_board
|
||||
)
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": "OS" if is_haos else "Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Hass.io component."""
|
||||
# Check local setup
|
||||
@@ -201,18 +244,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
async_load_websocket_api(hass)
|
||||
frontend.async_register_built_in_panel(hass, "app")
|
||||
|
||||
host = os.environ["SUPERVISOR"]
|
||||
websession = async_get_clientsession(hass)
|
||||
hass.data[DATA_COMPONENT] = HassIO(hass.loop, websession, host)
|
||||
hass.data[DATA_HASSIO_HOST] = host
|
||||
hass.data[DATA_HASSIO_HTTP_CONFIG] = config.get("http", {})
|
||||
|
||||
async_load_websocket_api(hass)
|
||||
hass.http.register_view(HassIOView(host, websession))
|
||||
async_setup_services(hass)
|
||||
async_setup_discovery_view(hass)
|
||||
async_setup_auth_view(hass)
|
||||
async_setup_ingress_view(hass)
|
||||
frontend.async_register_built_in_panel(hass, "app")
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
try:
|
||||
await supervisor_client.supervisor.ping()
|
||||
except SupervisorError:
|
||||
_LOGGER.warning("Not connected with the supervisor / system too busy!")
|
||||
except SupervisorError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="supervisor_not_connected",
|
||||
) from err
|
||||
|
||||
# Load the store
|
||||
config_store = HassioConfig(hass)
|
||||
@@ -240,34 +302,52 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user)
|
||||
config_store.update(hassio_user=user.id)
|
||||
|
||||
hass.http.register_view(HassIOView(host, websession))
|
||||
assert user is not None
|
||||
hass.data[DATA_HASSIO_SUPERVISOR_USER] = user
|
||||
|
||||
async def update_hass_api(http_config: dict[str, Any], refresh_token: RefreshToken):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
options = HomeAssistantOptions(
|
||||
ssl=CONF_SSL_CERTIFICATE in http_config,
|
||||
port=http_config.get(CONF_SERVER_PORT) or SERVER_PORT,
|
||||
refresh_token=refresh_token.token,
|
||||
)
|
||||
# Set up coordinators — these can raise ConfigEntryNotReady.
|
||||
# Register listeners only after all refreshes succeed to avoid accumulation
|
||||
# across retries.
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options = replace(options, watchdog=False)
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
coordinator = HassioMainDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[MAIN_COORDINATOR] = coordinator
|
||||
|
||||
try:
|
||||
await supervisor_client.homeassistant.set_options(options)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to update Home Assistant options in Supervisor: %s", err
|
||||
)
|
||||
|
||||
update_hass_api_task = hass.async_create_task(
|
||||
update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
addon_coordinator = HassioAddOnDataUpdateCoordinator(
|
||||
hass, entry, dev_reg, coordinator.jobs
|
||||
)
|
||||
await addon_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = addon_coordinator
|
||||
|
||||
stats_coordinator = HassioStatsDataUpdateCoordinator(hass, entry)
|
||||
await stats_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[STATS_COORDINATOR] = stats_coordinator
|
||||
|
||||
# All coordinators refreshed successfully. Start the issues listener and
|
||||
# install the stop handler now so they are never left in a partial state
|
||||
# if a coordinator refresh raises ConfigEntryNotReady.
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass)
|
||||
|
||||
async def _async_stop(hass: HomeAssistant, restart: bool) -> None:
|
||||
"""Stop or restart home assistant."""
|
||||
if restart:
|
||||
await supervisor_client.homeassistant.restart()
|
||||
else:
|
||||
await supervisor_client.homeassistant.stop()
|
||||
|
||||
# Install a custom handler for the homeassistant.restart / stop services,
|
||||
# and restore the previous one when this entry unloads.
|
||||
prev_stop_handler = hass.data.get(DATA_STOP_HANDLER)
|
||||
async_set_stop_handler(hass, _async_stop)
|
||||
|
||||
def _restore_stop_handler() -> None:
|
||||
if prev_stop_handler is not None:
|
||||
async_set_stop_handler(hass, prev_stop_handler)
|
||||
else:
|
||||
hass.data.pop(DATA_STOP_HANDLER, None)
|
||||
|
||||
entry.async_on_unload(_restore_stop_handler)
|
||||
last_timezone = None
|
||||
last_country = None
|
||||
|
||||
@@ -290,103 +370,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Failed to update Supervisor options: %s", err)
|
||||
|
||||
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
|
||||
entry.async_on_unload(hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config))
|
||||
|
||||
push_config_task = hass.async_create_task(push_config(None), eager_start=True)
|
||||
# Start listening for problems with supervisor and making issues
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass)
|
||||
issues_task = hass.async_create_task(issues.setup(), eager_start=True)
|
||||
http_config: dict[str, Any] = hass.data.get(DATA_HASSIO_HTTP_CONFIG, {})
|
||||
|
||||
# Register services
|
||||
async_setup_services(hass, supervisor_client)
|
||||
async def update_hass_api(refresh_token: RefreshToken) -> None:
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
options = HomeAssistantOptions(
|
||||
ssl=CONF_SSL_CERTIFICATE in http_config,
|
||||
port=http_config.get(CONF_SERVER_PORT) or SERVER_PORT,
|
||||
refresh_token=refresh_token.token,
|
||||
)
|
||||
|
||||
async def update_info_data(_: datetime | None = None) -> None:
|
||||
"""Update last available supervisor information."""
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
try:
|
||||
(
|
||||
root_info,
|
||||
host_info,
|
||||
store_info,
|
||||
homeassistant_info,
|
||||
supervisor_info,
|
||||
os_info,
|
||||
network_info,
|
||||
addons_list,
|
||||
) = cast(
|
||||
tuple[
|
||||
RootInfo,
|
||||
HostInfo,
|
||||
StoreInfo,
|
||||
HomeAssistantInfo,
|
||||
SupervisorInfo,
|
||||
OSInfo,
|
||||
NetworkInfo,
|
||||
list[InstalledAddon],
|
||||
],
|
||||
await asyncio.gather(
|
||||
create_eager_task(supervisor_client.info()),
|
||||
create_eager_task(supervisor_client.host.info()),
|
||||
create_eager_task(supervisor_client.store.info()),
|
||||
create_eager_task(supervisor_client.homeassistant.info()),
|
||||
create_eager_task(supervisor_client.supervisor.info()),
|
||||
create_eager_task(supervisor_client.os.info()),
|
||||
create_eager_task(supervisor_client.network.info()),
|
||||
create_eager_task(supervisor_client.addons.list()),
|
||||
),
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options = replace(options, watchdog=False)
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
|
||||
try:
|
||||
await supervisor_client.homeassistant.set_options(options)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Can't read Supervisor data: %s", err)
|
||||
else:
|
||||
hass.data[DATA_INFO] = root_info
|
||||
hass.data[DATA_HOST_INFO] = host_info
|
||||
hass.data[DATA_STORE] = store_info
|
||||
hass.data[DATA_CORE_INFO] = homeassistant_info
|
||||
hass.data[DATA_SUPERVISOR_INFO] = supervisor_info
|
||||
hass.data[DATA_OS_INFO] = os_info
|
||||
hass.data[DATA_NETWORK_INFO] = network_info
|
||||
hass.data[DATA_ADDONS_LIST] = addons_list
|
||||
_LOGGER.warning(
|
||||
"Failed to update Home Assistant options in Supervisor: %s", err
|
||||
)
|
||||
|
||||
# Fetch data
|
||||
update_info_task = hass.async_create_task(update_info_data(), eager_start=True)
|
||||
|
||||
async def _async_stop(hass: HomeAssistant, restart: bool) -> None:
|
||||
"""Stop or restart home assistant."""
|
||||
if restart:
|
||||
await supervisor_client.homeassistant.restart()
|
||||
else:
|
||||
await supervisor_client.homeassistant.stop()
|
||||
|
||||
# Set a custom handler for the homeassistant.restart and homeassistant.stop services
|
||||
async_set_stop_handler(hass, _async_stop)
|
||||
|
||||
# Init discovery Hass.io feature
|
||||
async_setup_discovery_view(hass)
|
||||
|
||||
# Init auth Hass.io feature
|
||||
assert user is not None
|
||||
async_setup_auth_view(hass, user)
|
||||
|
||||
# Init ingress Hass.io feature
|
||||
async_setup_ingress_view(hass, host)
|
||||
|
||||
# Init add-on ingress panels
|
||||
panels_task = hass.async_create_task(
|
||||
async_setup_addon_panel(hass), eager_start=True
|
||||
await asyncio.gather(
|
||||
update_hass_api(refresh_token),
|
||||
push_config(None),
|
||||
issues.setup(),
|
||||
async_setup_addon_panel(hass),
|
||||
)
|
||||
|
||||
# Make sure to await the update_info task before
|
||||
# _async_setup_hardware_integration is called
|
||||
# so the hardware integration can be set up
|
||||
# and does not fallback to calling later
|
||||
await update_hass_api_task
|
||||
await panels_task
|
||||
await update_info_task
|
||||
await push_config_task
|
||||
await issues_task
|
||||
|
||||
# Setup hardware integration for the detected board type
|
||||
@callback
|
||||
def _async_setup_hardware_integration(_: datetime | None = None) -> None:
|
||||
@@ -412,81 +428,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
_async_setup_hardware_integration()
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
coordinator = HassioMainDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[MAIN_COORDINATOR] = coordinator
|
||||
|
||||
addon_coordinator = HassioAddOnDataUpdateCoordinator(
|
||||
hass, entry, dev_reg, coordinator.jobs
|
||||
)
|
||||
await addon_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = addon_coordinator
|
||||
|
||||
stats_coordinator = HassioStatsDataUpdateCoordinator(hass, entry)
|
||||
await stats_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[STATS_COORDINATOR] = stats_coordinator
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
info = get_info(hass)
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
board = os_info.get("board")
|
||||
arch = info.get("arch", "unknown")
|
||||
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
|
||||
unsupported_os_on_board = board in {"rpi3", "rpi4"}
|
||||
if is_haos and (unsupported_board or unsupported_os_on_board):
|
||||
issue_id = "deprecated_os_"
|
||||
if unsupported_os_on_board:
|
||||
issue_id += "aarch64"
|
||||
elif unsupported_board:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
bit32 = _is_32_bit()
|
||||
deprecated_architecture = bit32 and not (
|
||||
unsupported_board or unsupported_os_on_board
|
||||
)
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": "OS" if is_haos else "Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
_check_deprecated_setup(hass)
|
||||
listener()
|
||||
|
||||
listener = coordinator.async_add_listener(deprecated_setup_issue)
|
||||
@@ -504,9 +448,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator: HassioMainDataUpdateCoordinator = hass.data[MAIN_COORDINATOR]
|
||||
coordinator.unload()
|
||||
|
||||
# Pop coordinators
|
||||
# Pop coordinators and entry-level data
|
||||
hass.data.pop(MAIN_COORDINATOR, None)
|
||||
hass.data.pop(ADDONS_COORDINATOR, None)
|
||||
hass.data.pop(STATS_COORDINATOR, None)
|
||||
hass.data.pop(DATA_CONFIG_STORE, None)
|
||||
hass.data.pop(DATA_HASSIO_SUPERVISOR_USER, None)
|
||||
|
||||
if (
|
||||
supervisor_issues := hass.data.pop(DATA_KEY_SUPERVISOR_ISSUES, None)
|
||||
) is not None:
|
||||
supervisor_issues.unload()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -6,41 +6,44 @@ import logging
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPNotFound, HTTPUnauthorized
|
||||
from aiohttp.web_exceptions import (
|
||||
HTTPNotFound,
|
||||
HTTPServiceUnavailable,
|
||||
HTTPUnauthorized,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.providers import homeassistant as auth_ha
|
||||
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME
|
||||
from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME, DATA_HASSIO_SUPERVISOR_USER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_auth_view(hass: HomeAssistant, user: User) -> None:
|
||||
def async_setup_auth_view(hass: HomeAssistant) -> None:
|
||||
"""Auth setup."""
|
||||
hassio_auth = HassIOAuth(hass, user)
|
||||
hassio_password_reset = HassIOPasswordReset(hass, user)
|
||||
|
||||
hass.http.register_view(hassio_auth)
|
||||
hass.http.register_view(hassio_password_reset)
|
||||
hass.http.register_view(HassIOAuth(hass))
|
||||
hass.http.register_view(HassIOPasswordReset(hass))
|
||||
|
||||
|
||||
class HassIOBaseAuth(HomeAssistantView):
|
||||
"""Hass.io view to handle auth requests."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, user: User) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize WebView."""
|
||||
self.hass = hass
|
||||
self.user = user
|
||||
|
||||
def _check_access(self, request: web.Request) -> None:
|
||||
"""Check if this call is from Supervisor."""
|
||||
user = self.hass.data.get(DATA_HASSIO_SUPERVISOR_USER)
|
||||
if user is None:
|
||||
raise HTTPServiceUnavailable
|
||||
|
||||
# Check caller IP
|
||||
hassio_ip = os.environ["SUPERVISOR"].split(":")[0]
|
||||
assert request.transport
|
||||
@@ -51,7 +54,7 @@ class HassIOBaseAuth(HomeAssistantView):
|
||||
raise HTTPUnauthorized
|
||||
|
||||
# Check caller token
|
||||
if request[KEY_HASS_USER].id != self.user.id:
|
||||
if request[KEY_HASS_USER].id != user.id:
|
||||
_LOGGER.error("Invalid auth request from %s", request[KEY_HASS_USER].name)
|
||||
raise HTTPUnauthorized
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -20,6 +20,8 @@ if TYPE_CHECKING:
|
||||
SupervisorInfo,
|
||||
)
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
|
||||
from .config import HassioConfig
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
@@ -145,6 +147,9 @@ DATA_KEY_CORE = "core"
|
||||
DATA_KEY_HOST = "host"
|
||||
DATA_KEY_SUPERVISOR_ISSUES: HassKey[SupervisorIssues] = HassKey("supervisor_issues")
|
||||
DATA_KEY_MOUNTS = "mounts"
|
||||
DATA_HASSIO_HTTP_CONFIG: HassKey[dict[str, Any]] = HassKey("hassio_http_config")
|
||||
DATA_HASSIO_HOST: HassKey[str] = HassKey("hassio_host")
|
||||
DATA_HASSIO_SUPERVISOR_USER: HassKey[User] = HassKey("hassio_supervisor_user")
|
||||
|
||||
PLACEHOLDER_KEY_ADDON = "addon"
|
||||
PLACEHOLDER_KEY_ADDON_INFO = "addon_info"
|
||||
|
||||
@@ -780,10 +780,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
|
||||
)
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
if info := self.hass.data.get(DATA_INFO):
|
||||
self.is_hass_os = info.hassos is not None
|
||||
else:
|
||||
self.is_hass_os = False
|
||||
self.is_hass_os = False
|
||||
self.supervisor_client = get_supervisor_client(hass)
|
||||
self.jobs = SupervisorJobs(hass)
|
||||
self._dispatcher_disconnect = async_dispatcher_connect(
|
||||
@@ -843,6 +840,7 @@ class HassioMainDataUpdateCoordinator(DataUpdateCoordinator[HassioMainData]):
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
# Build clean coordinator data
|
||||
self.is_hass_os = info.hassos is not None
|
||||
new_data = HassioMainData(
|
||||
core=core_info,
|
||||
supervisor=supervisor_info,
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
|
||||
from .const import X_HASS_SOURCE, X_INGRESS_PATH
|
||||
from .const import DATA_HASSIO_HOST, X_HASS_SOURCE, X_INGRESS_PATH
|
||||
from .http import should_compress
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -50,8 +50,9 @@ DISABLED_TIMEOUT = ClientTimeout(total=None)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_ingress_view(hass: HomeAssistant, host: str) -> None:
|
||||
"""Auth setup."""
|
||||
def async_setup_ingress_view(hass: HomeAssistant) -> None:
|
||||
"""Set up the Hass.io ingress HTTP view."""
|
||||
host = hass.data[DATA_HASSIO_HOST]
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
hassio_ingress = HassIOIngress(host, websession)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Supervisor events monitor."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
import logging
|
||||
@@ -180,6 +181,8 @@ class SupervisorIssues:
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[UUID, Issue] = {}
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
self._disconnect: Callable[[], None] | None = None
|
||||
self._cancel_update_retry: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
@@ -352,22 +355,32 @@ class SupervisorIssues:
|
||||
"""Create supervisor events listener."""
|
||||
await self._update()
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._disconnect = async_dispatcher_connect(
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues
|
||||
)
|
||||
|
||||
def unload(self) -> None:
|
||||
"""Remove supervisor events listener."""
|
||||
if self._disconnect is not None:
|
||||
self._disconnect()
|
||||
self._disconnect = None
|
||||
if self._cancel_update_retry is not None:
|
||||
self._cancel_update_retry()
|
||||
self._cancel_update_retry = None
|
||||
|
||||
async def _update(self, _: datetime | None = None) -> None:
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
try:
|
||||
data = await self._supervisor_client.resolution.info()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.error("Failed to update supervisor issues: %r", err)
|
||||
async_call_later(
|
||||
self._cancel_update_retry = async_call_later(
|
||||
self._hass,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
HassJob(self._update, cancel_on_shutdown=True),
|
||||
)
|
||||
return
|
||||
self._cancel_update_retry = None
|
||||
self.unhealthy_reasons = set(data.unhealthy)
|
||||
self.unsupported_reasons = set(data.unsupported)
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ from .const import (
|
||||
SupervisorEntityModel,
|
||||
)
|
||||
from .coordinator import HassioMainDataUpdateCoordinator, get_addons_info
|
||||
from .handler import get_supervisor_client
|
||||
|
||||
SERVICE_ADDON_START = "addon_start"
|
||||
SERVICE_ADDON_STOP = "addon_stop"
|
||||
@@ -163,10 +164,9 @@ SCHEMA_MOUNT_RELOAD = vol.Schema(
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(
|
||||
hass: HomeAssistant, supervisor_client: SupervisorClient
|
||||
) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the Supervisor services."""
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
async_register_app_services(hass, supervisor_client)
|
||||
async_register_host_services(hass, supervisor_client)
|
||||
async_register_backup_restore_services(hass, supervisor_client)
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
},
|
||||
"mount_reload_unknown_device_id": {
|
||||
"message": "Device ID not found"
|
||||
},
|
||||
"supervisor_not_connected": {
|
||||
"message": "Not connected with the supervisor / system too busy"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -31,6 +31,7 @@ from .const import (
|
||||
ATTR_WS_EVENT,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
DOMAIN,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
WS_ID,
|
||||
WS_TYPE,
|
||||
@@ -209,9 +210,13 @@ def websocket_update_config_info(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Send the stored backup config."""
|
||||
connection.send_result(
|
||||
msg["id"], hass.data[DATA_CONFIG_STORE].data.update_config.to_dict()
|
||||
)
|
||||
if (
|
||||
not hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
or (config_store := hass.data.get(DATA_CONFIG_STORE)) is None
|
||||
):
|
||||
connection.send_error(msg["id"], "not_loaded", "Supervisor not loaded")
|
||||
return
|
||||
connection.send_result(msg["id"], config_store.data.update_config.to_dict())
|
||||
|
||||
|
||||
@callback
|
||||
@@ -230,10 +235,14 @@ def websocket_update_config_update(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update the stored backup config."""
|
||||
if (
|
||||
not hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
or (config_store := hass.data.get(DATA_CONFIG_STORE)) is None
|
||||
):
|
||||
connection.send_error(msg["id"], "not_loaded", "Supervisor not loaded")
|
||||
return
|
||||
changes = dict(msg)
|
||||
changes.pop("id")
|
||||
changes.pop("type")
|
||||
hass.data[DATA_CONFIG_STORE].update(
|
||||
update_config=cast(HassioUpdateParametersDict, changes)
|
||||
)
|
||||
config_store.update(update_config=cast(HassioUpdateParametersDict, changes))
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -872,6 +872,10 @@ def supervisor_client() -> Generator[AsyncMock]:
|
||||
"homeassistant.components.hassio.repairs.get_supervisor_client",
|
||||
return_value=supervisor_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.services.get_supervisor_client",
|
||||
return_value=supervisor_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.update_helper.get_supervisor_client",
|
||||
return_value=supervisor_client,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test add-on panel."""
|
||||
|
||||
from http import HTTPStatus
|
||||
import os
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohasupervisor.models import IngressPanel
|
||||
@@ -12,17 +13,15 @@ from homeassistant.setup import async_setup_component
|
||||
from tests.common import MockUser
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_all(
|
||||
supervisor_is_connected: AsyncMock,
|
||||
homeassistant_info: AsyncMock,
|
||||
ingress_panels: AsyncMock,
|
||||
) -> None:
|
||||
def mock_all(all_setup_requests: None) -> None:
|
||||
"""Mock all setup requests."""
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_startup(
|
||||
hass: HomeAssistant, ingress_panels: AsyncMock
|
||||
) -> None:
|
||||
@@ -37,8 +36,9 @@ async def test_hassio_addon_panel_startup(
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_panel._register_panel",
|
||||
) as mock_panel:
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ingress_panels.assert_called_once()
|
||||
assert mock_panel.called
|
||||
@@ -49,7 +49,7 @@ async def test_hassio_addon_panel_startup(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_api(
|
||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
|
||||
) -> None:
|
||||
@@ -64,8 +64,9 @@ async def test_hassio_addon_panel_api(
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_panel._register_panel",
|
||||
) as mock_panel:
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ingress_panels.assert_called_once()
|
||||
assert mock_panel.called
|
||||
@@ -91,7 +92,7 @@ async def test_hassio_addon_panel_api(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_api_non_admin(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
@@ -106,8 +107,9 @@ async def test_hassio_addon_panel_api_non_admin(
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_panel._register_panel",
|
||||
) as mock_panel:
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
ingress_panels.assert_called_once()
|
||||
mock_panel.assert_called_once()
|
||||
@@ -127,7 +129,7 @@ async def test_hassio_addon_panel_api_non_admin(
|
||||
mock_panel.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_registration(
|
||||
hass: HomeAssistant, ingress_panels: AsyncMock
|
||||
) -> None:
|
||||
@@ -141,8 +143,9 @@ async def test_hassio_addon_panel_registration(
|
||||
with patch(
|
||||
"homeassistant.components.hassio.addon_panel.frontend.async_register_built_in_panel"
|
||||
) as mock_register:
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify that async_register_built_in_panel was called with correct arguments
|
||||
# for our test addon
|
||||
@@ -157,7 +160,7 @@ async def test_hassio_addon_panel_registration(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_api_delete(
|
||||
hass: HomeAssistant, hass_client: ClientSessionGenerator, ingress_panels: AsyncMock
|
||||
) -> None:
|
||||
@@ -165,8 +168,9 @@ async def test_hassio_addon_panel_api_delete(
|
||||
ingress_panels.return_value = {
|
||||
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
||||
}
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass_client = await hass_client()
|
||||
|
||||
@@ -178,7 +182,7 @@ async def test_hassio_addon_panel_api_delete(
|
||||
mock_remove.assert_called_once_with(hass, "test1")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_hassio_addon_panel_api_delete_non_admin(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
@@ -189,8 +193,9 @@ async def test_hassio_addon_panel_api_delete_non_admin(
|
||||
ingress_panels.return_value = {
|
||||
"test1": IngressPanel(enable=True, title="Test", icon="mdi:test", admin=False),
|
||||
}
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass_admin_user.groups = []
|
||||
hass_client = await hass_client()
|
||||
|
||||
@@ -53,6 +53,7 @@ from homeassistant.components.hassio import (
|
||||
)
|
||||
from homeassistant.components.hassio.config import STORAGE_KEY
|
||||
from homeassistant.components.hassio.const import (
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
HASSIO_MAIN_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
)
|
||||
@@ -60,7 +61,9 @@ from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.homeassistant.const import DATA_STOP_HANDLER
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
@@ -166,11 +169,28 @@ async def test_setup_api_ping(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
assert get_core_info(hass)["version_latest"] == "1.0.0"
|
||||
assert is_hassio(hass)
|
||||
|
||||
|
||||
async def test_setup_api_ping_fails(
|
||||
hass: HomeAssistant, supervisor_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test that a failed ping raises ConfigEntryNotReady and retries."""
|
||||
supervisor_client.supervisor.ping.side_effect = SupervisorError
|
||||
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# async_setup succeeds (domain registered), but the config entry is in retry
|
||||
assert result
|
||||
assert is_hassio(hass)
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_app_panel(hass: HomeAssistant) -> None:
|
||||
"""Test app panel is registered."""
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
@@ -204,7 +224,7 @@ async def test_setup_api_push_api_data(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY)
|
||||
)
|
||||
@@ -220,7 +240,7 @@ async def test_setup_api_push_api_data_error(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
assert "Failed to update Home Assistant options in Supervisor: boom" in caplog.text
|
||||
|
||||
|
||||
@@ -237,7 +257,7 @@ async def test_setup_api_push_api_data_server_host(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=9999, refresh_token=ANY, watchdog=False)
|
||||
)
|
||||
@@ -255,7 +275,7 @@ async def test_setup_api_push_api_data_default(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=8123, refresh_token=ANY)
|
||||
)
|
||||
@@ -332,7 +352,7 @@ async def test_setup_api_existing_hassio_user(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.homeassistant.set_options.assert_called_once_with(
|
||||
HomeAssistantOptions(ssl=False, port=8123, refresh_token=token.token)
|
||||
)
|
||||
@@ -349,7 +369,7 @@ async def test_setup_core_push_config(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
supervisor_client.supervisor.set_options.assert_called_once_with(
|
||||
SupervisorOptions(timezone="testzone")
|
||||
)
|
||||
@@ -374,7 +394,7 @@ async def test_setup_core_push_config_error(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
assert "Failed to update Supervisor options: boom" in caplog.text
|
||||
|
||||
|
||||
@@ -390,7 +410,7 @@ async def test_setup_hassio_no_additional_data(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
|
||||
|
||||
async def test_fail_setup_without_environ_var(hass: HomeAssistant) -> None:
|
||||
@@ -402,17 +422,17 @@ async def test_fail_setup_without_environ_var(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_warn_when_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
) -> None:
|
||||
"""Fail warn when we cannot connect."""
|
||||
"""Test that a failed ping puts the config entry in retry state."""
|
||||
supervisor_is_connected.side_effect = SupervisorError
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
assert is_hassio(hass)
|
||||
assert "Not connected with the supervisor / system too busy!" in caplog.text
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@@ -607,11 +627,8 @@ async def test_service_calls(
|
||||
"app_or_addon",
|
||||
["app", "addon"],
|
||||
)
|
||||
async def test_invalid_service_calls(
|
||||
hass: HomeAssistant, supervisor_is_connected: AsyncMock, app_or_addon: str
|
||||
) -> None:
|
||||
async def test_invalid_service_calls(hass: HomeAssistant, app_or_addon: str) -> None:
|
||||
"""Call service with invalid input and check that it raises."""
|
||||
supervisor_is_connected.side_effect = SupervisorError
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
@@ -702,25 +719,23 @@ async def test_addon_service_call_with_complex_slug(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_service_calls_core(
|
||||
hass: HomeAssistant, supervisor_client: AsyncMock
|
||||
) -> None:
|
||||
"""Call core service and check the API calls behind that."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
|
||||
await hass.services.async_call("homeassistant", "stop")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
supervisor_client.homeassistant.stop.assert_called_once_with()
|
||||
assert len(supervisor_client.mock_calls) == 21
|
||||
|
||||
await hass.services.async_call("homeassistant", "check_config")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(supervisor_client.mock_calls) == 21
|
||||
|
||||
with patch(
|
||||
"homeassistant.config.async_check_ha_config_file", return_value=None
|
||||
) as mock_check_config:
|
||||
@@ -729,7 +744,6 @@ async def test_service_calls_core(
|
||||
assert mock_check_config.called
|
||||
|
||||
supervisor_client.homeassistant.restart.assert_called_once_with()
|
||||
assert len(supervisor_client.mock_calls) == 22
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1046,7 +1060,7 @@ async def test_setup_hardware_integration(
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert result
|
||||
assert len(supervisor_client.mock_calls) == 25
|
||||
assert len(supervisor_client.mock_calls) == 17
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -1729,3 +1743,44 @@ async def test_get_core_info(hass: HomeAssistant) -> None:
|
||||
assert result["version"] == "1.0.0"
|
||||
assert result["version_latest"] == "1.0.0"
|
||||
assert result["image"] == "homeassistant"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("all_setup_requests")
|
||||
async def test_stop_handler_restored_on_unload(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the default stop handler is restored when the hassio entry unloads."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hassio_stop_handler = hass.data[DATA_STOP_HANDLER]
|
||||
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
# After unload the hassio handler must no longer be registered.
|
||||
assert hass.data.get(DATA_STOP_HANDLER) is not hassio_stop_handler
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_supervisor_issues_not_set_on_coordinator_failure(
|
||||
hass: HomeAssistant,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
supervisor_root_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test DATA_KEY_SUPERVISOR_ISSUES is not populated when coordinator fails.
|
||||
|
||||
If a coordinator first-refresh raises ConfigEntryNotReady the issues
|
||||
listener must not be registered, preventing accumulation across retries.
|
||||
"""
|
||||
supervisor_root_info.side_effect = SupervisorError()
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert DATA_KEY_SUPERVISOR_ISSUES not in hass.data
|
||||
|
||||
@@ -27,8 +27,11 @@ from aiohasupervisor.models import (
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio.const import DATA_HOST_INFO
|
||||
from homeassistant.components.hassio.issues import SupervisorIssues
|
||||
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .test_init import MOCK_ENVIRON
|
||||
@@ -1109,11 +1112,13 @@ async def test_supervisor_issues_free_space_host_info_fail(
|
||||
) -> None:
|
||||
"""Test supervisor issue for too little free space remaining without host info."""
|
||||
mock_resolution_info(supervisor_client)
|
||||
host_info.side_effect = SupervisorError()
|
||||
|
||||
result = await async_setup_component(hass, "hassio", {})
|
||||
assert result
|
||||
|
||||
# Simulate host info being unavailable after setup (e.g., cached data cleared)
|
||||
hass.data.pop(DATA_HOST_INFO, None)
|
||||
|
||||
client = await hass_supervisor_ws_client()
|
||||
|
||||
await client.send_json(
|
||||
@@ -1206,3 +1211,47 @@ async def test_supervisor_issues_addon_pwned(
|
||||
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def test_supervisor_issues_unload_disconnects_listener(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
resolution_info: AsyncMock,
|
||||
) -> None:
|
||||
"""Test SupervisorIssues.unload() disconnects the EVENT_SUPERVISOR_EVENT listener.
|
||||
|
||||
After calling unload(), dispatching supervisor events must not trigger
|
||||
the listener — preventing listener accumulation on config-entry reload.
|
||||
"""
|
||||
mock_resolution_info(supervisor_client)
|
||||
issues = SupervisorIssues(hass)
|
||||
await issues.setup()
|
||||
|
||||
# While connected, a health_changed event updates unhealthy_reasons.
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
"supervisor_event",
|
||||
{
|
||||
"event": "health_changed",
|
||||
"data": {"healthy": False, "unhealthy_reasons": ["docker"]},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert "docker" in issues.unhealthy_reasons
|
||||
|
||||
# After unload(), the same event is silently ignored.
|
||||
issues.unload()
|
||||
async_dispatcher_send(
|
||||
hass,
|
||||
"supervisor_event",
|
||||
{
|
||||
"event": "health_changed",
|
||||
"data": {"healthy": True},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
# unhealthy_reasons unchanged — listener did not fire.
|
||||
assert "docker" in issues.unhealthy_reasons
|
||||
|
||||
# Calling unload() again is safe (idempotent).
|
||||
issues.unload()
|
||||
|
||||
@@ -27,6 +27,7 @@ from homeassistant.components.hassio.const import (
|
||||
WS_TYPE_API,
|
||||
WS_TYPE_SUBSCRIBE,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@@ -986,3 +987,31 @@ async def test_read_update_config(
|
||||
|
||||
await websocket_client.send_json_auto_id({"type": "hassio/update/config/info"})
|
||||
assert await websocket_client.receive_json() == snapshot
|
||||
|
||||
|
||||
async def test_update_config_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test update config commands return not_loaded when entry is in SETUP_RETRY."""
|
||||
supervisor_client.supervisor.ping.side_effect = SupervisorError
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
await async_setup_component(hass, "hassio", {})
|
||||
|
||||
entry = hass.config_entries.async_entries("hassio")[0]
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
websocket_client = await hass_ws_client(hass)
|
||||
|
||||
await websocket_client.send_json_auto_id({"type": "hassio/update/config/info"})
|
||||
result = await websocket_client.receive_json()
|
||||
assert not result["success"]
|
||||
assert result["error"]["code"] == "not_loaded"
|
||||
|
||||
await websocket_client.send_json_auto_id(
|
||||
{"type": "hassio/update/config/update", "add_on_backup_before_update": True}
|
||||
)
|
||||
result = await websocket_client.receive_json()
|
||||
assert not result["success"]
|
||||
assert result["error"]["code"] == "not_loaded"
|
||||
|
||||
Reference in New Issue
Block a user