From 7ff633f531b9b0f75189f7ef778b76492b8ae81e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 2 Sep 2020 05:55:10 -0400 Subject: [PATCH] Automatically update app list for Vizio SmartTV's (#38641) --- homeassistant/components/vizio/__init__.py | 55 ++++++++++++++- homeassistant/components/vizio/config_flow.py | 11 ++- homeassistant/components/vizio/const.py | 5 +- homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 33 +++++++-- homeassistant/components/vizio/services.yaml | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/conftest.py | 15 ++-- tests/components/vizio/const.py | 16 ++++- tests/components/vizio/test_config_flow.py | 69 +++++++++++++------ tests/components/vizio/test_init.py | 8 ++- tests/components/vizio/test_media_player.py | 52 ++++++++++++-- 13 files changed, 225 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index a52b395c5c9..25960da72cf 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,15 +1,24 @@ """The vizio component.""" import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict, List +from pyvizio.const import APPS +from pyvizio.util import gen_apps_list_from_url import voluptuous as vol from homeassistant.components.media_player import DEVICE_CLASS_TV -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA +_LOGGER = logging.getLogger(__name__) + def validate_apps(config: ConfigType) -> ConfigType: """Validate CONF_APPS is only used when CONF_DEVICE_CLASS == DEVICE_CLASS_TV.""" @@ -47,6 +56,16 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" + + hass.data.setdefault(DOMAIN, {}) + if ( + CONF_APPS not in hass.data[DOMAIN] + and config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + ): + coordinator = VizioAppsDataUpdateCoordinator(hass) + await coordinator.async_refresh() + hass.data[DOMAIN][CONF_APPS] = coordinator + for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, platform) @@ -68,4 +87,38 @@ async def async_unload_entry( ) ) + # Exclude this config entry because its not unloaded yet + if not any( + entry.state == ENTRY_STATE_LOADED + and entry.entry_id != config_entry.entry_id + and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + for entry in hass.config_entries.async_entries(DOMAIN) + ): + hass.data[DOMAIN].pop(CONF_APPS) + + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return unload_ok + + +class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Vizio app config data.""" + + def __init__(self, hass: HomeAssistantType) -> None: + """Initialize.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(days=1), + update_method=self._async_update_data, + ) + self.data = APPS + + async def _async_update_data(self) -> List[Dict[str, Any]]: + """Update data via library.""" + data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) + if not data: + raise UpdateFailed + return sorted(data, key=lambda app: app["name"]) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index ba7bb39f7ea..74fc0d746e3 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -5,6 +5,7 @@ import socket from typing import Any, Dict, Optional from pyvizio import VizioAsync, async_guess_device_type +from pyvizio.const import APP_HOME import voluptuous as vol from homeassistant import config_entries @@ -154,7 +155,15 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): default=self.config_entry.options.get(CONF_APPS, {}).get( default_include_or_exclude, [] ), - ): cv.multi_select(VizioAsync.get_apps_list()), + ): cv.multi_select( + [ + APP_HOME["name"], + *[ + app["name"] + for app in self.hass.data[DOMAIN][CONF_APPS].data + ], + ] + ), } ) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index e0b60769e45..bcfc38950d3 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,5 +1,4 @@ """Constants used by vizio component.""" -from pyvizio import VizioAsync from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -101,10 +100,10 @@ VIZIO_SCHEMA = { vol.Optional(CONF_APPS): vol.All( { vol.Exclusive(CONF_INCLUDE, "apps_filter"): vol.All( - cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))] + cv.ensure_list, [cv.string] ), vol.Exclusive(CONF_EXCLUDE, "apps_filter"): vol.All( - cv.ensure_list, [vol.All(cv.string, vol.In(VizioAsync.get_apps_list()))] + cv.ensure_list, [cv.string] ), vol.Optional(CONF_ADDITIONAL_CONFIGS): vol.All( cv.ensure_list, diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index a0c36dc0089..7aa544b4a0b 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.51"], + "requirements": ["pyvizio==0.1.56"], "codeowners": ["@raman325"], "config_flow": true, "zeroconf": ["_viziocast._tcp.local."], diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index cbddbaa6897..ab5386c151b 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -5,10 +5,11 @@ from typing import Any, Callable, Dict, List, Optional, Union from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name -from pyvizio.const import APP_HOME, APPS, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP +from pyvizio.const import APP_HOME, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, SUPPORT_SELECT_SOUND_MODE, MediaPlayerEntity, ) @@ -23,6 +24,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -32,6 +34,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_ADDITIONAL_CONFIGS, @@ -78,6 +81,7 @@ async def async_setup_entry( params = {} if not config_entry.options: params["options"] = {CONF_VOLUME_STEP: volume_step} + include_or_exclude_key = next( ( key @@ -115,7 +119,9 @@ async def async_setup_entry( _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady - entity = VizioDevice(config_entry, device, name, device_class) + apps_coordinator = hass.data[DOMAIN].get(CONF_APPS) + + entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator) async_add_entities([entity], update_before_add=True) platform = entity_platform.current_platform.get() @@ -133,10 +139,12 @@ class VizioDevice(MediaPlayerEntity): device: VizioAsync, name: str, device_class: str, + apps_coordinator: DataUpdateCoordinator, ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry self._async_unsub_listeners = [] + self._apps_coordinator = apps_coordinator self._name = name self._state = None @@ -150,6 +158,7 @@ class VizioDevice(MediaPlayerEntity): self._available_sound_modes = [] self._available_inputs = [] self._available_apps = [] + self._all_apps = apps_coordinator.data if apps_coordinator else None self._conf_apps = config_entry.options.get(CONF_APPS, {}) self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( CONF_ADDITIONAL_CONFIGS, [] @@ -255,14 +264,15 @@ class VizioDevice(MediaPlayerEntity): # Create list of available known apps from known app list after # filtering by CONF_INCLUDE/CONF_EXCLUDE - self._available_apps = self._apps_list(self._device.get_apps_list()) + self._available_apps = self._apps_list([app["name"] for app in self._all_apps]) self._current_app_config = await self._device.get_current_app_config( log_api_exception=False ) self._current_app = find_app_name( - self._current_app_config, [APP_HOME, *APPS, *self._additional_app_configs] + self._current_app_config, + [APP_HOME, *self._all_apps, *self._additional_app_configs], ) if self._current_app == NO_APP_RUNNING: @@ -286,6 +296,7 @@ class VizioDevice(MediaPlayerEntity): async def _async_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from this entity.""" self._volume_step = config_entry.options[CONF_VOLUME_STEP] + # Update so that CONF_ADDITIONAL_CONFIGS gets retained for imports self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) async def async_update_setting( @@ -314,6 +325,18 @@ class VizioDevice(MediaPlayerEntity): ) ) + # Register callback for app list updates if device is a TV + @callback + def apps_list_update(): + """Update list of all apps.""" + self._all_apps = self._apps_coordinator.data + self.async_write_ha_state() + + if self._device_class == DEVICE_CLASS_TV: + self._async_unsub_listeners.append( + self._apps_coordinator.async_add_listener(apps_list_update) + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks when entity is removed.""" for listener in self._async_unsub_listeners: @@ -479,7 +502,7 @@ class VizioDevice(MediaPlayerEntity): ) ) elif source in self._available_apps: - await self._device.launch_app(source) + await self._device.launch_app(source, self._all_apps) async def async_volume_up(self) -> None: """Increase volume of the device.""" diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml index c652b622de0..50bde6cab78 100644 --- a/homeassistant/components/vizio/services.yaml +++ b/homeassistant/components/vizio/services.yaml @@ -2,7 +2,7 @@ update_setting: description: Update the value of a setting on a particular Vizio media player device. fields: entity_id: - description: Name of an entity to send command. + description: Name of an entity to send command to. example: "media_player.vizio_smartcast" setting_type: description: The type of setting to be changed. Available types are listed in the `setting_types` property. diff --git a/requirements_all.txt b/requirements_all.txt index 64287ff4d7f..e6a49977d10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.51 +pyvizio==0.1.56 # homeassistant.components.velux pyvlx==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bafccebb99c..c0354d60ade 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -857,7 +857,7 @@ pyvera==0.3.9 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.51 +pyvizio==0.1.56 # homeassistant.components.volumio pyvolumio==0.1.2 diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 492494fc75e..e1f5dfa18b0 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,5 +1,6 @@ """Configure py.test.""" import pytest +from pyvizio.api.apps import AppConfig from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME from .const import ( @@ -57,6 +58,15 @@ def vizio_get_unique_id_fixture(): yield +@pytest.fixture(name="vizio_data_coordinator_update", autouse=True) +def vizio_data_coordinator_update_fixture(): + """Mock get data coordinator update.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", return_value=APP_LIST, + ): + yield + + @pytest.fixture(name="vizio_no_unique_id") def vizio_no_unique_id_fixture(): """Mock no vizio unique ID returrned.""" @@ -191,15 +201,12 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_apps_list", - return_value=APP_LIST, ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", return_value="CAST", ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", - return_value=CURRENT_APP_CONFIG, + return_value=AppConfig(**CURRENT_APP_CONFIG), ): yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 43605de67ad..6ec746b2c54 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -72,7 +72,21 @@ INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] CURRENT_APP = "Hulu" CURRENT_APP_CONFIG = {CONF_APP_ID: "3", CONF_NAME_SPACE: 4, CONF_MESSAGE: None} -APP_LIST = ["Hulu", "Netflix"] +APP_LIST = [ + { + "name": "Hulu", + "country": ["*"], + "id": ["1"], + "config": [{"NAME_SPACE": 4, "APP_ID": "3", "MESSAGE": None}], + }, + { + "name": "Netflix", + "country": ["*"], + "id": ["2"], + "config": [{"NAME_SPACE": 1, "APP_ID": "2", "MESSAGE": None}], + }, +] +APP_NAME_LIST = [app["name"] for app in APP_LIST] INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"] CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} ADDITIONAL_APP_CONFIG = { diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 41490e9f4be..2506a685e43 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -109,12 +109,18 @@ async def test_user_flow_all_fields( assert CONF_APPS not in result["data"] -async def test_speaker_options_flow(hass: HomeAssistantType) -> None: +async def test_speaker_options_flow( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for speaker.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -131,12 +137,18 @@ async def test_speaker_options_flow(hass: HomeAssistantType) -> None: assert CONF_APPS not in result["data"] -async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_no_apps( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV without providing apps option.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -156,12 +168,18 @@ async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: assert CONF_APPS not in result["data"] -async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_with_apps( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV with providing apps option.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) - entry.add_to_hass(hass) - - assert not entry.options + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) @@ -182,14 +200,23 @@ async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} -async def test_tv_options_flow_start_with_volume(hass: HomeAssistantType) -> None: +async def test_tv_options_flow_start_with_volume( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: """Test options config flow for TV with providing apps option after providing volume step in initial config.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_USER_VALID_TV_CONFIG, - options={CONF_VOLUME_STEP: VOLUME_STEP}, + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - entry.add_to_hass(hass) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = result["result"] + + result = await hass.config_entries.options.async_init( + entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 1be067e9570..715800d006d 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.vizio.const import DOMAIN from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID @@ -37,7 +38,12 @@ async def test_load_and_unload( assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert DOMAIN in hass.data + assert "apps" in hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN]["apps"], DataUpdateCoordinator) - assert await hass.config_entries.async_unload(config_entry.entry_id) + assert await config_entry.async_unload(hass) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 + assert "apps" not in hass.data.get(DOMAIN, {}) + assert DOMAIN not in hass.data diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 6b19089057b..3dc093d38ea 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -8,6 +8,7 @@ import pytest from pytest import raises from pyvizio.api.apps import AppConfig from pyvizio.const import ( + APPS, DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, INPUT_APPS, @@ -51,6 +52,7 @@ from homeassistant.util import dt as dt_util from .const import ( ADDITIONAL_APP_CONFIG, APP_LIST, + APP_NAME_LIST, CURRENT_APP, CURRENT_APP_CONFIG, CURRENT_EQ, @@ -358,7 +360,11 @@ async def test_services( await _test_service(hass, MP_DOMAIN, "pow_on", SERVICE_TURN_ON, None) await _test_service(hass, MP_DOMAIN, "pow_off", SERVICE_TURN_OFF, None) await _test_service( - hass, MP_DOMAIN, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + hass, + MP_DOMAIN, + "mute_on", + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: True}, ) await _test_service( hass, @@ -511,7 +517,7 @@ async def test_setup_with_apps( hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert CURRENT_APP in attr["source_list"] assert attr["source"] == CURRENT_APP assert attr["app_name"] == CURRENT_APP @@ -524,6 +530,7 @@ async def test_setup_with_apps( SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP}, CURRENT_APP, + APP_LIST, ) @@ -580,13 +587,13 @@ async def test_setup_with_apps_additional_apps_config( _assert_source_list_with_apps( list( INPUT_LIST_WITH_APPS - + APP_LIST + + APP_NAME_LIST + [ app["name"] for app in MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG[CONF_APPS][ CONF_ADDITIONAL_CONFIGS ] - if app["name"] not in APP_LIST + if app["name"] not in APP_NAME_LIST ] ), attr, @@ -603,6 +610,7 @@ async def test_setup_with_apps_additional_apps_config( SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "Netflix"}, "Netflix", + APP_LIST, ) await _test_service( hass, @@ -649,7 +657,7 @@ async def test_setup_with_unknown_app_config( hass, MOCK_USER_VALID_TV_CONFIG, UNKNOWN_APP_CONFIG ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert attr["source"] == UNKNOWN_APP assert attr["app_name"] == UNKNOWN_APP assert attr["app_id"] == UNKNOWN_APP_CONFIG @@ -666,7 +674,7 @@ async def test_setup_with_no_running_app( hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig()) ): attr = hass.states.get(ENTITY_ID).attributes - _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_LIST), attr) + _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) assert attr["source"] == "CAST" assert "app_id" not in attr assert "app_name" not in attr @@ -694,3 +702,35 @@ async def test_setup_tv_without_mute( _assert_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV) assert "sound_mode" not in attr assert "is_volume_muted" not in attr + + +async def test_apps_update( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update_with_apps: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device setup with apps where no app is running.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", return_value=None, + ): + async with _cm_for_test_setup_tv_with_apps( + hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig()) + ): + # Check source list, remove TV inputs, and verify that the integration is + # using the default APPS list + sources = hass.states.get(ENTITY_ID).attributes["source_list"] + apps = list(set(sources) - set(INPUT_LIST)) + assert len(apps) == len(APPS) + + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=APP_LIST, + ): + async_fire_time_changed(hass, dt_util.now() + timedelta(days=2)) + await hass.async_block_till_done() + # Check source list, remove TV inputs, and verify that the integration is + # now using the APP_LIST list + sources = hass.states.get(ENTITY_ID).attributes["source_list"] + apps = list(set(sources) - set(INPUT_LIST)) + assert len(apps) == len(APP_LIST)