Initial Home Assistant commit
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
"""Application credentials platform for spotify."""
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""
|
||||
Return authorization server.
|
||||
"""
|
||||
# create a new authorization server instance for Spotify Web API usage.
|
||||
auth_server:AuthorizationServer = AuthorizationServer(
|
||||
authorize_url="https://accounts.spotify.com/authorize",
|
||||
token_url="https://accounts.spotify.com/api/token",
|
||||
)
|
||||
|
||||
return auth_server
|
||||
@@ -0,0 +1,134 @@
|
||||
# external package imports.
|
||||
# none
|
||||
|
||||
# our package imports.
|
||||
# none
|
||||
|
||||
class STAppMessages:
|
||||
"""
|
||||
A strongly-typed resource class, for looking up localized strings, etc.
|
||||
|
||||
Threadsafety:
|
||||
This class is fully thread-safe.
|
||||
"""
|
||||
|
||||
MSG_SERVICE_ARGUMENT_NULL:str = "The '%s' service parameter was not specified for the '%s' service call"
|
||||
"""
|
||||
The '%s' service parameter was not specified for the '%s' service call
|
||||
"""
|
||||
|
||||
MSG_ARGUMENT_NULL:str = "The '%s' argument was not specified for the '%s' function"
|
||||
"""
|
||||
The '%s' argument was not specified for the '%s' function
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLE_REQUEST:str = "Handling intent request '%s'"
|
||||
"""
|
||||
Handling intent request '%s'
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLE_REQUEST_PARMS:str = "Intent Handler Parameters: `%s`"
|
||||
"""
|
||||
Intent Handler Parameters: `%s`
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLE_REQUEST_SLOTS:str = "Intent Handler Parameters: `%s` (slots)"
|
||||
"""
|
||||
Intent Handler Parameters: `%s` (slots)
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLER_EXCEPTION:str = "Intent handler exception detected for \"%s\": %s"
|
||||
"""
|
||||
Intent handler exception detected for \"%s\": %s
|
||||
"""
|
||||
|
||||
MSG_INTENT_VALIDATE_SLOTS_EXCEPTION:str = "Slot validation failed for Intent handler method '%s': %s"
|
||||
"""
|
||||
An unhandled exception occurred in Intent handler method '%s'; exception: %s
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLER_REGISTER:str = "Component is registering intent handler: '%s'"
|
||||
"""
|
||||
Component is registering intent handler: '%s'
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLER_RESPONSE:str = "Intent handler response: '%s'"
|
||||
"""
|
||||
Intent handler response: '%s'
|
||||
"""
|
||||
|
||||
MSG_INTENT_HANDLER_SLOT_INFO:str = "Current slot values for intent: '%s'"
|
||||
"""
|
||||
Current slot values for intent: '%s'
|
||||
"""
|
||||
|
||||
MSG_INTENT_MATCH_CONSTRAINTS_REQ:str="Intent matching contraints request: '%s'"
|
||||
"""
|
||||
Intent matching contraints request: '%s'
|
||||
"""
|
||||
|
||||
MSG_INTENT_MATCH_CONSTRAINTS_RSLT:str="Intent matching contraints result: '%s' (isMatch=%s, noMatchReason=%s, noMatchName=%s)"
|
||||
"""
|
||||
Intent matching contraints result: '%s' (isMatch=%s, noMatchReason=%s, noMatchName=%s)
|
||||
"""
|
||||
|
||||
MSG_INTENT_TARGET_ENTITY_STATE:str="Intent '%s' target entity state: '%s'"
|
||||
"""
|
||||
Intent '%s' target entity state: '%s'
|
||||
"""
|
||||
|
||||
WARN_INTENT_RESPONSE_TEMPLATE_RENDER_ERROR:str=" \
|
||||
SpotifyPlus Intent response render error: \
|
||||
Text: \"%s\" \
|
||||
Error: %s"
|
||||
|
||||
MSG_SERVICE_CALL_START:str = "Processing service call '%s' in async '%s' method"
|
||||
"""
|
||||
Processing service call '%s' in async '%s' method
|
||||
"""
|
||||
|
||||
MSG_SERVICE_CALL_PARM:str = "ServiceCall Parameters"
|
||||
"""
|
||||
ServiceCall Parameters
|
||||
"""
|
||||
|
||||
MSG_SERVICE_CALL_DATA:str = "ServiceCall Data"
|
||||
"""
|
||||
ServiceCall Data
|
||||
"""
|
||||
|
||||
MSG_SERVICE_REQUEST_REGISTER:str = "Component async_setup is registering async service: '%s'"
|
||||
"""
|
||||
Component async_setup is registering async service: '%s'
|
||||
"""
|
||||
|
||||
MSG_SERVICE_REQUEST_UNKNOWN:str = "Service request '%s' was not recognized by the '%s' method"
|
||||
"""
|
||||
Service request '%s' was not recognized by the '%s' method
|
||||
"""
|
||||
|
||||
MSG_SERVICE_REQUEST_EXCEPTION:str = "An unhandled exception occurred in Service request method '%s'; exception: %s"
|
||||
"""
|
||||
An unhandled exception occurred in Service request method '%s'; exception: %s
|
||||
"""
|
||||
|
||||
MSG_SERVICE_EXECUTE:str = "Executing '%s' service on media player '%s'"
|
||||
"""
|
||||
Executing '%s' service on media player '%s'
|
||||
"""
|
||||
|
||||
MSG_SERVICE_QUERY_WEB_API:str = "Retrieving information from the Spotify Web API"
|
||||
"""
|
||||
Retrieving information from the Spotify Web API
|
||||
"""
|
||||
|
||||
MSG_MEDIAPLAYER_SERVICE:str = "'%s': MediaPlayer is executing service '%s'"
|
||||
"""
|
||||
'%s': MediaPlayer is executing service '%s'
|
||||
"""
|
||||
|
||||
MSG_MEDIAPLAYER_SERVICE_WITH_PARMS:str = "'%s': MediaPlayer is executing service '%s' - parameters: %s"
|
||||
"""
|
||||
'%s': MediaPlayer is executing service '%s' - parameters: %s
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,724 @@
|
||||
"""Support for Spotify media browsing."""
|
||||
from __future__ import annotations
|
||||
from functools import partial
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
import base64
|
||||
import os
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
from spotifywebapipython import SpotifyClient
|
||||
from spotifywebapipython.models import *
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
|
||||
import logging
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BrowsableMedia(StrEnum):
|
||||
"""
|
||||
Enum of browsable media.
|
||||
Contains the library root node key value definitions.
|
||||
"""
|
||||
# library custom root node title definitions.
|
||||
SPOTIFY_LIBRARY_INDEX = "spotify_library_index"
|
||||
SPOTIFY_CATEGORY_PLAYLISTS = "spotify_category_playlists"
|
||||
SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU = "spotify_category_playlists_madeforyou"
|
||||
SPOTIFY_CATEGORYS = "spotify_categorys"
|
||||
SPOTIFY_FEATURED_PLAYLISTS = "spotify_featured_playlists"
|
||||
SPOTIFY_NEW_RELEASES = "spotify_new_releases"
|
||||
SPOTIFY_USER_FOLLOWED_ARTISTS = "spotify_user_followed_artists"
|
||||
SPOTIFY_USER_PLAYLISTS = "spotify_user_playlists"
|
||||
SPOTIFY_USER_RECENTLY_PLAYED = "spotify_user_recently_played"
|
||||
SPOTIFY_USER_SAVED_ALBUMS = "spotify_user_saved_albums"
|
||||
SPOTIFY_USER_SAVED_AUDIOBOOKS = "spotify_user_saved_audiobooks"
|
||||
SPOTIFY_USER_SAVED_SHOWS = "spotify_user_saved_shows"
|
||||
SPOTIFY_USER_SAVED_TRACKS = "spotify_user_saved_tracks"
|
||||
SPOTIFY_USER_TOP_ARTISTS = "spotify_user_top_artists"
|
||||
SPOTIFY_USER_TOP_TRACKS = "spotify_user_top_tracks"
|
||||
|
||||
|
||||
# Spotify Library index definitions, containing media attributes that control content display.
|
||||
# The order listed is how they are displayed in the media browser.
|
||||
SPOTIFY_LIBRARY_MAP = {
|
||||
BrowsableMedia.SPOTIFY_LIBRARY_INDEX.value: {
|
||||
"title": "SpotifyPlus Media Library",
|
||||
"image": None,
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.DIRECTORY,
|
||||
"is_index_item": False,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_PLAYLISTS.value: {
|
||||
"title": "Playlists",
|
||||
"title_node": "Spotify Playlist Favorites",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_playlists.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.PLAYLIST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_FOLLOWED_ARTISTS.value: {
|
||||
"title": "Artists",
|
||||
"title_node": "Spotify Artists Followed",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_artists.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ARTIST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_SAVED_ALBUMS.value: {
|
||||
"title": "Albums",
|
||||
"title_node": "Spotify Album Favorites",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_albums.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ALBUM,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_SAVED_TRACKS.value: {
|
||||
"title": "Tracks",
|
||||
"title_node": "Spotify Track Favorites",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_tracks.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_SAVED_SHOWS.value: {
|
||||
"title": "Podcasts",
|
||||
"title_node": "Spotify Podcast Favorites",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_podcasts.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.PODCAST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_SAVED_AUDIOBOOKS.value: {
|
||||
"title": "Audiobooks",
|
||||
"title_node": "Spotify Audiobook Favorites",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_audiobooks.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.APP, # spotify audiobook support
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_TOP_ARTISTS.value: {
|
||||
"title": "Top Artists",
|
||||
"title_node": "Spotify Top Artists",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_top_artists.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ARTIST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_TOP_TRACKS.value: {
|
||||
"title": "Top Tracks",
|
||||
"title_node": "Spotify Top Tracks",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_top_tracks.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_FEATURED_PLAYLISTS.value: {
|
||||
"title": "Featured Playlists",
|
||||
"title_node": "Spotify Featured Playlists",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_featured_playlists.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.PLAYLIST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_NEW_RELEASES.value: {
|
||||
"title": "New Releases",
|
||||
"title_node": "Spotify New Releases",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_new_releases.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.ALBUM,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_CATEGORYS.value: {
|
||||
"title": "Categories",
|
||||
"title_node": "Spotify Categories ",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_categorys.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.GENRE,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS.value: {
|
||||
"title": "Category Playlists",
|
||||
"title_node": "Spotify Category Playlists",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_category_playlists.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.PLAYLIST,
|
||||
"is_index_item": False,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU.value: {
|
||||
"title": "Made For You",
|
||||
"title_node": "Spotify Playlists Made For You",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_spotify_category_playlists_made_for_you.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.PLAYLIST,
|
||||
},
|
||||
BrowsableMedia.SPOTIFY_USER_RECENTLY_PLAYED.value: {
|
||||
"title": "Recently Played",
|
||||
"title_node": "Spotify Recently Played",
|
||||
"image": f"/local/images/{DOMAIN}_medialib_recently_played.png",
|
||||
"parent": MediaClass.DIRECTORY,
|
||||
"children": MediaClass.TRACK,
|
||||
},
|
||||
# the following are HA media types, and will not be displayed in the library index.
|
||||
# they are required for playing base-level types of media for this library index.
|
||||
MediaType.ALBUM: {
|
||||
"parent": MediaClass.ALBUM,
|
||||
"children": MediaClass.TRACK,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.ARTIST: {
|
||||
"parent": MediaClass.ARTIST,
|
||||
"children": MediaClass.ALBUM,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.APP: { # spotify audiobook support
|
||||
"parent": MediaClass.APP,
|
||||
"children": MediaClass.TRACK,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.EPISODE: {
|
||||
"parent": MediaClass.EPISODE,
|
||||
"children": None,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.GENRE: {
|
||||
"parent": MediaClass.PLAYLIST,
|
||||
"children": MediaClass.TRACK,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.PLAYLIST: {
|
||||
"parent": MediaClass.PLAYLIST,
|
||||
"children": MediaClass.TRACK,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.PODCAST: {
|
||||
"parent": MediaClass.PODCAST,
|
||||
"children": MediaClass.EPISODE,
|
||||
"is_index_item": False,
|
||||
},
|
||||
MediaType.TRACK: {
|
||||
"parent": MediaClass.TRACK,
|
||||
"children": None,
|
||||
"is_index_item": False,
|
||||
}
|
||||
}
|
||||
"""
|
||||
# Spotify Library index definitions, containing media attributes that control content display.
|
||||
# The order listed is how they are displayed in the media browser.
|
||||
"""
|
||||
|
||||
BROWSE_LIMIT = 50
|
||||
""" Max number of items to return from a Spotify Web API query. """
|
||||
|
||||
SPOTIFY_BROWSE_LIMIT_TOTAL = 100
|
||||
""" Max number of items to return from a SpotifyPlus integration request that supports paging. """
|
||||
|
||||
CATEGORY_BASE64:str = "category_base64::"
|
||||
""" Eye-catcher used to denote a serialized ContentItem. """
|
||||
|
||||
LOCAL_IMAGE_PREFIX:str = "/local/"
|
||||
""" Local image prefix value. """
|
||||
|
||||
PLAYABLE_MEDIA_TYPES = [
|
||||
MediaType.PLAYLIST,
|
||||
MediaType.ALBUM,
|
||||
MediaType.ARTIST,
|
||||
MediaType.APP, # spotify audiobook support
|
||||
MediaType.EPISODE,
|
||||
MediaType.PODCAST,
|
||||
MediaType.TRACK,
|
||||
]
|
||||
""" Array of all media types that are playable. """
|
||||
|
||||
|
||||
class MissingMediaInformation(BrowseError):
|
||||
"""Missing media required information."""
|
||||
|
||||
|
||||
class UnknownMediaType(BrowseError):
|
||||
"""Unknown media type."""
|
||||
|
||||
|
||||
def deserialize_object(txt:str) -> object:
|
||||
"""
|
||||
Deserialize an object from a plain text string.
|
||||
|
||||
Args:
|
||||
txt (str):
|
||||
The serialized version of the object in the form of a string.
|
||||
|
||||
Returns:
|
||||
An object that was deserialized from a base64 string representation.
|
||||
"""
|
||||
base64_bytes = txt.encode('ascii')
|
||||
message_bytes = base64.b64decode(base64_bytes)
|
||||
obj = pickle.loads(message_bytes)
|
||||
return obj
|
||||
|
||||
|
||||
@staticmethod
|
||||
def serialize_object(obj:object) -> str:
|
||||
"""
|
||||
Serialize an object into a plain text string.
|
||||
|
||||
Args:
|
||||
obj (object):
|
||||
The object to serialize.
|
||||
|
||||
Returns:
|
||||
A serialized base64 string representation of the object.
|
||||
"""
|
||||
message_bytes = pickle.dumps(obj)
|
||||
base64_bytes = base64.b64encode(message_bytes)
|
||||
txt = base64_bytes.decode('ascii')
|
||||
return txt
|
||||
|
||||
|
||||
async def async_browse_media_library_index(
|
||||
hass:HomeAssistant,
|
||||
client:SpotifyClient,
|
||||
playerName:str,
|
||||
source:str|None,
|
||||
libraryMap:dict,
|
||||
libraryIndex:BrowsableMedia,
|
||||
media_content_type:str|None,
|
||||
media_content_id:str|None,
|
||||
) -> BrowseMedia:
|
||||
"""
|
||||
Builds a BrowseMedia object for the top level index page, and all of it's
|
||||
child nodes.
|
||||
|
||||
Args:
|
||||
hass (HomeAssistant):
|
||||
HomeAssistant instance.
|
||||
client (SpotifyClient):
|
||||
The SpotifyClient instance that will make calls to the device
|
||||
to retrieve the data for display in the media browser.
|
||||
playerName (str):
|
||||
Name of the media player that is calling this method (for tracing purposes).
|
||||
source (str):
|
||||
Currently selected source value.
|
||||
libraryMap (dict):
|
||||
The library map that contains media content attributes for each library index entry.
|
||||
libraryIndex (BrowseMedia):
|
||||
The library index of media content types.
|
||||
media_content_type (str):
|
||||
Selected media content type in the media browser.
|
||||
This value will be None upon the initial entry to the media browser.
|
||||
media_content_id (str):
|
||||
Selected media content id in the media browser.
|
||||
This value will be None upon the initial entry to the media browser.
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug)
|
||||
methodParms.AppendKeyValue("playerName", playerName)
|
||||
methodParms.AppendKeyValue("source", source)
|
||||
methodParms.AppendKeyValue("libraryMap", libraryMap)
|
||||
methodParms.AppendKeyValue("libraryIndex", libraryIndex)
|
||||
methodParms.AppendKeyValue("media_content_type", media_content_type)
|
||||
methodParms.AppendKeyValue("media_content_id", media_content_id)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "'%s': browsing for media - top level index: '%s'" % (playerName, libraryIndex), methodParms)
|
||||
|
||||
# validations.
|
||||
if source is None:
|
||||
source = "unknownSource"
|
||||
|
||||
# get parent media atttributes based upon selected media content type.
|
||||
parentAttrs:dict[str, Any] = libraryMap.get(libraryIndex.value, None)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for parent media content type: '%s'" % (playerName, libraryIndex.value), parentAttrs)
|
||||
|
||||
# create the index.
|
||||
browseMedia:BrowseMedia = BrowseMedia(
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
children=[],
|
||||
children_media_class=parentAttrs["children"],
|
||||
media_class=parentAttrs["parent"],
|
||||
media_content_id=libraryIndex.value,
|
||||
media_content_type=libraryIndex.value,
|
||||
thumbnail=parentAttrs["image"],
|
||||
title=parentAttrs["title"],
|
||||
)
|
||||
|
||||
# add child items to the index.
|
||||
for mediaType, childAttrs in libraryMap.items():
|
||||
|
||||
# if not an index item then don't bother.
|
||||
isIndexItem:bool = childAttrs.get("is_index_item", True)
|
||||
if not isIndexItem:
|
||||
continue
|
||||
|
||||
# trace.
|
||||
#_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for child media content type: '%s'" % (playerName, mediaType), childAttrs)
|
||||
|
||||
# if a LOCAL index image was specified, then ensure it exists.
|
||||
# otherwise, default to null.
|
||||
image:str = childAttrs.get("image", None)
|
||||
if image is not None and image.startswith(LOCAL_IMAGE_PREFIX):
|
||||
imagePath:str = "%s/www/%s" % (hass.config.config_dir, image[len(LOCAL_IMAGE_PREFIX):])
|
||||
if not os.path.exists(imagePath):
|
||||
#_logsi.LogVerbose("'%s': could not find logo image path '%s'; image will be reset to null" % (playerName, imagePath))
|
||||
image = None
|
||||
|
||||
browseMediaChild:BrowseMedia = BrowseMedia(
|
||||
can_expand=True,
|
||||
can_play=False,
|
||||
children=None,
|
||||
children_media_class=childAttrs["children"],
|
||||
media_class=childAttrs["parent"],
|
||||
media_content_id=f"{mediaType}",
|
||||
media_content_type=f"{mediaType}",
|
||||
thumbnail=image,
|
||||
title=childAttrs["title"],
|
||||
)
|
||||
browseMedia.children.append(browseMediaChild)
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Child Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMediaChild.media_content_type, browseMediaChild.media_content_id, browseMediaChild.title), browseMediaChild)
|
||||
|
||||
# trace.
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Parent Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia)
|
||||
|
||||
return browseMedia
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("'%s': BrowseMedia async_browse_media_library_index exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False)
|
||||
raise IntegrationError(str(ex)) from ex
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
def browse_media_node(
|
||||
hass:HomeAssistant,
|
||||
client:SpotifyClient,
|
||||
playerName:str,
|
||||
source:str|None,
|
||||
libraryMap:dict,
|
||||
media_content_type:str|None,
|
||||
media_content_id:str|None,
|
||||
) -> BrowseMedia:
|
||||
"""
|
||||
Builds a BrowseMedia object for a selected media content type, and all of it's
|
||||
child nodes.
|
||||
|
||||
Args:
|
||||
hass (HomeAssistant):
|
||||
HomeAssistant instance.
|
||||
client (SpotifyClient):
|
||||
The SpotifyClient instance that will make calls to the device
|
||||
to retrieve the data for display in the media browser.
|
||||
playerName (str):
|
||||
Name of the media player that is calling this method (for tracing purposes).
|
||||
source (str):
|
||||
Currently selected source value.
|
||||
libraryMap (dict):
|
||||
The library map that contains media content attributes for each library index entry.
|
||||
media_content_type (str):
|
||||
Selected media content type in the media browser.
|
||||
This value will be None upon the initial entry to the media browser.
|
||||
media_content_id (str):
|
||||
Selected media content id in the media browser.
|
||||
This value will be None upon the initial entry to the media browser.
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug)
|
||||
methodParms.AppendKeyValue("playerName", playerName)
|
||||
methodParms.AppendKeyValue("source", source)
|
||||
methodParms.AppendKeyValue("libraryMap", libraryMap)
|
||||
methodParms.AppendKeyValue("media_content_type", media_content_type)
|
||||
methodParms.AppendKeyValue("media_content_id", media_content_id)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "'%s': browsing for media - selected node: '%s'" % (playerName, media_content_type), methodParms)
|
||||
|
||||
# validations.
|
||||
if source is None:
|
||||
source = "unknownSource"
|
||||
|
||||
# initialize child item attributes.
|
||||
title:str = None
|
||||
image:str = None
|
||||
media:object = None
|
||||
items:list = []
|
||||
|
||||
# build selection list based upon the browsable media type.
|
||||
# - media: will contain the result of the spotify web api call.
|
||||
# - items: will contain the child items to display for the parent media item.
|
||||
# - title: the title to display in the media browser.
|
||||
# - image: the image (if any) to display in the media browser (can be none).
|
||||
if media_content_type == BrowsableMedia.SPOTIFY_USER_PLAYLISTS:
|
||||
_logsi.LogVerbose("'%s': querying spotify for Playlist Favorites" % playerName)
|
||||
media:PlaylistPageSimplified = client.GetPlaylistFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_FOLLOWED_ARTISTS:
|
||||
_logsi.LogVerbose("'%s': querying spotify for Artists Followed" % playerName)
|
||||
media:ArtistPage = client.GetArtistsFollowed(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_ALBUMS:
|
||||
_logsi.LogVerbose("Getting Spotify user Album favorites")
|
||||
media:AlbumPageSaved = client.GetAlbumFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.GetAlbums()
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_TRACKS:
|
||||
_logsi.LogVerbose("Getting Spotify user Track favorites")
|
||||
media:TrackPageSaved = client.GetTrackFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.GetTracks()
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_SHOWS:
|
||||
_logsi.LogVerbose("Getting Spotify user Show favorites")
|
||||
media:ShowPageSaved = client.GetShowFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.GetShows()
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_AUDIOBOOKS:
|
||||
_logsi.LogVerbose("Getting Spotify user Audiobook favorites")
|
||||
media:AudiobookPageSimplified = client.GetAudiobookFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_RECENTLY_PLAYED:
|
||||
_logsi.LogVerbose("Getting Spotify user Recently Played Tracks")
|
||||
media:PlayHistoryPage = client.GetPlayerRecentTracks(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.GetTracks()
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_TOP_ARTISTS:
|
||||
_logsi.LogVerbose("Getting Spotify user Top Artists")
|
||||
media:ArtistPage = client.GetUsersTopArtists(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_USER_TOP_TRACKS:
|
||||
_logsi.LogVerbose("Getting Spotify user Top Tracks")
|
||||
media:TrackPage = client.GetUsersTopTracks(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_FEATURED_PLAYLISTS:
|
||||
_logsi.LogVerbose("Getting Spotify Featured Playlists")
|
||||
media:PlaylistPageSimplified
|
||||
media, message = client.GetFeaturedPlaylists(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORYS:
|
||||
_logsi.LogVerbose("Getting Spotify Categories")
|
||||
media:list[Category] = client.GetBrowseCategorysList(refresh=False)
|
||||
items = media
|
||||
|
||||
# add a "Uri" attribute to each category in the cached category list.
|
||||
# this is so we can process categories just like other index types, as
|
||||
# spotify categories are simply playlists of tracks.
|
||||
category:Category
|
||||
for category in client.ConfigurationCache["GetBrowseCategorysList"]:
|
||||
if hasattr(category, "Uri"):
|
||||
_logsi.LogVerbose("Category Uri's have already been set in the cache; don't need to do it again")
|
||||
break
|
||||
setattr(category, "Uri", f"spotify:category:{category.Id}")
|
||||
_logsi.LogVerbose("Category Uri set: Name='%s', Id='%s', Uri='%s'" % (category.Name, category.Id, category.Uri))
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS:
|
||||
_logsi.LogVerbose("Getting Spotify Category Playlist")
|
||||
|
||||
# was a base64 encoded category object supplied? if not, then it's a problem!
|
||||
if not media_content_id.startswith(CATEGORY_BASE64):
|
||||
raise ValueError("'%s': media content type '%s' is not a serialized Category object!" % (playerName, media_content_type))
|
||||
|
||||
# drop the eye-ctacher and deserialize the category object.
|
||||
category:Category = Category()
|
||||
media_content_id = media_content_id[len(CATEGORY_BASE64):]
|
||||
category = deserialize_object(media_content_id)
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': deserialized %s" % (playerName, category.ToString()), category, excludeNonPublic=True)
|
||||
media_content_id = category.Id
|
||||
|
||||
# get the playlists for the category id.
|
||||
media:PlaylistPageSimplified
|
||||
media, message = client.GetCategoryPlaylists(media_content_id, limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
title = category.Name
|
||||
image = category.ImageUrl
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU:
|
||||
_logsi.LogVerbose("Getting Spotify 'Made For You' Category Playlist")
|
||||
media_content_id = '0JQ5DAt0tbjZptfcdMSKl3' # special hidden category "Made For You"
|
||||
media, message = client.GetCategoryPlaylists(media_content_id, limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == BrowsableMedia.SPOTIFY_NEW_RELEASES:
|
||||
_logsi.LogVerbose("Getting Spotify Album New Releases")
|
||||
media:AlbumPageSimplified = client.GetAlbumNewReleases(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
|
||||
elif media_content_type == MediaType.ALBUM:
|
||||
_logsi.LogVerbose("Getting Spotify Album Tracks")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
media:Album = client.GetAlbum(spotifyId)
|
||||
items = media.Tracks.Items
|
||||
title = media.Name
|
||||
image = media.ImageUrl
|
||||
|
||||
elif media_content_type == MediaType.ARTIST:
|
||||
_logsi.LogVerbose("Getting Spotify Artist Albums")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
artist:Artist = client.GetArtist(spotifyId) # for cover image
|
||||
media:AlbumPageSimplified = client.GetArtistAlbums(spotifyId, include_groups='album', limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
|
||||
items = media.Items
|
||||
title = artist.Name
|
||||
image = artist.ImageUrl
|
||||
|
||||
elif media_content_type == MediaType.GENRE:
|
||||
_logsi.LogVerbose("Getting Spotify Genre Playlist")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
media:Playlist = client.GetPlaylist(spotifyId)
|
||||
items = media.GetTracks()
|
||||
title = media.Name
|
||||
image = media.ImageUrl
|
||||
|
||||
elif media_content_type == MediaType.PLAYLIST:
|
||||
_logsi.LogVerbose("Getting Spotify Playlist")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
media:Playlist = client.GetPlaylist(spotifyId)
|
||||
items = media.GetTracks()
|
||||
title = media.Name
|
||||
image = media.ImageUrl
|
||||
|
||||
elif media_content_type == MediaType.PODCAST:
|
||||
_logsi.LogVerbose("Getting Spotify Show / Podcast")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
media:Show = client.GetShow(spotifyId)
|
||||
items = media.Episodes.Items
|
||||
title = media.Name
|
||||
image = media.ImageUrl
|
||||
|
||||
elif media_content_type == MediaType.APP: # spotify audiobook support
|
||||
_logsi.LogVerbose("Getting Spotify Audiobook")
|
||||
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
|
||||
media:Audiobook = client.GetAudiobook(spotifyId)
|
||||
items = media.Chapters.Items
|
||||
title = media.Name
|
||||
image = media.ImageUrl
|
||||
|
||||
# if media was not set then we are done.
|
||||
if media is None:
|
||||
raise ValueError("'%s': could not find media items for content type '%s'" % (playerName, media_content_type))
|
||||
|
||||
# set index flag indicating if index media can be played or not.
|
||||
canPlay:bool = media_content_type in PLAYABLE_MEDIA_TYPES
|
||||
|
||||
# track and episode media items cannot be expanded (only played);
|
||||
# other media types can be expanded to display child items (e.g. Album, Artist, Playlist, etc).
|
||||
canExpand = media_content_type not in [
|
||||
MediaType.TRACK,
|
||||
MediaType.EPISODE,
|
||||
]
|
||||
|
||||
# if a LOCAL index image was specified, then ensure it exists.
|
||||
# otherwise, default to null.
|
||||
if image is not None and image.startswith(LOCAL_IMAGE_PREFIX):
|
||||
imagePath:str = "%s/www/%s" % (hass.config.config_dir, image[len(LOCAL_IMAGE_PREFIX):])
|
||||
if not os.path.exists(imagePath):
|
||||
image = None
|
||||
|
||||
# get parent media atttributes based upon selected media content type.
|
||||
parentAttrs:dict[str, Any] = libraryMap.get(media_content_type, None)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for parent media content type: '%s'" % (playerName, media_content_type), parentAttrs)
|
||||
|
||||
# get parent attributes that are not set.
|
||||
if title is None:
|
||||
title = parentAttrs.get("title_node", media_content_id)
|
||||
|
||||
# create the index.
|
||||
browseMedia:BrowseMedia = BrowseMedia(
|
||||
can_expand=canExpand,
|
||||
can_play=canPlay,
|
||||
children=[],
|
||||
children_media_class=parentAttrs["children"],
|
||||
media_class=parentAttrs["parent"],
|
||||
media_content_id=media_content_id,
|
||||
media_content_type=media_content_type,
|
||||
thumbnail=image,
|
||||
title=title,
|
||||
)
|
||||
|
||||
# add child items to the index.
|
||||
for item in items:
|
||||
|
||||
# resolve media content type.
|
||||
mediaType:str = parentAttrs["children"]
|
||||
|
||||
# get child media atttributes based upon child item media content type.
|
||||
childAttrs:dict[str, Any] = libraryMap.get(mediaType, None)
|
||||
#_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for child media content type: '%s'" % (playerName, mediaType), childAttrs)
|
||||
|
||||
# set child flag indicating if media can be played or not.
|
||||
canPlay:bool = mediaType in PLAYABLE_MEDIA_TYPES
|
||||
|
||||
# track and episode media items cannot be expanded (only played);
|
||||
# other media types can be expanded to display child items (e.g. Album, Artist, Playlist, etc).
|
||||
canExpand = mediaType not in [
|
||||
MediaType.TRACK,
|
||||
MediaType.EPISODE,
|
||||
]
|
||||
|
||||
# resolve media content id.
|
||||
# default the value to the media type.
|
||||
mediaId:str = mediaType
|
||||
if canPlay:
|
||||
|
||||
# if it is playable then use the Uri value instead, as we know it's a track or episode.
|
||||
mediaId = item.Uri
|
||||
|
||||
elif mediaType == MediaType.GENRE:
|
||||
|
||||
# if it's GENRE content, then serialize the Category object and use it instead so that
|
||||
# we don't have to go get it again - we will deserialize it when the child node is
|
||||
# selected, and use it to resolve the category Id, Name, and imageUrl values;
|
||||
mediaId = "%s%s" % (CATEGORY_BASE64, serialize_object(item))
|
||||
mediaType = BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS.value
|
||||
|
||||
# build the child node.
|
||||
browseMediaChild:BrowseMedia = BrowseMedia(
|
||||
can_expand=canExpand,
|
||||
can_play=canPlay,
|
||||
children=None,
|
||||
children_media_class=childAttrs["children"],
|
||||
media_class=childAttrs["parent"],
|
||||
media_content_id=mediaId,
|
||||
media_content_type=mediaType,
|
||||
thumbnail=item.ImageUrl,
|
||||
title=item.Name,
|
||||
)
|
||||
|
||||
# if parent is an audiobook and we are displaying its track items (e.g. chapters), then
|
||||
# remove child images as they are all the same.
|
||||
if media_content_type == MediaType.APP and mediaType == MediaType.TRACK:
|
||||
browseMediaChild.thumbnail = None
|
||||
|
||||
# add the child item to the list of children.
|
||||
browseMedia.children.append(browseMediaChild)
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Child Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMediaChild.media_content_type, browseMediaChild.media_content_id, browseMediaChild.title), browseMediaChild)
|
||||
|
||||
# trace.
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Parent Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia)
|
||||
|
||||
return browseMedia
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("'%s': BrowseMedia browse_media_node exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False)
|
||||
raise IntegrationError(str(ex)) from ex
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
@@ -0,0 +1,839 @@
|
||||
"""
|
||||
User interface config flow for SpotifyPlus integration.
|
||||
|
||||
Integrations can be set up via the user interface by adding support for a config
|
||||
flow to create a config entry. Components that want to support config entries will
|
||||
need to define a Config Flow Handler. This handler will manage the creation of
|
||||
entries from user input, discovery or other sources (like Home Assistant OS).
|
||||
|
||||
Config Flow Handlers control the data that is stored in a config entry. This means
|
||||
that there is no need to validate that the config is correct when Home Assistant
|
||||
starts up. It will also prevent breaking changes, because we will be able to migrate
|
||||
configuration entries to new formats if the version changes.
|
||||
|
||||
When instantiating the handler, Home Assistant will make sure to load all
|
||||
dependencies and install the requirements of the component.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
import voluptuous as vol
|
||||
import ssl
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from spotifywebapipython import SpotifyClient
|
||||
from spotifywebapipython.models import Device, SpotifyConnectDevices
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
SOURCE_REAUTH,
|
||||
)
|
||||
from homeassistant.const import CONF_DESCRIPTION, CONF_ID, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv, selector
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_OPTION_ALWAYS_ON,
|
||||
CONF_OPTION_DEVICE_DEFAULT,
|
||||
CONF_OPTION_DEVICE_LOGINID,
|
||||
CONF_OPTION_DEVICE_PASSWORD,
|
||||
CONF_OPTION_DEVICE_USERNAME,
|
||||
CONF_OPTION_SCRIPT_TURN_OFF,
|
||||
CONF_OPTION_SCRIPT_TURN_ON,
|
||||
CONF_OPTION_SOURCE_LIST_HIDE,
|
||||
CONF_OPTION_SPOTIFY_SCAN_INTERVAL,
|
||||
CONF_OPTION_TURN_OFF_AUTO_PAUSE,
|
||||
CONF_OPTION_TURN_ON_AUTO_RESUME,
|
||||
CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT,
|
||||
DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
DOMAIN_SCRIPT,
|
||||
SPOTIFY_SCOPES
|
||||
)
|
||||
from .instancedata_spotifyplus import InstanceDataSpotifyPlus
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIColors, SIMethodParmListContext
|
||||
import logging
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpotifyPlusConfigFlow(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""
|
||||
Config flow to handle SpotifyPlus OAuth2 authentication.
|
||||
"""
|
||||
|
||||
# integration DOMAIN (must be a local variable; can't use the constant reference).
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
# integration configuration entry major version number.
|
||||
VERSION = 2
|
||||
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict[str, Any]:
|
||||
"""
|
||||
Extra data that needs to be appended to the authorize url.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the extra data.
|
||||
"""
|
||||
data:dict = \
|
||||
{
|
||||
"scope": ",".join(SPOTIFY_SCOPES),
|
||||
"show_dialog": "true"
|
||||
}
|
||||
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Configure Component extra_authorize_data - data (dictionary)", data, colorValue=SIColors.Tan)
|
||||
return data
|
||||
|
||||
|
||||
async def async_generate_authorize_url(self) -> str:
|
||||
"""
|
||||
Generate a url for the user to authorize.
|
||||
|
||||
Returns:
|
||||
A string containing the authorization url used to authenticate the user.
|
||||
"""
|
||||
# verify ssl / tls setup prior to authorizing, just in case the PyOpenSSL
|
||||
# module is injected (causes Spotify authorization to fail).
|
||||
self._ssl_verify()
|
||||
|
||||
# invoke base class method.
|
||||
return await super().async_generate_authorize_url()
|
||||
|
||||
|
||||
async def async_oauth_create_entry(self, data:dict[str,Any]) -> ConfigFlowResult:
|
||||
"""
|
||||
Create an oauth config entry or update existing entry for reauth.
|
||||
|
||||
Args:
|
||||
data (dict):
|
||||
Configuration data for the entry (e.g. id, name, token, auth_implementation, etc).
|
||||
|
||||
Returns:
|
||||
A `ConfigFlowResult` object that indicates the flow result.
|
||||
"""
|
||||
spotifyClient:SpotifyClient = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow is starting the OAuth2 config entry flow - parameters", data, prettyPrint=True, colorValue=SIColors.Tan)
|
||||
|
||||
try:
|
||||
|
||||
# get shared zeroconf instance.
|
||||
_logsi.LogVerbose("Retrieving the HA shared Zeroconf instance", colorValue=SIColors.Tan)
|
||||
zeroconf_instance = await zeroconf.async_get_instance(self.hass)
|
||||
|
||||
# create new spotify web api python client instance - "SpotifyClient()".
|
||||
# note that Spotify Connect Directory task will be disabled, since we don't need it
|
||||
# for creating the OAuth2 application credentials.
|
||||
_logsi.LogVerbose("Creating SpotifyClient instance, and retrieving account information", colorValue=SIColors.Tan)
|
||||
tokenStorageDir:str = "%s/.storage" % (self.hass.config.config_dir)
|
||||
tokenStorageFile:str = "%s_tokens.json" % (DOMAIN)
|
||||
spotifyClient = await self.hass.async_add_executor_job(
|
||||
SpotifyClient,
|
||||
None, # manager:PoolManager=None,
|
||||
tokenStorageDir, # tokenStorageDir:str=None,
|
||||
tokenStorageFile, # tokenStorageFile:str=None,
|
||||
None, # tokenUpdater:Callable=None,
|
||||
zeroconf_instance, # zeroconfClient:Zeroconf=None,
|
||||
None, # spotifyConnectUsername:str=None,
|
||||
None, # spotifyConnectPassword:str=None,
|
||||
None, # spotifyConnectLoginId:str=None,
|
||||
0, # spotifyConnectDiscoveryTimeout:float=2.0, # 0 to disable Spotify Connect Zeroconf browsing features.
|
||||
False, # spotifyConnectDirectoryEnabled:bool=True, # disable Spotify Connect Directory Task.
|
||||
None, # spotifyWebPlayerCookieSpdc:str=None,
|
||||
None, # spotifyWebPlayerCookieSpkey:str=None,
|
||||
)
|
||||
_logsi.LogObject(SILevel.Verbose, "SpotifyClient instance created - object", spotifyClient, colorValue=SIColors.Tan)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
_logsi.LogException(None, ex, colorValue=SIColors.Tan)
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
try:
|
||||
|
||||
clientId:str = None
|
||||
tokenProfileId:str = None
|
||||
|
||||
# set spotify web api token authorization from HA-managed OAuth2 session token.
|
||||
_logsi.LogVerbose("Setting SpotifyClient token authorization from OAuth2 session token", colorValue=SIColors.Tan)
|
||||
await self.hass.async_add_executor_job(
|
||||
spotifyClient.SetAuthTokenFromToken, clientId, data["token"], tokenProfileId
|
||||
)
|
||||
|
||||
_logsi.LogObject(SILevel.Verbose, "SpotifyClient token authorization was set - object (with AuthToken)", spotifyClient, colorValue=SIColors.Tan)
|
||||
_logsi.LogObject(SILevel.Verbose, "SpotifyClient UserProfile - object", spotifyClient.UserProfile, colorValue=SIColors.Tan)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
_logsi.LogException(None, ex, colorValue=SIColors.Tan)
|
||||
return self.async_abort(reason="setauthtoken_error")
|
||||
|
||||
# set configuration entry unique id (e.g. spotify profile id).
|
||||
# this value should match the value assigned in media_player `_attr_unique_id` attribute.
|
||||
await self.async_set_unique_id(spotifyClient.UserProfile.Id + "_" + DOMAIN)
|
||||
_logsi.LogVerbose("ConfigFlow assigned unique_id of '%s' for Spotify UserProfile '%s'" % (self.unique_id, spotifyClient.UserProfile.DisplayName), colorValue=SIColors.Tan)
|
||||
|
||||
# is this a reauthentication request?
|
||||
# if so, then abort if the unique ID does not match the reauth / reconfigure context.
|
||||
if self.source == SOURCE_REAUTH:
|
||||
_logsi.LogVerbose("ConfigFlow re-auth source detected; checking for account mismatch using unique_id of '%s' for Spotify DisplayName '%s'" % (self.unique_id, spotifyClient.UserProfile.DisplayName), colorValue=SIColors.Tan)
|
||||
self._abort_if_unique_id_mismatch(reason="reauth_account_mismatch")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), title=spotifyClient.UserProfile.DisplayName, data=data
|
||||
)
|
||||
|
||||
# one final check to see if a configuration entry already exists for the unique id.
|
||||
# if it IS already configured, then we will display an "already_configured" message
|
||||
# to the user and halt the flow to prevent a duplicate configuration entry.
|
||||
# this will also prevent the HA system log warning: "Detected that custom integration
|
||||
# 'xxx'xcreates a config entry when another entry with the same unique ID exists.".
|
||||
_logsi.LogVerbose("ConfigFlow is verifying USER ENTRY device details have not already been configured: unique_id=\"%s\", display name=\"%s\"" % (self.unique_id, spotifyClient.UserProfile.DisplayName), colorValue=SIColors.Tan)
|
||||
self._abort_if_unique_id_configured(
|
||||
error="already_configured",
|
||||
description_placeholders={
|
||||
"userinfo_name": spotifyClient.UserProfile.DisplayName,
|
||||
"userinfo_id": spotifyClient.UserProfile.Id
|
||||
},
|
||||
)
|
||||
|
||||
# set configuration data parameters.
|
||||
data[CONF_ID] = spotifyClient.UserProfile.Id
|
||||
data[CONF_NAME] = spotifyClient.UserProfile.DisplayName
|
||||
data[CONF_DESCRIPTION] = "(%s account)" % spotifyClient.UserProfile.Product.capitalize()
|
||||
|
||||
# create the configuration entry.
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow is creating a configuration entry for Spotify Id='%s', name='%s'" % (data[CONF_ID], data[CONF_NAME]), data, colorValue=SIColors.Tan)
|
||||
configEntry:FlowResult = self.async_create_entry(
|
||||
title=f"SpotifyPlus {data[CONF_NAME]}",
|
||||
description=data[CONF_DESCRIPTION],
|
||||
data=data
|
||||
)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow created configuration entry for Spotify Id='%s', name='%s'" % (data[CONF_ID], data[CONF_NAME]), data, prettyPrint=True, colorValue=SIColors.Tan)
|
||||
return configEntry
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False, colorValue=SIColors.Tan)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# dispose of resources.
|
||||
if (spotifyClient is not None):
|
||||
spotifyClient.Dispose()
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
|
||||
|
||||
async def async_step_reauth(self, entry_data:Mapping[str, Any]) -> ConfigFlowResult:
|
||||
"""
|
||||
Perform reauth upon an API authentication error or migration of old entries.
|
||||
|
||||
Args:
|
||||
entry_data (dict|None):
|
||||
A dictionary of entry data values.
|
||||
|
||||
Returns:
|
||||
A `ConfigFlowResult` object that indicates the flow result.
|
||||
|
||||
Refer to the following developer guide on how to handle expired application credentials:
|
||||
- https://developers.home-assistant.io/docs/integration_setup_failures/#handling-expired-credentials
|
||||
- https://developers.home-assistant.io/docs/config_entries_config_flow_handler/#reauthentication
|
||||
|
||||
ReAuth processing can be tested by calling the `entry.async_start_reauth(hass)` method
|
||||
from the __init__.py `async_setup_entry` logic (after session token is established).
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow is starting the OAuth2 Re-Authentication flow - user input parameters", entry_data, prettyPrint=True, colorValue=SIColors.Tan)
|
||||
|
||||
# prompt the user to re-authenticate.
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False, colorValue=SIColors.Tan)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input:dict[str,Any]|None=None) -> ConfigFlowResult:
|
||||
"""
|
||||
Dialog that informs the user that reauth is required.
|
||||
|
||||
Args:
|
||||
user_input (dict|None):
|
||||
A dictionary of input data values the user entered on the form if
|
||||
the form was submitted, or null if the form is being shown initially.
|
||||
|
||||
Returns:
|
||||
A `ConfigFlowResult` object that indicates the flow result.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow is confirming the OAuth2 Re-Authentication flow - user input parameters", user_input, prettyPrint=True, colorValue=SIColors.Tan)
|
||||
|
||||
reauth_account_name = "unknown"
|
||||
reauth_account_id = "unknown"
|
||||
|
||||
# get reauth config entry linked to the current context.
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if (reauth_entry is not None):
|
||||
|
||||
# trace.
|
||||
if (_logsi.IsOn(SILevel.Verbose)):
|
||||
_logsi.LogObject(SILevel.Verbose, "ConfigFlow OAuth2 Re-Authentication flow - reauth_entry object", reauth_entry, colorValue=SIColors.Tan)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "ConfigFlow OAuth2 Re-Authentication flow - reauth_entry data", reauth_entry.data, prettyPrint=True, colorValue=SIColors.Tan)
|
||||
|
||||
# default reauth account information details.
|
||||
# note that there could be instances where the "id" and "name" attributes
|
||||
# do not exist in the reauth entry data; in this case, we will use the
|
||||
# unique_id and the title instead.
|
||||
reauth_account_id = reauth_entry.data.get(CONF_ID, None) or reauth_entry.unique_id.replace("_" + DOMAIN, "") # drop the "_spotifyplus" suffix
|
||||
reauth_account_name = reauth_entry.data.get(CONF_NAME, None) or reauth_entry.title
|
||||
|
||||
# if user has not authenticated then prompt the user to authenticate.
|
||||
if user_input is None:
|
||||
|
||||
_logsi.LogVerbose("ConfigFlow is prompting user to authenticate for account name \"%s\", id \"%s\"" % (reauth_account_name, reauth_account_id), colorValue=SIColors.Tan)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={
|
||||
"account_id": reauth_account_id,
|
||||
"account_name": reauth_account_name
|
||||
},
|
||||
errors={},
|
||||
)
|
||||
|
||||
# at this point, the user has been authenticated to Spotify.
|
||||
# now the user needs to pick an OAuth2 implementation (e.g. application credentials) to use.
|
||||
return await self.async_step_pick_implementation(
|
||||
user_input={"implementation": reauth_entry.data["auth_implementation"]}
|
||||
)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False, colorValue=SIColors.Tan)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Tan)
|
||||
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> SpotifyPlusOptionsFlow:
|
||||
"""
|
||||
Get the options flow for this handler, which enables options support.
|
||||
|
||||
This method is invoked when a user clicks the "Configure" button from the integration
|
||||
details page of the UI.
|
||||
"""
|
||||
return SpotifyPlusOptionsFlow(config_entry)
|
||||
|
||||
|
||||
def _ssl_verify(self) -> None:
|
||||
"""
|
||||
Check if pyopenssl was injected into urllib3 by the `requests` module initialization.
|
||||
|
||||
At this point, the integration has not been installed yet but is in the process of being installed.
|
||||
|
||||
If pyopenssl is injected then we will log a warning as Spotify authorization will probably
|
||||
fail with a `certificate verify failed` error when pyopenssl `do_handshake()` method is called.
|
||||
"""
|
||||
apiMethodName:str = 'config_flow._ssl_verify'
|
||||
apiMethodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# if pyopenssl is injected then we will log a warning as Spotify authorization will probably
|
||||
# fail with a `certificate verify failed` error when pyopenssl `do_handshake()` method is called.
|
||||
# in this case, the certificate chain is valid and processing successfully, but the failure
|
||||
# occurs during TLS Server Name Indication (SNI) processing. the pyopenssl module does not
|
||||
# support TLS SNI processing.
|
||||
|
||||
# this is usually caused by a conflicting custom integration that is installed in HA.
|
||||
# the offending integration can usually be found by searching the HA /config/custom_components/
|
||||
# folder (and all sub-folders) for these phrases: "urllib3", "pyopenssl".
|
||||
|
||||
# try:
|
||||
# # TEST TODO force urllib3 to use PyOpenSSL for testing.
|
||||
# _logsi.LogWarning("TEST TODO forcing urllib3 to use PyOpenSSL for TLS verification testing", colorValue=SIColors.Red)
|
||||
# import urllib3.contrib.pyopenssl
|
||||
# urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
# except Exception as ex:
|
||||
# _logsi.LogException("TEST TODO Could not force urllib3 to use PyOpenSSL for TLS verification testing!", ex, logToSystemLogger=False, colorValue=SIColors.Red)
|
||||
|
||||
# force a reference to requests, which will call `inject_into_urllib3` method
|
||||
# if the PyOpenSSL library is installed and HAS_SNI = False (SNI support disabled).
|
||||
import requests
|
||||
|
||||
# is TLS SNI support enabled in Python?
|
||||
has_sni:bool = getattr(ssl, "HAS_SNI", False)
|
||||
|
||||
# trace.
|
||||
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName, colorValue=SIColors.Tan)
|
||||
apiMethodParms.AppendKeyValue("Python Version", sys.version)
|
||||
apiMethodParms.AppendKeyValue("TLS SNI Support?", "ENABLED in Python" if has_sni else "DISABLED")
|
||||
apiMethodParms.AppendKeyValue("OpenSSL Version", ssl.OPENSSL_VERSION)
|
||||
apiMethodParms.AppendKeyValue("OpenSSL HAS_SNI", getattr(ssl, "HAS_SNI", False))
|
||||
apiMethodParms.AppendKeyValue("OpenSSL Default Verify Paths", ssl.get_default_verify_paths())
|
||||
|
||||
is_pyopenssl:bool = False
|
||||
try:
|
||||
import urllib3.util.ssl_
|
||||
is_pyopenssl = getattr(urllib3.util.ssl_, "IS_PYOPENSSL", False)
|
||||
apiMethodParms.AppendKeyValue("urllib3 present?", "YES")
|
||||
apiMethodParms.AppendKeyValue("urllib3 Using PyOpenSSL?", is_pyopenssl)
|
||||
except Exception as e:
|
||||
apiMethodParms.AppendKeyValue("urllib3 check failed", str(e))
|
||||
|
||||
try:
|
||||
import cryptography
|
||||
apiMethodParms.AppendKeyValue("Cryptography Version", cryptography.__version__)
|
||||
except Exception:
|
||||
apiMethodParms.AppendKeyValue("Cryptography", "NOT INSTALLED")
|
||||
|
||||
try:
|
||||
import OpenSSL
|
||||
apiMethodParms.AppendKeyValue("pyOpenSSL Version", OpenSSL.__version__)
|
||||
except Exception:
|
||||
apiMethodParms.AppendKeyValue("pyOpenSSL", "NOT INSTALLED")
|
||||
|
||||
# trace.
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "Verifying TLS and SSL authorization environment parameters", apiMethodParms, colorValue=SIColors.Tan)
|
||||
|
||||
# log various conditions that may cause Spotify OAuth2 to fail.
|
||||
if has_sni == False:
|
||||
_logsi.LogWarning("SSL TLS SNI support is DISABLED, which may cause Spotify OAuth to fail with `certificate verify failed` errors if pyopenssl is installed!")
|
||||
|
||||
if is_pyopenssl == True:
|
||||
|
||||
# The following will try to prevent PyOpenSSL from overriding the Python OpenSSL stack,
|
||||
# which can be a problem when the `requests` and `urllib3` libraries are used together.
|
||||
# The `requests.__init__` method overrides the standard Python OpenSSL library with the
|
||||
# PyOpenSSL library if TLS SNI support is disabled AND the PyOpenSSL library is
|
||||
# installed; it does this by calling the `pyopenssl.inject_into_urllib3()` method.
|
||||
#
|
||||
# To restore the standard Python OpenSSL library, the `pyopenssl.extract_from_urllib3()`
|
||||
# method can be called to remove the PyOpenSSL override.
|
||||
#
|
||||
# The PyOpenSSL library cannot be used with the Spotify Web API, as it causes all calls
|
||||
# to fail with "certificate verify failed" exceptions. Note that the SSL certificate
|
||||
# validation works fine, but the surrounding TLS Server Name Indication is what fails.
|
||||
#
|
||||
# The standard Python OpenSSL library correctly handles the TLS SNI verification to the
|
||||
# Spotify Web API.
|
||||
try:
|
||||
|
||||
_logsi.LogWarning("pyOpenSSL has been injected into URLLIB3 by another custom component! This could cause SSL `certificate verify failed` errors to occur due to TLS handshake failures", colorValue=SIColors.Tan)
|
||||
_logsi.LogWarning("Extracting pyOpenSSL from URLLIB3, and restoring the standard Python OpenSSL library", colorValue=SIColors.Tan)
|
||||
|
||||
# remove PyOpenSSL from urllib3, restoring the standard Python OpenSSL library.
|
||||
import urllib3.contrib.pyopenssl
|
||||
urllib3.contrib.pyopenssl.extract_from_urllib3()
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
_logsi.LogWarning("Could not restore the standard Python OpenSSL library! This could cause SSL `certificate verify failed` errors to occur due to TLS handshake failures! Error: %s" % str(ex), colorValue=SIColors.Red)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Could not determine OAuth2 SSL authorization environment parameters! Error: %s" % str(e), colorValue=SIColors.Red)
|
||||
# ignore exceptions here, as they will be handled if / when authorization fails.
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, apiMethodName, colorValue=SIColors.Tan)
|
||||
|
||||
|
||||
class SpotifyPlusOptionsFlow(OptionsFlow):
|
||||
"""
|
||||
Handles options flow for the component.
|
||||
|
||||
The options flow allows a user to configure additional options for the component at any time by
|
||||
navigating to the integrations page and clicking the Options button on the card for your component.
|
||||
Generally speaking these configuration values are optional, whereas values in the config flow are
|
||||
required to make the component function.
|
||||
"""
|
||||
|
||||
def __init__(self, entry:ConfigEntry) -> None:
|
||||
"""
|
||||
Initialize options flow.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': OptionsFlow is initializing - entry (ConfigEntry) object" % entry.title, entry)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow entry.data dictionary" % entry.title, entry.data)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow entry.options dictionary" % entry.title, entry.options)
|
||||
|
||||
# initialize storage.
|
||||
self._entry = entry
|
||||
self._name:str = None
|
||||
|
||||
# load config entry base values.
|
||||
self._name = entry.data.get(CONF_NAME, None)
|
||||
|
||||
# load config entry options values.
|
||||
self._Options = dict(entry.options)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
|
||||
"""
|
||||
Manage the options for the custom component.
|
||||
|
||||
Since we have multiple options dialog forms, we will simply return the first
|
||||
options form reference.
|
||||
"""
|
||||
return await self.async_step_01_options_basic()
|
||||
|
||||
|
||||
async def async_step_01_options_basic(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""
|
||||
Manage the basic options for the custom component.
|
||||
|
||||
Args:
|
||||
user_input (dict[str,Any]):
|
||||
User input gathered from the input form.
|
||||
This argument defaults to None when this step is first called.
|
||||
When the user clicks the submit button on the form, the argument will contain
|
||||
a dictionary of the data that was entered. Home Assistant will do some basic
|
||||
validation on your behalf based on the data schema that you defined (e.g.
|
||||
required field, port number is within a numeric range, etc).
|
||||
|
||||
For a good example, look at HA demo source code:
|
||||
/home-assistant-core/homeassistant/components/demo/config_flow.py
|
||||
|
||||
Note that the "self.hass.data[DOMAIN][entry.entry_id]" object is present in the
|
||||
OptionsFlow "async_step_init" step. This allows you to access the client if one
|
||||
is assigned to the data area. All you need to do is assign a reference to the
|
||||
"entry:ConfigEntry" in the "__init__" in order to access it. This saves you from
|
||||
instantiating a new instance of the client to retrieve settings.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow async_step_01_options_basic is starting - user_input" % self._name, user_input)
|
||||
|
||||
# if not the initial entry, then save the entered options; otherwise, prepare the form.
|
||||
if user_input is not None:
|
||||
|
||||
# update config entry options from user input values.
|
||||
self._Options[CONF_OPTION_ALWAYS_ON] = user_input.get(CONF_OPTION_ALWAYS_ON, None)
|
||||
self._Options[CONF_OPTION_DEVICE_DEFAULT] = user_input.get(CONF_OPTION_DEVICE_DEFAULT, None)
|
||||
self._Options[CONF_OPTION_SPOTIFY_SCAN_INTERVAL] = user_input.get(CONF_OPTION_SPOTIFY_SCAN_INTERVAL, DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL)
|
||||
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)
|
||||
self._Options[CONF_OPTION_SOURCE_LIST_HIDE] = user_input.get(CONF_OPTION_SOURCE_LIST_HIDE, None)
|
||||
self._Options[CONF_OPTION_TURN_OFF_AUTO_PAUSE] = user_input.get(CONF_OPTION_TURN_OFF_AUTO_PAUSE, True)
|
||||
self._Options[CONF_OPTION_TURN_ON_AUTO_RESUME] = user_input.get(CONF_OPTION_TURN_ON_AUTO_RESUME, True)
|
||||
self._Options[CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT] = user_input.get(CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT, True)
|
||||
|
||||
# validations.
|
||||
# spotify scan interval must be in the 4 to 60 range (if specified).
|
||||
spotifyScanInterval:int = user_input.get(CONF_OPTION_SPOTIFY_SCAN_INTERVAL, DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL)
|
||||
if (spotifyScanInterval is not None) and ((spotifyScanInterval < 4) or (spotifyScanInterval > 60)):
|
||||
errors["base"] = "spotify_scan_interval_range_invalid"
|
||||
|
||||
# any validation errors?
|
||||
if "base" not in errors:
|
||||
|
||||
# no - store the updated config entry options.
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow is updating configuration options - options" % self._name, self._Options)
|
||||
self._Options.update(user_input)
|
||||
|
||||
# show the next configuration options form.
|
||||
return await self.async_step_02_options_player()
|
||||
|
||||
# load available spotify connect devices;
|
||||
device_list:list[str] = await self.hass.async_add_executor_job(self._GetPlayerDevicesList)
|
||||
|
||||
# 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))
|
||||
|
||||
# if no devices, then remove device default value.
|
||||
if (device_list is None):
|
||||
device_list = []
|
||||
_logsi.LogVerbose("'%s': OptionsFlow option '%s' - Spotify Connect device list is empty; removing default device selection" % (self._name, CONF_OPTION_DEVICE_DEFAULT))
|
||||
self._Options.pop(CONF_OPTION_DEVICE_DEFAULT, None)
|
||||
|
||||
# create validation schema.
|
||||
schema = vol.Schema(
|
||||
{
|
||||
# note - DO NOT use "default" argument on the following - use "suggested_value" instead.
|
||||
# using "default=" does not allow a null value to be selected!
|
||||
vol.Optional(
|
||||
CONF_OPTION_DEVICE_DEFAULT,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_DEVICE_DEFAULT)},
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=device_list or [],
|
||||
mode=SelectSelectorMode.DROPDOWN
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_OPTION_SOURCE_LIST_HIDE,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_SOURCE_LIST_HIDE)},
|
||||
): 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,
|
||||
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,
|
||||
multiple=False),
|
||||
),
|
||||
vol.Optional(CONF_OPTION_SPOTIFY_SCAN_INTERVAL,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_SPOTIFY_SCAN_INTERVAL)},
|
||||
): cv.positive_int,
|
||||
vol.Optional(CONF_OPTION_ALWAYS_ON,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_ALWAYS_ON)},
|
||||
): cv.boolean,
|
||||
vol.Optional(CONF_OPTION_TURN_OFF_AUTO_PAUSE,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_TURN_OFF_AUTO_PAUSE)},
|
||||
default=True, # default to True if not supplied
|
||||
): cv.boolean,
|
||||
vol.Optional(CONF_OPTION_TURN_ON_AUTO_RESUME,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_TURN_ON_AUTO_RESUME)},
|
||||
default=True, # default to True if not supplied
|
||||
): cv.boolean,
|
||||
vol.Optional(CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT)},
|
||||
default=True, # default to True if not supplied
|
||||
): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
# any validation errors? if so, then log them.
|
||||
if "base" in errors:
|
||||
_logsi.LogDictionary(SILevel.Warning, "'%s': OptionsFlow 01_options_basic contained validation errors" % self._name, errors)
|
||||
|
||||
_logsi.LogVerbose("'%s': OptionsFlow is showing the 01_options_basic configuration options form" % self._name)
|
||||
return self.async_show_form(
|
||||
step_id="01_options_basic",
|
||||
data_schema=schema,
|
||||
description_placeholders={CONF_NAME: self._name},
|
||||
errors=errors or {},
|
||||
last_step=False,
|
||||
)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
async def async_step_02_options_player(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
||||
"""
|
||||
Manage the credential options for the custom component.
|
||||
|
||||
Args:
|
||||
user_input (dict[str,Any]):
|
||||
User input gathered from the input form.
|
||||
This argument defaults to None when this step is first called.
|
||||
When the user clicks the submit button on the form, the argument will contain
|
||||
a dictionary of the data that was entered. Home Assistant will do some basic
|
||||
validation on your behalf based on the data schema that you defined (e.g.
|
||||
required field, port number is within a numeric range, etc).
|
||||
|
||||
For a good example, look at HA demo source code:
|
||||
/home-assistant-core/homeassistant/components/demo/config_flow.py
|
||||
|
||||
Note that the "self.hass.data[DOMAIN][entry.entry_id]" object is present in the
|
||||
OptionsFlow "async_step_init" step. This allows you to access the client if one
|
||||
is assigned to the data area. All you need to do is assign a reference to the
|
||||
"entry:ConfigEntry" in the "__init__" in order to access it. This saves you from
|
||||
instantiating a new instance of the client to retrieve settings.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "'%s': OptionsFlow async_step_02_options_player is starting - user_input" % self._name, user_input)
|
||||
|
||||
# if not the initial entry, then save the entered options; otherwise, prepare the form.
|
||||
if user_input is not None:
|
||||
|
||||
# update config entry options from user input values.
|
||||
self._Options[CONF_OPTION_DEVICE_LOGINID] = user_input.get(CONF_OPTION_DEVICE_LOGINID, 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)
|
||||
|
||||
# validations.
|
||||
# if device username was entered then device password is required.
|
||||
deviceLoginid:str = user_input.get(CONF_OPTION_DEVICE_LOGINID, None)
|
||||
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"
|
||||
if (deviceUsername is None) and (devicePassword is not None):
|
||||
errors["base"] = "device_username_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)
|
||||
|
||||
# for the last options form, we will call "async_create_entry" to update
|
||||
# the options and store them to disk.
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data=self._Options
|
||||
)
|
||||
|
||||
# log device that is currently selected.
|
||||
device_loginid:str = self._Options.get(CONF_OPTION_DEVICE_LOGINID, None)
|
||||
_logsi.LogVerbose("'%s': OptionsFlow option '%s' - value: '%s'" % (self._name, CONF_OPTION_DEVICE_LOGINID, device_loginid))
|
||||
device_username:str = self._Options.get(CONF_OPTION_DEVICE_USERNAME, None)
|
||||
_logsi.LogVerbose("'%s': OptionsFlow option '%s' - value: '%s'" % (self._name, CONF_OPTION_DEVICE_USERNAME, device_username))
|
||||
|
||||
# create validation schema.
|
||||
schema = vol.Schema(
|
||||
{
|
||||
# note - DO NOT use "default" argument on the following - use "suggested_value" instead.
|
||||
# using "default=" does not allow a null value to be selected!
|
||||
vol.Optional(CONF_OPTION_DEVICE_LOGINID,
|
||||
description={"suggested_value": self._Options.get(CONF_OPTION_DEVICE_LOGINID)},
|
||||
): cv.string,
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
# any validation errors? if so, then log them.
|
||||
if "base" in errors:
|
||||
_logsi.LogDictionary(SILevel.Warning, "'%s': OptionsFlow 02_options_player contained validation errors" % self._name, errors)
|
||||
|
||||
_logsi.LogVerbose("'%s': OptionsFlow is showing the 02_options_player configuration options form" % self._name)
|
||||
return self.async_show_form(
|
||||
step_id="02_options_player",
|
||||
data_schema=schema,
|
||||
description_placeholders={CONF_NAME: self._name},
|
||||
errors=errors or {},
|
||||
last_step=True
|
||||
)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException(None, ex, logToSystemLogger=False)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
def _GetPlayerDevicesList(self) -> list:
|
||||
"""
|
||||
Retrieves Spotify Connect device list from the Spotify Web API.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
|
||||
# get configuration instance data so we can reference the client instance.
|
||||
_logsi.LogVerbose("'%s': OptionsFlow is retrieving instance data" % self._name)
|
||||
data:InstanceDataSpotifyPlus = self.hass.data[DOMAIN].get(self._entry.entry_id, None)
|
||||
if (data) and (data.spotifyClient):
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': OptionsFlow instance data.spotifyClient" % self._name, data.spotifyClient)
|
||||
_logsi.LogObject(SILevel.Verbose, "'%s': OptionsFlow instance data.options" % self._name, data.options)
|
||||
|
||||
# get spotify connect player device list.
|
||||
_logsi.LogVerbose("'%s': OptionsFlow is retrieving Spotify Connect player devices" % self._name)
|
||||
devices:SpotifyConnectDevices = data.spotifyClient.GetSpotifyConnectDevices()
|
||||
|
||||
# build string array of all devices.
|
||||
result:list = []
|
||||
item:Device
|
||||
for item in devices.GetDeviceList():
|
||||
result.append(item.SelectItemNameAndId)
|
||||
|
||||
# trace.
|
||||
_logsi.LogArray(SILevel.Verbose, "'%s': OptionsFlow option '%s' - available values" % (self._name, CONF_OPTION_DEVICE_DEFAULT), result)
|
||||
return result
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
_logsi.LogError("'%s': OptionsFlow could not retrieve Spotify Connect player device list: %s" % (self._name, str(ex)))
|
||||
return []
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Constants for the SpotifyPlus component.
|
||||
"""
|
||||
import logging
|
||||
|
||||
DOMAIN = "spotifyplus"
|
||||
""" Domain identifier for this integration (spotifyplus). """
|
||||
|
||||
PLATFORM_SPOTIFYPLUS = DOMAIN
|
||||
""" Platform identifier for this integration (spotifyplus). """
|
||||
|
||||
DOMAIN_SCRIPT = "script"
|
||||
""" Domain identifier for script integration (script). """
|
||||
|
||||
DOMAIN_MEDIA_PLAYER = "media_player"
|
||||
""" Domain identifier for media players (media_player). """
|
||||
|
||||
SPOTIFY_WEB_URL_PFX = "https://open.spotify.com"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_OPTION_ALWAYS_ON = "always_on"
|
||||
CONF_OPTION_DEVICE_DEFAULT = "device_default"
|
||||
CONF_OPTION_DEVICE_LOGINID = "device_loginid"
|
||||
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"
|
||||
CONF_OPTION_SOURCE_LIST_HIDE = "source_list_hide"
|
||||
CONF_OPTION_SPOTIFY_SCAN_INTERVAL = "spotify_scan_interval"
|
||||
CONF_OPTION_TURN_OFF_AUTO_PAUSE = "turn_off_auto_pause"
|
||||
CONF_OPTION_TURN_ON_AUTO_RESUME = "turn_on_auto_resume"
|
||||
CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT = "turn_on_auto_source_select"
|
||||
|
||||
DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL = 30
|
||||
|
||||
# security scopes required by various Spotify Web API endpoints.
|
||||
SPOTIFY_SCOPES:list = \
|
||||
[
|
||||
'playlist-modify-private',
|
||||
'playlist-modify-public',
|
||||
'playlist-read-collaborative',
|
||||
'playlist-read-private',
|
||||
'ugc-image-upload',
|
||||
'user-follow-modify',
|
||||
'user-follow-read',
|
||||
'user-library-modify',
|
||||
'user-library-read',
|
||||
'user-modify-playback-state',
|
||||
'user-read-currently-playing',
|
||||
'user-read-email',
|
||||
'user-read-playback-position',
|
||||
'user-read-playback-state',
|
||||
'user-read-private',
|
||||
'user-read-recently-played',
|
||||
'user-top-read'
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Custom State attribute identifiers.
|
||||
# -----------------------------------------------------------------------------------
|
||||
ATTR_SPOTIFYPLUS_ARTIST_URI = "sp_artist_uri"
|
||||
ATTR_SPOTIFYPLUS_CONTEXT_URI = "sp_context_uri"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_ID = "sp_device_id"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_NAME = "sp_device_name"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_IS_BRAND_SONOS = "sp_device_is_brand_sonos"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_IS_CHROMECAST = "sp_device_is_chromecast"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_IS_RESTRICTED = "sp_device_is_restricted"
|
||||
ATTR_SPOTIFYPLUS_DEVICE_MUSIC_SOURCE = "sp_device_music_source"
|
||||
ATTR_SPOTIFYPLUS_ITEM_TYPE = "sp_item_type"
|
||||
ATTR_SPOTIFYPLUS_NOWPLAYING_IMAGE_URL = "sp_nowplaying_image_url"
|
||||
ATTR_SPOTIFYPLUS_PLAY_TIME_REMAINING_EST = "sp_play_time_remaining_est"
|
||||
ATTR_SPOTIFYPLUS_PLAYING_TYPE = "sp_playing_type"
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_NAME = "sp_playlist_name"
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_URI = "sp_playlist_uri"
|
||||
ATTR_SPOTIFYPLUS_SOURCE_LIST_HIDE = "sp_source_list_hide"
|
||||
ATTR_SPOTIFYPLUS_TRACK_IS_EXPLICIT = "sp_track_is_explicit"
|
||||
ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN = "sp_track_uri_origin"
|
||||
ATTR_SPOTIFYPLUS_USER_COUNTRY = "sp_user_country"
|
||||
ATTR_SPOTIFYPLUS_USER_DISPLAY_NAME = "sp_user_display_name"
|
||||
ATTR_SPOTIFYPLUS_USER_EMAIL = "sp_user_email"
|
||||
ATTR_SPOTIFYPLUS_USER_HAS_WEB_PLAYER_CREDENTIALS = "sp_user_has_web_player_credentials"
|
||||
ATTR_SPOTIFYPLUS_USER_ID = "sp_user_id"
|
||||
ATTR_SPOTIFYPLUS_USER_PRODUCT = "sp_user_product"
|
||||
ATTR_SPOTIFYPLUS_USER_URI = "sp_user_uri"
|
||||
ATTR_VOLUME_STEP = "volume_step"
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Custom Service identifiers.
|
||||
# -----------------------------------------------------------------------------------
|
||||
SERVICE_SPOTIFY_ADD_PLAYER_QUEUE_ITEMS = 'add_player_queue_items'
|
||||
SERVICE_SPOTIFY_CHECK_ALBUM_FAVORITES = 'check_album_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_ARTISTS_FOLLOWING = 'check_artists_following'
|
||||
SERVICE_SPOTIFY_CHECK_AUDIOBOOK_FAVORITES = 'check_audiobook_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_EPISODE_FAVORITES = 'check_episode_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_PLAYLIST_FOLLOWERS = 'check_playlist_followers'
|
||||
SERVICE_SPOTIFY_CHECK_SHOW_FAVORITES = 'check_show_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_TRACK_FAVORITES = 'check_track_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_USER_FAVORITES = 'check_user_favorites'
|
||||
SERVICE_SPOTIFY_CHECK_USERS_FOLLOWING = 'check_users_following'
|
||||
SERVICE_SPOTIFY_FOLLOW_ARTISTS = 'follow_artists'
|
||||
SERVICE_SPOTIFY_FOLLOW_PLAYLIST = 'follow_playlist'
|
||||
SERVICE_SPOTIFY_FOLLOW_USERS = 'follow_users'
|
||||
SERVICE_SPOTIFY_GET_ALBUM = 'get_album'
|
||||
SERVICE_SPOTIFY_GET_ALBUM_FAVORITES = 'get_album_favorites'
|
||||
SERVICE_SPOTIFY_GET_ALBUM_NEW_RELEASES = 'get_album_new_releases'
|
||||
SERVICE_SPOTIFY_GET_ALBUM_TRACKS = 'get_album_tracks'
|
||||
SERVICE_SPOTIFY_GET_ARTIST = 'get_artist'
|
||||
SERVICE_SPOTIFY_GET_ARTIST_ALBUMS = 'get_artist_albums'
|
||||
SERVICE_SPOTIFY_GET_ARTIST_INFO = 'get_artist_info'
|
||||
SERVICE_SPOTIFY_GET_ARTIST_RELATED_ARTISTS = 'get_artist_related_artists'
|
||||
SERVICE_SPOTIFY_GET_ARTIST_TOP_TRACKS = 'get_artist_top_tracks'
|
||||
SERVICE_SPOTIFY_GET_ARTISTS_FOLLOWED = 'get_artists_followed'
|
||||
SERVICE_SPOTIFY_GET_AUDIOBOOK = 'get_audiobook'
|
||||
SERVICE_SPOTIFY_GET_AUDIOBOOK_CHAPTERS = 'get_audiobook_chapters'
|
||||
SERVICE_SPOTIFY_GET_AUDIOBOOK_FAVORITES = 'get_audiobook_favorites'
|
||||
SERVICE_SPOTIFY_GET_BROWSE_CATEGORYS_LIST = 'get_browse_categorys_list'
|
||||
SERVICE_SPOTIFY_GET_CATEGORY_PLAYLISTS = 'get_category_playlists'
|
||||
SERVICE_SPOTIFY_GET_CHAPTER = 'get_chapter'
|
||||
SERVICE_SPOTIFY_GET_COVER_IMAGE_FILE = 'get_cover_image_file'
|
||||
SERVICE_SPOTIFY_GET_DEVICE_PLAYBACK_STATE = 'get_device_playback_state'
|
||||
SERVICE_SPOTIFY_GET_EPISODE = 'get_episode'
|
||||
SERVICE_SPOTIFY_GET_EPISODE_FAVORITES = 'get_episode_favorites'
|
||||
SERVICE_SPOTIFY_GET_FEATURED_PLAYLISTS = 'get_featured_playlists'
|
||||
SERVICE_SPOTIFY_GET_ID_FROM_URI = 'get_id_from_uri'
|
||||
SERVICE_SPOTIFY_GET_IMAGE_PALETTE_COLORS = 'get_image_palette_colors'
|
||||
SERVICE_SPOTIFY_GET_IMAGE_VIBRANT_COLORS = 'get_image_vibrant_colors'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_DEVICES = 'get_player_devices'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_LAST_PLAYED_INFO = 'get_player_last_played_info'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_NOW_PLAYING = 'get_player_now_playing'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_PLAYBACK_STATE = 'get_player_playback_state'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_QUEUE_INFO = 'get_player_queue_info'
|
||||
SERVICE_SPOTIFY_GET_PLAYER_RECENT_TRACKS = 'get_player_recent_tracks'
|
||||
SERVICE_SPOTIFY_GET_PLAYLIST = 'get_playlist'
|
||||
SERVICE_SPOTIFY_GET_PLAYLIST_COVER_IMAGE = 'get_playlist_cover_image'
|
||||
SERVICE_SPOTIFY_GET_PLAYLIST_FAVORITES = 'get_playlist_favorites'
|
||||
SERVICE_SPOTIFY_GET_PLAYLIST_ITEMS = 'get_playlist_items'
|
||||
SERVICE_SPOTIFY_GET_PLAYLISTS_FOR_USER = 'get_playlists_for_user'
|
||||
SERVICE_SPOTIFY_GET_SHOW = 'get_show'
|
||||
SERVICE_SPOTIFY_GET_SHOW_EPISODES = 'get_show_episodes'
|
||||
SERVICE_SPOTIFY_GET_SHOW_FAVORITES = 'get_show_favorites'
|
||||
SERVICE_SPOTIFY_GET_SPOTIFY_CONNECT_DEVICE = 'get_spotify_connect_device'
|
||||
SERVICE_SPOTIFY_GET_SPOTIFY_CONNECT_DEVICES = 'get_spotify_connect_devices'
|
||||
SERVICE_SPOTIFY_GET_TRACK = 'get_track'
|
||||
SERVICE_SPOTIFY_GET_TRACK_AUDIO_FEATURES = 'get_track_audio_features'
|
||||
SERVICE_SPOTIFY_GET_TRACK_FAVORITES = 'get_track_favorites'
|
||||
SERVICE_SPOTIFY_GET_TRACK_RECOMMENDATIONS = 'get_track_recommendations'
|
||||
SERVICE_SPOTIFY_GET_TRACKS_AUDIO_FEATURES = 'get_tracks_audio_features'
|
||||
SERVICE_SPOTIFY_GET_USERS_TOP_ARTISTS = 'get_users_top_artists'
|
||||
SERVICE_SPOTIFY_GET_USERS_TOP_TRACKS = 'get_users_top_tracks'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_PAUSE = 'player_media_pause'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT = 'player_media_play_context'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACK_FAVORITES = 'player_media_play_track_favorites'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS = 'player_media_play_tracks'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_RESUME = 'player_media_resume'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SEEK = 'player_media_seek'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_NEXT = 'player_media_skip_next'
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_PREVIOUS = 'player_media_skip_previous'
|
||||
SERVICE_SPOTIFY_PLAYER_SET_REPEAT_MODE = 'player_set_repeat_mode'
|
||||
SERVICE_SPOTIFY_PLAYER_SET_SHUFFLE_MODE = 'player_set_shuffle_mode'
|
||||
SERVICE_SPOTIFY_PLAYER_SET_VOLUME_LEVEL = 'player_set_volume_level'
|
||||
SERVICE_SPOTIFY_PLAYER_TRANSFER_PLAYBACK = 'player_transfer_playback'
|
||||
SERVICE_SPOTIFY_PLAYLIST_CHANGE = 'playlist_change'
|
||||
SERVICE_SPOTIFY_PLAYLIST_COVER_IMAGE_ADD = 'playlist_cover_image_add'
|
||||
SERVICE_SPOTIFY_PLAYLIST_CREATE = 'playlist_create'
|
||||
SERVICE_SPOTIFY_PLAYLIST_ITEMS_ADD = 'playlist_items_add'
|
||||
SERVICE_SPOTIFY_PLAYLIST_ITEMS_CLEAR = 'playlist_items_clear'
|
||||
SERVICE_SPOTIFY_PLAYLIST_ITEMS_REMOVE = 'playlist_items_remove'
|
||||
SERVICE_SPOTIFY_PLAYLIST_ITEMS_REORDER = 'playlist_items_reorder'
|
||||
SERVICE_SPOTIFY_PLAYLIST_ITEMS_REPLACE = 'playlist_items_replace'
|
||||
SERVICE_SPOTIFY_REMOVE_ALBUM_FAVORITES = 'remove_album_favorites'
|
||||
SERVICE_SPOTIFY_REMOVE_AUDIOBOOK_FAVORITES = 'remove_audiobook_favorites'
|
||||
SERVICE_SPOTIFY_REMOVE_EPISODE_FAVORITES = 'remove_episode_favorites'
|
||||
SERVICE_SPOTIFY_REMOVE_SHOW_FAVORITES = 'remove_show_favorites'
|
||||
SERVICE_SPOTIFY_REMOVE_TRACK_FAVORITES = 'remove_track_favorites'
|
||||
SERVICE_SPOTIFY_REMOVE_USER_FAVORITES = 'remove_user_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_ALBUM_FAVORITES = 'save_album_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_AUDIOBOOK_FAVORITES = 'save_audiobook_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_EPISODE_FAVORITES = 'save_episode_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_SHOW_FAVORITES = 'save_show_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_TRACK_FAVORITES = 'save_track_favorites'
|
||||
SERVICE_SPOTIFY_SAVE_USER_FAVORITES = 'save_user_favorites'
|
||||
SERVICE_SPOTIFY_SEARCH_ALL = 'search_all'
|
||||
SERVICE_SPOTIFY_SEARCH_ALBUMS = 'search_albums'
|
||||
SERVICE_SPOTIFY_SEARCH_ARTISTS = 'search_artists'
|
||||
SERVICE_SPOTIFY_SEARCH_AUDIOBOOKS = 'search_audiobooks'
|
||||
SERVICE_SPOTIFY_SEARCH_EPISODES = 'search_episodes'
|
||||
SERVICE_SPOTIFY_SEARCH_PLAYLISTS = 'search_playlists'
|
||||
SERVICE_SPOTIFY_SEARCH_SHOWS = 'search_shows'
|
||||
SERVICE_SPOTIFY_SEARCH_TRACKS = 'search_tracks'
|
||||
SERVICE_SPOTIFY_TRIGGER_SCAN_INTERVAL = 'trigger_scan_interval'
|
||||
SERVICE_SPOTIFY_UNFOLLOW_ARTISTS = 'unfollow_artists'
|
||||
SERVICE_SPOTIFY_UNFOLLOW_PLAYLIST = 'unfollow_playlist'
|
||||
SERVICE_SPOTIFY_UNFOLLOW_USERS = 'unfollow_users'
|
||||
SERVICE_SPOTIFY_ZEROCONF_DEVICE_CONNECT = 'zeroconf_device_connect'
|
||||
SERVICE_SPOTIFY_ZEROCONF_DEVICE_DISCONNECT = 'zeroconf_device_disconnect'
|
||||
SERVICE_SPOTIFY_ZEROCONF_DEVICE_GETINFO = 'zeroconf_device_getinfo'
|
||||
SERVICE_SPOTIFY_ZEROCONF_DISCOVER_DEVICES = 'zeroconf_discover_devices'
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Custom Service Schemas - MediaPlayerEntity enhancements.
|
||||
# -----------------------------------------------------------------------------------
|
||||
SERVICE_VOLUME_SET_STEP = 'volume_set_step'
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Custom Service Schemas - non-Spotify Web API related.
|
||||
# -----------------------------------------------------------------------------------
|
||||
SERVICE_LIST_APPLICATION_CREDENTIAL_MAPPPINGS = 'list_application_credential_mappings'
|
||||
SERVICE_TEST_TOKEN_EXPIRE = 'test_token_expire'
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Configuration constants.
|
||||
# -----------------------------------------------------------------------------------
|
||||
CONF_ADD = "add"
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_REMOVE = "remove"
|
||||
CONF_TEXT = "text"
|
||||
CONF_VALUE = "value"
|
||||
CONF_LIMIT_TOTAL_MINIMUM = 5 # ensure more than 1 so that user favorites are considered.
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Slot Argument constants.
|
||||
# -----------------------------------------------------------------------------------
|
||||
SLOT_AREA = "area"
|
||||
SLOT_ALBUM_NAME = "album_name"
|
||||
SLOT_ALBUM_TITLE = "album_title"
|
||||
SLOT_ALBUM_URL = "album_url"
|
||||
SLOT_ARTIST_BIO = "artist_bio"
|
||||
SLOT_ARTIST_NAME = "artist_name"
|
||||
SLOT_ARTIST_TITLE = "artist_title"
|
||||
SLOT_ARTIST_URL = "artist_url"
|
||||
SLOT_AUDIOBOOK_NAME = "audiobook_name"
|
||||
SLOT_AUDIOBOOK_TITLE = "audiobook_title"
|
||||
SLOT_AUDIOBOOK_URL = "audiobook_url"
|
||||
SLOT_AUTHOR_NAME = "author_name"
|
||||
SLOT_AUTHOR_TITLE = "author_title"
|
||||
SLOT_CHAPTER_NAME = "chapter_name"
|
||||
SLOT_CHAPTER_TITLE = "chapter_title"
|
||||
SLOT_CHAPTER_URL = "chapter_url"
|
||||
SLOT_CRITERIA_ARG_1 = "criteria_arg_1"
|
||||
SLOT_CRITERIA_ARG_2 = "criteria_arg_2"
|
||||
SLOT_DESCRIPTION = "description"
|
||||
SLOT_DELAY = "delay"
|
||||
SLOT_DEVICE_NAME = "device_name"
|
||||
SLOT_DEVICE_TITLE = "device_title"
|
||||
SLOT_EPISODE_NAME = "episode_name"
|
||||
SLOT_EPISODE_TITLE = "episode_title"
|
||||
SLOT_EPISODE_URL = "episode_url"
|
||||
SLOT_ERROR_FEATURES = "error_features"
|
||||
SLOT_ERROR_INFO = "error_info"
|
||||
SLOT_ERROR_STATES = "error_states"
|
||||
SLOT_FAVORITE_OPERATOR = "favorite_operator"
|
||||
SLOT_FLOOR = "floor"
|
||||
SLOT_ID = "id"
|
||||
SLOT_IMAGE_PATH = "image_path"
|
||||
SLOT_IS_COLLABORATIVE = "is_collaborative"
|
||||
SLOT_IS_PUBLIC = "is_public"
|
||||
SLOT_LATEST_EPISODE = "latest_episode"
|
||||
SLOT_LIMIT_TOTAL = "limit_total"
|
||||
SLOT_NAME = "name"
|
||||
SLOT_PLAYER_REPEAT_MODE = "player_repeat_mode"
|
||||
SLOT_PLAYER_SHUFFLE_MODE = "player_shuffle_mode"
|
||||
SLOT_PLAYER_VOLUME_LEVEL_PCT = "player_volume_level_pct"
|
||||
SLOT_PLAYER_VOLUME_STEP_PCT = "player_volume_step_pct"
|
||||
SLOT_PLAYLIST_NAME = "playlist_name"
|
||||
SLOT_PLAYLIST_TITLE = "playlist_title"
|
||||
SLOT_PLAYLIST_URL = "playlist_url"
|
||||
SLOT_PODCAST_NAME = "podcast_name"
|
||||
SLOT_PODCAST_TITLE = "podcast_title"
|
||||
SLOT_PODCAST_URL = "podcast_url"
|
||||
SLOT_PREFERRED_AREA_ID = "preferred_area_id"
|
||||
SLOT_PREFERRED_FLOOR_ID = "preferred_floor_id"
|
||||
SLOT_SEARCH_CRITERIA = "search_criteria"
|
||||
SLOT_SPOTIFYPLUS_MEDIA_TYPE = "spotifyplus_media_type"
|
||||
SLOT_SPOTIFYPLUS_PLAYLIST_NAMES = "spotifyplus_playlist_names"
|
||||
SLOT_SPOTIFYPLUS_PLAYER_DECK_CONTROL = "spotifyplus_player_deck_control"
|
||||
SLOT_SPOTIFYPLUS_PLAYER_VOLUME_CONTROL = "spotifyplus_player_volume_control"
|
||||
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL = "spotifyplus_search_play_control"
|
||||
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_MULTI = "spotifyplus_search_play_control_multi"
|
||||
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_SINGLE = "spotifyplus_search_play_control_single"
|
||||
SLOT_TARGET_PLAYER = "target_player"
|
||||
SLOT_TRACK_NAME = "track_name"
|
||||
SLOT_TRACK_TITLE = "track_title"
|
||||
SLOT_TRACK_URL = "track_url"
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Intent Handler identifiers.
|
||||
# -----------------------------------------------------------------------------------
|
||||
INTENT_FAVORITE_ADD_REMOVE = "SpotifyPlusFavoriteAddRemove"
|
||||
INTENT_GET_INFO_ARTIST_BIO = "SpotifyPlusGetInfoArtistBio"
|
||||
INTENT_GET_NOWPLAYING_INFO = "SpotifyPlusGetNowPlayingInfo"
|
||||
INTENT_PLAYER_DECK_CONTROL = "SpotifyPlusPlayerDeckControl"
|
||||
INTENT_PLAYER_SET_REPEAT_MODE = "SpotifyPlusPlayerSetRepeatMode"
|
||||
INTENT_PLAYER_SET_SHUFFLE_MODE = "SpotifyPlusPlayerSetShuffleMode"
|
||||
INTENT_PLAYER_TRANSFER_PLAYBACK = "SpotifyPlusPlayerTransferPlayback"
|
||||
INTENT_PLAYER_VOLUME_CONTROL = "SpotifyPlusPlayerVolumeControl"
|
||||
INTENT_PLAYLIST_CREATE = "SpotifyPlusPlaylistCreate"
|
||||
INTENT_SEARCH_PLAY_CONTROL = "SpotifyPlusSearchPlayControl"
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# Intent Handler response codes.
|
||||
# -----------------------------------------------------------------------------------
|
||||
RESPONSE_OK = "default"
|
||||
RESPONSE_ERROR_FAILED_TO_HANDLE = "error_failed_to_handle"
|
||||
RESPONSE_ERROR_MEDIA_TYPE_INVALID = "error_media_type_invalid"
|
||||
RESPONSE_ERROR_PLAYER_DECK_CONTROL_INVALID = "error_player_deck_control_invalid"
|
||||
RESPONSE_ERROR_PLAYER_VOLUME_CONTROL_INVALID = "error_player_volume_control_invalid"
|
||||
RESPONSE_ERROR_SEARCH_NO_CRITERIA = "error_search_no_criteria"
|
||||
RESPONSE_ERROR_SEARCH_PLAY_CONTROL_INVALID = "error_search_play_control_invalid"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_ALBUM_OK = "favorite_add_remove_album_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_ARTIST_OK = "favorite_add_remove_artist_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_AUDIOBOOK_OK = "favorite_add_remove_audiobook_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PLAYLIST_OK = "favorite_add_remove_playlist_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_OK = "favorite_add_remove_podcast_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_EPISODE_OK = "favorite_add_remove_podcast_episode_ok"
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_TRACK_OK = "favorite_add_remove_track_ok"
|
||||
RESPONSE_FAVORITE_OPERATION_INVALID = "favorite_operation_invalid"
|
||||
RESPONSE_GET_INFO_ARTIST_BIO = "get_info_artist_bio"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_ALBUM = "nowplaying_no_media_album"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_ARTIST = "nowplaying_no_media_artist"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_AUDIOBOOK = "nowplaying_no_media_audiobook"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PLAYLIST = "nowplaying_no_media_playlist"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST = "nowplaying_no_media_podcast"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST_EPISODE = "nowplaying_no_media_podcast_episode"
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_TRACK = "nowplaying_no_media_track"
|
||||
RESPONSE_NOWPLAYING_INFO_ALBUM = "nowplaying_info_album"
|
||||
RESPONSE_NOWPLAYING_INFO_AUDIOBOOK = "nowplaying_info_audiobook"
|
||||
RESPONSE_NOWPLAYING_INFO_PODCAST = "nowplaying_info_podcast"
|
||||
RESPONSE_NOWPLAYING_INFO_PODCAST_EPISODE = "nowplaying_info_podcast_episode"
|
||||
RESPONSE_NOWPLAYING_INFO_PLAYLIST = "nowplaying_info_playlist"
|
||||
RESPONSE_NOWPLAYING_INFO_TRACK = "nowplaying_info_track"
|
||||
RESPONSE_PLAY_ALBUM = "play_album"
|
||||
RESPONSE_PLAY_ALBUM_WITH_ARTIST = "play_album_with_artist"
|
||||
RESPONSE_PLAY_ARTIST = "play_artist"
|
||||
RESPONSE_PLAY_AUDIOBOOK = "play_audiobook"
|
||||
RESPONSE_PLAY_AUDIOBOOK_WITH_AUTHOR = "play_audiobook_with_author"
|
||||
RESPONSE_PLAY_FAVORITE_TRACKS = "play_favorite_tracks"
|
||||
RESPONSE_PLAY_FAVORITE_TRACKS_FOR_ARTIST = "play_favorite_tracks_for_artist"
|
||||
RESPONSE_PLAY_PLAYLIST = "play_playlist"
|
||||
RESPONSE_PLAY_PODCAST = "play_podcast"
|
||||
RESPONSE_PLAY_PODCAST_EPISODE = "play_podcast_episode"
|
||||
RESPONSE_PLAY_TRACK = "play_track"
|
||||
RESPONSE_PLAY_TRACK_WITH_ARTIST = "play_track_with_artist"
|
||||
RESPONSE_PLAYER_ALREADY_PLAYING_MEDIA = "player_already_playing_media"
|
||||
RESPONSE_PLAYER_DECK_CONTROL_PAUSE = "player_deck_control_pause"
|
||||
RESPONSE_PLAYER_DECK_CONTROL_RESUME = "player_deck_control_resume"
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SEEK_START = "player_deck_control_seek_start"
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SKIP_NEXT = "player_deck_control_skip_next"
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SKIP_PREVIOUS = "player_deck_control_skip_previous"
|
||||
RESPONSE_PLAYER_FEATURES_NOT_SUPPORTED = "player_features_not_supported"
|
||||
RESPONSE_PLAYER_NOT_EXPOSED_TO_VOICE = "player_not_exposed_to_voice"
|
||||
RESPONSE_PLAYER_NOT_MATCHED = "player_not_matched"
|
||||
RESPONSE_PLAYER_NOT_MATCHED_AREA = "player_not_matched_area"
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA = "player_not_playing_media"
|
||||
RESPONSE_PLAYER_NOT_SPOTIFYPLUS = "player_not_spotifyplus"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_DOWN = "player_volume_control_down"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_MUTE = "player_volume_control_mute"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_SET_LEVEL = "player_volume_control_set_level"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_SET_STEP_LEVEL = "player_volume_control_set_step_level"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_UNMUTE = "player_volume_control_unmute"
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_UP = "player_volume_control_up"
|
||||
RESPONSE_PLAYLIST_CREATED = "playlist_created"
|
||||
RESPONSE_SPOTIFY_NO_ARTIST_INFO = "spotify_no_artist_info"
|
||||
RESPONSE_SPOTIFY_PREMIUM_REQUIRED = "spotify_premium_required"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_ALBUM = "spotify_search_no_items_album"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_ARTIST = "spotify_search_no_items_artist"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_AUDIOBOOK = "spotify_search_no_items_audiobook"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PLAYLIST = "spotify_search_no_items_playlist"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST = "spotify_search_no_items_podcast"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST_EPISODE = "spotify_search_no_items_podcast_episode"
|
||||
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_TRACK = "spotify_search_no_items_track"
|
||||
@@ -0,0 +1,181 @@
|
||||
from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
import threading
|
||||
|
||||
from spotifywebapipython import SpotifyClient
|
||||
from spotifywebapipython.models import SpotifyConnectDevices
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntity
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_OPTION_ALWAYS_ON,
|
||||
CONF_OPTION_DEVICE_DEFAULT,
|
||||
CONF_OPTION_DEVICE_LOGINID,
|
||||
CONF_OPTION_DEVICE_PASSWORD,
|
||||
CONF_OPTION_DEVICE_USERNAME,
|
||||
CONF_OPTION_SCRIPT_TURN_OFF,
|
||||
CONF_OPTION_SCRIPT_TURN_ON,
|
||||
CONF_OPTION_SOURCE_LIST_HIDE,
|
||||
CONF_OPTION_SPOTIFY_SCAN_INTERVAL,
|
||||
CONF_OPTION_TURN_OFF_AUTO_PAUSE,
|
||||
CONF_OPTION_TURN_ON_AUTO_RESUME,
|
||||
CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT,
|
||||
DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class InstanceDataSpotifyPlus:
|
||||
"""
|
||||
SpotifyPlus instance data stored in the Home Assistant data object.
|
||||
|
||||
This contains various attributes and object instances that the integration needs
|
||||
to function. It is created in `__init__.py`, and referenced in various other
|
||||
modules.
|
||||
"""
|
||||
|
||||
media_player: MediaPlayerEntity
|
||||
"""
|
||||
The media player instance used to control media playback.
|
||||
"""
|
||||
|
||||
options: MappingProxyType[str, Any]
|
||||
"""
|
||||
Configuration entry options.
|
||||
"""
|
||||
|
||||
session: OAuth2Session
|
||||
"""
|
||||
The OAuth2 session used to communicate with the Spotify Web API.
|
||||
"""
|
||||
|
||||
spotifyClient: SpotifyClient
|
||||
"""
|
||||
The SpotifyClient instance used to interface with the Spotify Web API.
|
||||
"""
|
||||
|
||||
tokenUpdater_lock: threading.Lock
|
||||
"""
|
||||
Thread Lock object used to lock token refresh updates.
|
||||
"""
|
||||
|
||||
runtime_data: dict
|
||||
"""
|
||||
Run-time data area used to store information for the life of the instance.
|
||||
"""
|
||||
|
||||
|
||||
@property
|
||||
def OptionAlwaysOn(self) -> int:
|
||||
"""
|
||||
True if player state should never be set to OFF;
|
||||
Otherwise, False to allow turn on / off of the player.
|
||||
Defaults to False if not set.
|
||||
|
||||
If True, the player TURN_ON / TURN_OFF features are disabled will not be able
|
||||
to be called via the corresponding services.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_ALWAYS_ON, False)
|
||||
|
||||
@property
|
||||
def OptionDeviceDefault(self) -> str | None:
|
||||
"""
|
||||
The default Spotify Connect player device.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_DEVICE_DEFAULT, None)
|
||||
|
||||
@property
|
||||
def OptionDeviceLoginId(self) -> str | None:
|
||||
"""
|
||||
The default Spotify Connect loginid to use when connecting to an inactive device.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_DEVICE_LOGINID, 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 OptionSpotifyScanInterval(self) -> int:
|
||||
"""
|
||||
Scan Interval (in seconds) to use when querying Spotify Player for current playstate.
|
||||
Defaults to 30 (seconds) if not set.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_SPOTIFY_SCAN_INTERVAL, DEFAULT_OPTION_SPOTIFY_SCAN_INTERVAL)
|
||||
|
||||
@property
|
||||
def OptionScriptTurnOff(self) -> str | None:
|
||||
"""
|
||||
Script entity id that will be called to power off the device that plays media content.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_SCRIPT_TURN_OFF, None)
|
||||
|
||||
@property
|
||||
def OptionScriptTurnOn(self) -> str | None:
|
||||
"""
|
||||
Script entity id that will be called to power on the device that plays media content.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_SCRIPT_TURN_ON, None)
|
||||
|
||||
@property
|
||||
def OptionSourceListHide(self) -> list:
|
||||
"""
|
||||
The list of device names (in lower-case) to hide from the source list.
|
||||
"""
|
||||
result:list = []
|
||||
|
||||
# get option value.
|
||||
value:str = self.options.get(CONF_OPTION_SOURCE_LIST_HIDE, None)
|
||||
|
||||
# build a list from the semi-colon delimited string.
|
||||
if value is not None:
|
||||
result = value.split(';')
|
||||
for idx in range(0, len(result)):
|
||||
result[idx] = result[idx].strip().lower()
|
||||
|
||||
# return result.
|
||||
return result
|
||||
|
||||
@property
|
||||
def OptionTurnOffAutoPause(self) -> bool:
|
||||
"""
|
||||
Automatically pause (True) or not (False) the Spotify Player when the media player is turned off.
|
||||
|
||||
This option is only relevant if the player TURN_ON / TURN_OFF features are enabled.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_TURN_OFF_AUTO_PAUSE, True)
|
||||
|
||||
@property
|
||||
def OptionTurnOnAutoResume(self) -> bool:
|
||||
"""
|
||||
Automatically resume (True) or not (False) the Spotify Player when the media player is turned on.
|
||||
|
||||
This option is only relevant if the player TURN_ON / TURN_OFF features are enabled.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_TURN_ON_AUTO_RESUME, True)
|
||||
|
||||
@property
|
||||
def OptionTurnOnAutoSelectSource(self) -> bool:
|
||||
"""
|
||||
Automatically select the source device to transfer playback to (True) or not (False) when the
|
||||
media player is turned on (enabled by default).
|
||||
|
||||
If False, then source will be obtained from Spotify Player Playback state. If there is no active
|
||||
Spotify Player device, then a source will not be selected.
|
||||
|
||||
This option is only relevant if the player TURN_ON / TURN_OFF features are enabled.
|
||||
"""
|
||||
return self.options.get(CONF_OPTION_TURN_ON_AUTO_SOURCE_SELECT, True)
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Intents for the spotifyplus integration."""
|
||||
from typing import IO
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.intent import (
|
||||
IntentHandler,
|
||||
async_register as intent_async_register,
|
||||
)
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from home_assistant_intents import (
|
||||
_DATA_DIR as HA_BUILT_IN_INTENTS_DATA_DIR,
|
||||
get_intents,
|
||||
)
|
||||
|
||||
from custom_components.spotifyplus.const import PLATFORM_SPOTIFYPLUS
|
||||
|
||||
from .appmessages import STAppMessages
|
||||
from .intent_handlers import *
|
||||
from .intent_loader import IntentLoader
|
||||
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = _LOGGER
|
||||
|
||||
|
||||
def json_load(fp: IO[str]) -> JsonObjectType:
|
||||
"""Wrap json_loads for get_intents."""
|
||||
return json_loads_object(fp.read())
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""
|
||||
Set up voice assist intents.
|
||||
|
||||
Called by HA to register all intents that this integration supports.
|
||||
|
||||
Args:
|
||||
hass (HomeAssistant):
|
||||
HomeAssistant instance.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
_logsi.LogVerbose("Component async_setup_intents starting", colorValue=SIColors.Khaki)
|
||||
|
||||
# get slot list of spotifyplus media player names and any defined aliases.
|
||||
#player_name_list = await async_get_slot_list_player_name(hass)
|
||||
|
||||
# create intent loader instance.
|
||||
# in our case, we will only load OUR platform intent data.
|
||||
intentLoader:IntentLoader = IntentLoader(hass, PLATFORM_SPOTIFYPLUS)
|
||||
|
||||
# register all intents this component provides.
|
||||
register_intent_handler(hass, SpotifyPlusFavoriteAddRemove_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusGetInfoArtistBio_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusGetNowPlayingInfo_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlayerDeckControl_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlayerSetRepeatMode_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlayerSetShuffleMode_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlayerTransferPlayback_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlayerVolumeControl_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusPlaylistCreate_Handler(intentLoader))
|
||||
register_intent_handler(hass, SpotifyPlusSearchPlayControl_Handler(intentLoader))
|
||||
|
||||
# start listening for conversation.reload service call events.
|
||||
await intentLoader.async_register_cache_reload_listener()
|
||||
|
||||
# # trace.
|
||||
# # log information about built-in intents for this language variant.
|
||||
# if (_logsi.IsOn(SILevel.Verbose)):
|
||||
# try:
|
||||
# _logsi.LogVerbose("Component - loading HA built-in intents from data path: \"%s\"" % HA_BUILT_IN_INTENTS_DATA_DIR, colorValue=SIColors.Khaki)
|
||||
# language_variant = hass.config.language or "en"
|
||||
# lang_variant_intents = await hass.async_add_executor_job(
|
||||
# get_intents, language_variant, json_load
|
||||
# )
|
||||
# if lang_variant_intents:
|
||||
# _logsi.LogDictionary(SILevel.Verbose, "Component - HA built-in intents (dictionary)", lang_variant_intents, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
# else:
|
||||
# _logsi.LogVerbose("Component - HA built-in intents not found for language variant: \"%s\"" % language_variant, colorValue=SIColors.Khaki)
|
||||
# except Exception as ex:
|
||||
# # log exception, but not to system logger as HA will take care of it.
|
||||
# _logsi.LogException("Could not load HA built-in intents!", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
|
||||
# indicate success.
|
||||
_logsi.LogVerbose("Component async_setup_intents complete", colorValue=SIColors.Khaki)
|
||||
return True
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# log exception, but not to system logger as HA will take care of it.
|
||||
_logsi.LogException("Component async_setup_intents exception", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
def register_intent_handler(
|
||||
hass: HomeAssistant,
|
||||
intentObj:IntentHandler,
|
||||
) -> None:
|
||||
"""
|
||||
Registers a voice assist intent, and trace logs the object.
|
||||
"""
|
||||
# register single intent this component provides.
|
||||
_logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_REGISTER % intentObj.intent_type, intentObj, colorValue=SIColors.Khaki)
|
||||
intent_async_register(hass, intentObj)
|
||||
|
||||
|
||||
# async def async_get_slot_list_player_name(
|
||||
# hass: HomeAssistant,
|
||||
# ) -> object:
|
||||
# """
|
||||
# Builds a slot list of spotifyplus media player names and any defined aliases
|
||||
# made via the HA Voice Assist UI.
|
||||
# """
|
||||
# try:
|
||||
|
||||
# # trace.
|
||||
# _logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
# _logsi.LogVerbose("Component async_get_list_player_name starting", colorValue=SIColors.Khaki)
|
||||
|
||||
# # prepare to access the entity registry.
|
||||
# er = async_get(hass)
|
||||
|
||||
# slot_values: list[dict[str, Any]] = []
|
||||
|
||||
# # process all registry entities for spotifyplus media_player names and aliases.
|
||||
# entityObj:RegistryEntry
|
||||
# for entityObj in er.entities.values():
|
||||
|
||||
# # is this a spotifyplus media player?
|
||||
# if (entityObj.domain == DOMAIN_MEDIA_PLAYER) and (entityObj.platform == PLATFORM_SPOTIFYPLUS) and (entityObj.disabled == False):
|
||||
|
||||
# #_logsi.LogObject(SILevel.Verbose, "Entity object: %s" % entityObj.entity_id, entityObj)
|
||||
|
||||
# entity_id = entityObj.entity_id
|
||||
# friendly_name = entityObj.name or entityObj.original_name
|
||||
|
||||
# # include entity id without the underscores.
|
||||
# slot_entry = {
|
||||
# "in": entityObj.entity_id.split(".")[1].replace("_"," "),
|
||||
# "out": entity_id,
|
||||
# }
|
||||
# slot_values.append(slot_entry)
|
||||
# _logsi.LogDictionary(SILevel.Verbose, "Adding entity id without the underscores: %s = %s" % (entity_id, slot_entry["in"]), slot_entry, colorValue=SIColors.Khaki)
|
||||
|
||||
# # include primary name.
|
||||
# if friendly_name:
|
||||
# slot_entry = {
|
||||
# "in": friendly_name.lower(),
|
||||
# "out": entity_id,
|
||||
# }
|
||||
# if slot_entry not in slot_values:
|
||||
# slot_values.append(slot_entry)
|
||||
# _logsi.LogDictionary(SILevel.Verbose, "Adding entity primary name: %s = %s" % (entity_id, slot_entry["in"]), slot_entry, colorValue=SIColors.Khaki)
|
||||
|
||||
# # include aliases defined in voice assist ui.
|
||||
# if hasattr(entityObj, "aliases") and entityObj.aliases:
|
||||
# for alias in entityObj.aliases:
|
||||
# slot_entry = {
|
||||
# "in": alias.lower(),
|
||||
# "out": entity_id,
|
||||
# }
|
||||
# if slot_entry not in slot_values:
|
||||
# slot_values.append(slot_entry)
|
||||
# _logsi.LogDictionary(SILevel.Verbose, "Adding entity voice ui alias: %s = %s" % (entity_id, slot_entry["in"]), slot_entry, colorValue=SIColors.Khaki)
|
||||
|
||||
# # trace.
|
||||
# _logsi.LogArray(SILevel.Verbose, "Component async_get_list_player_name result (array)", slot_values, colorValue=SIColors.Khaki)
|
||||
|
||||
# # return value to caller.
|
||||
# return {
|
||||
# "player_name": { # slot name
|
||||
# "values": slot_values
|
||||
# }
|
||||
# }
|
||||
|
||||
# except Exception as ex:
|
||||
|
||||
# # log exception, but not to system logger as HA will take care of it.
|
||||
# _logsi.LogException("Component async_get_list_player_name exception", ex, logToSystemLogger=False)
|
||||
# # ignore exceptions
|
||||
|
||||
# finally:
|
||||
|
||||
# # trace.
|
||||
# _logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# """ Intent handlers namespace. """
|
||||
|
||||
# import all classes from the namespace.
|
||||
from .spotifyplusfavoriteaddremove_handler import SpotifyPlusFavoriteAddRemove_Handler
|
||||
from .spotifyplusgetinfoartistbio_handler import SpotifyPlusGetInfoArtistBio_Handler
|
||||
from .spotifyplusgetnowplayinginfo_handler import SpotifyPlusGetNowPlayingInfo_Handler
|
||||
from .spotifyplusplayerdeckcontrol_handler import SpotifyPlusPlayerDeckControl_Handler
|
||||
from .spotifyplusplayersetrepeatmode_handler import SpotifyPlusPlayerSetRepeatMode_Handler
|
||||
from .spotifyplusplayersetshufflemode_handler import SpotifyPlusPlayerSetShuffleMode_Handler
|
||||
from .spotifyplusplayertransferplayback_handler import SpotifyPlusPlayerTransferPlayback_Handler
|
||||
from .spotifyplusplayervolumecontrol_handler import SpotifyPlusPlayerVolumeControl_Handler
|
||||
from .spotifyplusplaylistcreate_handler import SpotifyPlusPlaylistCreate_Handler
|
||||
from .spotifyplussearchplaycontrol_handler import SpotifyPlusSearchPlayControl_Handler
|
||||
|
||||
# all classes to import when "import *" is specified.
|
||||
__all__ = [
|
||||
'SpotifyPlusFavoriteAddRemove_Handler',
|
||||
'SpotifyPlusGetInfoArtistBio_Handler',
|
||||
'SpotifyPlusGetNowPlayingInfo_Handler',
|
||||
'SpotifyPlusPlayerDeckControl_Handler',
|
||||
'SpotifyPlusPlayerSetRepeatMode_Handler',
|
||||
'SpotifyPlusPlayerSetShuffleMode_Handler',
|
||||
'SpotifyPlusPlayerTransferPlayback_Handler',
|
||||
'SpotifyPlusPlayerVolumeControl_Handler',
|
||||
'SpotifyPlusPlaylistCreate_Handler',
|
||||
'SpotifyPlusSearchPlayControl_Handler',
|
||||
]
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+760
@@ -0,0 +1,760 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_TITLE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
IntentResponseErrorCode,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from spotifywebapipython import SpotifyMediaTypes
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..utils import get_id_from_uri
|
||||
from ..const import (
|
||||
ATTR_SPOTIFYPLUS_ARTIST_URI,
|
||||
ATTR_SPOTIFYPLUS_CONTEXT_URI,
|
||||
ATTR_SPOTIFYPLUS_ITEM_TYPE,
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_NAME,
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_URI,
|
||||
ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN,
|
||||
CONF_ADD,
|
||||
CONF_REMOVE,
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
INTENT_FAVORITE_ADD_REMOVE,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_ERROR_MEDIA_TYPE_INVALID,
|
||||
RESPONSE_FAVORITE_OPERATION_INVALID,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_ALBUM_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_ARTIST_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_AUDIOBOOK_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PLAYLIST_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_EPISODE_OK,
|
||||
RESPONSE_FAVORITE_ADD_REMOVE_TRACK_OK,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_ALBUM,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_ARTIST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_AUDIOBOOK,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PLAYLIST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST_EPISODE,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_TRACK,
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
SERVICE_SPOTIFY_SAVE_ALBUM_FAVORITES,
|
||||
SERVICE_SPOTIFY_REMOVE_ALBUM_FAVORITES,
|
||||
SERVICE_SPOTIFY_FOLLOW_ARTISTS,
|
||||
SERVICE_SPOTIFY_UNFOLLOW_ARTISTS,
|
||||
SERVICE_SPOTIFY_SAVE_AUDIOBOOK_FAVORITES,
|
||||
SERVICE_SPOTIFY_REMOVE_AUDIOBOOK_FAVORITES,
|
||||
SERVICE_SPOTIFY_SAVE_EPISODE_FAVORITES,
|
||||
SERVICE_SPOTIFY_REMOVE_EPISODE_FAVORITES,
|
||||
SERVICE_SPOTIFY_FOLLOW_PLAYLIST,
|
||||
SERVICE_SPOTIFY_UNFOLLOW_PLAYLIST,
|
||||
SERVICE_SPOTIFY_SAVE_SHOW_FAVORITES,
|
||||
SERVICE_SPOTIFY_REMOVE_SHOW_FAVORITES,
|
||||
SERVICE_SPOTIFY_SAVE_TRACK_FAVORITES,
|
||||
SERVICE_SPOTIFY_REMOVE_TRACK_FAVORITES,
|
||||
SLOT_AREA,
|
||||
SLOT_ALBUM_TITLE,
|
||||
SLOT_ARTIST_TITLE,
|
||||
SLOT_ARTIST_URL,
|
||||
SLOT_AUDIOBOOK_TITLE,
|
||||
SLOT_AUDIOBOOK_URL,
|
||||
SLOT_AUTHOR_TITLE,
|
||||
SLOT_CHAPTER_TITLE,
|
||||
SLOT_EPISODE_TITLE,
|
||||
SLOT_EPISODE_URL,
|
||||
SLOT_FAVORITE_OPERATOR,
|
||||
SLOT_FLOOR,
|
||||
SLOT_IS_PUBLIC,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYLIST_TITLE,
|
||||
SLOT_PLAYLIST_URL,
|
||||
SLOT_PODCAST_TITLE,
|
||||
SLOT_PODCAST_URL,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_SPOTIFYPLUS_MEDIA_TYPE,
|
||||
SLOT_TRACK_TITLE,
|
||||
SLOT_TRACK_URL,
|
||||
SPOTIFY_WEB_URL_PFX,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusFavoriteAddRemove_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusFavoriteAddRemove.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Add / Remove the currently playing context type to / from Spotify user favorites."
|
||||
self.intent_type = INTENT_FAVORITE_ADD_REMOVE
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_FAVORITE_OPERATOR): cv.string,
|
||||
vol.Optional(SLOT_SPOTIFYPLUS_MEDIA_TYPE): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=None,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
favorite_operator:str = intentObj.slots.get(SLOT_FAVORITE_OPERATOR, {}).get(CONF_VALUE, "").lower()
|
||||
media_type:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_MEDIA_TYPE, {}).get(CONF_VALUE, "").lower()
|
||||
|
||||
# validations.
|
||||
if (favorite_operator not in [CONF_ADD, CONF_REMOVE]):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_OPERATION_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
# process based on media type.
|
||||
if (media_type == SpotifyMediaTypes.ALBUM.value):
|
||||
return await self.async_ProcessAlbum(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.ARTIST.value):
|
||||
return await self.async_ProcessArtist(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.AUDIOBOOK.value):
|
||||
return await self.async_ProcessAudiobook(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.PLAYLIST.value):
|
||||
return await self.async_ProcessPlaylist(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.async_ProcessPodcast(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.EPISODE.value):
|
||||
return await self.async_ProcessPodcastEpisode(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.TRACK.value):
|
||||
return await self.async_ProcessTrack(intentObj, intentResponse, playerEntityState, favorite_operator)
|
||||
|
||||
else:
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_MEDIA_TYPE_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
|
||||
async def async_ProcessAlbum(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_ALBUM)
|
||||
|
||||
# get now playing details.
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
album_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: "", CONF_TEXT: album_name }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SAVE_ALBUM_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_REMOVE_ALBUM_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_ALBUM_OK)
|
||||
|
||||
|
||||
async def async_ProcessArtist(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_ARTIST)
|
||||
|
||||
# get now playing details.
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_FOLLOW_ARTISTS
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_UNFOLLOW_ARTISTS
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_ARTIST_OK)
|
||||
|
||||
|
||||
async def async_ProcessAudiobook(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item an audiobook? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.AUDIOBOOK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_AUDIOBOOK)
|
||||
|
||||
# get now playing details.
|
||||
audiobook_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
audiobook_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
author_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
chapter_uri:str = playerEntityState.attributes.get(ATTR_MEDIA_CONTENT_ID)
|
||||
chapter_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
audiobook_id:str = get_id_from_uri(audiobook_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_AUDIOBOOK_TITLE] = { CONF_VALUE: audiobook_uri, CONF_TEXT: audiobook_name }
|
||||
intentObj.slots[SLOT_AUDIOBOOK_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{audiobook_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_AUTHOR_TITLE] = { CONF_VALUE: "", CONF_TEXT: author_name }
|
||||
intentObj.slots[SLOT_CHAPTER_TITLE] = { CONF_VALUE: chapter_uri, CONF_TEXT: chapter_name }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SAVE_AUDIOBOOK_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_REMOVE_AUDIOBOOK_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_AUDIOBOOK_OK)
|
||||
|
||||
|
||||
async def async_ProcessPlaylist(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# get optional arguments (if provided).
|
||||
is_public = intentObj.slots.get(SLOT_IS_PUBLIC, {}).get(CONF_VALUE, True)
|
||||
|
||||
# is now playing item a playlist (e.g. spotify:playlist:x)? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
if (item_type is None) or (item_type.find(SpotifyMediaTypes.PLAYLIST.value) == -1):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PLAYLIST)
|
||||
|
||||
# get now playing details.
|
||||
playlist_name:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_PLAYLIST_NAME)
|
||||
playlist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_PLAYLIST_URI)
|
||||
|
||||
# if playlist name is "unknown", then it's probably a Spotify generated list; if so
|
||||
# then we will indicate this to the user. Note that the favorite will be shown in
|
||||
# the Spotify Favorites with the correct name (e.g. "Daily Mix 01").
|
||||
if ((playlist_name or "").lower() == "unknown"):
|
||||
playlist_name = "Spotify Algorithmic Playlist"
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
playlist_id:str = get_id_from_uri(playlist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PLAYLIST_TITLE] = { CONF_VALUE: playlist_uri, CONF_TEXT: playlist_name }
|
||||
intentObj.slots[SLOT_PLAYLIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.PLAYLIST.value}/{playlist_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_FOLLOW_PLAYLIST
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"public": is_public
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_UNFOLLOW_PLAYLIST
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_PLAYLIST_OK)
|
||||
|
||||
|
||||
async def async_ProcessPodcast(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item an podcast? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST)
|
||||
|
||||
# get now playing details.
|
||||
podcast_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
podcast_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
podcast_id:str = get_id_from_uri(podcast_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
||||
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{podcast_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SAVE_SHOW_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_REMOVE_SHOW_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_OK)
|
||||
|
||||
|
||||
async def async_ProcessPodcastEpisode(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item an podcast? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST_EPISODE)
|
||||
|
||||
# get now playing details.
|
||||
podcast_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
podcast_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
episode_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
episode_uri:str = playerEntityState.attributes.get(ATTR_MEDIA_CONTENT_ID)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
podcast_id:str = get_id_from_uri(podcast_uri)
|
||||
episode_id:str = get_id_from_uri(episode_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
||||
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{podcast_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_name }
|
||||
intentObj.slots[SLOT_EPISODE_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.EPISODE.value}/{episode_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SAVE_EPISODE_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_REMOVE_EPISODE_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_PODCAST_EPISODE_OK)
|
||||
|
||||
|
||||
async def async_ProcessTrack(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
favorite_operator: str,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
favorite_operator (str):
|
||||
Favorite operator (e.g. add, remove).
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_TRACK)
|
||||
|
||||
# get now playing details.
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
track_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
track_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
track_id:str = get_id_from_uri(track_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_name }
|
||||
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.TRACK.value}/{track_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# add / remove the favorite.
|
||||
if (favorite_operator == CONF_ADD):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SAVE_TRACK_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
elif (favorite_operator == CONF_REMOVE):
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_REMOVE_TRACK_FAVORITES
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_FAVORITE_ADD_REMOVE_TRACK_OK)
|
||||
@@ -0,0 +1,188 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State, ServiceResponse
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..utils import get_id_from_uri
|
||||
from ..const import (
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
INTENT_GET_INFO_ARTIST_BIO,
|
||||
RESPONSE_GET_INFO_ARTIST_BIO,
|
||||
RESPONSE_SPOTIFY_NO_ARTIST_INFO,
|
||||
SERVICE_SPOTIFY_GET_ARTIST_INFO,
|
||||
SERVICE_SPOTIFY_SEARCH_ARTISTS,
|
||||
SLOT_AREA,
|
||||
SLOT_ARTIST_BIO,
|
||||
SLOT_ARTIST_NAME,
|
||||
SLOT_ARTIST_TITLE,
|
||||
SLOT_ARTIST_URL,
|
||||
SLOT_NAME,
|
||||
SLOT_FLOOR,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_SEARCH_CRITERIA,
|
||||
SPOTIFY_WEB_URL_PFX,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusGetInfoArtistBio_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusGetInfoArtistBio.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Gets Spotify artist bio information for the specified artist. Up to 400 characters of information are returned (if bio was found)."
|
||||
self.intent_type = INTENT_GET_INFO_ARTIST_BIO
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_ARTIST_BIO): cv.string,
|
||||
vol.Optional(SLOT_ARTIST_NAME): cv.string,
|
||||
vol.Optional(SLOT_ARTIST_TITLE): cv.string,
|
||||
vol.Optional(SLOT_ARTIST_URL): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=None,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
artist_name = intentObj.slots.get(SLOT_ARTIST_NAME, {}).get(CONF_VALUE, None)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: "", CONF_TEXT: artist_name }
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_SEARCH_ARTISTS
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"criteria": f"artist:{artist_name}",
|
||||
"limit_total": 1,
|
||||
"include_external": "audio"
|
||||
}
|
||||
|
||||
# update slots with search criteria.
|
||||
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
||||
|
||||
# search spotify catalog for matching artist name.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_ARTISTS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# if no matching items, then return appropriate response.
|
||||
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
||||
if (items_count == 0):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_NO_ARTIST_INFO)
|
||||
|
||||
# load returned info that we care about.
|
||||
artist_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
||||
artist_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
||||
artist_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_GET_ARTIST_INFO
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"artist_id": artist_id,
|
||||
}
|
||||
|
||||
# get artist information.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
info_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_ARTIST_INFO result", info_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# if artist info not found, then return appropriate response.
|
||||
artist_bio:str = info_result.get("result",{}).get("bio", None)
|
||||
if (artist_bio is None):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_NO_ARTIST_INFO)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_BIO] = { CONF_VALUE: "", CONF_TEXT: artist_bio }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_GET_INFO_ARTIST_BIO)
|
||||
+614
@@ -0,0 +1,614 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from hassil.intents import (
|
||||
TextSlotValue,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player.const import (
|
||||
ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_TITLE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
from homeassistant.core import State, ServiceResponse
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
IntentResponseErrorCode,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from spotifywebapipython import SpotifyMediaTypes
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader, LanguageIntents
|
||||
from ..utils import get_id_from_uri
|
||||
from ..const import (
|
||||
ATTR_SPOTIFYPLUS_ARTIST_URI,
|
||||
ATTR_SPOTIFYPLUS_CONTEXT_URI,
|
||||
ATTR_SPOTIFYPLUS_ITEM_TYPE,
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_NAME,
|
||||
ATTR_SPOTIFYPLUS_PLAYLIST_URI,
|
||||
ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN,
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
INTENT_GET_NOWPLAYING_INFO,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_ERROR_MEDIA_TYPE_INVALID,
|
||||
RESPONSE_GET_INFO_ARTIST_BIO,
|
||||
RESPONSE_NOWPLAYING_INFO_ALBUM,
|
||||
RESPONSE_NOWPLAYING_INFO_AUDIOBOOK,
|
||||
RESPONSE_NOWPLAYING_INFO_PLAYLIST,
|
||||
RESPONSE_NOWPLAYING_INFO_PODCAST,
|
||||
RESPONSE_NOWPLAYING_INFO_PODCAST_EPISODE,
|
||||
RESPONSE_NOWPLAYING_INFO_TRACK,
|
||||
RESPONSE_SPOTIFY_NO_ARTIST_INFO,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_ARTIST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_AUDIOBOOK,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PLAYLIST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST_EPISODE,
|
||||
RESPONSE_NOWPLAYING_NO_MEDIA_TRACK,
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
SERVICE_SPOTIFY_GET_ARTIST_INFO,
|
||||
SERVICE_SPOTIFY_GET_TRACK,
|
||||
SLOT_AREA,
|
||||
SLOT_ALBUM_TITLE,
|
||||
SLOT_ALBUM_URL,
|
||||
SLOT_ARTIST_BIO,
|
||||
SLOT_ARTIST_TITLE,
|
||||
SLOT_ARTIST_URL,
|
||||
SLOT_AUDIOBOOK_TITLE,
|
||||
SLOT_AUDIOBOOK_URL,
|
||||
SLOT_AUTHOR_TITLE,
|
||||
SLOT_CHAPTER_TITLE,
|
||||
SLOT_EPISODE_TITLE,
|
||||
SLOT_EPISODE_URL,
|
||||
SLOT_FLOOR,
|
||||
SLOT_SPOTIFYPLUS_PLAYLIST_NAMES,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYLIST_TITLE,
|
||||
SLOT_PLAYLIST_URL,
|
||||
SLOT_PODCAST_TITLE,
|
||||
SLOT_PODCAST_URL,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_SPOTIFYPLUS_MEDIA_TYPE,
|
||||
SLOT_TRACK_TITLE,
|
||||
SLOT_TRACK_URL,
|
||||
SPOTIFY_WEB_URL_PFX,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusGetNowPlayingInfo_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusGetNowPlayingInfo.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Queries media player state for now playing context information."
|
||||
self.intent_type = INTENT_GET_NOWPLAYING_INFO
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_SPOTIFYPLUS_MEDIA_TYPE): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=None,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
media_type:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_MEDIA_TYPE, {}).get(CONF_VALUE, "").lower()
|
||||
|
||||
# process based on media type.
|
||||
if (media_type == SpotifyMediaTypes.ALBUM.value):
|
||||
return await self.async_ProcessAlbum(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.ARTIST.value):
|
||||
return await self.async_ProcessArtist(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.AUDIOBOOK.value):
|
||||
return await self.async_ProcessAudiobook(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.PLAYLIST.value):
|
||||
return await self.async_ProcessPlaylist(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.async_ProcessPodcast(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.EPISODE.value):
|
||||
return await self.async_ProcessPodcastEpisode(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
elif (media_type == SpotifyMediaTypes.TRACK.value):
|
||||
return await self.async_ProcessTrack(intentObj, intentResponse, playerEntityState)
|
||||
|
||||
else:
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_MEDIA_TYPE_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
|
||||
async def async_ProcessAlbum(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_TRACK)
|
||||
|
||||
# get now playing details.
|
||||
album_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
track_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
track_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
track_id:str = get_id_from_uri(track_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_name }
|
||||
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.TRACK.value}/{track_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_GET_TRACK
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"track_id": track_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
info_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_TRACK result", info_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# get related details and update slot info.
|
||||
album_uri:str = info_result.get("result",{}).get("album",{}).get("uri",None)
|
||||
album_url:str = info_result.get("result",{}).get("album",{}).get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
artist_url:str = info_result.get("result",{}).get("artists",[])[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
track_url:str = info_result.get("result",{}).get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_name }
|
||||
intentObj.slots[SLOT_ALBUM_URL] = { CONF_VALUE: album_url, CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: track_url, CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_ALBUM)
|
||||
|
||||
|
||||
async def async_ProcessArtist(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_ARTIST)
|
||||
|
||||
# get now playing details.
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_GET_ARTIST_INFO
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"artist_id": artist_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
info_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_ARTIST_INFO result", info_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# get artist bio info and update slot info.
|
||||
artist_bio:str = info_result.get("result",{}).get("bio", None)
|
||||
|
||||
# if no artist info found, then we are done.
|
||||
if (artist_bio is None):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_NO_ARTIST_INFO)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_BIO] = { CONF_VALUE: "", CONF_TEXT: artist_bio }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_GET_INFO_ARTIST_BIO)
|
||||
|
||||
|
||||
async def async_ProcessAudiobook(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item an audiobook? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.AUDIOBOOK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_AUDIOBOOK)
|
||||
|
||||
# get now playing details.
|
||||
audiobook_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
audiobook_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
author_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
chapter_uri:str = playerEntityState.attributes.get(ATTR_MEDIA_CONTENT_ID)
|
||||
chapter_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
audiobook_id:str = get_id_from_uri(audiobook_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_AUDIOBOOK_TITLE] = { CONF_VALUE: audiobook_uri, CONF_TEXT: audiobook_name }
|
||||
intentObj.slots[SLOT_AUDIOBOOK_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{audiobook_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_AUTHOR_TITLE] = { CONF_VALUE: "", CONF_TEXT: author_name }
|
||||
intentObj.slots[SLOT_CHAPTER_TITLE] = { CONF_VALUE: chapter_uri, CONF_TEXT: chapter_name }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_AUDIOBOOK)
|
||||
|
||||
|
||||
async def async_ProcessPlaylist(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a playlist (e.g. spotify:playlist:x)? if not, then we are done.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
if (item_type is None) or (item_type.find(SpotifyMediaTypes.PLAYLIST.value) == -1):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PLAYLIST)
|
||||
|
||||
# get now playing details.
|
||||
playlist_name:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_PLAYLIST_NAME)
|
||||
playlist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_PLAYLIST_URI)
|
||||
|
||||
# if playlist name is "unknown", then it's probably a Spotify generated list; if so
|
||||
# then we will indicate this to the user. Note that the favorite will be shown in
|
||||
# the Spotify Favorites with the correct name (e.g. "Daily Mix 01").
|
||||
if ((playlist_name or "").lower() == "unknown"):
|
||||
|
||||
# check the "spotifyplus_playlist_name" list for a matching uri value.
|
||||
playlist_name = await self.GetTextSlotListInValue(
|
||||
intentObj,
|
||||
SLOT_SPOTIFYPLUS_PLAYLIST_NAMES,
|
||||
playlist_uri,
|
||||
"Spotify Algorithmic Playlist"
|
||||
)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
playlist_id:str = get_id_from_uri(playlist_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PLAYLIST_TITLE] = { CONF_VALUE: playlist_uri, CONF_TEXT: playlist_name }
|
||||
intentObj.slots[SLOT_PLAYLIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.PLAYLIST.value}/{playlist_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_PLAYLIST)
|
||||
|
||||
|
||||
async def async_ProcessPodcast(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a podcast episode? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST)
|
||||
|
||||
# get now playing details.
|
||||
podcast_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
podcast_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
episode_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
episode_uri:str = playerEntityState.attributes.get(ATTR_MEDIA_CONTENT_ID)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
podcast_id:str = get_id_from_uri(podcast_uri)
|
||||
episode_id:str = get_id_from_uri(episode_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
||||
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{podcast_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_name }
|
||||
intentObj.slots[SLOT_EPISODE_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.EPISODE.value}/{episode_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_PODCAST)
|
||||
|
||||
|
||||
async def async_ProcessPodcastEpisode(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a podcast episode? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.PODCAST.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_PODCAST_EPISODE)
|
||||
|
||||
# get now playing details.
|
||||
podcast_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
podcast_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_CONTEXT_URI)
|
||||
episode_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
episode_uri:str = playerEntityState.attributes.get(ATTR_MEDIA_CONTENT_ID)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
podcast_id:str = get_id_from_uri(podcast_uri)
|
||||
episode_id:str = get_id_from_uri(episode_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
||||
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{podcast_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_name }
|
||||
intentObj.slots[SLOT_EPISODE_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.EPISODE.value}/{episode_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_PODCAST_EPISODE)
|
||||
|
||||
|
||||
async def async_ProcessTrack(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
playerEntityState: State,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
playerEntityState (State):
|
||||
Target player entity state.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# is now playing item a track? if not, then don't bother.
|
||||
item_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ITEM_TYPE)
|
||||
if (item_type != SpotifyMediaTypes.TRACK.value):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_NO_MEDIA_TRACK)
|
||||
|
||||
# get now playing details.
|
||||
album_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ALBUM_NAME)
|
||||
artist_name:str = playerEntityState.attributes.get(ATTR_MEDIA_ARTIST)
|
||||
artist_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_ARTIST_URI)
|
||||
track_name:str = playerEntityState.attributes.get(ATTR_MEDIA_TITLE)
|
||||
track_uri:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_TRACK_URI_ORIGIN)
|
||||
|
||||
# get id portion of spotify uri value.
|
||||
artist_id:str = get_id_from_uri(artist_uri)
|
||||
track_id:str = get_id_from_uri(track_uri)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}", CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_name }
|
||||
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.TRACK.value}/{track_id}", CONF_TEXT: "Spotify" }
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_GET_TRACK
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"track_id": track_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
info_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_TRACK result", info_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# get related details and update slot info.
|
||||
album_uri:str = info_result.get("result",{}).get("album",{}).get("uri",None)
|
||||
album_url:str = info_result.get("result",{}).get("album",{}).get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
artist_url:str = info_result.get("result",{}).get("artists",[])[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
track_url:str = info_result.get("result",{}).get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_name }
|
||||
intentObj.slots[SLOT_ALBUM_URL] = { CONF_VALUE: album_url, CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
||||
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: track_url, CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_NOWPLAYING_INFO_TRACK)
|
||||
@@ -0,0 +1,739 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from homeassistant.components.media_player.const import MediaPlayerState
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.core import State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentError,
|
||||
IntentHandler,
|
||||
IntentResponse,
|
||||
IntentResponseErrorCode,
|
||||
IntentResponseType,
|
||||
MatchFailedReason,
|
||||
MatchTargetsConstraints,
|
||||
MatchTargetsPreferences,
|
||||
MatchTargetsResult,
|
||||
MatchFailedError,
|
||||
MatchTargetsResult,
|
||||
async_match_targets,
|
||||
)
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..const import (
|
||||
ATTR_SPOTIFYPLUS_USER_PRODUCT,
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_ERROR_FAILED_TO_HANDLE,
|
||||
RESPONSE_PLAYER_FEATURES_NOT_SUPPORTED,
|
||||
RESPONSE_PLAYER_NOT_MATCHED,
|
||||
RESPONSE_PLAYER_NOT_MATCHED_AREA,
|
||||
RESPONSE_SPOTIFY_PREMIUM_REQUIRED,
|
||||
SLOT_AREA,
|
||||
SLOT_ERROR_FEATURES,
|
||||
SLOT_ERROR_INFO,
|
||||
SLOT_ERROR_STATES,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_TARGET_PLAYER,
|
||||
)
|
||||
from ..intent_loader import (
|
||||
LanguageIntents,
|
||||
IntentLoader,
|
||||
)
|
||||
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = _LOGGER
|
||||
|
||||
|
||||
class SpotifyPlusIntentHandler(IntentHandler):
|
||||
"""
|
||||
Base class that handles intents for the SpotifyPlus integration.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
intentLoader:IntentLoader,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes a new instance of the class.
|
||||
|
||||
Args:
|
||||
intentLoader (IntentLoader):
|
||||
A IntentLoader instance that loads our platform intents from custom_sentences.
|
||||
"""
|
||||
# set trace reference.
|
||||
self.logsi = _logsi
|
||||
|
||||
# store intent loader reference.
|
||||
self._IntentLoader = intentLoader
|
||||
|
||||
# set intent handler basics.
|
||||
# these should be overridden in the inheriting class, but are here for validation.
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
self.intent_type = "INTENT_TYPE_NOT_SET_IN_INHERITING_CLASS"
|
||||
self.description = "This description should be overridden in the inheriting class!"
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Subclasses must implement this method to handle the intent.
|
||||
|
||||
This method is called from the `async_handle` method, and is wrapped in a `try...except`
|
||||
block to automatically capture exceptions and return an error response.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse):
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
async def async_handle(
|
||||
self,
|
||||
intentObj:Intent
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# create intent response object.
|
||||
intentResponse = intentObj.create_response()
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, intentObj.intent_type, colorValue=SIColors.Khaki)
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_INTENT_HANDLE_REQUEST % intentObj.intent_type, colorValue=SIColors.Khaki)
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLE_REQUEST_PARMS % intentObj.intent_type, intentObj, colorValue=SIColors.Khaki)
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLE_REQUEST_SLOTS % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# call internal method to handle the intent, and return the response.
|
||||
return await self.async_HandleIntent(intentObj, intentResponse)
|
||||
|
||||
except IntentError as ex:
|
||||
|
||||
# anything that inherits from IntentError: MatchFailedError, NoStatesMatchedError, etc.
|
||||
|
||||
# trace.
|
||||
self.logsi.LogException(STAppMessages.MSG_INTENT_HANDLER_EXCEPTION % (intentObj.intent_type, str(ex)), ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# determine type of exception.
|
||||
# if HA exception, then do not log to the system logger since HA has already done that.
|
||||
logToSystemLogger = True
|
||||
if (isinstance(ex, HomeAssistantError)):
|
||||
logToSystemLogger = False
|
||||
|
||||
# trace.
|
||||
self.logsi.LogException(STAppMessages.MSG_INTENT_HANDLER_EXCEPTION % (intentObj.intent_type, str(ex)), ex, logToSystemLogger=logToSystemLogger, colorValue=SIColors.Khaki)
|
||||
|
||||
# update slot error details.
|
||||
intentObj.slots[SLOT_ERROR_INFO] = { CONF_VALUE: RESPONSE_ERROR_FAILED_TO_HANDLE, CONF_TEXT: str(ex) }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_FAILED_TO_HANDLE, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, intentObj.intent_type, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_GetMatchingPlayerState(
|
||||
self,
|
||||
intentObj:Intent,
|
||||
intentResponse:IntentResponse,
|
||||
desiredFeatures:MediaPlayerEntityFeature=None,
|
||||
desiredStates:list[MediaPlayerState]=None,
|
||||
desiredStateResponseKey:str=None,
|
||||
requiresSpotifyPremium:bool=False,
|
||||
) -> State | None:
|
||||
"""
|
||||
Get matching player entity state, if one exists.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse):
|
||||
Intent response object that will be returned if an error occurs.
|
||||
desiredFeatures (MediaPlayerEntityFeature):
|
||||
Media player Features that are required for the match (e.g. MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE).
|
||||
desiredStates (list[MediaPlayerState]):
|
||||
A list of media player states that are required for the match (e.g. [STATE_PLAYING, STATE_PAUSED]).
|
||||
desiredStateResponseKey (str):
|
||||
Response message key that will be loaded and sent if the `desiredFeatures` are not supported.
|
||||
requiresSpotifyPremium (bool):
|
||||
If true, a check will be made to ensure the user is a Spotify premium member and raise an exception if not.
|
||||
|
||||
Returns:
|
||||
The resolved SpotifyPlus media player entity state if one exists; otherwise, None.
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
methodParms.AppendKeyValue("intent_type", intentObj.intent_type)
|
||||
methodParms.AppendKeyValue("language", intentObj.language)
|
||||
methodParms.AppendKeyValue("desiredFeatures", desiredFeatures)
|
||||
methodParms.AppendKeyValue("desiredStates", desiredStates)
|
||||
methodParms.AppendKeyValue("desiredStateResponseKey", desiredStateResponseKey)
|
||||
methodParms.AppendKeyValue("requiresSpotifyPremium", requiresSpotifyPremium)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "Resolving matching player state for intent: \"%s\"" % (intentObj.intent_type), methodParms, colorValue=SIColors.Khaki)
|
||||
|
||||
# validate slot arguments.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "Validating slot arguments for intent: \"%s\"" % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
slots = self.async_validate_slots(intentObj.slots)
|
||||
|
||||
# get player name / area / floor slot arguments.
|
||||
# note that HA conversation agent requires specific slot id's to perform it's magic
|
||||
# when resolving entity_id's related to the following: "name", "floor", "area".
|
||||
# if you customize outside of those values then entity matching will not work; you
|
||||
# have to define custom lists in order for things to match!
|
||||
player_name: str | None = slots.get(SLOT_NAME, {}).get(CONF_VALUE, "")
|
||||
area_id = slots.get(SLOT_AREA, {}).get(CONF_VALUE, "")
|
||||
floor_id = slots.get(SLOT_FLOOR, {}).get(CONF_VALUE, "")
|
||||
|
||||
# update target player slot in case we have any errors.
|
||||
slots[SLOT_TARGET_PLAYER] = {
|
||||
CONF_VALUE: "unknown",
|
||||
CONF_TEXT: player_name + area_id + floor_id, # only 1 should be populated, others are empty strings.
|
||||
}
|
||||
|
||||
# build matching entities criteria.
|
||||
matchConstraints = MatchTargetsConstraints(
|
||||
name=player_name,
|
||||
area_name=area_id,
|
||||
floor_name=floor_id,
|
||||
domains={DOMAIN_MEDIA_PLAYER},
|
||||
assistant=intentObj.assistant,
|
||||
#features=desiredFeatures,
|
||||
single_target=True,
|
||||
)
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_MATCH_CONSTRAINTS_REQ % intentObj.intent_type, matchConstraints, colorValue=SIColors.Khaki)
|
||||
|
||||
# find matching entities; this will try to match a media player entity
|
||||
# to the desired spoken player name / area / floor value.
|
||||
# it seems to match on friendly name, alias(es), and exact entity id.
|
||||
matchResult:MatchTargetsResult = async_match_targets(
|
||||
intentObj.hass,
|
||||
matchConstraints,
|
||||
MatchTargetsPreferences(
|
||||
area_id=slots.get(SLOT_PREFERRED_AREA_ID, {}).get(CONF_VALUE),
|
||||
floor_id=slots.get(SLOT_PREFERRED_FLOOR_ID, {}).get(CONF_VALUE),
|
||||
),
|
||||
)
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_MATCH_CONSTRAINTS_RSLT % (intentObj.intent_type, str(matchResult.is_match), str(matchResult.no_match_reason), str(matchResult.no_match_name)), matchResult, colorValue=SIColors.Khaki)
|
||||
|
||||
playerEntityState:State = None
|
||||
playerEntity:RegistryEntry = None
|
||||
resolvedDesc:str = None
|
||||
|
||||
# did we find a matching entity?
|
||||
if matchResult.is_match:
|
||||
|
||||
# yes - let's verify the matched entity is an active spotifyplus media player.
|
||||
# the HA matching engine is not great at matching by platform!
|
||||
playerEntityState = matchResult.states[0]
|
||||
playerEntity = get_registry_entry_media_player(intentObj, platform=PLATFORM_SPOTIFYPLUS, entity_id=playerEntityState.entity_id)
|
||||
resolvedDesc = "MatchTargetsResult"
|
||||
|
||||
else:
|
||||
|
||||
# determine why contraints were not matched.
|
||||
if matchResult.no_match_reason == MatchFailedReason.AREA:
|
||||
|
||||
# media player entity not found for specified area.
|
||||
#raise MatchFailedError(result=matchResult, constraints=matchConstraints)
|
||||
intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_NOT_MATCHED_AREA, IntentResponseErrorCode.NO_VALID_TARGETS)
|
||||
return None
|
||||
|
||||
elif matchResult.no_match_reason == MatchFailedReason.ASSISTANT:
|
||||
|
||||
# media player entity has not been exposed to HA Voice Assist.
|
||||
# raise HA MatchFailedError, which will cause the `conversation.default_agent` logic
|
||||
# to read `response: -> errors: -> no_x_exposed:` key message.
|
||||
raise MatchFailedError(result=matchResult, constraints=matchConstraints)
|
||||
|
||||
# media player entity has not been exposed to HA Voice Assist.
|
||||
# intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_NOT_EXPOSED_TO_VOICE, IntentResponseErrorCode.NO_VALID_TARGETS)
|
||||
# return None
|
||||
|
||||
elif matchResult.no_match_reason == MatchFailedReason.MULTIPLE_TARGETS:
|
||||
|
||||
# if multiple targets matched, then loop through them to find the
|
||||
# first SpotifyPlus platform.
|
||||
for stateEntry in matchResult.states:
|
||||
playerEntity = get_registry_entry_media_player(intentObj, platform=PLATFORM_SPOTIFYPLUS, entity_id=stateEntry.entity_id)
|
||||
if (playerEntity):
|
||||
playerEntityState = stateEntry
|
||||
resolvedDesc = "MatchTargetsResult (First of Multiple)"
|
||||
break
|
||||
|
||||
else:
|
||||
|
||||
# no - search for the first active spotifyplus media player entity.
|
||||
playerEntity = get_registry_entry_media_player(intentObj, platform=PLATFORM_SPOTIFYPLUS)
|
||||
if (playerEntity):
|
||||
playerEntityState = intentObj.hass.states.get(playerEntity.entity_id)
|
||||
resolvedDesc = "RegistryEntry"
|
||||
|
||||
# if player not found, then give up and inform the user.
|
||||
if (playerEntity is None):
|
||||
intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_NOT_MATCHED, IntentResponseErrorCode.NO_VALID_TARGETS)
|
||||
return None
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogObject(SILevel.Verbose, "Resolved player state for entity_id: \"%s\" (%s)" % (playerEntityState.entity_id, resolvedDesc), playerEntityState, colorValue=SIColors.Khaki)
|
||||
|
||||
# update target player slot that contains the selected player info.
|
||||
# note we will use the friendly name / area / floor value supplied,
|
||||
# and just update the entity_id value.
|
||||
slots[SLOT_TARGET_PLAYER] = {
|
||||
CONF_VALUE: playerEntityState.entity_id,
|
||||
CONF_TEXT: player_name + area_id + floor_id, # only 1 should be populated, others are empty strings.
|
||||
}
|
||||
|
||||
# update slots with target media player info.
|
||||
intentObj.slots.update(slots)
|
||||
intentResponse.speech_slots = slots
|
||||
|
||||
# is spotify premium account required for this function?
|
||||
# we check this BEFORE the features supported check, otherwise it would
|
||||
# always hit the features not supported since free accounts don't support
|
||||
# player features. it's a more meaningful message to say that the "spotify
|
||||
# premium is required for this" versus "feature x not supported".
|
||||
if (requiresSpotifyPremium):
|
||||
account_type:str = playerEntityState.attributes.get(ATTR_SPOTIFYPLUS_USER_PRODUCT)
|
||||
if ((account_type or "").lower().find("premium") == -1):
|
||||
intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_PREMIUM_REQUIRED, IntentResponseErrorCode.NO_VALID_TARGETS)
|
||||
return None
|
||||
|
||||
# do we need to check for desired features?
|
||||
if (desiredFeatures is not None):
|
||||
|
||||
# are all desired features supported for this player? if not, then we are done.
|
||||
if (playerEntity.supported_features & desiredFeatures) != desiredFeatures:
|
||||
|
||||
# extract and format the names of desired features.
|
||||
featureNames = ", ".join(
|
||||
feature.name.replace("_", " ").lower()
|
||||
for feature in MediaPlayerEntityFeature
|
||||
if feature & desiredFeatures
|
||||
)
|
||||
|
||||
# media player does not support requested features.
|
||||
intentObj.slots[SLOT_ERROR_FEATURES] = { CONF_VALUE: RESPONSE_PLAYER_FEATURES_NOT_SUPPORTED, CONF_TEXT: featureNames }
|
||||
intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_FEATURES_NOT_SUPPORTED, IntentResponseErrorCode.NO_VALID_TARGETS)
|
||||
return None
|
||||
|
||||
# do we need to check player state?
|
||||
if (desiredStates is not None):
|
||||
|
||||
# is media player state in the desired state (e.g. playing? paused? etc)?
|
||||
if (playerEntityState.state not in desiredStates):
|
||||
intentObj.slots[SLOT_ERROR_STATES] = { CONF_VALUE: desiredStateResponseKey, CONF_TEXT: ", ".join(lbl for lbl in desiredStates).lower() }
|
||||
intentResponse = await self.ReturnResponseByKey(intentObj, intentResponse, desiredStateResponseKey, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
return None
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# return response and the found player entity.
|
||||
return playerEntityState
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def ReturnResponseByKey(
|
||||
self,
|
||||
intentObj:Intent,
|
||||
intentResponse:IntentResponse,
|
||||
responseKey:str,
|
||||
responseErrorCode:IntentResponseErrorCode=None,
|
||||
) -> IntentResponse | None:
|
||||
"""
|
||||
Look up a response template in `custom_sentences/<language>/*.yaml` files by a key value,
|
||||
render it (with template support), and sets the intent response to return the message.
|
||||
|
||||
If the `responseErrorCode` argument is specified, the intent response is updated to return
|
||||
a response type of error, and the error code set with the `responseErrorCode` value.
|
||||
|
||||
In the HA Companion App Assist, the message background is red if a `responseErrorCode`
|
||||
argument value is passed; otherwise, the background is black.
|
||||
|
||||
Args:
|
||||
intentObj (Intent|None):
|
||||
Intent object that is handling the request.
|
||||
intentResponse (IntentResponse):
|
||||
Intent response object.
|
||||
responseKey (str):
|
||||
Intent Response key to find; this value is case-sensitive.
|
||||
responseErrorCode (IntentResponseErrorCode)
|
||||
The IntentResponseErrorCode value to use for the response error code;
|
||||
defaults to `FAILED_TO_HANDLE` if not set.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object with the response.
|
||||
"""
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogDictionary(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_SLOT_INFO % intentObj.intent_type, intentObj.slots, colorValue=SIColors.Khaki)
|
||||
|
||||
# if response error code set, then treat it as an error.
|
||||
if (responseErrorCode is not None):
|
||||
intentResponse.response_type = IntentResponseType.ERROR
|
||||
intentResponse.error_code = responseErrorCode
|
||||
|
||||
# get the response code message text, and update the intent response.
|
||||
responseText = await self.GetIntentResponseByKey(intentObj, responseKey)
|
||||
intentResponse.async_set_speech(responseText)
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_RESPONSE % (intentObj.intent_type), intentResponse, colorValue=SIColors.Khaki)
|
||||
|
||||
# return response.
|
||||
return intentResponse
|
||||
|
||||
|
||||
async def GetIntentResponseByKey(
|
||||
self,
|
||||
intentObj:Intent,
|
||||
responseKey:str,
|
||||
) -> str | None:
|
||||
"""
|
||||
Look up a response template in `custom_sentences/<language>/*.yaml` files by a key value
|
||||
and render it (with template support).
|
||||
|
||||
Args:
|
||||
intentObj (Intent|None):
|
||||
Intent object that is handling the request.
|
||||
responseKey (str):
|
||||
Intent Response key to find; this value is case-sensitive.
|
||||
|
||||
Returns:
|
||||
A rendered template string for the response key if found; otherwise, a default English
|
||||
message stating that the response key could not be found.
|
||||
|
||||
Response key templates may be nested using the following schemas.
|
||||
This is also the search prevalence heirarchy, in that the first layout that contains
|
||||
the response key will be the value that is used.
|
||||
- intent-specific: `responses -> intents -> MyIntentName -> my_message_key: "My message text"`
|
||||
- platform-specific: `responses -> MyPlatform -> my_message_key: "My message text"`
|
||||
- generic-response: `responses -> my_message_key: "My message text"`
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
# default result if response key could not be resolved.
|
||||
result:str = "Resource message for response key \"%s\" could not be found." % responseKey
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
methodParms.AppendKeyValue("responseKey", responseKey)
|
||||
methodParms.AppendKeyValue("intent_type", intentObj.intent_type)
|
||||
methodParms.AppendKeyValue("language", intentObj.language)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "Loading response text for response key: \"%s\" (language=%s)" % (responseKey, intentObj.language), methodParms, colorValue=SIColors.Khaki)
|
||||
|
||||
# validations.
|
||||
language = intentObj.language
|
||||
intent_type = intentObj.intent_type
|
||||
|
||||
# get intent data; if none found, then return default message.
|
||||
langIntents:LanguageIntents = await self._IntentLoader.async_get_or_load_intents(language)
|
||||
if (langIntents is None):
|
||||
return result
|
||||
|
||||
# search for intent response key using the following search heirarchy:
|
||||
# - intent-response: `responses -> intents -> MyIntentName -> my_message_key: "My message text"`
|
||||
# - platform-response: `responses -> MyPlatform -> my_message_key: "My message text"`
|
||||
# - error-response: `responses -> errors -> my_message_key: "My message text"`
|
||||
# - generic-response: `responses -> my_message_key: "My message text"`
|
||||
|
||||
# create searchable dictionarys of response data.
|
||||
intents_block = langIntents.intent_responses or {}
|
||||
platform_block = langIntents.platform_responses or {}
|
||||
error_block = langIntents.error_responses or {}
|
||||
generic_block = langIntents.generic_responses or {}
|
||||
|
||||
# check for response key in each response data dictionary.
|
||||
candidates = []
|
||||
|
||||
# check for response key message at the intent level (intent-response).
|
||||
if intent_type in intents_block:
|
||||
intent_dict = intents_block[intent_type] or {}
|
||||
if responseKey in intent_dict:
|
||||
_logsi.LogDebug("Found candidate for response key (intent-response) \"%s\": \"%s\"" % (responseKey, intent_dict[responseKey]), colorValue=SIColors.Khaki)
|
||||
candidates.append(intent_dict[responseKey])
|
||||
|
||||
# check for response key message by platform (platform-response).
|
||||
if responseKey in platform_block:
|
||||
_logsi.LogDebug("Found candidate for response key (platform-response) \"%s\": \"%s\"" % (responseKey, platform_block[responseKey]), colorValue=SIColors.Khaki)
|
||||
candidates.append(platform_block[responseKey])
|
||||
|
||||
# check for response key message in response errors (error-response):
|
||||
if responseKey in error_block:
|
||||
_logsi.LogDebug("Found candidate for response key (error-response) \"%s\": \"%s\"" % (responseKey, error_block[responseKey]), colorValue=SIColors.Khaki)
|
||||
candidates.append(error_block[responseKey])
|
||||
|
||||
# check for response key message by simple lookup under responses (generic-response):
|
||||
if responseKey in generic_block and isinstance(generic_block[responseKey], str):
|
||||
_logsi.LogDebug("Found candidate for response key (generic-response) \"%s\": \"%s\"" % (responseKey, generic_block[responseKey]), colorValue=SIColors.Khaki)
|
||||
candidates.append(generic_block[responseKey])
|
||||
|
||||
# trace.
|
||||
_logsi.LogDictionary(SILevel.Verbose,"Responses candidates dictionary", candidates, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# did we find any matching candidates?
|
||||
if candidates:
|
||||
|
||||
# render the first candidate entry found.
|
||||
template_text = candidates[0]
|
||||
|
||||
# just in case there are exceptions processing the template.
|
||||
# e.g. "dict object' has no attribute 'name'" <- slot reference error.
|
||||
try:
|
||||
|
||||
# use Home Assistant Template helper to render, with access to hass template functions.
|
||||
# provide `slots` to the template context as well (like intent scripts do).
|
||||
tpl = Template(template_text, intentObj.hass)
|
||||
rendered = tpl.async_render({"slots": intentObj.slots}, parse_result=False)
|
||||
result = rendered
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("Intent handler GetIntentResponseByKey template render exception: %s" % (str(ex)), ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
|
||||
# ignore template render exceptions.
|
||||
# we will use the resource message as-is, and let the user figure it out.
|
||||
# HA will take care of logging the exception to the system log.
|
||||
result = template_text
|
||||
|
||||
# return result text.
|
||||
_logsi.LogText(SILevel.Verbose,"Response text for response key \"%s\": \"%s\"" % (responseKey, result), result, colorValue=SIColors.Khaki)
|
||||
return result
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("Intent handler GetIntentResponseByKey exception: %s" % (str(ex)), ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
|
||||
# ignore exceptions
|
||||
return "Could not find intent resource message for response key \"%s\" (language=\"%s\", platform=\"%s\")." % (responseKey, intentObj.language, self._IntentLoader._Platform)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def GetTextSlotListInValue(
|
||||
self,
|
||||
intentObj:Intent,
|
||||
listName:str,
|
||||
outValue:str,
|
||||
defaultValue:str=None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Queries a TextSlotList of values for the specified "out" value, returning its
|
||||
corresponding "in" value if found.
|
||||
|
||||
Args:
|
||||
intentObj (Intent|None):
|
||||
Intent object that is handling the request.
|
||||
listName (str):
|
||||
Intent list name to query; must be a TextSlotList of "in" / "out" values.
|
||||
outValue (str):
|
||||
TextSlotList "out" value to find.
|
||||
defaultValue (str):
|
||||
Default value to return if TextSlotList "out" value could not be found.
|
||||
|
||||
Returns:
|
||||
A TextSlotList "in" value assigned to the found "out" value if found; otherwise, None.
|
||||
|
||||
This method does NOT include the built-in intent "lists", only the lists that
|
||||
were loaded by this integration!
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
result:str = defaultValue
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
methodParms.AppendKeyValue("listName", listName)
|
||||
methodParms.AppendKeyValue("outValue", outValue)
|
||||
methodParms.AppendKeyValue("defaultValue", defaultValue)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "Querying TextSlotList IN value for OUT value key: \"%s\"" % (outValue), methodParms, colorValue=SIColors.Khaki)
|
||||
|
||||
# validations.
|
||||
if (not isinstance(intentObj, Intent)):
|
||||
return None
|
||||
if (not isinstance(listName, str)):
|
||||
return None
|
||||
if (not isinstance(outValue, str)):
|
||||
return None
|
||||
|
||||
language = intentObj.language
|
||||
|
||||
# get intent data; if none found, then we are done.
|
||||
langIntents:LanguageIntents = await self._IntentLoader.async_get_or_load_intents(language)
|
||||
if (langIntents is None):
|
||||
return None
|
||||
|
||||
# example "lists" dictionary for "spotifyplus_playlist_names" list:
|
||||
# 'lists': {
|
||||
# 'spotifyplus_playlist_names': {
|
||||
# 'values': [{
|
||||
# 'in': 'Daily Mix (1|One)',
|
||||
# 'out': 'spotify:playlist:37i9dQZF1E39vTG3GurFPW'
|
||||
# },
|
||||
# {
|
||||
# 'in': 'Daily Mix (2|Two)',
|
||||
# 'out': 'spotify:playlist:37i874jngdjhg8577kjjss'
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
|
||||
# query the global "lists" dictionary for the specified list name, returning
|
||||
# it's underlying list of values.
|
||||
slotListValuesArray:list = langIntents.intents_dict.get("lists",{}).get(listName,{}).get("values",[])
|
||||
|
||||
# trace.
|
||||
if (self.logsi.IsOn(SILevel.Verbose)):
|
||||
self.logsi.LogArray(SILevel.Verbose, "TextSlotList list of values: \"%s\"" % (listName), slotListValuesArray, colorValue=SIColors.Khaki)
|
||||
|
||||
# prepare for comparison.
|
||||
outValueLower = outValue.lower()
|
||||
|
||||
# check for a matching "out" key value.
|
||||
# if found, then return the "in" value.
|
||||
slotValueDict:dict = None
|
||||
for slotValueDict in slotListValuesArray:
|
||||
outValue = slotValueDict.get("out", "")
|
||||
if (outValue.lower() == outValueLower):
|
||||
result = slotValueDict.get("in", None)
|
||||
self.logsi.LogVerbose("Matched TextSlotList OUT value \"%s\" - IN value: \"%s\"" % (outValue, result), colorValue=SIColors.Khaki)
|
||||
break
|
||||
|
||||
# return result to caller.
|
||||
return result
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("Intent handler GetTextSlotListInValue exception: %s" % (str(ex)), ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
|
||||
# ignore exceptions.
|
||||
return result
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
def get_registry_entry_media_player(
|
||||
intentObj:Intent,
|
||||
platform:str=None,
|
||||
entity_id:str=None,
|
||||
) -> RegistryEntry | None:
|
||||
"""
|
||||
Retrieves a registry entry for the specified media player criteria.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent instance that is calling the method.
|
||||
platform (str):
|
||||
Platform name of the media player entity to retrieve (e.g.
|
||||
"spotifyplus", "spotify", etc).
|
||||
entity_id (str):
|
||||
Entity id to retrieve
|
||||
|
||||
Returns:
|
||||
A `RegistryEntry` object found in the HA entity registry.
|
||||
"""
|
||||
# prepare to access the entity registry.
|
||||
er_registry = er.async_get(intentObj.hass)
|
||||
result:RegistryEntry = None
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Searching HA entity registry for active media player platform \"%s\", entity id: \"%s\"" % (platform, entity_id or "*any*"), colorValue=SIColors.Khaki)
|
||||
|
||||
# was a specific entity supplied?
|
||||
if (entity_id):
|
||||
|
||||
# yes - get the value directly.
|
||||
entityObj = er_registry.async_get(entity_id)
|
||||
if (entityObj):
|
||||
if (entityObj.domain == DOMAIN_MEDIA_PLAYER) and (entityObj.platform == platform) and (entityObj.disabled == False):
|
||||
result = entityObj
|
||||
|
||||
if (result is None):
|
||||
_logsi.LogVerbose("No active HA entity registry entry found for media player platform \"%s\", entity id: \"%s\"" % (platform, entity_id), colorValue=SIColors.Khaki)
|
||||
#No active HA entity registry entry found for "spotifyplus" media player entity id: "media_player.sonos_01"
|
||||
|
||||
return result
|
||||
|
||||
else:
|
||||
|
||||
# no - get all active media player entities for the supplied platform.
|
||||
entities = [
|
||||
entityObj for entityObj in er_registry.entities.values()
|
||||
if (entityObj.domain == DOMAIN_MEDIA_PLAYER) and (entityObj.platform == platform) and (entityObj.disabled == False)
|
||||
]
|
||||
if entities:
|
||||
result = entities[0]
|
||||
if (result is None):
|
||||
_logsi.LogObject("No active HA entity registry entries were found for media player platform \"%s\"" % (platform), colorValue=SIColors.Khaki)
|
||||
return result
|
||||
|
||||
# trace.
|
||||
_logsi.LogObject(SILevel.Verbose, "Found HA entity registry for active media player platform \"%s\", entity id: \"%s\"" % (platform, result.entity_id), result, colorValue=SIColors.Khaki)
|
||||
|
||||
# return to caller.
|
||||
return result
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.const import (
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
IntentResponseErrorCode,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..const import (
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
INTENT_PLAYER_DECK_CONTROL,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_ERROR_PLAYER_DECK_CONTROL_INVALID,
|
||||
RESPONSE_PLAYER_ALREADY_PLAYING_MEDIA,
|
||||
RESPONSE_PLAYER_DECK_CONTROL_PAUSE,
|
||||
RESPONSE_PLAYER_DECK_CONTROL_RESUME,
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SEEK_START,
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SKIP_NEXT,
|
||||
RESPONSE_PLAYER_DECK_CONTROL_SKIP_PREVIOUS,
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_PAUSE,
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_RESUME,
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SEEK,
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_NEXT,
|
||||
SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_PREVIOUS,
|
||||
SLOT_AREA,
|
||||
SLOT_DELAY,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_SPOTIFYPLUS_PLAYER_DECK_CONTROL,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlayerDeckControl_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlayerDeckControl.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Controls media player deck functions (pause, resume, next track, previous track, restart track, etc)."
|
||||
self.intent_type = INTENT_PLAYER_DECK_CONTROL
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_SPOTIFYPLUS_PLAYER_DECK_CONTROL): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# get optional arguments (if provided).
|
||||
player_deck_control:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_PLAYER_DECK_CONTROL, {}).get(CONF_VALUE, "").lower()
|
||||
|
||||
# process based on media type.
|
||||
if (player_deck_control == "pause"):
|
||||
return await self.async_ProcessPause(intentObj, intentResponse)
|
||||
|
||||
elif (player_deck_control == "resume"):
|
||||
return await self.async_ProcessResume(intentObj, intentResponse)
|
||||
|
||||
elif (player_deck_control == "seek_start"):
|
||||
return await self.async_ProcessSeekStart(intentObj, intentResponse)
|
||||
|
||||
elif (player_deck_control == "skip_previous"):
|
||||
return await self.async_ProcessSkipPrevious(intentObj, intentResponse)
|
||||
|
||||
elif (player_deck_control == "skip_next"):
|
||||
return await self.async_ProcessSkipNext(intentObj, intentResponse)
|
||||
|
||||
else:
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_PLAYER_DECK_CONTROL_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
|
||||
async def async_ProcessPause(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PAUSE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_DECK_CONTROL_PAUSE)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessResume(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# if already playing, then there is nothing to do.
|
||||
if (playerEntityState.state == STATE_PLAYING):
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_ALREADY_PLAYING_MEDIA)
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_RESUME
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_DECK_CONTROL_RESUME)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessSeekStart(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_SEEK
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"position_ms": 0, # restart track
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_DECK_CONTROL_SEEK_START)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessSkipNext(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_NEXT
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_DECK_CONTROL_SKIP_NEXT)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessSkipPrevious(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.PREVIOUS_TRACK | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_SKIP_PREVIOUS
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_DECK_CONTROL_SKIP_PREVIOUS)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..const import (
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
INTENT_PLAYER_SET_REPEAT_MODE,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
SERVICE_SPOTIFY_PLAYER_SET_REPEAT_MODE,
|
||||
SLOT_AREA,
|
||||
SLOT_DELAY,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYER_REPEAT_MODE,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlayerSetRepeatMode_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlayerSetRepeatMode.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Set repeat mode for the specified SpotifyPlus media player."
|
||||
self.intent_type = INTENT_PLAYER_SET_REPEAT_MODE
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_DELAY, default=0.50): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0, max=10.0))),
|
||||
vol.Optional(SLOT_PLAYER_REPEAT_MODE): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
player_repeat_mode = intentObj.slots.get(SLOT_PLAYER_REPEAT_MODE, {}).get(CONF_VALUE, "on")
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_SET_REPEAT_MODE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"state": player_repeat_mode,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
intentResponse.speech_slots = intentObj.slots
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_RESPONSE % (intentObj.intent_type), intentResponse, colorValue=SIColors.Khaki)
|
||||
return intentResponse
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..const import (
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
INTENT_PLAYER_SET_SHUFFLE_MODE,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
SERVICE_SPOTIFY_PLAYER_SET_SHUFFLE_MODE,
|
||||
SLOT_AREA,
|
||||
SLOT_DELAY,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYER_SHUFFLE_MODE,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlayerSetShuffleMode_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlayerSetShuffleMode.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Set shuffle mode for the specified SpotifyPlus media player."
|
||||
self.intent_type = INTENT_PLAYER_SET_SHUFFLE_MODE
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_DELAY, default=0.50): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0, max=10.0))),
|
||||
vol.Optional(SLOT_PLAYER_SHUFFLE_MODE): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=[STATE_PLAYING, STATE_PAUSED],
|
||||
desiredStateResponseKey=RESPONSE_PLAYER_NOT_PLAYING_MEDIA,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_SET_SHUFFLE_MODE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"state": True if player_shuffle_mode == "on" else False,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
intentResponse.speech_slots = intentObj.slots
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_RESPONSE % (intentObj.intent_type), intentResponse, colorValue=SIColors.Khaki)
|
||||
return intentResponse
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player.const import MediaPlayerEntityFeature
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..const import (
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
INTENT_PLAYER_TRANSFER_PLAYBACK,
|
||||
SERVICE_SPOTIFY_PLAYER_TRANSFER_PLAYBACK,
|
||||
SLOT_AREA,
|
||||
SLOT_DELAY,
|
||||
SLOT_DEVICE_NAME,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlayerTransferPlayback_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlayerTransferPlayback
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Transfer playback to another Spotify Connect device name."
|
||||
self.intent_type = INTENT_PLAYER_TRANSFER_PLAYBACK
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_DELAY, default=0.50): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0, max=10.0))),
|
||||
vol.Optional(SLOT_DEVICE_NAME): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_TRANSFER_PLAYBACK
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"device_id": device_name,
|
||||
"delay": delay,
|
||||
"play": True,
|
||||
}
|
||||
|
||||
# transfer playback to specified device.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=False,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
intentResponse.speech_slots = intentObj.slots
|
||||
self.logsi.LogObject(SILevel.Verbose, STAppMessages.MSG_INTENT_HANDLER_RESPONSE % (intentObj.intent_type), intentResponse, colorValue=SIColors.Khaki)
|
||||
return intentResponse
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
from homeassistant.const import (
|
||||
SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
IntentResponseErrorCode,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..const import (
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
INTENT_PLAYER_VOLUME_CONTROL,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
RESPONSE_ERROR_PLAYER_VOLUME_CONTROL_INVALID,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_DOWN,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_MUTE,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_SET_STEP_LEVEL,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_SET_LEVEL,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_UNMUTE,
|
||||
RESPONSE_PLAYER_VOLUME_CONTROL_UP,
|
||||
SERVICE_SPOTIFY_PLAYER_SET_VOLUME_LEVEL,
|
||||
SERVICE_VOLUME_SET_STEP,
|
||||
SLOT_AREA,
|
||||
SLOT_DELAY,
|
||||
SLOT_FLOOR,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYER_VOLUME_LEVEL_PCT,
|
||||
SLOT_PLAYER_VOLUME_STEP_PCT,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SLOT_SPOTIFYPLUS_PLAYER_VOLUME_CONTROL,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlayerVolumeControl_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlayerVolumeControl.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Controls media player volume functions (mute, unmute, step up, step down, etc)."
|
||||
self.intent_type = INTENT_PLAYER_VOLUME_CONTROL
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_SPOTIFYPLUS_PLAYER_VOLUME_CONTROL): cv.string,
|
||||
vol.Optional(SLOT_PLAYER_VOLUME_LEVEL_PCT, default=10): vol.Any(None, vol.All(vol.Coerce(int), vol.Range(min=0, max=100))),
|
||||
vol.Optional(SLOT_PLAYER_VOLUME_STEP_PCT, default=10): vol.Any(None, vol.All(vol.Coerce(int), vol.Range(min=1, max=100))),
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# get optional arguments (if provided).
|
||||
player_volume_control:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_PLAYER_VOLUME_CONTROL, {}).get(CONF_VALUE, "").lower()
|
||||
|
||||
# process based on media type.
|
||||
if (player_volume_control == "mute"):
|
||||
return await self.async_ProcessMute(intentObj, intentResponse)
|
||||
|
||||
elif (player_volume_control == "unmute"):
|
||||
return await self.async_ProcessUnMute(intentObj, intentResponse)
|
||||
|
||||
elif (player_volume_control == "step_down"):
|
||||
return await self.async_ProcessStepDown(intentObj, intentResponse)
|
||||
|
||||
elif (player_volume_control == "step_up"):
|
||||
return await self.async_ProcessStepUp(intentObj, intentResponse)
|
||||
|
||||
elif (player_volume_control == "set_step_level"):
|
||||
return await self.async_ProcessSetStepLevel(intentObj, intentResponse)
|
||||
|
||||
elif (player_volume_control == "set_level"):
|
||||
return await self.async_ProcessSetLevel(intentObj, intentResponse)
|
||||
|
||||
else:
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_PLAYER_VOLUME_CONTROL_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
||||
|
||||
|
||||
async def async_ProcessMute(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_VOLUME_MUTE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"is_volume_muted": True
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_MUTE)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessUnMute(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_MUTE | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
# n/a
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_VOLUME_MUTE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"is_volume_muted": False
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_UNMUTE)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessStepDown(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get current step level from player attributes and update slot.
|
||||
player_volume_step_pct_default = int(intentObj.slots.get(SLOT_PLAYER_VOLUME_STEP_PCT, {}).get(CONF_VALUE, 10))
|
||||
player_volume_step = float(playerEntityState.attributes.get("volume_step", player_volume_step_pct_default / 100))
|
||||
player_volume_step_pct = int(player_volume_step * 100)
|
||||
intentObj.slots[SLOT_PLAYER_VOLUME_STEP_PCT] = { CONF_VALUE: player_volume_step_pct, CONF_TEXT: player_volume_step_pct }
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_VOLUME_DOWN
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_DOWN)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessStepUp(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_STEP | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get current step level from player attributes and update slot.
|
||||
player_volume_step_pct_default = int(intentObj.slots.get(SLOT_PLAYER_VOLUME_STEP_PCT, {}).get(CONF_VALUE, 10))
|
||||
player_volume_step = float(playerEntityState.attributes.get("volume_step", player_volume_step_pct_default / 100))
|
||||
player_volume_step_pct = int(player_volume_step * 100)
|
||||
intentObj.slots[SLOT_PLAYER_VOLUME_STEP_PCT] = { CONF_VALUE: player_volume_step_pct, CONF_TEXT: player_volume_step_pct }
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_VOLUME_UP
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN_MEDIA_PLAYER,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_UP)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessSetStepLevel(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_STEP,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
player_volume_step_pct = intentObj.slots.get(SLOT_PLAYER_VOLUME_STEP_PCT, {}).get(CONF_VALUE, 10)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_VOLUME_SET_STEP
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"level_percent": player_volume_step_pct,
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_SET_STEP_LEVEL)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_ProcessSetLevel(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse,
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.PLAY_MEDIA,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
||||
player_volume_level_pct = intentObj.slots.get(SLOT_PLAYER_VOLUME_LEVEL_PCT, {}).get(CONF_VALUE, "on")
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYER_SET_VOLUME_LEVEL
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"volume_level": player_volume_level_pct,
|
||||
"device_id": "", # always use current device for this service call.
|
||||
"delay": delay
|
||||
}
|
||||
|
||||
# call integration service for this intent.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
)
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYER_VOLUME_CONTROL_SET_LEVEL)
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
@@ -0,0 +1,154 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import State, ServiceResponse
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.intent import (
|
||||
Intent,
|
||||
IntentResponse,
|
||||
)
|
||||
|
||||
from smartinspectpython.siauto import SILevel, SIColors
|
||||
|
||||
from ..appmessages import STAppMessages
|
||||
from ..intent_loader import IntentLoader
|
||||
from ..utils import get_id_from_uri
|
||||
from ..const import (
|
||||
CONF_TEXT,
|
||||
CONF_VALUE,
|
||||
DOMAIN,
|
||||
PLATFORM_SPOTIFYPLUS,
|
||||
INTENT_PLAYLIST_CREATE,
|
||||
RESPONSE_PLAYLIST_CREATED,
|
||||
SERVICE_SPOTIFY_PLAYLIST_CREATE,
|
||||
SLOT_AREA,
|
||||
SLOT_DESCRIPTION,
|
||||
SLOT_FLOOR,
|
||||
SLOT_IMAGE_PATH,
|
||||
SLOT_IS_COLLABORATIVE,
|
||||
SLOT_IS_PUBLIC,
|
||||
SLOT_NAME,
|
||||
SLOT_PLAYLIST_NAME,
|
||||
SLOT_PLAYLIST_TITLE,
|
||||
SLOT_PLAYLIST_URL,
|
||||
SLOT_PREFERRED_AREA_ID,
|
||||
SLOT_PREFERRED_FLOOR_ID,
|
||||
SPOTIFY_WEB_URL_PFX,
|
||||
)
|
||||
|
||||
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
||||
|
||||
|
||||
class SpotifyPlusPlaylistCreate_Handler(SpotifyPlusIntentHandler):
|
||||
"""
|
||||
Handles intents for SpotifyPlusPlaylistCreate.
|
||||
"""
|
||||
def __init__(self, intentLoader:IntentLoader) -> None:
|
||||
"""
|
||||
Initializes a new instance of the IntentHandler class.
|
||||
"""
|
||||
# invoke base class method.
|
||||
super().__init__(intentLoader)
|
||||
|
||||
# set intent handler basics.
|
||||
self.description = "Creates a new empty playlist for the current user."
|
||||
self.intent_type = INTENT_PLAYLIST_CREATE
|
||||
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
||||
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""
|
||||
Returns the slot schema for this intent.
|
||||
"""
|
||||
return {
|
||||
|
||||
# slots that determine which media player entity will be used.
|
||||
vol.Optional(SLOT_NAME): cv.string,
|
||||
vol.Optional(SLOT_AREA): cv.string,
|
||||
vol.Optional(SLOT_FLOOR): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
||||
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
||||
|
||||
# slots for other service arguments.
|
||||
vol.Optional(SLOT_DESCRIPTION): cv.string,
|
||||
vol.Optional(SLOT_IMAGE_PATH): cv.string,
|
||||
vol.Optional(SLOT_IS_COLLABORATIVE): cv.boolean,
|
||||
vol.Optional(SLOT_IS_PUBLIC): cv.boolean,
|
||||
vol.Optional(SLOT_PLAYLIST_NAME): cv.string,
|
||||
}
|
||||
|
||||
|
||||
async def async_HandleIntent(
|
||||
self,
|
||||
intentObj: Intent,
|
||||
intentResponse: IntentResponse
|
||||
) -> IntentResponse:
|
||||
"""
|
||||
Handles the intent.
|
||||
|
||||
Args:
|
||||
intentObj (Intent):
|
||||
Intent object.
|
||||
intentResponse (IntentResponse)
|
||||
Intent response object.
|
||||
|
||||
Returns:
|
||||
An IntentResponse object.
|
||||
"""
|
||||
# invoke base class method to resolve the player entity and its state.
|
||||
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
||||
intentObj,
|
||||
intentResponse,
|
||||
desiredFeatures=None,
|
||||
desiredStates=None,
|
||||
desiredStateResponseKey=None,
|
||||
requiresSpotifyPremium=True,
|
||||
)
|
||||
|
||||
# if media player was not resolved, then we are done;
|
||||
# note that the base class method above already called `async_set_speech` with a response.
|
||||
if playerEntityState is None:
|
||||
return intentResponse
|
||||
|
||||
# get optional arguments (if provided).
|
||||
playlist_name = intentObj.slots.get(SLOT_PLAYLIST_NAME, {}).get(CONF_TEXT, None)
|
||||
playlist_description = intentObj.slots.get(SLOT_DESCRIPTION, {}).get(CONF_TEXT, None)
|
||||
playlist_is_collaborative = intentObj.slots.get(SLOT_IS_COLLABORATIVE, {}).get(CONF_TEXT, False)
|
||||
playlist_is_public = intentObj.slots.get(SLOT_IS_PUBLIC, {}).get(CONF_TEXT, False)
|
||||
playlist_image_path = intentObj.slots.get(SLOT_IMAGE_PATH, {}).get(CONF_TEXT, None)
|
||||
|
||||
# set service name and build parameters.
|
||||
svcName:str = SERVICE_SPOTIFY_PLAYLIST_CREATE
|
||||
svcData:dict = \
|
||||
{
|
||||
"entity_id": playerEntityState.entity_id,
|
||||
"name": playlist_name,
|
||||
"description": playlist_description,
|
||||
"public": playlist_is_public,
|
||||
"collaborative": playlist_is_collaborative,
|
||||
}
|
||||
if (playlist_image_path) and (len(playlist_image_path) > 0):
|
||||
svcData["image_path"] = playlist_image_path
|
||||
|
||||
# create the new playlist.
|
||||
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
||||
info_result:ServiceResponse = await intentObj.hass.services.async_call(
|
||||
DOMAIN,
|
||||
svcName,
|
||||
svcData,
|
||||
blocking=True,
|
||||
context=intentObj.context,
|
||||
return_response=True,
|
||||
)
|
||||
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_PLAYLIST_CREATE result", info_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# get related details and update slot info.
|
||||
playlist_uri:str = info_result.get("result",{}).get("uri",None)
|
||||
playlist_url:str = info_result.get("result",{}).get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
||||
|
||||
# update slots with returned info.
|
||||
intentObj.slots[SLOT_PLAYLIST_TITLE] = { CONF_VALUE: playlist_uri, CONF_TEXT: playlist_name }
|
||||
intentObj.slots[SLOT_PLAYLIST_URL] = { CONF_VALUE: playlist_url, CONF_TEXT: "Spotify" }
|
||||
|
||||
# return intent response.
|
||||
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAYLIST_CREATED)
|
||||
+1606
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,463 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import IO, Any, cast, Dict
|
||||
import yaml
|
||||
|
||||
from home_assistant_intents import (
|
||||
get_languages,
|
||||
)
|
||||
|
||||
from hassil.intents import (
|
||||
Intents,
|
||||
TextSlotList,
|
||||
)
|
||||
from hassil.util import merge_dict
|
||||
|
||||
from homeassistant.const import EVENT_CALL_SERVICE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import language as language_util
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
from homeassistant.components.conversation.const import (
|
||||
SERVICE_RELOAD as CONVERSATION_SERVICE_RELOAD,
|
||||
DOMAIN as DOMAIN_CONVERSATION,
|
||||
)
|
||||
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = _LOGGER
|
||||
|
||||
ERROR_SENTINEL = object()
|
||||
METADATA_CUSTOM_SENTENCE = "hass_custom_sentence"
|
||||
METADATA_CUSTOM_FILE = "hass_custom_file"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LanguageIntents:
|
||||
"""Loaded intents for a language."""
|
||||
intents: Intents
|
||||
intents_dict: dict[str, Any]
|
||||
intent_responses: dict[str, Any]
|
||||
error_responses: dict[str, Any]
|
||||
platform_responses: dict[str, Any]
|
||||
generic_responses: dict[str, Any]
|
||||
language_variant: str | None
|
||||
|
||||
|
||||
def json_load(fp: IO[str]) -> JsonObjectType:
|
||||
"""Wrap json_loads for get_intents."""
|
||||
return json_loads_object(fp.read())
|
||||
|
||||
|
||||
class IntentLoader():
|
||||
"""
|
||||
Intent loader class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass:HomeAssistant,
|
||||
platform:str=None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize a new instance of the class.
|
||||
|
||||
Args:
|
||||
hass (HomeAssistant):
|
||||
Hass instance.
|
||||
platform (str):
|
||||
Specify a platform prefix to only load `custom_sentences` files that
|
||||
begin with the specified platform name.
|
||||
"""
|
||||
self.hass = hass
|
||||
self._LangIntents:dict[str, LanguageIntents | object] = {}
|
||||
self._LoadIntentsLock = asyncio.Lock()
|
||||
self._Platform:str = platform
|
||||
self.unsubscribe_event = None
|
||||
|
||||
|
||||
@property
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return a list of supported languages."""
|
||||
return get_languages()
|
||||
|
||||
|
||||
async def async_clear_cache(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
Clears the cache of any loaded intents.
|
||||
"""
|
||||
try:
|
||||
|
||||
# anything in the cache? if not, then there's nothing to do.
|
||||
if (self._LangIntents == {}):
|
||||
_logsi.LogVerbose("Intent cache is already empty; nothing to do", colorValue=SIColors.Khaki)
|
||||
return
|
||||
|
||||
# let's prepare to load the intent data;
|
||||
# set a lock in case we get multiple requests at the same time.
|
||||
_logsi.LogVerbose("Acquiring lock prior to clearing cache", colorValue=SIColors.Khaki)
|
||||
async with self._LoadIntentsLock:
|
||||
|
||||
# clear the cache.
|
||||
self._LangIntents.clear()
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Intent cache has been cleared", colorValue=SIColors.Khaki)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# log exception, but not to system logger as HA will take care of it.
|
||||
_logsi.LogException("Component async_clear_cache exception", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
|
||||
async def async_get_or_load_intents(
|
||||
self,
|
||||
language:str=None,
|
||||
) -> LanguageIntents | None:
|
||||
"""
|
||||
Gets the cached intent data for the specified language.
|
||||
|
||||
Args:
|
||||
language (str):
|
||||
Language indicator, used to find the language-specific folder under the
|
||||
custom_sentences base folder.
|
||||
|
||||
Returns:
|
||||
A LanguageIntents instance if intent data was found; otherwise, None.
|
||||
|
||||
For the first call of this method, the intents are loaded from disk for the
|
||||
specified language (in a thread-safe manner).
|
||||
|
||||
For subsequent calls, the cached intent list is returned.
|
||||
|
||||
The intent cache is cleared and reloaded if the conversation "reload" service
|
||||
is called.
|
||||
"""
|
||||
try:
|
||||
|
||||
# validations.
|
||||
if (not isinstance(language, str)) or (len(language.strip()) == 0):
|
||||
language = self.hass.config.language or "en"
|
||||
|
||||
# if we already loaded intents, then return the cached data.
|
||||
if lang_intents := self._LangIntents.get(language):
|
||||
if lang_intents is ERROR_SENTINEL:
|
||||
return None
|
||||
return cast(LanguageIntents, lang_intents)
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Intents first-time load detected; calling load intents (language=%s)" % (language), colorValue=SIColors.Khaki)
|
||||
|
||||
# let's prepare to load the intent data;
|
||||
# set a lock in case we get multiple requests at the same time.
|
||||
_logsi.LogVerbose("Acquiring lock prior to loading intents", colorValue=SIColors.Khaki)
|
||||
async with self._LoadIntentsLock:
|
||||
|
||||
# was another request waiting while we were loading?
|
||||
# if so, then there is no need to load it again.
|
||||
if lang_intents := self._LangIntents.get(language):
|
||||
if lang_intents is ERROR_SENTINEL:
|
||||
return None
|
||||
return cast(LanguageIntents, lang_intents)
|
||||
|
||||
# get start time so we can see how long it takes to load.
|
||||
start = time.monotonic()
|
||||
|
||||
# load the intents from custom sentences.
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._load_intent_definitions, language
|
||||
)
|
||||
|
||||
# if we didn't find anything, then set result to an empty object.
|
||||
# this denotes that we tried to load the intent data and it did not return
|
||||
# anything, so that we don't try to load it again on the next request.
|
||||
# otherwise, store the loaded intent data under the language-specific key.
|
||||
if result is None:
|
||||
self._LangIntents[language] = ERROR_SENTINEL
|
||||
else:
|
||||
self._LangIntents[language] = result
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Full intent load completed (language=%s) in %.2f seconds" % (language, (time.monotonic() - start)), colorValue=SIColors.Khaki)
|
||||
|
||||
# return result.
|
||||
return result
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# log exception, but not to system logger as HA will take care of it.
|
||||
_logsi.LogException("Component async_get_or_load_intents exception", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
|
||||
async def async_get_intent_list_byname(
|
||||
self,
|
||||
language:str=None,
|
||||
listName:str=None,
|
||||
) -> Dict[str, TextSlotList] | None:
|
||||
"""
|
||||
Gets the specified intent list name definition.
|
||||
|
||||
Args:
|
||||
language (str):
|
||||
Language indicator, used to find the language-specific folder under the
|
||||
custom_sentences base folder.
|
||||
listName (str):
|
||||
Name of the specific list to retrieve; if null, ALL lists are returned.
|
||||
|
||||
Returns:
|
||||
A dictionary of intent list data if found; otherwise, None.
|
||||
"""
|
||||
try:
|
||||
|
||||
# get intent cache; return null if intents could not be loaded.
|
||||
langIntents:LanguageIntents = await self.async_get_or_load_intents(language)
|
||||
if langIntents is None:
|
||||
return None
|
||||
|
||||
# if list name not supplied, then return ALL lists.
|
||||
if (listName is None):
|
||||
return langIntents.intents.slot_lists
|
||||
else:
|
||||
return langIntents.intents.slot_lists.get(listName, None)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# log exception, but not to system logger as HA will take care of it.
|
||||
_logsi.LogException("Component async_get_intent_list exception", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
|
||||
def _load_intent_definitions(
|
||||
self,
|
||||
language:str=None,
|
||||
) -> LanguageIntents | None:
|
||||
"""
|
||||
Loads language-specific intent template definitions from `custom_sentences/<lang>/*.yaml`
|
||||
files (if found).
|
||||
|
||||
Must run inside executor!
|
||||
|
||||
Args:
|
||||
language (str):
|
||||
Language indicator, used to find the language-specific folder under the
|
||||
custom_sentences base folder.
|
||||
|
||||
Returns:
|
||||
A LanguageIntents object with loaded intent data if found; otherwise, None.
|
||||
|
||||
Response key templates may be nested using the following schemas:
|
||||
- standard layout: `responses -> intents -> MyIntentName -> my_message_key: "My message text"`
|
||||
- platform layout: `responses -> MyPlatform -> my_message_key: "My message text"`
|
||||
- flatfile layout: `responses -> my_message_key: "My message text"`
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
methodParms.AppendKeyValue("language", str(language))
|
||||
methodParms.AppendKeyValue("platform", self._Platform)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "Component _load_intent_definitions starting", methodParms, colorValue=SIColors.Khaki)
|
||||
|
||||
intents_dict: dict[str, Any] = {}
|
||||
supported_langs = set(get_languages())
|
||||
|
||||
# choose a language variant upfront and commit to it.
|
||||
lang_matches = language_util.matches(language, supported_langs)
|
||||
if not lang_matches:
|
||||
_logsi.LogWarning("Unable to find supported language variant for \"%s\"" % language)
|
||||
return None
|
||||
|
||||
language_variant = lang_matches[0]
|
||||
|
||||
# # load built-in intents and responses for this language variant.
|
||||
# lang_variant_intents = get_intents(language_variant, json_load=json_load)
|
||||
|
||||
# if lang_variant_intents:
|
||||
# # merge sentences into existing dictionary
|
||||
# # overriding because source dict is empty
|
||||
# intents_dict = lang_variant_intents
|
||||
|
||||
# _LOGGER.debug(
|
||||
# "Loaded built-in intents for language=%s (%s)",
|
||||
# language,
|
||||
# language_variant,
|
||||
# )
|
||||
|
||||
# check for custom sentences in "<config>/custom_sentences/<language>/" path.
|
||||
custom_sentences_dir = Path(
|
||||
self.hass.config.path("custom_sentences", language_variant)
|
||||
)
|
||||
_logsi.LogVerbose("Checking custom_sentences directory: \"%s\"" % custom_sentences_dir, colorValue=SIColors.Khaki)
|
||||
|
||||
# is this a directory?
|
||||
if custom_sentences_dir.is_dir():
|
||||
|
||||
# check for platform prefix file limiting.
|
||||
platformPfx = ""
|
||||
if self._Platform is not None:
|
||||
platformPfx = f"{self._Platform}_"
|
||||
_logsi.LogVerbose("Limiting custom_sentences files by platform prefix: \"%s\"" % platformPfx, colorValue=SIColors.Khaki)
|
||||
|
||||
# create file search pattern.
|
||||
fileSearchPattern = f"{platformPfx}*.yaml"
|
||||
_logsi.LogVerbose("Searching custom_sentences directory (and sub-directories) for this file pattern: \"%s\"" % fileSearchPattern, colorValue=SIColors.Khaki)
|
||||
|
||||
# process all found files in the directory, including sub-directories.
|
||||
for custom_sentences_path in sorted(custom_sentences_dir.rglob(fileSearchPattern)):
|
||||
|
||||
# if processing only our platform files, then disacrd files that aren't ours.
|
||||
# this assumes that our files are prefixed by the platform name (e.g. "/spotifyplus_IntentNamexxx.yaml").
|
||||
# TODO should not need this, as the rglob takes care of filtering out unwanted files!
|
||||
#fnameCompare = custom_sentences_path.name.lower()
|
||||
# if (self._Platform is not None) and (not fnameCompare.startswith(self._Platform)):
|
||||
# _logsi.LogDebug("Discarding non-platform custom_sentences file: %s" % custom_sentences_path, colorValue=SIColors.Khaki)
|
||||
# continue
|
||||
|
||||
# process the file.
|
||||
_logsi.LogVerbose("Loading custom_sentences file: %s" % custom_sentences_path, colorValue=SIColors.Khaki)
|
||||
with custom_sentences_path.open(encoding="utf-8") as custom_sentences_file:
|
||||
|
||||
# merge custom sentences.
|
||||
if not isinstance(
|
||||
custom_sentences_yaml := yaml.safe_load(custom_sentences_file),
|
||||
dict,
|
||||
):
|
||||
_logsi.LogWarning("Custom sentences file does not match expected format: \"%s\"" % custom_sentences_file.name)
|
||||
continue
|
||||
|
||||
# add metadata so we can identify custom sentences in the debugger.
|
||||
custom_intents_dict = custom_sentences_yaml.get("intents", {})
|
||||
for intent_dict in custom_intents_dict.values():
|
||||
intent_data_list = intent_dict.get("data", [])
|
||||
for intent_data in intent_data_list:
|
||||
sentence_metadata = intent_data.get("metadata", {})
|
||||
sentence_metadata[METADATA_CUSTOM_SENTENCE] = True
|
||||
sentence_metadata[METADATA_CUSTOM_FILE] = str(
|
||||
custom_sentences_path.relative_to(
|
||||
custom_sentences_dir.parent
|
||||
)
|
||||
)
|
||||
intent_data["metadata"] = sentence_metadata
|
||||
|
||||
# merge dictionary into base dictionary.
|
||||
merge_dict(intents_dict, custom_sentences_yaml)
|
||||
|
||||
# if no custom sentences defined then we are done.
|
||||
if not intents_dict:
|
||||
_logsi.LogVerbose("Could not find custom_sentences files for language=\"%s\", platform=\"%s\"" % (language, self._Platform), colorValue=SIColors.Khaki)
|
||||
return None
|
||||
|
||||
# load intent objects from dictionary format.
|
||||
intents = Intents.from_dict(intents_dict)
|
||||
|
||||
# load responses.
|
||||
responses_dict = intents_dict.get("responses", {})
|
||||
intent_responses = responses_dict.get("intents", {})
|
||||
platform_responses = responses_dict.get(self._Platform, {})
|
||||
error_responses = responses_dict.get("errors", {})
|
||||
|
||||
# remove any child dictionary items from generic.
|
||||
generic_responses = copy.deepcopy(intents_dict.get("responses", {}))
|
||||
for key in [k for k, v in generic_responses.items() if isinstance(v, dict)]:
|
||||
del generic_responses[key]
|
||||
|
||||
# trace.
|
||||
if (_logsi.IsOn(SILevel.Verbose)):
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - all (dictionary)" % (self._Platform, language_variant), intents_dict, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - responses (dictionary)" % (self._Platform, language_variant), responses_dict, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - intent_responses (dictionary)" % (self._Platform, language_variant), intent_responses, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - platform_responses (dictionary)" % (self._Platform, language_variant), platform_responses, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - error_responses (dictionary)" % (self._Platform, language_variant), error_responses, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - generic_responses (dictionary)" % (self._Platform, language_variant), generic_responses, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - intents slot_lists (dictionary)" % (self._Platform, language_variant), intents.slot_lists, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - intents expansion_rules (dictionary)" % (self._Platform, language_variant), intents.expansion_rules, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Component intents information: %s (%s) - intents skip_words (dictionary)" % (self._Platform, language_variant), intents.skip_words, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# return results.
|
||||
result = LanguageIntents(
|
||||
intents,
|
||||
intents_dict,
|
||||
intent_responses,
|
||||
error_responses,
|
||||
platform_responses,
|
||||
generic_responses,
|
||||
language_variant,
|
||||
)
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Component _load_intent_definitions complete", colorValue=SIColors.Khaki)
|
||||
return result
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# log exception, but not to system logger as HA will take care of it.
|
||||
_logsi.LogException("Component _load_intent_definitions exception", ex, logToSystemLogger=False, colorValue=SIColors.Khaki)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_register_cache_reload_listener(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
Registers an event listener that listens for `conversation.reload` service calls,
|
||||
so that our cache will also be reloaded on the next access call.
|
||||
"""
|
||||
|
||||
@callback
|
||||
async def _handle_call_service_event(event):
|
||||
""" Called when a "call_service" event is detected. """
|
||||
|
||||
# get event data.
|
||||
data = event.data
|
||||
domain = data.get("domain")
|
||||
service = data.get("service")
|
||||
|
||||
# trace.
|
||||
#_logsi.LogDictionary(SILevel.Verbose, "Call Service detected: domain=\"%s\", service=\"%s\"" % (domain, service), event, prettyPrint=True, colorValue=SIColors.Red)
|
||||
|
||||
# was the "conversation.reload" service called?
|
||||
if domain == DOMAIN_CONVERSATION and service == CONVERSATION_SERVICE_RELOAD:
|
||||
|
||||
_logsi.LogDictionary(SILevel.Verbose, "Intent Loader detected a Conversation integration Reload event", event, prettyPrint=True, colorValue=SIColors.Khaki)
|
||||
|
||||
# clear our intent cache, so it will be reloaded next time.
|
||||
await self.async_clear_cache()
|
||||
|
||||
# listen for all call service events, and store unsubscribe function.
|
||||
self.unsubscribe_event = self.hass.bus.async_listen(EVENT_CALL_SERVICE, _handle_call_service_event)
|
||||
|
||||
# trace.
|
||||
_logsi.LogVerbose("Intent Loader registered the conversation reload event listener", colorValue=SIColors.Khaki)
|
||||
|
||||
|
||||
async def async_unregister_cache_reload_listener(
|
||||
self,
|
||||
) -> None:
|
||||
"""
|
||||
Unregisters an event listener that listens for `conversation.reload` service calls.
|
||||
"""
|
||||
# did we register an event listener? if so, then call it's unregister function.
|
||||
if self.unsubscribe_event:
|
||||
self.unsubscribe_event() # <-- stop listening
|
||||
_logsi.LogVerbose("Unregistered the conversation reload event listener", colorValue=SIColors.Khaki)
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"domain": "spotifyplus",
|
||||
"name": "SpotifyPlus",
|
||||
"codeowners": [
|
||||
"@thlucas1"
|
||||
],
|
||||
"config_flow": true,
|
||||
"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",
|
||||
"requirements": [
|
||||
"numpy>=2.3.2",
|
||||
"oauthlib>=3.2.2",
|
||||
"pillow>=11.3.0",
|
||||
"platformdirs>=4.1.0",
|
||||
"requests>=2.31.0",
|
||||
"requests_oauthlib>=1.3.1",
|
||||
"smartinspectpython>=3.0.38",
|
||||
"spotifywebapipython>=1.0.270",
|
||||
"urllib3>=2.0",
|
||||
"zeroconf>=0.132.2"
|
||||
],
|
||||
"version": "v1.0.201"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
"""Support for Spotify media searching."""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
from spotifywebapipython import SpotifyClient, SpotifyMediaTypes
|
||||
from spotifywebapipython.models import *
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
SearchMediaQuery,
|
||||
SearchMedia,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import IntegrationError
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
||||
import logging
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SPOTIFY_SEARCH_LIMIT_TOTAL = 10
|
||||
""" Max number of items to return (for each type) from a search request (5). """
|
||||
|
||||
|
||||
def search_media_node(
|
||||
hass:HomeAssistant,
|
||||
client:SpotifyClient,
|
||||
playerName:str,
|
||||
query: SearchMediaQuery,
|
||||
) -> SearchMedia:
|
||||
"""
|
||||
Searches the Spotify catalog for the requested criteria, and returns a list of
|
||||
BrowseMedia items with the results.
|
||||
|
||||
Args:
|
||||
hass (HomeAssistant):
|
||||
HomeAssistant instance.
|
||||
client (SpotifyClient):
|
||||
The SpotifyClient instance that will make calls to the device
|
||||
to retrieve the data for display in the media browser.
|
||||
playerName (str):
|
||||
Name of the media player that is calling this method (for tracing purposes).
|
||||
query (SearchMediaQuery):
|
||||
Search media query criteria to search for.
|
||||
Note that `media_filter_classes` can also be passed as a dictionary of values,
|
||||
which differs from the method signature which indicates a `list[MediaClass]`.
|
||||
|
||||
Returns:
|
||||
A `SearchMedia` object that contains the search results.
|
||||
"""
|
||||
methodParms:SIMethodParmListContext = None
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
methodParms = _logsi.EnterMethodParmList(SILevel.Debug)
|
||||
methodParms.AppendKeyValue("playerName", playerName)
|
||||
methodParms.AppendKeyValue("query", query)
|
||||
methodParms.AppendKeyValue("query.media_content_id", query.media_content_id)
|
||||
methodParms.AppendKeyValue("query.media_content_type", query.media_content_type)
|
||||
methodParms.AppendKeyValue("query.media_filter_classes", query.media_filter_classes)
|
||||
methodParms.AppendKeyValue("query.search_query", query.search_query)
|
||||
_logsi.LogMethodParmList(SILevel.Verbose, "'%s': Preparing to search for media" % (playerName), methodParms)
|
||||
|
||||
result:list[BrowseMedia] = []
|
||||
|
||||
# default search criteria type (comma-delimited string of criteria types).
|
||||
criteriaType:str = None # will default to "track"
|
||||
|
||||
# was query criteria specified?
|
||||
if (query):
|
||||
|
||||
# was media filter class(es) specified?
|
||||
if (query.media_filter_classes):
|
||||
|
||||
_logsi.LogVerbose("'%s': Filtering by media filter class: \"%s\"" % (playerName, str(query.media_filter_classes)))
|
||||
|
||||
# create a comma-delimited string of criteria types.
|
||||
mapping = {
|
||||
MediaClass.ARTIST: SpotifyMediaTypes.ARTIST.value,
|
||||
MediaClass.ALBUM: SpotifyMediaTypes.ALBUM.value,
|
||||
MediaClass.TRACK: SpotifyMediaTypes.TRACK.value,
|
||||
MediaClass.MUSIC: SpotifyMediaTypes.TRACK.value,
|
||||
MediaClass.PLAYLIST: SpotifyMediaTypes.PLAYLIST.value,
|
||||
MediaClass.APP: SpotifyMediaTypes.AUDIOBOOK.value,
|
||||
MediaClass.PODCAST: SpotifyMediaTypes.SHOW.value,
|
||||
MediaClass.EPISODE: SpotifyMediaTypes.EPISODE.value,
|
||||
}
|
||||
media_types = [
|
||||
mapping[cls] for cls in query.media_filter_classes if cls in mapping
|
||||
]
|
||||
criteriaType = ','.join(media_types) # comma-delimited string of criteria types
|
||||
|
||||
# was a media content type specified?
|
||||
# note that this integration does not use media content type, but rather media filter classes.
|
||||
# some generic media players use media content type though, so support it.
|
||||
elif (query.media_content_type):
|
||||
|
||||
_logsi.LogVerbose("'%s': Filtering by media content type: \"%s\"" % (playerName, str(query.media_content_type)))
|
||||
criteriaType = query.media_content_type
|
||||
|
||||
# change default "music" to "track".
|
||||
if (isinstance(criteriaType,str)):
|
||||
criteriaType = criteriaType.replace("music","track")
|
||||
criteriaType = criteriaType.replace("MUSIC","track")
|
||||
|
||||
# search spotify.
|
||||
_logsi.LogVerbose("'%s': Searching for media: \"%s\"" % (playerName, query.search_query))
|
||||
searchResp:SearchResponse = client.Search(query.search_query, criteriaType, limitTotal=SPOTIFY_SEARCH_LIMIT_TOTAL)
|
||||
|
||||
# add search results for all media types.
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.ALBUM, searchResp.Albums.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.ARTIST, searchResp.Artists.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.AUDIOBOOK, searchResp.Audiobooks.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.EPISODE, searchResp.Episodes.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.PLAYLIST, searchResp.Playlists.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.SHOW, searchResp.Shows.Items)
|
||||
_ProcessFoundItems(result, SpotifyMediaTypes.TRACK, searchResp.Tracks.Items)
|
||||
|
||||
# return search results.
|
||||
return SearchMedia(result=result)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("'%s': SearchMedia search_media_node exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False)
|
||||
raise IntegrationError(str(ex)) from ex
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
def _ProcessFoundItems(
|
||||
result:list[BrowseMedia],
|
||||
spotifyMediaType:SpotifyMediaTypes,
|
||||
items:list[SearchResultBase],
|
||||
) -> None:
|
||||
"""
|
||||
Builds a BrowseMedia object for each search result returned, and appends
|
||||
them to the result collection.
|
||||
|
||||
Args:
|
||||
result (list[BrowseMedia]):
|
||||
List of BrowseMedia items to append results to.
|
||||
spotifyMediaType (str):
|
||||
Spotify media type.
|
||||
items (list[SearchResultBase]):
|
||||
List of matching search items found.
|
||||
"""
|
||||
# track and episode media items cannot be expanded (only played);
|
||||
# other media types can be expanded to display child items (e.g. Album, Artist, Playlist, etc).
|
||||
canExpand:bool = spotifyMediaType not in [
|
||||
SpotifyMediaTypes.TRACK,
|
||||
SpotifyMediaTypes.EPISODE,
|
||||
]
|
||||
|
||||
# get HA media class for Spotify media type.
|
||||
mediaClass:str = _GetMediaClassFromSpotifyMediaType(spotifyMediaType) or MediaClass.DIRECTORY
|
||||
|
||||
# process found items.
|
||||
item:SearchResultBase
|
||||
for item in items:
|
||||
|
||||
# build BrowseMedia object for found item.
|
||||
browseMedia:BrowseMedia = BrowseMedia(
|
||||
can_expand=canExpand,
|
||||
can_play=True,
|
||||
children=None,
|
||||
children_media_class=None,
|
||||
media_class=mediaClass,
|
||||
media_content_id=item.Uri,
|
||||
media_content_type=item.Type,
|
||||
thumbnail=item.ImageUrl,
|
||||
title=item.Name
|
||||
)
|
||||
|
||||
# trace.
|
||||
_logsi.LogObject(SILevel.Verbose, "Search BrowseMedia Object: Type='%s', Id='%s', Title='%s'" % (browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia)
|
||||
|
||||
# append object to results.
|
||||
result.append(browseMedia)
|
||||
|
||||
|
||||
def _GetMediaClassFromSpotifyMediaType(media_type:str) -> MediaClass|None:
|
||||
"""
|
||||
Get the appropriate HA media class for a given Spotify media type.
|
||||
"""
|
||||
result:MediaClass = None
|
||||
|
||||
if (media_type == SpotifyMediaTypes.TRACK.value):
|
||||
result = MediaClass.TRACK
|
||||
elif (media_type == SpotifyMediaTypes.ALBUM.value):
|
||||
result = MediaClass.ALBUM
|
||||
elif (media_type == SpotifyMediaTypes.ARTIST.value):
|
||||
result = MediaClass.ARTIST
|
||||
elif (media_type == SpotifyMediaTypes.PLAYLIST.value):
|
||||
result = MediaClass.PLAYLIST
|
||||
elif (media_type == SpotifyMediaTypes.SHOW.value):
|
||||
result = MediaClass.PODCAST
|
||||
elif (media_type == SpotifyMediaTypes.AUDIOBOOK.value):
|
||||
result = MediaClass.APP
|
||||
elif (media_type == SpotifyMediaTypes.EPISODE.value):
|
||||
result = MediaClass.EPISODE
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _GetSpotifyMediaTypeFromMediaClass(media_class:str|MediaClass) -> SpotifyMediaTypes|None:
|
||||
"""
|
||||
Get the appropriate Spotify media type for a given HA media class.
|
||||
"""
|
||||
result:SpotifyMediaTypes = None
|
||||
media_class_str = str(media_class)
|
||||
|
||||
if (media_class_str == MediaClass.ALBUM.value):
|
||||
result = SpotifyMediaTypes.ALBUM
|
||||
elif (media_class_str == MediaClass.ARTIST.value):
|
||||
result = SpotifyMediaTypes.ARTIST
|
||||
elif (media_class_str == MediaClass.APP.value):
|
||||
result = SpotifyMediaTypes.AUDIOBOOK
|
||||
elif (media_class_str == MediaClass.EPISODE.value):
|
||||
result = SpotifyMediaTypes.EPISODE.value
|
||||
elif (media_class_str == MediaClass.PLAYLIST.value):
|
||||
result = SpotifyMediaTypes.PLAYLIST
|
||||
elif (media_class_str == MediaClass.PODCAST.value):
|
||||
result = SpotifyMediaTypes.SHOW
|
||||
elif (media_class_str == MediaClass.TRACK.value):
|
||||
result = SpotifyMediaTypes.TRACK
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,85 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusFavoriteAddRemove
|
||||
# Location: custom_sentences/en/spotifyplus_FavoriteAddRemove_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusFavoriteAddRemove:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites)"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "add spotify track to favorites"
|
||||
# - "add spotify track favorite"
|
||||
# - "remove spotify track from favorites"
|
||||
# - "remove spotify track favorite"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default player to use.
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites) <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "add spotify track favorite for player spotify premium"
|
||||
# - "add spotify track favorite for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites) <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "add spotify track favorite in the Office area"
|
||||
# - "add spotify track favorite in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusFavoriteAddRemove:
|
||||
|
||||
default: "" # not used - see common `favorite_add_remove_xxxxxxx_ok` messages.
|
||||
@@ -0,0 +1,93 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Absicht: SpotifyPlusGetInfoArtistBio
|
||||
# Ort: custom_sentences/de/spotifyplus_GetInfoArtistBio_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetInfoArtistBio:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify künstler (info|informationen|details|bio) [für|von] {artist_name}"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Standard-Player__
|
||||
# - "spotify künstler info für Phil Wickham"
|
||||
# - "gib mir spotify künstler info von Phil Wickham"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Alias für den Media Player.
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify künstler (info|informationen|details|bio) [für|von] {artist_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "gib mir spotify künstler info für Phil Wickham auf player spotify premium"
|
||||
# - "hole spotify künstler info von Phil Wickham auf spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify künstler (info|informationen|details|bio) [für|von] {artist_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "hole spotify künstler info für Phil Wickham im Büro"
|
||||
# - "gib mir spotify künstler info von Phil Wickham im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Absicht.
|
||||
# Hinweis - Geteilte Antworten für alle Absichten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetInfoArtistBio:
|
||||
|
||||
default: "" # nicht verwendet - siehe allgemeine `get_info_artist_bio`-Nachricht in der common-Datei.
|
||||
@@ -0,0 +1,97 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Absicht: SpotifyPlusGetNowPlayingInfo
|
||||
# Ort: custom_sentences/de/spotifyplus_GetNowPlayingInfo_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetNowPlayingInfo:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify [was läuft|aktuelle wiedergabe] {spotifyplus_media_type} (info|informationen|details)"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Standard-Player__
|
||||
# - "spotify was läuft album info"
|
||||
# - "spotify album info"
|
||||
# - "get spotify album info"
|
||||
# - "spotify nowplaying artist info"
|
||||
# - "spotify artist info"
|
||||
# - "get spotify artist info"
|
||||
# - "spotify nowplaying audiobook info"
|
||||
# - "spotify audiobook info"
|
||||
# - "get spotify audiobook info"
|
||||
# - "spotify nowplaying playlist info"
|
||||
# - "spotify playlist info"
|
||||
# - "get spotify playlist info"
|
||||
# - "spotify nowplaying podcast episode info"
|
||||
# - "spotify podcast episode info"
|
||||
# - "get spotify podcast episode info"
|
||||
# - "spotify nowplaying podcast info"
|
||||
# - "spotify podcast info"
|
||||
# - "get spotify podcast info"
|
||||
# - "spotify nowplaying track info"
|
||||
# - "spotify track info"
|
||||
# - "get spotify track info"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Player.
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify [was läuft|aktuelle wiedergabe] {spotifyplus_media_type} (info|informationen|details) <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "gib mir spotify künstler info für player spotify premium"
|
||||
# - "hole spotify künstler informationen für spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[hole|gib mir] spotify [was läuft|aktuelle wiedergabe] {spotifyplus_media_type} (info|informationen|details) <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "hole spotify künstler info im Büro"
|
||||
# - "gib mir spotify künstler informationen im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Absicht.
|
||||
# Hinweis - Geteilte Antworten für alle Absichten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetNowPlayingInfo:
|
||||
|
||||
default: "" # nicht verwendet - siehe allgemeine `nowplaying_info_xxxxxxx`-Nachrichten in der common-Datei.
|
||||
@@ -0,0 +1,86 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Intent: SpotifyPlusPlayerDeckControl
|
||||
# Ort: custom_sentences/de/spotifyplus_PlayerDeckControl_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerDeckControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control}"
|
||||
|
||||
# Basissätze für diese Intent.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Kein Player-Name__
|
||||
# - "spotify pause"
|
||||
# - "spotify stopp"
|
||||
# - "spotify weiter"
|
||||
# - "spotify fortsetzen"
|
||||
# - "spotify nächster titel"
|
||||
# - "spotify vorheriger titel"
|
||||
# - "spotify neustart"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Player.
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify pause auf player spotify premium"
|
||||
# - "spotify stopp auf spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify pause im Büro"
|
||||
# - "spotify stopp im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Intent.
|
||||
# Hinweis - Geteilte Antworten für alle Intenten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerDeckControl:
|
||||
|
||||
default: "" # nicht verwendet - siehe allgemeine `player_deck_control_xxxxxxx`-Nachrichten in der common-Datei.
|
||||
@@ -0,0 +1,98 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Absicht: SpotifyPlusPlayerSetRepeatMode
|
||||
# Ort: custom_sentences/de/spotifyplus_PlayerSetRepeatMode_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetRepeatMode:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify wiederholungsmodus {player_repeat_mode}"
|
||||
- "spotify wiederholung {player_repeat_mode}"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify repeat on"
|
||||
# - "spotify repeat track"
|
||||
# - "spotify repeat 1 track"
|
||||
# - "spotify repeat single track"
|
||||
# - "spotify repeat context"
|
||||
# - "spotify repeat all"
|
||||
# - "spotify repeat all tracks"
|
||||
# - "spotify repeat list"
|
||||
# - "spotify repeat off"
|
||||
# - "spotify repeat disabled"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Alias für den Media Player.
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify wiederholungsmodus {player_repeat_mode} <on_spotifyplus_player_name>"
|
||||
- "spotify wiederholung {player_repeat_mode} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify wiederholungsmodus titel für player spotify premium"
|
||||
# - "spotify wiederholung titel für spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify wiederholungsmodus {player_repeat_mode} <in_spotifyplus_player_area>"
|
||||
- "spotify wiederholung {player_repeat_mode} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify wiederholungsmodus titel im Büro"
|
||||
# - "spotify wiederholung titel im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Absicht.
|
||||
# Hinweis - Geteilte Antworten für alle Absichten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetRepeatMode:
|
||||
|
||||
default_by_name: "Okay, der Spotify-Wiederholungsmodus ist \"{{ slots.player_repeat_mode.text }}\" für den Player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, der Spotify-Wiederholungsmodus ist \"{{ slots.player_repeat_mode.text }}\" im Bereich \"{{ slots.target_player.text }}\"."
|
||||
default_no_player: "Okay, der Spotify-Wiederholungsmodus ist \"{{ slots.player_repeat_mode.text }}\" für den Standard-Player \"{{ slots.target_player.text }}\"."
|
||||
default: "Okay, der Spotify-Wiederholungsmodus ist \"{{ slots.player_repeat_mode.text }}\"."
|
||||
@@ -0,0 +1,94 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Absicht: SpotifyPlusPlayerSetShuffleMode
|
||||
# Ort: custom_sentences/de/spotifyplus_PlayerSetShuffleMode_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetShuffleMode:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify zufallswiedergabe [modus] {player_shuffle_mode}"
|
||||
- "spotify shuffle [modus] {player_shuffle_mode}"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify shuffle mode on"
|
||||
# - "spotify shuffle mode off"
|
||||
# - "spotify shuffle on"
|
||||
# - "spotify shuffle off"
|
||||
# - "spotify shuffle enabled"
|
||||
# - "spotify shuffle disabled"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Alias für den Media Player.
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify zufallswiedergabe [modus] {player_shuffle_mode} <on_spotifyplus_player_name>"
|
||||
- "spotify shuffle [modus] {player_shuffle_mode} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify shuffle an für player spotify premium"
|
||||
# - "spotify zufallswiedergabe an für spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify zufallswiedergabe [modus] {player_shuffle_mode} <in_spotifyplus_player_area>"
|
||||
- "spotify shuffle [modus] {player_shuffle_mode} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify shuffle an im Büro"
|
||||
# - "spotify zufallswiedergabe an im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Absicht.
|
||||
# Hinweis - Geteilte Antworten für alle Absichten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetShuffleMode:
|
||||
|
||||
default_no_player: "Okay, der Spotify-Zufallswiedergabemodus ist \"{{ slots.player_shuffle_mode.text }}\" für den Standard-Player \"{{ slots.target_player.text }}\"."
|
||||
default_by_name: "Okay, der Spotify-Zufallswiedergabemodus ist \"{{ slots.player_shuffle_mode.text }}\" für den Player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, der Spotify-Zufallswiedergabemodus ist \"{{ slots.player_shuffle_mode.text }}\" im Bereich \"{{ slots.target_player.text }}\"."
|
||||
default: "Okay, der Spotify-Zufallswiedergabemodus ist \"{{ slots.player_shuffle_mode.text }}\"."
|
||||
@@ -0,0 +1,92 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Absicht: SpotifyPlusPlayerTransferPlayback
|
||||
# Ort: custom_sentences/de/spotifyplus_PlayerTransferPlayback_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerTransferPlayback:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify (wiedergabe übertragen auf|übertrage auf|wiedergabe auf) [gerät] {spotifyplus_device_names:device_name}"
|
||||
- "spotify (wiedergabe übertragen auf|übertrage auf|wiedergabe auf) [gerät] {device_name}"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Standard-Player__
|
||||
# - "spotify wiedergabe übertragen auf gerät Bose-ST10-1"
|
||||
# - "spotify übertrage auf Bose-ST10-1"
|
||||
# - "spotify wiedergabe auf Bose-ST10-1"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Alias für den Media Player.
|
||||
target_player: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify (wiedergabe übertragen|übertrage|wiedergabe) [auf] [gerät] {spotifyplus_device_names:device_name} <on_spotifyplus_player_name>"
|
||||
- "spotify (wiedergabe übertragen|übertrage|wiedergabe) [auf] [gerät] {device_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify wiedergabe übertragen auf Bose-ST10-1 für player spotify premium"
|
||||
# - "spotify übertrage auf Bose-ST10-1 für spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify (wiedergabe übertragen|übertrage|wiedergabe) [auf] [gerät] {spotifyplus_device_names:device_name} <in_spotifyplus_player_area>"
|
||||
- "spotify (wiedergabe übertragen|übertrage|wiedergabe) [auf] [gerät] {device_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify wiedergabe übertragen auf Bose-ST10-1 im Büro"
|
||||
# - "spotify übertrage auf Bose-ST10-1 im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Absicht.
|
||||
# Hinweis - Geteilte Antworten für alle Absichten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerTransferPlayback:
|
||||
|
||||
default_no_player: "Okay, Wiedergabe auf Gerät \"{{ slots.device_name.text }}\" übertragen, für Standard-Player \"{{ slots.target_player.text }}\"."
|
||||
default_by_name: "Okay, Wiedergabe auf Gerät \"{{ slots.device_name.text }}\" übertragen, für Player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, Wiedergabe auf Gerät \"{{ slots.device_name.text }}\" übertragen, im Bereich \"{{ slots.target_player.text }}\"."
|
||||
default: "Okay, Wiedergabe auf Gerät \"{{ slots.device_name.text }}\" übertragen."
|
||||
@@ -0,0 +1,89 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Intent: SpotifyPlusPlayerVolumeControl
|
||||
# Ort: custom_sentences/de/spotifyplus_PlayerVolumeControl_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerVolumeControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[setze] spotify {spotifyplus_player_volume_control}"
|
||||
|
||||
# Basissätze für diese Intent.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Kein Player-Name__
|
||||
# - "spotify lauter"
|
||||
# - "spotify leiser"
|
||||
# - "spotify stumm"
|
||||
# - "spotify lautstärke auf 50 prozent"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Player.
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[setze] spotify {spotifyplus_player_volume_control} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify lauter auf player spotify premium"
|
||||
# - "setze spotify lautstärke auf 50 prozent auf player spotify premium"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[setze] spotify {spotifyplus_player_volume_control} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify lauter im Büro"
|
||||
# - "setze spotify lautstärke auf 50 prozent im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Intent.
|
||||
# Hinweis - Geteilte Antworten für alle Intenten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerVolumeControl:
|
||||
|
||||
default: "" # nicht verwendet - siehe allgemeine `player_volume_control_xxxxxxx`-Nachrichten in der common-Datei.
|
||||
@@ -0,0 +1,99 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Intent: SpotifyPlusPlaylistCreate
|
||||
# Ort: custom_sentences/de/spotifyplus_PlaylistCreate_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlaylistCreate:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify erstelle (playlist|wiedergabeliste) {playlist_name}"
|
||||
- "erstelle spotify (playlist|wiedergabeliste) {playlist_name}"
|
||||
|
||||
# Basissätze für diese Intent.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im Sprachbefehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Standard-Player__
|
||||
# - "spotify erstelle playlist Meine neue Playlist"
|
||||
# - "erstelle spotify wiedergabeliste Meine neue Playlist"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Alias für den Media Player.
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Erstellt durch Home Assistant Voice Assist Intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # Beispiel: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify erstelle (playlist|wiedergabeliste) {playlist_name} <on_spotifyplus_player_name>"
|
||||
- "erstelle spotify (playlist|wiedergabeliste) {playlist_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im Sprachbefehl angegeben wird
|
||||
# (z.B. "auf Player Spotify Premium", "auf Todds Player", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify erstelle playlist Meine neue Playlist auf player spotify premium"
|
||||
# - "erstelle spotify wiedergabeliste Meine neue Playlist auf player spotify premium"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Erstellt durch Home Assistant Voice Assist Intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # Beispiel: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify erstelle (playlist|wiedergabeliste) {playlist_name} <in_spotifyplus_player_area>"
|
||||
- "erstelle spotify (playlist|wiedergabeliste) {playlist_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im Sprachbefehl angegeben wird
|
||||
# (z.B. "im Wohnzimmer", "in der Küche", etc.).
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify erstelle playlist Meine neue Playlist im Büro"
|
||||
# - "erstelle spotify wiedergabeliste Meine neue Playlist im Büro"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Erstellt durch Home Assistant Voice Assist Intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # Beispiel: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Intents.
|
||||
# Hinweis - Geteilte Antworten für alle Intenten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlaylistCreate:
|
||||
|
||||
default: "Okay, habe die Spotify-Playlist mit dem Namen \"{{ slots.playlist_title.text }}\" erstellt."
|
||||
@@ -0,0 +1,167 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration benutzerdefinierte Satz- und Antwortdefinitionen.
|
||||
#
|
||||
# Sprache: de, Deutsch
|
||||
# Intent: SpotifyPlusSearchPlayControl
|
||||
# Ort: custom_sentences/de/spotifyplus_SearchPlayControl_de.yaml
|
||||
# ============================================================================
|
||||
language: de
|
||||
intents:
|
||||
|
||||
SpotifyPlusSearchPlayControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn mehrere Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2}"
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2}"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2}"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2}"
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn einzelne Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_single}"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1}"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}]"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single}"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1}"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}]"
|
||||
|
||||
# Basissätze für diese Absicht.
|
||||
# Dieser Satz wird erkannt, wenn kein Name / Bereich im gesprochenen Befehl angegeben wird.
|
||||
# In diesem Fall wird der erste aktive SpotifyPlus Media Player verwendet.
|
||||
|
||||
# __Beispielsätze - Standard-Player__
|
||||
# - "spotify spiele künstler album Take Everything von künstler Seventh Day Slumber"
|
||||
# - "spiele spotify künstler album Take Everything von künstler Seventh Day Slumber"
|
||||
|
||||
# - "spotify spiele künstler titel I Need You von künstler Seventh Day Slumber"
|
||||
# - "spiele spotify künstler titel I Need You von künstler Seventh Day Slumber"
|
||||
|
||||
# - "spotify spiele episode Kurt Russell für podcast Armchair Expert with Dax Shepard"
|
||||
# - "spiele spotify episode Kurt Russell für podcast Armchair Expert with Dax Shepard"
|
||||
|
||||
# - "spotify spiele beliebte songs von künstler test artist list entry"
|
||||
# - "spiele spotify beliebte songs von künstler test artist list entry"
|
||||
# - "spotify spiele favoriten titel von künstler Jeremy Camp"
|
||||
# - "spiele spotify favoriten titel von künstler Jeremy Camp"
|
||||
# - "spotify spiele lieblingslieder"
|
||||
# - "spiele spotify lieblingslieder"
|
||||
|
||||
# - "spotify spiele hörbuch Test Audiobook List Entry"
|
||||
# - "spiele spotify hörbuch Test Audiobook List Entry"
|
||||
# - "spotify spiele hörbuch Die Elfensteine von Shannara"
|
||||
# - "spiele spotify hörbuch Die Elfensteine von Shannara"
|
||||
|
||||
# - "spotify spiele playlist Test Playlist List Entry"
|
||||
# - "spiele spotify playlist Test Playlist List Entry"
|
||||
# - "spotify spiele playlist Old Christian Contemporary"
|
||||
# - "spiele spotify playlist Old Christian Contemporary"
|
||||
|
||||
# - "spotify spiele podcast Test Podcast List Entry"
|
||||
# - "spiele spotify podcast Test Podcast List Entry"
|
||||
# - "spotify spiele podcast Armchair Expert with Dax Shepard"
|
||||
# - "spiele spotify podcast Armchair Expert with Dax Shepard"
|
||||
# - "spotify spiele podcast Armchair Expert with Dax Shepard latest episode"
|
||||
# - "spiele spotify podcast Armchair Expert with Dax Shepard latest episode"
|
||||
|
||||
# - "spotify spiele titel Test Track List Entry"
|
||||
# - "spiele spotify titel Test Track List Entry"
|
||||
# - "spotify spiele titel More Of You"
|
||||
# - "spiele spotify titel More Of You"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # Standard-Mediaplayer-Alias.
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn mehrere Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn einzelne Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} <on_spotifyplus_player_name>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1} <on_spotifyplus_player_name>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <on_spotifyplus_player_name>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} <on_spotifyplus_player_name>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1} <on_spotifyplus_player_name>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <on_spotifyplus_player_name>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Name im gesprochenen
|
||||
# Befehl angegeben wird (z.B. "auf player Spotify Premium", "auf Todds player", etc).
|
||||
# Unterstützt die gleichen Basissätze für diese Absicht.
|
||||
|
||||
# __Beispielsätze - Nach Player-Name__
|
||||
# - "spotify spiele künstler titel I Need You von künstler Seventh Day Slumber auf player spotify premium"
|
||||
# - "spotify spiele titel Test Track List Entry auf player spotify premium"
|
||||
# - "spotify spiele titel More Of You auf player spotify premium"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn mehrere Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von (podcast|show) {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} von künstler {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
|
||||
# Diese Satzvorlage sollte verwendet werden, wenn einzelne Begriffe erforderlich sind.
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} <in_spotifyplus_player_area>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1} <in_spotifyplus_player_area>"
|
||||
- "spotify spiele {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <in_spotifyplus_player_area>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} <in_spotifyplus_player_area>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} von künstler {freeform:criteria_arg_1} <in_spotifyplus_player_area>"
|
||||
- "spiele spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <in_spotifyplus_player_area>"
|
||||
|
||||
# Dieser Satz wird erkannt, wenn ein Player-Bereich im gesprochenen
|
||||
# Befehl angegeben wird (z.B. "im Wohnzimmer bereich", "im Wohnzimmer", etc).
|
||||
# Unterstützt die gleichen Basissätze für diese Absicht.
|
||||
|
||||
# __Beispielsätze - Nach Player-Bereich__
|
||||
# - "spotify spiele künstler titel I Need You von künstler Seventh Day Slumber im Büro bereich"
|
||||
# - "spotify spiele titel Test Track List Entry im Büro bereich"
|
||||
# - "spotify spiele titel More Of You im Büro bereich"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
# ============================================================================
|
||||
# Benutzerdefinierte Antwortdefinitionen für DIESE Intent.
|
||||
# Hinweis - Geteilte Antworten für alle Intenten sind in der Common-Datei gespeichert.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusSearchPlayControl:
|
||||
|
||||
default: "" # nicht verwendet - siehe allgemeine `play_xxxxx`-Nachricht in der common-Datei.
|
||||
@@ -0,0 +1,460 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus Integration allgemeine Antworten, Listen, Erweiterungsregeln, etc.
|
||||
#
|
||||
# Ort: custom_sentences/de/spotifyplus_common_de.yaml
|
||||
# Sprache: de, Deutsch
|
||||
# ============================================================================
|
||||
language: de
|
||||
responses:
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus Plattform-Antworten.
|
||||
#
|
||||
# Beachten Sie, dass die HA-Konversationsintegration dieses Konzept nicht versteht,
|
||||
# da diese Funktionalität einzigartig für UNSERE Integration ist. Daher müssen Sie
|
||||
# die Integration neu laden (oder HA neu starten), damit Änderungen an diesen
|
||||
# Nachrichten wirksam werden.
|
||||
# ============================================================================
|
||||
spotifyplus:
|
||||
|
||||
# Allgemeine Fehlermeldungen.
|
||||
error_unhandled: "Ein unerwarteter Fehler ist aufgetreten:\n\n{{ slots.error_info.text }}"
|
||||
error_failed_to_handle: "Absicht konnte nicht verarbeitet werden:\n\n{{ slots.error_info.text }}"
|
||||
error_media_type_invalid: "Medientyp-Wert \"{{ slots.spotifyplus_media_type.text }}\" ({{ slots.spotifyplus_media_type.value }}) wurde nicht erkannt."
|
||||
error_player_deck_control_invalid: "Player-Steuerungswert \"{{ slots.spotifyplus_player_deck_control.text }}\" ({{ slots.spotifyplus_player_deck_control.value }}) wurde nicht erkannt."
|
||||
error_player_volume_control_invalid: "Player-Lautstärkereglerwert \"{{ slots.spotifyplus_player_volume_control.text }}\" ({{ slots.spotifyplus_player_volume_control.value }}) wurde nicht erkannt."
|
||||
error_search_play_control_invalid: "Such-Wiedergabe-Steuerungswert \"{{ slots.spotifyplus_search_play_control.text }}\" ({{ slots.spotifyplus_search_play_control.value }}) wurde nicht erkannt."
|
||||
error_search_no_criteria: "Suchkriterien nicht angegeben für Such-Wiedergabe-Steuerungstyp \"{{ slots.spotifyplus_search_play_control.text }}\" ({{ slots.spotifyplus_search_play_control.value }})."
|
||||
|
||||
# Nachrichten zu Informationsanfragen.
|
||||
get_info_artist_bio: "Künstlerbiografie für \"{{ slots.artist_title.text }}\" ... \n\n{{ slots.artist_bio.text | truncate(400, end=' ...') }}\n\n[mehr Infos unter {{ slots.artist_url.text }}]({{ slots.artist_url.value }})"
|
||||
|
||||
# Nachrichten zur Favoritenverwaltung.
|
||||
favorite_operation_invalid: "Favoriten-Operationstyp war ungültig: \"{{ slots.favorite_operator.text }}\"."
|
||||
favorite_add_remove_album_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Albumfavoriten \"{{ slots.album_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_artist_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Künstlerfavoriten \"{{ slots.artist_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_audiobook_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Hörbuchfavoriten \"{{ slots.audiobook_title.text }}\" von Autor \"{{ slots.author_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_playlist_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Playlistfavoriten \"{{ slots.playlist_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_podcast_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Podcastfavoriten \"{{ slots.podcast_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_podcast_episode_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Episodenfavoriten \"{{ slots.episode_title.text }}\" aus Podcast \"{{ slots.podcast_title.text }}\" war erfolgreich."
|
||||
favorite_add_remove_track_ok: "Okay, {{ slots.favorite_operator.text }} des Spotify-Titelfavoriten \"{{ slots.track_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\" war erfolgreich."
|
||||
|
||||
# Nachrichten zu "Jetzt läuft"-Informationen.
|
||||
nowplaying_info_album: "Media Player spielt einen Titel vom Album \"{{ slots.album_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\".\n\n[mehr Infos unter {{ slots.album_url.text }}]({{ slots.album_url.value }})"
|
||||
nowplaying_info_audiobook: "Media Player spielt \"{{ slots.chapter_title.text }}\" aus dem Hörbuch \"{{ slots.audiobook_title.text }}\" von Autor \"{{ slots.author_title.text }}\".\n\n[mehr Infos unter {{ slots.audiobook_url.text }}]({{ slots.audiobook_url.value }})"
|
||||
nowplaying_info_playlist: "Media Player spielt einen Titel aus der Playlist \"{{ slots.playlist_title.text }}\".\n\n[mehr Infos unter {{ slots.playlist_url.text }}]({{ slots.playlist_url.value }})"
|
||||
nowplaying_info_podcast: "Media Player spielt die Episode \"{{ slots.episode_title.text }}\" aus dem Podcast \"{{ slots.podcast_title.text }}\".\n\n[mehr Infos unter {{ slots.podcast_url.text }}]({{ slots.podcast_url.value }})"
|
||||
nowplaying_info_podcast_episode: "Media Player spielt die Episode \"{{ slots.episode_title.text }}\" aus dem Podcast \"{{ slots.podcast_title.text }}\".\n\n[mehr Infos unter {{ slots.episode_url.text }}]({{ slots.episode_url.value }})"
|
||||
nowplaying_info_track: "Media Player spielt den Titel \"{{ slots.track_title.text }}\" vom Album \"{{ slots.album_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\".\n\n[mehr Infos unter {{ slots.album_url.text }}]({{ slots.album_url.value }})"
|
||||
nowplaying_no_media_album: "SpotifyPlus Media Player spielt derzeit keine Medien, die Album-Informationen enthalten."
|
||||
nowplaying_no_media_artist: "SpotifyPlus Media Player spielt derzeit keine Medien, die Künstler-Informationen enthalten."
|
||||
nowplaying_no_media_audiobook: "SpotifyPlus Media Player spielt derzeit keine Medien, die Hörbuch-Informationen enthalten."
|
||||
nowplaying_no_media_playlist: "SpotifyPlus Media Player spielt derzeit keine Medien, die Playlist-Informationen enthalten."
|
||||
nowplaying_no_media_podcast: "SpotifyPlus Media Player spielt derzeit keine Medien, die Podcast-Informationen enthalten."
|
||||
nowplaying_no_media_podcast_episode: "SpotifyPlus Media Player spielt derzeit keine Medien, die Podcast-Episoden-Informationen enthalten."
|
||||
nowplaying_no_media_track: "SpotifyPlus Media Player spielt derzeit keine Medien, die Titel-Informationen enthalten."
|
||||
|
||||
# Media-Player-bezogene Nachrichten.
|
||||
play_album_with_artist: "Spiele Spotify-Album \"{{ slots.album_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\"."
|
||||
play_album: "Spiele Spotify-Album \"{{ slots.album_title.text }}\"."
|
||||
play_audiobook: "Spiele Spotify-Hörbuch \"{{ slots.audiobook_title.text }}\"."
|
||||
play_audiobook_with_author: "Spiele Spotify-Hörbuch \"{{ slots.audiobook_title.text }}\" von Autor \"{{ slots.author_title.text }}\"."
|
||||
play_favorite_tracks: "Spiele Spotify-Favoriten."
|
||||
play_favorite_tracks_for_artist: "Spiele Spotify-Favoriten für Künstler \"{{ slots.artist_title.text }}\"."
|
||||
play_playlist: "Spiele Spotify-Playlist \"{{ slots.playlist_title.text }}\"."
|
||||
play_podcast: "Spiele Spotify-Podcast \"{{ slots.podcast_title.text }}\"."
|
||||
play_podcast_episode: "Spiele Spotify-Episode \"{{ slots.episode_title.text }}\" aus Podcast \"{{ slots.podcast_title.text }}\"."
|
||||
play_track_with_artist: "Spiele Spotify-Titel \"{{ slots.track_title.text }}\" von Künstler \"{{ slots.artist_title.text }}\"."
|
||||
play_track: "Spiele Spotify-Titel \"{{ slots.track_title.text }}\"."
|
||||
player_already_playing_media: "Media Player \"{{ slots.target_player.text }}\" spielt bereits Medien ab."
|
||||
player_deck_control_pause: "Okay, Spotify hat die Wiedergabe pausiert."
|
||||
player_deck_control_resume: "Okay, Spotify hat die Wiedergabe fortgesetzt."
|
||||
player_deck_control_seek_start: "Okay, Spotify hat den Titel neu gestartet."
|
||||
player_deck_control_skip_next: "Okay, Spotify hat zum nächsten Titel gesprungen."
|
||||
player_deck_control_skip_previous: "Okay, Spotify hat zum vorherigen Titel gesprungen."
|
||||
player_features_not_supported: "Media Player \"{{ slots.name.text }}\" unterstützt die für diese Anfrage erforderlichen Funktionen nicht: \"{{ slots.error_features.text }}\"."
|
||||
player_not_exposed_to_voice: "Media Player \"{{ slots.target_player.text }}\" wurde nicht für den Sprachassistenten freigegeben."
|
||||
player_not_matched: "SpotifyPlus Media Player konnte mit \"{{ slots.target_player.text }}\" nicht gefunden werden."
|
||||
player_not_matched_area: "SpotifyPlus Media Player konnte für den Bereich \"{{ slots.area.text }}\" nicht gefunden werden."
|
||||
player_not_playing_media: "Media Player \"{{ slots.target_player.text }}\" spielt derzeit keine Medien ab."
|
||||
player_not_spotifyplus: "Der gefundene Media Player \"{{ slots.target_player.text }}\" ist kein SpotifyPlus Media Player."
|
||||
player_volume_control_down: "Okay, die Spotify-Lautstärke wurde um {{ slots.player_volume_step_pct.text }} Prozent verringert."
|
||||
player_volume_control_mute: "Okay, die Spotify-Lautstärke wurde stummgeschaltet."
|
||||
player_volume_control_set_step_level: "Okay, die Spotify-Lautstärkeschrittweite ist {{ slots.player_volume_step_pct.text }} Prozent."
|
||||
player_volume_control_set_level: "Okay, die Spotify-Lautstärke wurde auf {{ slots.player_volume_level_pct.text }} Prozent eingestellt."
|
||||
player_volume_control_unmute: "Okay, die Spotify-Lautstärke wurde wiederhergestellt."
|
||||
player_volume_control_up: "Okay, die Spotify-Lautstärke wurde um {{ slots.player_volume_step_pct.text }} Prozent erhöht."
|
||||
playlist_created: "Okay, habe die Spotify-Playlist mit dem Namen \"{{ slots.playlist_title.text }}\" erstellt.\n\n[mehr Infos unter {{ slots.playlist_url.text }}]({{ slots.playlist_url.value }})"
|
||||
|
||||
# Allgemeine Spotify-Informationsnachrichten.
|
||||
spotify_no_artist_info: "Künstlerinformationen für \"{{ slots.artist_name.text }}\" konnten nicht gefunden werden."
|
||||
spotify_search_no_items_album: "Albuminformationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_audiobook: "Hörbuch-Informationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_playlist: "Playlist-Informationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_podcast: "Podcast-Informationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_podcast_episode: "Episodeninformationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_track: "Titelinformationen für \"{{ slots.criteria_arg_1.text }}\" konnten nicht gefunden werden. Suchkriterien: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_premium_required: "Entschuldigung, für die angeforderte Funktion mit dem Media Player \"{{ slots.target_player.text }}\" ist ein Spotify-Premium-Konto erforderlich."
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus generic responses.
|
||||
#
|
||||
# Note that the HA conversation integration does not understand this concept,
|
||||
# as this functionality is unique to OUR integration. With that said, you
|
||||
# must reload the integration (or restart HA) in order for changes to take
|
||||
# effect for these messages.
|
||||
# ============================================================================
|
||||
generic-response-1: "This is generic response message 1"
|
||||
generic-response-2: "This is generic response message 2"
|
||||
generic-response-3: "This is generic response message 3"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Home Assistant error responses.
|
||||
#
|
||||
# The values here will override HA built-in error responses.
|
||||
# They are purposefully designed to be as generic as possible, as they cover
|
||||
# multiple domain / platform types.
|
||||
# ============================================================================
|
||||
errors:
|
||||
|
||||
# Allgemeine Fehler.
|
||||
no_intent: "Entschuldigung, das habe ich nicht verstanden."
|
||||
handle_error: "Ein unerwarteter Fehler ist aufgetreten."
|
||||
|
||||
# Fehler bei der Intent-Zuordnung, wenn Ziele nicht aufgelöst werden können.
|
||||
no_area: "Entschuldigung, ich kenne keinen Bereich namens \"{{ area }}\"."
|
||||
no_floor: "Entschuldigung, ich kenne keine Etage namens \"{{ floor }}\"."
|
||||
no_domain: "Entschuldigung, ich kenne keine \"{{ domain.replace('_', ' ') }}\"."
|
||||
no_domain_in_area: "Entschuldigung, ich kenne keine \"{{ domain.replace('_', ' ') }}\" im Bereich \"{{ area }}\"."
|
||||
no_domain_in_floor: "Entschuldigung, ich kenne keine \"{{ domain.replace('_', ' ') }}\" auf der Etage \"{{ floor }}\"."
|
||||
no_device_class: "Entschuldigung, ich kenne keine \"{{ device_class }}\"."
|
||||
no_device_class_in_area: "Entschuldigung, ich kenne keine \"{{ device_class }}\" im Bereich \"{{ area }}\"."
|
||||
no_device_class_in_floor: "Entschuldigung, ich kenne keine \"{{ device_class }}\" auf der Etage \"{{ floor }}\"."
|
||||
no_entity: "Entschuldigung, ich kenne kein Gerät namens \"{{ entity }}\"."
|
||||
no_entity_in_area: "Entschuldigung, ich kenne kein Gerät namens \"{{ entity }}\" im Bereich \"{{ area }}\"."
|
||||
no_entity_in_floor: "Entschuldigung, ich kenne kein Gerät namens \"{{ entity }}\" auf der Etage \"{{ floor }}\"."
|
||||
entity_wrong_state: "Entschuldigung, kein Gerät ist {{ state | lower }}."
|
||||
feature_not_supported: "Entschuldigung, kein Gerät unterstützt die erforderlichen Funktionen."
|
||||
|
||||
# Fehler bei der Intent-Zuordnung, wenn Ziele aufgelöst werden können, aber nicht für den Sprachassistenten freigegeben sind.
|
||||
no_entity_exposed: "Entschuldigung, \"{{ entity }}\" ist nicht für den Sprachassistenten freigegeben."
|
||||
no_entity_in_area_exposed: "Entschuldigung, \"{{ entity }}\" im Bereich \"{{ area }}\" ist nicht für den Sprachassistenten freigegeben."
|
||||
no_entity_in_floor_exposed: "Entschuldigung, \"{{ entity }}\" auf der Etage \"{{ floor }}\" ist nicht für den Sprachassistenten freigegeben."
|
||||
no_domain_exposed: "Entschuldigung, keine \"{{ domain.replace('_', ' ') }}\" ist für den Sprachassistenten freigegeben."
|
||||
no_domain_in_area_exposed: "Entschuldigung, keine \"{{ domain.replace('_', ' ') }}\" im Bereich \"{{ area }}\" ist für den Sprachassistenten freigegeben."
|
||||
no_domain_in_floor_exposed: "Entschuldigung, keine \"{{ domain.replace('_', ' ') }}\" auf der Etage \"{{ floor }}\" ist für den Sprachassistenten freigegeben."
|
||||
no_device_class_exposed: "Entschuldigung, keine \"{{ device_class }}\" ist für den Sprachassistenten freigegeben."
|
||||
no_device_class_in_area_exposed: "Entschuldigung, keine \"{{ device_class }}\" im Bereich \"{{ area }}\" ist für den Sprachassistenten freigegeben."
|
||||
no_device_class_in_floor_exposed: "Entschuldigung, keine \"{{ device_class }}\" auf der Etage \"{{ floor }}\" ist für den Sprachassistenten freigegeben."
|
||||
|
||||
# Fehler bei der Intent-Zuordnung, wenn mehrere (freigegebene) Geräte denselben Namen haben.
|
||||
duplicate_entities: "Entschuldigung, es gibt mehrere Geräte namens \"{{ entity }}\"."
|
||||
duplicate_entities_in_area: "Entschuldigung, es gibt mehrere Geräte namens \"{{ entity }}\" im Bereich \"{{ area }}\"."
|
||||
duplicate_entities_in_floor: "Entschuldigung, es gibt mehrere Geräte namens \"{{ entity }}\" auf der Etage \"{{ floor }}\"."
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus gemeinsame Listen
|
||||
# ============================================================================
|
||||
lists:
|
||||
|
||||
freeform:
|
||||
wildcard: true
|
||||
album_name:
|
||||
wildcard: true
|
||||
artist_name:
|
||||
wildcard: true
|
||||
device_name:
|
||||
wildcard: true
|
||||
playlist_name:
|
||||
wildcard: true
|
||||
track_name:
|
||||
wildcard: true
|
||||
|
||||
favorite_operator:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "(hinzufügen|add)"
|
||||
out: "add"
|
||||
- in: "(entfernen|remove)"
|
||||
out: "remove"
|
||||
|
||||
latest_episode:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(latest|current) episode"
|
||||
out: "on"
|
||||
- in: ""
|
||||
out: "off"
|
||||
|
||||
player_repeat_mode:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "(an|ein|titel)"
|
||||
out: "track"
|
||||
- in: "(kontext|alle|liste)"
|
||||
out: "context"
|
||||
- in: "(aus|deaktiviert)"
|
||||
out: "off"
|
||||
|
||||
player_shuffle_mode:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "(an|ein|aktiviert)"
|
||||
out: "on"
|
||||
- in: "(aus|deaktiviert)"
|
||||
out: "off"
|
||||
|
||||
player_volume_level:
|
||||
values:
|
||||
- in: (max|maximum|höchste)
|
||||
out: 100
|
||||
- in: (min|minimum|niedrigste)
|
||||
out: 1
|
||||
|
||||
player_volume_level_pct:
|
||||
range:
|
||||
type: "percentage"
|
||||
from: 0
|
||||
to: 100
|
||||
|
||||
player_volume_step_pct:
|
||||
range:
|
||||
type: "percentage"
|
||||
from: 1
|
||||
to: 100
|
||||
|
||||
spotifyplus_media_type:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "album"
|
||||
out: "album"
|
||||
- in: "künstler"
|
||||
out: "artist"
|
||||
- in: "hörbuch"
|
||||
out: "audiobook"
|
||||
- in: "playlist"
|
||||
out: "playlist"
|
||||
- in: "(podcast|show) episode"
|
||||
out: "episode"
|
||||
- in: "(podcast|show)"
|
||||
out: "podcast"
|
||||
- in: "(titel|lied|song)"
|
||||
out: "track"
|
||||
|
||||
spotifyplus_player_deck_control:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "(pause|stopp)"
|
||||
out: "pause"
|
||||
- in: "(fortsetzen|start)"
|
||||
out: "resume"
|
||||
- in: "(neustart|von vorne)"
|
||||
out: "seek_start"
|
||||
- in: "(nächster|weiter)"
|
||||
out: "skip_next"
|
||||
- in: "(vorheriger|zurück)"
|
||||
out: "skip_previous"
|
||||
|
||||
spotifyplus_player_volume_control:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "stumm"
|
||||
out: "mute"
|
||||
- in: "laut"
|
||||
out: "unmute"
|
||||
- in: "leiser"
|
||||
out: "step_down"
|
||||
- in: "lauter"
|
||||
out: "step_up"
|
||||
- in: "lautstärke schritt auf {player_volume_step_pct} [prozent|%]"
|
||||
out: "set_step_level"
|
||||
- in: "lautstärke auf {player_volume_level_pct} [prozent|%]"
|
||||
out: "set_level"
|
||||
|
||||
spotifyplus_search_play_control_multi:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "episode"
|
||||
out: "podcast_episode"
|
||||
- in: "künstler album"
|
||||
out: "artist_album"
|
||||
- in: "künstler (titel|lied|song)"
|
||||
out: "artist_track"
|
||||
|
||||
spotifyplus_search_play_control_single:
|
||||
# out-Werte müssen englische Werte sein.
|
||||
values:
|
||||
- in: "album [{spotifyplus_album_names:criteria_arg_1}]"
|
||||
out: "album"
|
||||
- in: "hörbuch [{spotifyplus_audiobook_names:criteria_arg_1}]"
|
||||
out: "audiobook"
|
||||
- in: "playlist [{spotifyplus_playlist_names:criteria_arg_1}]"
|
||||
out: "playlist"
|
||||
- in: "(podcast|show) [{spotifyplus_podcast_names:criteria_arg_1}] [{latest_episode}]"
|
||||
out: "podcast"
|
||||
- in: "(beliebte songs|beliebte lieder|Favoriten Titel)"
|
||||
out: "favorite_tracks"
|
||||
- in: "(beliebte songs|beliebte lieder|Favoriten Titel) von künstler {spotifyplus_artist_names:criteria_arg_1}"
|
||||
out: "favorite_tracks_artist"
|
||||
- in: "(titel|lied|song) [{spotifyplus_track_names:criteria_arg_1}]"
|
||||
out: "track"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Album-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um Spotify-Alben zu identifizieren, die
|
||||
# bei einem gesprochenen Satz falsch interpretiert werden, oder wenn Sie ein
|
||||
# bestimmtes Album von mehreren Album-Versionen wünschen.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Album-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_album_names:
|
||||
values:
|
||||
- in: "Test Listeneintrag"
|
||||
out: "spotify:album:4guAwaniEAEQSW0NCpo4gm" # Journey, 2022 Remaster
|
||||
- in: "Escape"
|
||||
out: "spotify:album:4guAwaniEAEQSW0NCpo4gm" # Journey, 2022 Remaster
|
||||
- in: "Escape 1981"
|
||||
out: "spotify:album:0gHmMKZ9s3ztqeMfAzwxRL" # Journey, 1981 Live, Escape Tour
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Künstler-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um Spotify-Künstler zu identifizieren, die
|
||||
# bei einem gesprochenen Satz falsch interpretiert werden, oder wenn Sie einen
|
||||
# bestimmten Künstler von mehreren Künstler-Versionen wünschen.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Künstler-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_artist_names:
|
||||
values:
|
||||
- in: "Test Listeneintrag"
|
||||
out: "spotify:artist:6kFLnclYFc3gzpNt13wim5" # Seventh Day Slumber
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Hörbuch-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um Spotify-Hörbücher zu identifizieren, die
|
||||
# bei einem gesprochenen Satz falsch interpretiert werden, oder wenn Sie ein
|
||||
# bestimmtes Hörbuch von mehreren Hörbuch-Versionen wünschen.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Hörbuch-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_audiobook_names:
|
||||
values:
|
||||
- in: "Test Listeneintrag"
|
||||
out: "spotify:show:74aydHJKgYz3AIq3jjBSv1"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Gerätenamen-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird verwendet, um Spotify Connect-Gerätenamen zu identifizieren.
|
||||
# Diese sollten mit dem übereinstimmen, was im `source_list`-Attribut des Media Players steht.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: SpotifyPlus media_player source_list Eintrag.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_device_names:
|
||||
values:
|
||||
- in: "Büro"
|
||||
out: "Office Speaker"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Playlist-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um algorithmische Spotify-Playlists zu
|
||||
# identifizieren, die von der Spotify Web API seit dem 27.11.2024 nicht mehr
|
||||
# zurückgegeben werden können.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Playlist-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_playlist_names:
|
||||
values:
|
||||
- in: "Test Listeneintrag"
|
||||
out: "spotify:playlist:37i9dQZEVXcTh0pBxo9pZe"
|
||||
- in: "(Daily Mix|Tagesmix) (1|eins)"
|
||||
out: "spotify:playlist:37i9dQZF1E39vTG3GurFPW"
|
||||
- in: "(Daily Mix|Tagesmix) (2|zwei)"
|
||||
out: "spotify:playlist:37i9dQZF1E39DIjrju3A9t"
|
||||
- in: "(Daily Mix|Tagesmix) (3|drei)"
|
||||
out: "spotify:playlist:37i9dQZF1E36yv9pXdC1Mb"
|
||||
- in: "(Daily Mix|Tagesmix) (4|vier)"
|
||||
out: "spotify:playlist:37i9dQZF1E35Ejqg6yJW0p"
|
||||
- in: "(Daily Mix|Tagesmix) (5|fünf)"
|
||||
out: "spotify:playlist:37i9dQZF1E35nVVgp8iMex"
|
||||
- in: "Mix der Woche"
|
||||
out: "spotify:playlist:37i9dQZEVXcTh0pBxo9pZe"
|
||||
- in: "(DJ|deejay|dee jay)"
|
||||
out: "spotify:playlist:37i9dQZF1EYkqdzj48dyYq"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Podcast-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um Spotify-Podcasts zu identifizieren, die
|
||||
# bei einem gesprochenen Satz falsch interpretiert werden, oder wenn Sie einen
|
||||
# bestimmten Podcast von mehreren Podcast-Versionen wünschen.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Podcast-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_podcast_names:
|
||||
values:
|
||||
- in: "Test Listeneintrag"
|
||||
out: "spotify:show:6kAsbP8pxwaU2kPibKTuHE"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus sprachfreundliche Titel-Identifikatoren.
|
||||
#
|
||||
# Diese Liste wird hauptsächlich verwendet, um Spotify-Titel zu identifizieren, die
|
||||
# bei einem gesprochenen Satz falsch interpretiert werden, oder wenn Sie einen
|
||||
# bestimmten Titel von mehreren Titel-Versionen wünschen.
|
||||
#
|
||||
# in: Sprachfreundlicher Identifikator.
|
||||
# out: Spotify-Titel-URI-Wert.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_track_names:
|
||||
values:
|
||||
- in: "Test Listeintrag"
|
||||
out: "spotify:track:5ehcf6UL1TkwozB386cRAp" # Journey, 2024 Remaster, Don't Stop believin'
|
||||
- in: "Don't Stop Believing"
|
||||
out: "spotify:track:5ehcf6UL1TkwozB386cRAp" # Journey, 2024 Remaster
|
||||
- in: "Don't Stop Believing 1981"
|
||||
out: "spotify:track:3Ab9C0oz70wDyigunvnG5N" # Journey, 1981 Live, Escape Tour
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus gemeinsame Erweiterungsregeln
|
||||
# ============================================================================
|
||||
expansion_rules:
|
||||
in_spotifyplus_player_area: "in [dem|der|im] ({area}|bereich {area})"
|
||||
on_spotifyplus_player_name: "auf [dem] ({name} player|player {name})"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus gemeinsame zu ignorierende Wörter
|
||||
# ============================================================================
|
||||
skip_words:
|
||||
- "bitte"
|
||||
- "kannst du"
|
||||
- "könntest du"
|
||||
- "würdest du"
|
||||
- "für mich"
|
||||
- "ich möchte"
|
||||
- "ich will"
|
||||
- "der"
|
||||
- "die"
|
||||
- "das"
|
||||
- "den"
|
||||
- "dem"
|
||||
- "des"
|
||||
- "ein"
|
||||
- "eine"
|
||||
- "einen"
|
||||
- "einem"
|
||||
- "eines"
|
||||
@@ -0,0 +1,85 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusFavoriteAddRemove
|
||||
# Location: custom_sentences/en/spotifyplus_FavoriteAddRemove_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusFavoriteAddRemove:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites)"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "add spotify track to favorites"
|
||||
# - "add spotify track favorite"
|
||||
# - "remove spotify track from favorites"
|
||||
# - "remove spotify track favorite"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default player to use.
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites) <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "add spotify track favorite for player spotify premium"
|
||||
# - "add spotify track favorite for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "{favorite_operator} spotify {spotifyplus_media_type} [to|2|from] (favorite|favorites) <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "add spotify track favorite in the Office area"
|
||||
# - "add spotify track favorite in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
is_public: False
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusFavoriteAddRemove:
|
||||
|
||||
default: "" # not used - see common `favorite_add_remove_xxxxxxx_ok` messages.
|
||||
@@ -0,0 +1,95 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusGetInfoArtistBio
|
||||
# Location: custom_sentences/en/spotifyplus_GetInfoArtistBio_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetInfoArtistBio:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify artist (info|information|details|bio) [for|4] {artist_name}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify artist info for Phil Wickham"
|
||||
# - "get spotify artist info for Phil Wickham"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify artist (info|information|details|bio) [for|4] {artist_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "get spotify artist info for Phil Wickham on player spotify premium"
|
||||
# - "get spotify artist info for Phil Wickham on spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify artist (info|information|details|bio) [for|4] {artist_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "get spotify artist info for Phil Wickham in the Office area"
|
||||
# - "get spotify artist info for Phil Wickham in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
target_player: ""
|
||||
artist_bio: ""
|
||||
artist_name: ""
|
||||
artist_title: ""
|
||||
artist_url: ""
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetInfoArtistBio:
|
||||
|
||||
default: "" # not used - see common `get_info_artist_bio` message.
|
||||
@@ -0,0 +1,100 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusGetNowPlayingInfo
|
||||
# Location: custom_sentences/en/spotifyplus_GetNowPlayingInfo_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetNowPlayingInfo:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify [nowplaying|now playing] {spotifyplus_media_type} (info|information|details)"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify nowplaying album info"
|
||||
# - "spotify album info"
|
||||
# - "get spotify album info"
|
||||
# - "spotify nowplaying artist info"
|
||||
# - "spotify artist info"
|
||||
# - "get spotify artist info"
|
||||
# - "spotify nowplaying audiobook info"
|
||||
# - "spotify audiobook info"
|
||||
# - "get spotify audiobook info"
|
||||
# - "spotify nowplaying playlist info"
|
||||
# - "spotify playlist info"
|
||||
# - "get spotify playlist info"
|
||||
# - "spotify nowplaying podcast episode info"
|
||||
# - "spotify podcast episode info"
|
||||
# - "get spotify podcast episode info"
|
||||
# - "spotify nowplaying podcast info"
|
||||
# - "spotify podcast info"
|
||||
# - "get spotify podcast info"
|
||||
# - "spotify nowplaying track info"
|
||||
# - "spotify track info"
|
||||
# - "get spotify track info"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default player to use.
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify [nowplaying|now playing] {spotifyplus_media_type} (info|information|details) <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "get spotify artist info for player spotify premium"
|
||||
# - "get spotify artist info for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[get] spotify [nowplaying|now playing] {spotifyplus_media_type} (info|information|details) <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "get spotify artist info in the Office area"
|
||||
# - "get spotify artist info in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusGetNowPlayingInfo:
|
||||
|
||||
default: "" # not used - see common `nowplaying_info_xxxxxxx` messages.
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlayerDeckControl
|
||||
# Location: custom_sentences/en/spotifyplus_PlayerDeckControl_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerDeckControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - No Player Name__
|
||||
# - "spotify pause"
|
||||
# - "spotify pause track"
|
||||
# - "spotify stop"
|
||||
# - "spotify stop track"
|
||||
# - "spotify resume"
|
||||
# - "spotify resume play"
|
||||
# - "spotify resume player"
|
||||
# - "spotify resume track"
|
||||
# - "spotify start"
|
||||
# - "spotify start track"
|
||||
# - "spotify start player"
|
||||
# - "spotify start play"
|
||||
# - "spotify next track"
|
||||
# - "spotify play next track"
|
||||
# - "spotify play the next track"
|
||||
# - "spotify skip to next track"
|
||||
# - "spotify skip track"
|
||||
# - "spotify previous track"
|
||||
# - "spotify play previous track"
|
||||
# - "spotify play the previous track"
|
||||
# - "spotify skip to previous track"
|
||||
# - "spotify restart track"
|
||||
# - "spotify skip to start of track"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default player to use.
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify pause track on player spotify premium"
|
||||
# - "spotify pause track on spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify {spotifyplus_player_deck_control} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify pause track in the Office area"
|
||||
# - "spotify pause track in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerDeckControl:
|
||||
|
||||
default: "" # not used - see common `player_deck_control_xxxxxxx` messages.
|
||||
@@ -0,0 +1,97 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlayerSetRepeatMode
|
||||
# Location: custom_sentences/en/spotifyplus_PlayerSetRepeatMode_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetRepeatMode:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify repeat [mode] {player_repeat_mode}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify repeat on"
|
||||
# - "spotify repeat track"
|
||||
# - "spotify repeat 1 track"
|
||||
# - "spotify repeat single track"
|
||||
# - "spotify repeat context"
|
||||
# - "spotify repeat all"
|
||||
# - "spotify repeat all tracks"
|
||||
# - "spotify repeat list"
|
||||
# - "spotify repeat off"
|
||||
# - "spotify repeat disabled"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify repeat [mode] {player_repeat_mode} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify repeat track for player spotify premium"
|
||||
# - "spotify repeat track for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify repeat [mode] {player_repeat_mode} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify repeat track in the Office area"
|
||||
# - "spotify repeat track in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_repeat_mode: "off"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetRepeatMode:
|
||||
|
||||
default_by_name: "Okay, Spotify repeat mode is \"{{ slots.player_repeat_mode.text }}\" for named player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, Spotify repeat mode is \"{{ slots.player_repeat_mode.text }}\" in the \"{{ slots.target_player.text }}\" area."
|
||||
default_no_player: "Okay, Spotify repeat mode is \"{{ slots.player_repeat_mode.text }}\" for default player \"{{ slots.target_player.text }}\"."
|
||||
default: "Okay, Spotify repeat mode is \"{{ slots.player_repeat_mode.text }}\"."
|
||||
@@ -0,0 +1,94 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlayerSetShuffleMode
|
||||
# Location: custom_sentences/en/spotifyplus_PlayerSetShuffleMode_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetShuffleMode:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify shuffle [mode] {player_shuffle_mode}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify shuffle mode on"
|
||||
# - "spotify shuffle mode off"
|
||||
# - "spotify shuffle on"
|
||||
# - "spotify shuffle off"
|
||||
# - "spotify shuffle enabled"
|
||||
# - "spotify shuffle disabled"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify shuffle [mode] {player_shuffle_mode} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify shuffle on for player spotify premium"
|
||||
# - "spotify shuffle on for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify shuffle [mode] {player_shuffle_mode} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify shuffle on in the Office area"
|
||||
# - "spotify shuffle on in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerSetShuffleMode:
|
||||
|
||||
default_no_player: "Okay, Spotify shuffle mode is \"{{ slots.player_shuffle_mode.text }}\" for default player \"{{ slots.target_player.text }}\"."
|
||||
default_by_name: "Okay, Spotify shuffle mode is \"{{ slots.player_shuffle_mode.text }}\" for named player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, Spotify shuffle mode is \"{{ slots.player_shuffle_mode.text }}\" in the \"{{ slots.target_player.text }}\" area."
|
||||
default: "Okay, Spotify shuffle mode is \"{{ slots.player_shuffle_mode.text }}\"."
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlayerTransferPlayback
|
||||
# Location: custom_sentences/en/spotifyplus_PlayerTransferPlayback_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerTransferPlayback:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify transfer playback [to] [device] {spotifyplus_device_names:device_name}"
|
||||
- "spotify transfer playback [to] [device] {device_name}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify transfer playback to device Bose-ST10-1"
|
||||
# - "spotify transfer playback to Bose-ST10-1"
|
||||
# - "spotify transfer playback Bose-ST10-1"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
target_player: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_no_player
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify transfer playback [to] [device] {spotifyplus_device_names:device_name} <on_spotifyplus_player_name>"
|
||||
- "spotify transfer playback [to] [device] {device_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify transfer playback to Bose-ST10-1 for player spotify premium"
|
||||
# - "spotify transfer playback to Bose-ST10-1 for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_by_name
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify transfer playback [to] [device] {spotifyplus_device_names:device_name} <in_spotifyplus_player_area>"
|
||||
- "spotify transfer playback [to] [device] {device_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify transfer playback to Bose-ST10-1 in the Office area"
|
||||
# - "spotify transfer playback to Bose-ST10-1 in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
|
||||
response: default_by_area
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerTransferPlayback:
|
||||
|
||||
default_no_player: "Okay, playback transferred to device \"{{ slots.device_name.text }}\", for default player \"{{ slots.target_player.text }}\"."
|
||||
default_by_name: "Okay, playback transferred to device \"{{ slots.device_name.text }}\", for named player \"{{ slots.target_player.text }}\"."
|
||||
default_by_area: "Okay, playback transferred to device \"{{ slots.device_name.text }}\", in the \"{{ slots.target_player.text }}\" area."
|
||||
default: "Okay, playback transferred to device \"{{ slots.device_name.text }}\"."
|
||||
@@ -0,0 +1,123 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlayerVolumeControl
|
||||
# Location: custom_sentences/en/spotifyplus_PlayerVolumeControl_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerVolumeControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[set] spotify {spotifyplus_player_volume_control}"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - No Player Name__
|
||||
# - "spotify player volume level down"
|
||||
# - "spotify player volume level decrease"
|
||||
# - "spotify volume level decrease"
|
||||
# - "spotify volume decrease"
|
||||
# - "spotify volume down"
|
||||
|
||||
# - "spotify player volume level up"
|
||||
# - "spotify player volume level increase"
|
||||
# - "spotify volume level increase"
|
||||
# - "spotify volume increase"
|
||||
# - "spotify volume up"
|
||||
|
||||
# - "spotify player volume mute"
|
||||
# - "spotify player volume off"
|
||||
# - "spotify volume mute"
|
||||
# - "spotify volume off"
|
||||
|
||||
# - "spotify player volume unmute"
|
||||
# - "spotify player volume on"
|
||||
# - "spotify player volume restore"
|
||||
# - "spotify volume unmute"
|
||||
# - "spotify volume on"
|
||||
# - "spotify volume restore"
|
||||
|
||||
# - "spotify volume step level to 5 percent"
|
||||
# - "spotify volume step 5 percent"
|
||||
# - "spotify volume step 5"
|
||||
# - "set spotify volume step level to 5 percent"
|
||||
|
||||
# - "spotify volume level 5 percent"
|
||||
# - "spotify volume level 5"
|
||||
# - "spotify volume 5"
|
||||
# - "set spotify volume level to 5 percent"
|
||||
# - "set spotify volume level 5"
|
||||
# - "set spotify volume 5"
|
||||
# - "set spotify player volume level to 5 percent"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default player to use.
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[set] spotify {spotifyplus_player_volume_control} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify volume decrease for player spotify premium"
|
||||
# - "spotify volume decrease for spotify premium player"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "[set] spotify {spotifyplus_player_volume_control} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify volume decrease in the Office area"
|
||||
# - "spotify volume decrease in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
player_volume_level_pct: 10
|
||||
player_volume_step_pct: 10
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlayerVolumeControl:
|
||||
|
||||
default: "" # not used - see common `player_volume_control_xxxxxxx` messages.
|
||||
@@ -0,0 +1,101 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusPlaylistCreate
|
||||
# Location: custom_sentences/en/spotifyplus_PlaylistCreate_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlaylistCreate:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify create (playlist|play list) {playlist_name}" # freeform playlist text.
|
||||
- "create spotify (playlist|play list) {playlist_name}" # freeform playlist text.
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify create playlist My New Playlist"
|
||||
# - "create spotify playlist My New Playlist"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Created by Home Assistant Voice Assist intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # example: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify create (playlist|play list) {playlist_name} <on_spotifyplus_player_name>"
|
||||
- "create spotify (playlist|play list) {playlist_name} <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify create playlist My New Playlist on player spotify premium"
|
||||
# - "create spotify playlist My New Playlist on player spotify premium"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Created by Home Assistant Voice Assist intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # example: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
- "spotify create (playlist|play list) {playlist_name} <in_spotifyplus_player_area>"
|
||||
- "create spotify (playlist|play list) {playlist_name} <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify create playlist My New Playlist in the Office area"
|
||||
# - "create spotify playlist My New Playlist in the Office"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
target_player: ""
|
||||
playlist_name: ""
|
||||
description: "Created by Home Assistant Voice Assist intent."
|
||||
is_collaborative: False
|
||||
is_public: False
|
||||
image_path: "" # example: "www/spotifyplus/spotify_playlist_default_image.png"
|
||||
|
||||
response: default
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusPlaylistCreate:
|
||||
|
||||
default: "Okay, created Spotify playlist named \"{{ slots.playlist_title.text }}\"."
|
||||
@@ -0,0 +1,167 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent custom sentence and response definitions.
|
||||
#
|
||||
# Language: en, English
|
||||
# Intent: SpotifyPlusSearchPlayControl
|
||||
# Location: custom_sentences/en/spotifyplus_SearchPlayControl_en.yaml
|
||||
# ============================================================================
|
||||
language: en
|
||||
intents:
|
||||
|
||||
SpotifyPlusSearchPlayControl:
|
||||
|
||||
data:
|
||||
|
||||
- sentences:
|
||||
|
||||
# this sentence template should be used when multiple terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2}"
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2}"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2}"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2}"
|
||||
|
||||
# this sentence template should be used when single terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_single}"
|
||||
- "spotify play {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1}"
|
||||
- "spotify play {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}]"
|
||||
- "play spotify {spotifyplus_search_play_control_single}"
|
||||
- "play spotify {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1}"
|
||||
- "play spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}]"
|
||||
|
||||
# Base sentence phrases for this intent.
|
||||
# This sentence is recognized when a name / area is NOT provided in the spoken command.
|
||||
# In this case, the first active SpotifyPlus media player is used.
|
||||
|
||||
# __Example Phrases - Default Player__
|
||||
# - "spotify play artist album Take Everything by artist Seventh Day Slumber"
|
||||
# - "play spotify artist album Take Everything by artist Seventh Day Slumber"
|
||||
|
||||
# - "spotify play artist track I Need You by artist Seventh Day Slumber"
|
||||
# - "play spotify artist track I Need You by artist Seventh Day Slumber"
|
||||
|
||||
# - "spotify play episode Kurt Russell for podcast Armchair Expert with Dax Shepard"
|
||||
# - "play spotify episode Kurt Russell for podcast Armchair Expert with Dax Shepard"
|
||||
|
||||
# - "spotify play favorite tracks for artist test artist list entry"
|
||||
# - "play spotify favorite tracks for artist test artist list entry"
|
||||
# - "spotify play favorite tracks for artist Jeremy Camp"
|
||||
# - "play spotify favorite tracks for artist Jeremy Camp"
|
||||
# - "spotify play favorite tracks"
|
||||
# - "play spotify favorite tracks"
|
||||
|
||||
# - "spotify play audiobook Test Audiobook List Entry"
|
||||
# - "play spotify audiobook Test Audiobook List Entry"
|
||||
# - "spotify play audiobook The Elfstones of Shannara"
|
||||
# - "play spotify audiobook The Elfstones of Shannara"
|
||||
|
||||
# - "spotify play playlist Test Playlist List Entry"
|
||||
# - "play spotify playlist Test Playlist List Entry"
|
||||
# - "spotify play playlist Old Christian Contemporary"
|
||||
# - "play spotify playlist Old Christian Contemporary"
|
||||
|
||||
# - "spotify play podcast Test Podcast List Entry"
|
||||
# - "play spotify podcast Test Podcast List Entry"
|
||||
# - "spotify play podcast Armchair Expert with Dax Shepard"
|
||||
# - "play spotify podcast Armchair Expert with Dax Shepard"
|
||||
# - "spotify play podcast Armchair Expert with Dax Shepard latest episode"
|
||||
# - "play spotify podcast Armchair Expert with Dax Shepard latest episode"
|
||||
|
||||
# - "spotify play track Test Track List Entry"
|
||||
# - "play spotify track Test Track List Entry"
|
||||
# - "spotify play track More Of You"
|
||||
# - "play spotify track More Of You"
|
||||
|
||||
slots:
|
||||
|
||||
name: "Spotify Premium" # default media player alias.
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
# this sentence template should be used when multiple terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2} <on_spotifyplus_player_name>"
|
||||
|
||||
# this sentence template should be used when single terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_single} <on_spotifyplus_player_name>"
|
||||
- "spotify play {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1} <on_spotifyplus_player_name>"
|
||||
- "spotify play {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <on_spotifyplus_player_name>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} <on_spotifyplus_player_name>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1} <on_spotifyplus_player_name>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <on_spotifyplus_player_name>"
|
||||
|
||||
# This sentence is recognized when a player name is provided in the spoken
|
||||
# command (e.g. "on player Spotify Premium", "on Todds player", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Name__
|
||||
# - "spotify play artist track I Need You by artist Seventh Day Slumber on player spotify premium"
|
||||
# - "spotify play track Test Track List Entry on player spotify premium"
|
||||
# - "spotify play track More Of You on player spotify premium"
|
||||
|
||||
slots:
|
||||
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
- sentences:
|
||||
|
||||
# this sentence template should be used when multiple terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "spotify play {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) (podcast|pod cast|show) {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
- "play spotify {spotifyplus_search_play_control_multi} {freeform:criteria_arg_1} (by|for) artist {freeform:criteria_arg_2} <in_spotifyplus_player_area>"
|
||||
|
||||
# this sentence template should be used when single terms are required.
|
||||
- "spotify play {spotifyplus_search_play_control_single} <in_spotifyplus_player_area>"
|
||||
- "spotify play {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1} <in_spotifyplus_player_area>"
|
||||
- "spotify play {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <in_spotifyplus_player_area>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} <in_spotifyplus_player_area>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} (by|for) artist {freeform:criteria_arg_1} <in_spotifyplus_player_area>"
|
||||
- "play spotify {spotifyplus_search_play_control_single} {freeform:criteria_arg_1} [{latest_episode}] <in_spotifyplus_player_area>"
|
||||
|
||||
# This sentence is recognized when a player area is provided in the spoken
|
||||
# command (e.g. "in the Living Room area", "in the Living Room", etc).
|
||||
# Supports the same base sentence phrases for this intent.
|
||||
|
||||
# __Example Phrases - By Player Area__
|
||||
# - "spotify play artist track I Need You by artist Seventh Day Slumber in the Office area"
|
||||
# - "spotify play track Test Track List Entry in the Office area"
|
||||
# - "spotify play track More Of You in the Office area"
|
||||
|
||||
slots:
|
||||
|
||||
area: ""
|
||||
name: ""
|
||||
delay: 0.50
|
||||
device_name: "*"
|
||||
latest_episode: "off"
|
||||
limit_total: 200
|
||||
player_shuffle_mode: "off"
|
||||
|
||||
response: default
|
||||
|
||||
# ============================================================================
|
||||
# Custom response definitions for THIS intent.
|
||||
# Note - Shared responses for all intents are stored in the Common file.
|
||||
# ============================================================================
|
||||
responses:
|
||||
intents:
|
||||
|
||||
SpotifyPlusSearchPlayControl:
|
||||
|
||||
default: "" # not used - see common `play_xxxxx` message.
|
||||
@@ -0,0 +1,461 @@
|
||||
# ============================================================================
|
||||
# SpotifyPlus integration intent common responses, lists, expand rules, etc.
|
||||
#
|
||||
# Location: custom_sentences/en/spotifyplus_common_en.yaml
|
||||
# Language: en, English
|
||||
# ============================================================================
|
||||
language: en
|
||||
responses:
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus platform responses.
|
||||
#
|
||||
# Note that the HA conversation integration does not understand this concept,
|
||||
# as this functionality is unique to OUR integration. With that said, you
|
||||
# must reload the integration (or restart HA) in order for changes to take
|
||||
# effect for these messages.
|
||||
# ============================================================================
|
||||
spotifyplus:
|
||||
|
||||
# general error messages.
|
||||
error_unhandled: "An unexpected error occurred:\n\n{{ slots.error_info.text }}"
|
||||
error_failed_to_handle: "Failed to handle intent:\n\n{{ slots.error_info.text }}"
|
||||
error_media_type_invalid: "Media type value \"{{ slots.spotifyplus_media_type.text }}\" ({{ slots.spotifyplus_media_type.value }}) was not recognized."
|
||||
error_player_deck_control_invalid: "Player deck control value \"{{ slots.spotifyplus_player_deck_control.text }}\" ({{ slots.spotifyplus_player_deck_control.value }}) was not recognized."
|
||||
error_player_volume_control_invalid: "Player volume control value \"{{ slots.spotifyplus_player_volume_control.text }}\" ({{ slots.spotifyplus_player_volume_control.value }}) was not recognized."
|
||||
error_search_play_control_invalid: "Search play control value \"{{ slots.spotifyplus_search_play_control.text }}\" ({{ slots.spotifyplus_search_play_control.value }}) was not recognized."
|
||||
error_search_no_criteria: "Search criteria not specified for search play control type \"{{ slots.spotifyplus_search_play_control.text }}\" ({{ slots.spotifyplus_search_play_control.value }})."
|
||||
|
||||
# get information request related messages.
|
||||
get_info_artist_bio: "Artist bio for \"{{ slots.artist_title.text }}\" ... \n\n{{ slots.artist_bio.text | truncate(400, end=' ...') }}\n\n[more info at {{ slots.artist_url.text }}]({{ slots.artist_url.value }})"
|
||||
|
||||
# favorites management related messages.
|
||||
favorite_operation_invalid: "Favorite operation type was invalid: \"{{ slots.favorite_operator.text }}\"."
|
||||
favorite_add_remove_album_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify album favorite \"{{ slots.album_title.text }}\", by artist \"{{ slots.artist_title.text }}\", was successful."
|
||||
favorite_add_remove_artist_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify artist favorite \"{{ slots.artist_title.text }}\", was successful."
|
||||
favorite_add_remove_audiobook_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify audiobook favorite \"{{ slots.audiobook_title.text }}\", by author \"{{ slots.author_title.text }}\", was successful."
|
||||
favorite_add_remove_playlist_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify playlist favorite \"{{ slots.playlist_title.text }}\", was successful."
|
||||
favorite_add_remove_podcast_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify podcast favorite \"{{ slots.podcast_title.text }}\", was successful."
|
||||
favorite_add_remove_podcast_episode_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify episode favorite \"{{ slots.episode_title.text }}\", from podcast \"{{ slots.podcast_title.text }}\", was successful."
|
||||
favorite_add_remove_track_ok: "Okay, {{ slots.favorite_operator.text }} of Spotify track favorite \"{{ slots.track_title.text }}\", by artist \"{{ slots.artist_title.text }}\", was successful."
|
||||
|
||||
# now playing information related messages.
|
||||
nowplaying_info_album: "Media player is playing a track from album \"{{ slots.album_title.text }}\", by artist \"{{ slots.artist_title.text }}\".\n\n[more info at {{ slots.album_url.text }}]({{ slots.album_url.value }})"
|
||||
nowplaying_info_audiobook: "Media player is playing \"{{ slots.chapter_title.text }}\", from audiobook \"{{ slots.audiobook_title.text }}\", by author \"{{ slots.author_title.text }}\".\n\n[more info at {{ slots.audiobook_url.text }}]({{ slots.audiobook_url.value }})"
|
||||
nowplaying_info_playlist: "Media player is playing a track from playlist \"{{ slots.playlist_title.text }}\".\n\n[more info at {{ slots.playlist_url.text }}]({{ slots.playlist_url.value }})"
|
||||
nowplaying_info_podcast: "Media player is playing episode \"{{ slots.episode_title.text }}\", from podcast \"{{ slots.podcast_title.text }}\".\n\n[more info at {{ slots.podcast_url.text }}]({{ slots.podcast_url.value }})"
|
||||
nowplaying_info_podcast_episode: "Media player is playing episode \"{{ slots.episode_title.text }}\", from podcast \"{{ slots.podcast_title.text }}\".\n\n[more info at {{ slots.episode_url.text }}]({{ slots.episode_url.value }})"
|
||||
nowplaying_info_track: "Media player is playing track \"{{ slots.track_title.text }}\", from album \"{{ slots.album_title.text }}\", by artist \"{{ slots.artist_title.text }}\".\n\n[more info at {{ slots.album_url.text }}]({{ slots.album_url.value }})"
|
||||
nowplaying_no_media_album: "SpotifyPlus media player is currently not playing media that contains Track Album information."
|
||||
nowplaying_no_media_artist: "SpotifyPlus media player is currently not playing media that contains Track Artist information."
|
||||
nowplaying_no_media_audiobook: "SpotifyPlus media player is currently not playing media that contains Audiobook information."
|
||||
nowplaying_no_media_playlist: "SpotifyPlus media player is currently not playing media that contains Playlist information."
|
||||
nowplaying_no_media_podcast: "SpotifyPlus media player is currently not playing media that contains Podcast information."
|
||||
nowplaying_no_media_podcast_episode: "SpotifyPlus media player is currently not playing media that contains Podcast Episode information."
|
||||
nowplaying_no_media_track: "SpotifyPlus media player is currently not playing media that contains Track information."
|
||||
|
||||
# media player related messages.
|
||||
play_album_with_artist: "Playing Spotify album \"{{ slots.album_title.text }}\", by artist \"{{ slots.artist_title.text }}\"."
|
||||
play_album: "Playing Spotify album \"{{ slots.album_title.text }}\"."
|
||||
play_audiobook: "Playing Spotify audiobook \"{{ slots.audiobook_title.text }}\"."
|
||||
play_audiobook_with_author: "Playing Spotify audiobook \"{{ slots.audiobook_title.text }}\", by author \"{{ slots.author_title.text }}\"."
|
||||
play_favorite_tracks: "Playing Spotify track favorites."
|
||||
play_favorite_tracks_for_artist: "Playing Spotify track favorites, for artist \"{{ slots.artist_title.text }}\"."
|
||||
play_playlist: "Playing Spotify playlist \"{{ slots.playlist_title.text }}\"."
|
||||
play_podcast: "Playing Spotify podcast \"{{ slots.podcast_title.text }}\"."
|
||||
play_podcast_episode: "Playing Spotify episode \"{{ slots.episode_title.text }}\", from podcast \"{{ slots.podcast_title.text }}\"."
|
||||
play_track_with_artist: "Playing Spotify track \"{{ slots.track_title.text }}\", by artist \"{{ slots.artist_title.text }}\"."
|
||||
play_track: "Playing Spotify track \"{{ slots.track_title.text }}\"."
|
||||
player_already_playing_media: "Media player \"{{ slots.target_player.text }}\" is already playing media."
|
||||
player_deck_control_pause: "Okay, Spotify paused the track."
|
||||
player_deck_control_resume: "Okay, Spotify resumed play."
|
||||
player_deck_control_seek_start: "Okay, Spotify restarted the track."
|
||||
player_deck_control_skip_next: "Okay, Spotify skipped to next track."
|
||||
player_deck_control_skip_previous: "Okay, Spotify skipped to previous track."
|
||||
player_features_not_supported: "Media player \"{{ slots.name.text }}\" does not support required features for this request: \"{{ slots.error_features.text }}\"."
|
||||
player_not_exposed_to_voice: "Media player \"{{ slots.target_player.text }}\" has not been exposed to Voice Assistant."
|
||||
player_not_matched: "SpotifyPlus media player could not be resolved by \"{{ slots.target_player.text }}\"."
|
||||
player_not_matched_area: "SpotifyPlus media player could not be resolved for the \"{{ slots.area.text }}\" area."
|
||||
player_not_playing_media: "Media player \"{{ slots.target_player.text }}\" is currently not playing media."
|
||||
player_not_spotifyplus: "Resolved media player \"{{ slots.target_player.text }}\" is not a SpotifyPlus media player."
|
||||
player_volume_control_down: "Okay, Spotify volume level was decreased by {{ slots.player_volume_step_pct.text }} percent."
|
||||
player_volume_control_mute: "Okay, Spotify volume level was muted."
|
||||
player_volume_control_set_step_level: "Okay, Spotify volume step level is {{ slots.player_volume_step_pct.text }} percent."
|
||||
player_volume_control_set_level: "Okay, Spotify volume level set to {{ slots.player_volume_level_pct.text }} percent."
|
||||
player_volume_control_unmute: "Okay, Spotify volume level was restored."
|
||||
player_volume_control_up: "Okay, Spotify volume level was increased by {{ slots.player_volume_step_pct.text }} percent."
|
||||
playlist_created: "Okay, created Spotify playlist named \"{{ slots.playlist_title.text }}\".\n\n[more info at {{ slots.playlist_url.text }}]({{ slots.playlist_url.value }})"
|
||||
|
||||
# general spotify information related messages.
|
||||
spotify_no_artist_info: "Artist information could not be found for \"{{ slots.artist_name.text }}\"."
|
||||
spotify_search_no_items_album: "Album information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_audiobook: "Audiobook information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_playlist: "Playlist information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_podcast: "Podcast information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_podcast_episode: "Episode information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_search_no_items_track: "Track information could not be found for \"{{ slots.criteria_arg_1.text }}\". Search criteria: \"{{ slots.search_criteria.text }}\"."
|
||||
spotify_premium_required: "Sorry, a Spotify premium account is required for the requested function using media player \"{{ slots.target_player.text }}\"."
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus generic responses.
|
||||
#
|
||||
# Note that the HA conversation integration does not understand this concept,
|
||||
# as this functionality is unique to OUR integration. With that said, you
|
||||
# must reload the integration (or restart HA) in order for changes to take
|
||||
# effect for these messages.
|
||||
# ============================================================================
|
||||
generic-response-1: "This is generic response message 1"
|
||||
generic-response-2: "This is generic response message 2"
|
||||
generic-response-3: "This is generic response message 3"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Home Assistant error responses.
|
||||
#
|
||||
# The values here will override HA built-in error responses.
|
||||
# They are purposefully designed to be as generic as possible, as they cover
|
||||
# multiple domain / platform types.
|
||||
# ============================================================================
|
||||
errors:
|
||||
|
||||
# general errors.
|
||||
no_intent: "Sorry, I couldn't understand that"
|
||||
handle_error: "An unexpected error occurred"
|
||||
|
||||
# intent matching contraint errors used when target(s) cannot be resolved.
|
||||
no_area: "Sorry, I am not aware of any area called \"{{ area }}\"."
|
||||
no_floor: "Sorry, I am not aware of any floor called \"{{ floor }}\"."
|
||||
no_domain: "Sorry, I am not aware of any \"{{ domain.replace('_', ' ') }}\"."
|
||||
no_domain_in_area: "Sorry, I am not aware of any \"{{ domain.replace('_', ' ') }}\" in the \"{{ area }}\" area."
|
||||
no_domain_in_floor: "Sorry, I am not aware of any \"{{ domain.replace('_', ' ') }}\" on the \"{{ floor }}\" floor."
|
||||
no_device_class: "Sorry, I am not aware of any \"{{ device_class }}\"."
|
||||
no_device_class_in_area: "Sorry, I am not aware of any \"{{ device_class }}\" in the \"{{ area }}\" area."
|
||||
no_device_class_in_floor: "Sorry, I am not aware of any \"{{ device_class }}\" in the \"{{ floor }}\" floor."
|
||||
no_entity: "Sorry, I am not aware of any device called \"{{ entity }}\"."
|
||||
no_entity_in_area: "Sorry, I am not aware of any device called \"{{ entity }}\" in the \"{{ area }}\" area."
|
||||
no_entity_in_floor: "Sorry, I am not aware of any device called \"{{ entity }}\" in the \"{{ floor }}\" floor."
|
||||
entity_wrong_state: "Sorry, no device is {{ state | lower }}."
|
||||
feature_not_supported: "Sorry, no device supports the required features."
|
||||
|
||||
# intent matching contraint errors used when target(s) can be resolved, but are not exposed to voice assistant.
|
||||
no_entity_exposed: "Sorry, \"{{ entity }}\" is not exposed to Voice Assistant."
|
||||
no_entity_in_area_exposed: "Sorry, \"{{ entity }}\" in the \"{{ area }}\" area is not exposed to Voice Assistant."
|
||||
no_entity_in_floor_exposed: "Sorry, \"{{ entity }}\" in the \"{{ floor }}\" floor is not exposed to Voice Assistant."
|
||||
no_domain_exposed: "Sorry, no \"{{ domain.replace('_', ' ') }}\" is exposed to Voice Assistant."
|
||||
no_domain_in_area_exposed: "Sorry, no \"{{ domain.replace('_', ' ') }}\" in the \"{{ area }}\" area is exposed to Voice Assistant."
|
||||
no_domain_in_floor_exposed: "Sorry, no \"{{ domain.replace('_', ' ') }}\" in the \"{{ floor }}\" floor is exposed to Voice Assistant."
|
||||
no_device_class_exposed: "Sorry, no \"{{ device_class }}\" is exposed to Voice Assistant."
|
||||
no_device_class_in_area_exposed: "Sorry, no \"{{ device_class }}\" in the \"{{ area }}\" area is exposed to Voice Assistant."
|
||||
no_device_class_in_floor_exposed: "Sorry, no \"{{ device_class }}\" in the \"{{ floor }}\" floor is exposed to Voice Assistant."
|
||||
|
||||
# intent matching contraint errors used when multiple (exposed) devices have the same name.
|
||||
duplicate_entities: "Sorry, there are multiple devices called \"{{ entity }}\"."
|
||||
duplicate_entities_in_area: "Sorry, there are multiple devices called \"{{ entity }}\" in the \"{{ area }}\" area."
|
||||
duplicate_entities_in_floor: "Sorry, there are multiple devices called \"{{ entity }}\" in the \"{{ floor }}\" floor."
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus common lists
|
||||
# ============================================================================
|
||||
lists:
|
||||
|
||||
freeform:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
album_name:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
artist_name:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
device_name:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
playlist_name:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
track_name:
|
||||
wildcard: true # accept freeform text.
|
||||
|
||||
favorite_operator:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "add"
|
||||
out: "add"
|
||||
- in: "remove"
|
||||
out: "remove"
|
||||
|
||||
latest_episode:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(latest|current) episode"
|
||||
out: "on"
|
||||
- in: ""
|
||||
out: "off"
|
||||
|
||||
player_repeat_mode:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(on|[single|1|one] track|)"
|
||||
out: "track"
|
||||
- in: "(context|all|all tracks|list)"
|
||||
out: "context"
|
||||
- in: "(off|disable[d])"
|
||||
out: "off"
|
||||
|
||||
player_shuffle_mode:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(on|true|enable[d])"
|
||||
out: "on"
|
||||
- in: "(off|false|disable[d])"
|
||||
out: "off"
|
||||
|
||||
player_volume_level:
|
||||
values:
|
||||
- in: (max|maximum|highest)
|
||||
out: 100
|
||||
- in: (min|minimum|lowest)
|
||||
out: 1
|
||||
|
||||
player_volume_level_pct:
|
||||
range:
|
||||
type: "percentage"
|
||||
from: 0
|
||||
to: 100
|
||||
|
||||
player_volume_step_pct:
|
||||
range:
|
||||
type: "percentage"
|
||||
from: 1
|
||||
to: 100
|
||||
|
||||
spotifyplus_media_type:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(album|lp)"
|
||||
out: "album"
|
||||
- in: "artist"
|
||||
out: "artist"
|
||||
- in: "(audiobook|audio book|book)"
|
||||
out: "audiobook"
|
||||
- in: "(playlist|play list)"
|
||||
out: "playlist"
|
||||
- in: "(podcast|pod cast|show) episode"
|
||||
out: "episode"
|
||||
- in: "(podcast|pod cast|show)"
|
||||
out: "podcast"
|
||||
- in: "(track|song)"
|
||||
out: "track"
|
||||
|
||||
spotifyplus_player_deck_control:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(pause|stop) [[<the>] (play[er]|track|song)]"
|
||||
out: "pause"
|
||||
- in: "(resume|start) [[<the>] (play[er]|track|song)]"
|
||||
out: "resume"
|
||||
- in: "(restart [[<the>] (track|song)]|skip (to|2) [<the>] start of [<the>] (track|song))"
|
||||
out: "seek_start"
|
||||
- in: "([play] [<the>] next|skip|skip (to|2) [<the>] next) (track|song)"
|
||||
out: "skip_next"
|
||||
- in: "([play] [<the>] previous|skip|skip (to|2) [<the>] previous) (track|song)"
|
||||
out: "skip_previous"
|
||||
|
||||
spotifyplus_player_volume_control:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(mute|[player] volume [level] (mute|off))"
|
||||
out: "mute"
|
||||
- in: "(unmute|[player] volume [level] (unmute|un mute|restore|on))"
|
||||
out: "unmute"
|
||||
- in: "[player] volume [level] (decrease[d]|down|lower)"
|
||||
out: "step_down"
|
||||
- in: "[player] volume [level] (increase[d]|up|higher|raise)"
|
||||
out: "step_up"
|
||||
- in: "[player] volume step [level] [to|2] {player_volume_step_pct} [percent|%]"
|
||||
out: "set_step_level"
|
||||
- in: "[player] volume [level] [to|2] {player_volume_level_pct} [percent|%]"
|
||||
out: "set_level"
|
||||
|
||||
spotifyplus_search_play_control_multi:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "episode"
|
||||
out: "podcast_episode"
|
||||
- in: "artist (album|LP)"
|
||||
out: "artist_album"
|
||||
- in: "artist (track|song)"
|
||||
out: "artist_track"
|
||||
|
||||
spotifyplus_search_play_control_single:
|
||||
# out values must be English value.
|
||||
values:
|
||||
- in: "(album|LP) [{spotifyplus_album_names:criteria_arg_1}]"
|
||||
out: "album"
|
||||
- in: "(audiobook|audio book|book) [{spotifyplus_audiobook_names:criteria_arg_1}]"
|
||||
out: "audiobook"
|
||||
- in: "(playlist|play list) [{spotifyplus_playlist_names:criteria_arg_1}]"
|
||||
out: "playlist"
|
||||
- in: "(podcast|pod cast|show) [{spotifyplus_podcast_names:criteria_arg_1}] [{latest_episode}]"
|
||||
out: "podcast"
|
||||
- in: "(track favorites|favorite tracks)"
|
||||
out: "favorite_tracks"
|
||||
- in: "(track favorites|favorite tracks) (by|for) artist {spotifyplus_artist_names:criteria_arg_1}"
|
||||
out: "favorite_tracks_artist"
|
||||
- in: "(track|song) [{spotifyplus_track_names:criteria_arg_1}]"
|
||||
out: "track"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly album identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Albums that either get misinterpreted
|
||||
# when a spoken phrase is used, or if you want a specific album of multiple album
|
||||
# renditions. For example, do you want Journey's original 1981 version of the
|
||||
# "Escape" album tour, or the 2022 remastered version?
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify album uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_album_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:album:4guAwaniEAEQSW0NCpo4gm" # Journey, 2022 Remaster
|
||||
- in: "Escape"
|
||||
out: "spotify:album:4guAwaniEAEQSW0NCpo4gm" # Journey, 2022 Remaster
|
||||
- in: "Escape 1981"
|
||||
out: "spotify:album:0gHmMKZ9s3ztqeMfAzwxRL" # Journey, 1981 Live, Escape Tour
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly artist identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Artists that either get misinterpreted
|
||||
# when a spoken phrase is used, or if you want a specific artist of multiple artist
|
||||
# renditions.
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify artist uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_artist_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:artist:6kFLnclYFc3gzpNt13wim5" # Seventh Day Slumber
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly audiobook identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Audiobooks that either get misinterpreted
|
||||
# when a spoken phrase is used, or if you want a specific audiobook of multiple audiobook
|
||||
# renditions.
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify audiobook uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_audiobook_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:show:74aydHJKgYz3AIq3jjBSv1" # The Elfstones of Shannara
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly device name identifiers.
|
||||
#
|
||||
# This list is used to identify Spotify Connect device names.
|
||||
# These should align with what's in the `source_list` attribute of the media player.
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: SpotifyPlus media_player source_list entry.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_device_names:
|
||||
values:
|
||||
- in: "Office"
|
||||
out: "Office Speaker"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly playlist identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Algorithmic Playlists, which
|
||||
# cannot be returned by the Spotify Web API as of 2024/11/27.
|
||||
# More info: https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify playlist uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_playlist_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:playlist:37i9dQZEVXcTh0pBxo9pZe" # Discover Weekly
|
||||
- in: "Daily Mix (1|One)"
|
||||
out: "spotify:playlist:37i9dQZF1E39vTG3GurFPW"
|
||||
- in: "Daily Mix (2|Two)"
|
||||
out: "spotify:playlist:37i9dQZF1E39DIjrju3A9t"
|
||||
- in: "Daily Mix (3|Three)"
|
||||
out: "spotify:playlist:37i9dQZF1E36yv9pXdC1Mb"
|
||||
- in: "Daily Mix (4|Four)"
|
||||
out: "spotify:playlist:37i9dQZF1E35Ejqg6yJW0p"
|
||||
- in: "Daily Mix (5|Five)"
|
||||
out: "spotify:playlist:37i9dQZF1E35nVVgp8iMex"
|
||||
- in: "Discover Weekly"
|
||||
out: "spotify:playlist:37i9dQZEVXcTh0pBxo9pZe"
|
||||
- in: "(DJ|deejay|dee jay)"
|
||||
out: "spotify:playlist:37i9dQZF1EYkqdzj48dyYq"
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly podcast identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Podcasts that either get misinterpreted
|
||||
# when a spoken phrase is used, or if you want a specific podcast of multiple podcast
|
||||
# renditions. For example, do you want "The Joe Rogan Experience", or "The Joe Rogan AI
|
||||
# Experience"?
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify podcast uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_podcast_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:show:6kAsbP8pxwaU2kPibKTuHE" # Armchair Expert with Dax Shepard
|
||||
|
||||
# --------------------------------------------------------------------------------------
|
||||
# SpotifyPlus voice-friendly track identifiers.
|
||||
#
|
||||
# This list is used primarily to identify Spotify Tracks that either get misinterpreted
|
||||
# when a spoken phrase is used, or if you want a specific track of multiple track
|
||||
# renditions. For example, do you want Journey's original 1981 version of the
|
||||
# "Don't Stop Believin" track, or the 2022 remastered version?
|
||||
#
|
||||
# in: Voice friendly identifier.
|
||||
# out: Spotify track uri value.
|
||||
# --------------------------------------------------------------------------------------
|
||||
spotifyplus_track_names:
|
||||
values:
|
||||
- in: "Test List Entry"
|
||||
out: "spotify:track:5ehcf6UL1TkwozB386cRAp" # Journey, 2024 Remaster, Don't Stop believin'
|
||||
- in: "Don't Stop Believing"
|
||||
out: "spotify:track:5ehcf6UL1TkwozB386cRAp" # Journey, 2024 Remaster
|
||||
- in: "Don't Stop Believing 1981"
|
||||
out: "spotify:track:3Ab9C0oz70wDyigunvnG5N" # Journey, 1981 Live, Escape Tour
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus common expansion rules
|
||||
# ============================================================================
|
||||
expansion_rules:
|
||||
|
||||
in_spotifyplus_player_area: "(in|inn) [<the>] ({area}|{area} area|area {area})" # e.g. "in the Office", "in the Office area", "in area Office"
|
||||
on_spotifyplus_player_name: "(on|for) [<the>] ({name} player|player {name})" # e.g. "on the Spotify Premium player", "on player Spotify Premium"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# SpotifyPlus common skip words
|
||||
# ============================================================================
|
||||
skip_words:
|
||||
- "please"
|
||||
- "can you"
|
||||
- "could you"
|
||||
- "would you"
|
||||
- "for me"
|
||||
- "i'd like"
|
||||
- "i'd like to"
|
||||
- "i want"
|
||||
- "i want to"
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
"""Provide info to system health."""
|
||||
from typing import Any
|
||||
import json
|
||||
|
||||
from homeassistant.components import system_health
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .instancedata_spotifyplus import InstanceDataSpotifyPlus
|
||||
|
||||
# get smartinspect logger reference; create a new session for this module name.
|
||||
from smartinspectpython.siauto import SIAuto, SILevel, SISession
|
||||
import logging
|
||||
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
||||
if (_logsi == None):
|
||||
_logsi = SIAuto.Si.AddSession(__name__, True)
|
||||
_logsi.SystemLogger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register(hass: HomeAssistant, register: system_health.SystemHealthRegistration) -> None:
|
||||
"""Register system health callbacks."""
|
||||
register.async_register_info(system_health_info, "/config/integrations/integration/%s" % DOMAIN)
|
||||
|
||||
|
||||
async def system_health_info(hass):
|
||||
"""Get info for the info page."""
|
||||
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
|
||||
# create dictionary for health information.
|
||||
healthInfo: dict[str, Any] = {}
|
||||
|
||||
# add manifest file details.
|
||||
# as of HA 2024.6, we have to use an executor job to load the file contents as the trace uses a blocking file open / read call.
|
||||
myConfigDir:str = "%s/custom_components/%s" % (hass.config.config_dir, DOMAIN)
|
||||
myManifestPath:str = "%s/manifest.json" % (myConfigDir)
|
||||
myManifest:dict = await hass.async_add_executor_job(_getManifestFile, myManifestPath, "Integration Manifest File (%s)" % myManifestPath)
|
||||
healthInfo["integration_version"] = myManifest.get('version','unknown')
|
||||
|
||||
# add client configuration data.
|
||||
clientConfig:str = ""
|
||||
if len(hass.data[DOMAIN]) > 0:
|
||||
clientConfig = str("%d: " % len(hass.data[DOMAIN]))
|
||||
data:InstanceDataSpotifyPlus = None
|
||||
for data in hass.data[DOMAIN].values():
|
||||
_logsi.LogDictionary(SILevel.Verbose, "InstanceDataSpotifyPlus data", data, prettyPrint=True)
|
||||
if data.spotifyClient != None:
|
||||
if data.spotifyClient.UserProfile != None:
|
||||
clientConfig = clientConfig + "%s (%s), " % (data.spotifyClient.UserProfile.DisplayName, data.spotifyClient.UserProfile.Product)
|
||||
clientConfig = clientConfig[:len(clientConfig)-2] # drop ending ", "
|
||||
else:
|
||||
clientConfig = "(None Defined)"
|
||||
healthInfo["clients_configured"] = clientConfig
|
||||
|
||||
# check if Spotify Web API endpoint is reachable.
|
||||
healthInfo["api_endpoint_reachable"] = system_health.async_check_can_reach_url(hass, "https://api.spotify.com")
|
||||
|
||||
# trace.
|
||||
_logsi.LogDictionary(SILevel.Verbose, "System Health results", healthInfo)
|
||||
|
||||
# return system health data.
|
||||
return healthInfo
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("system_health_info exception: %s" % str(ex), ex, logToSystemLogger=False)
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
|
||||
|
||||
def _getManifestFile(filePath: str, title: str) -> str:
|
||||
"""
|
||||
Loads the contents of the specified text file and returns them.
|
||||
|
||||
Args:
|
||||
filePath (str):
|
||||
Fully-qualified file path to log.
|
||||
title (str):
|
||||
Title to assign to the log entry.
|
||||
|
||||
"""
|
||||
try:
|
||||
|
||||
# trace.
|
||||
_logsi.EnterMethod(SILevel.Debug)
|
||||
_logsi.LogTextFile(SILevel.Verbose, title, filePath)
|
||||
|
||||
# open file and read the contents.
|
||||
with open(filePath) as reader:
|
||||
data = reader.read()
|
||||
|
||||
# return contents to caller.
|
||||
return json.loads(data)
|
||||
|
||||
except Exception as ex:
|
||||
|
||||
# trace.
|
||||
_logsi.LogException("system_health_info _getManifestFile exception: %s" % str(ex), ex, logToSystemLogger=False)
|
||||
return {}
|
||||
|
||||
finally:
|
||||
|
||||
# trace.
|
||||
_logsi.LeaveMethod(SILevel.Debug)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
|
||||
def get_id_from_uri(
|
||||
uri:str,
|
||||
) -> str:
|
||||
"""
|
||||
Get the id portion from a Spotify uri value.
|
||||
|
||||
Args:
|
||||
uri (str):
|
||||
The Spotify URI value.
|
||||
Example: `spotify:track:5v5ETK9WFXAnGQ3MRubKuE`
|
||||
|
||||
Returns:
|
||||
A string containing the id value.
|
||||
|
||||
No exceptions are raised with this method.
|
||||
"""
|
||||
result:str = None
|
||||
|
||||
try:
|
||||
|
||||
# validations.
|
||||
if uri is None or len(uri.strip()) == 0:
|
||||
return result
|
||||
|
||||
# get Id from uri value.
|
||||
colonCnt:int = uri.count(':')
|
||||
if colonCnt == 2:
|
||||
idx:int = uri.rfind(':')
|
||||
if idx > -1:
|
||||
result = uri[idx+1:]
|
||||
|
||||
return result
|
||||
|
||||
except Exception:
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def passwordMaskDictionary(inputObj:dict) -> dict:
|
||||
"""
|
||||
Checks keys in a dictionary any keys that contain `password` and masks the
|
||||
value so that the password is not displayed in a trace file.
|
||||
|
||||
Args:
|
||||
inputObj (dict):
|
||||
Dictionary object to check.
|
||||
|
||||
Returns:
|
||||
A copy of the `inputObj` source dictionary with passwor(s) masked.
|
||||
|
||||
Note that this method performs a simple copy of the dictionary.
|
||||
"""
|
||||
# if input is null then don't bother.
|
||||
if (inputObj is None):
|
||||
return inputObj
|
||||
|
||||
# create a new dictionary.
|
||||
result:dict = {}
|
||||
|
||||
# process keys in the dictionary.
|
||||
key:str
|
||||
for key in inputObj.keys():
|
||||
keyLower:str = key.lower()
|
||||
if (keyLower.find('password') == -1):
|
||||
result[key] = inputObj[key]
|
||||
else:
|
||||
value:str = inputObj[key]
|
||||
if (value is not None) and (isinstance(value, str)):
|
||||
result[key] = ''.ljust(len(value), '*')
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def passwordMaskString(inputObj:str) -> str:
|
||||
"""
|
||||
Checks a string for a password value and masks the value so that the password is not displayed
|
||||
in a trace file.
|
||||
|
||||
Args:
|
||||
inputObj (str):
|
||||
String object to check.
|
||||
|
||||
Returns:
|
||||
A copy of the `inputObj` value with password masked.
|
||||
"""
|
||||
# if input is null then don't bother.
|
||||
if (inputObj is None):
|
||||
return inputObj
|
||||
|
||||
# create a new value.
|
||||
result:str = ''.ljust(len(inputObj), '*')
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user