Skip to content

Commit

Permalink
[ 1.0.28 ] * Added service get_spotify_connect_devices that gets in…
Browse files Browse the repository at this point in the history
…formation about all available Spotify Connect player devices.

  * Added service `get_player_now_playing` that gets object properties currently being played on the user's Spotify account.
  * Added service `player_activate_devices` that activates all static Spotify Connect player devices, and (optionally) switches the active user context to the current user context.
  * Added service `player_resolve_device_id` that resolves a Spotify Connect device identifier from a specified device id, name, alias id, or alias name.  This will ensure that the device id can be found on the network, as well as connect to the device if necessary with the current user context.
  * Added service `get_player_playback_state` that gets information about the user's current playback state, including track or episode, progress, and active device.
  * Added extra state attribute `media_context_content_id` that contains the Context Content ID of current playing context if one is active; otherwise, None.
  * Updated underlying `spotifywebapiPython` package requirement to version 1.0.59.
  • Loading branch information
thlucas1 committed Jun 20, 2024
1 parent dd11439 commit 6519595
Show file tree
Hide file tree
Showing 11 changed files with 927 additions and 74 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ Change are listed in reverse chronological order (newest to oldest).

<span class="changelog">

###### [ 1.0.28 ] - 2024/06/14

* Added service `get_spotify_connect_devices` that gets information about all available Spotify Connect player devices.
* Added service `get_player_now_playing` that gets object properties currently being played on the user's Spotify account.
* Added service `player_activate_devices` that activates all static Spotify Connect player devices, and (optionally) switches the active user context to the current user context.
* Added service `player_resolve_device_id` that resolves a Spotify Connect device identifier from a specified device id, name, alias id, or alias name. This will ensure that the device id can be found on the network, as well as connect to the device if necessary with the current user context.
* Added service `get_player_playback_state` that gets information about the user's current playback state, including track or episode, progress, and active device.
* Added extra state attribute `media_context_content_id` that contains the Context Content ID of current playing context if one is active; otherwise, None.
* Updated underlying `spotifywebapiPython` package requirement to version 1.0.59.

###### [ 1.0.27 ] - 2024/06/12

* Added extra state attribute `media_playlist_content_id` that contains the Content ID of current playing playlist context if one is active; otherwise, None.
Expand Down
182 changes: 170 additions & 12 deletions custom_components/spotifyplus/__init__.py

Large diffs are not rendered by default.

63 changes: 43 additions & 20 deletions custom_components/spotifyplus/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import voluptuous as vol

from spotifywebapipython import SpotifyClient
from spotifywebapipython.models import Device
from spotifywebapipython.models import Device, SpotifyConnectDevices

from homeassistant.config_entries import ConfigEntry, OptionsFlow
from homeassistant.const import CONF_DESCRIPTION, CONF_ID, CONF_NAME, Platform
Expand All @@ -36,6 +36,8 @@

