Skip to content

Commit

Permalink
[ 1.0.21 ] * Fixed a bug that caused player state to return an error …
Browse files Browse the repository at this point in the history
…every 30 seconds when playing the Spotify DJ playlist. As the Spotify Web API does not support retrieving the DJ playlist (`spotify:playlist:37i9dQZF1EYkqdzj48dyYq`), it simply returns a manually built representation of the Spotify DJ playlist with limited properties populated (uri, id, name, description, etc). Note that Spotify DJ support is not supported (as of 2024/06/07) by the Spotify Web API (this includes starting play, retrieving playlist details, retrieving playlist items, etc).

  * Added service `zeroconf_discover_devices` to discover Spotify Connect devices on the local network via the ZeroConf (aka MDNS) service, and return details about each device.
  * Added service `zeroconf_device_getinfo` to retrieve Spotify Connect device information from the Spotify Zeroconf API `getInfo` endpoint.
  * Added service `zeroconf_device_resetusers` to reset users for a Spotify Connect device by calling the Spotify Zeroconf API `resetUsers` endpoint.
  * Updated underlying `spotifywebapiPython` package requirement to version 1.0.44.
  • Loading branch information
thlucas1 committed Jun 7, 2024
1 parent 56d40cb commit caf3dc3
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 5 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Change are listed in reverse chronological order (newest to oldest).

<span class="changelog">

###### [ 1.0.21 ] - 2024/06/07

* Fixed a bug that caused player state to return an error every 30 seconds when playing the Spotify DJ playlist. As the Spotify Web API does not support retrieving the DJ playlist (`spotify:playlist:37i9dQZF1EYkqdzj48dyYq`), it simply returns a manually built representation of the Spotify DJ playlist with limited properties populated (uri, id, name, description, etc). Note that Spotify DJ support is not supported (as of 2024/06/07) by the Spotify Web API (this includes starting play, retrieving playlist details, retrieving playlist items, etc).
* Added service `zeroconf_discover_devices` to discover Spotify Connect devices on the local network via the ZeroConf (aka MDNS) service, and return details about each device.
* Added service `zeroconf_device_getinfo` to retrieve Spotify Connect device information from the Spotify Zeroconf API `getInfo` endpoint.
* Added service `zeroconf_device_resetusers` to reset users for a Spotify Connect device by calling the Spotify Zeroconf API `resetUsers` endpoint.
* Updated underlying `spotifywebapiPython` package requirement to version 1.0.44.

###### [ 1.0.20 ] - 2024/06/06

* Changed all `media_player.schedule_update_ha_state(force_refresh=True)` calls to `schedule_update_ha_state(force_refresh=False)` to improve performance. Suggested by @bdraco, along with an explanation of why. Thanks @bdraco!
Expand Down
73 changes: 73 additions & 0 deletions custom_components/spotifyplus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@
SERVICE_SPOTIFY_UNFOLLOW_ARTISTS:str = 'unfollow_artists'
SERVICE_SPOTIFY_UNFOLLOW_PLAYLIST:str = 'unfollow_playlist'
SERVICE_SPOTIFY_UNFOLLOW_USERS:str = 'unfollow_users'
SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO:str = 'zeroconf_device_getinfo'
SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS:str = 'zeroconf_device_resetusers'
SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES:str = 'zeroconf_discover_devices'



SERVICE_SPOTIFY_FOLLOW_ARTISTS_SCHEMA = vol.Schema(
Expand Down Expand Up @@ -568,6 +572,27 @@
}
)

SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO_SCHEMA = vol.Schema(
{
vol.Required("entity_id"): cv.entity_id,
vol.Required("action_url"): cv.string,
}
)

SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS_SCHEMA = vol.Schema(
{
vol.Required("entity_id"): cv.entity_id,
vol.Required("action_url"): cv.string,
}
)

SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES_SCHEMA = vol.Schema(
{
vol.Required("entity_id"): cv.entity_id,
vol.Optional("timeout", default=5): vol.All(vol.Range(min=1,max=10)),
}
)


