diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 05caae0ec08..c60ae4582ad 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -33,7 +33,6 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -142,16 +141,9 @@ class VizioDevice(MediaPlayerEntity): self._config_entry = config_entry self._apps_coordinator = apps_coordinator - self._name = name - self._state = None - self._volume_level = None self._volume_step = config_entry.options[CONF_VOLUME_STEP] - self._is_volume_muted = None self._current_input = None - self._current_app = None self._current_app_config = None - self._current_sound_mode = None - self._available_sound_modes = [] self._available_inputs = [] self._available_apps = [] self._all_apps = apps_coordinator.data if apps_coordinator else None @@ -159,14 +151,19 @@ class VizioDevice(MediaPlayerEntity): self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( CONF_ADDITIONAL_CONFIGS, [] ) - self._device_class = device_class - self._supported_commands = SUPPORTED_COMMANDS[device_class] self._device = device self._max_volume = float(self._device.get_max_volume()) - self._icon = ICON[device_class] - self._available = True - self._model = None - self._sw_version = None + + # Entity class attributes that will change with each update (we only include + # the ones that are initialized differently from the defaults) + self._attr_sound_mode_list = [] + self._attr_supported_features = SUPPORTED_COMMANDS[device_class] + + # Entity class attributes that will not change + self._attr_name = name + self._attr_icon = ICON[device_class] + self._attr_unique_id = self._config_entry.unique_id + self._attr_device_class = device_class def _apps_list(self, apps: list[str]) -> list[str]: """Return process apps list based on configured filters.""" @@ -183,64 +180,67 @@ class VizioDevice(MediaPlayerEntity): is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: - if self._available: + if self._attr_available: _LOGGER.warning( "Lost connection to %s", self._config_entry.data[CONF_HOST] ) - self._available = False + self._attr_available = False return - if not self._available: + if not self._attr_available: _LOGGER.info( "Restored connection to %s", self._config_entry.data[CONF_HOST] ) - self._available = True + self._attr_available = True - if not self._model: - self._model = await self._device.get_model_name(log_api_exception=False) - - if not self._sw_version: - self._sw_version = await self._device.get_version(log_api_exception=False) + if not self._attr_device_info: + self._attr_device_info = { + "identifiers": {(DOMAIN, self._attr_unique_id)}, + "name": self._attr_name, + "manufacturer": "VIZIO", + "model": await self._device.get_model_name(log_api_exception=False), + "sw_version": await self._device.get_version(log_api_exception=False), + } if not is_on: - self._state = STATE_OFF - self._volume_level = None - self._is_volume_muted = None + self._attr_state = STATE_OFF + self._attr_volume_level = None + self._attr_is_volume_muted = None self._current_input = None - self._current_app = None + self._attr_app_name = None self._current_app_config = None - self._current_sound_mode = None + self._attr_sound_mode = None return - self._state = STATE_ON + self._attr_state = STATE_ON audio_settings = await self._device.get_all_settings( VIZIO_AUDIO_SETTINGS, log_api_exception=False ) if audio_settings: - self._volume_level = float(audio_settings[VIZIO_VOLUME]) / self._max_volume + self._attr_volume_level = ( + float(audio_settings[VIZIO_VOLUME]) / self._max_volume + ) if VIZIO_MUTE in audio_settings: - self._is_volume_muted = ( + self._attr_is_volume_muted = ( audio_settings[VIZIO_MUTE].lower() == VIZIO_MUTE_ON ) else: - self._is_volume_muted = None + self._attr_is_volume_muted = None if VIZIO_SOUND_MODE in audio_settings: - self._supported_commands |= SUPPORT_SELECT_SOUND_MODE - self._current_sound_mode = audio_settings[VIZIO_SOUND_MODE] - if not self._available_sound_modes: - self._available_sound_modes = ( - await self._device.get_setting_options( - VIZIO_AUDIO_SETTINGS, - VIZIO_SOUND_MODE, - log_api_exception=False, - ) + self._attr_supported_features |= SUPPORT_SELECT_SOUND_MODE + self._attr_sound_mode = audio_settings[VIZIO_SOUND_MODE] + if not self._attr_sound_mode_list: + self._attr_sound_mode_list = await self._device.get_setting_options( + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + log_api_exception=False, ) else: # Explicitly remove SUPPORT_SELECT_SOUND_MODE from supported features - self._supported_commands &= ~SUPPORT_SELECT_SOUND_MODE + self._attr_supported_features &= ~SUPPORT_SELECT_SOUND_MODE input_ = await self._device.get_current_input(log_api_exception=False) if input_: @@ -255,7 +255,7 @@ class VizioDevice(MediaPlayerEntity): self._available_inputs = [input_.name for input_ in inputs] # Return before setting app variables if INPUT_APPS isn't in available inputs - if self._device_class == DEVICE_CLASS_SPEAKER or not any( + if self._attr_device_class == DEVICE_CLASS_SPEAKER or not any( app for app in INPUT_APPS if app in self._available_inputs ): return @@ -268,13 +268,13 @@ class VizioDevice(MediaPlayerEntity): log_api_exception=False ) - self._current_app = find_app_name( + self._attr_app_name = find_app_name( self._current_app_config, [APP_HOME, *self._all_apps, *self._additional_app_configs], ) - if self._current_app == NO_APP_RUNNING: - self._current_app = None + if self._attr_app_name == NO_APP_RUNNING: + self._attr_app_name = None def _get_additional_app_names(self) -> list[dict[str, Any]]: """Return list of additional apps that were included in configuration.yaml.""" @@ -331,46 +331,16 @@ class VizioDevice(MediaPlayerEntity): self._all_apps = self._apps_coordinator.data self.async_write_ha_state() - if self._device_class == DEVICE_CLASS_TV: + if self._attr_device_class == DEVICE_CLASS_TV: self.async_on_remove( self._apps_coordinator.async_add_listener(apps_list_update) ) - @property - def available(self) -> bool: - """Return the availabiliity of the device.""" - return self._available - - @property - def state(self) -> str | None: - """Return the state of the device.""" - return self._state - - @property - def name(self) -> str: - """Return the name of the device.""" - return self._name - - @property - def icon(self) -> str: - """Return the icon of the device.""" - return self._icon - - @property - def volume_level(self) -> float | None: - """Return the volume level of the device.""" - return self._volume_level - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._is_volume_muted - @property def source(self) -> str | None: """Return current input of the device.""" - if self._current_app is not None and self._current_input in INPUT_APPS: - return self._current_app + if self._attr_app_name is not None and self._current_input in INPUT_APPS: + return self._attr_app_name return self._current_input @@ -378,7 +348,7 @@ class VizioDevice(MediaPlayerEntity): def source_list(self) -> list[str]: """Return list of available inputs of the device.""" # If Smartcast app is in input list, and the app list has been retrieved, - # show the combination with , otherwise just return inputs + # show the combination with, otherwise just return inputs if self._available_apps: return [ *( @@ -408,50 +378,9 @@ class VizioDevice(MediaPlayerEntity): return None - @property - def app_name(self) -> str | None: - """Return the friendly name of the current app.""" - return self._current_app - - @property - def supported_features(self) -> int: - """Flag device features that are supported.""" - return self._supported_commands - - @property - def unique_id(self) -> str: - """Return the unique id of the device.""" - return self._config_entry.unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information.""" - return { - "identifiers": {(DOMAIN, self._config_entry.unique_id)}, - "name": self.name, - "manufacturer": "VIZIO", - "model": self._model, - "sw_version": self._sw_version, - } - - @property - def device_class(self) -> str: - """Return device class for entity.""" - return self._device_class - - @property - def sound_mode(self) -> str | None: - """Name of the current sound mode.""" - return self._current_sound_mode - - @property - def sound_mode_list(self) -> list[str] | None: - """List of available sound modes.""" - return self._available_sound_modes - async def async_select_sound_mode(self, sound_mode): """Select sound mode.""" - if sound_mode in self._available_sound_modes: + if sound_mode in self._attr_sound_mode_list: await self._device.set_setting( VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, @@ -471,10 +400,10 @@ class VizioDevice(MediaPlayerEntity): """Mute the volume.""" if mute: await self._device.mute_on(log_api_exception=False) - self._is_volume_muted = True + self._attr_is_volume_muted = True else: await self._device.mute_off(log_api_exception=False) - self._is_volume_muted = False + self._attr_is_volume_muted = False async def async_media_previous_track(self) -> None: """Send previous channel command.""" @@ -506,29 +435,29 @@ class VizioDevice(MediaPlayerEntity): """Increase volume of the device.""" await self._device.vol_up(num=self._volume_step, log_api_exception=False) - if self._volume_level is not None: - self._volume_level = min( - 1.0, self._volume_level + self._volume_step / self._max_volume + if self._attr_volume_level is not None: + self._attr_volume_level = min( + 1.0, self._attr_volume_level + self._volume_step / self._max_volume ) async def async_volume_down(self) -> None: """Decrease volume of the device.""" await self._device.vol_down(num=self._volume_step, log_api_exception=False) - if self._volume_level is not None: - self._volume_level = max( - 0.0, self._volume_level - self._volume_step / self._max_volume + if self._attr_volume_level is not None: + self._attr_volume_level = max( + 0.0, self._attr_volume_level - self._volume_step / self._max_volume ) async def async_set_volume_level(self, volume: float) -> None: """Set volume level.""" - if self._volume_level is not None: - if volume > self._volume_level: - num = int(self._max_volume * (volume - self._volume_level)) + if self._attr_volume_level is not None: + if volume > self._attr_volume_level: + num = int(self._max_volume * (volume - self._attr_volume_level)) await self._device.vol_up(num=num, log_api_exception=False) - self._volume_level = volume + self._attr_volume_level = volume - elif volume < self._volume_level: - num = int(self._max_volume * (self._volume_level - volume)) + elif volume < self._attr_volume_level: + num = int(self._max_volume * (self._attr_volume_level - volume)) await self._device.vol_down(num=num, log_api_exception=False) - self._volume_level = volume + self._attr_volume_level = volume diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index c137f112976..7a030ade53f 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -38,6 +38,7 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, ) +from homeassistant.components.media_player.const import ATTR_INPUT_SOURCE_LIST from homeassistant.components.vizio import validate_apps from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, @@ -102,8 +103,8 @@ def _get_ha_power_state(vizio_power_state: bool | None) -> str: def _assert_sources_and_volume(attr: dict[str, Any], vizio_device_class: str) -> None: """Assert source list, source, and volume level based on attr dict and device class.""" - assert attr["source_list"] == INPUT_LIST - assert attr["source"] == CURRENT_INPUT + 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)) @@ -236,7 +237,7 @@ def _assert_source_list_with_apps( if app_to_remove in list_to_test: list_to_test.remove(app_to_remove) - assert attr["source_list"] == list_to_test + assert attr[ATTR_INPUT_SOURCE_LIST] == list_to_test async def _test_service( @@ -533,8 +534,8 @@ async def test_setup_with_apps( ): 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["source_list"] - assert attr["source"] == CURRENT_APP + 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 @@ -561,8 +562,8 @@ async def test_setup_with_apps_include( ): attr = hass.states.get(ENTITY_ID).attributes _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + [CURRENT_APP]), attr) - assert CURRENT_APP in attr["source_list"] - assert attr["source"] == CURRENT_APP + 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 @@ -579,8 +580,8 @@ async def test_setup_with_apps_exclude( ): attr = hass.states.get(ENTITY_ID).attributes _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + [CURRENT_APP]), attr) - assert CURRENT_APP in attr["source_list"] - assert attr["source"] == CURRENT_APP + 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 @@ -598,7 +599,7 @@ async def test_setup_with_apps_additional_apps_config( ADDITIONAL_APP_CONFIG["config"], ): attr = hass.states.get(ENTITY_ID).attributes - assert attr["source_list"].count(CURRENT_APP) == 1 + assert attr[ATTR_INPUT_SOURCE_LIST].count(CURRENT_APP) == 1 _assert_source_list_with_apps( list( INPUT_LIST_WITH_APPS @@ -613,8 +614,8 @@ async def test_setup_with_apps_additional_apps_config( ), attr, ) - assert ADDITIONAL_APP_CONFIG["name"] in attr["source_list"] - assert attr["source"] == ADDITIONAL_APP_CONFIG["name"] + 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 @@ -673,7 +674,7 @@ async def test_setup_with_unknown_app_config( ): attr = hass.states.get(ENTITY_ID).attributes _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) - assert attr["source"] == UNKNOWN_APP + assert attr[ATTR_INPUT_SOURCE] == UNKNOWN_APP assert attr["app_name"] == UNKNOWN_APP assert attr["app_id"] == UNKNOWN_APP_CONFIG @@ -690,7 +691,7 @@ async def test_setup_with_no_running_app( ): attr = hass.states.get(ENTITY_ID).attributes _assert_source_list_with_apps(list(INPUT_LIST_WITH_APPS + APP_NAME_LIST), attr) - assert attr["source"] == "CAST" + assert attr[ATTR_INPUT_SOURCE] == "CAST" assert "app_id" not in attr assert "app_name" not in attr @@ -735,7 +736,7 @@ async def test_apps_update( ): # 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"] + sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] apps = list(set(sources) - set(INPUT_LIST)) assert len(apps) == len(APPS) @@ -747,6 +748,6 @@ async def test_apps_update( 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"] + sources = hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] apps = list(set(sources) - set(INPUT_LIST)) assert len(apps) == len(APP_LIST)