from .const import (
CONF_OPTION_DEVICE_DEFAULT,
CONF_OPTION_DEVICE_PASSWORD,
CONF_OPTION_DEVICE_USERNAME,
CONF_OPTION_SCRIPT_TURN_OFF,
CONF_OPTION_SCRIPT_TURN_ON,
DOMAIN,
Expand Down Expand Up @@ -106,11 +108,14 @@ async def async_oauth_create_entry(self, data:dict[str,Any]) -> FlowResult:

try:

# create new spotify web api python client instance.
# create new spotify web api python client instance - "SpotifyClient()".
_logsi.LogVerbose("Creating SpotifyClient instance")
tokenStorageDir:str = "%s/custom_components/%s/data" % (self.hass.config.config_dir, DOMAIN)
spotifyClient:SpotifyClient = await self.hass.async_add_executor_job(
SpotifyClient, None, tokenStorageDir, None
SpotifyClient,
None, # manager:PoolManager=None,
tokenStorageDir, # tokenStorageDir:str=None,
None # tokenUpdater:Callable=None,
)

_logsi.LogObject(SILevel.Verbose, "SpotifyClient instance created - object", spotifyClient)
Expand Down Expand Up @@ -353,25 +358,38 @@ async def async_step_init(self, user_input:dict[str,Any]=None) -> FlowResult:

# update config entry options from user input values.
self._Options[CONF_OPTION_DEVICE_DEFAULT] = user_input.get(CONF_OPTION_DEVICE_DEFAULT, None)
self._Options[CONF_OPTION_DEVICE_USERNAME] = user_input.get(CONF_OPTION_DEVICE_USERNAME, None)
self._Options[CONF_OPTION_DEVICE_PASSWORD] = user_input.get(CONF_OPTION_DEVICE_PASSWORD, None)
self._Options[CONF_OPTION_SCRIPT_TURN_OFF] = user_input.get(CONF_OPTION_SCRIPT_TURN_OFF, None)
self._Options[CONF_OPTION_SCRIPT_TURN_ON] = user_input.get(CONF_OPTION_SCRIPT_TURN_ON, None)

# store the updated config entry options.
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow is updating configuration options - options" % self._name, self._Options)
return self.async_create_entry(
title="",
data=self._Options
)
# validations.
# if device username was entered then device password is required.
deviceUsername:str = user_input.get(CONF_OPTION_DEVICE_USERNAME, None)
devicePassword:str = user_input.get(CONF_OPTION_DEVICE_PASSWORD, None)
if (deviceUsername is not None) and (devicePassword is None):
errors["base"] = "device_password_required"

# any validation errors? if not, then ...
if "base" not in errors:

# store the updated config entry options.
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow is updating configuration options - options" % self._name, self._Options)
return self.async_create_entry(
title="",
data=self._Options
)

# load available spotify connect devices.
device_list:list[str] = await self.hass.async_add_executor_job(self._GetPlayerDevicesList)
if device_list is None:
if (device_list is None) or (len(device_list) == 0):
errors["base"] = "no_player_devices"
return

# log device that is currently selected.
device_default:str = self._Options.get(CONF_OPTION_DEVICE_DEFAULT, None)
_logsi.LogVerbose("'%s': OptionsFlow option '%s' - SELECTED value: '%s'" % (self._name, CONF_OPTION_DEVICE_DEFAULT, device_default))
device_username:str = self._Options.get(CONF_OPTION_DEVICE_USERNAME, None)
_logsi.LogVerbose("'%s': OptionsFlow option '%s' - SELECTED value: '%s'" % (self._name, CONF_OPTION_DEVICE_USERNAME, device_username))

# create validation schema.
schema = vol.Schema(
Expand All @@ -383,25 +401,33 @@ async def async_step_init(self, user_input:dict[str,Any]=None) -> FlowResult:
description={"suggested_value": self._Options.get(CONF_OPTION_DEVICE_DEFAULT)},
): SelectSelector(
SelectSelectorConfig(
options=device_list,
options=device_list or [],
mode=SelectSelectorMode.DROPDOWN
)
),
vol.Optional(CONF_OPTION_DEVICE_USERNAME,
description={"suggested_value": self._Options.get(CONF_OPTION_DEVICE_USERNAME)},
): cv.string,
vol.Optional(CONF_OPTION_DEVICE_PASSWORD,
description={"suggested_value": self._Options.get(CONF_OPTION_DEVICE_PASSWORD)},
): cv.string,
vol.Optional(CONF_OPTION_SCRIPT_TURN_ON,
description={"suggested_value": self._Options.get(CONF_OPTION_SCRIPT_TURN_ON)},
): selector.EntitySelector(selector.EntitySelectorConfig(integration=DOMAIN_SCRIPT,
#domain=Platform.SCENE,
multiple=False),
),
vol.Optional(CONF_OPTION_SCRIPT_TURN_OFF,
description={"suggested_value": self._Options.get(CONF_OPTION_SCRIPT_TURN_OFF)},
): selector.EntitySelector(selector.EntitySelectorConfig(integration=DOMAIN_SCRIPT,
#domain=Platform.SCENE,
multiple=False),
),
}
)

# any validation errors? if so, then log them.
if "base" in errors:
_logsi.LogDictionary(SILevel.Warning, "'%s': OptionsFlow contained validation errors" % self._name, errors)