def _trace_LogTextFile(filePath: str, title: str) -> None:
"""
Expand Down Expand Up @@ -1146,6 +1171,27 @@ async def service_handle_spotify_serviceresponse(service: ServiceCall) -> Servic
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
response = await hass.async_add_executor_job(entity.service_spotify_search_tracks, criteria, limit, offset, market, include_external, limit_total)

elif service.service == SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO:

# get zeroconf spotify connect device information.
action_url = service.data.get("action_url")
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
response = await hass.async_add_executor_job(entity.service_spotify_zeroconf_device_getinfo, action_url)

elif service.service == SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS:

# zeroconf spotify connect device reset users.
action_url = service.data.get("action_url")
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
response = await hass.async_add_executor_job(entity.service_spotify_zeroconf_device_resetusers, action_url)

elif service.service == SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES:

# zeroconf discover devices service.
timeout = service.data.get("timeout")
_logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (service.service, entity.name))
response = await hass.async_add_executor_job(entity.service_spotify_zeroconf_discover_devices, timeout)

else:

raise HomeAssistantError("Unrecognized service identifier '%s' in method service_handle_spotify_serviceresponse" % service.service)
Expand Down Expand Up @@ -1642,6 +1688,33 @@ def _GetEntityFromServiceData(hass:HomeAssistant, service:ServiceCall, field_id:
supports_response=SupportsResponse.NONE,
)

_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO, SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO_SCHEMA)
hass.services.async_register(
DOMAIN,
SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO,
service_handle_spotify_serviceresponse,
schema=SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS, SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS_SCHEMA)
hass.services.async_register(
DOMAIN,
SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS,
service_handle_spotify_serviceresponse,
schema=SERVICE_SPOTIFY_ZEROCONF_DEVICE_RESETUSERS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_SERVICE_REQUEST_REGISTER % SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES, SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES_SCHEMA)
hass.services.async_register(
DOMAIN,
SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES,
service_handle_spotify_serviceresponse,
schema=SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

# indicate success.
_logsi.LogVerbose("Component async_setup complete")
return True
Expand Down
4 changes: 2 additions & 2 deletions custom_components/spotifyplus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"issue_tracker": "https://github.com/thlucas1/homeassistantcomponent_spotifyplus/issues",
"requirements": [
"smartinspectPython==3.0.33",
"spotifywebapiPython==1.0.43",
"spotifywebapiPython==1.0.44",
"urllib3>=1.21.1,<1.27"
],
"version": "1.0.20",
"version": "1.0.21",
"zeroconf": [ "_spotify-connect._tcp.local." ]
}
160 changes: 158 additions & 2 deletions custom_components/spotifyplus/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar, Tuple
from yarl import URL

from spotifywebapipython import SpotifyClient, SpotifyApiError, SpotifyWebApiError
from spotifywebapipython import SpotifyClient, SpotifyDiscovery, SpotifyApiError, SpotifyWebApiError
from spotifywebapipython.models import (
Album,
AlbumPageSaved,
Expand All @@ -39,7 +39,9 @@
Track,
TrackPage,
TrackPageSaved,
UserProfile
UserProfile,
ZeroconfGetInfo,
ZeroconfResponse
)

from homeassistant.components.media_player import (
Expand Down Expand Up @@ -4276,6 +4278,160 @@ def service_spotify_unfollow_users(self,
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)


def service_spotify_zeroconf_device_getinfo(self,
actionUrl:int=5,
) -> dict:
"""
Retrieve Spotify Connect device information from the Spotify Zeroconf API `getInfo` endpoint.
Args:
actionUrl (str):
The Zeroconf action url to issue the request to.
Example: `http://192.168.1.80:8200/zc?action=getInfo`
Returns:
A dictionary that contains the following keys:
- user_profile: A (partial) user profile that retrieved the result.
- result: A `ZeroconfGetInfo` object that contains the response.
"""
apiMethodName:str = 'service_spotify_zeroconf_getinfo'
apiMethodParms:SIMethodParmListContext = None
result:ZeroconfGetInfo = None

try:

# trace.
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
apiMethodParms.AppendKeyValue("actionUrl", actionUrl)
_logsi.LogMethodParmList(SILevel.Verbose, "Spotify Connect ZeroConf Device GetInformation Service", apiMethodParms)

# get Spotify zeroconf api action "getInfo" response.
result = self.data.spotifyClient.ZeroconfGetInformation(actionUrl)

# return the (partial) user profile that retrieved the result, as well as the result itself.
return {
"user_profile": self._GetUserProfilePartialDictionary(self.data.spotifyClient.UserProfile),
"result": result.ToDictionary()
}

# the following exceptions have already been logged, so we just need to
# pass them back to HA for display in the log (or service UI).
except SpotifyApiError as ex:
raise HomeAssistantError(ex.Message)
except SpotifyWebApiError as ex:
raise HomeAssistantError(ex.Message)

finally:

# trace.
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)


def service_spotify_zeroconf_device_resetusers(self,
actionUrl:int=5,
) -> dict:
"""
Reset users for a Spotify Connect device by calling the Spotify Zeroconf API `resetUsers` endpoint.
Args:
actionUrl (str):
The Zeroconf action url to issue the request to.
Example: `http://192.168.1.80:8200/zc?action=resetUsers`
Returns:
A dictionary that contains the following keys:
- user_profile: A (partial) user profile that retrieved the result.
- result: A `ZeroconfResponse` object that contains the response.
"""
apiMethodName:str = 'service_spotify_zeroconf_getinfo'
apiMethodParms:SIMethodParmListContext = None
result:ZeroconfResponse = None

try:

# trace.
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
apiMethodParms.AppendKeyValue("actionUrl", actionUrl)
_logsi.LogMethodParmList(SILevel.Verbose, "Spotify Connect ZeroConf Device ResetUsers Service", apiMethodParms)

# get Spotify zeroconf api action "resetUsers" response.
result = self.data.spotifyClient.ZeroconfResetUsers(actionUrl)

# return the (partial) user profile that retrieved the result, as well as the result itself.
return {
"user_profile": self._GetUserProfilePartialDictionary(self.data.spotifyClient.UserProfile),
"result": result.ToDictionary()
}

# the following exceptions have already been logged, so we just need to
# pass them back to HA for display in the log (or service UI).
except SpotifyApiError as ex:
raise HomeAssistantError(ex.Message)
except SpotifyWebApiError as ex:
raise HomeAssistantError(ex.Message)

finally:

# trace.
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)


def service_spotify_zeroconf_discover_devices(self,
timeout:int=5,
) -> dict:
"""
Discover Spotify Connect devices on the local network via the
ZeroConf (aka MDNS) service, and return details about each device.
Args:
timeout (int):
Maximum amount of time to wait (in seconds) for the
discovery to complete.
Default is 5 seconds.
Returns:
A dictionary that contains the following keys:
- user_profile: A (partial) user profile that retrieved the result.
- result: An array of `ZeroconfDiscoveryResult` objects of matching results.
"""
apiMethodName:str = 'service_spotify_zeroconf_discover_devices'
apiMethodParms:SIMethodParmListContext = None

try:

# trace.
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
apiMethodParms.AppendKeyValue("timeout", timeout)
_logsi.LogMethodParmList(SILevel.Verbose, "Spotify ZeroConf Discover Devices Service", apiMethodParms)

# create a new instance of the discovery class.
# do not verify device connections;
# do not print device details to the console as they are discovered.
discovery:SpotifyDiscovery = SpotifyDiscovery(self.data.spotifyClient, False, printToConsole=False)

# discover Spotify Connect devices on the network, waiting up to the specified
# time in seconds for all devices to be discovered.
discovery.DiscoverDevices(timeout)

# return the (partial) user profile that retrieved the result, as well as the result itself.
return {
"user_profile": self._GetUserProfilePartialDictionary(self.data.spotifyClient.UserProfile),
"result": [ item.ToDictionary() for item in discovery.DiscoveryResults ]
}

# the following exceptions have already been logged, so we just need to
# pass them back to HA for display in the log (or service UI).
except SpotifyApiError as ex:
raise HomeAssistantError(ex.Message)
except SpotifyWebApiError as ex:
raise HomeAssistantError(ex.Message)

finally:

# trace.
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)


async def async_added_to_hass(self) -> None:
"""
Run when this Entity has been added to HA.
Expand Down
67 changes: 67 additions & 0 deletions custom_components/spotifyplus/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1894,3 +1894,70 @@ unfollow_users:
required: true
selector:
text:

