From b4f8fe8d4d8a65b1420009b2b95bae07fe585f21 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Fri, 22 Dec 2023 21:07:47 +1000 Subject: [PATCH] Add media player platform to Tessie (#106214) * Add media platform * Add more props * Fix platform filename * Working * Add a test * Update test and fixture * Refactor media player properties to handle null values * Add comments * add more assertions * Fix test docstring * Use walrus instead. Co-authored-by: Joost Lekkerkerker * Test when media player is idle * Fix tests * Remove None type from volume_level Co-authored-by: Joost Lekkerkerker * Return media position only when a media duration is > 0 * Remove impossible None type * Add snapshot and freezer --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/tessie/__init__.py | 1 + .../components/tessie/media_player.py | 109 ++++++++++++++++++ tests/components/tessie/fixtures/online.json | 14 +-- .../components/tessie/fixtures/vehicles.json | 2 +- .../tessie/snapshots/test_media_player.ambr | 61 ++++++++++ tests/components/tessie/test_media_player.py | 46 ++++++++ 6 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/tessie/media_player.py create mode 100644 tests/components/tessie/snapshots/test_media_player.ambr create mode 100644 tests/components/tessie/test_media_player.py diff --git a/homeassistant/components/tessie/__init__.py b/homeassistant/components/tessie/__init__.py index e7fb41b0788..f344cef2484 100644 --- a/homeassistant/components/tessie/__init__.py +++ b/homeassistant/components/tessie/__init__.py @@ -21,6 +21,7 @@ PLATFORMS = [ Platform.COVER, Platform.DEVICE_TRACKER, Platform.LOCK, + Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/tessie/media_player.py b/homeassistant/components/tessie/media_player.py new file mode 100644 index 00000000000..ffbb6619668 --- /dev/null +++ b/homeassistant/components/tessie/media_player.py @@ -0,0 +1,109 @@ +"""Media Player platform for Tessie integration.""" +from __future__ import annotations + +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, + MediaPlayerState, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import TessieDataUpdateCoordinator +from .entity import TessieEntity + +STATES = { + "Playing": MediaPlayerState.PLAYING, + "Paused": MediaPlayerState.PAUSED, + "Stopped": MediaPlayerState.IDLE, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Tessie Media platform from a config entry.""" + coordinators = hass.data[DOMAIN][entry.entry_id] + + async_add_entities(TessieMediaEntity(coordinator) for coordinator in coordinators) + + +class TessieMediaEntity(TessieEntity, MediaPlayerEntity): + """Vehicle Location Media Class.""" + + _attr_name = None + _attr_device_class = MediaPlayerDeviceClass.SPEAKER + + def __init__( + self, + coordinator: TessieDataUpdateCoordinator, + ) -> None: + """Initialize the media player entity.""" + super().__init__(coordinator, "media") + + @property + def state(self) -> MediaPlayerState: + """State of the player.""" + return STATES.get( + self.get("vehicle_state_media_info_media_playback_status"), + MediaPlayerState.OFF, + ) + + @property + def volume_level(self) -> float: + """Volume level of the media player (0..1).""" + return self.get("vehicle_state_media_info_audio_volume", 0) / self.get( + "vehicle_state_media_info_audio_volume_max", 10.333333 + ) + + @property + def media_duration(self) -> int | None: + """Duration of current playing media in seconds.""" + if duration := self.get("vehicle_state_media_info_now_playing_duration"): + return duration / 1000 + return None + + @property + def media_position(self) -> int | None: + """Position of current playing media in seconds.""" + # Return media position only when a media duration is > 0 + if self.get("vehicle_state_media_info_now_playing_duration"): + return self.get("vehicle_state_media_info_now_playing_elapsed") / 1000 + return None + + @property + def media_title(self) -> str | None: + """Title of current playing media.""" + if title := self.get("vehicle_state_media_info_now_playing_title"): + return title + return None + + @property + def media_artist(self) -> str | None: + """Artist of current playing media, music track only.""" + if artist := self.get("vehicle_state_media_info_now_playing_artist"): + return artist + return None + + @property + def media_album_name(self) -> str | None: + """Album name of current playing media, music track only.""" + if album := self.get("vehicle_state_media_info_now_playing_album"): + return album + return None + + @property + def media_playlist(self) -> str | None: + """Title of Playlist currently playing.""" + if playlist := self.get("vehicle_state_media_info_now_playing_station"): + return playlist + return None + + @property + def source(self) -> str | None: + """Name of the current input source.""" + if source := self.get("vehicle_state_media_info_now_playing_source"): + return source + return None diff --git a/tests/components/tessie/fixtures/online.json b/tests/components/tessie/fixtures/online.json index 8fbab1ab948..863e9bca783 100644 --- a/tests/components/tessie/fixtures/online.json +++ b/tests/components/tessie/fixtures/online.json @@ -204,14 +204,14 @@ "audio_volume": 2.3333, "audio_volume_increment": 0.333333, "audio_volume_max": 10.333333, - "media_playback_status": "Stopped", - "now_playing_album": "", - "now_playing_artist": "", - "now_playing_duration": 0, - "now_playing_elapsed": 0, + "media_playback_status": "Playing", + "now_playing_album": "Album", + "now_playing_artist": "Artist", + "now_playing_duration": 60000, + "now_playing_elapsed": 30000, "now_playing_source": "Spotify", - "now_playing_station": "", - "now_playing_title": "" + "now_playing_station": "Playlist", + "now_playing_title": "Song" }, "media_state": { "remote_control_enabled": false diff --git a/tests/components/tessie/fixtures/vehicles.json b/tests/components/tessie/fixtures/vehicles.json index 05b19261c36..9d2305a04cd 100644 --- a/tests/components/tessie/fixtures/vehicles.json +++ b/tests/components/tessie/fixtures/vehicles.json @@ -222,7 +222,7 @@ "now_playing_artist": "", "now_playing_duration": 0, "now_playing_elapsed": 0, - "now_playing_source": "Spotify", + "now_playing_source": "", "now_playing_station": "", "now_playing_title": "" }, diff --git a/tests/components/tessie/snapshots/test_media_player.ambr b/tests/components/tessie/snapshots/test_media_player.ambr new file mode 100644 index 00000000000..8dc07797d6c --- /dev/null +++ b/tests/components/tessie/snapshots/test_media_player.ambr @@ -0,0 +1,61 @@ +# serializer version: 1 +# name: test_media_player_idle + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'Test', + 'supported_features': , + 'volume_level': 0.22580323309042688, + }), + 'context': , + 'entity_id': 'media_player.test', + 'last_changed': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_media_player_idle.1 + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'Test', + 'supported_features': , + 'volume_level': 0.22580323309042688, + }), + 'context': , + 'entity_id': 'media_player.test', + 'last_changed': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_media_player_playing + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'Test', + 'supported_features': , + 'volume_level': 0.22580323309042688, + }), + 'context': , + 'entity_id': 'media_player.test', + 'last_changed': , + 'last_updated': , + 'state': 'idle', + }) +# --- +# name: test_sensors + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'speaker', + 'friendly_name': 'Test', + 'supported_features': , + 'volume_level': 0.22580323309042688, + }), + 'context': , + 'entity_id': 'media_player.test', + 'last_changed': , + 'last_updated': , + 'state': 'idle', + }) +# --- diff --git a/tests/components/tessie/test_media_player.py b/tests/components/tessie/test_media_player.py new file mode 100644 index 00000000000..8e3e339b560 --- /dev/null +++ b/tests/components/tessie/test_media_player.py @@ -0,0 +1,46 @@ +"""Test the Tessie media player platform.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory +from syrupy import SnapshotAssertion + +from homeassistant.components.tessie.coordinator import TESSIE_SYNC_INTERVAL +from homeassistant.core import HomeAssistant + +from .common import ( + TEST_STATE_OF_ALL_VEHICLES, + TEST_VEHICLE_STATE_ONLINE, + setup_platform, +) + +from tests.common import async_fire_time_changed + +WAIT = timedelta(seconds=TESSIE_SYNC_INTERVAL) + +MEDIA_INFO_1 = TEST_STATE_OF_ALL_VEHICLES["results"][0]["last_state"]["vehicle_state"][ + "media_info" +] +MEDIA_INFO_2 = TEST_VEHICLE_STATE_ONLINE["vehicle_state"]["media_info"] + + +async def test_media_player_idle( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, snapshot: SnapshotAssertion +) -> None: + """Tests that the media player entity is correct when idle.""" + + assert len(hass.states.async_all("media_player")) == 0 + + await setup_platform(hass) + + assert len(hass.states.async_all("media_player")) == 1 + + state = hass.states.get("media_player.test") + assert state == snapshot + + # Trigger coordinator refresh since it has a different fixture. + freezer.tick(WAIT) + async_fire_time_changed(hass) + + state = hass.states.get("media_player.test") + assert state == snapshot