Automatically update app list for Vizio SmartTV's (#38641)

This commit is contained in:
Raman Gupta 2020-09-02 05:55:10 -04:00 committed by GitHub
parent 9f5baa0bf7
commit 7ff633f531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 225 additions and 47 deletions

View File

@ -1,15 +1,24 @@
"""The vizio component.""" """The vizio component."""
import asyncio 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 import voluptuous as vol
from homeassistant.components.media_player import DEVICE_CLASS_TV 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 import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType, HomeAssistantType 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 from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA
_LOGGER = logging.getLogger(__name__)
def validate_apps(config: ConfigType) -> ConfigType: def validate_apps(config: ConfigType) -> ConfigType:
"""Validate CONF_APPS is only used when CONF_DEVICE_CLASS == DEVICE_CLASS_TV.""" """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: async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool:
"""Load the saved entities.""" """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: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform) 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 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"])

View File

@ -5,6 +5,7 @@ import socket
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from pyvizio import VizioAsync, async_guess_device_type from pyvizio import VizioAsync, async_guess_device_type
from pyvizio.const import APP_HOME
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -154,7 +155,15 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow):
default=self.config_entry.options.get(CONF_APPS, {}).get( default=self.config_entry.options.get(CONF_APPS, {}).get(
default_include_or_exclude, [] 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
],
]
),
} }
) )

View File

