Fix hassio delaying startup to fetch container stats (#102775)

This commit is contained in:
J. Nick Koston 2023-10-25 08:32:43 -05:00 committed by GitHub
parent 6e72499f96
commit 4447336083
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 30 deletions

View file

@ -34,6 +34,7 @@ from homeassistant.core import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.storage import Store
@ -74,6 +75,7 @@ from .const import (
DATA_KEY_SUPERVISOR,
DATA_KEY_SUPERVISOR_ISSUES,
DOMAIN,
REQUEST_REFRESH_DELAY,
SUPERVISOR_CONTAINER,
SupervisorEntityModel,
)
@ -334,7 +336,7 @@ def get_addons_stats(hass):
Async friendly.
"""
return hass.data.get(DATA_ADDONS_STATS)
return hass.data.get(DATA_ADDONS_STATS) or {}
@callback
@ -344,7 +346,7 @@ def get_core_stats(hass):
Async friendly.
"""
return hass.data.get(DATA_CORE_STATS)
return hass.data.get(DATA_CORE_STATS) or {}
@callback
@ -354,7 +356,7 @@ def get_supervisor_stats(hass):
Async friendly.
"""
return hass.data.get(DATA_SUPERVISOR_STATS)
return hass.data.get(DATA_SUPERVISOR_STATS) or {}
@callback
@ -754,6 +756,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER,
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: HassIO = hass.data[DOMAIN]
self.data = {}
@ -875,9 +883,9 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(),
DATA_OS_INFO: hassio.get_os_info(),
}
if first_update or CONTAINER_STATS in container_updates[CORE_CONTAINER]:
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
updates[DATA_CORE_STATS] = hassio.get_core_stats()
if first_update or CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
if CONTAINER_STATS in container_updates[SUPERVISOR_CONTAINER]:
updates[DATA_SUPERVISOR_STATS] = hassio.get_supervisor_stats()
results = await asyncio.gather(*updates.values())
@ -903,20 +911,28 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
# API calls since otherwise we would fetch stats for all containers
# and throw them away.
#
for data_key, update_func, enabled_key, wanted_addons in (
for data_key, update_func, enabled_key, wanted_addons, needs_first_update in (
(
DATA_ADDONS_STATS,
self._update_addon_stats,
CONTAINER_STATS,
started_addons,
False,
),
(
DATA_ADDONS_CHANGELOGS,
self._update_addon_changelog,
CONTAINER_CHANGELOG,
all_addons,
True,
),
(
DATA_ADDONS_INFO,
self._update_addon_info,
CONTAINER_INFO,
all_addons,
True,
),
(DATA_ADDONS_INFO, self._update_addon_info, CONTAINER_INFO, all_addons),
):
container_data: dict[str, Any] = data.setdefault(data_key, {})
container_data.update(
@ -925,7 +941,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
*[
update_func(slug)
for slug in wanted_addons
if first_update or enabled_key in container_updates[slug]
if (first_update and needs_first_update)
or enabled_key in container_updates[slug]
]
)
)

View file

@ -101,6 +101,8 @@ KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
ATTR_STATE: {CONTAINER_INFO},
}
REQUEST_REFRESH_DELAY = 10
class SupervisorEntityModel(StrEnum):
"""Supervisor entity model."""

View file

@ -10,6 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DOMAIN, HassioDataUpdateCoordinator
from .const import (
ATTR_SLUG,
CONTAINER_STATS,
CORE_CONTAINER,
DATA_KEY_ADDONS,
DATA_KEY_CORE,
@ -58,6 +59,8 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
self._addon_slug, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()
class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
@ -147,6 +150,8 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
SUPERVISOR_CONTAINER, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()
class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
@ -183,3 +188,5 @@ class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
CORE_CONTAINER, self.entity_id, update_types
)
)
if CONTAINER_STATS in update_types:
await self.coordinator.async_request_refresh()

View file

@ -17,6 +17,7 @@ from homeassistant.components.hassio import (
async_get_addon_store_info,
hostname_from_addon_slug,
)
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant
@ -244,7 +245,7 @@ async def test_setup_api_ping(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0"
assert hass.components.hassio.is_hassio()
@ -289,7 +290,7 @@ async def test_setup_api_push_api_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert aioclient_mock.mock_calls[1][2]["watchdog"]
@ -308,7 +309,7 @@ async def test_setup_api_push_api_data_server_host(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
assert not aioclient_mock.mock_calls[1][2]["watchdog"]
@ -325,7 +326,7 @@ async def test_setup_api_push_api_data_default(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"]
@ -405,7 +406,7 @@ async def test_setup_api_existing_hassio_user(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert not aioclient_mock.mock_calls[1][2]["ssl"]
assert aioclient_mock.mock_calls[1][2]["port"] == 8123
assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token
@ -422,7 +423,7 @@ async def test_setup_core_push_timezone(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone"
with patch("homeassistant.util.dt.set_default_time_zone"):
@ -442,7 +443,7 @@ async def test_setup_hassio_no_additional_data(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456"
@ -524,14 +525,14 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 26
assert aioclient_mock.call_count == 24
assert aioclient_mock.mock_calls[-1][2] == "test"
await hass.services.async_call("hassio", "host_shutdown", {})
await hass.services.async_call("hassio", "host_reboot", {})
await hass.async_block_till_done()
assert aioclient_mock.call_count == 28
assert aioclient_mock.call_count == 26
await hass.services.async_call("hassio", "backup_full", {})
await hass.services.async_call(
@ -546,7 +547,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 30
assert aioclient_mock.call_count == 28
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"homeassistant": True,
@ -571,7 +572,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 32
assert aioclient_mock.call_count == 30
assert aioclient_mock.mock_calls[-1][2] == {
"addons": ["test"],
"folders": ["ssl"],
@ -590,7 +591,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 33
assert aioclient_mock.call_count == 31
assert aioclient_mock.mock_calls[-1][2] == {
"name": "backup_name",
"location": "backup_share",
@ -606,7 +607,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 34
assert aioclient_mock.call_count == 32
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 03:48:00",
"location": None,
@ -624,7 +625,7 @@ async def test_service_calls(
)
await hass.async_block_till_done()
assert aioclient_mock.call_count == 36
assert aioclient_mock.call_count == 34
assert aioclient_mock.mock_calls[-1][2] == {
"name": "2021-11-13 11:48:00",
"location": None,
@ -896,6 +897,7 @@ async def test_coordinator_updates(
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Initial refresh without stats
assert refresh_updates_mock.call_count == 1
with patch(
@ -919,10 +921,12 @@ async def test_coordinator_updates(
},
blocking=True,
)
assert refresh_updates_mock.call_count == 1
assert refresh_updates_mock.call_count == 0
# There is a 10s cooldown on the debouncer
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=10))
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
with patch(
@ -940,6 +944,88 @@ async def test_coordinator_updates(
},
blocking=True,
)
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 1
assert "Error on Supervisor API: Unknown" in caplog.text
async def test_coordinator_updates_stats_entities_enabled(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
entity_registry_enabled_by_default: None,
) -> None:
"""Test coordinator updates with stats entities enabled."""
await async_setup_component(hass, "homeassistant", {})
with patch.dict(os.environ, MOCK_ENVIRON), patch(
"homeassistant.components.hassio.HassIO.refresh_updates"
) as refresh_updates_mock:
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Initial refresh without stats
assert refresh_updates_mock.call_count == 1
# Refresh with stats once we know which ones are needed
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 2
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
) as refresh_updates_mock:
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=20))
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 0
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
) as refresh_updates_mock:
await hass.services.async_call(
"homeassistant",
"update_entity",
{
"entity_id": [
"update.home_assistant_core_update",
"update.home_assistant_supervisor_update",
]
},
blocking=True,
)
assert refresh_updates_mock.call_count == 0
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
with patch(
"homeassistant.components.hassio.HassIO.refresh_updates",
side_effect=HassioAPIError("Unknown"),
) as refresh_updates_mock:
await hass.services.async_call(
"homeassistant",
"update_entity",
{
"entity_id": [
"update.home_assistant_core_update",
"update.home_assistant_supervisor_update",
]
},
blocking=True,
)
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
assert refresh_updates_mock.call_count == 1
assert "Error on Supervisor API: Unknown" in caplog.text
@ -973,7 +1059,7 @@ async def test_setup_hardware_integration(
await hass.async_block_till_done()
assert result
assert aioclient_mock.call_count == 22
assert aioclient_mock.call_count == 20
assert len(mock_setup_entry.mock_calls) == 1

View file

@ -10,6 +10,7 @@ from homeassistant.components.hassio import (
HASSIO_UPDATE_INTERVAL,
HassioAPIError,
)
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
@ -245,6 +246,12 @@ async def test_sensor(
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
# Verify that the entity have the expected state.
state = hass.states.get(entity_id)
assert state.state == expected
@ -306,6 +313,12 @@ async def test_stats_addon_sensor(
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
# Verify that the entity have the expected state.
state = hass.states.get(entity_id)
assert state.state == expected

View file

@ -1,16 +1,18 @@
"""The tests for the hassio update entities."""
from datetime import timedelta
import os
from unittest.mock import patch
import pytest
from homeassistant.components.hassio import DOMAIN
from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.hassio import DOMAIN, HassioAPIError
from homeassistant.components.hassio.const import REQUEST_REFRESH_DELAY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import WebSocketGenerator
@ -609,8 +611,13 @@ async def test_setting_up_core_update_when_addon_fails(
await hass.async_block_till_done()
assert result
# There is a REQUEST_REFRESH_DELAYs cooldown on the debouncer
async_fire_time_changed(
hass, dt_util.now() + timedelta(seconds=REQUEST_REFRESH_DELAY)
)
await hass.async_block_till_done()
# Verify that the core update entity does exist
state = hass.states.get("update.home_assistant_core_update")
assert state
assert state.state == "on"
assert "Could not fetch stats for test: add-on is not running" in caplog.text