diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 72bd9b6b08a..6c8a46c358f 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -27,6 +27,18 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv +SERVICE_UPDATE_SETTING = "update_setting" + +ATTR_SETTING_TYPE = "setting_type" +ATTR_SETTING_NAME = "setting_name" +ATTR_NEW_VALUE = "new_value" + +UPDATE_SETTING_SCHEMA = { + vol.Required(ATTR_SETTING_TYPE): cv.string, + vol.Required(ATTR_SETTING_NAME): cv.string, + vol.Required(ATTR_NEW_VALUE): vol.Or(vol.Coerce(int), cv.string), +} + CONF_ADDITIONAL_CONFIGS = "additional_configs" CONF_APP_ID = "APP_ID" CONF_APPS = "apps" @@ -66,6 +78,8 @@ SUPPORTED_COMMANDS = { VIZIO_SOUND_MODE = "eq" VIZIO_AUDIO_SETTINGS = "audio" VIZIO_MUTE_ON = "on" +VIZIO_VOLUME = "volume" +VIZIO_MUTE = "mute" # Since Vizio component relies on device class, this dict will ensure that changes to # the values of DEVICE_CLASS_SPEAKER or DEVICE_CLASS_TV don't require changes to pyvizio. diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 4a93e7886ef..28201b51db7 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,7 @@ """Vizio SmartCast Device support.""" from datetime import timedelta import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, Union from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -24,6 +24,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -41,16 +42,20 @@ from .const import ( DEVICE_ID, DOMAIN, ICON, + SERVICE_UPDATE_SETTING, SUPPORTED_COMMANDS, + UPDATE_SETTING_SCHEMA, VIZIO_AUDIO_SETTINGS, VIZIO_DEVICE_CLASSES, + VIZIO_MUTE, VIZIO_MUTE_ON, VIZIO_SOUND_MODE, + VIZIO_VOLUME, ) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) PARALLEL_UPDATES = 0 @@ -113,6 +118,10 @@ async def async_setup_entry( entity = VizioDevice(config_entry, device, name, device_class) async_add_entities([entity], update_before_add=True) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_UPDATE_SETTING, UPDATE_SETTING_SCHEMA, "async_update_setting" + ) class VizioDevice(MediaPlayerEntity): @@ -203,10 +212,13 @@ class VizioDevice(MediaPlayerEntity): audio_settings = await self._device.get_all_settings( VIZIO_AUDIO_SETTINGS, log_api_exception=False ) + if audio_settings: - self._volume_level = float(audio_settings["volume"]) / self._max_volume - if "mute" in audio_settings: - self._is_volume_muted = audio_settings["mute"].lower() == VIZIO_MUTE_ON + self._volume_level = float(audio_settings[VIZIO_VOLUME]) / self._max_volume + if VIZIO_MUTE in audio_settings: + self._is_volume_muted = ( + audio_settings[VIZIO_MUTE].lower() == VIZIO_MUTE_ON + ) else: self._is_volume_muted = None @@ -274,6 +286,16 @@ class VizioDevice(MediaPlayerEntity): self._volume_step = config_entry.options[CONF_VOLUME_STEP] self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) + async def async_update_setting( + self, setting_type: str, setting_name: str, new_value: Union[int, str] + ) -> None: + """Update a setting when update_setting service is called.""" + await self._device.set_setting( + setting_type.lower().replace(" ", "_"), + setting_name.lower().replace(" ", "_"), + new_value, + ) + async def async_added_to_hass(self): """Register callbacks when entity is added.""" # Register callback for when config entry is updated. diff --git a/homeassistant/components/vizio/services.yaml b/homeassistant/components/vizio/services.yaml new file mode 100644 index 00000000000..c652b622de0 --- /dev/null +++ b/homeassistant/components/vizio/services.yaml @@ -0,0 +1,15 @@ +update_setting: + description: Update the value of a setting on a particular Vizio media player device. + fields: + entity_id: + description: Name of an entity to send command. + example: "media_player.vizio_smartcast" + setting_type: + description: The type of setting to be changed. Available types are listed in the `setting_types` property. + example: "audio" + setting_name: + description: The name of the setting to be changed. Available settings for a given setting_type are listed in the `_settings` property. + example: "eq" + new_value: + description: The new value for the setting + example: "Music" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 7a2ff1d1c7a..a4ef7c1a1ac 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -41,6 +41,7 @@ from homeassistant.components.vizio.const import ( CONF_APPS, CONF_VOLUME_STEP, DOMAIN, + SERVICE_UPDATE_SETTING, VIZIO_SCHEMA, ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -174,13 +175,14 @@ async def _test_setup_speaker( unique_id=UNIQUE_ID, ) + audio_settings = { + "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_SPEAKER] / 2), + "mute": "Off", + "eq": CURRENT_EQ, + } + async with _cm_for_test_setup_without_apps( - { - "volume": int(MAX_VOLUME[VIZIO_DEVICE_CLASS_SPEAKER] / 2), - "mute": "Off", - "eq": CURRENT_EQ, - }, - vizio_power_state, + audio_settings, vizio_power_state, ): with patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", @@ -248,6 +250,7 @@ async def _test_setup_failure(hass: HomeAssistantType, config: str) -> None: async def _test_service( hass: HomeAssistantType, + domain: str, vizio_func_name: str, ha_service_name: str, additional_service_data: Optional[Dict[str, Any]], @@ -263,7 +266,7 @@ async def _test_service( 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, + domain, ha_service_name, service_data=service_data, blocking=True, ) assert service_call.called @@ -347,29 +350,49 @@ async def test_services( """Test all Vizio media player entity services.""" await _test_setup_tv(hass, True) - await _test_service(hass, "pow_on", SERVICE_TURN_ON, None) - await _test_service(hass, "pow_off", SERVICE_TURN_OFF, 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, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} + hass, MP_DOMAIN, "mute_on", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True} ) await _test_service( - hass, "mute_off", SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False} + hass, + MP_DOMAIN, + "mute_off", + SERVICE_VOLUME_MUTE, + {ATTR_MEDIA_VOLUME_MUTED: False}, ) await _test_service( - hass, "set_input", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "USB"}, "USB" + hass, + MP_DOMAIN, + "set_input", + SERVICE_SELECT_SOURCE, + {ATTR_INPUT_SOURCE: "USB"}, + "USB", ) - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None) - await _test_service(hass, "vol_down", SERVICE_VOLUME_DOWN, None) + await _test_service(hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None) + await _test_service(hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( - hass, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} ) await _test_service( - hass, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} ) - await _test_service(hass, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) - await _test_service(hass, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) + await _test_service(hass, MP_DOMAIN, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) + await _test_service(hass, MP_DOMAIN, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) await _test_service( - hass, "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"} + hass, + MP_DOMAIN, + "set_setting", + SERVICE_SELECT_SOUND_MODE, + {ATTR_SOUND_MODE: "Music"}, + ) + await _test_service( + hass, + DOMAIN, + "set_setting", + SERVICE_UPDATE_SETTING, + {"setting_type": "Audio", "setting_name": "EQ", "new_value": "Music"}, ) @@ -389,7 +412,9 @@ async def test_options_update( entry=config_entry, options=new_options, ) assert config_entry.options == updated_options - await _test_service(hass, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP) + await _test_service( + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP + ) async def _test_update_availability_switch( @@ -474,6 +499,7 @@ async def test_setup_with_apps( await _test_service( hass, + MP_DOMAIN, "launch_app", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP}, @@ -550,6 +576,7 @@ async def test_setup_with_apps_additional_apps_config( await _test_service( hass, + MP_DOMAIN, "launch_app", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "Netflix"}, @@ -557,6 +584,7 @@ async def test_setup_with_apps_additional_apps_config( ) await _test_service( hass, + MP_DOMAIN, "launch_app_config", SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: CURRENT_APP},