diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc6901..b35e12d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,14 @@ Change are listed in reverse chronological order (newest to oldest). -###### [ 1.0.14 ] - 2024/04/01 +###### [ 1.0.15 ] - 2024/04/05 + + * Added `MediaPlayerEntityFeature.VOLUME_MUTE` support to handle volume mute requests. + * Added `MediaPlayerEntityFeature.VOLUME_STEP` support to handle volume step requests. + * Updated Media Browser logic to return an empty `BrowseMedia` object when ignoring Sonos-Card 'favorites' node requests, as a null object was causing numerous `Browse Media should use new BrowseMedia class` log warnings. + * Updated underlying `spotifywebapiPython` package requirement to version 1.0.42. + +###### [ 1.0.14 ] - 2024/04/04 * Added service `player_media_play_track_favorites` to play all track favorites for the current user. * Increased all browse media limits from 50 items to 150 items. diff --git a/custom_components/spotifyplus/manifest.json b/custom_components/spotifyplus/manifest.json index 69a02d7..16e537b 100644 --- a/custom_components/spotifyplus/manifest.json +++ b/custom_components/spotifyplus/manifest.json @@ -10,9 +10,9 @@ "issue_tracker": "https://github.com/thlucas1/homeassistantcomponent_spotifyplus/issues", "requirements": [ "smartinspectPython==3.0.33", - "spotifywebapiPython==1.0.41", + "spotifywebapiPython==1.0.42", "urllib3>=1.21.1,<1.27" ], - "version": "1.0.14", + "version": "1.0.15", "zeroconf": [ "_spotify-connect._tcp.local." ] } diff --git a/custom_components/spotifyplus/media_player.py b/custom_components/spotifyplus/media_player.py index 3653c3f..aeb595b 100644 --- a/custom_components/spotifyplus/media_player.py +++ b/custom_components/spotifyplus/media_player.py @@ -256,6 +256,7 @@ def __init__(self, data:InstanceDataSpotifyPlus) -> None: self._commandScanInterval:int = 0 self._lastKnownTimeRemainingSeconds:int = 0 self._isInCommandEvent:bool = False + self._volume_level_saved:float = None # initialize base class attributes (MediaPlayerEntity). self._attr_icon = "mdi:spotify" @@ -304,7 +305,9 @@ def __init__(self, data:InstanceDataSpotifyPlus) -> None: | MediaPlayerEntityFeature.SHUFFLE_SET \ | MediaPlayerEntityFeature.TURN_OFF \ | MediaPlayerEntityFeature.TURN_ON \ - | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_MUTE \ + | MediaPlayerEntityFeature.VOLUME_SET \ + | MediaPlayerEntityFeature.VOLUME_STEP else: _logsi.LogVerbose("'%s': MediaPlayer is setting supported features for Spotify Non-Premium user" % self.name) self._attr_supported_features = MediaPlayerEntityFeature.BROWSE_MEDIA @@ -336,12 +339,6 @@ def state(self) -> MediaPlayerState: return self._attr_state - @property - def volume_level(self) -> float | None: - """ Volume level of the media player (0.0 to 1.0). """ - return self._attr_volume_level - - @property def media_content_id(self) -> str | None: """ Return the media URL. """ @@ -439,6 +436,18 @@ def shuffle(self) -> bool | None: def repeat(self) -> RepeatMode | str | None: """ Return current repeat mode. """ return self._attr_repeat + + + @property + def volume_level(self) -> float | None: + """ Volume level of the media player (0.0 to 1.0). """ + return self._attr_volume_level + + + @property + def is_volume_muted(self): + """ Boolean if volume is currently muted. """ + return self._attr_is_volume_muted @spotify_exception_handler @@ -497,13 +506,34 @@ def media_seek(self, position: float) -> None: # update ha state. self._attr_media_position = position self._attr_media_position_updated_at = utcnow() - self.async_schedule_update_ha_state() + self.async_write_ha_state() # seek to track position. deviceId:str = self._VerifyDeviceIdByName() self.data.spotifyClient.PlayerMediaSeek(int(position * 1000), deviceId) + @spotify_exception_handler + def mute_volume(self, mute:bool) -> None: + """ Send mute command. """ + _logsi.LogVerbose(STAppMessages.MSG_MEDIAPLAYER_SERVICE_WITH_PARMS, self.name, "mute_volume", "mute='%s'" % (mute)) + + self._attr_is_volume_muted = mute + + if mute: + self._volume_level_saved = self._attr_volume_level + self.async_write_ha_state() + self.set_volume_level(0.0) + else: + # did we save the volume on a previous mute request? if not, then default volume. + if self._volume_level_saved is None or self._volume_level_saved == 0.0: + _logsi.LogVerbose("Previously saved volume was not set; defaulting to 0.10") + self._volume_level_saved = 0.10 + self._attr_volume_level = self._volume_level_saved + self.async_write_ha_state() + self.set_volume_level(self._volume_level_saved) + + @spotify_exception_handler def play_media(self, media_type: MediaType | str, media_id: str, **kwargs: Any) -> None: """ @@ -609,7 +639,7 @@ def set_shuffle(self, shuffle: bool) -> None: # update ha state. self._attr_shuffle = shuffle - self.async_schedule_update_ha_state() + self.async_write_ha_state() # set shuffle mode. deviceId:str = self._VerifyDeviceIdByName() @@ -625,7 +655,7 @@ def set_repeat(self, repeat: RepeatMode) -> None: if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: raise ValueError(f"Unsupported repeat mode: {repeat}") self._attr_repeat = repeat - self.async_schedule_update_ha_state() + self.async_write_ha_state() # set repeat mode. deviceId:str = self._VerifyDeviceIdByName() @@ -636,6 +666,10 @@ def set_repeat(self, repeat: RepeatMode) -> None: def set_volume_level(self, volume: float) -> None: """ Set the volume level. """ _logsi.LogVerbose(STAppMessages.MSG_MEDIAPLAYER_SERVICE_WITH_PARMS, self.name, "set_volume_level", "volume='%s'" % (volume)) + + # validations. + if volume is None: + volume = 0.0 # update ha state. self._attr_volume_level = volume @@ -910,7 +944,8 @@ def _UpdateHAFromPlayerPlayState(self, playerPlayState:PlayerPlayState) -> None: self._attr_shuffle = None self._attr_source = None self._attr_volume_level = None - + self._attr_is_volume_muted = None + # does player state exist? if not, then we are done. if playerPlayState is None: _logsi.LogVerbose("'%s': Spotify PlayerPlayState object was not set; nothing to do" % self.name) @@ -928,6 +963,7 @@ def _UpdateHAFromPlayerPlayState(self, playerPlayState:PlayerPlayState) -> None: self._attr_state = MediaPlayerState.IDLE _logsi.LogVerbose("'%s': MediaPlayerState set to '%s'" % (self.name, self._attr_state)) + self._attr_is_volume_muted = playerPlayState.IsMuted self._attr_shuffle = playerPlayState.ShuffleState # update item-related attributes (e.g. track? episode? etc)? @@ -4289,7 +4325,19 @@ async def async_browse_media( elif media_content_type == 'favorites': # ignore Sonos-Card "favorites" node queries. _logsi.LogVerbose("'%s': ignoring Sonos-Card favorites query (no SoundTouch equivalent)" % self.name) - return None + + # Sonos-Card requires a valid BrowseMedia object, so return an empty one. + browseMedia:BrowseMedia = BrowseMedia( + can_expand=False, + can_play=False, + children=[], + children_media_class=None, + media_class=None, + media_content_id=media_content_id, + media_content_type=media_content_type, + title="Favorites not supported", + ) + return browseMedia else: diff --git a/custom_components/spotifyplus/services.yaml b/custom_components/spotifyplus/services.yaml index 590bcaf..a9909d3 100644 --- a/custom_components/spotifyplus/services.yaml +++ b/custom_components/spotifyplus/services.yaml @@ -1003,7 +1003,7 @@ player_media_play_track_favorites: name: Delay description: Time delay (in seconds) to wait AFTER issuing the command to the player. This delay will give the spotify web api time to process the change before another command is issued. Default is 0.50; value range is 0 - 10. example: "0.50" - required: true + required: false selector: text: diff --git a/requirements.txt b/requirements.txt index f894c71..bbfd583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ colorlog==6.7.0 homeassistant==2023.10.5 ruff==0.1.3 smartinspectPython>=3.0.33 -spotifywebapiPython==1.0.41 +spotifywebapiPython==1.0.42