diff --git a/.coveragerc b/.coveragerc index 693959684f1..39a46d97616 100644 --- a/.coveragerc +++ b/.coveragerc @@ -782,9 +782,6 @@ omit = homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* homeassistant/components/vivotek/camera.py - homeassistant/components/vizio/__init__.py - homeassistant/components/vizio/const.py - homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index ea1162540cf..2f3a581e113 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -6,5 +6,6 @@ "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, - "zeroconf": ["_viziocast._tcp.local."] + "zeroconf": ["_viziocast._tcp.local."], + "quality_scale": "platinum" } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b2f529bce10..3ea70fe2acb 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,4 +1,5 @@ """Vizio SmartCast Device support.""" +from datetime import timedelta import logging from typing import Callable, List @@ -39,7 +40,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) - +SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -55,13 +56,13 @@ async def async_setup_entry( device_class = config_entry.data[CONF_DEVICE_CLASS] # If config entry options not set up, set them up, otherwise assign values managed in options + volume_step = config_entry.options.get( + CONF_VOLUME_STEP, config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP), + ) if not config_entry.options: - volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP) hass.config_entries.async_update_entry( config_entry, options={CONF_VOLUME_STEP: volume_step} ) - else: - volume_step = config_entry.options[CONF_VOLUME_STEP] device = VizioAsync( DEVICE_ID, @@ -74,18 +75,7 @@ async def async_setup_entry( ) if not await device.can_connect(): - fail_auth_msg = "" - if token: - fail_auth_msg = f"and auth token '{token}' are correct." - else: - fail_auth_msg = "is correct." - _LOGGER.warning( - "Failed to connect to Vizio device, please check if host '%s' " - "is valid and available. Also check if device class '%s' %s", - host, - device_class, - fail_auth_msg, - ) + _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady entity = VizioDevice(config_entry, device, name, volume_step, device_class) @@ -127,10 +117,18 @@ class VizioDevice(MediaPlayerDevice): is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: - self._available = False + if self._available: + _LOGGER.warning( + "Lost connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = False return - self._available = True + if not self._available: + _LOGGER.info( + "Restored connection to %s", self._config_entry.data[CONF_HOST] + ) + self._available = True if not is_on: self._state = STATE_OFF @@ -157,7 +155,7 @@ class VizioDevice(MediaPlayerDevice): async def _async_send_update_options_signal( hass: HomeAssistantType, config_entry: ConfigEntry ) -> None: - """Send update event when when Vizio config entry is updated.""" + """Send update event when Vizio config entry is updated.""" # Move this method to component level if another entity ever gets added for a single config entry. # See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121 async_dispatcher_send(hass, config_entry.entry_id, config_entry) @@ -276,7 +274,7 @@ class VizioDevice(MediaPlayerDevice): await self._device.input_switch(source) async def async_volume_up(self) -> None: - """Increasing volume of the device.""" + """Increase volume of the device.""" await self._device.vol_up(num=self._volume_step) if self._volume_level is not None: @@ -285,7 +283,7 @@ class VizioDevice(MediaPlayerDevice): ) async def async_volume_down(self) -> None: - """Decreasing volume of the device.""" + """Decrease volume of the device.""" await self._device.vol_down(num=self._volume_step) if self._volume_level is not None: diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py new file mode 100644 index 00000000000..fd78952bba4 --- /dev/null +++ b/tests/components/vizio/conftest.py @@ -0,0 +1,102 @@ +"""Configure py.test.""" +from asynctest import patch +import pytest +from pyvizio.const import DEVICE_CLASS_SPEAKER +from pyvizio.vizio import MAX_VOLUME + +from .const import CURRENT_INPUT, INPUT_LIST, UNIQUE_ID + + +class MockInput: + """Mock Vizio device input.""" + + def __init__(self, name): + """Initialize mock Vizio device input.""" + self.meta_name = name + self.name = name + + +def get_mock_inputs(input_list): + """Return list of MockInput.""" + return [MockInput(input) for input in input_list] + + +@pytest.fixture(name="skip_notifications", autouse=True) +def skip_notifications_fixture(): + """Skip notification calls.""" + with patch("homeassistant.components.persistent_notification.async_create"), patch( + "homeassistant.components.persistent_notification.async_dismiss" + ): + yield + + +@pytest.fixture(name="vizio_connect") +def vizio_connect_fixture(): + """Mock valid vizio device and entry setup.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=True, + ), patch( + "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", + return_value=UNIQUE_ID, + ): + yield + + +@pytest.fixture(name="vizio_bypass_setup") +def vizio_bypass_setup_fixture(): + """Mock component setup.""" + with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): + yield + + +@pytest.fixture(name="vizio_bypass_update") +def vizio_bypass_update_fixture(): + """Mock component update.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=True, + ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): + yield + + +@pytest.fixture(name="vizio_guess_device_type") +def vizio_guess_device_type_fixture(): + """Mock vizio async_guess_device_type function.""" + with patch( + "homeassistant.components.vizio.config_flow.async_guess_device_type", + return_value="speaker", + ): + yield + + +@pytest.fixture(name="vizio_cant_connect") +def vizio_cant_connect_fixture(): + """Mock vizio device cant connect.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=False, + ): + yield + + +@pytest.fixture(name="vizio_update") +def vizio_update_fixture(): + """Mock valid updates to vizio device.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=True, + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume", + return_value=int(MAX_VOLUME[DEVICE_CLASS_SPEAKER] / 2), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", + return_value=MockInput(CURRENT_INPUT), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_inputs", + return_value=get_mock_inputs(INPUT_LIST), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=True, + ): + yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py new file mode 100644 index 00000000000..dd6ecda55d0 --- /dev/null +++ b/tests/components/vizio/const.py @@ -0,0 +1,77 @@ +"""Constants for the Vizio integration tests.""" +import logging + +from homeassistant.components.media_player import ( + DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, + DOMAIN as MP_DOMAIN, +) +from homeassistant.components.vizio.const import CONF_VOLUME_STEP +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, +) +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +NAME = "Vizio" +NAME2 = "Vizio2" +HOST = "192.168.1.1:9000" +HOST2 = "192.168.1.2:9000" +ACCESS_TOKEN = "deadbeef" +VOLUME_STEP = 2 +UNIQUE_ID = "testid" + +MOCK_USER_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, +} + +MOCK_OPTIONS = { + CONF_VOLUME_STEP: VOLUME_STEP, +} + +MOCK_IMPORT_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_VOLUME_STEP: VOLUME_STEP, +} + +MOCK_INVALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, +} + +MOCK_SPEAKER_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, +} + +VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." +ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" +ZEROCONF_HOST = HOST.split(":")[0] +ZEROCONF_PORT = HOST.split(":")[1] + +MOCK_ZEROCONF_SERVICE_INFO = { + CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, + CONF_NAME: ZEROCONF_NAME, + CONF_HOST: ZEROCONF_HOST, + CONF_PORT: ZEROCONF_PORT, + "properties": {"name": "SB4031-D5"}, +} + +CURRENT_INPUT = "HDMI" +INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] + +ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index c82c7a8de0f..9805d2def46 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -1,7 +1,4 @@ """Tests for Vizio config flow.""" -import logging - -from asynctest import patch import pytest import voluptuous as vol @@ -20,113 +17,26 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, - CONF_PORT, - CONF_TYPE, ) from homeassistant.helpers.typing import HomeAssistantType +from .const import ( + ACCESS_TOKEN, + HOST, + HOST2, + MOCK_IMPORT_VALID_TV_CONFIG, + MOCK_INVALID_TV_CONFIG, + MOCK_SPEAKER_CONFIG, + MOCK_USER_VALID_TV_CONFIG, + MOCK_ZEROCONF_SERVICE_INFO, + NAME, + NAME2, + UNIQUE_ID, + VOLUME_STEP, +) + from tests.common import MockConfigEntry -_LOGGER = logging.getLogger(__name__) - -NAME = "Vizio" -NAME2 = "Vizio2" -HOST = "192.168.1.1:9000" -HOST2 = "192.168.1.2:9000" -ACCESS_TOKEN = "deadbeef" -VOLUME_STEP = 2 -UNIQUE_ID = "testid" - -MOCK_USER_VALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, -} - -MOCK_IMPORT_VALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, - CONF_VOLUME_STEP: VOLUME_STEP, -} - -MOCK_INVALID_TV_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, -} - -MOCK_SPEAKER_CONFIG = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, -} - -VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." -ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" -ZEROCONF_HOST = HOST.split(":")[0] -ZEROCONF_PORT = HOST.split(":")[1] - -MOCK_ZEROCONF_ENTRY = { - CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, - CONF_NAME: ZEROCONF_NAME, - CONF_HOST: ZEROCONF_HOST, - CONF_PORT: ZEROCONF_PORT, - "properties": {"name": "SB4031-D5"}, -} - - -@pytest.fixture(name="vizio_connect") -def vizio_connect_fixture(): - """Mock valid vizio device and entry setup.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - return_value=True, - ), patch( - "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", - return_value=UNIQUE_ID, - ): - yield - - -@pytest.fixture(name="vizio_bypass_setup") -def vizio_bypass_setup_fixture(): - """Mock component setup.""" - with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): - yield - - -@pytest.fixture(name="vizio_bypass_update") -def vizio_bypass_update_fixture(): - """Mock component update.""" - with patch( - "homeassistant.components.vizio.media_player.VizioAsync.can_connect", - return_value=True, - ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): - yield - - -@pytest.fixture(name="vizio_guess_device_type") -def vizio_guess_device_type_fixture(): - """Mock vizio async_guess_device_type function.""" - with patch( - "homeassistant.components.vizio.config_flow.async_guess_device_type", - return_value="speaker", - ): - yield - - -@pytest.fixture(name="vizio_cant_connect") -def vizio_cant_connect_fixture(): - """Mock vizio device cant connect.""" - with patch( - "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", - return_value=False, - ): - yield - async def test_user_flow_minimum_fields( hass: HomeAssistantType, @@ -142,12 +52,7 @@ async def test_user_flow_minimum_fields( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, - }, + result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -172,13 +77,7 @@ async def test_user_flow_all_fields( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_TV, - CONF_ACCESS_TOKEN: ACCESS_TOKEN, - }, + result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -408,7 +307,7 @@ async def test_zeroconf_flow( vizio_guess_device_type: pytest.fixture, ) -> None: """Test zeroconf config flow.""" - discovery_info = MOCK_ZEROCONF_ENTRY.copy() + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) @@ -444,7 +343,7 @@ async def test_zeroconf_flow_already_configured( entry.add_to_hass(hass) # Try rediscovering same device - discovery_info = MOCK_ZEROCONF_ENTRY.copy() + discovery_info = MOCK_ZEROCONF_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py new file mode 100644 index 00000000000..1be067e9570 --- /dev/null +++ b/tests/components/vizio/test_init.py @@ -0,0 +1,43 @@ +"""Tests for Vizio init.""" +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.setup import async_setup_component + +from .const import MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID + +from tests.common import MockConfigEntry + + +async def test_setup_component( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test component setup.""" + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: MOCK_USER_VALID_TV_CONFIG} + ) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + + +async def test_load_and_unload( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, +) -> None: + """Test loading and unloading entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + 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 await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 0 diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py new file mode 100644 index 00000000000..d87b86f8642 --- /dev/null +++ b/tests/components/vizio/test_media_player.py @@ -0,0 +1,297 @@ +"""Tests for Vizio config flow.""" +from datetime import timedelta +from unittest.mock import call + +from asynctest import patch +import pytest +from pyvizio.const import ( + DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, +) +from pyvizio.vizio import MAX_VOLUME + +from homeassistant.components.media_player import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV, + DOMAIN as MP_DOMAIN, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_SELECT_SOURCE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, +) +from homeassistant.components.vizio.const import CONF_VOLUME_STEP, DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from .const import ( + CURRENT_INPUT, + ENTITY_ID, + INPUT_LIST, + MOCK_SPEAKER_CONFIG, + MOCK_USER_VALID_TV_CONFIG, + NAME, + UNIQUE_ID, + VOLUME_STEP, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def _test_setup( + hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool +) -> None: + """Test Vizio Device entity setup.""" + if vizio_power_state: + ha_power_state = STATE_ON + elif vizio_power_state is False: + ha_power_state = STATE_OFF + else: + ha_power_state = STATE_UNAVAILABLE + + if ha_device_class == DEVICE_CLASS_SPEAKER: + vizio_device_class = VIZIO_DEVICE_CLASS_SPEAKER + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ) + else: + vizio_device_class = VIZIO_DEVICE_CLASS_TV + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_volume", + return_value=int(MAX_VOLUME[vizio_device_class] / 2), + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=vizio_power_state, + ): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + attr = hass.states.get(ENTITY_ID).attributes + assert attr["friendly_name"] == NAME + assert attr["device_class"] == ha_device_class + + assert hass.states.get(ENTITY_ID).state == ha_power_state + if ha_power_state == STATE_ON: + assert attr["source_list"] == INPUT_LIST + assert attr["source"] == CURRENT_INPUT + assert ( + attr["volume_level"] + == float(int(MAX_VOLUME[vizio_device_class] / 2)) + / MAX_VOLUME[vizio_device_class] + ) + + +async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None: + """Test generic Vizio entity setup failure.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=False, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=config, unique_id=UNIQUE_ID) + config_entry.add_to_hass(hass) + 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)) == 0 + + +async def _test_service( + hass: HomeAssistantType, + vizio_func_name: str, + ha_service_name: str, + additional_service_data: dict = 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.media_player.VizioAsync.{vizio_func_name}" + ) as service_call: + await hass.services.async_call( + MP_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) + + +async def test_speaker_on( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when on.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, True) + + +async def test_speaker_off( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when off.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, False) + + +async def test_speaker_unavailable( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio Speaker entity setup when unavailable.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, None) + + +async def test_init_tv_on( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when on.""" + await _test_setup(hass, DEVICE_CLASS_TV, True) + + +async def test_init_tv_off( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when off.""" + await _test_setup(hass, DEVICE_CLASS_TV, False) + + +async def test_init_tv_unavailable( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test Vizio TV entity setup when unavailable.""" + await _test_setup(hass, DEVICE_CLASS_TV, None) + + +async def test_setup_failure_speaker( + hass: HomeAssistantType, vizio_connect: pytest.fixture +) -> None: + """Test speaker entity setup failure.""" + await _test_setup_failure(hass, MOCK_SPEAKER_CONFIG) + + +async def test_setup_failure_tv( + hass: HomeAssistantType, vizio_connect: pytest.fixture +) -> None: + """Test TV entity setup failure.""" + await _test_setup_failure(hass, MOCK_USER_VALID_TV_CONFIG) + + +async def test_services( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test all Vizio media player entity services.""" + await _test_setup(hass, DEVICE_CLASS_TV, True) + + await _test_service(hass, "pow_on", SERVICE_TURN_ON) + await _test_service(hass, "pow_off", SERVICE_TURN_OFF) + await _test_service( + hass, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + ) + await _test_service( + hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} + ) + await _test_service( + hass, "input_switch", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" + ) + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP) + await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN) + await _test_service( + hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + ) + await _test_service( + hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + ) + await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK) + await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK) + + +async def test_options_update( + hass: HomeAssistantType, vizio_connect: pytest.fixture, vizio_update: pytest.fixture +) -> None: + """Test when config entry update event fires.""" + await _test_setup(hass, DEVICE_CLASS_SPEAKER, True) + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.options + new_options = config_entry.options.copy() + updated_options = {CONF_VOLUME_STEP: VOLUME_STEP} + new_options.update(updated_options) + hass.config_entries.async_update_entry( + entry=config_entry, options=new_options, + ) + assert config_entry.options == updated_options + await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, num=VOLUME_STEP) + + +async def _test_update_availability_switch( + hass: HomeAssistantType, + initial_power_state: bool, + final_power_state: bool, + caplog: pytest.fixture, +) -> None: + now = dt_util.utcnow() + future_interval = timedelta(minutes=1) + + # Setup device as if time is right now + with patch("homeassistant.util.dt.utcnow", return_value=now): + await _test_setup(hass, DEVICE_CLASS_SPEAKER, initial_power_state) + + # Clear captured logs so that only availability state changes are captured for + # future assertion + caplog.clear() + + # Fast forward time to future twice to trigger update and assert vizio log message + for i in range(1, 3): + future = now + (future_interval * i) + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", + return_value=final_power_state, + ), patch("homeassistant.util.dt.utcnow", return_value=future), patch( + "homeassistant.util.utcnow", return_value=future + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + if final_power_state is None: + assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE + else: + assert hass.states.get(ENTITY_ID).state != STATE_UNAVAILABLE + + # Ensure connection status messages from vizio.media_player appear exactly once + # (on availability state change) + vizio_log_list = [ + log + for log in caplog.records + if log.name == "homeassistant.components.vizio.media_player" + ] + assert len(vizio_log_list) == 1 + + +async def test_update_unavailable_to_available( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device becomes available after being unavailable.""" + await _test_update_availability_switch(hass, None, True, caplog) + + +async def test_update_available_to_unavailable( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device becomes unavailable after being available.""" + await _test_update_availability_switch(hass, True, None, caplog)