22
33from __future__ import annotations
44
5+ from tesla_fleet_api import VehicleSpecific
56from tesla_fleet_api .const import Scope
67
78from homeassistant .components .media_player import (
1213)
1314from homeassistant .core import HomeAssistant
1415from homeassistant .helpers .entity_platform import AddConfigEntryEntitiesCallback
16+ from homeassistant .helpers .restore_state import RestoreEntity
1517
1618from . import TeslemetryConfigEntry
17- from .entity import TeslemetryVehicleEntity
19+ from .entity import (
20+ TeslemetryRootEntity ,
21+ TeslemetryVehicleEntity ,
22+ TeslemetryVehicleStreamEntity ,
23+ )
1824from .helpers import handle_vehicle_command
1925from .models import TeslemetryVehicleData
2026
2430 "Stopped" : MediaPlayerState .IDLE ,
2531 "Off" : MediaPlayerState .OFF ,
2632}
27- VOLUME_MAX = 11.0
28- VOLUME_STEP = 1.0 / 3
33+ DISPLAY_STATES = {
34+ "On" : MediaPlayerState .IDLE ,
35+ "Accessory" : MediaPlayerState .IDLE ,
36+ "Charging" : MediaPlayerState .OFF ,
37+ "Sentry" : MediaPlayerState .OFF ,
38+ "Off" : MediaPlayerState .OFF ,
39+ }
40+ # Tesla uses 31 steps, in 0.333 increments up to 10.333
41+ VOLUME_STEP = 1 / 31
42+ VOLUME_FACTOR = 31 / 3 # 10.333
2943
3044PARALLEL_UPDATES = 0
3145
@@ -38,68 +52,99 @@ async def async_setup_entry(
3852 """Set up the Teslemetry Media platform from a config entry."""
3953
4054 async_add_entities (
41- TeslemetryMediaEntity (vehicle , Scope .VEHICLE_CMDS in entry .runtime_data .scopes )
55+ TeslemetryPollingMediaEntity (vehicle , entry .runtime_data .scopes )
56+ if vehicle .api .pre2021 or vehicle .firmware < "2025.2.6"
57+ else TeslemetryStreamingMediaEntity (vehicle , entry .runtime_data .scopes )
4258 for vehicle in entry .runtime_data .vehicles
4359 )
4460
4561
46- class TeslemetryMediaEntity (TeslemetryVehicleEntity , MediaPlayerEntity ):
47- """Vehicle media player class."""
62+ class TeslemetryMediaEntity (TeslemetryRootEntity , MediaPlayerEntity ):
63+ """Base vehicle media player class."""
64+
65+ api : VehicleSpecific
4866
4967 _attr_device_class = MediaPlayerDeviceClass .SPEAKER
50- _attr_supported_features = (
51- MediaPlayerEntityFeature .NEXT_TRACK
52- | MediaPlayerEntityFeature .PAUSE
53- | MediaPlayerEntityFeature .PLAY
54- | MediaPlayerEntityFeature .PREVIOUS_TRACK
55- | MediaPlayerEntityFeature .VOLUME_SET
56- )
57- _volume_max : float = VOLUME_MAX
68+ _attr_volume_step = VOLUME_STEP
69+
70+ async def async_set_volume_level (self , volume : float ) -> None :
71+ """Set volume level, range 0..1."""
72+ self .raise_for_scope (Scope .VEHICLE_CMDS )
73+
74+ await handle_vehicle_command (self .api .adjust_volume (volume * VOLUME_FACTOR ))
75+ self ._attr_volume_level = volume
76+ self .async_write_ha_state ()
77+
78+ async def async_media_play (self ) -> None :
79+ """Send play command."""
80+ if self .state != MediaPlayerState .PLAYING :
81+ self .raise_for_scope (Scope .VEHICLE_CMDS )
82+
83+ await handle_vehicle_command (self .api .media_toggle_playback ())
84+ self ._attr_state = MediaPlayerState .PLAYING
85+ self .async_write_ha_state ()
86+
87+ async def async_media_pause (self ) -> None :
88+ """Send pause command."""
89+
90+ if self .state == MediaPlayerState .PLAYING :
91+ self .raise_for_scope (Scope .VEHICLE_CMDS )
92+
93+ await handle_vehicle_command (self .api .media_toggle_playback ())
94+ self ._attr_state = MediaPlayerState .PAUSED
95+ self .async_write_ha_state ()
96+
97+ async def async_media_next_track (self ) -> None :
98+ """Send next track command."""
99+
100+ self .raise_for_scope (Scope .VEHICLE_CMDS )
101+ await handle_vehicle_command (self .api .media_next_track ())
102+
103+ async def async_media_previous_track (self ) -> None :
104+ """Send previous track command."""
105+
106+ self .raise_for_scope (Scope .VEHICLE_CMDS )
107+ await handle_vehicle_command (self .api .media_prev_track ())
108+
109+
110+ class TeslemetryPollingMediaEntity (TeslemetryVehicleEntity , TeslemetryMediaEntity ):
111+ """Polling vehicle media player class."""
58112
59113 def __init__ (
60114 self ,
61115 data : TeslemetryVehicleData ,
62- scoped : bool ,
116+ scopes : list [ Scope ] ,
63117 ) -> None :
64118 """Initialize the media player entity."""
65119 super ().__init__ (data , "media" )
66- self .scoped = scoped
67- if not scoped :
120+
121+ self ._attr_supported_features = (
122+ MediaPlayerEntityFeature .NEXT_TRACK
123+ | MediaPlayerEntityFeature .PAUSE
124+ | MediaPlayerEntityFeature .PLAY
125+ | MediaPlayerEntityFeature .PREVIOUS_TRACK
126+ | MediaPlayerEntityFeature .VOLUME_SET
127+ )
128+ self .scoped = Scope .VEHICLE_CMDS in scopes
129+ if not self .scoped :
68130 self ._attr_supported_features = MediaPlayerEntityFeature (0 )
69131
70132 def _async_update_attrs (self ) -> None :
71133 """Update entity attributes."""
72- self ._volume_max = (
73- self .get ("vehicle_state_media_info_audio_volume_max" ) or VOLUME_MAX
74- )
75- self ._attr_state = STATES .get (
76- self .get ("vehicle_state_media_info_media_playback_status" ) or "Off" ,
77- )
78- self ._attr_volume_step = (
79- 1.0
80- / self ._volume_max
81- / (
82- self .get ("vehicle_state_media_info_audio_volume_increment" )
83- or VOLUME_STEP
84- )
85- )
134+ state = self .get ("vehicle_state_media_info_media_playback_status" )
135+ self ._attr_state = STATES .get (state ) if state else None
136+ self ._attr_volume_level = (
137+ self .get ("vehicle_state_media_info_audio_volume" ) or 0
138+ ) / VOLUME_FACTOR
86139
87- if volume := self .get ("vehicle_state_media_info_audio_volume" ):
88- self ._attr_volume_level = volume / self ._volume_max
89- else :
90- self ._attr_volume_level = None
140+ duration = self .get ("vehicle_state_media_info_now_playing_duration" )
141+ self ._attr_media_duration = duration / 1000 if duration is not None else None
91142
92- if duration := self .get ("vehicle_state_media_info_now_playing_duration" ):
93- self ._attr_media_duration = duration / 1000
94- else :
95- self ._attr_media_duration = None
96-
97- if duration and (
98- position := self .get ("vehicle_state_media_info_now_playing_elapsed" )
99- ):
100- self ._attr_media_position = position / 1000
101- else :
102- self ._attr_media_position = None
143+ # Return media position only when a media duration is > 0.
144+ elapsed = self .get ("vehicle_state_media_info_now_playing_elapsed" )
145+ self ._attr_media_position = (
146+ elapsed / 1000 if duration and elapsed is not None else None
147+ )
103148
104149 self ._attr_media_title = self .get ("vehicle_state_media_info_now_playing_title" )
105150 self ._attr_media_artist = self .get (
@@ -113,42 +158,151 @@ def _async_update_attrs(self) -> None:
113158 )
114159 self ._attr_source = self .get ("vehicle_state_media_info_now_playing_source" )
115160
116- async def async_set_volume_level (self , volume : float ) -> None :
117- """Set volume level, range 0..1."""
118- self .raise_for_scope (Scope .VEHICLE_CMDS )
119- await self .wake_up_if_asleep ()
120- await handle_vehicle_command (
121- self .api .adjust_volume (int (volume * self ._volume_max ))
161+
162+ class TeslemetryStreamingMediaEntity (
163+ TeslemetryVehicleStreamEntity , TeslemetryMediaEntity , RestoreEntity
164+ ):
165+ """Streaming vehicle media player class."""
166+
167+ def __init__ (
168+ self ,
169+ data : TeslemetryVehicleData ,
170+ scopes : list [Scope ],
171+ ) -> None :
172+ """Initialize the media player entity."""
173+ super ().__init__ (data , "media" )
174+
175+ self ._attr_supported_features = (
176+ MediaPlayerEntityFeature .NEXT_TRACK
177+ | MediaPlayerEntityFeature .PAUSE
178+ | MediaPlayerEntityFeature .PLAY
179+ | MediaPlayerEntityFeature .PREVIOUS_TRACK
180+ | MediaPlayerEntityFeature .VOLUME_SET
122181 )
123- self ._attr_volume_level = volume
124- self .async_write_ha_state ()
182+ self .scoped = Scope .VEHICLE_CMDS in scopes
183+ if not self .scoped :
184+ self ._attr_supported_features = MediaPlayerEntityFeature (0 )
185+
186+ async def async_added_to_hass (self ) -> None :
187+ """Call when entity is added to hass."""
188+
189+ await super ().async_added_to_hass ()
190+ if (state := await self .async_get_last_state ()) is not None :
191+ try :
192+ self ._attr_state = MediaPlayerState (state .state )
193+ except ValueError :
194+ self ._attr_state = None
195+ self ._attr_volume_level = state .attributes .get ("volume_level" )
196+ self ._attr_media_title = state .attributes .get ("media_title" )
197+ self ._attr_media_artist = state .attributes .get ("media_artist" )
198+ self ._attr_media_album_name = state .attributes .get ("media_album_name" )
199+ self ._attr_media_playlist = state .attributes .get ("media_playlist" )
200+ self ._attr_media_duration = state .attributes .get ("media_duration" )
201+ self ._attr_media_position = state .attributes .get ("media_position" )
202+ self ._attr_source = state .attributes .get ("source" )
125203
126- async def async_media_play (self ) -> None :
127- """Send play command."""
128- if self .state != MediaPlayerState .PLAYING :
129- self .raise_for_scope (Scope .VEHICLE_CMDS )
130- await self .wake_up_if_asleep ()
131- await handle_vehicle_command (self .api .media_toggle_playback ())
132- self ._attr_state = MediaPlayerState .PLAYING
133204 self .async_write_ha_state ()
134205
135- async def async_media_pause (self ) -> None :
136- """Send pause command."""
137- if self .state == MediaPlayerState .PLAYING :
138- self .raise_for_scope (Scope .VEHICLE_CMDS )
139- await self .wake_up_if_asleep ()
140- await handle_vehicle_command (self .api .media_toggle_playback ())
141- self ._attr_state = MediaPlayerState .PAUSED
206+ self .async_on_remove (
207+ self .vehicle .stream_vehicle .listen_CenterDisplay (
208+ self ._async_handle_center_display
209+ )
210+ )
211+ self .async_on_remove (
212+ self .vehicle .stream_vehicle .listen_MediaPlaybackStatus (
213+ self ._async_handle_media_playback_status
214+ )
215+ )
216+ self .async_on_remove (
217+ self .vehicle .stream_vehicle .listen_MediaPlaybackSource (
218+ self ._async_handle_media_playback_source
219+ )
220+ )
221+ self .async_on_remove (
222+ self .vehicle .stream_vehicle .listen_MediaAudioVolume (
223+ self ._async_handle_media_audio_volume
224+ )
225+ )
226+ self .async_on_remove (
227+ self .vehicle .stream_vehicle .listen_MediaNowPlayingDuration (
228+ self ._async_handle_media_now_playing_duration
229+ )
230+ )
231+ self .async_on_remove (
232+ self .vehicle .stream_vehicle .listen_MediaNowPlayingElapsed (
233+ self ._async_handle_media_now_playing_elapsed
234+ )
235+ )
236+ self .async_on_remove (
237+ self .vehicle .stream_vehicle .listen_MediaNowPlayingArtist (
238+ self ._async_handle_media_now_playing_artist
239+ )
240+ )
241+ self .async_on_remove (
242+ self .vehicle .stream_vehicle .listen_MediaNowPlayingAlbum (
243+ self ._async_handle_media_now_playing_album
244+ )
245+ )
246+ self .async_on_remove (
247+ self .vehicle .stream_vehicle .listen_MediaNowPlayingTitle (
248+ self ._async_handle_media_now_playing_title
249+ )
250+ )
251+ self .async_on_remove (
252+ self .vehicle .stream_vehicle .listen_MediaNowPlayingStation (
253+ self ._async_handle_media_now_playing_station
254+ )
255+ )
256+
257+ def _async_handle_center_display (self , value : str | None ) -> None :
258+ """Update entity attributes."""
259+ if value is not None :
260+ self ._attr_state = DISPLAY_STATES .get (value )
142261 self .async_write_ha_state ()
143262
144- async def async_media_next_track (self ) -> None :
145- """Send next track command."""
146- self .raise_for_scope (Scope .VEHICLE_CMDS )
147- await self .wake_up_if_asleep ()
148- await handle_vehicle_command (self .api .media_next_track ())
263+ def _async_handle_media_playback_status (self , value : str | None ) -> None :
264+ """Update entity attributes."""
265+ self ._attr_state = MediaPlayerState .OFF if value is None else STATES .get (value )
266+ self .async_write_ha_state ()
149267
150- async def async_media_previous_track (self ) -> None :
151- """Send previous track command."""
152- self .raise_for_scope (Scope .VEHICLE_CMDS )
153- await self .wake_up_if_asleep ()
154- await handle_vehicle_command (self .api .media_prev_track ())
268+ def _async_handle_media_playback_source (self , value : str | None ) -> None :
269+ """Update entity attributes."""
270+ self ._attr_source = value
271+ self .async_write_ha_state ()
272+
273+ def _async_handle_media_audio_volume (self , value : float | None ) -> None :
274+ """Update entity attributes."""
275+ self ._attr_volume_level = None if value is None else value / VOLUME_FACTOR
276+ self .async_write_ha_state ()
277+
278+ def _async_handle_media_now_playing_duration (self , value : int | None ) -> None :
279+ """Update entity attributes."""
280+ self ._attr_media_duration = None if value is None else int (value / 1000 )
281+ self .async_write_ha_state ()
282+
283+ def _async_handle_media_now_playing_elapsed (self , value : int | None ) -> None :
284+ """Update entity attributes."""
285+ self ._attr_media_position = None if value is None else int (value / 1000 )
286+ self .async_write_ha_state ()
287+
288+ def _async_handle_media_now_playing_artist (self , value : str | None ) -> None :
289+ """Update entity attributes."""
290+ self ._attr_media_artist = value # Check if this is album artist or not
291+ self .async_write_ha_state ()
292+
293+ def _async_handle_media_now_playing_album (self , value : str | None ) -> None :
294+ """Update entity attributes."""
295+ self ._attr_media_album_name = value
296+ self .async_write_ha_state ()
297+
298+ def _async_handle_media_now_playing_title (self , value : str | None ) -> None :
299+ """Update entity attributes."""
300+ self ._attr_media_title = value
301+ self .async_write_ha_state ()
302+
303+ def _async_handle_media_now_playing_station (self , value : str | None ) -> None :
304+ """Update entity attributes."""
305+ self ._attr_media_channel = (
306+ value # could also be _attr_media_playlist when Spotify
307+ )
308+ self .async_write_ha_state ()
0 commit comments