From c59197e87aeca0ee4b81c77d11516681400ba9a1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sat, 26 Oct 2024 14:43:32 +0200 Subject: [PATCH] Add more spotify sensors (#129215) --- homeassistant/components/spotify/icons.json | 35 ++ homeassistant/components/spotify/sensor.py | 101 +++- homeassistant/components/spotify/strings.json | 34 ++ .../spotify/fixtures/audio_features.json | 2 +- .../spotify/snapshots/test_diagnostics.ambr | 2 +- .../spotify/snapshots/test_sensor.ambr | 544 ++++++++++++++++++ tests/components/spotify/test_sensor.py | 1 + 7 files changed, 714 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/spotify/icons.json b/homeassistant/components/spotify/icons.json index 00c63141eae..e1b08127e43 100644 --- a/homeassistant/components/spotify/icons.json +++ b/homeassistant/components/spotify/icons.json @@ -4,6 +4,41 @@ "spotify": { "default": "mdi:spotify" } + }, + "sensor": { + "song_tempo": { + "default": "mdi:metronome" + }, + "danceability": { + "default": "mdi:dance-ballroom" + }, + "energy": { + "default": "mdi:lightning-bolt" + }, + "mode": { + "default": "mdi:music" + }, + "speechiness": { + "default": "mdi:speaker-message" + }, + "acousticness": { + "default": "mdi:guitar-acoustic" + }, + "instrumentalness": { + "default": "mdi:guitar-electric" + }, + "valence": { + "default": "mdi:emoticon-happy" + }, + "liveness": { + "default": "mdi:music-note" + }, + "time_signature": { + "default": "mdi:music-clef-treble" + }, + "key": { + "default": "mdi:music-clef-treble" + } } } } diff --git a/homeassistant/components/spotify/sensor.py b/homeassistant/components/spotify/sensor.py index 96b390ec907..032799e69d0 100644 --- a/homeassistant/components/spotify/sensor.py +++ b/homeassistant/components/spotify/sensor.py @@ -5,7 +5,12 @@ from dataclasses import dataclass from spotifyaio.models import AudioFeatures -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,7 +22,17 @@ from .entity import SpotifyEntity class SpotifyAudioFeaturesSensorEntityDescription(SensorEntityDescription): """Describes Spotify sensor entity.""" - value_fn: Callable[[AudioFeatures], float] + value_fn: Callable[[AudioFeatures], float | str | None] + + +def _get_key(audio_features: AudioFeatures) -> str | None: + if audio_features.key is None: + return None + key_name = audio_features.key.name + base = key_name[0] + if len(key_name) > 1: + base = f"{base}♯" + return base AUDIO_FEATURE_SENSORS: tuple[SpotifyAudioFeaturesSensorEntityDescription, ...] = ( @@ -28,6 +43,86 @@ AUDIO_FEATURE_SENSORS: tuple[SpotifyAudioFeaturesSensorEntityDescription, ...] = suggested_display_precision=0, value_fn=lambda audio_features: audio_features.tempo, ), + SpotifyAudioFeaturesSensorEntityDescription( + key="danceability", + translation_key="danceability", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.danceability * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="energy", + translation_key="energy", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.energy * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="mode", + translation_key="mode", + device_class=SensorDeviceClass.ENUM, + options=["major", "minor"], + value_fn=lambda audio_features: audio_features.mode.name.lower(), + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="speechiness", + translation_key="speechiness", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.speechiness * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="acousticness", + translation_key="acousticness", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.acousticness * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="instrumentalness", + translation_key="instrumentalness", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.instrumentalness * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="liveness", + translation_key="liveness", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.liveness * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="valence", + translation_key="valence", + native_unit_of_measurement=PERCENTAGE, + suggested_display_precision=0, + value_fn=lambda audio_features: audio_features.valence * 100, + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="time_signature", + translation_key="time_signature", + device_class=SensorDeviceClass.ENUM, + options=["3/4", "4/4", "5/4", "6/4", "7/4"], + value_fn=lambda audio_features: f"{audio_features.time_signature}/4", + entity_registry_enabled_default=False, + ), + SpotifyAudioFeaturesSensorEntityDescription( + key="key", + translation_key="key", + device_class=SensorDeviceClass.ENUM, + options=["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"], + value_fn=_get_key, + entity_registry_enabled_default=False, + ), ) @@ -63,7 +158,7 @@ class SpotifyAudioFeatureSensor(SpotifyEntity, SensorEntity): self.entity_description = entity_description @property - def native_value(self) -> float | None: + def native_value(self) -> float | str | None: """Return the state of the sensor.""" if (audio_features := self.coordinator.data.audio_features) is None: return None diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index d98e70b9fe1..faf20d740d9 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -35,6 +35,40 @@ "sensor": { "song_tempo": { "name": "Song tempo" + }, + "danceability": { + "name": "Song danceability" + }, + "energy": { + "name": "Song energy" + }, + "mode": { + "name": "Song mode", + "state": { + "minor": "Minor", + "major": "Major" + } + }, + "speechiness": { + "name": "Song speechiness" + }, + "acousticness": { + "name": "Song acousticness" + }, + "instrumentalness": { + "name": "Song instrumentalness" + }, + "valence": { + "name": "Song valence" + }, + "liveness": { + "name": "Song liveness" + }, + "time_signature": { + "name": "Song time signature" + }, + "key": { + "name": "Song key" } } } diff --git a/tests/components/spotify/fixtures/audio_features.json b/tests/components/spotify/fixtures/audio_features.json index 1263d231f5e..52dfee060f7 100644 --- a/tests/components/spotify/fixtures/audio_features.json +++ b/tests/components/spotify/fixtures/audio_features.json @@ -1,7 +1,7 @@ { "danceability": 0.696, "energy": 0.905, - "key": 2, + "key": 3, "loudness": -2.743, "mode": 1, "speechiness": 0.103, diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr index 264f99bed60..161b6025ff3 100644 --- a/tests/components/spotify/snapshots/test_diagnostics.ambr +++ b/tests/components/spotify/snapshots/test_diagnostics.ambr @@ -19,7 +19,7 @@ 'danceability': 0.696, 'energy': 0.905, 'instrumentalness': 0.000905, - 'key': 2, + 'key': 3, 'liveness': 0.302, 'loudness': -2.743, 'mode': 1, diff --git a/tests/components/spotify/snapshots/test_sensor.ambr b/tests/components/spotify/snapshots/test_sensor.ambr index 5c99c878286..347b12dd1d8 100644 --- a/tests/components/spotify/snapshots/test_sensor.ambr +++ b/tests/components/spotify/snapshots/test_sensor.ambr @@ -1,4 +1,436 @@ # serializer version: 1 +# name: test_entities[sensor.spotify_spotify_1_song_acousticness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_acousticness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song acousticness', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'acousticness', + 'unique_id': '1112264111_acousticness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_acousticness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song acousticness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_acousticness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.1', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_danceability-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_danceability', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song danceability', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'danceability', + 'unique_id': '1112264111_danceability', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_danceability-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song danceability', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_danceability', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '69.6', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song energy', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy', + 'unique_id': '1112264111_energy', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song energy', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '90.5', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_instrumentalness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_instrumentalness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song instrumentalness', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'instrumentalness', + 'unique_id': '1112264111_instrumentalness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_instrumentalness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song instrumentalness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_instrumentalness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0905', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_key-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'C', + 'C♯', + 'D', + 'D♯', + 'E', + 'F', + 'F♯', + 'G', + 'G♯', + 'A', + 'A♯', + 'B', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_key', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Song key', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'key', + 'unique_id': '1112264111_key', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_key-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Spotify spotify_1 Song key', + 'options': list([ + 'C', + 'C♯', + 'D', + 'D♯', + 'E', + 'F', + 'F♯', + 'G', + 'G♯', + 'A', + 'A♯', + 'B', + ]), + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_key', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'D♯', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_liveness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_liveness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song liveness', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'liveness', + 'unique_id': '1112264111_liveness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_liveness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song liveness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_liveness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '30.2', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'major', + 'minor', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Song mode', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mode', + 'unique_id': '1112264111_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Spotify spotify_1 Song mode', + 'options': list([ + 'major', + 'minor', + ]), + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'major', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_speechiness-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_speechiness', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song speechiness', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'speechiness', + 'unique_id': '1112264111_speechiness', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_speechiness-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song speechiness', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_speechiness', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.3', + }) +# --- # name: test_entities[sensor.spotify_spotify_1_song_tempo-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -49,3 +481,115 @@ 'state': '114.944', }) # --- +# name: test_entities[sensor.spotify_spotify_1_song_time_signature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + '3/4', + '4/4', + '5/4', + '6/4', + '7/4', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_time_signature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Song time signature', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'time_signature', + 'unique_id': '1112264111_time_signature', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_time_signature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Spotify spotify_1 Song time signature', + 'options': list([ + '3/4', + '4/4', + '5/4', + '6/4', + '7/4', + ]), + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_time_signature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4/4', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_valence-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.spotify_spotify_1_song_valence', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Song valence', + 'platform': 'spotify', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'valence', + 'unique_id': '1112264111_valence', + 'unit_of_measurement': '%', + }) +# --- +# name: test_entities[sensor.spotify_spotify_1_song_valence-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Spotify spotify_1 Song valence', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.spotify_spotify_1_song_valence', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '62.5', + }) +# --- diff --git a/tests/components/spotify/test_sensor.py b/tests/components/spotify/test_sensor.py index b5fd2389e69..11ce361034a 100644 --- a/tests/components/spotify/test_sensor.py +++ b/tests/components/spotify/test_sensor.py @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry, load_fixture, snapshot_platform @pytest.mark.usefixtures("setup_credentials") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_entities( hass: HomeAssistant, mock_spotify: MagicMock,