@ -1,5 +1,4 @@
"""Constants used by vizio component.""" """Constants used by vizio component."""
from pyvizio import VizioAsync
from pyvizio.const import ( from pyvizio.const import (
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
@ -101,10 +100,10 @@ VIZIO_SCHEMA = {
vol.Optional(CONF_APPS): vol.All( vol.Optional(CONF_APPS): vol.All(
{ {
vol.Exclusive(CONF_INCLUDE, "apps_filter"): 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( 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( vol.Optional(CONF_ADDITIONAL_CONFIGS): vol.All(
cv.ensure_list, cv.ensure_list,

View File

@ -2,7 +2,7 @@
"domain": "vizio", "domain": "vizio",
"name": "VIZIO SmartCast", "name": "VIZIO SmartCast",
"documentation": "https://www.home-assistant.io/integrations/vizio", "documentation": "https://www.home-assistant.io/integrations/vizio",
"requirements": ["pyvizio==0.1.51"], "requirements": ["pyvizio==0.1.56"],
"codeowners": ["@raman325"], "codeowners": ["@raman325"],
"config_flow": true, "config_flow": true,
"zeroconf": ["_viziocast._tcp.local."], "zeroconf": ["_viziocast._tcp.local."],

View File

@ -5,10 +5,11 @@ from typing import Any, Callable, Dict, List, Optional, Union
from pyvizio import VizioAsync from pyvizio import VizioAsync
from pyvizio.api.apps import find_app_name 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 ( from homeassistant.components.media_player import (
DEVICE_CLASS_SPEAKER, DEVICE_CLASS_SPEAKER,
DEVICE_CLASS_TV,
SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOUND_MODE,
MediaPlayerEntity, MediaPlayerEntity,
) )
@ -23,6 +24,7 @@ from homeassistant.const import (
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
) )
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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.entity import Entity
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import ( from .const import (
CONF_ADDITIONAL_CONFIGS, CONF_ADDITIONAL_CONFIGS,
@ -78,6 +81,7 @@ async def async_setup_entry(
params = {} params = {}
if not config_entry.options: if not config_entry.options:
params["options"] = {CONF_VOLUME_STEP: volume_step} params["options"] = {CONF_VOLUME_STEP: volume_step}
include_or_exclude_key = next( include_or_exclude_key = next(
( (
key key
@ -115,7 +119,9 @@ async def async_setup_entry(
_LOGGER.warning("Failed to connect to %s", host) _LOGGER.warning("Failed to connect to %s", host)
raise PlatformNotReady 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) async_add_entities([entity], update_before_add=True)
platform = entity_platform.current_platform.get() platform = entity_platform.current_platform.get()
@ -133,10 +139,12 @@ class VizioDevice(MediaPlayerEntity):
device: VizioAsync, device: VizioAsync,
name: str, name: str,
device_class: str, device_class: str,
apps_coordinator: DataUpdateCoordinator,
) -> None: ) -> None:
"""Initialize Vizio device.""" """Initialize Vizio device."""
self._config_entry = config_entry self._config_entry = config_entry
self._async_unsub_listeners = [] self._async_unsub_listeners = []
self._apps_coordinator = apps_coordinator
self._name = name self._name = name
self._state = None self._state = None
@ -150,6 +158,7 @@ class VizioDevice(MediaPlayerEntity):
self._available_sound_modes = [] self._available_sound_modes = []
self._available_inputs = [] self._available_inputs = []
self._available_apps = [] self._available_apps = []
self._all_apps = apps_coordinator.data if apps_coordinator else None
self._conf_apps = config_entry.options.get(CONF_APPS, {}) self._conf_apps = config_entry.options.get(CONF_APPS, {})
self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get(
CONF_ADDITIONAL_CONFIGS, [] CONF_ADDITIONAL_CONFIGS, []
@ -255,14 +264,15 @@ class VizioDevice(MediaPlayerEntity):
# Create list of available known apps from known app list after # Create list of available known apps from known app list after
# filtering by CONF_INCLUDE/CONF_EXCLUDE # 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( self._current_app_config = await self._device.get_current_app_config(
log_api_exception=False log_api_exception=False
) )
self._current_app = find_app_name( 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: if self._current_app == NO_APP_RUNNING:
@ -286,6 +296,7 @@ class VizioDevice(MediaPlayerEntity):
async def _async_update_options(self, config_entry: ConfigEntry) -> None: async def _async_update_options(self, config_entry: ConfigEntry) -> None:
"""Update options if the update signal comes from this entity.""" """Update options if the update signal comes from this entity."""
self._volume_step = config_entry.options[CONF_VOLUME_STEP] 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, {})) self._conf_apps.update(config_entry.options.get(CONF_APPS, {}))
async def async_update_setting( 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: async def async_will_remove_from_hass(self) -> None:
"""Disconnect callbacks when entity is removed.""" """Disconnect callbacks when entity is removed."""
for listener in self._async_unsub_listeners: for listener in self._async_unsub_listeners:
@ -479,7 +502,7 @@ class VizioDevice(MediaPlayerEntity):
) )
) )
elif source in self._available_apps: 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: async def async_volume_up(self) -> None:
"""Increase volume of the device.""" """Increase volume of the device."""

View File

@ -2,7 +2,7 @@ update_setting:
description: Update the value of a setting on a particular Vizio media player device. description: Update the value of a setting on a particular Vizio media player device.
fields: fields:
entity_id: entity_id:
description: Name of an entity to send command. description: Name of an entity to send command to.
example: "media_player.vizio_smartcast" example: "media_player.vizio_smartcast"
setting_type: setting_type:
description: The type of setting to be changed. Available types are listed in the `setting_types` property. description: The type of setting to be changed. Available types are listed in the `setting_types` property.

View File

@ -1830,7 +1830,7 @@ pyversasense==0.0.6
pyvesync==1.1.0 pyvesync==1.1.0
# homeassistant.components.vizio # homeassistant.components.vizio
pyvizio==0.1.51 pyvizio==0.1.56
# homeassistant.components.velux # homeassistant.components.velux
pyvlx==0.2.16 pyvlx==0.2.16

View File

@ -857,7 +857,7 @@ pyvera==0.3.9
pyvesync==1.1.0 pyvesync==1.1.0
# homeassistant.components.vizio # homeassistant.components.vizio
pyvizio==0.1.51 pyvizio==0.1.56
# homeassistant.components.volumio # homeassistant.components.volumio
pyvolumio==0.1.2 pyvolumio==0.1.2

View File

@ -1,5 +1,6 @@
"""Configure py.test.""" """Configure py.test."""
import pytest import pytest
from pyvizio.api.apps import AppConfig
from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME
from .const import ( from .const import (
@ -57,6 +58,15 @@ def vizio_get_unique_id_fixture():
yield 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") @pytest.fixture(name="vizio_no_unique_id")
def vizio_no_unique_id_fixture(): def vizio_no_unique_id_fixture():
"""Mock no vizio unique ID returrned.""" """Mock no vizio unique ID returrned."""
@ -191,15 +201,12 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture):
with patch( with patch(
"homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list", "homeassistant.components.vizio.media_player.VizioAsync.get_inputs_list",
return_value=get_mock_inputs(INPUT_LIST_WITH_APPS), return_value=get_mock_inputs(INPUT_LIST_WITH_APPS),
), patch(
"homeassistant.components.vizio.media_player.VizioAsync.get_apps_list",
return_value=APP_LIST,
), patch( ), patch(
"homeassistant.components.vizio.media_player.VizioAsync.get_current_input", "homeassistant.components.vizio.media_player.VizioAsync.get_current_input",
return_value="CAST", return_value="CAST",
), patch( ), patch(
"homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config",
return_value=CURRENT_APP_CONFIG, return_value=AppConfig(**CURRENT_APP_CONFIG),
): ):
yield yield

View File

@ -72,7 +72,21 @@ INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"]
CURRENT_APP = "Hulu" CURRENT_APP = "Hulu"
CURRENT_APP_CONFIG = {CONF_APP_ID: "3", CONF_NAME_SPACE: 4, CONF_MESSAGE: None} 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"] INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"]
CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10}
ADDITIONAL_APP_CONFIG = { ADDITIONAL_APP_CONFIG = {

View File

@ -109,12 +109,18 @@ async def test_user_flow_all_fields(
assert CONF_APPS not in result["data"] 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.""" """Test options config flow for speaker."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) result = await hass.config_entries.flow.async_init(
entry.add_to_hass(hass) DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG
)
assert not entry.options 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) 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"] 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.""" """Test options config flow for TV without providing apps option."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) result = await hass.config_entries.flow.async_init(
entry.add_to_hass(hass) DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
)
assert not entry.options 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) 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"] 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.""" """Test options config flow for TV with providing apps option."""
entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) result = await hass.config_entries.flow.async_init(
entry.add_to_hass(hass) DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
)
assert not entry.options 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) 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]} 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.""" """Test options config flow for TV with providing apps option after providing volume step in initial config."""
entry = MockConfigEntry( result = await hass.config_entries.flow.async_init(
domain=DOMAIN, DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG
data=MOCK_USER_VALID_TV_CONFIG,
options={CONF_VOLUME_STEP: VOLUME_STEP},
) )
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
assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP}

View File

@ -4,6 +4,7 @@ import pytest
from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN
from homeassistant.components.vizio.const import DOMAIN from homeassistant.components.vizio.const import DOMAIN
from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 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() await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0
assert "apps" not in hass.data.get(DOMAIN, {})
assert DOMAIN not in hass.data

View File

@ -8,6 +8,7 @@ import pytest
from pytest import raises from pytest import raises
from pyvizio.api.apps import AppConfig from pyvizio.api.apps import AppConfig
from pyvizio.const import ( from pyvizio.const import (
APPS,
DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER,
DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV,
INPUT_APPS, INPUT_APPS,
@ -51,6 +52,7 @@ from homeassistant.util import dt as dt_util
from .const import ( from .const import (
ADDITIONAL_APP_CONFIG, ADDITIONAL_APP_CONFIG,
APP_LIST, APP_LIST,
APP_NAME_LIST,
CURRENT_APP, CURRENT_APP,
CURRENT_APP_CONFIG, CURRENT_APP_CONFIG,
CURRENT_EQ, 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_on", SERVICE_TURN_ON, None)
await _test_service(hass, MP_DOMAIN, "pow_off", SERVICE_TURN_OFF, None) await _test_service(hass, MP_DOMAIN, "pow_off", SERVICE_TURN_OFF, None)
await _test_service( 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( await _test_service(
hass, hass,
@ -511,7 +517,7 @@ async def test_setup_with_apps(
hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP_CONFIG hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP_CONFIG
): ):
attr = hass.states.get(ENTITY_ID).attributes 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 CURRENT_APP in attr["source_list"]
assert attr["source"] == CURRENT_APP assert attr["source"] == CURRENT_APP
assert attr["app_name"] == CURRENT_APP assert attr["app_name"] == CURRENT_APP
@ -524,6 +530,7 @@ async def test_setup_with_apps(
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: CURRENT_APP}, {ATTR_INPUT_SOURCE: CURRENT_APP},
CURRENT_APP, CURRENT_APP,
APP_LIST,
) )
@ -580,13 +587,13 @@ async def test_setup_with_apps_additional_apps_config(
_assert_source_list_with_apps( _assert_source_list_with_apps(
list( list(
INPUT_LIST_WITH_APPS INPUT_LIST_WITH_APPS
+ APP_LIST + APP_NAME_LIST
+ [ + [
app["name"] app["name"]
for app in MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG[CONF_APPS][ for app in MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG[CONF_APPS][
CONF_ADDITIONAL_CONFIGS CONF_ADDITIONAL_CONFIGS
] ]
if app["name"] not in APP_LIST if app["name"] not in APP_NAME_LIST
] ]
), ),
attr, attr,
@ -603,6 +610,7 @@ async def test_setup_with_apps_additional_apps_config(
SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: "Netflix"}, {ATTR_INPUT_SOURCE: "Netflix"},
"Netflix", "Netflix",
APP_LIST,
) )
await _test_service( await _test_service(
hass, hass,
@ -649,7 +657,7 @@ async def test_setup_with_unknown_app_config(
hass, MOCK_USER_VALID_TV_CONFIG, UNKNOWN_APP_CONFIG hass, MOCK_USER_VALID_TV_CONFIG, UNKNOWN_APP_CONFIG
): ):
attr = hass.states.get(ENTITY_ID).attributes 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["source"] == UNKNOWN_APP
assert attr["app_name"] == UNKNOWN_APP assert attr["app_name"] == UNKNOWN_APP
assert attr["app_id"] == UNKNOWN_APP_CONFIG 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()) hass, MOCK_USER_VALID_TV_CONFIG, vars(AppConfig())
): ):
attr = hass.states.get(ENTITY_ID).attributes 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 attr["source"] == "CAST"
assert "app_id" not in attr assert "app_id" not in attr
assert "app_name" 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_sources_and_volume(attr, VIZIO_DEVICE_CLASS_TV)
assert "sound_mode" not in attr assert "sound_mode" not in attr
assert "is_volume_muted" 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)