mirror of
https://github.com/home-assistant/core.git
synced 2026-08-16 10:13:55 +01:00
389 lines
13 KiB
Python
389 lines
13 KiB
Python
"""Tests for Vizio init."""
|
|
|
|
from datetime import timedelta
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from vizaio import VizioConnectionError, VizioNotFoundError
|
|
|
|
from homeassistant.components.media_player import (
|
|
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
|
MediaPlayerDeviceClass,
|
|
)
|
|
from homeassistant.components.vizio import DATA_APPS
|
|
from homeassistant.components.vizio.const import (
|
|
CONF_ADDITIONAL_CONFIGS,
|
|
CONF_APPS,
|
|
CONF_VOLUME_STEP,
|
|
DOMAIN,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import (
|
|
CONF_ACCESS_TOKEN,
|
|
CONF_DEVICE_CLASS,
|
|
CONF_HOST,
|
|
CONF_INCLUDE,
|
|
CONF_NAME,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_UNAVAILABLE,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import device_registry as dr
|
|
|
|
from .conftest import setup_integration
|
|
from .const import (
|
|
ADDITIONAL_APP_CONFIG,
|
|
APP_RECORDS,
|
|
CURRENT_APP,
|
|
CURRENT_INPUT,
|
|
ENTITY_ID,
|
|
HOST,
|
|
HOST2,
|
|
MOCK_USER_VALID_TV_CONFIG,
|
|
MODEL,
|
|
NAME2,
|
|
PORTLESS_HOST,
|
|
UNIQUE_ID,
|
|
VERSION,
|
|
VOLUME_STEP,
|
|
state_extended,
|
|
)
|
|
|
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
async def test_tv_load_and_unload(
|
|
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test loading and unloading TV entry."""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
|
|
assert DATA_APPS in hass.data
|
|
|
|
assert await hass.config_entries.async_unload(mock_tv_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
entities = hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)
|
|
assert len(entities) == 1
|
|
for entity in entities:
|
|
assert hass.states.get(entity).state == STATE_UNAVAILABLE
|
|
assert DATA_APPS not in hass.data
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
async def test_speaker_load_and_unload(
|
|
hass: HomeAssistant, mock_speaker_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test loading and unloading speaker entry."""
|
|
await setup_integration(hass, mock_speaker_config_entry)
|
|
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
|
|
|
|
assert await hass.config_entries.async_unload(mock_speaker_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
entities = hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)
|
|
assert len(entities) == 1
|
|
for entity in entities:
|
|
assert hass.states.get(entity).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.usefixtures(
|
|
"vizio_connect", "vizio_bypass_update", "vizio_data_coordinator_update_failure"
|
|
)
|
|
async def test_coordinator_update_failure(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test coordinator update failure after 10 days."""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
assert len(hass.states.async_entity_ids(MEDIA_PLAYER_DOMAIN)) == 1
|
|
assert DATA_APPS 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):
|
|
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,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test shared apps coordinator is not shut down until the last TV entry unloads."""
|
|
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",
|
|
)
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
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(MEDIA_PLAYER_DOMAIN)) == 2
|
|
|
|
# Unload first TV — coordinator should still be fetching apps
|
|
assert await hass.config_entries.async_unload(mock_tv_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
with patch(
|
|
"homeassistant.components.vizio.coordinator.fetch_remote_app_catalog",
|
|
return_value=APP_RECORDS,
|
|
) 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.fetch_remote_app_catalog",
|
|
return_value=APP_RECORDS,
|
|
) 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
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
async def test_device_registry_model_and_version(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
) -> None:
|
|
"""Test that coordinator populates device registry with model and version."""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
device = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, UNIQUE_ID), mock_tv_config_entry.entry_id
|
|
)
|
|
assert device is not None
|
|
assert device.model == MODEL
|
|
assert device.sw_version == VERSION
|
|
assert device.manufacturer == "VIZIO"
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_bypass_update")
|
|
async def test_device_registry_without_model_or_version(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
device_registry: dr.DeviceRegistry,
|
|
) -> None:
|
|
"""Test device registry when model and version are unavailable."""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
device = device_registry.async_get_device_by_identifier(
|
|
(DOMAIN, UNIQUE_ID), mock_tv_config_entry.entry_id
|
|
)
|
|
assert device is not None
|
|
assert device.model is None
|
|
assert device.sw_version is None
|
|
assert device.manufacturer == "VIZIO"
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect")
|
|
async def test_state_extended_polling(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
mock_vizio: AsyncMock,
|
|
) -> None:
|
|
"""Test modern firmware polls via a single state_extended call."""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
state = hass.states.get(ENTITY_ID)
|
|
assert state.state == STATE_ON
|
|
assert state.attributes["source"] == CURRENT_INPUT
|
|
# The bundled endpoint replaces the individual state getters
|
|
mock_vizio.get_power_state.assert_not_called()
|
|
mock_vizio.get_current_input.assert_not_called()
|
|
mock_vizio.get_current_app_config.assert_not_called()
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect")
|
|
async def test_state_extended_power_off(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
mock_vizio: AsyncMock,
|
|
) -> None:
|
|
"""Test state_extended reporting the device as off."""
|
|
mock_vizio.get_state_extended.return_value = state_extended(power_on=False)
|
|
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
assert hass.states.get(ENTITY_ID).state == STATE_OFF
|
|
mock_vizio.get_settings.assert_not_called()
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect")
|
|
async def test_state_extended_probed_only_once(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
mock_vizio: AsyncMock,
|
|
freezer: FrozenDateTimeFactory,
|
|
) -> None:
|
|
"""Test firmware without state_extended is not re-probed every refresh."""
|
|
mock_vizio.get_state_extended.side_effect = VizioNotFoundError("not supported")
|
|
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
mock_vizio.get_state_extended.reset_mock()
|
|
|
|
for _ in range(3):
|
|
freezer.tick(timedelta(minutes=1))
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done()
|
|
|
|
mock_vizio.get_state_extended.assert_not_called()
|
|
assert hass.states.get(ENTITY_ID).state == STATE_ON
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect")
|
|
async def test_state_extended_connection_error(
|
|
hass: HomeAssistant,
|
|
mock_tv_config_entry: MockConfigEntry,
|
|
mock_vizio: AsyncMock,
|
|
) -> None:
|
|
"""Test a state_extended connection error fails the update."""
|
|
mock_vizio.get_state_extended.side_effect = VizioConnectionError("cannot connect")
|
|
|
|
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_portless_host_is_resolved_and_persisted(hass: HomeAssistant) -> None:
|
|
"""Test a config entry storing a host without a port is repaired on setup."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**MOCK_USER_VALID_TV_CONFIG, CONF_HOST: PORTLESS_HOST},
|
|
unique_id=UNIQUE_ID,
|
|
)
|
|
with patch(
|
|
"homeassistant.components.vizio.async_resolve_host",
|
|
AsyncMock(return_value=HOST),
|
|
) as mock_resolve:
|
|
await setup_integration(hass, entry)
|
|
|
|
assert mock_resolve.call_args[0][0] == PORTLESS_HOST
|
|
assert entry.state is ConfigEntryState.LOADED
|
|
assert entry.data[CONF_HOST] == HOST
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
async def test_portless_host_resolve_failure_retries(hass: HomeAssistant) -> None:
|
|
"""Test setup is retried when the port cannot be determined."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={**MOCK_USER_VALID_TV_CONFIG, CONF_HOST: PORTLESS_HOST},
|
|
unique_id=UNIQUE_ID,
|
|
)
|
|
entry.add_to_hass(hass)
|
|
with patch(
|
|
"homeassistant.components.vizio.async_resolve_host",
|
|
AsyncMock(side_effect=VizioConnectionError("no SmartCast API")),
|
|
):
|
|
assert not await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
|
assert entry.data[CONF_HOST] == PORTLESS_HOST
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
async def test_host_with_port_is_left_alone(
|
|
hass: HomeAssistant, mock_tv_config_entry: MockConfigEntry
|
|
) -> None:
|
|
"""Test a host that already has a port is not rewritten.
|
|
|
|
async_resolve_host is idempotent, so it is called unconditionally and
|
|
returns the host untouched without any probing of its own.
|
|
"""
|
|
await setup_integration(hass, mock_tv_config_entry)
|
|
|
|
assert mock_tv_config_entry.data[CONF_HOST] == HOST
|
|
|
|
|
|
@pytest.mark.usefixtures("vizio_connect", "vizio_update")
|
|
@pytest.mark.parametrize(
|
|
("data", "options", "expected_data", "expected_options"),
|
|
[
|
|
pytest.param(
|
|
{
|
|
**MOCK_USER_VALID_TV_CONFIG,
|
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
|
CONF_APPS: {
|
|
CONF_INCLUDE: [CURRENT_APP],
|
|
CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG],
|
|
},
|
|
},
|
|
{},
|
|
{
|
|
**MOCK_USER_VALID_TV_CONFIG,
|
|
CONF_APPS: {CONF_ADDITIONAL_CONFIGS: [ADDITIONAL_APP_CONFIG]},
|
|
},
|
|
{
|
|
CONF_VOLUME_STEP: VOLUME_STEP,
|
|
CONF_APPS: {CONF_INCLUDE: [CURRENT_APP]},
|
|
},
|
|
id="moves_settings_to_options",
|
|
),
|
|
pytest.param(
|
|
{**MOCK_USER_VALID_TV_CONFIG, CONF_VOLUME_STEP: VOLUME_STEP},
|
|
{CONF_VOLUME_STEP: VOLUME_STEP + 1},
|
|
MOCK_USER_VALID_TV_CONFIG,
|
|
{CONF_VOLUME_STEP: VOLUME_STEP + 1},
|
|
id="existing_options_win",
|
|
),
|
|
pytest.param(
|
|
MOCK_USER_VALID_TV_CONFIG,
|
|
{},
|
|
MOCK_USER_VALID_TV_CONFIG,
|
|
{},
|
|
id="nothing_to_migrate",
|
|
),
|
|
],
|
|
)
|
|
async def test_migrate_entry_to_minor_version_2(
|
|
hass: HomeAssistant,
|
|
data: dict[str, Any],
|
|
options: dict[str, Any],
|
|
expected_data: dict[str, Any],
|
|
expected_options: dict[str, Any],
|
|
) -> None:
|
|
"""Test migrating a 1.1 entry moves settings from data to options."""
|
|
config_entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data=data,
|
|
options=options,
|
|
unique_id=UNIQUE_ID,
|
|
minor_version=1,
|
|
)
|
|
await setup_integration(hass, config_entry)
|
|
|
|
assert config_entry.version == 1
|
|
assert config_entry.minor_version == 2
|
|
assert dict(config_entry.data) == expected_data
|
|
assert dict(config_entry.options) == expected_options
|