Initial Home Assistant commit

This commit is contained in:
2026-06-05 22:34:31 -04:00
commit 6a58b10e6c
4494 changed files with 297833 additions and 0 deletions
@@ -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',
]
@@ -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)
@@ -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
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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)