mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
Split hassio data coordinator for add-ons
Use a separate data coordinator to update add-ons independently from the main update components (Core, Supervisor, OS, Host, Mounts). The main HassioDataUpdateCoordinator keeps the 5-minute interval for Core, Supervisor, and Operating System updates. The new HassioAddOnDataUpdateCoordinator uses a 15-minute interval for add-on store updates, reducing unnecessary API calls. This avoids force refreshing the main update components on add-on update, which was often the cause of spurious "Supervisor needs update" errors while updating add-ons. The main coordinator now uses reload_updates() while the add-on coordinator uses store.reload(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,7 @@ from .config import HassioConfig
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_REPOSITORIES,
|
||||
COORDINATOR,
|
||||
DATA_ADDONS_LIST,
|
||||
DATA_COMPONENT,
|
||||
DATA_CONFIG_STORE,
|
||||
@@ -94,6 +95,7 @@ from .const import (
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioAddOnDataUpdateCoordinator,
|
||||
HassioDataUpdateCoordinator,
|
||||
get_addons_info,
|
||||
get_addons_list,
|
||||
@@ -462,9 +464,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a config entry."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
hass.data[COORDINATOR] = coordinator
|
||||
|
||||
addon_coordinator = HassioAddOnDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
addon_coordinator.set_jobs(coordinator.jobs)
|
||||
await addon_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = addon_coordinator
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
@@ -531,10 +539,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
# Unload coordinator
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
|
||||
coordinator.unload()
|
||||
|
||||
# Pop coordinator
|
||||
# Pop coordinators
|
||||
hass.data.pop(COORDINATOR, None)
|
||||
hass.data.pop(ADDONS_COORDINATOR, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_STARTED,
|
||||
ATTR_STATE,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_MOUNTS,
|
||||
)
|
||||
@@ -60,17 +61,18 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Binary sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
|
||||
async_add_entities(
|
||||
itertools.chain(
|
||||
[
|
||||
HassioAddonBinarySensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
],
|
||||
[
|
||||
|
||||
@@ -77,6 +77,7 @@ EVENT_JOB = "job"
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
STARTUP_COMPLETE = "complete"
|
||||
|
||||
COORDINATOR = "hassio_coordinator"
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
|
||||
|
||||
@@ -95,6 +96,7 @@ DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_ADDONS_LIST = "hassio_addons_list"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
HASSIO_ADDON_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import defaultdict
|
||||
from collections.abc import Awaitable
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
|
||||
from aiohasupervisor.models import (
|
||||
@@ -59,6 +59,7 @@ from .const import (
|
||||
DATA_SUPERVISOR_INFO,
|
||||
DATA_SUPERVISOR_STATS,
|
||||
DOMAIN,
|
||||
HASSIO_ADDON_UPDATE_INTERVAL,
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
SUPERVISOR_CONTAINER,
|
||||
@@ -318,8 +319,8 @@ def async_remove_devices_from_dev_reg(
|
||||
dev_reg.async_remove_device(dev.id)
|
||||
|
||||
|
||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io status."""
|
||||
class HassioAddOnDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io Add-on status."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
@@ -332,7 +333,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
update_interval=HASSIO_ADDON_UPDATE_INTERVAL,
|
||||
# We don't want an immediate refresh since we want to avoid
|
||||
# fetching the container stats right away and avoid hammering
|
||||
# the Supervisor API on startup
|
||||
@@ -344,12 +345,15 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
self.data = {}
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
|
||||
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
|
||||
lambda: defaultdict(set)
|
||||
)
|
||||
self.supervisor_client = get_supervisor_client(hass)
|
||||
self.jobs = SupervisorJobs(hass)
|
||||
self.jobs: SupervisorJobs = None # type: ignore[assignment]
|
||||
|
||||
def set_jobs(self, jobs: SupervisorJobs) -> None:
|
||||
"""Set the shared jobs instance."""
|
||||
self.jobs = jobs
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
@@ -361,11 +365,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
new_data: dict[str, Any] = {}
|
||||
supervisor_info = get_supervisor_info(self.hass) or {}
|
||||
addons_info = get_addons_info(self.hass) or {}
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
mounts_info = await self.supervisor_client.mounts.info()
|
||||
addons_list = get_addons_list(self.hass) or []
|
||||
|
||||
if store_data:
|
||||
@@ -389,39 +391,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
}
|
||||
for addon in addons_list
|
||||
}
|
||||
if self.is_hass_os:
|
||||
new_data[DATA_KEY_OS] = get_os_info(self.hass)
|
||||
|
||||
new_data[DATA_KEY_CORE] = {
|
||||
**(get_core_info(self.hass) or {}),
|
||||
**get_core_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_SUPERVISOR] = {
|
||||
**supervisor_info,
|
||||
**get_supervisor_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
|
||||
new_data[DATA_KEY_MOUNTS] = {mount.name: mount for mount in mounts_info.mounts}
|
||||
|
||||
# If this is the initial refresh, register all addons and return the dict
|
||||
if is_first_update:
|
||||
async_register_addons_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
async_register_mounts_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_MOUNTS].values()
|
||||
)
|
||||
async_register_core_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE]
|
||||
)
|
||||
async_register_supervisor_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR]
|
||||
)
|
||||
async_register_host_in_dev_reg(self.entry_id, self.dev_reg)
|
||||
if self.is_hass_os:
|
||||
async_register_os_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
|
||||
)
|
||||
|
||||
# Remove add-ons that are no longer installed from device registry
|
||||
supervisor_addon_devices = {
|
||||
@@ -434,31 +409,11 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
|
||||
async_remove_devices_from_dev_reg(self.dev_reg, stale_addons)
|
||||
|
||||
# Remove mounts that no longer exists from device registry
|
||||
supervisor_mount_devices = {
|
||||
device.name
|
||||
for device in self.dev_reg.devices.get_devices_for_config_entry_id(
|
||||
self.entry_id
|
||||
)
|
||||
if device.model == SupervisorEntityModel.MOUNT
|
||||
}
|
||||
if stale_mounts := supervisor_mount_devices - set(new_data[DATA_KEY_MOUNTS]):
|
||||
async_remove_devices_from_dev_reg(
|
||||
self.dev_reg, {f"mount_{stale_mount}" for stale_mount in stale_mounts}
|
||||
)
|
||||
|
||||
if not self.is_hass_os and (
|
||||
dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")})
|
||||
):
|
||||
# Remove the OS device if it exists and the installation is not hassos
|
||||
self.dev_reg.async_remove_device(dev.id)
|
||||
|
||||
# If there are new add-ons or mounts, we should reload the config entry so we can
|
||||
# If there are new add-ons, we should reload the config entry so we can
|
||||
# create new devices and entities. We can return an empty dict because
|
||||
# coordinator will be recreated.
|
||||
if self.data and (
|
||||
set(new_data[DATA_KEY_ADDONS]) - set(self.data[DATA_KEY_ADDONS])
|
||||
or set(new_data[DATA_KEY_MOUNTS]) - set(self.data[DATA_KEY_MOUNTS])
|
||||
):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.entry_id)
|
||||
@@ -481,36 +436,13 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
data = self.hass.data
|
||||
client = self.supervisor_client
|
||||
|
||||
updates: dict[str, Awaitable[ResponseData]] = {
|
||||
DATA_INFO: client.info(),
|
||||
DATA_CORE_INFO: client.homeassistant.info(),
|
||||
DATA_SUPERVISOR_INFO: client.supervisor.info(),
|
||||
DATA_OS_INFO: client.os.info(),
|
||||
DATA_STORE: client.store.info(),
|
||||
}
|
||||
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
|
||||
updates[DATA_CORE_STATS] = client.homeassistant.stats()
|
||||
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
|
||||
updates[DATA_SUPERVISOR_STATS] = client.supervisor.stats()
|
||||
|
||||
# Pull off addons.list results for further processing before caching
|
||||
addons_list, *results = await asyncio.gather(
|
||||
client.addons.list(), *updates.values()
|
||||
)
|
||||
for key, result in zip(updates, cast(list[ResponseData], results), strict=True):
|
||||
data[key] = result.to_dict()
|
||||
|
||||
installed_addons = cast(list[InstalledAddon], addons_list)
|
||||
installed_addons: list[InstalledAddon] = await client.addons.list()
|
||||
data[DATA_ADDONS_LIST] = [addon.to_dict() for addon in installed_addons]
|
||||
|
||||
# Deprecated 2026.4.0: Folding repositories and addons.list results into supervisor_info for compatibility
|
||||
# Deprecated 2026.4.0: Folding addons.list results into supervisor_info for compatibility
|
||||
# Can drop this after removal period
|
||||
data[DATA_SUPERVISOR_INFO].update(
|
||||
{
|
||||
"repositories": data[DATA_STORE][ATTR_REPOSITORIES],
|
||||
"addons": [addon.to_dict() for addon in installed_addons],
|
||||
}
|
||||
)
|
||||
if DATA_SUPERVISOR_INFO in data:
|
||||
data[DATA_SUPERVISOR_INFO]["addons"] = data[DATA_ADDONS_LIST]
|
||||
|
||||
all_addons = {addon.slug for addon in installed_addons}
|
||||
started_addons = {
|
||||
@@ -566,9 +498,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
)
|
||||
)
|
||||
|
||||
# Refresh jobs data
|
||||
await self.jobs.refresh_data(first_update)
|
||||
|
||||
async def _update_addon_stats(self, slug: str) -> tuple[str, dict[str, Any] | None]:
|
||||
"""Update single addon stats."""
|
||||
try:
|
||||
@@ -616,14 +545,16 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force refreshing updates for non-scheduled updates
|
||||
# Force reloading add-on updates for non-scheduled
|
||||
# updates.
|
||||
#
|
||||
# If `raise_on_auth_failed` is set, it means this is
|
||||
# the first refresh and we do not want to delay
|
||||
# startup or cause a timeout so we only refresh the
|
||||
# updates if this is not a scheduled refresh and
|
||||
# we are not doing the first refresh.
|
||||
try:
|
||||
await self.supervisor_client.refresh_updates()
|
||||
await self.supervisor_client.store.reload()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
@@ -643,6 +574,188 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Could not refresh info for %s: %s", addon_slug, err)
|
||||
|
||||
|
||||
class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to retrieve Hass.io status."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
# We don't want an immediate refresh since we want to avoid
|
||||
# fetching the container stats right away and avoid hammering
|
||||
# the Supervisor API on startup
|
||||
request_refresh_debouncer=Debouncer(
|
||||
hass, _LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=False
|
||||
),
|
||||
)
|
||||
self.hassio = hass.data[DATA_COMPONENT]
|
||||
self.data = {}
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
|
||||
self._container_updates: defaultdict[str, dict[str, set[str]]] = defaultdict(
|
||||
lambda: defaultdict(set)
|
||||
)
|
||||
self.supervisor_client = get_supervisor_client(hass)
|
||||
self.jobs = SupervisorJobs(hass)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
is_first_update = not self.data
|
||||
|
||||
try:
|
||||
await self.force_data_refresh(is_first_update)
|
||||
except SupervisorError as err:
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
new_data: dict[str, Any] = {}
|
||||
supervisor_info = get_supervisor_info(self.hass) or {}
|
||||
mounts_info = await self.supervisor_client.mounts.info()
|
||||
|
||||
if self.is_hass_os:
|
||||
new_data[DATA_KEY_OS] = get_os_info(self.hass)
|
||||
|
||||
new_data[DATA_KEY_CORE] = {
|
||||
**(get_core_info(self.hass) or {}),
|
||||
**get_core_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_SUPERVISOR] = {
|
||||
**supervisor_info,
|
||||
**get_supervisor_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
|
||||
new_data[DATA_KEY_MOUNTS] = {mount.name: mount for mount in mounts_info.mounts}
|
||||
|
||||
# If this is the initial refresh, register all main components
|
||||
if is_first_update:
|
||||
async_register_mounts_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_MOUNTS].values()
|
||||
)
|
||||
async_register_core_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_CORE]
|
||||
)
|
||||
async_register_supervisor_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_SUPERVISOR]
|
||||
)
|
||||
async_register_host_in_dev_reg(self.entry_id, self.dev_reg)
|
||||
if self.is_hass_os:
|
||||
async_register_os_in_dev_reg(
|
||||
self.entry_id, self.dev_reg, new_data[DATA_KEY_OS]
|
||||
)
|
||||
|
||||
# Remove mounts that no longer exists from device registry
|
||||
supervisor_mount_devices = {
|
||||
device.name
|
||||
for device in self.dev_reg.devices.get_devices_for_config_entry_id(
|
||||
self.entry_id
|
||||
)
|
||||
if device.model == SupervisorEntityModel.MOUNT
|
||||
}
|
||||
if stale_mounts := supervisor_mount_devices - set(new_data[DATA_KEY_MOUNTS]):
|
||||
async_remove_devices_from_dev_reg(
|
||||
self.dev_reg, {f"mount_{stale_mount}" for stale_mount in stale_mounts}
|
||||
)
|
||||
|
||||
if not self.is_hass_os and (
|
||||
dev := self.dev_reg.async_get_device(identifiers={(DOMAIN, "OS")})
|
||||
):
|
||||
# Remove the OS device if it exists and the installation is not hassos
|
||||
self.dev_reg.async_remove_device(dev.id)
|
||||
|
||||
# If there are new mounts, we should reload the config entry so we can
|
||||
# create new devices and entities. We can return an empty dict because
|
||||
# coordinator will be recreated.
|
||||
if self.data and (
|
||||
set(new_data[DATA_KEY_MOUNTS]) - set(self.data.get(DATA_KEY_MOUNTS, {}))
|
||||
):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.entry_id)
|
||||
)
|
||||
return {}
|
||||
|
||||
return new_data
|
||||
|
||||
async def force_data_refresh(self, first_update: bool) -> None:
|
||||
"""Force update of the main component info."""
|
||||
container_updates = self._container_updates
|
||||
|
||||
data = self.hass.data
|
||||
client = self.supervisor_client
|
||||
|
||||
updates: dict[str, Awaitable[ResponseData]] = {
|
||||
DATA_INFO: client.info(),
|
||||
DATA_CORE_INFO: client.homeassistant.info(),
|
||||
DATA_SUPERVISOR_INFO: client.supervisor.info(),
|
||||
DATA_OS_INFO: client.os.info(),
|
||||
DATA_STORE: client.store.info(),
|
||||
}
|
||||
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
|
||||
updates[DATA_CORE_STATS] = client.homeassistant.stats()
|
||||
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
|
||||
updates[DATA_SUPERVISOR_STATS] = client.supervisor.stats()
|
||||
|
||||
api_results: list[ResponseData] = await asyncio.gather(*updates.values())
|
||||
for key, result in zip(updates, api_results, strict=True):
|
||||
data[key] = result.to_dict()
|
||||
|
||||
# Deprecated 2026.4.0: Folding repositories into supervisor_info for compatibility
|
||||
# Can drop this after removal period
|
||||
data[DATA_SUPERVISOR_INFO]["repositories"] = data[DATA_STORE][ATTR_REPOSITORIES]
|
||||
|
||||
# Refresh jobs data
|
||||
await self.jobs.refresh_data(first_update)
|
||||
|
||||
@callback
|
||||
def async_enable_container_updates(
|
||||
self, slug: str, entity_id: str, types: set[str]
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Enable updates for an add-on."""
|
||||
enabled_updates = self._container_updates[slug]
|
||||
for key in types:
|
||||
enabled_updates[key].add(entity_id)
|
||||
|
||||
@callback
|
||||
def _remove() -> None:
|
||||
for key in types:
|
||||
enabled_updates[key].remove(entity_id)
|
||||
|
||||
return _remove
|
||||
|
||||
async def _async_refresh(
|
||||
self,
|
||||
log_failures: bool = True,
|
||||
raise_on_auth_failed: bool = False,
|
||||
scheduled: bool = False,
|
||||
raise_on_entry_error: bool = False,
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled and not raise_on_auth_failed:
|
||||
# Force reloading updates of main components for
|
||||
# non-scheduled updates.
|
||||
#
|
||||
# If `raise_on_auth_failed` is set, it means this is
|
||||
# the first refresh and we do not want to delay
|
||||
# startup or cause a timeout so we only refresh the
|
||||
# updates if this is not a scheduled refresh and
|
||||
# we are not doing the first refresh.
|
||||
try:
|
||||
await self.supervisor_client.reload_updates()
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
|
||||
await super()._async_refresh(
|
||||
log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error
|
||||
)
|
||||
|
||||
@callback
|
||||
def unload(self) -> None:
|
||||
"""Clean up when config entry unloaded."""
|
||||
|
||||
@@ -11,8 +11,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import ADDONS_COORDINATOR
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .const import ADDONS_COORDINATOR, COORDINATOR
|
||||
from .coordinator import HassioAddOnDataUpdateCoordinator, HassioDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -20,7 +20,8 @@ async def async_get_config_entry_diagnostics(
|
||||
config_entry: ConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator: HassioDataUpdateCoordinator = hass.data[COORDINATOR]
|
||||
addons_coordinator: HassioAddOnDataUpdateCoordinator = hass.data[ADDONS_COORDINATOR]
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -53,5 +54,6 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
return {
|
||||
"coordinator_data": coordinator.data,
|
||||
"addons_coordinator_data": addons_coordinator.data,
|
||||
"devices": devices,
|
||||
}
|
||||
|
||||
@@ -24,17 +24,17 @@ from .const import (
|
||||
KEY_TO_UPDATE_TYPES,
|
||||
SUPERVISOR_CONTAINER,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator
|
||||
from .coordinator import HassioAddOnDataUpdateCoordinator, HassioDataUpdateCoordinator
|
||||
|
||||
|
||||
class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||
class HassioAddonEntity(CoordinatorEntity[HassioAddOnDataUpdateCoordinator]):
|
||||
"""Base entity for a Hass.io add-on."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HassioDataUpdateCoordinator,
|
||||
coordinator: HassioAddOnDataUpdateCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
addon: dict[str, Any],
|
||||
) -> None:
|
||||
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_SLUG,
|
||||
@@ -38,6 +37,7 @@ from .const import (
|
||||
ATTR_UNSUPPORTED_REASONS,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
COORDINATOR,
|
||||
DOMAIN,
|
||||
EVENT_HEALTH_CHANGED,
|
||||
EVENT_ISSUE_CHANGED,
|
||||
@@ -418,7 +418,7 @@ class SupervisorIssues:
|
||||
def _async_coordinator_refresh(self) -> None:
|
||||
"""Refresh coordinator to update latest data in entities."""
|
||||
coordinator: HassioDataUpdateCoordinator | None
|
||||
if coordinator := self._hass.data.get(ADDONS_COORDINATOR):
|
||||
if coordinator := self._hass.data.get(COORDINATOR):
|
||||
coordinator.config_entry.async_create_task(
|
||||
self._hass, coordinator.async_refresh()
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ from .const import (
|
||||
ATTR_MEMORY_PERCENT,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_CORE,
|
||||
DATA_KEY_HOST,
|
||||
@@ -114,20 +115,21 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Sensor set up for Hass.io config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
|
||||
entities: list[
|
||||
HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor
|
||||
] = [
|
||||
HassioAddonSensor(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=entity_description,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
for entity_description in ADDON_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
entities.extend(
|
||||
CoreSensor(
|
||||
coordinator=coordinator,
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
COORDINATOR,
|
||||
ATTR_ADDON,
|
||||
ATTR_ADDONS,
|
||||
ATTR_APP,
|
||||
@@ -417,7 +417,7 @@ def async_register_network_storage_services(
|
||||
if (
|
||||
device.name is None
|
||||
or device.model != SupervisorEntityModel.MOUNT
|
||||
or (coordinator := hass.data.get(ADDONS_COORDINATOR)) is None
|
||||
or (coordinator := hass.data.get(COORDINATOR)) is None
|
||||
or coordinator.entry_id not in device.config_entries
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
|
||||
@@ -25,6 +25,7 @@ from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
COORDINATOR,
|
||||
DATA_KEY_ADDONS,
|
||||
DATA_KEY_CORE,
|
||||
DATA_KEY_OS,
|
||||
@@ -51,9 +52,9 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Supervisor update based on a config entry."""
|
||||
coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
coordinator = hass.data[COORDINATOR]
|
||||
|
||||
entities = [
|
||||
entities: list[UpdateEntity] = [
|
||||
SupervisorSupervisorUpdateEntity(
|
||||
coordinator=coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
@@ -64,15 +65,6 @@ async def async_setup_entry(
|
||||
),
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
SupervisorAddonUpdateEntity(
|
||||
addon=addon,
|
||||
coordinator=coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
)
|
||||
for addon in coordinator.data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
|
||||
if coordinator.is_hass_os:
|
||||
entities.append(
|
||||
SupervisorOSUpdateEntity(
|
||||
@@ -81,6 +73,16 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
addons_coordinator = hass.data[ADDONS_COORDINATOR]
|
||||
entities.extend(
|
||||
SupervisorAddonUpdateEntity(
|
||||
addon=addon,
|
||||
coordinator=addons_coordinator,
|
||||
entity_description=ENTITY_DESCRIPTION,
|
||||
)
|
||||
for addon in addons_coordinator.data[DATA_KEY_ADDONS].values()
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -107,10 +107,10 @@ async def test_diagnostics(
|
||||
hass, hass_client, config_entry
|
||||
)
|
||||
|
||||
assert "addons" in diagnostics["coordinator_data"]
|
||||
assert "core" in diagnostics["coordinator_data"]
|
||||
assert "supervisor" in diagnostics["coordinator_data"]
|
||||
assert "os" in diagnostics["coordinator_data"]
|
||||
assert "host" in diagnostics["coordinator_data"]
|
||||
assert "addons" in diagnostics["addons_coordinator_data"]
|
||||
|
||||
assert len(diagnostics["devices"]) == 6
|
||||
|
||||
@@ -732,12 +732,12 @@ async def test_service_calls_core(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
supervisor_client.homeassistant.stop.assert_called_once_with()
|
||||
assert len(supervisor_client.mock_calls) == 20
|
||||
assert len(supervisor_client.mock_calls) == 19
|
||||
|
||||
await hass.services.async_call("homeassistant", "check_config")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(supervisor_client.mock_calls) == 20
|
||||
assert len(supervisor_client.mock_calls) == 19
|
||||
|
||||
with patch(
|
||||
"homeassistant.config.async_check_ha_config_file", return_value=None
|
||||
@@ -747,7 +747,7 @@ 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) == 21
|
||||
assert len(supervisor_client.mock_calls) == 20
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -903,13 +903,13 @@ async def test_coordinator_updates(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Initial refresh, no update refresh call
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Scheduled refresh, no update refresh call
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
await hass.services.async_call(
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
@@ -924,15 +924,15 @@ async def test_coordinator_updates(
|
||||
)
|
||||
|
||||
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
async_fire_time_changed(
|
||||
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
|
||||
)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
supervisor_client.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_called_once()
|
||||
|
||||
supervisor_client.refresh_updates.reset_mock()
|
||||
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
|
||||
supervisor_client.reload_updates.reset_mock()
|
||||
supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
|
||||
await hass.services.async_call(
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
@@ -949,7 +949,7 @@ async def test_coordinator_updates(
|
||||
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
supervisor_client.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_called_once()
|
||||
assert "Error on Supervisor API: Unknown" in caplog.text
|
||||
|
||||
|
||||
@@ -967,7 +967,7 @@ async def test_coordinator_updates_stats_entities_enabled(
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
# Initial refresh without stats
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
# Refresh with stats once we know which ones are needed
|
||||
async_fire_time_changed(
|
||||
@@ -975,12 +975,12 @@ async def test_coordinator_updates_stats_entities_enabled(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
supervisor_client.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_called_once()
|
||||
|
||||
supervisor_client.refresh_updates.reset_mock()
|
||||
supervisor_client.reload_updates.reset_mock()
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
|
||||
await hass.async_block_till_done()
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
await hass.services.async_call(
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
@@ -993,7 +993,7 @@ async def test_coordinator_updates_stats_entities_enabled(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
supervisor_client.refresh_updates.assert_not_called()
|
||||
supervisor_client.reload_updates.assert_not_called()
|
||||
|
||||
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
|
||||
async_fire_time_changed(
|
||||
@@ -1001,8 +1001,8 @@ async def test_coordinator_updates_stats_entities_enabled(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
supervisor_client.refresh_updates.reset_mock()
|
||||
supervisor_client.refresh_updates.side_effect = SupervisorError("Unknown")
|
||||
supervisor_client.reload_updates.reset_mock()
|
||||
supervisor_client.reload_updates.side_effect = SupervisorError("Unknown")
|
||||
await hass.services.async_call(
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
@@ -1019,7 +1019,7 @@ async def test_coordinator_updates_stats_entities_enabled(
|
||||
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
supervisor_client.refresh_updates.assert_called_once()
|
||||
supervisor_client.reload_updates.assert_called_once()
|
||||
assert "Error on Supervisor API: Unknown" in caplog.text
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hassio import DOMAIN, HASSIO_UPDATE_INTERVAL
|
||||
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
|
||||
from homeassistant.components.hassio import DOMAIN
|
||||
from homeassistant.components.hassio.const import (
|
||||
HASSIO_ADDON_UPDATE_INTERVAL,
|
||||
REQUEST_REFRESH_DELAY,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -176,14 +179,14 @@ async def test_stats_addon_sensor(
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
addon_stats.side_effect = SupervisorError
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_ADDON_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert "Could not fetch stats" not in caplog.text
|
||||
|
||||
addon_stats.side_effect = None
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_ADDON_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
@@ -199,13 +202,13 @@ async def test_stats_addon_sensor(
|
||||
assert entity_registry.async_get(entity_id).disabled_by is None
|
||||
|
||||
# The config entry just reloaded, so we need to wait for the next update
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_ADDON_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.get(entity_id) is not None
|
||||
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_ADDON_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
# Verify that the entity have the expected state.
|
||||
@@ -213,7 +216,7 @@ async def test_stats_addon_sensor(
|
||||
assert state.state == expected
|
||||
|
||||
addon_stats.side_effect = SupervisorError
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
freezer.tick(HASSIO_ADDON_UPDATE_INTERVAL + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user