Initial Home Assistant commit
This commit is contained in:
@@ -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
Reference in New Issue
Block a user