1
0
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:
Raman Gupta
2026-03-03 05:22:41 -05:00
committed by GitHub
parent f1856e6ef6
commit f94a075641
3 changed files with 85 additions and 16 deletions

View File

@@ -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)

View File

@@ -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]]:

View File

@@ -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