#!/bin/sh # # Amazon Alexa Remote Control (PLAIN shell) # alex(at)loetzimmer.de # # 2021-01-28: v0.17c (for updates see http://blog.loetzimmer.de/2017/10/amazon-alexa-hort-auf-die-shell-echo.html) # 2021-09-02: v0.17d includes fixes for playing tunein (base64 required) # # !!! THIS IS THE FINAL VERSION !!! # # Due to JQ being widely available across platforms there is no need to expose oneself to the hacks # required when parsing JSON with BASH. # ### # # (no BASHisms were used, should run with any shell) # - requires cURL for web communication # - (GNU) sed and awk for extraction # - base64 for B64 encoding (make sure "-w 0" option is available on your platform) # - oathtool as OATH one-time password tool (optional for two-factor authentication) # ########################################## SET_EMAIL='amazon_account@email.address' SET_PASSWORD='Very_Secret_Amazon_Account_Password' SET_MFA_SECRET='' # something like: # 1234 5678 9ABC DEFG HIJK LMNO PQRS TUVW XYZ0 1234 5678 9ABC DEFG SET_LANGUAGE='de,en-US;q=0.7,en;q=0.3' #SET_LANGUAGE='en-US' SET_TTS_LOCALE='de-DE' SET_AMAZON='amazon.de' #SET_AMAZON='amazon.com' SET_ALEXA='alexa.amazon.de' #SET_ALEXA='pitangui.amazon.com' # cURL binary SET_CURL='/usr/bin/curl' # cURL options # -k : if your cURL cannot verify CA certificates, you'll have to trust any # --compressed : if your cURL was compiled with libz you may use compression # --http1.1 : cURL defaults to HTTP/2 on HTTPS connections if available SET_OPTS='--compressed --http1.1' #SET_OPTS='-k --compressed --http1.1' # browser identity SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:1.0) bash-script/1.0' #SET_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0' # oathtool command line tool SET_OATHTOOL='/usr/bin/oathtool' # tmp path SET_TMP="/tmp" # Volume for speak commands (a SPEAKVOL of 0 leaves the volume settings untouched) SET_SPEAKVOL="0" # if no current playing volume can be determined, fall back to normal volume SET_NORMALVOL="10" # Device specific volumes (overriding the above) SET_DEVICEVOLNAME="EchoDot2ndGen Echo1stGen" SET_DEVICEVOLSPEAK="100 30" SET_DEVICEVOLNORMAL="100 20" ########################################### # nothing to configure below here # # retrieving environment variables if any are set EMAIL=${EMAIL:-$SET_EMAIL} PASSWORD=${PASSWORD:-$SET_PASSWORD} MFA_SECRET=${MFA_SECRET:-$SET_MFA_SECRET} AMAZON=${AMAZON:-$SET_AMAZON} ALEXA=${ALEXA:-$SET_ALEXA} LANGUAGE=${LANGUAGE:-$SET_LANGUAGE} BROWSER=${BROWSER:-$SET_BROWSER} CURL=${CURL:-$SET_CURL} OPTS=${OPTS:-$SET_OPTS} TTS_LOCALE=${TTS_LOCALE:-$SET_TTS_LOCALE} TMP=${TMP:-$SET_TMP} OATHTOOL=${OATHTOOL:-$SET_OATHTOOL} SPEAKVOL=${SPEAKVOL:-$SET_SPEAKVOL} NORMALVOL=${NORMALVOL:-$SET_NORMALVOL} DEVICEVOLNAME=${DEVICEVOLNAME:-$SET_DEVICEVOLNAME} DEVICEVOLSPEAK=${DEVICEVOLSPEAK:-$SET_DEVICEVOLSPEAK} DEVICEVOLNORMAL=${DEVICEVOLNORMAL:-$SET_DEVICEVOLNORMAL} COOKIE="${TMP}/.alexa.cookie" DEVLIST="${TMP}/.alexa.devicelist.json" DEVTXT="${TMP}/.alexa.devicelist.txt" DEVALL="${TMP}/.alexa.devicelist.all" GUIVERSION=0 LIST="" LOGOFF="" COMMAND="" TTS="" SEQUENCECMD="" SEQUENCEVAL="" STATIONID="" QUEUE="" SONG="" ALBUM="" ARTIST="" ASIN="" SEEDID="" HIST="" LEMUR="" CHILD="" PLIST="" BLUETOOTH="" LASTALEXA="" NOTIFICATIONS="" usage() { echo "$0 [-d |ALL] -e > |" echo " -b [list|<\"AA:BB:CC:DD:EE:FF\">] | -q | -n | -r <\"station name\"|stationid> |" echo " -s | -t | -u | -v | -w |" echo " -a | -m [device_1 .. device_X] | -lastalexa | -l | -h" echo echo " -e : run command, additional SEQUENCECMDs:" echo " weather,traffic,flashbriefing,goodmorning,singasong,tellstory," echo " speak:'',sound:," echo " textcommand:''" echo " -b : connect/disconnect/list bluetooth device" echo " -q : query queue" echo " -n : query notifications" echo " -r : play tunein radio" echo " -s : play library track/library album" echo " -t : play Prime playlist" echo " -u : play Prime station" echo " -v : play Prime historical queue" echo " -w : play library playlist" echo " -a : list available devices" echo " -m : delete multiroom and/or create new multiroom containing devices" echo " -lastalexa : print serial number that received the last voice command" echo " -login : Logs in, without further command" echo " -l : logoff" echo " -h : help" } while [ "$#" -gt 0 ] ; do case "$1" in --version) echo "v0.17d" exit 0 ;; -d) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi DEVICE=$2 shift ;; -e) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi COMMAND=$2 shift ;; -b) if [ "${2#-}" = "${2}" -a -n "$2" ] ; then BLUETOOTH=$2 shift else BLUETOOTH="null" fi ;; -m) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi LEMUR=$2 shift while [ "${2#-}" = "${2}" -a -n "$2" ] ; do CHILD="${CHILD} ${2}" shift done ;; -r) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi STATIONID=$2 shift ;; -s) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi SONG=$2 shift if [ "${2#-}" = "${2}" -a -n "$2" ] ; then ALBUM=$2 ARTIST=$SONG shift fi ;; -t) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi ASIN=$2 shift ;; -u) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi SEEDID=$2 shift ;; -v) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi HIST=$2 shift ;; -w) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi PLIST=$2 shift ;; -d) if [ "${2#-}" != "${2}" -o -z "$2" ] ; then echo "ERROR: missing argument for ${1}" usage exit 1 fi DEVICE=$2 shift ;; -login) LOGIN="true" ;; -l) LOGOFF="true" ;; -a) LIST="true" ;; -q) QUEUE="true" ;; -n) NOTIFICATIONS="true" ;; -lastalexa) LASTALEXA="true" ;; -h|-\?|--help) usage exit 0 ;; *) echo "ERROR: unknown option ${1}" usage exit 1 ;; esac shift done case "$COMMAND" in pause) COMMAND='{"type":"PauseCommand"}' ;; play) COMMAND='{"type":"PlayCommand"}' ;; next) COMMAND='{"type":"NextCommand"}' ;; prev) COMMAND='{"type":"PreviousCommand"}' ;; fwd) COMMAND='{"type":"ForwardCommand"}' ;; rwd) COMMAND='{"type":"RewindCommand"}' ;; shuffle) COMMAND='{"type":"ShuffleCommand","shuffle":"true"}' ;; repeat) COMMAND='{"type":"RepeatCommand","repeat":true}' ;; vol:*) VOL=${COMMAND##*:} # volume as integer! if [ $VOL -le 100 -a $VOL -ge 0 ] ; then # COMMAND='{"type":"VolumeLevelCommand","volumeLevel":'${VOL}'}' SEQUENCECMD='Alexa.DeviceControls.Volume' SEQUENCEVAL=',\"value\":\"'${VOL}'\"' else echo "ERROR: volume should be an integer between 0 and 100" usage exit 1 fi ;; textcommand:*) SEQUENCECMD='Alexa.TextCommand\",\"skillId\":\"amzn1.ask.1p.tellalexa' SEQUENCEVAL=$(echo ${COMMAND##textcommand:} | sed -r s/\"/\'/g) SEQUENCEVAL=',\"text\":\"'${SEQUENCEVAL}'\"' ;; speak:*) TTS=$(echo ${COMMAND##*:} | sed -r 's/["\\]/ /g') TTS=',\"textToSpeak\":\"'${TTS}'\"' SEQUENCECMD='Alexa.Speak' SEQUENCEVAL=$TTS ;; sound:*) SEQUENCECMD='Alexa.Sound' SEQUENCEVAL=',\"soundStringId\":\"'${COMMAND##sound:}'\"' ;; weather) SEQUENCECMD='Alexa.Weather.Play' ;; traffic) SEQUENCECMD='Alexa.Traffic.Play' ;; flashbriefing) SEQUENCECMD='Alexa.FlashBriefing.Play' ;; goodmorning) SEQUENCECMD='Alexa.GoodMorning.Play' ;; singasong) SEQUENCECMD='Alexa.SingASong.Play' ;; tellstory) SEQUENCECMD='Alexa.TellStory.Play' ;; "") ;; *) echo "ERROR: unknown command \"${COMMAND}\"!" usage exit 1 ;; esac # # Amazon Login # log_in() { ################################################################ # # following headers are required: # Accept-Language (possibly for determining login region) # User-Agent (CURL wouldn't store cookies without) # ################################################################ rm -f ${DEVLIST} rm -f ${DEVTXT} rm -f ${DEVALL} rm -f ${COOKIE} # # get first cookie and write redirection target into referer # ${CURL} ${OPTS} -s -D "${TMP}/.alexa.header" -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ https://alexa.${AMAZON} | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata" # # login empty to generate session # ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ -H "$(grep 'Location: ' ${TMP}/.alexa.header | sed 's/Location: /Referer: /')" -d "@${TMP}/.alexa.postdata" https://www.${AMAZON}/ap/signin | grep "hidden" | sed 's/hidden/\n/g' | grep "value=\"" | sed -r 's/^.*name="([^"]+)".*value="([^"]+)".*/\1=\2\&/g' > "${TMP}/.alexa.postdata2" # # add OTP if using MFA # if [ -n "${MFA_SECRET}" ] ; then OTP=$(${OATHTOOL} -b --totp "${MFA_SECRET}") PASSWORD="${PASSWORD}${OTP}" fi # # login with filled out form # !!! referer now contains session in URL # ${CURL} ${OPTS} -s -D "${TMP}/.alexa.header2" -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "Accept-Language: ${LANGUAGE}" -H "DNT: 1" -H "Connection: keep-alive" -H "Upgrade-Insecure-Requests: 1" -L\ -H "Referer: https://www.${AMAZON}/ap/signin/$(awk "\$0 ~/.${AMAZON}.*session-id[ \\s\\t]+/ {print \$7}" ${COOKIE})" --data-urlencode "email=${EMAIL}" --data-urlencode "password=${PASSWORD}" -d "@${TMP}/.alexa.postdata2" https://www.${AMAZON}/ap/signin > "${TMP}/.alexa.login" # check whether the login has been successful or exit otherwise if [ -z "$(grep 'Location: https://alexa.*html' ${TMP}/.alexa.header2)" ] ; then echo "ERROR: Amazon Login was unsuccessful. Possibly you get a captcha login screen." echo " Try logging in to https://alexa.${AMAZON} with your browser. In your browser" echo " make sure to have all Amazon related cookies deleted and Javascript disabled!" echo echo " (For more information have a look at ${TMP}/.alexa.login)" echo echo " To avoid issues with captcha, try using Multi-Factor Authentication." echo " To do so, first set up Two-Step Verification on your Amazon account, then" echo " configure this script (or the environment) with your MFA secret." echo " Support for Multi-Factor Authentication requires 'oathtool' to be installed." rm -f ${COOKIE} rm -f "${TMP}/.alexa.header" rm -f "${TMP}/.alexa.header2" rm -f "${TMP}/.alexa.postdata" rm -f "${TMP}/.alexa.postdata2" exit 1 fi # # get CSRF # ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/api/language > /dev/null if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "trying to get CSRF from handlebars" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/templates/oobe/d-device-pick.handlebars > /dev/null fi if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "trying to get CSRF from devices-v2" ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ https://${ALEXA}/api/devices-v2/device?cached=false > /dev/null fi rm -f "${TMP}/.alexa.login" rm -f "${TMP}/.alexa.header" rm -f "${TMP}/.alexa.header2" rm -f "${TMP}/.alexa.postdata" rm -f "${TMP}/.alexa.postdata2" if [ -z "$(grep ".${AMAZON}.*csrf" ${COOKIE})" ] ; then echo "ERROR: no CSRF cookie received" exit 1 fi } # # get JSON device list # get_devlist() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})"\ "https://${ALEXA}/api/devices-v2/device?cached=false" > ${DEVLIST} if [ ! -f ${DEVTXT} ] ; then cat ${DEVLIST}| sed 's/\\\\\//\//g' | sed 's/[{}]//g' | awk -v k="text" '{n=split($0,a,","); for (i=1; i<=n; i++) print a[i]}' | sed 's/\"\:\"/\|/g' | sed 's/[\,]/ /g' | sed 's/\"//g' > ${DEVTXT} fi # create a file that contains valid device names for the "ALL" device ATTR="accountName" NAME=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="deviceFamily" FAMILY=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') IDX=0 for N in $NAME ; do C=0 for F in $FAMILY ; do if [ $C -eq $IDX ] ; then if [ "$F" = "WHA" -o "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then echo ${N} >> ${DEVALL} fi break fi C=$((C+1)) done IDX=$((IDX+1)) done } check_status() { # # bootstrap with GUI-Version writes GUI version to cookie # returns among other the current authentication state # AUTHSTATUS=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L https://${ALEXA}/api/bootstrap?version=${GUIVERSION}) MEDIAOWNERCUSTOMERID=$(echo $AUTHSTATUS | sed -r 's/^.*"customerId":"([^,]+)",.*$/\1/g') AUTHSTATUS=$(echo $AUTHSTATUS | sed -r 's/^.*"authenticated":([^,]+),.*$/\1/g') if [ "$AUTHSTATUS" = "true" ] ; then return 1 fi return 0 } # # set device specific variables from JSON device list # set_var() { ATTR="accountName" NAME=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="deviceType" TYPE=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="serialNumber" SERIAL=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') # ATTR="deviceOwnerCustomerId" # MEDIAID=`grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="deviceFamily" FAMILY=$(grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g') ATTR="online" ONLINE=$(grep ${ATTR}\: ${DEVTXT} | sed "s/^.*${ATTR}://") if [ -z "${DEVICE}" ] ; then # if no device was supplied, use the first Echo(dot) in device list IDX=0 for F in $FAMILY ; do if [ "$F" = "ECHO" -o "$F" = "KNIGHT" -o "$F" = "ROOK" ] ; then break; fi IDX=$((IDX+1)) done C=0 for N in $NAME ; do if [ $C -eq $IDX ] ; then DEVICE=$N break fi C=$((C+1)) done echo "setting default device to:" echo ${DEVICE} else DEVICE=`echo $DEVICE | sed 's/ /_/g'` IDX=0 for N in $NAME ; do if [ "$N" = "$DEVICE" ] ; then break; fi IDX=$((IDX+1)) done fi # customerId is now retrieved from the logged in user # the customerId in the device list is always from the user registering the device initially # C=0 # for I in $MEDIAID ; do # if [ $C -eq $IDX ] ; then # MEDIAOWNERCUSTOMERID=$I # break # fi # C=$((C+1)) # done C=0 for T in $TYPE ; do if [ $C -eq $IDX ] ; then DEVICETYPE=$T break fi C=$((C+1)) done C=0 for S in $SERIAL ; do if [ $C -eq $IDX ] ; then DEVICESERIALNUMBER=$S break fi C=$((C+1)) done C=0 for F in $FAMILY ; do if [ $C -eq $IDX ] ; then DEVICEFAMILY=$F break fi C=$((C+1)) done C=0 for O in $ONLINE ; do if [ $C -eq $IDX ] ; then DEVICESTATE=$O break fi C=$((C+1)) done if [ -z "${DEVICESERIALNUMBER}" ] ; then echo "ERROR: unkown device dev:${DEVICE}" exit 1 fi } # # execute command # (SequenceCommands by Michael Geramb and Ralf Otto) # run_cmd() { if [ -n "${SEQUENCECMD}" ] ; then if echo $COMMAND | grep -q -E "weather|traffic|flashbriefing|goodmorning|singasong|tellstory|speak|sound|textcommand" ; then if [ "${DEVICEFAMILY}" = "WHA" ] ; then echo "Skipping unsupported command: ${COMMAND} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} family:${DEVICEFAMILY}" return fi fi # the speak command is treated differently if $SPEAKVOL > 0 if [ -n "${TTS}" -a $SPEAKVOL -gt 0 ] || [ "${COMMAND%%:*}" = 'sound' -a $SPEAKVOL -gt 0 ] ; then SVOL=$SPEAKVOL # Not using arrays here in order to be compatible with non-Bash # Get the list position of the current device type IDX=0 for D in $DEVICEVOLNAME ; do if [ "${D}" = "${DEVICE}" ] ; then break; fi IDX=$((IDX+1)) done # get the speak volume at that position C=0 for D in $DEVICEVOLSPEAK ; do if [ $C -eq $IDX ] ; then if [ -n "${D}" ] ; then SVOL=$D ; fi break fi C=$((C+1)) done # try to retrieve the "currently playing" volume VOL=$(${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" | grep 'volume' | sed -r 's/^.*"volume":\s*([0-9]+)[^0-9]*$/\1/g') # in order to prevent a "Rate exceeded" we need to delay the command sleep 1 if [ -z "${VOL}" ] ; then # get the normal volume of the current device type C=0 for D in $DEVICEVOLNORMAL; do if [ $C -eq $IDX ] ; then VOL=$D break fi C=$((C+1)) done # if the volume is still undefined, use $NORMALVOL if [ -z "${VOL}" ] ; then VOL=$NORMALVOL fi fi ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.SerialNode\",\"nodesToExecute\":[{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"Alexa.DeviceControls.Volume\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\",\"value\":\"'${SVOL}'\"}},{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}},{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"Alexa.DeviceControls.Volume\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\",\"value\":\"'${VOL}'\"}}]}}","status":"ENABLED"}' else ALEXACMD='{"behaviorId":"PREVIEW","sequenceJson":"{\"@type\":\"com.amazon.alexa.behaviors.model.Sequence\",\"startNode\":{\"@type\":\"com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode\",\"type\":\"'${SEQUENCECMD}'\",\"operationPayload\":{\"deviceType\":\"'${DEVICETYPE}'\",\"deviceSerialNumber\":\"'${DEVICESERIALNUMBER}'\",\"customerId\":\"'${MEDIAOWNERCUSTOMERID}'\",\"locale\":\"'${TTS_LOCALE}'\"'${SEQUENCEVAL}'}}}","status":"ENABLED"}' fi # Due to some weird shell-escape-behavior the command has to be written to a file before POSTing it echo $ALEXACMD > "${TMP}/.alexa.cmd" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d @"${TMP}/.alexa.cmd"\ "https://${ALEXA}/api/behaviors/preview" rm -f "${TMP}/.alexa.cmd" else ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d ${COMMAND}\ "https://${ALEXA}/api/np/command?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" fi } # # play TuneIn radio station # play_radio() { JSON='{"contentToken":"music:'$(echo '["music/tuneIn/stationId","'${STATIONID}'"]|{"previousPageId":"TuneIn_SEARCH"}'| base64 -w 0| base64 -w 0 )'"}' ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X PUT -d "${JSON}" \ "https://${ALEXA}/api/entertainment/v1/player/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" } # # play library track # play_song() { if [ -z "${ALBUM}" ] ; then JSON="{\"trackId\":\"${SONG}\",\"playQueuePrime\":true}" else JSON="{\"albumArtistName\":\"${ARTIST}\",\"albumName\":\"${ALBUM}\"}" fi ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}"\ "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" } # # play library playlist # play_playlist() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"playlistId\":\"${PLIST}\",\"playQueuePrime\":true}"\ "https://${ALEXA}/api/cloudplayer/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}&shuffle=false" } # # play PRIME playlist # play_prime_playlist() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"asin\":\"${ASIN}\"}"\ "https://${ALEXA}/api/prime/prime-playlist-queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" } # # play PRIME station # play_prime_station() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"seed\":\"{\\\"type\\\":\\\"KEY\\\",\\\"seedId\\\":\\\"${SEEDID}\\\"}\",\"stationName\":\"none\",\"seedType\":\"KEY\"}"\ "https://${ALEXA}/api/gotham/queue-and-play?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}&mediaOwnerCustomerId=${MEDIAOWNERCUSTOMERID}" } # # play PRIME historical queue # play_prime_hist_queue() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"deviceType\":\"${DEVICETYPE}\",\"deviceSerialNumber\":\"${DEVICESERIALNUMBER}\",\"mediaOwnerCustomerId\":\"${MEDIAOWNERCUSTOMERID}\",\"queueId\":\"${HIST}\",\"service\":null,\"trackSource\":\"TRACK\"}"\ "https://${ALEXA}/api/media/play-historical-queue" } # # current queue # show_queue() { echo "/api/np/player" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/np/player?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo echo "/api/np/queue" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/np/queue?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo echo "/api/media/state" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/media/state?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo } # # show notifications and alarms # show_notifications() { echo "/api/notifications" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/notifications?deviceSerialNumber=${DEVICESERIALNUMBER}&deviceType=${DEVICETYPE}" echo } # # deletes a multiroom device # delete_multiroom() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X DELETE \ "https://${ALEXA}/api/lemur/tail/${DEVICESERIALNUMBER}" } # # creates a multiroom device # create_multiroom() { JSON="{\"id\":null,\"name\":\"${LEMUR}\",\"members\":[" for DEVICE in $CHILD ; do set_var JSON="${JSON}{\"dsn\":\"${DEVICESERIALNUMBER}\",\"deviceType\":\"${DEVICETYPE}\"}," done JSON="${JSON%,}]}" ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "${JSON}" \ "https://${ALEXA}/api/lemur/tail" } # # list bluetooth devices # list_bluetooth() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/bluetooth?cached=false" } # # connect bluetooth device # connect_bluetooth() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST -d "{\"bluetoothDeviceAddress\":\"${BLUETOOTH}\"}"\ "https://${ALEXA}/api/bluetooth/pair-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # disconnect bluetooth device # disconnect_bluetooth() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X POST \ "https://${ALEXA}/api/bluetooth/disconnect-sink/${DEVICETYPE}/${DEVICESERIALNUMBER}" } # # device that sent the last command # (by Markus Wennesheimer) # last_alexa() { ${CURL} ${OPTS} -s -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ -H "Content-Type: application/json; charset=UTF-8" -H "Referer: https://alexa.${AMAZON}/spa/index.html" -H "Origin: https://alexa.${AMAZON}"\ -H "csrf: $(awk "\$0 ~/.${AMAZON}.*csrf[ \\s\\t]+/ {print \$7}" ${COOKIE})" -X GET \ "https://${ALEXA}/api/activities?startTime=&size=1&offset=1" | sed -r 's/^.*serialNumber":"([^"]+)".*$/\1/' } # # logout # log_off() { ${CURL} ${OPTS} -s -c ${COOKIE} -b ${COOKIE} -A "${BROWSER}" -H "DNT: 1" -H "Connection: keep-alive" -L\ https://${ALEXA}/logout > /dev/null rm -f ${DEVLIST} rm -f ${DEVTXT} rm -f ${DEVALL} rm -f ${COOKIE} } if [ -z "$LASTALEXA" -a -z "$BLUETOOTH" -a -z "$LEMUR" -a -z "$PLIST" -a -z "$HIST" -a -z "$SEEDID" -a -z "$ASIN" -a -z "$QUEUE" -a -z "$NOTIFICATIONS" -a -z "$COMMAND" -a -z "$STATIONID" -a -z "$SONG" -a -n "$LOGOFF" ] ; then echo "only logout option present, logging off ..." log_off exit 0 fi if [ ! -f ${COOKIE} ] ; then echo "cookie does not exist. logging in ..." log_in fi check_status if [ $? -eq 0 ] ; then echo "cookie expired, logging in again ..." log_in check_status if [ $? -eq 0 ] ; then echo "log in failed, aborting" exit 1 fi fi if [ ! -f ${DEVTXT} -o ! -f ${DEVALL} ] ; then echo "device list does not exist. downloading ..." get_devlist if [ ! -f ${DEVTXT} ] ; then echo "failed to download device list, aborting" exit 1 fi fi if [ -n "$LOGIN" ] ; then echo "logged in" exit 0 fi if [ -n "$COMMAND" -o -n "$QUEUE" -o -n "$NOTIFICATIONS" ] ; then if [ "${DEVICE}" = "ALL" ] ; then while IFS= read -r DEVICE ; do set_var if [ "$DEVICESTATE" = "true" ] ; then if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd # in order to prevent a "Rate exceeded" we need to delay the command sleep 1 echo elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi fi done < ${DEVALL} else set_var if [ -n "$COMMAND" ] ; then echo "sending cmd:${COMMAND} to dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} customerid:${MEDIAOWNERCUSTOMERID}" run_cmd echo elif [ -n "$NOTIFICATIONS" ] ; then echo "notifications info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_notifications else echo "queue info for dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER}" show_queue echo fi fi elif [ -n "$LEMUR" ] ; then DEVICE="${LEMUR}" set_var if [ -n "$DEVICESERIALNUMBER" ] ; then delete_multiroom fi if [ -z "$CHILD" ] ; then echo "Deleted multi room dev:${LEMUR} serial:${DEVICESERIALNUMBER}" else echo "Creating multi room dev:${LEMUR} member_dev(s):${CHILD}" create_multiroom echo fi rm -f ${DEVLIST} rm -f ${DEVALL} rm -f ${DEVTXT} get_devlist elif [ -n "$BLUETOOTH" ] ; then if [ "$BLUETOOTH" = "list" -o "$BLUETOOTH" = "List" -o "$BLUETOOTH" = "LIST" ] ; then if [ "${DEVICE}" = "ALL" ] ; then while IFS= read -r DEVICE ; do set_var echo "bluetooth api list:" list_bluetooth echo done < ${DEVALL} else set_var echo "bluetooth api list:" list_bluetooth echo fi elif [ "$BLUETOOTH" = "null" ] ; then set_var echo "disconnecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} from bluetooth" disconnect_bluetooth echo else set_var echo "connecting dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} to bluetooth device:${BLUETOOTH}" connect_bluetooth echo fi elif [ -n "$STATIONID" ] ; then set_var echo "playing stationID:${STATIONID} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" play_radio elif [ -n "$SONG" ] ; then set_var echo "playing library track:${SONG} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" play_song elif [ -n "$PLIST" ] ; then set_var echo "playing library playlist:${PLIST} on dev:${DEVICE} type:${DEVICETYPE} serial:${DEVICESERIALNUMBER} mediaownerid:${MEDIAOWNERCUSTOMERID}" play_playlist elif [ -n "$ASIN" ] ; then set_var echo "playing PRIME playlist ${ASIN}" play_prime_playlist elif [ -n "$SEEDID" ] ; then set_var echo "playing PRIME station ${SEEDID}" play_prime_station elif [ -n "$HIST" ] ; then set_var echo "playing PRIME historical queue ${HIST}" play_prime_hist_queue elif [ -n "$LIST" ] ; then ATTR="accountName" echo "the following devices exist in your account:" grep ${ATTR}\| ${DEVTXT} | sed "s/^.*${ATTR}|//" | sed 's/ /_/g' elif [ -n "$LASTALEXA" ] ; then last_alexa else echo "no alexa command received" fi if [ -n "$LOGOFF" ] ; then echo "logout option present, logging off ..." log_off fi