diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 24266e2d8bb..fb66622d893 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1,9 +1,12 @@ """The spotify integration.""" +from __future__ import annotations from dataclasses import dataclass +from datetime import timedelta from typing import Any import aiohttp +import requests from spotipy import Spotify, SpotifyException import voluptuous as vol @@ -22,10 +25,11 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from . import config_flow from .browse_media import async_browse_media -from .const import DOMAIN, SPOTIFY_SCOPES +from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES from .util import is_spotify_media_type, resolve_spotify_media_type CONFIG_SCHEMA = vol.Schema( @@ -57,6 +61,7 @@ class HomeAssistantSpotifyData: client: Spotify current_user: dict[str, Any] + devices: DataUpdateCoordinator session: OAuth2Session @@ -101,10 +106,35 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not current_user: raise ConfigEntryNotReady + async def _update_devices() -> list[dict[str, Any]]: + try: + devices: dict[str, Any] | None = await hass.async_add_executor_job( + spotify.devices + ) + except (requests.RequestException, SpotifyException) as err: + raise UpdateFailed from err + + if devices is None: + return [] + + return devices.get("devices", []) + + device_coordinator: DataUpdateCoordinator[ + list[dict[str, Any]] + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{entry.title} Devices", + update_interval=timedelta(minutes=5), + update_method=_update_devices, + ) + await device_coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = HomeAssistantSpotifyData( client=spotify, current_user=current_user, + devices=device_coordinator, session=session, ) diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py index 4c86234045b..ad73262921b 100644 --- a/homeassistant/components/spotify/const.py +++ b/homeassistant/components/spotify/const.py @@ -1,4 +1,7 @@ """Define constants for the Spotify integration.""" + +import logging + from homeassistant.components.media_player.const import ( MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -9,6 +12,8 @@ from homeassistant.components.media_player.const import ( DOMAIN = "spotify" +LOGGER = logging.getLogger(__package__) + SPOTIFY_SCOPES = [ # Needed to be able to control playback "user-modify-playback-state", diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index fb431ab4824..f6b229e99fd 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -33,7 +33,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID, STATE_IDLE, STATE_PAUSED, STATE_PLAYING -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -147,7 +147,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): SPOTIFY_SCOPES ) self._currently_playing: dict | None = {} - self._devices: list[dict] | None = [] self._playlist: dict | None = None @property @@ -258,9 +257,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @property def source_list(self) -> list[str] | None: """Return a list of source devices.""" - if not self._devices: - return None - return [device["name"] for device in self._devices] + return [device["name"] for device in self.data.devices.data] @property def shuffle(self) -> bool | None: @@ -332,19 +329,16 @@ class SpotifyMediaPlayer(MediaPlayerEntity): if ( self._currently_playing and not self._currently_playing.get("device") - and self._devices + and self.data.devices.data ): - kwargs["device_id"] = self._devices[0].get("id") + kwargs["device_id"] = self.data.devices.data[0].get("id") self.data.client.start_playback(**kwargs) @spotify_exception_handler def select_source(self, source: str) -> None: """Select playback device.""" - if not self._devices: - return - - for device in self._devices: + for device in self.data.devices.data: if device["name"] == source: self.data.client.transfer_playback( device["id"], self.state == STATE_PLAYING @@ -386,9 +380,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): if context["type"] == MEDIA_TYPE_PLAYLIST: self._playlist = self.data.client.playlist(current["context"]["uri"]) - devices = self.data.client.devices() or {} - self._devices = devices.get("devices", []) - async def async_browse_media( self, media_content_type: str | None = None, media_content_id: str | None = None ) -> BrowseMedia: @@ -408,3 +399,17 @@ class SpotifyMediaPlayer(MediaPlayerEntity): media_content_type, media_content_id, ) + + @callback + def _handle_devices_update(self) -> None: + """Handle updated data from the coordinator.""" + if not self.enabled: + return + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.data.devices.async_add_listener(self._handle_devices_update) + )