Files

1107 lines
37 KiB
Python

"""Tests for Vizio config flow."""
from collections.abc import AsyncIterator, Generator
from contextlib import asynccontextmanager
from datetime import timedelta
from typing import Any
from unittest.mock import call, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from vizaio import AppConfig, RemoteKey, VizioConnectionError, VizioNotFoundError
from vizaio.apps import BUNDLED_APPS, UNKNOWN_APP, is_app_input
from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_SOUND_MODE,
DOMAIN as MP_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
MediaPlayerDeviceClass,
MediaPlayerEntityFeature,
)
from homeassistant.components.vizio.const import (
CONF_ADDITIONAL_CONFIGS,
CONF_APPS,
CONF_VOLUME_STEP,
DEFAULT_VOLUME_STEP,
DOMAIN,
)
from homeassistant.components.vizio.services import (
SERVICE_SEND_TEXT,
SERVICE_UPDATE_SETTING,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_EXCLUDE,
CONF_INCLUDE,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from .conftest import setup_integration
from .const import (
ADDITIONAL_APP_CONFIG,
APP_NAME_LIST,
APP_RECORDS,
CURRENT_APP,
CURRENT_APP_CONFIG_OBJ,
CURRENT_EQ,
CURRENT_INPUT,
CUSTOM_CONFIG_OBJ,
ENTITY_ID,
EQ_LIST,
INPUT_LIST,
INPUT_LIST_WITH_APPS,
MAX_VOLUME,
MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG,
MOCK_USER_VALID_TV_CONFIG,
NAME,
UNIQUE_ID,
UNKNOWN_APP_CONFIG,
UNKNOWN_APP_CONFIG_OBJ,
VOLUME_STEP,
audio_setting,
)
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@pytest.fixture(autouse=True)
def media_player_only() -> Generator[None]:
"""Only set up the media_player platform."""
with patch(
"homeassistant.components.vizio.PLATFORMS",
[Platform.MEDIA_PLAYER],
):
yield
@pytest.mark.parametrize(
"mock_config_entry_fixture",
["mock_tv_config_entry", "mock_speaker_config_entry"],
ids=["tv", "speaker"],
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_media_player_entity_setup(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry_fixture: str,
request: pytest.FixtureRequest,
) -> None:
"""Test media player entity is created for TV and speaker."""
config_entry: MockConfigEntry = request.getfixturevalue(mock_config_entry_fixture)
await setup_integration(hass, config_entry)
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
def _get_ha_power_state(vizio_power_state: bool) -> str:
"""Return HA power state given Vizio power state."""
if vizio_power_state:
return STATE_ON
return STATE_OFF
def _assert_sources_and_volume(
attr: dict[str, Any], vizio_device_class: MediaPlayerDeviceClass
) -> None:
"""Assert source list, source, and volume level based on device class."""
assert attr[ATTR_INPUT_SOURCE_LIST] == INPUT_LIST
assert attr[ATTR_INPUT_SOURCE] == CURRENT_INPUT
assert (
attr["volume_level"]
== float(int(MAX_VOLUME[vizio_device_class] / 2))
/ MAX_VOLUME[vizio_device_class]
)
def _get_attr_and_assert_base_attr(
hass: HomeAssistant, device_class: str, power_state: str
) -> dict[str, Any]:
"""Return entity attributes after asserting name, device class, and power state."""
attr = hass.states.get(ENTITY_ID).attributes
assert attr["friendly_name"] == NAME
assert attr["device_class"] == device_class
assert hass.states.get(ENTITY_ID).state == power_state
return attr
@asynccontextmanager
async def _cm_for_test_setup_without_apps(
all_settings: dict[str, Any], vizio_power_state: bool
) -> AsyncIterator[None]:
"""Context manager to setup test for Vizio devices without app patches."""
with (
patch(
"homeassistant.components.vizio.Vizio.get_settings",
return_value={
name: audio_setting(name, value) for name, value in all_settings.items()
},
),
patch(
"homeassistant.components.vizio.Vizio.get_setting",
return_value=audio_setting("eq", CURRENT_EQ, tuple(EQ_LIST)),
),
patch(
"homeassistant.components.vizio.Vizio.get_power_state",
return_value=vizio_power_state,
),
# The coordinator falls back to these when the audio settings
# collection omits volume or mute. Default them to unsupported so
# a test opting out of a setting really gets no value for it;
# tests exercising the fallback patch over these.
patch(
"homeassistant.components.vizio.Vizio.get_volume",
side_effect=VizioNotFoundError("not supported"),
),
patch(
"homeassistant.components.vizio.Vizio.is_muted",
side_effect=VizioNotFoundError("not supported"),
),
):
yield
async def _test_setup_tv(
hass: HomeAssistant, config_entry: MockConfigEntry, vizio_power_state: bool
) -> None:
"""Test Vizio TV entity setup."""
ha_power_state = _get_ha_power_state(vizio_power_state)
async with _cm_for_test_setup_without_apps(
{
"volume": int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2),
"mute": "Off",
"eq": CURRENT_EQ,
},
vizio_power_state,
):
await setup_integration(hass, config_entry)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.TV, ha_power_state
)
if ha_power_state == STATE_ON:
_assert_sources_and_volume(attr, MediaPlayerDeviceClass.TV)
assert attr[ATTR_SOUND_MODE] == CURRENT_EQ
async def _test_setup_speaker(
hass: HomeAssistant, config_entry: MockConfigEntry, vizio_power_state: bool
) -> None:
"""Test Vizio Speaker entity setup."""
ha_power_state = _get_ha_power_state(vizio_power_state)
audio_settings = {
"volume": int(MAX_VOLUME[MediaPlayerDeviceClass.SPEAKER] / 2),
"mute": "Off",
"eq": CURRENT_EQ,
}
async with _cm_for_test_setup_without_apps(
audio_settings,
vizio_power_state,
):
await setup_integration(hass, config_entry)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.SPEAKER, ha_power_state
)
if ha_power_state == STATE_ON:
_assert_sources_and_volume(attr, MediaPlayerDeviceClass.SPEAKER)
assert "sound_mode" in attr
@asynccontextmanager
async def _cm_for_test_setup_tv_with_apps(
hass: HomeAssistant, config_entry: MockConfigEntry, app_config: AppConfig | None
) -> AsyncIterator[None]:
"""Context manager to setup test for Vizio TV with support for apps."""
async with _cm_for_test_setup_without_apps(
{"volume": int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2), "mute": "Off"},
True,
):
with patch(
"homeassistant.components.vizio.Vizio.get_current_app_config",
return_value=app_config,
):
await setup_integration(hass, config_entry)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.TV, STATE_ON
)
assert (
attr["volume_level"]
== float(int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2))
/ MAX_VOLUME[MediaPlayerDeviceClass.TV]
)
yield
def _assert_source_list_with_apps(
list_to_test: list[str], attr: dict[str, Any]
) -> None:
"""Assert source list matches list_to_test after removing app inputs."""
list_to_test = [item for item in list_to_test if not is_app_input(item)]
assert attr[ATTR_INPUT_SOURCE_LIST] == list_to_test
async def _test_service(
hass: HomeAssistant,
domain: str,
vizio_func_name: str,
ha_service_name: str,
additional_service_data: dict[str, Any] | None,
*args,
**kwargs,
) -> None:
"""Test generic Vizio media player entity service."""
service_data = {ATTR_ENTITY_ID: ENTITY_ID}
if additional_service_data:
service_data.update(additional_service_data)
with patch(
f"homeassistant.components.vizio.Vizio.{vizio_func_name}"
) as service_call:
await hass.services.async_call(
domain,
ha_service_name,
service_data=service_data,
blocking=True,
)
assert service_call.called
if args or kwargs:
assert service_call.call_args == call(*args, **kwargs)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_tv_without_volume_in_audio_settings(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test a TV that exposes volume and mute nowhere at all.
Some firmware does not list them in the `audio` settings collection;
when the individual settings are unavailable too, there is nothing
left to read. The entity must still load with both attributes unset,
rather than raising on every coordinator update.
"""
async with _cm_for_test_setup_without_apps({"eq": CURRENT_EQ}, True):
await setup_integration(hass, mock_tv_config_entry)
attr = _get_attr_and_assert_base_attr(hass, MediaPlayerDeviceClass.TV, STATE_ON)
# Unset attributes are omitted from the state entirely.
assert attr.get("volume_level") is None
assert attr.get("is_volume_muted") is None
assert attr[ATTR_SOUND_MODE] == CURRENT_EQ
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_tv_volume_and_mute_read_individually(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test a TV whose audio settings omit volume and mute.
Some firmware does not list them in the `audio` collection even
though the individual settings still work, so the coordinator reads
them directly and the entity still reports both.
"""
volume = int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2)
async with _cm_for_test_setup_without_apps({"eq": CURRENT_EQ}, True):
with (
patch(
"homeassistant.components.vizio.Vizio.get_volume",
return_value=volume,
) as get_volume,
patch(
"homeassistant.components.vizio.Vizio.is_muted",
return_value=True,
) as is_muted,
):
await setup_integration(hass, mock_tv_config_entry)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.TV, STATE_ON
)
assert (
attr["volume_level"]
== float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]
)
assert attr["is_volume_muted"] is True
assert get_volume.called
assert is_muted.called
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_tv_volume_and_mute_not_read_when_present(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test that the individual reads are skipped when not needed.
Devices that do list volume and mute in the collection must not pay
an extra round trip for each on every coordinator update.
"""
volume = int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2)
async with _cm_for_test_setup_without_apps(
{"volume": volume, "mute": "Off", "eq": CURRENT_EQ}, True
):
with (
patch("homeassistant.components.vizio.Vizio.get_volume") as get_volume,
patch("homeassistant.components.vizio.Vizio.is_muted") as is_muted,
):
await setup_integration(hass, mock_tv_config_entry)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.TV, STATE_ON
)
assert (
attr["volume_level"]
== float(volume) / MAX_VOLUME[MediaPlayerDeviceClass.TV]
)
assert attr["is_volume_muted"] is False
assert not get_volume.called
assert not is_muted.called
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_speaker_on(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test Vizio Speaker entity setup when on."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_speaker_off(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test Vizio Speaker entity setup when off."""
await _test_setup_speaker(hass, mock_speaker_config_entry, False)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_init_tv_on(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test Vizio TV entity setup when on."""
await _test_setup_tv(hass, mock_tv_config_entry, True)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_init_tv_off(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test Vizio TV entity setup when off."""
await _test_setup_tv(hass, mock_tv_config_entry, False)
@pytest.mark.usefixtures("vizio_cant_connect")
async def test_setup_unavailable_speaker(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test speaker config entry retries setup when device is unavailable."""
mock_speaker_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_speaker_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_speaker_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("vizio_cant_connect")
async def test_setup_unavailable_tv(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test TV config entry retries setup when device is unavailable."""
mock_tv_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_tv_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_tv_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_services(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test all Vizio media player entity services."""
await _test_setup_tv(hass, mock_tv_config_entry, True)
await _test_service(hass, MP_DOMAIN, "power_on", SERVICE_TURN_ON, None)
await _test_service(hass, MP_DOMAIN, "power_off", SERVICE_TURN_OFF, None)
await _test_service(
hass,
MP_DOMAIN,
"mute",
SERVICE_VOLUME_MUTE,
{ATTR_MEDIA_VOLUME_MUTED: True},
)
await _test_service(
hass,
MP_DOMAIN,
"unmute",
SERVICE_VOLUME_MUTE,
{ATTR_MEDIA_VOLUME_MUTED: False},
)
await _test_service(
hass,
MP_DOMAIN,
"set_input",
SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: "USB"},
"USB",
)
await _test_service(
hass, MP_DOMAIN, "volume_up", SERVICE_VOLUME_UP, None, steps=DEFAULT_VOLUME_STEP
)
await _test_service(
hass,
MP_DOMAIN,
"volume_down",
SERVICE_VOLUME_DOWN,
None,
steps=DEFAULT_VOLUME_STEP,
)
await _test_service(
hass,
MP_DOMAIN,
"set_volume",
SERVICE_VOLUME_SET,
{ATTR_MEDIA_VOLUME_LEVEL: 1},
100, # TV max volume is 100
)
await _test_service(
hass,
MP_DOMAIN,
"set_volume",
SERVICE_VOLUME_SET,
{ATTR_MEDIA_VOLUME_LEVEL: 0.35},
35, # Absolute set does not depend on the previously known level
)
await _test_service(
hass,
MP_DOMAIN,
"set_volume",
SERVICE_VOLUME_SET,
{ATTR_MEDIA_VOLUME_LEVEL: 0},
0,
)
await _test_service(
hass, MP_DOMAIN, "send_key", SERVICE_MEDIA_NEXT_TRACK, None, RemoteKey.CH_UP
)
await _test_service(
hass,
MP_DOMAIN,
"send_key",
SERVICE_MEDIA_PREVIOUS_TRACK,
None,
RemoteKey.CH_DOWN,
)
await _test_service(
hass,
MP_DOMAIN,
"set_setting",
SERVICE_SELECT_SOUND_MODE,
{ATTR_SOUND_MODE: "Music"},
"audio",
"eq",
"Music",
)
# Test that the update_setting service does config
# validation/transformation correctly
await _test_service(
hass,
DOMAIN,
"set_setting",
SERVICE_UPDATE_SETTING,
{"setting_type": "Audio", "setting_name": "AV Delay", "new_value": "0"},
"audio",
"av_delay",
0,
)
await _test_service(
hass,
DOMAIN,
"set_setting",
SERVICE_UPDATE_SETTING,
{"setting_type": "Audio", "setting_name": "EQ", "new_value": "Music"},
"audio",
"eq",
"Music",
)
await _test_service(
hass, MP_DOMAIN, "send_key", SERVICE_MEDIA_PLAY, None, RemoteKey.PLAY
)
await _test_service(
hass, MP_DOMAIN, "send_key", SERVICE_MEDIA_PAUSE, None, RemoteKey.PAUSE
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_options_update(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test when config entry update event fires."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
assert not config_entry.options
hass.config_entries.async_update_entry(
entry=config_entry,
options={CONF_VOLUME_STEP: VOLUME_STEP},
)
assert config_entry.options == {CONF_VOLUME_STEP: VOLUME_STEP}
await hass.async_block_till_done()
await _test_service(
hass, MP_DOMAIN, "volume_up", SERVICE_VOLUME_UP, None, steps=VOLUME_STEP
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_update_available_to_unavailable(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device becomes unavailable after being available."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
# Simulate device becoming unreachable
with patch(
"homeassistant.components.vizio.Vizio.get_power_state",
side_effect=VizioConnectionError("cannot connect"),
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_update_unavailable_to_available(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device becomes available after being unavailable."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
# First, make device unavailable
with patch(
"homeassistant.components.vizio.Vizio.get_power_state",
side_effect=VizioConnectionError("cannot connect"),
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE
# Then, make device available again
with patch(
"homeassistant.components.vizio.Vizio.get_power_state",
return_value=True,
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_apps(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps."""
async with _cm_for_test_setup_tv_with_apps(
hass, mock_tv_config_entry, CURRENT_APP_CONFIG_OBJ
):
attr = hass.states.get(ENTITY_ID).attributes
_assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr)
assert CURRENT_APP in attr[ATTR_INPUT_SOURCE_LIST]
assert attr[ATTR_INPUT_SOURCE] == CURRENT_APP
assert attr["app_name"] == CURRENT_APP
assert "app_id" not in attr
await _test_service(
hass,
MP_DOMAIN,
"launch_app",
SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: CURRENT_APP},
CURRENT_APP,
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_apps_include(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps and apps["include"] in options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_USER_VALID_TV_CONFIG,
options={CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]}},
unique_id=UNIQUE_ID,
)
async with _cm_for_test_setup_tv_with_apps(
hass, config_entry, CURRENT_APP_CONFIG_OBJ
):
attr = hass.states.get(ENTITY_ID).attributes
_assert_source_list_with_apps([*INPUT_LIST_WITH_APPS, CURRENT_APP], attr)
assert CURRENT_APP in attr[ATTR_INPUT_SOURCE_LIST]
assert attr[ATTR_INPUT_SOURCE] == CURRENT_APP
assert attr["app_name"] == CURRENT_APP
assert "app_id" not in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_apps_exclude(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps and apps["exclude"] in options."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data=MOCK_USER_VALID_TV_CONFIG,
options={CONF_APPS: {CONF_EXCLUDE: ["Netflix"]}},
unique_id=UNIQUE_ID,
)
async with _cm_for_test_setup_tv_with_apps(
hass, config_entry, CURRENT_APP_CONFIG_OBJ
):
attr = hass.states.get(ENTITY_ID).attributes
_assert_source_list_with_apps([*INPUT_LIST_WITH_APPS, CURRENT_APP], attr)
assert CURRENT_APP in attr[ATTR_INPUT_SOURCE_LIST]
assert attr[ATTR_INPUT_SOURCE] == CURRENT_APP
assert attr["app_name"] == CURRENT_APP
assert "app_id" not in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_apps_additional_apps_config(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps and apps["additional_configs"] in config."""
config_entry = MockConfigEntry(
domain=DOMAIN, data=MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, unique_id=UNIQUE_ID
)
async with _cm_for_test_setup_tv_with_apps(
hass,
config_entry,
CUSTOM_CONFIG_OBJ,
):
attr = hass.states.get(ENTITY_ID).attributes
assert attr[ATTR_INPUT_SOURCE_LIST].count(CURRENT_APP) == 1
_assert_source_list_with_apps(
list(
INPUT_LIST_WITH_APPS
+ 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_NAME_LIST
]
),
attr,
)
assert ADDITIONAL_APP_CONFIG["name"] in attr[ATTR_INPUT_SOURCE_LIST]
assert attr[ATTR_INPUT_SOURCE] == ADDITIONAL_APP_CONFIG["name"]
assert attr["app_name"] == ADDITIONAL_APP_CONFIG["name"]
assert "app_id" not in attr
await _test_service(
hass,
MP_DOMAIN,
"launch_app",
SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: "Netflix"},
"Netflix",
)
await _test_service(
hass,
MP_DOMAIN,
"launch_app_config",
SERVICE_SELECT_SOURCE,
{ATTR_INPUT_SOURCE: CURRENT_APP},
CUSTOM_CONFIG_OBJ,
)
# Test that invalid app raises
with (
patch("homeassistant.components.vizio.Vizio.launch_app") as service_call1,
patch(
"homeassistant.components.vizio.Vizio.launch_app_config"
) as service_call2,
pytest.raises(ServiceValidationError, match='Source "_" is not valid'),
):
await hass.services.async_call(
MP_DOMAIN,
SERVICE_SELECT_SOURCE,
service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "_"},
blocking=True,
)
assert not service_call1.called
assert not service_call2.called
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_unknown_app_config(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps where app config returned is unknown."""
async with _cm_for_test_setup_tv_with_apps(
hass, mock_tv_config_entry, UNKNOWN_APP_CONFIG_OBJ
):
attr = hass.states.get(ENTITY_ID).attributes
_assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr)
assert attr[ATTR_INPUT_SOURCE] == UNKNOWN_APP
assert attr["app_name"] == UNKNOWN_APP
assert attr["app_id"] == UNKNOWN_APP_CONFIG
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_setup_with_no_running_app(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps where no app is running."""
async with _cm_for_test_setup_tv_with_apps(hass, mock_tv_config_entry, None):
attr = hass.states.get(ENTITY_ID).attributes
_assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr)
assert attr[ATTR_INPUT_SOURCE] == "CAST"
assert "app_id" not in attr
assert "app_name" not in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_setup_tv_without_mute(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test Vizio TV entity setup when mute property isn't returned by Vizio API."""
async with _cm_for_test_setup_without_apps(
{"volume": int(MAX_VOLUME[MediaPlayerDeviceClass.TV] / 2)},
True,
):
await setup_integration(hass, mock_tv_config_entry)
attr = _get_attr_and_assert_base_attr(hass, MediaPlayerDeviceClass.TV, STATE_ON)
_assert_sources_and_volume(attr, MediaPlayerDeviceClass.TV)
assert "sound_mode" not in attr
assert "is_volume_muted" not in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps")
async def test_apps_update(
hass: HomeAssistant,
mock_tv_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test device setup with apps where no app is running."""
with patch(
"homeassistant.components.vizio.coordinator.fetch_remote_app_catalog",
side_effect=VizioConnectionError("fetch failed"),
):
async with _cm_for_test_setup_tv_with_apps(hass, mock_tv_config_entry, None):
# Check source list, remove TV inputs, and verify that the integration is
# using the default bundled apps list
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
apps = list(set(sources) - set(INPUT_LIST))
assert len(apps) == len(BUNDLED_APPS)
with patch(
"homeassistant.components.vizio.coordinator.fetch_remote_app_catalog",
return_value=APP_RECORDS,
):
async_fire_time_changed(hass, dt_util.now() + timedelta(days=2))
await hass.async_block_till_done()
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_RECORDS list
sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
apps = list(set(sources) - set(INPUT_LIST))
assert len(apps) == len(APP_RECORDS)
@pytest.mark.parametrize(
("entry_data", "app_config", "expected_app_name"),
[
pytest.param(
MOCK_USER_VALID_TV_CONFIG,
UNKNOWN_APP_CONFIG_OBJ,
None,
id="unknown_config_hides_app_name",
),
pytest.param(
MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG,
CUSTOM_CONFIG_OBJ,
ADDITIONAL_APP_CONFIG["name"],
id="additional_config_keeps_mapped_name",
),
],
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update_with_apps_on_input")
async def test_vizio_update_with_apps_on_input(
hass: HomeAssistant,
entry_data: dict[str, Any],
app_config: AppConfig,
expected_app_name: str | None,
) -> None:
"""Test a vizio TV with apps that is on a TV input.
The device reports an app config even on HDMI. An unrecognized one must
not surface as the unknown-app sentinel, while a name mapped through
additional_configs is deliberate user configuration and is kept.
"""
config_entry = MockConfigEntry(domain=DOMAIN, data=entry_data, unique_id=UNIQUE_ID)
with patch(
"homeassistant.components.vizio.Vizio.get_current_app_config",
return_value=app_config,
):
await setup_integration(hass, config_entry)
attr = _get_attr_and_assert_base_attr(hass, MediaPlayerDeviceClass.TV, STATE_ON)
assert attr[ATTR_INPUT_SOURCE] == CURRENT_INPUT
assert attr.get("app_name") == expected_app_name
assert "app_id" not in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_coordinator_update_on_to_off(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device transitions from on to off during coordinator refresh."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.SPEAKER, STATE_ON
)
assert attr[ATTR_MEDIA_VOLUME_LEVEL] is not None
assert ATTR_SOUND_MODE in attr
# Device turns off
with patch(
"homeassistant.components.vizio.Vizio.get_power_state",
return_value=False,
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == STATE_OFF
attr = hass.states.get(ENTITY_ID).attributes
assert attr.get(ATTR_MEDIA_VOLUME_LEVEL) is None
assert attr.get(ATTR_MEDIA_VOLUME_MUTED) is None
assert attr.get(ATTR_SOUND_MODE) is None
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_coordinator_update_off_to_on(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test device transitions from off to on during coordinator refresh."""
await _test_setup_speaker(hass, mock_speaker_config_entry, False)
assert hass.states.get(ENTITY_ID).state == STATE_OFF
# Device turns on
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).state == STATE_ON
attr = hass.states.get(ENTITY_ID).attributes
assert attr[ATTR_MEDIA_VOLUME_LEVEL] is not None
assert ATTR_SOUND_MODE in attr
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_sound_mode_feature_toggling(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test sound mode feature is added when present and removed when absent."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
attr = _get_attr_and_assert_base_attr(
hass, MediaPlayerDeviceClass.SPEAKER, STATE_ON
)
assert ATTR_SOUND_MODE in attr
state = hass.states.get(ENTITY_ID)
assert (
state.attributes["supported_features"]
& MediaPlayerEntityFeature.SELECT_SOUND_MODE
)
# Update with audio settings that have no sound mode
with (
patch(
"homeassistant.components.vizio.Vizio.get_settings",
return_value={
"volume": audio_setting("volume", 50),
"mute": audio_setting("mute", "Off"),
},
),
patch(
"homeassistant.components.vizio.Vizio.get_power_state",
return_value=True,
),
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state.state == STATE_ON
assert not (
state.attributes["supported_features"]
& MediaPlayerEntityFeature.SELECT_SOUND_MODE
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_sound_mode_list_cached(
hass: HomeAssistant,
mock_speaker_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test sound mode list is cached after first retrieval."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
attr = hass.states.get(ENTITY_ID).attributes
assert attr["sound_mode_list"] == EQ_LIST
# Update with different sound mode options — cached list should persist
with (
patch(
"homeassistant.components.vizio.Vizio.get_setting",
return_value=audio_setting("eq", CURRENT_EQ, ("Different1", "Different2")),
),
patch(
"homeassistant.components.vizio.Vizio.get_power_state",
return_value=True,
),
):
freezer.tick(timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
attr = hass.states.get(ENTITY_ID).attributes
# Sound mode list should still be the original cached list
assert attr["sound_mode_list"] == EQ_LIST
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_select_invalid_sound_mode(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test selecting an invalid sound mode raises."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
with (
patch("homeassistant.components.vizio.Vizio.set_setting") as set_setting,
pytest.raises(
ServiceValidationError, match='Sound mode "invalid" is not valid'
),
):
await hass.services.async_call(
MP_DOMAIN,
SERVICE_SELECT_SOUND_MODE,
service_data={ATTR_ENTITY_ID: ENTITY_ID, ATTR_SOUND_MODE: "invalid"},
blocking=True,
)
set_setting.assert_not_called()
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_command_error_raises(
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
) -> None:
"""Test a device command failure raises HomeAssistantError."""
await _test_setup_speaker(hass, mock_speaker_config_entry, True)
with (
patch(
"homeassistant.components.vizio.Vizio.power_on",
side_effect=VizioConnectionError("cannot connect"),
),
pytest.raises(HomeAssistantError, match="Failed to send command"),
):
await hass.services.async_call(
MP_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_send_text(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test the send_text service types text on the device."""
await setup_integration(hass, mock_tv_config_entry)
with patch("homeassistant.components.vizio.Vizio.send_text") as mock_send_text:
await hass.services.async_call(
DOMAIN,
SERVICE_SEND_TEXT,
{ATTR_ENTITY_ID: ENTITY_ID, "text": "stranger things"},
blocking=True,
)
mock_send_text.assert_called_once_with("stranger things")
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
async def test_send_text_device_error(
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
) -> None:
"""Test send_text surfaces device errors as HomeAssistantError."""
await setup_integration(hass, mock_tv_config_entry)
with (
patch(
"homeassistant.components.vizio.Vizio.send_text",
side_effect=VizioConnectionError("cannot connect"),
),
pytest.raises(HomeAssistantError, match="Failed to send command"),
):
await hass.services.async_call(
DOMAIN,
SERVICE_SEND_TEXT,
{ATTR_ENTITY_ID: ENTITY_ID, "text": "abc"},
blocking=True,
)