diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e6ff9888b15..92999fc8f8a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -63,8 +63,6 @@ from .const import ( ATTR_PASSWORD, ATTR_REPOSITORY, ATTR_SLUG, - ATTR_STARTED, - ATTR_STATE, ATTR_URL, ATTR_VERSION, DATA_KEY_ADDONS, @@ -718,7 +716,7 @@ def async_register_supervisor_in_dev_reg( params = DeviceInfo( identifiers={(DOMAIN, "supervisor")}, manufacturer="Home Assistant", - model=SupervisorEntityModel.SUPERVIOSR, + model=SupervisorEntityModel.SUPERVISOR, sw_version=supervisor_dict[ATTR_VERSION], name="Home Assistant Supervisor", entry_type=dr.DeviceEntryType.SERVICE, @@ -765,7 +763,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): new_data: dict[str, Any] = {} supervisor_info = get_supervisor_info(self.hass) or {} addons_info = get_addons_info(self.hass) - addons_stats = get_addons_stats(self.hass) addons_changelogs = get_addons_changelogs(self.hass) store_data = get_store(self.hass) or {} @@ -777,7 +774,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): new_data[DATA_KEY_ADDONS] = { addon[ATTR_SLUG]: { **addon, - **((addons_stats or {}).get(addon[ATTR_SLUG]) or {}), ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get( ATTR_AUTO_UPDATE, False ), @@ -793,11 +789,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): 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 {} @@ -857,27 +851,16 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): ( self.hass.data[DATA_INFO], self.hass.data[DATA_CORE_INFO], - self.hass.data[DATA_CORE_STATS], self.hass.data[DATA_SUPERVISOR_INFO], - self.hass.data[DATA_SUPERVISOR_STATS], self.hass.data[DATA_OS_INFO], ) = await asyncio.gather( self.hassio.get_info(), self.hassio.get_core_info(), - self.hassio.get_core_stats(), self.hassio.get_supervisor_info(), - self.hassio.get_supervisor_stats(), self.hassio.get_os_info(), ) all_addons = self.hass.data[DATA_SUPERVISOR_INFO].get("addons", []) - started_addons = [ - addon for addon in all_addons if addon[ATTR_STATE] == ATTR_STARTED - ] - stats_data = await asyncio.gather( - *[self._update_addon_stats(addon[ATTR_SLUG]) for addon in started_addons] - ) - self.hass.data[DATA_ADDONS_STATS] = dict(stats_data) self.hass.data[DATA_ADDONS_CHANGELOGS] = dict( await asyncio.gather( *[ @@ -892,15 +875,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): ) ) - async def _update_addon_stats(self, slug): - """Update single addon stats.""" - try: - stats = await self.hassio.get_addon_stats(slug) - return (slug, stats) - except HassioAPIError as err: - _LOGGER.warning("Could not fetch stats for %s: %s", slug, err) - return (slug, None) - async def _update_addon_changelog(self, slug): """Return the changelog for an add-on.""" try: @@ -936,3 +910,41 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): await super()._async_refresh( log_failures, raise_on_auth_failed, scheduled, raise_on_entry_error ) + + +class HassioStatsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Custom coordinator for Hass.io stats.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + model: SupervisorEntityModel, + *, + addon_slug: str | None = None, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name="_".join([x for x in (DOMAIN, model, addon_slug) if x]), + update_interval=HASSIO_UPDATE_INTERVAL, + ) + self.hassio: HassIO = hass.data[DOMAIN] + self.model = model + self.addon_slug = addon_slug + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch the latest data from the source.""" + try: + if self.model == SupervisorEntityModel.ADDON: + return await self.hassio.get_addon_stats(self.addon_slug) + + if self.model == SupervisorEntityModel.SUPERVISOR: + return await self.hassio.get_supervisor_stats() + + # SupervisorEntityModel.CORE + return await self.hassio.get_core_stats() + except HassioAPIError as err: + raise UpdateFailed(f"Error communicating with Supervisor: {err}") from err diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index cc9c58a3d27..f17e1052da8 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -77,5 +77,5 @@ class SupervisorEntityModel(str, Enum): ADDON = "Home Assistant Add-on" OS = "Home Assistant Operating System" CORE = "Home Assistant Core" - SUPERVIOSR = "Home Assistant Supervisor" + SUPERVISOR = "Home Assistant Supervisor" HOST = "Home Assistant Host" diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index 3a6a5a9f7c3..91b39eff4be 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN, HassioDataUpdateCoordinator +from . import DOMAIN, HassioDataUpdateCoordinator, HassioStatsDataUpdateCoordinator from .const import ( ATTR_SLUG, DATA_KEY_ADDONS, @@ -17,14 +17,16 @@ from .const import ( ) -class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): +class HassioAddonEntity( + CoordinatorEntity[HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator] +): """Base entity for a Hass.io add-on.""" _attr_has_entity_name = True def __init__( self, - coordinator: HassioDataUpdateCoordinator, + coordinator: HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator, entity_description: EntityDescription, addon: dict[str, Any], ) -> None: @@ -46,14 +48,16 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): ) -class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): +class HassioOSEntity( + CoordinatorEntity[HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator] +): """Base Entity for Hass.io OS.""" _attr_has_entity_name = True def __init__( self, - coordinator: HassioDataUpdateCoordinator, + coordinator: HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator, entity_description: EntityDescription, ) -> None: """Initialize base entity.""" @@ -72,14 +76,16 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): ) -class HassioHostEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): +class HassioHostEntity( + CoordinatorEntity[HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator] +): """Base Entity for Hass.io host.""" _attr_has_entity_name = True def __init__( self, - coordinator: HassioDataUpdateCoordinator, + coordinator: HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator, entity_description: EntityDescription, ) -> None: """Initialize base entity.""" @@ -98,14 +104,16 @@ class HassioHostEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): ) -class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): +class HassioSupervisorEntity( + CoordinatorEntity[HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator] +): """Base Entity for Supervisor.""" _attr_has_entity_name = True def __init__( self, - coordinator: HassioDataUpdateCoordinator, + coordinator: HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator, entity_description: EntityDescription, ) -> None: """Initialize base entity.""" @@ -125,14 +133,16 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): ) -class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): +class HassioCoreEntity( + CoordinatorEntity[HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator] +): """Base Entity for Core.""" _attr_has_entity_name = True def __init__( self, - coordinator: HassioDataUpdateCoordinator, + coordinator: HassioDataUpdateCoordinator | HassioStatsDataUpdateCoordinator, entity_description: EntityDescription, ) -> None: """Initialize base entity.""" diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index b9a97adcbc2..355807c8f43 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -12,10 +12,11 @@ from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ADDONS_COORDINATOR +from . import ADDONS_COORDINATOR, HassioStatsDataUpdateCoordinator from .const import ( ATTR_CPU_PERCENT, ATTR_MEMORY_PERCENT, + ATTR_SLUG, ATTR_VERSION, ATTR_VERSION_LATEST, DATA_KEY_ADDONS, @@ -23,6 +24,7 @@ from .const import ( DATA_KEY_HOST, DATA_KEY_OS, DATA_KEY_SUPERVISOR, + SupervisorEntityModel, ) from .entity import ( HassioAddonEntity, @@ -64,10 +66,8 @@ STATS_ENTITY_DESCRIPTIONS = ( ), ) -ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + STATS_ENTITY_DESCRIPTIONS -CORE_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS +ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS OS_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS -SUPERVISOR_ENTITY_DESCRIPTIONS = STATS_ENTITY_DESCRIPTIONS HOST_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( @@ -120,6 +120,9 @@ async def async_setup_entry( entities: list[ HassioOSSensor | HassioAddonSensor | CoreSensor | SupervisorSensor | HostSensor ] = [] + stats_entities: list[ + AddonStatsSensor | CoreStatsSensor | SupervisorStatsSensor + ] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): for entity_description in ADDON_ENTITY_DESCRIPTIONS: @@ -130,19 +133,35 @@ async def async_setup_entry( entity_description=entity_description, ) ) + for entity_description in STATS_ENTITY_DESCRIPTIONS: + stats_entities.append( + AddonStatsSensor( + addon=addon, + coordinator=HassioStatsDataUpdateCoordinator( + hass=hass, + model=SupervisorEntityModel.ADDON, + addon_slug=addon[ATTR_SLUG], + ), + entity_description=entity_description, + ) + ) - for entity_description in CORE_ENTITY_DESCRIPTIONS: - entities.append( - CoreSensor( - coordinator=coordinator, + for entity_description in STATS_ENTITY_DESCRIPTIONS: + stats_entities.append( + CoreStatsSensor( + coordinator=HassioStatsDataUpdateCoordinator( + hass=hass, + model=SupervisorEntityModel.CORE, + ), entity_description=entity_description, ) ) - - for entity_description in SUPERVISOR_ENTITY_DESCRIPTIONS: - entities.append( - SupervisorSensor( - coordinator=coordinator, + stats_entities.append( + SupervisorStatsSensor( + coordinator=HassioStatsDataUpdateCoordinator( + hass=hass, + model=SupervisorEntityModel.SUPERVISOR, + ), entity_description=entity_description, ) ) @@ -165,6 +184,7 @@ async def async_setup_entry( ) async_add_entities(entities) + async_add_entities(stats_entities, True) class HassioAddonSensor(HassioAddonEntity, SensorEntity): @@ -212,3 +232,63 @@ class HostSensor(HassioHostEntity, SensorEntity): def native_value(self) -> str: """Return native value of entity.""" return self.coordinator.data[DATA_KEY_HOST][self.entity_description.key] + + +class AddonStatsSensor(HassioAddonEntity, SensorEntity): + """Sensor to track a Hass.io add-on stats attribute.""" + + coordinator: HassioStatsDataUpdateCoordinator + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[self.entity_description.key] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + self.coordinator.last_update_success + and self.coordinator.data is not None + and self.entity_description.key in self.coordinator.data + ) + + +class CoreStatsSensor(HassioCoreEntity, SensorEntity): + """Sensor to track a Hass.io core stats attribute.""" + + coordinator: HassioStatsDataUpdateCoordinator + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[self.entity_description.key] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + self.coordinator.last_update_success + and self.coordinator.data is not None + and self.entity_description.key in self.coordinator.data + ) + + +class SupervisorStatsSensor(HassioSupervisorEntity, SensorEntity): + """Sensor to track a Hass.io supervisor stats attribute.""" + + coordinator: HassioStatsDataUpdateCoordinator + + @property + def native_value(self) -> str: + """Return native value of entity.""" + return self.coordinator.data[self.entity_description.key] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + self.coordinator.last_update_success + and self.coordinator.data is not None + and self.entity_description.key in self.coordinator.data + ) diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index 285a2663d92..2f258df7299 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( ADDONS_COORDINATOR, + HassioDataUpdateCoordinator, async_update_addon, async_update_core, async_update_os, @@ -89,6 +90,8 @@ async def async_setup_entry( class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): """Update entity to handle updates for the Supervisor add-ons.""" + coordinator: HassioDataUpdateCoordinator + _attr_supported_features = ( UpdateEntityFeature.INSTALL | UpdateEntityFeature.BACKUP