mirror of
https://github.com/home-assistant/core.git
synced 2025-04-19 14:57:52 +00:00
Add streaming media platform to Teslemetry (#140482)
* Update media player * Add media player platform with tests and bump firmware
This commit is contained in:
parent
de0efd61d1
commit
251bb30dc7
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from tesla_fleet_api import VehicleSpecific
|
||||
from tesla_fleet_api.const import Scope
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
@ -12,9 +13,14 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import TeslemetryConfigEntry
|
||||
from .entity import TeslemetryVehicleEntity
|
||||
from .entity import (
|
||||
TeslemetryRootEntity,
|
||||
TeslemetryVehicleEntity,
|
||||
TeslemetryVehicleStreamEntity,
|
||||
)
|
||||
from .helpers import handle_vehicle_command
|
||||
from .models import TeslemetryVehicleData
|
||||
|
||||
@ -24,8 +30,16 @@ STATES = {
|
||||
"Stopped": MediaPlayerState.IDLE,
|
||||
"Off": MediaPlayerState.OFF,
|
||||
}
|
||||
VOLUME_MAX = 11.0
|
||||
VOLUME_STEP = 1.0 / 3
|
||||
DISPLAY_STATES = {
|
||||
"On": MediaPlayerState.IDLE,
|
||||
"Accessory": MediaPlayerState.IDLE,
|
||||
"Charging": MediaPlayerState.OFF,
|
||||
"Sentry": MediaPlayerState.OFF,
|
||||
"Off": MediaPlayerState.OFF,
|
||||
}
|
||||
# Tesla uses 31 steps, in 0.333 increments up to 10.333
|
||||
VOLUME_STEP = 1 / 31
|
||||
VOLUME_FACTOR = 31 / 3 # 10.333
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@ -38,68 +52,99 @@ async def async_setup_entry(
|
||||
"""Set up the Teslemetry Media platform from a config entry."""
|
||||
|
||||
async_add_entities(
|
||||
TeslemetryMediaEntity(vehicle, Scope.VEHICLE_CMDS in entry.runtime_data.scopes)
|
||||
TeslemetryPollingMediaEntity(vehicle, entry.runtime_data.scopes)
|
||||
if vehicle.api.pre2021 or vehicle.firmware < "2025.2.6"
|
||||
else TeslemetryStreamingMediaEntity(vehicle, entry.runtime_data.scopes)
|
||||
for vehicle in entry.runtime_data.vehicles
|
||||
)
|
||||
|
||||
|
||||
class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
|
||||
"""Vehicle media player class."""
|
||||
class TeslemetryMediaEntity(TeslemetryRootEntity, MediaPlayerEntity):
|
||||
"""Base vehicle media player class."""
|
||||
|
||||
api: VehicleSpecific
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
)
|
||||
_volume_max: float = VOLUME_MAX
|
||||
_attr_volume_step = VOLUME_STEP
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
|
||||
await handle_vehicle_command(self.api.adjust_volume(volume * VOLUME_FACTOR))
|
||||
self._attr_volume_level = volume
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state != MediaPlayerState.PLAYING:
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
|
||||
await handle_vehicle_command(self.api.media_toggle_playback())
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
|
||||
if self.state == MediaPlayerState.PLAYING:
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
|
||||
await handle_vehicle_command(self.api.media_toggle_playback())
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await handle_vehicle_command(self.api.media_next_track())
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await handle_vehicle_command(self.api.media_prev_track())
|
||||
|
||||
|
||||
class TeslemetryPollingMediaEntity(TeslemetryVehicleEntity, TeslemetryMediaEntity):
|
||||
"""Polling vehicle media player class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: TeslemetryVehicleData,
|
||||
scoped: bool,
|
||||
scopes: list[Scope],
|
||||
) -> None:
|
||||
"""Initialize the media player entity."""
|
||||
super().__init__(data, "media")
|
||||
self.scoped = scoped
|
||||
if not scoped:
|
||||
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
)
|
||||
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||
if not self.scoped:
|
||||
self._attr_supported_features = MediaPlayerEntityFeature(0)
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._volume_max = (
|
||||
self.get("vehicle_state_media_info_audio_volume_max") or VOLUME_MAX
|
||||
)
|
||||
self._attr_state = STATES.get(
|
||||
self.get("vehicle_state_media_info_media_playback_status") or "Off",
|
||||
)
|
||||
self._attr_volume_step = (
|
||||
1.0
|
||||
/ self._volume_max
|
||||
/ (
|
||||
self.get("vehicle_state_media_info_audio_volume_increment")
|
||||
or VOLUME_STEP
|
||||
)
|
||||
)
|
||||
state = self.get("vehicle_state_media_info_media_playback_status")
|
||||
self._attr_state = STATES.get(state) if state else None
|
||||
self._attr_volume_level = (
|
||||
self.get("vehicle_state_media_info_audio_volume") or 0
|
||||
) / VOLUME_FACTOR
|
||||
|
||||
if volume := self.get("vehicle_state_media_info_audio_volume"):
|
||||
self._attr_volume_level = volume / self._volume_max
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
duration = self.get("vehicle_state_media_info_now_playing_duration")
|
||||
self._attr_media_duration = duration / 1000 if duration is not None else None
|
||||
|
||||
if duration := self.get("vehicle_state_media_info_now_playing_duration"):
|
||||
self._attr_media_duration = duration / 1000
|
||||
else:
|
||||
self._attr_media_duration = None
|
||||
|
||||
if duration and (
|
||||
position := self.get("vehicle_state_media_info_now_playing_elapsed")
|
||||
):
|
||||
self._attr_media_position = position / 1000
|
||||
else:
|
||||
self._attr_media_position = None
|
||||
# Return media position only when a media duration is > 0.
|
||||
elapsed = self.get("vehicle_state_media_info_now_playing_elapsed")
|
||||
self._attr_media_position = (
|
||||
elapsed / 1000 if duration and elapsed is not None else None
|
||||
)
|
||||
|
||||
self._attr_media_title = self.get("vehicle_state_media_info_now_playing_title")
|
||||
self._attr_media_artist = self.get(
|
||||
@ -113,42 +158,151 @@ class TeslemetryMediaEntity(TeslemetryVehicleEntity, MediaPlayerEntity):
|
||||
)
|
||||
self._attr_source = self.get("vehicle_state_media_info_now_playing_source")
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await self.wake_up_if_asleep()
|
||||
await handle_vehicle_command(
|
||||
self.api.adjust_volume(int(volume * self._volume_max))
|
||||
|
||||
class TeslemetryStreamingMediaEntity(
|
||||
TeslemetryVehicleStreamEntity, TeslemetryMediaEntity, RestoreEntity
|
||||
):
|
||||
"""Streaming vehicle media player class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: TeslemetryVehicleData,
|
||||
scopes: list[Scope],
|
||||
) -> None:
|
||||
"""Initialize the media player entity."""
|
||||
super().__init__(data, "media")
|
||||
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
)
|
||||
self._attr_volume_level = volume
|
||||
self.scoped = Scope.VEHICLE_CMDS in scopes
|
||||
if not self.scoped:
|
||||
self._attr_supported_features = MediaPlayerEntityFeature(0)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
|
||||
await super().async_added_to_hass()
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
try:
|
||||
self._attr_state = MediaPlayerState(state.state)
|
||||
except ValueError:
|
||||
self._attr_state = None
|
||||
self._attr_volume_level = state.attributes.get("volume_level")
|
||||
self._attr_media_title = state.attributes.get("media_title")
|
||||
self._attr_media_artist = state.attributes.get("media_artist")
|
||||
self._attr_media_album_name = state.attributes.get("media_album_name")
|
||||
self._attr_media_playlist = state.attributes.get("media_playlist")
|
||||
self._attr_media_duration = state.attributes.get("media_duration")
|
||||
self._attr_media_position = state.attributes.get("media_position")
|
||||
self._attr_source = state.attributes.get("source")
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_CenterDisplay(
|
||||
self._async_handle_center_display
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaPlaybackStatus(
|
||||
self._async_handle_media_playback_status
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaPlaybackSource(
|
||||
self._async_handle_media_playback_source
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaAudioVolume(
|
||||
self._async_handle_media_audio_volume
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingDuration(
|
||||
self._async_handle_media_now_playing_duration
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingElapsed(
|
||||
self._async_handle_media_now_playing_elapsed
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingArtist(
|
||||
self._async_handle_media_now_playing_artist
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingAlbum(
|
||||
self._async_handle_media_now_playing_album
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingTitle(
|
||||
self._async_handle_media_now_playing_title
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.vehicle.stream_vehicle.listen_MediaNowPlayingStation(
|
||||
self._async_handle_media_now_playing_station
|
||||
)
|
||||
)
|
||||
|
||||
def _async_handle_center_display(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
if value is not None:
|
||||
self._attr_state = DISPLAY_STATES.get(value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_media_playback_status(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_state = MediaPlayerState.OFF if value is None else STATES.get(value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state != MediaPlayerState.PLAYING:
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await self.wake_up_if_asleep()
|
||||
await handle_vehicle_command(self.api.media_toggle_playback())
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
self.async_write_ha_state()
|
||||
def _async_handle_media_playback_source(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_source = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
if self.state == MediaPlayerState.PLAYING:
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await self.wake_up_if_asleep()
|
||||
await handle_vehicle_command(self.api.media_toggle_playback())
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
self.async_write_ha_state()
|
||||
def _async_handle_media_audio_volume(self, value: float | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_volume_level = None if value is None else value / VOLUME_FACTOR
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await self.wake_up_if_asleep()
|
||||
await handle_vehicle_command(self.api.media_next_track())
|
||||
def _async_handle_media_now_playing_duration(self, value: int | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_duration = None if value is None else int(value / 1000)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
self.raise_for_scope(Scope.VEHICLE_CMDS)
|
||||
await self.wake_up_if_asleep()
|
||||
await handle_vehicle_command(self.api.media_prev_track())
|
||||
def _async_handle_media_now_playing_elapsed(self, value: int | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_position = None if value is None else int(value / 1000)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_media_now_playing_artist(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_artist = value # Check if this is album artist or not
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_media_now_playing_album(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_album_name = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_media_now_playing_title(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_title = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _async_handle_media_now_playing_station(self, value: str | None) -> None:
|
||||
"""Update entity attributes."""
|
||||
self._attr_media_channel = (
|
||||
value # could also be _attr_media_playlist when Spotify
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
@ -18,7 +18,6 @@ VEHICLE_DATA_ALT = load_json_object_fixture("vehicle_data_alt.json", DOMAIN)
|
||||
LIVE_STATUS = load_json_object_fixture("live_status.json", DOMAIN)
|
||||
SITE_INFO = load_json_object_fixture("site_info.json", DOMAIN)
|
||||
ENERGY_HISTORY = load_json_object_fixture("energy_history.json", DOMAIN)
|
||||
METADATA = load_json_object_fixture("metadata.json", DOMAIN)
|
||||
|
||||
COMMAND_OK = {"response": {"result": True, "reason": ""}}
|
||||
COMMAND_REASON = {"response": {"result": False, "reason": "already closed"}}
|
||||
@ -52,7 +51,7 @@ METADATA = {
|
||||
"proxy": False,
|
||||
"access": True,
|
||||
"polling": True,
|
||||
"firmware": "2024.44.25",
|
||||
"firmware": "2026.0.0",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"uid": "abc-123",
|
||||
"region": "NA",
|
||||
"scopes": [
|
||||
"openid",
|
||||
"offline_access",
|
||||
"user_data",
|
||||
"vehicle_device_data",
|
||||
"vehicle_cmds",
|
||||
"vehicle_charging_cmds",
|
||||
"energy_device_data",
|
||||
"energy_cmds"
|
||||
],
|
||||
"vehicles": {
|
||||
"LRW3F7EK4NC700000": {
|
||||
"access": true,
|
||||
"polling": true,
|
||||
"proxy": true,
|
||||
"firmware": "2024.44.25"
|
||||
}
|
||||
}
|
||||
}
|
@ -192,7 +192,7 @@
|
||||
"api_version": 71,
|
||||
"autopark_state_v2": "unavailable",
|
||||
"calendar_supported": true,
|
||||
"car_version": "2024.44.25 06f534d46010",
|
||||
"car_version": "2026.0.0 06f534d46010",
|
||||
"center_display_state": 0,
|
||||
"dashcam_clip_save_available": true,
|
||||
"dashcam_state": "Recording",
|
||||
|
@ -1371,6 +1371,147 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_favorite-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_located_at_favorite',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Located at favorite',
|
||||
'platform': 'teslemetry',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'located_at_favorite',
|
||||
'unique_id': 'LRW3F7EK4NC700000-located_at_favorite',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_favorite-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at favorite',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_favorite',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_home-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_located_at_home',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Located at home',
|
||||
'platform': 'teslemetry',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'located_at_home',
|
||||
'unique_id': 'LRW3F7EK4NC700000-located_at_home',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_home-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at home',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_home',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_work-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_located_at_work',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Located at work',
|
||||
'platform': 'teslemetry',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'located_at_work',
|
||||
'unique_id': 'LRW3F7EK4NC700000-located_at_work',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_located_at_work-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at work',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_work',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor[binary_sensor.test_offroad_lightbar-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@ -2801,6 +2942,45 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor_refresh[binary_sensor.test_located_at_favorite-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at favorite',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_favorite',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor_refresh[binary_sensor.test_located_at_home-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at home',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_home',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor_refresh[binary_sensor.test_located_at_work-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Located at work',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_located_at_work',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensor_refresh[binary_sensor.test_offroad_lightbar-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
@ -375,7 +375,7 @@
|
||||
'vehicle_state_api_version': 71,
|
||||
'vehicle_state_autopark_state_v2': 'unavailable',
|
||||
'vehicle_state_calendar_supported': True,
|
||||
'vehicle_state_car_version': '2024.44.25 06f534d46010',
|
||||
'vehicle_state_car_version': '2026.0.0 06f534d46010',
|
||||
'vehicle_state_center_display_state': 0,
|
||||
'vehicle_state_dashcam_clip_save_available': True,
|
||||
'vehicle_state_dashcam_state': 'Recording',
|
||||
|
@ -47,7 +47,7 @@
|
||||
'media_title': 'Chapter 51: Cybertruck: Tesla, 2018–2019',
|
||||
'source': 'Audible',
|
||||
'supported_features': <MediaPlayerEntityFeature: 16437>,
|
||||
'volume_level': 0.16129355359011466,
|
||||
'volume_level': 0.16129354838709678,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_media_player',
|
||||
@ -64,10 +64,12 @@
|
||||
'friendly_name': 'Test Media player',
|
||||
'media_album_name': '',
|
||||
'media_artist': '',
|
||||
'media_duration': 0.0,
|
||||
'media_playlist': '',
|
||||
'media_title': '',
|
||||
'source': 'Spotify',
|
||||
'supported_features': <MediaPlayerEntityFeature: 16437>,
|
||||
'volume_level': 0.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_media_player',
|
||||
@ -125,7 +127,43 @@
|
||||
'media_title': 'Chapter 51: Cybertruck: Tesla, 2018–2019',
|
||||
'source': 'Audible',
|
||||
'supported_features': <MediaPlayerEntityFeature: 0>,
|
||||
'volume_level': 0.16129355359011466,
|
||||
'volume_level': 0.16129354838709678,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_media_player',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'playing',
|
||||
})
|
||||
# ---
|
||||
# name: test_update_streaming[off]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'speaker',
|
||||
'friendly_name': 'Test Media player',
|
||||
'supported_features': <MediaPlayerEntityFeature: 16437>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_media_player',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_update_streaming[on]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'speaker',
|
||||
'friendly_name': 'Test Media player',
|
||||
'media_album_name': 'Test Album',
|
||||
'media_artist': 'Test Artist',
|
||||
'media_duration': 60,
|
||||
'media_position': 5,
|
||||
'source': 'Spotify',
|
||||
'supported_features': <MediaPlayerEntityFeature: 16437>,
|
||||
'volume_level': 0.1935483870967742,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.test_media_player',
|
||||
|
@ -41,7 +41,7 @@
|
||||
'entity_picture': 'https://brands.home-assistant.io/_/teslemetry/icon.png',
|
||||
'friendly_name': 'Test Update',
|
||||
'in_progress': False,
|
||||
'installed_version': '2024.44.25',
|
||||
'installed_version': '2026.0.0',
|
||||
'latest_version': '2024.12.0.0',
|
||||
'release_summary': None,
|
||||
'release_url': None,
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from teslemetry_stream import Signal
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
@ -18,7 +20,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import assert_entities, assert_entities_alt, setup_platform
|
||||
from . import assert_entities, assert_entities_alt, reload_platform, setup_platform
|
||||
from .const import COMMAND_OK, METADATA_NOSCOPE, VEHICLE_DATA_ALT
|
||||
|
||||
|
||||
@ -26,6 +28,7 @@ async def test_media_player(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_legacy: AsyncMock,
|
||||
) -> None:
|
||||
"""Tests that the media player entities are correct."""
|
||||
|
||||
@ -38,6 +41,7 @@ async def test_media_player_alt(
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
mock_legacy: AsyncMock,
|
||||
) -> None:
|
||||
"""Tests that the media player entities are correct."""
|
||||
|
||||
@ -51,6 +55,7 @@ async def test_media_player_noscope(
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_metadata: AsyncMock,
|
||||
mock_legacy: AsyncMock,
|
||||
) -> None:
|
||||
"""Tests that the media player entities are correct without required scope."""
|
||||
|
||||
@ -62,6 +67,7 @@ async def test_media_player_noscope(
|
||||
async def test_media_player_services(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_legacy: AsyncMock,
|
||||
) -> None:
|
||||
"""Tests that the media player services work."""
|
||||
|
||||
@ -137,3 +143,62 @@ async def test_media_player_services(
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
call.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_update_streaming(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
mock_add_listener: AsyncMock,
|
||||
) -> None:
|
||||
"""Tests that the media player entities with streaming are correct."""
|
||||
|
||||
entry = await setup_platform(hass, [Platform.MEDIA_PLAYER])
|
||||
|
||||
# Stream update
|
||||
mock_add_listener.send(
|
||||
{
|
||||
"vin": VEHICLE_DATA_ALT["response"]["vin"],
|
||||
"data": {
|
||||
Signal.CENTER_DISPLAY: "Off",
|
||||
Signal.MEDIA_PLAYBACK_STATUS: None,
|
||||
Signal.MEDIA_PLAYBACK_SOURCE: None,
|
||||
Signal.MEDIA_AUDIO_VOLUME: None,
|
||||
Signal.MEDIA_NOW_PLAYING_DURATION: None,
|
||||
Signal.MEDIA_NOW_PLAYING_ELAPSED: None,
|
||||
Signal.MEDIA_NOW_PLAYING_ARTIST: None,
|
||||
Signal.MEDIA_NOW_PLAYING_ALBUM: None,
|
||||
},
|
||||
"createdAt": "2024-10-04T10:45:17.537Z",
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("media_player.test_media_player")
|
||||
assert state == snapshot(name="off")
|
||||
|
||||
mock_add_listener.send(
|
||||
{
|
||||
"vin": VEHICLE_DATA_ALT["response"]["vin"],
|
||||
"data": {
|
||||
Signal.CENTER_DISPLAY: "Driving",
|
||||
Signal.MEDIA_PLAYBACK_STATUS: "Playing",
|
||||
Signal.MEDIA_PLAYBACK_SOURCE: "Spotify",
|
||||
Signal.MEDIA_AUDIO_VOLUME: 2,
|
||||
Signal.MEDIA_NOW_PLAYING_DURATION: 60000,
|
||||
Signal.MEDIA_NOW_PLAYING_ELAPSED: 5000,
|
||||
Signal.MEDIA_NOW_PLAYING_ARTIST: "Test Artist",
|
||||
Signal.MEDIA_NOW_PLAYING_ALBUM: "Test Album",
|
||||
},
|
||||
"createdAt": "2024-10-04T10:55:17.000Z",
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("media_player.test_media_player")
|
||||
assert state == snapshot(name="on")
|
||||
|
||||
await reload_platform(hass, entry, [Platform.MEDIA_PLAYER])
|
||||
|
||||
# Ensure the restored state is the same as the previous state
|
||||
state = hass.states.get("media_player.test_media_player")
|
||||
assert state == snapshot(name="on")
|
||||
|
Loading…
x
Reference in New Issue
Block a user