mirror of
https://github.com/home-assistant/core.git
synced 2026-04-17 15:44:52 +01:00
Decouple Vizio apps coordinator from config entry (#163923)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
@@ -35,9 +35,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
and entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
):
|
||||
store: Store[list[dict[str, Any]]] = Store(hass, 1, DOMAIN)
|
||||
coordinator = VizioAppsDataUpdateCoordinator(hass, entry, store)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinator = VizioAppsDataUpdateCoordinator(hass, store)
|
||||
await coordinator.async_setup()
|
||||
hass.data[DOMAIN][CONF_APPS] = coordinator
|
||||
await coordinator.async_refresh()
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -53,7 +54,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
entry.data[CONF_DEVICE_CLASS] == MediaPlayerDeviceClass.TV
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
):
|
||||
hass.data[DOMAIN].pop(CONF_APPS, None)
|
||||
if coordinator := hass.data[DOMAIN].pop(CONF_APPS, None):
|
||||
await coordinator.async_shutdown()
|
||||
|
||||
if not hass.data[DOMAIN]:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
from pyvizio.const import APPS
|
||||
from pyvizio.util import gen_apps_list_from_url
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -23,19 +22,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]):
|
||||
"""Define an object to hold Vizio app config data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
store: Store[list[dict[str, Any]]],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
config_entry=None,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(days=1),
|
||||
)
|
||||
@@ -43,8 +39,9 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]
|
||||
self.fail_threshold = 10
|
||||
self.store = store
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Refresh data for the first time when a config entry is setup."""
|
||||
async def async_setup(self) -> None:
|
||||
"""Load initial data from storage and register shutdown."""
|
||||
await self.async_register_shutdown()
|
||||
self.data = await self.store.async_load() or APPS
|
||||
|
||||
async def _async_update_data(self) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
"""Tests for Vizio init."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerDeviceClass
|
||||
from homeassistant.components.vizio.const import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID
|
||||
from .const import (
|
||||
APP_LIST,
|
||||
HOST2,
|
||||
MOCK_SPEAKER_CONFIG,
|
||||
MOCK_USER_VALID_TV_CONFIG,
|
||||
NAME2,
|
||||
UNIQUE_ID,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
@@ -61,10 +77,10 @@ async def test_speaker_load_and_unload(hass: HomeAssistant) -> None:
|
||||
)
|
||||
async def test_coordinator_update_failure(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test coordinator update failure after 10 days."""
|
||||
now = dt_util.now()
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
|
||||
)
|
||||
@@ -72,13 +88,67 @@ async def test_coordinator_update_failure(
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 1
|
||||
assert DOMAIN in hass.data
|
||||
|
||||
# Failing 25 days in a row should result in a single log message
|
||||
# (first one after 10 days, next one would be at 30 days)
|
||||
for days in range(1, 25):
|
||||
async_fire_time_changed(hass, now + timedelta(days=days))
|
||||
freezer.tick(timedelta(days=days))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
err_msg = "Unable to retrieve the apps list from the external server"
|
||||
assert len([record for record in caplog.records if err_msg in record.msg]) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update")
|
||||
async def test_apps_coordinator_persists_until_last_tv_unloads(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test shared apps coordinator is not shut down until the last TV entry unloads."""
|
||||
config_entry_1 = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID
|
||||
)
|
||||
config_entry_2 = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_NAME: NAME2,
|
||||
CONF_HOST: HOST2,
|
||||
CONF_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
|
||||
CONF_ACCESS_TOKEN: "deadbeef2",
|
||||
},
|
||||
unique_id="testid2",
|
||||
)
|
||||
config_entry_1.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry_1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry_2.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry_2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)) == 2
|
||||
|
||||
# Unload first TV — coordinator should still be fetching apps
|
||||
assert await hass.config_entries.async_unload(config_entry_1.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.vizio.coordinator.gen_apps_list_from_url",
|
||||
return_value=APP_LIST,
|
||||
) as mock_fetch:
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_fetch.call_count == 1
|
||||
|
||||
# Unload second (last) TV — coordinator should stop fetching apps
|
||||
assert await hass.config_entries.async_unload(config_entry_2.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.vizio.coordinator.gen_apps_list_from_url",
|
||||
return_value=APP_LIST,
|
||||
) as mock_fetch:
|
||||
freezer.tick(timedelta(days=2))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert mock_fetch.call_count == 0
|
||||
|
||||
Reference in New Issue
Block a user