zeroconf_device_getinfo:
name: ZeroConf Device GetInformation
description: Retrieve Spotify Connect device information from the Spotify Zeroconf API `getInfo` endpoint.
fields:
entity_id:
name: Entity ID
description: Entity ID of the SpotifyPlus device that will make the request to the ZeroConf service.
example: "media_player.spotifyplus_username"
required: true
selector:
entity:
integration: spotifyplus
domain: media_player
action_url:
name: Action URL
description: The Zeroconf action url to issue the request to (e.g. http://192.168.1.80:8200/zc?action=getInfo).
example: "http://192.168.1.80:8200/zc?action=getInfo"
required: true
selector:
text:

zeroconf_device_resetusers:
name: ZeroConf Device ResetUsers
description: Reset users for a Spotify Connect device by calling the Spotify Zeroconf API `resetUsers` endpoint.
fields:
entity_id:
name: Entity ID
description: Entity ID of the SpotifyPlus device that will make the request to the ZeroConf service.
example: "media_player.spotifyplus_username"
required: true
selector:
entity:
integration: spotifyplus
domain: media_player
action_url:
name: Action URL
description: The Zeroconf action url to issue the request to (e.g. http://192.168.1.80:8200/zc?action=resetUsers).
example: "http://192.168.1.80:8200/zc?action=resetUsers"
required: true
selector:
text:

zeroconf_discover_devices:
name: ZeroConf Discover Devices
description: Discover Spotify Connect devices on the local network via the ZeroConf (aka MDNS) service, and return details about each device.
fields:
entity_id:
name: Entity ID
description: Entity ID of the SpotifyPlus device that will make the request to the ZeroConf service.
example: "media_player.spotifyplus_username"
required: true
selector:
entity:
integration: spotifyplus
domain: media_player
timeout:
name: Timeout
description: Maximum amount of time to wait (in seconds) for the discovery to complete. Default is 5, range is 1 thru 10.
example: 5
required: false
selector:
number:
min: 1
max: 10
step: 1
mode: slider
Loading

0 comments on commit caf3dc3

Please sign in to comment.