_logsi.LogVerbose("'%s': OptionsFlow is showing the init configuration options form" % self._name)
return self.async_show_form(
step_id="init",
Expand Down Expand Up @@ -438,15 +464,12 @@ def _GetPlayerDevicesList(self) -> list:

# get spotify connect player device list.
_logsi.LogVerbose("'%s': OptionsFlow is retrieving Spotify Connect player devices" % self._name)
devices:list[Device] = data.spotifyClient.GetPlayerDevices(refresh=True)

# sort the results (in place) by Name, ascending order.
devices.sort(key=lambda x: (x.Name or "").lower(), reverse=False)

devices:SpotifyConnectDevices = data.spotifyClient.GetSpotifyConnectDevices()

# build string array of all devices.
result:list = []
item:Device
for item in devices:
for item in devices.GetDeviceList():
result.append(item.SelectItemNameAndId)

# trace.
Expand Down
2 changes: 2 additions & 0 deletions custom_components/spotifyplus/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
LOGGER = logging.getLogger(__package__)

CONF_OPTION_DEVICE_DEFAULT = "device_default"
CONF_OPTION_DEVICE_PASSWORD = "device_password"
CONF_OPTION_DEVICE_USERNAME = "device_username"
CONF_OPTION_SCRIPT_TURN_ON = "script_turn_on"
CONF_OPTION_SCRIPT_TURN_OFF = "script_turn_off"

Expand Down
20 changes: 18 additions & 2 deletions custom_components/spotifyplus/instancedata_spotifyplus.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

from .const import (
CONF_OPTION_DEVICE_DEFAULT,
CONF_OPTION_DEVICE_PASSWORD,
CONF_OPTION_DEVICE_USERNAME,
CONF_OPTION_SCRIPT_TURN_OFF,
CONF_OPTION_SCRIPT_TURN_ON,
)
Expand All @@ -29,7 +31,7 @@ class InstanceDataSpotifyPlus:

devices: DataUpdateCoordinator[list[Device]]
"""
List of Spotify Connect devices that are available for this Spotify user.
List of Spotify Connect devices that are available.
This property is refreshed every 5 minutes by a DataUpdateCoordinator.
"""

Expand All @@ -52,7 +54,7 @@ class InstanceDataSpotifyPlus:
"""
The SpotifyClient instance used to interface with the Spotify Web API.
"""


@property
def OptionDeviceDefault(self) -> str | None:
Expand All @@ -61,6 +63,20 @@ def OptionDeviceDefault(self) -> str | None:
"""
return self.options.get(CONF_OPTION_DEVICE_DEFAULT, None)

@property
def OptionDevicePassword(self) -> str | None:
"""
The default Spotify Connect password to use when connecting to an inactive device.
"""
return self.options.get(CONF_OPTION_DEVICE_PASSWORD, None)

@property
def OptionDeviceUsername(self) -> str | None:
"""
The default Spotify Connect username to use when connecting to an inactive device.
"""
return self.options.get(CONF_OPTION_DEVICE_USERNAME, None)

@property
def OptionScriptTurnOff(self) -> str | None:
"""
Expand Down
11 changes: 7 additions & 4 deletions custom_components/spotifyplus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
"name": "SpotifyPlus",
"codeowners": [ "@thlucas1" ],
"config_flow": true,
"dependencies": [ "application_credentials" ],
"documentation": "https://github.com/thlucas1/homeassistantcomponent_spotifyplus/wiki",
"dependencies": [
"application_credentials",
"zeroconf"
],
"documentation": "https://github.com/thlucas1/homeassistantcomponent_spotifyplus/wiki/Device-Configuration-Options#integration-options",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/thlucas1/homeassistantcomponent_spotifyplus/issues",
Expand All @@ -14,10 +17,10 @@
"requests>=2.31.0",
"requests_oauthlib>=1.3.1",
"smartinspectPython>=3.0.33",
"spotifywebapiPython>=1.0.48",
"spotifywebapiPython>=1.0.59",
"urllib3>=1.21.1,<1.27",
"zeroconf>=0.132.2"
],
"version": "1.0.27",
"version": "1.0.28",
"zeroconf": [ "_spotify-connect._tcp.local." ]
}
Loading

0 comments on commit 6519595

Please sign in to comment.