Fix hassio delaying startup to fetch container stats (#102775)
This commit is contained in:
parent
6e72499f96
commit
4447336083
6 changed files with 162 additions and 30 deletions
|
@ -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]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue