/**
* Echo Speaks Device
*
* Copyright 2018, 2019 Anthony Santilli
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*/
String devVersion() { return "3.2.0.3"}
String devModified() { return "2019-10-22" }
Boolean isBeta() { return false }
Boolean isST() { return (getPlatform() == "SmartThings") }
Boolean isWS() { return false }
metadata {
definition (name: "Echo Speaks Device", namespace: "tonesto7", author: "Anthony Santilli", mnmn: "SmartThings", vid: "generic-music-player", importUrl: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/devicetypes/tonesto7/echo-speaks-device.src/echo-speaks-device.groovy") {
//capability "Audio Mute" // Not Compatible with Hubitat
capability "Audio Notification"
// capability "Audio Track Data" // To support SharpTools.io Album Art feature
capability "Audio Volume"
capability "Music Player"
capability "Notification"
capability "Refresh"
capability "Sensor"
capability "Speech Synthesis"
attribute "alarmVolume", "number"
attribute "alexaMusicProviders", "JSON_OBJECT"
attribute "alexaNotifications", "JSON_OBJECT"
attribute "alexaPlaylists", "JSON_OBJECT"
// attribute "alexaGuardStatus", "string"
attribute "alexaWakeWord", "string"
attribute "btDeviceConnected", "string"
attribute "btDevicesPaired", "JSON_OBJECT"
attribute "currentAlbum", "string"
attribute "currentStation", "string"
attribute "deviceFamily", "string"
attribute "deviceStatus", "string"
attribute "deviceStyle", "string"
attribute "deviceType", "string"
attribute "doNotDisturb", "string"
attribute "firmwareVer", "string"
attribute "followUpMode", "string"
attribute "lastCmdSentDt", "string"
attribute "lastSpeakCmd", "string"
attribute "lastSpokenToTime", "number"
attribute "lastVoiceActivity", "string"
attribute "lastUpdated", "string"
attribute "mediaSource", "string"
attribute "onlineStatus", "string"
attribute "permissions", "string"
attribute "supportedMusic", "string"
attribute "trackImage", "string"
attribute "trackImageHtml", "string"
attribute "volume", "number"
attribute "wakeWords", "enum"
attribute "wifiNetwork", "string"
attribute "wasLastSpokenToDevice", "string"
attribute "audioTrackData", "JSON_OBJECT"
command "playText", ["string"] //This command is deprecated in ST but will work
command "playTextAndResume"
command "playTrackAndResume"
command "playTrackAndRestore"
command "playTextAndRestore"
command "replayText"
command "doNotDisturbOn"
command "doNotDisturbOff"
// command "followUpModeOn"
// command "followUpModeOff"
command "setAlarmVolume", ["number"]
command "resetQueue"
command "playWeather", ["number", "number"]
command "playSingASong", ["number", "number"]
command "playFlashBrief", ["number", "number"]
command "playFunFact", ["number", "number"]
command "playTraffic", ["number", "number"]
command "playJoke", ["number", "number"]
command "playTellStory", ["number", "number"]
command "sayGoodbye", ["number", "number"]
command "sayGoodNight", ["number", "number"]
command "sayBirthday", ["number", "number"]
command "sayCompliment", ["number", "number"]
command "sayGoodMorning", ["number", "number"]
command "sayWelcomeHome", ["number", "number"]
// command "playCannedRandomTts", ["string", "number", "number"]
// command "playCannedTts", ["string", "string", "number", "number"]
command "playAnnouncement", ["string", "number", "number"]
command "playAnnouncement", ["string", "string", "number", "number"]
command "playAnnouncementAll", ["string", "string"]
command "playCalendarToday", ["number", "number"]
command "playCalendarTomorrow", ["number", "number"]
command "playCalendarNext", ["number", "number"]
command "stopAllDevices"
command "searchMusic", ["string", "string", "number", "number"]
command "searchAmazonMusic", ["string", "number", "number"]
command "searchAppleMusic", ["string", "number", "number"]
command "searchPandora", ["string", "number", "number"]
command "searchIheart", ["string", "number", "number"]
command "searchSiriusXm", ["string", "number", "number"]
command "searchSpotify", ["string", "number", "number"]
// command "searchTidal", ["string", "number", "number"]
command "searchTuneIn", ["string", "number", "number"]
command "sendAlexaAppNotification", ["string"]
command "executeSequenceCommand", ["string"]
command "executeRoutineId", ["string"]
command "createAlarm", ["string", "string", "string"]
command "createReminder", ["string", "string", "string"]
command "removeNotification", ["string"]
command "setWakeWord", ["string"]
command "renameDevice", ["string"]
command "storeCurrentVolume"
command "restoreLastVolume"
command "togglePlayback"
command "setVolumeAndSpeak", ["number", "string"]
command "setVolumeSpeakAndRestore", ["number", "string", "number"]
command "volumeUp"
command "volumeDown"
command "speechTest"
command "sendTestAnnouncement"
command "sendTestAnnouncementAll"
command "getDeviceActivity"
command "getBluetoothDevices"
command "connectBluetooth", ["string"]
command "disconnectBluetooth"
command "removeBluetooth", ["string"]
command "sendAnnouncementToDevices", ["string", "string", "string", "number", "number"]
}
tiles (scale: 2) {
multiAttributeTile(name: "mediaMulti", type:"mediaPlayer", width:6, height:4) {
tileAttribute("device.status", key: "PRIMARY_CONTROL") {
attributeState("paused", label:"Paused")
attributeState("playing", label:"Playing")
attributeState("stopped", label:"Stopped", defaultState: true)
}
tileAttribute("device.status", key: "MEDIA_STATUS") {
attributeState("paused", label:"Paused", action:"music Player.play", nextState: "playing")
attributeState("playing", label:"Playing", action:"music Player.pause", nextState: "paused")
attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing", defaultState: true)
}
tileAttribute("device.status", key: "PREVIOUS_TRACK") {
attributeState("status", action:"music Player.previousTrack", defaultState: true)
}
tileAttribute("device.status", key: "NEXT_TRACK") {
attributeState("status", action:"music Player.nextTrack", defaultState: true)
}
tileAttribute ("device.level", key: "SLIDER_CONTROL") {
attributeState("level", action:"music Player.setLevel", defaultState: true)
}
tileAttribute ("device.mute", key: "MEDIA_MUTED") {
attributeState("unmuted", action:"music Player.mute", nextState: "muted")
attributeState("muted", action:"music Player.unmute", nextState: "unmuted", defaultState: true)
}
tileAttribute("device.trackDescription", key: "MARQUEE") {
attributeState("trackDescription", label:"${currentValue}", defaultState: true)
}
}
standardTile("deviceStatus", "device.deviceStatus", height: 1, width: 1, inactiveLabel: false, decoration: "flat") {
state("paused_unknown", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/unknown.png", backgroundColor: "#cccccc")
state("playing_unknown", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/unknown.png", backgroundColor: "#00a0dc")
state("stopped_unknown", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/unknown.png")
// ECHO (GEN1)
state("paused_echo_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen1.png")
// ECHO (GEN2)
state("paused_echo_gen2", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen2.png", backgroundColor: "#cccccc")
state("playing_echo_gen2", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen2.png", backgroundColor: "#00a0dc")
state("stopped_echo_gen2", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen2.png")
// ECHO (GEN3)
state("paused_echo_gen3", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen3.png", backgroundColor: "#cccccc")
state("playing_echo_gen3", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen3.png", backgroundColor: "#00a0dc")
state("stopped_echo_gen3", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_gen3.png")
// ECHO PLUS (GEN1)
state("paused_echo_plus_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_plus_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_plus_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen1.png")
// ECHO PLUS (GEN2)
state("paused_echo_plus_gen2", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen2.png", backgroundColor: "#cccccc")
state("playing_echo_plus_gen2", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen2.png", backgroundColor: "#00a0dc")
state("stopped_echo_plus_gen2", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_plus_gen2.png")
// ECHO DOT (GEN1)
state("paused_echo_dot_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_dot_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_dot_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen1.png")
// ECHO DOT (GEN2)
state("paused_echo_dot_gen2", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen2.png", backgroundColor: "#cccccc")
state("playing_echo_dot_gen2", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen2.png", backgroundColor: "#00a0dc")
state("stopped_echo_dot_gen2", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen2.png")
// ECHO DOT (GEN3)
state("paused_echo_dot_gen3", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen3.png", backgroundColor: "#cccccc")
state("playing_echo_dot_gen3", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen3.png", backgroundColor: "#00a0dc")
state("stopped_echo_dot_gen3", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_dot_gen3.png")
// ECHO SPOT (GEN1)
state("paused_echo_spot_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_spot_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_spot_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_spot_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_spot_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_spot_gen1.png")
// ECHO SHOW (GEN1)
state("paused_echo_show_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_show_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_show_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen1.png")
// ECHO SHOW (GEN2)
state("paused_echo_show_gen2", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen2.png", backgroundColor: "#cccccc")
state("playing_echo_show_gen2", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen2.png", backgroundColor: "#00a0dc")
state("stopped_echo_show_gen2", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_gen2.png")
// ECHO SHOW 5 (GEN1)
state("paused_echo_show_5", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_5.png", backgroundColor: "#cccccc")
state("playing_echo_show_5", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_5.png", backgroundColor: "#00a0dc")
state("stopped_echo_show_5", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_5.png")
// ECHO SHOW 8 (GEN1)
state("paused_echo_show_8", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_8.png", backgroundColor: "#cccccc")
state("playing_echo_show_8", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_8.png", backgroundColor: "#00a0dc")
state("stopped_echo_show_8", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_show_8.png")
// ECHO TAP (GEN1)
state("paused_echo_tap", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_tap.png", backgroundColor: "#cccccc")
state("playing_echo_tap", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_tap.png", backgroundColor: "#00a0dc")
state("stopped_echo_tap", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_tap.png")
// ECHO STUDIO (GEN1)
state("paused_echo_studio", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_studio.png", backgroundColor: "#cccccc")
state("playing_echo_studio", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_studio.png", backgroundColor: "#00a0dc")
state("stopped_echo_studio", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_studio.png")
// ECHO AUTO (GEN1)
state("paused_echo_auto", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png", backgroundColor: "#cccccc")
state("playing_echo_auto", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png", backgroundColor: "#00a0dc")
state("stopped_echo_auto", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png")
// ECHO BUDS (GEN1)
state("paused_echo_buds", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_buds.png", backgroundColor: "#cccccc")
state("playing_echo_buds", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_buds.png", backgroundColor: "#00a0dc")
state("stopped_echo_buds", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_buds.png")
// ECHO INPUT (GEN1)
state("paused_echo_input", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png", backgroundColor: "#cccccc")
state("playing_echo_input", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png", backgroundColor: "#00a0dc")
state("stopped_echo_input", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png")
state("paused_amazon_tablet", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/amazon_tablet.png", backgroundColor: "#cccccc")
state("playing_amazon_tablet", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/amazon_tablet.png", backgroundColor: "#00a0dc")
state("stopped_amazon_tablet", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/amazon_tablet.png")
state("paused_echo_sub_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_sub_gen1.png", backgroundColor: "#cccccc")
state("playing_echo_sub_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_sub_gen1.png", backgroundColor: "#00a0dc")
state("stopped_echo_sub_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_sub_gen1.png")
state("paused_firetv_cube", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_cube.png", backgroundColor: "#cccccc")
state("playing_firetv_cube", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_cube.png", backgroundColor: "#00a0dc")
state("stopped_firetv_cube", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_cube.png")
state("paused_firetv_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_gen1.png", backgroundColor: "#cccccc")
state("playing_firetv_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_gen1.png", backgroundColor: "#00a0dc")
state("stopped_firetv_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_gen1.png")
state("paused_tablet_hd10", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/tablet_hd10.png", backgroundColor: "#cccccc")
state("playing_tablet_hd10", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/tablet_hd10.png", backgroundColor: "#00a0dc")
state("stopped_tablet_hd10", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/tablet_hd10.png")
state("paused_firetv_stick_gen1", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_stick_gen1.png", backgroundColor: "#cccccc")
state("playing_firetv_stick_gen1", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_stick_gen1.png", backgroundColor: "#00a0dc")
state("stopped_firetv_stick_gen1", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/firetv_stick_gen1.png")
state("paused_echo_wha", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_wha.png", backgroundColor: "#cccccc")
state("playing_echo_wha", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_wha.png", backgroundColor: "#00a0dc")
state("stopped_echo_wha", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_wha.png")
state("paused_echo_auto", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png", backgroundColor: "#cccccc")
state("playing_echo_auto", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png", backgroundColor: "#00a0dc")
state("stopped_echo_auto", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_auto.png")
state("paused_echo_input", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png", backgroundColor: "#cccccc")
state("playing_echo_input", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png", backgroundColor: "#00a0dc")
state("stopped_echo_input", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_input.png")
state("paused_one_link", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/one_link.png", backgroundColor: "#cccccc")
state("playing_one_link", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/one_link.png", backgroundColor: "#00a0dc")
state("stopped_one_link", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/one_link.png")
state("paused_sonos_generic", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_generic.png", backgroundColor: "#cccccc")
state("playing_sonos_generic", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_generic.png", backgroundColor: "#00a0dc")
state("stopped_sonos_generic", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_generic.png")
state("paused_sonos_beam", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_beam.png", backgroundColor: "#cccccc")
state("playing_sonos_beam", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_beam.png", backgroundColor: "#00a0dc")
state("stopped_sonos_beam", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/sonos_beam.png")
state("paused_alexa_windows", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/alexa_windows.png", backgroundColor: "#cccccc")
state("playing_alexa_windows", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/alexa_windows.png", backgroundColor: "#00a0dc")
state("stopped_alexa_windows", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/alexa_windows.png")
state("paused_dash_wand", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/dash_wand.png", backgroundColor: "#cccccc")
state("playing_dash_wand", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/dash_wand.png", backgroundColor: "#00a0dc")
state("stopped_dash_wand", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/dash_wand.png")
state("paused_fabriq_chorus", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_chorus.png", backgroundColor: "#cccccc")
state("playing_fabriq_chorus", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_chorus.png", backgroundColor: "#00a0dc")
state("stopped_fabriq_chorus", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_chorus.png")
state("paused_fabriq_riff", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_riff.png", backgroundColor: "#cccccc")
state("playing_fabriq_riff", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_riff.png", backgroundColor: "#00a0dc")
state("stopped_fabriq_riff", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/fabriq_riff.png")
state("paused_lenovo_smarttab_m10", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/lenovo_smarttab_m10.png", backgroundColor: "#cccccc")
state("playing_lenovo_smarttab_m10", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/lenovo_smarttab_m10.png", backgroundColor: "#00a0dc")
state("stopped_lenovo_smarttab_m10", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/lenovo_smarttab_m10.png")
state("paused_vobot_bunny", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/vobot_bunny.png", backgroundColor: "#cccccc")
state("playing_vobot_bunny", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/vobot_bunny.png", backgroundColor: "#00a0dc")
state("stopped_vobot_bunny", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/vobot_bunny.png")
state("paused_logitect_blast", label:"Paused", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/logitect_blast.png", backgroundColor: "#cccccc")
state("playing_logitect_blast", label:"Playing", action:"music Player.pause", nextState: "paused", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/logitect_blast.png", backgroundColor: "#00a0dc")
state("stopped_logitect_blast", label:"Stopped", action:"music Player.play", nextState: "playing", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/logitect_blast.png")
}
valueTile("blank1x1", "device.blank", height: 1, width: 1, inactiveLabel: false, decoration: "flat") {
state("default", label:'')
}
valueTile("blank2x1", "device.blank", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("default", label:'')
}
valueTile("blank2x2", "device.blank", height: 2, width: 2, inactiveLabel: false, decoration: "flat") {
state("default", label:'')
}
valueTile("alarmVolume", "device.alarmVolume", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("alarmVolume", label:'Alarm Volume:\n${currentValue}%')
}
valueTile("deviceStyle", "device.deviceStyle", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("deviceStyle", label:'Device Style:\n${currentValue}')
}
valueTile("onlineStatus", "device.onlineStatus", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("onlineStatus", label:'Online Status:\n${currentValue}')
}
valueTile("mediaSource", "device.mediaSource", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("default", label:'Source:\n${currentValue}')
}
valueTile("currentStation", "device.currentStation", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("default", label:'Station:\n${currentValue}')
}
valueTile("currentAlbum", "device.currentAlbum", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("default", label:'Album:\n${currentValue}')
}
valueTile("lastSpeakCmd", "device.lastSpeakCmd", height: 2, width: 3, inactiveLabel: false, decoration: "flat") {
state("lastSpeakCmd", label:'Last Text Sent:\n${currentValue}')
}
valueTile("lastCmdSentDt", "device.lastCmdSentDt", height: 2, width: 3, inactiveLabel: false, decoration: "flat") {
state("lastCmdSentDt", label:'Last Text Sent:\n${currentValue}')
}
valueTile("lastVoiceActivity", "device.lastVoiceActivity", height: 2, width: 3, inactiveLabel: false, decoration: "flat") {
state("lastVoiceActivity", label:'Last Voice Cmd:\n${currentValue}')
}
valueTile("alexaWakeWord", "device.alexaWakeWord", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("alexaWakeWord", label:'Wake Word:\n${currentValue}')
}
valueTile("supportedMusic", "device.supportedMusic", height: 2, width: 3, inactiveLabel: false, decoration: "flat") {
state("supportedMusic", label:'Supported Music:\n${currentValue}')
}
valueTile("btDeviceConnected", "device.btDeviceConnected", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("btDeviceConnected", label:'Connected Bluetooth Device:\n${currentValue}')
}
valueTile("btDevicesPaired", "device.btDevicesPaired", height: 1, width: 2, inactiveLabel: false, decoration: "flat") {
state("btDevicesPaired", label:'Paired Bluetooth Devices:\n${currentValue}')
}
standardTile("speechTest", "speechTest", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'speechTest', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/speak_test.png")
}
standardTile("searchTest", "searchTest", height: 1, width: 2, decoration: "flat") {
state("default", label:'MusicSearch Test', action: 'searchTest')
}
standardTile("sendTestAnnouncement", "sendTestAnnouncement", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sendTestAnnouncement', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/announcement.png")
}
standardTile("sendTestAnnouncementAll", "sendTestAnnouncementAll", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sendTestAnnouncementAll', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/announcement_all.png")
}
standardTile("stopAllDevices", "stopAllDevices", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'stopAllDevices', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/stop_all.png")
}
standardTile("playWeather", "playWeather", height: 1, width: 1, decoration: "flat") {
state("default", action: 'playWeather', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/weather_report.png")
}
standardTile("playSingASong", "playSingASong", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playSingASong', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/sing_song.png")
}
standardTile("playFlashBrief", "playFlashBrief", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playFlashBrief', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/flash_brief.png")
}
standardTile("sayGoodMorning", "sayGoodMorning", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sayGoodMorning', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/good_morning.png")
}
standardTile("playTraffic", "playTraffic", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playTraffic', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/play_traffic.png")
}
standardTile("playTellStory", "playTellStory", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playTellStory', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/story.png")
}
standardTile("playJoke", "playJoke", height: 1, width: 1, decoration: "flat") {
state("default", action: 'playJoke', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/play_joke.png")
}
standardTile("playFunFact", "playFunFact", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playFunFact', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/fact.png")
}
standardTile("playCalendarToday", "playCalendarToday", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playCalendarToday', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/calendar_today.png")
}
standardTile("playCalendarTomorrow", "playCalendarTomorrow", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playCalendarTomorrow', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/calendar_tomorrow.png")
}
standardTile("playCalendarNext", "playCalendarNext", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'playCalendarNext', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/calendar_next.png")
}
standardTile("sayWelcomeHome", "sayWelcomeHome", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sayWelcomeHome', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/welcome_home.png")
}
standardTile("sayGoodNight", "sayGoodNight", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sayGoodNight', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/good_night.png")
}
standardTile("sayCompliment", "sayCompliment", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'sayCompliment', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/compliment.png")
}
standardTile("volumeUp", "volumeUp", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'volumeUp', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/volume_up.png")
}
standardTile("volumeDown", "volumeDown", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'volumeDown', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/volume_down.png")
}
standardTile("resetQueue", "resetQueue", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'resetQueue', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/reset_queue.png")
}
standardTile("refresh", "device.refresh", width:1, height:1, decoration: "flat") {
state "default", action:"refresh.refresh", icon:"https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/refresh.png"
}
standardTile("doNotDisturb", "device.doNotDisturb", height: 1, width: 1, inactiveLabel: false, decoration: "flat") {
state "true", label: '', action: "doNotDisturbOff", nextState: "false", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/dnd_on.png"
state "false", label: '', action: "doNotDisturbOn", nextState: "true", icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/dnd_off.png"
}
standardTile("followUpMode", "device.followUpMode", height: 1, width: 1, inactiveLabel: false, decoration: "flat") {
state "true", label: 'Followup: On', action: "followUpModeOff", nextState: "false"
state "false", label: 'Followup: Off', action: "followUpModeOn", nextState: "true"
}
standardTile("disconnectBluetooth", "disconnectBluetooth", height: 1, width: 1, decoration: "flat") {
state("default", label:'', action: 'disconnectBluetooth', icon: "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/device/disconnect_bluetooth.png")
}
valueTile("permissions", "device.permissions", height: 2, width: 6, inactiveLabel: false, decoration: "flat") {
state("permissions", label:'Capabilities:\n${currentValue}')
}
main(["deviceStatus"])
details([
"mediaMulti",
"volumeUp", "volumeDown", "stopAllDevices", "doNotDisturb", "refresh", "disconnectBluetooth",
"playWeather", "playSingASong", "playFlashBrief", "playTraffic", "playTellStory", "playFunFact",
"playJoke", "sayWelcomeHome", "sayGoodMorning", "sayGoodNight", "sayCompliment", "resetQueue",
"playCalendarToday", "playCalendarTomorrow", "playCalendarNext", "speechTest", "sendTestAnnouncement", "sendTestAnnouncementAll",
"mediaSource", "currentAlbum", "currentStation",
"alarmVolume", "btDeviceConnected", "btDevicesPaired", "deviceStyle", "onlineStatus", "alexaWakeWord", "supportedMusic", "lastSpeakCmd", "lastCmdSentDt", "lastVoiceActivity",
"permissions"
])
}
preferences {
section("Preferences") {
input "logInfo", "bool", title: "Show Info Logs?", required: false, defaultValue: true
input "logWarn", "bool", title: "Show Warning Logs?", required: false, defaultValue: true
input "logError", "bool", title: "Show Error Logs?", required: false, defaultValue: true
input "logDebug", "bool", title: "Show Debug Logs?", description: "Only leave on when required", required: false, defaultValue: false
input "logTrace", "bool", title: "Show Detailed Logs?", description: "Only Enabled when asked by the developer", required: false, defaultValue: false
input "disableQueue", "bool", required: false, title: "Don't Allow Queuing?", defaultValue: false
input "disableTextTransform", "bool", required: false, title: "Disable Text Transform?", description: "This will attempt to convert items in text like temp units and directions like `WSW` to west southwest", defaultValue: false
input "maxVolume", "number", required: false, title: "Set Max Volume for this device", description: "There will be a delay of 30-60 seconds in getting the current volume level"
input "ttsWordDelay", "number", required: true, title: "Speech queue delay (per character)", description: "Currently there is a 2 second delay per every 14 characters.", defaultValue: 2
input "autoResetQueue", "number", required: false, title: "Auto reset queue (xx seconds) after last speak command", description: "This will reset the queue 3 minutes after last message sent.", defaultValue: 180
}
}
}
def installed() {
logInfo("${device?.displayName} Executing Installed...")
sendEvent(name: "mute", value: "unmuted")
sendEvent(name: "status", value: "stopped")
sendEvent(name: "deviceStatus", value: "stopped_echo_gen1")
sendEvent(name: "trackDescription", value: "")
sendEvent(name: "lastSpeakCmd", value: "Nothing sent yet...")
sendEvent(name: "doNotDisturb", value: false)
sendEvent(name: "onlineStatus", value: "online")
sendEvent(name: "followUpMode", value: false)
sendEvent(name: "alarmVolume", value: 0)
sendEvent(name: "alexaWakeWord", value: "ALEXA")
sendEvent(name: "mediaSource", value: "")
state?.doNotDisturb = false
initialize()
runIn(20, "postInstall")
}
def updated() {
logTrace("${device?.displayName} Executing Updated()")
state?.fullRefreshOk = true
initialize()
}
def initialize() {
logInfo("${device?.displayName} Executing initialize()")
sendEvent(name: "DeviceWatch-DeviceStatus", value: "online")
sendEvent(name: "DeviceWatch-Enroll", value: new groovy.json.JsonOutput().toJson([protocol: "cloud", scheme:"untracked"]), displayed: false)
resetQueue()
stateCleanup()
if(checkMinVersion()) { logError("CODE UPDATE required to RESUME operation. No Device Events will updated."); return; }
schedDataRefresh(true)
refreshData(true)
//TODO: Add Queue cleanup task to schedule. If q_speakingNow != true
//TODO: Have the queue validated based on the last time it was processed and have it cleanup if it's been too long
}
def postInstall() {
if(device?.currentState('level') == 0) { setLevel(30) }
if(device?.currentState('alarmVolume') == 0) { setAlarmVolume(30) }
}
public triggerInitialize() { runIn(3, "initialize") }
String getEchoDeviceType() { return state?.deviceType ?: null }
String getEchoSerial() { return state?.serialNumber ?: null }
String getHealthStatus(lower=false) {
String res = device?.getStatus()
if(lower) { return res?.toString()?.toLowerCase() }
return res as String
}
def getShortDevName(){
return device?.displayName?.replace("Echo - ", "")
}
public setAuthState(authenticated) {
state?.authValid = (authenticated == true)
if(authenticated != true && state?.refreshScheduled) {
removeCookies()
}
}
public updateCookies(cookies) {
logWarn("Cookies Update by Parent. Re-Initializing Device in 5 Seconds...")
state?.cookie = cookies
state?.authValid = true
runIn(5, "initialize")
}
public removeCookies(isParent=false) {
logWarn("Cookie Authentication Cleared by ${isParent ? "Parent" : "Device"} | Scheduled Refreshes also cancelled!")
unschedule("refreshData")
state?.cookie = null
state?.authValid = false
state?.refreshScheduled = false
}
Boolean isAuthOk(noLogs=false) {
if(state?.authValid != true) {
if(state?.refreshScheduled) { unschedule("refreshData"); state?.refreshScheduled = false; }
if(state?.cookie != null) {
if(!noLogs) { logWarn("Echo Speaks Authentication is no longer valid... Please login again and commands will be allowed again!!!", true) }
state?.remove("cookie")
}
return false
} else { return true }
}
Boolean isCommandTypeAllowed(String type, noLogs=false) {
Boolean isOnline = (device?.currentValue("onlineStatus") == "online")
if(!isOnline) { if(!noLogs) { logWarn("Commands NOT Allowed! Device is currently (OFFLINE) | Type: (${type})", true) }; return false; }
if(!isAuthOk(noLogs)) { return false }
if(!getAmazonDomain()) { if(!noLogs) { logWarn("amazonDomain State Value Missing: ${getAmazonDomain()}", true) }; return false }
if(!state?.cookie || !state?.cookie?.cookie || !state?.cookie?.csrf) { if(!noLogs) { logWarn("Amazon Cookie State Values Missing: ${state?.cookie}", true) }; setAuthState(false, null); return false }
if(!state?.serialNumber) { if(!noLogs) { logWarn("SerialNumber State Value Missing: ${state?.serialNumber}", true) }; return false }
if(!state?.deviceType) { if(!noLogs) { logWarn("DeviceType State Value Missing: ${state?.deviceType}", true) }; return false }
if(!state?.deviceOwnerCustomerId) { if(!noLogs) { logWarn("OwnerCustomerId State Value Missing: ${state?.deviceOwnerCustomerId}", true) }; return false; }
if(state?.isSupportedDevice == false) { logWarn("You are using an Unsupported/Unknown Device all restrictions have been removed for testing! If commands function please report device info to developer", true); return true; }
if(!type || state?.permissions == null) { if(!noLogs) { logWarn("Permissions State Object Missing: ${state?.permissions}", true) }; return false }
if(device?.currentValue("doNotDisturb") == "true" && (!(type in ["volumeControl", "alarms", "reminders", "doNotDisturb", "wakeWord", "bluetoothControl", "mediaPlayer"]))) { if(!noLogs) { logWarn("All Voice Output Blocked... Do Not Disturb is ON", true) }; return false }
if(state?.permissions?.containsKey(type) && state?.permissions[type] == true) { return true }
else {
String warnMsg = null
switch(type) {
case "TTS":
warnMsg = "OOPS... Text to Speech is NOT Supported by this Device!!!"
break
case "announce":
warnMsg = "OOPS... Announcements are NOT Supported by this Device!!!"
break
case "followUpMode":
warnMsg = "OOPS... Follow-Up Mode is NOT Supported by this Device!!!"
break
case "mediaPlayer":
warnMsg = "OOPS... Media Player Controls are NOT Supported by this Device!!!"
break
case "volumeControl":
warnMsg = "OOPS... Volume Control is NOT Supported by this Device!!!"
break
case "bluetoothControl":
warnMsg = "OOPS... Bluetooth Control is NOT Supported by this Device!!!"
break
case "alarms":
warnMsg = "OOPS... Alarm Notification are NOT Supported by this Device!!!"
break
case "reminders":
warnMsg = "OOPS... Reminders Notifications are NOT Supported by this Device!!!"
break
case "doNotDisturb":
warnMsg = "OOPS... Do Not Disturb Control is NOT Supported by this Device!!!"
break
case "wakeWord":
warnMsg = "OOPS... Alexa Wake Word Control is NOT Supported by this Device!!!"
break
case "amazonMusic":
warnMsg = "OOPS... Amazon Music is NOT Supported by this Device!!!"
break
case "appleMusic":
warnMsg = "OOPS... Apple Music is NOT Supported by this Device!!!"
break
case "tuneInRadio":
warnMsg = "OOPS... Tune-In Radio is NOT Supported by this Device!!!"
break
case "iHeartRadio":
warnMsg = "OOPS... iHeart Radio is NOT Supported by this Device!!!"
break
case "pandoraRadio":
warnMsg = "OOPS... Pandora Radio is NOT Supported by this Device!!!"
break
case "siriusXm":
warnMsg = "OOPS... Sirius XM Radio is NOT Supported by this Device!!!"
break
// case "tidal":
// warnMsg = "OOPS... Tidal Music is NOT Supported by this Device!!!"
// break
case "spotify":
warnMsg = "OOPS... Spotify is NOT Supported by this Device!!!"
break
case "flashBriefing":
warnMsg = "OOPS... Flash Briefs are NOT Supported by this Device!!!"
break
}
if(warnMsg && !noLogs) { logWarn(warnMsg, true) }
return false
}
}
Boolean permissionOk(type) {
if(type && state?.permissions?.containsKey(type) && state?.permissions[type] == true) { return true }
return false
}
void updateDeviceStatus(Map devData) {
Boolean isOnline = false
if(devData?.size()) {
isOnline = (devData?.online != false)
// log.debug "isOnline: ${isOnline}"
// log.debug "deviceFamily: ${devData?.deviceFamily} | deviceType: ${devData?.deviceType}" // UNCOMMENT to identify unidentified devices
// NOTE: These allow you to log all device data items
// devData?.each { k,v ->
// if(!(k in ["playerState", "capabilities", "deviceAccountId"])) {
// log.debug("$k: $v")
// }
// }
state?.isSupportedDevice = (devData?.unsupported != true)
state?.isEchoDevice = (devData?.isEchoDevice == true)
state?.serialNumber = devData?.serialNumber
state?.deviceType = devData?.deviceType
state?.deviceOwnerCustomerId = devData?.deviceOwnerCustomerId
state?.deviceAccountId = devData?.deviceAccountId
state?.softwareVersion = devData?.softwareVersion
// state?.mainAccountCommsId = devData?.mainAccountCommsId ?: null
// log.debug "mainAccountCommsId: ${state?.mainAccountCommsId}"
state?.cookie = devData?.cookie
state?.authValid = (devData?.authValid == true)
state?.amazonDomain = devData?.amazonDomain
state?.regionLocale = devData?.regionLocale
Map permissions = state?.permissions ?: [:]
devData?.permissionMap?.each {k,v -> permissions[k] = v }
state?.permissions = permissions
state?.hasClusterMembers = devData?.hasClusterMembers
state?.isWhaDevice = (devData?.permissionMap?.isMultiroomDevice == true)
// log.trace "hasClusterMembers: ${ state?.hasClusterMembers}"
// log.trace "permissions: ${state?.permissions}"
List permissionList = permissions?.findAll { it?.value == true }?.collect { it?.key }
if(isStateChange(device, "permissions", permissionList?.toString())) {
sendEvent(name: "permissions", value: permissionList, display: false, displayed: false)
}
Map deviceStyle = devData?.deviceStyle
state?.deviceStyle = devData?.deviceStyle
// logInfo("deviceStyle (${devData?.deviceFamily}): ${devData?.deviceType} | Desc: ${deviceStyle?.name}")
state?.deviceImage = deviceStyle?.image as String
if(isStateChange(device, "deviceStyle", deviceStyle?.name?.toString())) {
sendEvent(name: "deviceStyle", value: deviceStyle?.name?.toString(), descriptionText: "Device Style is ${deviceStyle?.name}", display: true, displayed: true)
}
String firmwareVer = devData?.softwareVersion ?: "Not Set"
if(isStateChange(device, "firmwareVer", firmwareVer?.toString())) {
sendEvent(name: "firmwareVer", value: firmwareVer?.toString(), descriptionText: "Firmware Version is ${firmwareVer}", display: true, displayed: true)
}
String devFamily = devData?.deviceFamily ?: ""
if(isStateChange(device, "deviceFamily", devFamily?.toString())) {
sendEvent(name: "deviceFamily", value: devFamily?.toString(), descriptionText: "Echo Device Family is ${devFamily}", display: true, displayed: true)
}
String devType = devData?.deviceType ?: ""
if(isStateChange(device, "deviceType", devType?.toString())) {
sendEvent(name: "deviceType", value: devType?.toString(), display: false, displayed: false)
}
Map musicProviders = devData?.musicProviders ?: [:]
String lItems = musicProviders?.collect{ it?.value }?.sort()?.join(", ")
if(isStateChange(device, "supportedMusic", lItems?.toString())) {
sendEvent(name: "supportedMusic", value: lItems?.toString(), display: false, displayed: false)
}
if(isStateChange(device, "alexaMusicProviders", musicProviders?.toString())) {
// log.trace "Alexa Music Providers Changed to ${musicProviders}"
sendEvent(name: "alexaMusicProviders", value: musicProviders?.toString(), display: false, displayed: false)
}
// if(devData?.guardStatus) { updGuardStatus(devData?.guardStatus) }
if(!isOnline) {
sendEvent(name: "mute", value: "unmuted")
sendEvent(name: "status", value: "stopped")
sendEvent(name: "deviceStatus", value: "stopped_${state?.deviceStyle?.image}")
sendEvent(name: "trackDescription", value: "")
} else { state?.fullRefreshOk = true; triggerDataRrsh(); }
}
setOnlineStatus(isOnline)
sendEvent(name: "lastUpdated", value: formatDt(new Date()), display: false, displayed: false)
schedDataRefresh()
}
public updSocketStatus(active) {
if(active != true) { schedDataRefresh(true) }
state?.websocketActive = active
}
void websocketUpdEvt(triggers) {
logDebug("websocketEvt: $triggers")
if(state?.isWhaDevice) { return }
if(triggers?.size()) {
triggers?.each { k->
switch(k) {
case "all":
state?.fullRefreshOk = true
runIn(2, "refreshData")
break
case "media":
runIn(2, "getPlaybackState")
break
case "queue":
runIn(4, "getPlaylists")
case "notif":
runIn(2, "getNotifications")
break
case "bluetooth":
runIn(2, "getBluetoothDevices")
break
case "notification":
runIn(2, "getNotifications")
break
case "online":
setOnlineStatus(true)
break
case "offline":
setOnlineStatus(false)
break
case "activity":
runIn(2, "getDeviceActivity")
break
}
//TODO: BUILD A DATA REFRESH QUEUE System
}
}
}
void refresh() {
logTrace("refresh()")
parent?.childInitiatedRefresh()
refreshData(true)
}
private triggerDataRrsh(parentRefresh=false) {
runIn(4, parentRefresh ? "refresh" : "refreshData")
}
public schedDataRefresh(frc) {
if(frc || state?.refreshScheduled != true) {
runEvery1Minute("refreshData")
state?.refreshScheduled = true
}
}
private refreshData(full=false) {
// logTrace("trace", "refreshData()...")
Boolean wsActive = (state?.websocketActive == true)
Boolean isWHA = (state?.isWhaDevice == true)
Boolean isEchoDev = (state?.isEchoDevice == true)
if(device?.currentValue("onlineStatus") != "online") {
logTrace("Skipping Device Data Refresh... Device is OFFLINE... (Offline Status Updated Every 10 Minutes)")
return
}
if(!isAuthOk()) {return}
if(checkMinVersion()) { logError("CODE UPDATE required to RESUME operation. No Device Events will updated."); return; }
// logTrace("permissions: ${state?.permissions}")
if(state?.permissions?.mediaPlayer == true && (full || state?.fullRefreshOk || !wsActive)) {
getPlaybackState()
if(!isWHA) { getPlaylists() }
}
if(!isWHA) {
if (full || state?.fullRefreshOk) {
if(isEchoDev) { getWifiDetails() }
getDeviceSettings()
}
if(state?.permissions?.doNotDisturb == true) { getDoNotDisturb() }
getDeviceActivity()
runIn(3, "refreshStage2")
} else { state?.fullRefreshOk = false }
}
private refreshStage2() {
Boolean wsActive = (state?.websocketActive == true)
if(state?.permissions?.wakeWord) {
getWakeWord()
getAvailableWakeWords()
}
if((state?.permissions?.alarms == true) || (state?.permissions?.reminders == true)) {
if(state?.permissions?.alarms == true) { getAlarmVolume() }
getNotifications()
}
if(state?.permissions?.bluetoothControl && !wsActive) {
getBluetoothDevices()
}
state?.fullRefreshOk = false
// updGuardStatus()
}
public setOnlineStatus(Boolean isOnline) {
String onlStatus = (isOnline ? "online" : "offline")
if(isStateChange(device, "DeviceWatch-DeviceStatus", onlStatus?.toString())) {
sendEvent(name: "DeviceWatch-DeviceStatus", value: onlStatus?.toString(), displayed: false, isStateChange: true)
}
if(isStateChange(device, "onlineStatus", onlStatus?.toString())) {
logDebug("OnlineStatus has changed to (${onlStatus})")
sendEvent(name: "onlineStatus", value: onlStatus?.toString(), displayed: true, isStateChange: true)
}
}
private getPlaybackState(isGroupResponse=false) {
Map params = [
uri: getAmazonUrl(),
path: "/api/np/player",
query: [ deviceSerialNumber: state?.serialNumber, deviceType: state?.deviceType, screenWidth: 2560, _: now() ],
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal() ],
contentType: "application/json"
]
Map playerInfo = [:]
try {
httpGet(params) { response->
Map sData = response?.data ?: [:]
playerInfo = sData?.playerInfo ?: [:]
}
} catch (ex) {
respExceptionHandler(ex, "getPlaybackState", false, true)
}
playbackStateHandler(playerInfo)
}
def playbackStateHandler(playerInfo, isGroupResponse=false) {
// log.debug "playerInfo: ${playerInfo}"
Boolean isPlayStateChange = false
Boolean isMediaInfoChange = false
if (state?.isGroupPlaying && !isGroupResponse) {
logDebug("ignoring getPlaybackState because group is playing here")
return
}
// logTrace("getPlaybackState: ${playerInfo}")
String playState = playerInfo?.state == 'PLAYING' ? "playing" : "stopped"
String deviceStatus = "${playState}_${state?.deviceStyle?.image}"
// log.debug "deviceStatus: ${deviceStatus}"
if(isStateChange(device, "status", playState?.toString()) || isStateChange(device, "deviceStatus", deviceStatus?.toString())) {
logTrace("Status Changed to ${playState}")
isPlayStateChange = true
if (isGroupResponse) {
state?.isGroupPlaying = (playerInfo?.state == 'PLAYING')
}
sendEvent(name: "status", value: playState?.toString(), descriptionText: "Player Status is ${playState}", display: true, displayed: true)
sendEvent(name: "deviceStatus", value: deviceStatus?.toString(), display: false, displayed: false)
}
//Track Title
String title = playerInfo?.infoText?.title ?: ""
if(isStateChange(device, "trackDescription", title?.toString())) {
isMediaInfoChange = true
sendEvent(name: "trackDescription", value: title?.toString(), descriptionText: "Track Description is ${title}", display: true, displayed: true)
}
//Track Sub-Text2
String subText1 = playerInfo?.infoText?.subText1 ?: "Idle"
if(isStateChange(device, "currentAlbum", subText1?.toString())) {
isMediaInfoChange = true
sendEvent(name: "currentAlbum", value: subText1?.toString(), descriptionText: "Album is ${subText1}", display: true, displayed: true)
}
//Track Sub-Text2
String subText2 = playerInfo?.infoText?.subText2 ?: "Idle"
if(isStateChange(device, "currentStation", subText2?.toString())) {
isMediaInfoChange = true
sendEvent(name: "currentStation", value: subText2?.toString(), descriptionText: "Station is ${subText2}", display: true, displayed: true)
}
//Track Art Image
String trackImg = (playerInfo && playerInfo?.mainArt && playerInfo?.mainArt?.url) ? playerInfo?.mainArt?.url : ""
if(isStateChange(device, "trackImage", trackImg?.toString())) {
isMediaInfoChange = true
sendEvent(name: "trackImage", value: trackImg?.toString(), descriptionText: "Track Image is ${trackImg}", display: false, displayed: false)
sendEvent(name: "trackImageHtml", value: """""", display: false, displayed: false)
}
//Media Source Provider
String mediaSource = playerInfo?.provider?.providerName ?: ""
if(isStateChange(device, "mediaSource", mediaSource?.toString())) {
isMediaInfoChange = true
sendEvent(name: "mediaSource", value: mediaSource?.toString(), descriptionText: "Media Source is ${mediaSource}", display: true, displayed: true)
}
//Update Audio Track Data
if (isMediaInfoChange){
Map trackData = [:]
if(playerInfo?.infoText?.title) { trackData?.title = playerInfo?.infoText?.title }
if(playerInfo?.infoText?.subText1) { trackData?.artist = playerInfo?.infoText?.subText1 }
//To avoid media source provider being used as album (ex: Apple Music), only inject `album` if subText2 and providerName are different
if(playerInfo?.infoText?.subText2 && playerInfo?.provider?.providerName!=playerInfo?.infoText?.subText2) { trackData?.album = playerInfo?.infoText?.subText2 }
if(playerInfo?.mainArt?.url) { trackData?.albumArtUrl = playerInfo?.mainArt?.url }
if(playerInfo?.provider?.providerName) { trackData?.mediaSource = playerInfo?.provider?.providerName }
//log.debug(trackData)
sendEvent(name: "audioTrackData", value: new groovy.json.JsonOutput().toJson(trackData), display: false, displayed: false)
}
// Group response data never has valida data for volume
if(!isGroupResponse && playerInfo?.volume) {
if(playerInfo?.volume?.volume != null) {
Integer level = playerInfo?.volume?.volume
if(level < 0) { level = 0 }
if(level > 100) { level = 100 }
if(isStateChange(device, "level", level?.toString()) || isStateChange(device, "volume", level?.toString())) {
logDebug("Volume Level Set to ${level}%")
sendEvent(name: "level", value: level, display: false, displayed: false)
sendEvent(name: "volume", value: level, display: false, displayed: false)
}
}
if(playerInfo?.volume?.muted != null) {
String muteState = (playerInfo?.volume?.muted == true) ? "muted" : "unmuted"
if(isStateChange(device, "mute", muteState?.toString())) {
logDebug("Mute Changed to ${muteState}")
sendEvent(name: "mute", value: muteState, descriptionText: "Volume has been ${muteState}", display: true, displayed: true)
}
}
}
// Update cluster (unless we remain paused)
if (state?.hasClusterMembers && (playerInfo?.state == 'PLAYING' || isPlayStateChange)) {
parent?.sendPlaybackStateToClusterMembers(state?.serialNumber, playerInfo)
}
}
private getAlarmVolume() {
Map params = [
uri: getAmazonUrl(),
path: "/api/device-notification-state/${state?.deviceType}/${device.currentValue("firmwareVer") as String}/${state.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
query: [_: new Date().getTime()],
contentType: "application/json",
]
try {
httpGet(params) { response->
def sData = response?.data ?: null
// logTrace("getAlarmVolume: $sData")
if(sData && isStateChange(device, "alarmVolume", (sData?.volumeLevel ?: 0)?.toString())) {
logDebug("Alarm Volume Changed to ${(sData?.volumeLevel ?: 0)}")
sendEvent(name: "alarmVolume", value: (sData?.volumeLevel ?: 0), display: false, displayed: false)
}
}
} catch (ex) {
respExceptionHandler(ex, "getAlarmVolume")
}
}
private getWakeWord() {
Map params = [
uri: getAmazonUrl(),
path: "/api/wake-word",
query: [cached: true, _: new Date().getTime()],
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
]
try {
httpGet(params) { response->
def sData = response?.data ?: null
// log.debug "sData: $sData"
if(sData && sData?.wakeWords) {
def wakeWord = sData?.wakeWords?.find { it?.deviceSerialNumber == state?.serialNumber } ?: null
// logTrace("getWakeWord: ${wakeWord?.wakeWord}")
if(isStateChange(device, "alexaWakeWord", wakeWord?.wakeWord?.toString())) {
logDebug("Wake Word Changed to ${(wakeWord?.wakeWord)}")
sendEvent(name: "alexaWakeWord", value: wakeWord?.wakeWord, display: false, displayed: false)
}
}
}
} catch (ex) {
respExceptionHandler(ex, "getWakeWord")
}
}
private getWifiDetails() {
Map params = [
uri: getAmazonUrl(),
path: "/api/device-wifi-details",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
query: [ cached: true, _: new Date().getTime(), deviceSerialNumber: state?.serialNumber, deviceType: state?.deviceType ],
contentType: "application/json",
]
try {
httpGet(params) { response->
def sData = response?.data ?: null
// log.debug "sData: $sData"
if(sData && sData?.wakeWords) {
def wakeWord = sData?.wakeWords?.find { it?.deviceSerialNumber == state?.serialNumber } ?: null
// logTrace("getWakeWord: ${wakeWord?.wakeWord}")
if(isStateChange(device, "alexaWakeWord", wakeWord?.wakeWord?.toString())) {
logDebug("Wake Word Changed to ${(wakeWord?.wakeWord)}")
sendEvent(name: "alexaWakeWord", value: wakeWord?.wakeWord, display: false, displayed: false)
}
}
}
} catch (ex) {
respExceptionHandler(ex, "getWifiDetails")
}
}
private getDeviceSettings() {
Map params = [
uri: getAmazonUrl(),
path: "/api/device-preferences",
query: [cached: true, _: new Date().getTime()],
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
]
try {
httpGet(params) { response->
Map sData = response?.data ?: null
// log.debug "sData: $sData"
def devData = sData?.devicePreferences?.find { it?.deviceSerialNumber == state?.serialNumber } ?: null
state?.devicePreferences = devData ?: [:]
// log.debug "devData: $devData"
def fupMode = (devData?.goldfishEnabled == true)
if(isStateChange(device, "followUpMode", fupMode?.toString())) {
logDebug("FollowUp Mode Changed to ${(fupMode)}")
sendEvent(name: "followUpMode", value: fupMode, display: false, displayed: false)
}
// logTrace("getDeviceSettingsHandler: ${sData}")
}
} catch (ex) {
respExceptionHandler(ex, "getDeviceSettings")
}
}
private getAvailableWakeWords() {
Map params = [
uri: getAmazonUrl(),
path: "/api/wake-words-locale",
query: [ cached: true, _: new Date().getTime(), deviceSerialNumber: state?.serialNumber, deviceType: state?.deviceType, softwareVersion: device.currentValue('firmwareVer') ],
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
]
try {
httpGet(params) { response->
Map sData = response?.data ?: null
// log.debug "sData: $sData"
String wakeWords = (sData && sData?.wakeWords) ? sData?.wakeWords?.join(",") : null
logTrace("getAvailableWakeWords: ${wakeWords}")
if(isStateChange(device, "wakeWords", wakeWords?.toString())) {
sendEvent(name: "wakeWords", value: wakeWords, display: false, displayed: false)
}
}
} catch (ex) {
respExceptionHandler(ex, "getAvailableWakeWords")
}
}
def getBluetoothDevices() {
Map btData = parent?.getBluetoothData(state?.serialNumber) ?: [:]
String curConnName = btData?.curConnName ?: null
Map btObjs = btData?.btObjs ?: [:]
// logDebug("Current Bluetooth Device: ${curConnName} | Bluetooth Objects: ${btObjs}")
state?.bluetoothObjs = btObjs
String pairedNames = (btData && btData?.pairedNames) ? btData?.pairedNames?.join(",") : null
// if(isStateChange(device, "btDeviceConnected", curConnName?.toString())) {
// log.info "Bluetooth Device Connected: (${curConnName})"
sendEvent(name: "btDeviceConnected", value: curConnName?.toString(), descriptionText: "Bluetooth Device Connected (${curConnName})", display: true, displayed: true)
// }
if(isStateChange(device, "btDevicesPaired", pairedNames?.toString())) {
logDebug("Paired Bluetooth Devices: ${pairedNames}")
sendEvent(name: "btDevicesPaired", value: pairedNames, descriptionText: "Paired Bluetooth Devices: ${pairedNames}", display: true, displayed: true)
}
}
def updGuardStatus(val=null) {
//TODO: Update this because it's not working
String gState = val ?: (state?.permissions?.guardSupported ? (parent?.getAlexaGuardStatus() ?: "Unknown") : "Not Supported")
if(isStateChange(device, "alexaGuardStatus", gState?.toString())) {
sendEvent(name: "alexaGuardStatus", value: gState, display: false, displayed: false)
logDebug("Alexa Guard Status: (${gState})")
}
}
private String getBtAddrByAddrOrName(String btNameOrAddr) {
Map btObj = state?.bluetoothObjs
String curBtAddr = btObj?.find { it?.value?.friendlyName == btNameOrAddr || it?.value?.address == btNameOrAddr }?.key ?: null
// logDebug("curBtAddr: ${curBtAddr}")
return curBtAddr
}
private getDoNotDisturb() {
Boolean dndEnabled = (parent?.getDndEnabled(state?.serialNumber) == true)
logTrace("getDoNotDisturb: $dndEnabled")
state?.doNotDisturb = dndEnabled
if(isStateChange(device, "doNotDisturb", (dndEnabled == true)?.toString())) {
logInfo("Do Not Disturb: (${(dndEnabled == true)})")
sendEvent(name: "doNotDisturb", value: (dndEnabled == true)?.toString(), descriptionText: "Do Not Disturb Enabled ${(dndEnabled == true)}", display: true, displayed: true)
}
}
private getPlaylists() {
Map params = [
uri: getAmazonUrl(),
path: "/api/cloudplayer/playlists",
query: [ deviceSerialNumber: state?.serialNumber, deviceType: state?.deviceType, mediaOwnerCustomerId: state?.deviceOwnerCustomerId, screenWidth: 2560 ],
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1"],
contentType: "application/json"
]
try {
httpGet(params) { response->
def sData = response?.data ?: null
// logTrace("getPlaylistsHandler: ${sData}")
Map playlists = sData ? sData?.playlists : "{}"
if(isStateChange(device, "alexaPlaylists", playlists?.toString())) {
// log.trace "Alexa Playlists Changed to ${playlists}"
sendEvent(name: "alexaPlaylists", value: playlists, display: false, displayed: false)
}
}
} catch (ex) {
respExceptionHandler(ex, "getPlaylists")
}
}
private getNotifications() {
Map params = [
uri: getAmazonUrl(),
path: "/api/notifications",
query: [cached: true],
headers: [Cookie: getCookieVal(), csrf: getCsrfVal()],
contentType: "application/json"
]
try {
httpGet(params) { response->
List newList = []
def sData = response?.data ?: null
if(sData?.size()) {
List items = sData?.notifications ? sData?.notifications?.findAll { it?.status == "ON" && it?.deviceSerialNumber == state?.serialNumber} : []
items?.each { item->
Map li = [:]
item?.keySet().each { key-> if(key in ['id', 'reminderLabel', 'originalDate', 'originalTime', 'deviceSerialNumber', 'type', 'remainingDuration']) { li[key] = item[key] } }
newList?.push(li)
}
}
if(isStateChange(device, "alexaNotifications", newList?.toString())) {
sendEvent(name: "alexaNotifications", value: newList, display: false, displayed: false)
}
// log.trace "notifications: $newList"
}
} catch (ex) {
respExceptionHandler(ex, "getNotifications")
}
}
private getDeviceActivity() {
Map params = [
uri: getAmazonUrl(),
path: "/api/activities",
query: [ startTime:"", size:"50", offset:"-1" ],
headers: [Cookie: getCookieVal(), csrf: getCsrfVal()],
contentType: "application/json"
]
try {
httpGet(params) { response->
List newList = []
def sData = response?.data ?: null
Boolean wasLastDevice = false
def actTS = null
if (sData && sData?.activities != null) {
def lastCommand = sData?.activities?.find {
(it?.domainAttributes == null || it?.domainAttributes.startsWith("{")) &&
it?.activityStatus?.equals("SUCCESS") &&
it?.utteranceId?.startsWith(it?.sourceDeviceIds?.deviceType)
}
if (lastCommand) {
def lastDescription = new groovy.json.JsonSlurper().parseText(lastCommand?.description)
def spokenText = lastDescription?.summary
def lastDevice = lastCommand?.sourceDeviceIds?.get(0)
if(lastDevice?.serialNumber == state?.serialNumber) {
wasLastDevice = true
if(isStateChange(device, "lastVoiceActivity", spokenText?.toString())) {
sendEvent(name: "lastVoiceActivity", value: spokenText?.toString(), display: false, displayed: false)
}
if(isStateChange(device, "lastSpokenToTime", lastCommand?.creationTimestamp?.toString())) {
sendEvent(name: "lastSpokenToTime", value: lastCommand?.creationTimestamp, display: false, displayed: false)
}
}
}
if(isStateChange(device, "wasLastSpokenToDevice", wasLastDevice?.toString())) {
sendEvent(name: "wasLastSpokenToDevice", value: wasLastDevice, display: false, displayed: false)
}
}
}
} catch (ex) {
respExceptionHandler(ex, "getDeviceActivity")
}
}
String getCookieVal() { return (state?.cookie && state?.cookie?.cookie) ? state?.cookie?.cookie as String : null }
String getCsrfVal() { return (state?.cookie && state?.cookie?.csrf) ? state?.cookie?.csrf as String : null }
/*******************************************************************
Amazon Command Logic
*******************************************************************/
private sendAmazonBasicCommand(String cmdType) {
sendAmazonCommand("POST", [
uri: getAmazonUrl(),
path: "/api/np/command",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal()],
query: [ deviceSerialNumber: state?.serialNumber, deviceType: state?.deviceType ],
contentType: "application/json",
body: [type: cmdType]
], [cmdDesc: cmdType])
}
private execAsyncCmd(String method, String callbackHandler, Map params, Map otherData = null) {
if(method && callbackHandler && params) {
String m = method?.toString()?.toLowerCase()
if(isST()) {
include 'asynchttp_v1'
asynchttp_v1."${m}"(callbackHandler, params, otherData)
} else { "asynchttp${m?.capitalize()}"("${callbackHandler}", params, otherData) }
}
}
private sendAmazonCommand(String method, Map params, Map otherData=null) {
try {
def rData = null
def rStatus = null
switch(method) {
case "POST":
httpPostJson(params) { response->
rData = response?.data ?: null
rStatus = response?.status
}
break
case "PUT":
if(params?.body) { params?.body = new groovy.json.JsonOutput().toJson(params?.body) }
httpPutJson(params) { response->
rData = response?.data ?: null
rStatus = response?.status
}
break
case "DELETE":
httpDelete(params) { response->
rData = response?.data ?: null
rStatus = response?.status
}
break
}
if (otherData?.cmdDesc?.startsWith("connectBluetooth") || otherData?.cmdDesc?.startsWith("disconnectBluetooth") || otherData?.cmdDesc?.startsWith("removeBluetooth")) {
triggerDataRrsh()
} else if(otherData?.cmdDesc?.startsWith("renameDevice")) { triggerDataRrsh(true) }
logDebug("sendAmazonCommand | Status: (${rStatus})${rData != null ? " | Response: ${rData}" : ""} | ${otherData?.cmdDesc} was Successfully Sent!!!")
} catch (ex) {
respExceptionHandler(ex, "${otherData?.cmdDesc}", true)
}
}
private sendSequenceCommand(type, command, value) {
// logTrace("sendSequenceCommand($type) | command: $command | value: $value")
Map seqObj = sequenceBuilder(command, value)
sendAmazonCommand("POST", [
uri: getAmazonUrl(),
path: "/api/behaviors/preview",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: new groovy.json.JsonOutput().toJson(seqObj)
], [cmdDesc: "SequenceCommand (${type})"])
}
private sendMultiSequenceCommand(commands, String srcDesc, Boolean parallel=false) {
String seqType = parallel ? "ParallelNode" : "SerialNode"
List nodeList = []
commands?.each { cmdItem->
if(cmdItem?.command instanceof Map) {
nodeList?.push(cmdItem?.command)
} else { nodeList?.push(createSequenceNode(cmdItem?.command, cmdItem?.value, cmdItem?.devType ?: null, cmdItem?.devSerial ?: null)) }
}
Map seqJson = [ "sequence": [ "@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": [ "@type": "com.amazon.alexa.behaviors.model.${seqType}", "name": null, "nodesToExecute": nodeList ] ] ]
sendSequenceCommand("${srcDesc} | MultiSequence: ${parallel ? "Parallel" : "Sequential"}", seqJson, null)
}
def respExceptionHandler(ex, String mName, clearOn401=false, ignNullMsg=false) {
if(ex instanceof groovyx.net.http.HttpResponseException ) {
Integer sCode = ex?.getResponse()?.getStatus()
def respData = ex?.getResponse()?.getData()
def errMsg = ex?.getMessage()
if(sCode == 401) {
// logError("${mName} | Amazon Authentication is no longer valid | Msg: ${errMsg}")
if(clearOn401) { setAuthState(false) }
} else if (sCode == 400) {
switch(errMsg) {
case "Bad Request":
if(respData && respData?.message == null && ignNullMsg) {
// Ignoring Null message
} else {
if (respData && respData?.message?.startsWith("Music metadata")) {
// Ignoring metadata error message
} else if(respData && respData?.message?.startsWith("Unknown device type in request")) {
// Ignoring Unknown device type in request
} else if(respData && respData?.message?.startsWith("device not connected")) {
// Ignoring device not connect error
} else { logError("${mName} Code: ($sCode) | Message: ${errMsg} | Data: ${respData}") }
}
break
case "Rate Exceeded":
logError("${mName} | Amazon is currently rate-limiting your requests | Msg: ${errMsg}")
break
default:
if(respData && respData?.message == null && ignNullMsg) {
// Ignoring Null message
} else {
logError("${mName} | 400 Error | Msg: ${errMsg}")
}
break
}
} else if(sCode == 429) {
logWarn("${mName} | Too Many Requests Made to Amazon | Msg: ${errMsg}")
} else if(sCode == 200) {
if(errMsg != "OK") { logError("${mName} Response Exception | Status: (${sCode}) | Msg: ${errMsg}") }
} else {
logError("${mName} Response Exception | Status: (${sCode}) | Msg: ${errMsg}")
}
} else if(ex instanceof java.net.SocketTimeoutException) {
logError("${mName} Response Socket Timeout | Msg: ${ex?.getMessage()}")
} else if(ex instanceof java.net.UnknownHostException) {
logError("${mName} HostName Not Found | Msg: ${ex?.getMessage()}")
} else if(ex instanceof org.apache.http.conn.ConnectTimeoutException) {
logError("${mName} Request Timeout | Msg: ${ex?.getMessage()}")
} else { logError("${mName} Exception: ${ex}") }
}
def searchTest() {
searchMusic("thriller", "AMAZON_MUSIC")
}
/*******************************************************************
Device Command FUNCTIONS
*******************************************************************/
def play() {
logTrace("play() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("PlayCommand")
incrementCntByKey("use_cnt_playCmd")
if(isStateChange(device, "status", "playing")) {
sendEvent(name: "status", value: "playing", descriptionText: "Player Status is playing", display: true, displayed: true)
// log.debug "deviceStatus: playing_${state?.deviceStyle?.image}"
sendEvent(name: "deviceStatus", value: "playing_${state?.deviceStyle?.image}", display: false, displayed: false)
}
triggerDataRrsh()
}
}
def playTrack(uri) {
if(isCommandTypeAllowed("TTS")) {
String tts = uriSpeechParser(uri)
if (tts) {
logDebug("playTrack($uri) | Attempting to parse out message from trackUri. This might not work in all scenarios...")
speak(tts as String)
} else {
logWarn("Uh-Oh... The playTrack($uri) Command is NOT Supported by this Device!!!")
}
}
}
def pause() {
logTrace("pause() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("PauseCommand")
incrementCntByKey("use_cnt_pauseCmd")
if(isStateChange(device, "status", "stopped")) {
sendEvent(name: "status", value: "stopped", descriptionText: "Player Status is stopped", display: true, displayed: true)
// log.debug "deviceStatus: stopped_${state?.deviceStyle?.image}"
sendEvent(name: "deviceStatus", value: "stopped_${state?.deviceStyle?.image}", display: false, displayed: false)
}
triggerDataRrsh()
}
}
def stop() {
logTrace("stop() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("PauseCommand")
incrementCntByKey("use_cnt_stopCmd")
if(isStateChange(device, "status", "stopped")) {
sendEvent(name: "status", value: "stopped", descriptionText: "Player Status is stopped", display: true, displayed: true)
}
triggerDataRrsh()
}
}
def togglePlayback() {
logTrace("togglePlayback() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
def isPlaying = (device?.currentValue('status') == "playing")
if(isPlaying) {
stop()
} else {
play()
}
}
}
def stopAllDevices() {
doSequenceCmd("StopAllDevicesCommand", "stopalldevices")
incrementCntByKey("use_cnt_stopAllDevices")
triggerDataRrsh()
}
def previousTrack() {
logTrace("previousTrack() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("PreviousCommand")
incrementCntByKey("use_cnt_prevTrackCmd")
triggerDataRrsh()
}
}
def nextTrack() {
logTrace("nextTrack() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("NextCommand")
incrementCntByKey("use_cnt_nextTrackCmd")
triggerDataRrsh()
}
}
def mute() {
logTrace("mute() command received...")
if(isCommandTypeAllowed("volumeControl")) {
state.muteLevel = device?.currentValue("level")?.toInteger()
incrementCntByKey("use_cnt_muteCmd")
if(isStateChange(device, "mute", "muted")) {
sendEvent(name: "mute", value: "muted", descriptionText: "Mute is set to muted", display: true, displayed: true)
}
setLevel(0)
}
}
def repeat() {
logTrace("repeat() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("RepeatCommand")
incrementCntByKey("use_cnt_repeatCmd")
triggerDataRrsh()
}
}
def shuffle() {
logTrace("shuffle() command received...")
if(isCommandTypeAllowed("mediaPlayer")) {
sendAmazonBasicCommand("ShuffleCommand")
incrementCntByKey("use_cnt_shuffleCmd")
triggerDataRrsh()
}
}
def unmute() {
logTrace("unmute() command received...")
if(isCommandTypeAllowed("volumeControl")) {
if(state?.muteLevel) {
setLevel(state?.muteLevel)
state?.muteLevel = null
incrementCntByKey("use_cnt_unmuteCmd")
if(isStateChange(device, "mute", "unmuted")) {
sendEvent(name: "mute", value: "unmuted", descriptionText: "Mute is set to unmuted", display: true, displayed: true)
}
}
}
}
def setMute(muteState) {
if(muteState) { (muteState == "muted") ? mute() : unmute() }
}
def setLevel(level) {
logTrace("setVolume($level) command received...")
if(isCommandTypeAllowed("volumeControl") && level>=0 && level<=100) {
if(level != device?.currentValue('level')) {
sendSequenceCommand("VolumeCommand", "volume", level)
incrementCntByKey("use_cnt_volumeCmd")
sendEvent(name: "level", value: level?.toInteger(), display: false, displayed: false)
sendEvent(name: "volume", value: level?.toInteger(), display: false, displayed: false)
}
}
}
def setAlarmVolume(vol) {
logTrace("setAlarmVolume($vol) command received...")
if(isCommandTypeAllowed("alarms") && vol>=0 && vol<=100) {
sendAmazonCommand("PUT", [
uri: getAmazonUrl(),
path: "/api/device-notification-state/${state?.deviceType}/${state?.softwareVersion}/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [
deviceSerialNumber: state?.serialNumber,
deviceType: state?.deviceType,
softwareVersion: device?.currentValue('firmwareVer'),
volumeLevel: vol
]
], [cmdDesc: "AlarmVolume"])
incrementCntByKey("use_cnt_alarmVolumeCmd")
sendEvent(name: "alarmVolume", value: vol, display: false, displayed: false)
}
}
def setVolume(vol) {
if(vol) { setLevel(vol?.toInteger()) }
}
def volumeUp() {
def curVol = (device?.currentValue('level') ?: 1)
if(curVol >= 0 && curVol < 100) { setVolume(curVol?.toInteger()+5) }
}
def volumeDown() {
def curVol = (device?.currentValue('level') ?: 0)
if(curVol > 0) { setVolume(curVol?.toInteger()-5) }
}
def setTrack(String uri, metaData="") {
logWarn("Uh-Oh... The setTrack(uri: $uri, meta: $meta) Command is NOT Supported by this Device!!!", true)
}
def resumeTrack() {
logWarn("Uh-Oh... The resumeTrack() Command is NOT Supported by this Device!!!", true)
}
def restoreTrack() {
logWarn("Uh-Oh... The restoreTrack() Command is NOT Supported by this Device!!!", true)
}
def doNotDisturbOff() {
setDoNotDisturb(false)
}
def doNotDisturbOn() {
setDoNotDisturb(true)
}
def followUpModeOff() {
setFollowUpMode(false)
}
def followUpModeOn() {
setFollowUpMode(true)
}
def setDoNotDisturb(Boolean val) {
logTrace("setDoNotDisturb($val) command received...")
if(isCommandTypeAllowed("doNotDisturb")) {
sendAmazonCommand("PUT", [
uri: getAmazonUrl(),
path: "/api/dnd/status",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [
deviceSerialNumber: state?.serialNumber,
deviceType: state?.deviceType,
enabled: (val==true)
]
], [cmdDesc: "SetDoNotDisturb${val ? "On" : "Off"}"])
incrementCntByKey("use_cnt_dndCmd${val ? "On" : "Off"}")
sendEvent(name: "doNotDisturb", value: (val == true)?.toString(), descriptionText: "Do Not Disturb Enabled ${(val == true)}", display: true, displayed: true)
parent?.getDoNotDisturb()
}
}
def setFollowUpMode(Boolean val) {
logTrace("setFollowUpMode($val) command received...")
if(state?.devicePreferences == null || !state?.devicePreferences?.size()) { return }
if(!state?.deviceAccountId) { logError("renameDevice Failed because deviceAccountId is not found..."); return; }
if(isCommandTypeAllowed("followUpMode")) {
sendAmazonCommand("PUT", [
uri: getAmazonUrl(),
path: "/api/device-preferences/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal()],
contentType: "application/json",
body: [
deviceSerialNumber: state?.serialNumber,
deviceType: state?.deviceType,
deviceAccountId: state?.deviceAccountId,
goldfishEnabled: (val==true)
]
], [cmdDesc: "setFollowUpMode${val ? "On" : "Off"}"])
incrementCntByKey("use_cnt_followUpModeCmd${val ? "On" : "Off"}")
}
}
def deviceNotification(String msg) {
logTrace("deviceNotification(msg: $msg) command received...")
if(isCommandTypeAllowed("TTS")) {
if(!msg) { logWarn("No Message sent with deviceNotification($msg) command", true); return; }
// logTrace("deviceNotification(${msg?.toString()?.length() > 200 ? msg?.take(200)?.trim() +"..." : msg})"
incrementCntByKey("use_cnt_devNotif")
speak(msg as String)
}
}
def setVolumeAndSpeak(volume, String msg) {
logTrace("setVolumeAndSpeak(volume: $volume, msg: $msg) command received...")
if(volume != null && permissionOk("volumeControl")) {
state?.newVolume = volume
}
incrementCntByKey("use_cnt_setVolSpeak")
speak(msg)
}
def setVolumeSpeakAndRestore(volume, String msg, restVolume=null) {
logTrace("setVolumeSpeakAndRestore(volume: $volume, msg: $msg, restVolume) command received...")
if(msg) {
if(volume != null && permissionOk("volumeControl")) {
state?.newVolume = volume?.toInteger()
if(restVolume != null) {
state?.oldVolume = restVolume as Integer
incrementCntByKey("use_cnt_setVolSpeak")
} else {
storeCurrentVolume()
incrementCntByKey("use_cnt_setVolumeSpeakRestore")
}
}
speak(msg)
}
}
def storeCurrentVolume() {
Integer curVol = device?.currentValue("level") ?: 1
logTrace("storeCurrentVolume(${curVol}) command received...")
if(curVol != null) { state?.oldVolume = curVol as Integer }
}
private restoreLastVolume() {
Integer lastVol = state?.oldVolume
logTrace("restoreLastVolume(${lastVol}) command received...")
if(lastVol && permissionOk("volumeControl")) {
setVolume(lastVol as Integer)
sendEvent(name: "level", value: lastVol, display: false, displayed: false)
sendEvent(name: "volume", value: lastVol, display: false, displayed: false)
} else { logWarn("Unable to restore Last Volume!!! restoreVolume State Value not found...", true) }
}
def sayWelcomeHome(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "iamhome"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayWelcomeHome")
} else { doSequenceCmd("sayWelcomeHome", "cannedtts_random", "iamhome") }
incrementCntByKey("use_cnt_sayWelcomeHome")
}
def sayCompliment(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "compliments"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayCompliment")
} else { doSequenceCmd("sayCompliment", "cannedtts_random", "compliments") }
incrementCntByKey("use_cnt_sayCompliment")
}
def sayBirthday(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "birthday"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayBirthday")
} else { doSequenceCmd("sayBirthday", "cannedtts_random", "birthday") }
incrementCntByKey("use_cnt_sayBirthday")
}
def sayGoodNight(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "goodnight"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayGoodNight")
} else { doSequenceCmd("sayGoodNight", "cannedtts_random", "goodnight") }
incrementCntByKey("use_cnt_sayGoodNight")
}
def sayGoodMorning(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "goodmorning"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayGoodMorning")
} else { doSequenceCmd("sayGoodMorning", "cannedtts_random", "goodmorning") }
incrementCntByKey("use_cnt_sayGoodMorning")
}
def sayGoodbye(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: "goodbye"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "sayGoodbye")
} else { doSequenceCmd("sayGoodbye", "cannedtts_random", "goodbye") }
incrementCntByKey("use_cnt_sayGoodbye")
}
def executeRoutineId(String rId) {
def execDt = now()
logTrace("executeRoutineId($rId) command received...")
if(!rId) { logWarn("No Routine ID sent with executeRoutineId($rId) command", true) }
if(parent?.executeRoutineById(rId as String)) {
logDebug("Executed Alexa Routine | Process Time: (${(now()-execDt)}ms) | RoutineId: ${rId}")
incrementCntByKey("use_cnt_executeRoutine")
}
}
def playWeather(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "weather"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playWeather")
} else { doSequenceCmd("playWeather", "weather") }
incrementCntByKey("use_cnt_playWeather")
}
def playTraffic(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "traffic"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playTraffic")
} else { doSequenceCmd("playTraffic", "traffic") }
incrementCntByKey("use_cnt_playTraffic")
}
def playSingASong(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "singasong"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playSingASong")
} else { doSequenceCmd("playSingASong", "singasong") }
incrementCntByKey("use_cnt_playSong")
}
def playFlashBrief(volume=null, restoreVolume=null) {
if(isCommandTypeAllowed("flashBriefing")) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "flashbriefing"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playFlashBrief")
} else { doSequenceCmd("playFlashBrief", "flashbriefing") }
incrementCntByKey("use_cnt_playBrief")
}
}
def playTellStory(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "tellstory"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playTellStory")
} else { doSequenceCmd("playTellStory", "tellstory") }
incrementCntByKey("use_cnt_playStory")
}
def playFunFact(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "funfact"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playFunFact")
} else { doSequenceCmd("playFunFact", "funfact") }
incrementCntByKey("use_cnt_funfact")
}
def playJoke(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "joke"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playJoke")
} else { doSequenceCmd("playJoke", "joke") }
incrementCntByKey("use_cnt_joke")
}
def playCalendarToday(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "calendartoday"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playCalendarToday")
} else { doSequenceCmd("playCalendarToday", "calendartoday") }
incrementCntByKey("use_cnt_calendarToday")
}
def playCalendarTomorrow(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "calendartomorrow"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playCalendarTomorrow")
} else { doSequenceCmd("playCalendarTomorrow", "calendartomorrow") }
incrementCntByKey("use_cnt_calendarTomorrow")
}
def playCalendarNext(volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "calendarnext"]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playCalendarNext")
} else { doSequenceCmd("playCalendarNext", "calendarnext") }
incrementCntByKey("use_cnt_calendarNext")
}
def playCannedRandomTts(String type, volume=null, restoreVolume=null) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "cannedtts_random", value: type]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playCannedRandomTts($type)")
} else { doSequenceCmd("playCannedRandomTts($type)", "cannedtts_random", type) }
incrementCntByKey("use_cnt_playCannedRandomTTS")
}
def playAnnouncement(String msg, volume=null, restoreVolume=null) {
if(isCommandTypeAllowed("announce")) {
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "announcement", value: msg]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playAnnouncement")
} else { doSequenceCmd("playAnnouncement", "announcement", msg) }
incrementCntByKey("use_cnt_announcement")
}
}
def playAnnouncement(String msg, String title, volume=null, restoreVolume=null) {
if(isCommandTypeAllowed("announce")) {
msg = "${title ? "${title}::" : ""}${msg}"
if(volume != null) {
List seqs = [[command: "volume", value: volume], [command: "announcement", value: msg]]
if(restoreVolume != null) { seqs?.push([command: "volume", value: restoreVolume]) }
sendMultiSequenceCommand(seqs, "playAnnouncement")
} else { doSequenceCmd("playAnnouncement", "announcement", msg) }
incrementCntByKey("use_cnt_announcement")
}
}
def sendAnnouncementToDevices(String msg, String title=null, devObj, volume=null, restoreVolume=null) {
// log.debug "sendAnnouncementToDevices(msg: $msg, title: $title, devObj: devObj, volume: $volume, restoreVolume: $restoreVolume)"
if(isCommandTypeAllowed("announce") && devObj) {
def devJson = new groovy.json.JsonOutput().toJson(devObj)
msg = "${title ?: "Echo Speaks"}::${msg}::${devJson?.toString()}"
// log.debug "sendAnnouncementToDevices | msg: ${msg}"
if(volume || restoreVolume) {
List mainSeq = []
if(volume) { devObj?.each { dev-> mainSeq?.push([command: "volume", value: volume, devType: dev?.deviceTypeId, devSerial: dev?.deviceSerialNumber]) } }
mainSeq?.push([command: "announcement_devices", value: msg])
if(restoreVolume) { devObj?.each { dev-> mainSeq?.push([command: "volume", value: restoreVolume, devType: dev?.deviceTypeId, devSerial: dev?.deviceSerialNumber]) } }
sendMultiSequenceCommand(mainSeq, "sendAnnouncementToDevices")
} else { doSequenceCmd("sendAnnouncementToDevices", "announcement_devices", msg) }
incrementCntByKey("use_cnt_announcementDevices")
}
}
def playAnnouncementAll(String msg, String title=null) {
// if(isCommandTypeAllowed("announce")) {bvxdsa
doSequenceCmd("AnnouncementAll", "announcementall", msg)
incrementCntByKey("use_cnt_announcementAll")
// }
}
def searchMusic(String searchPhrase, String providerId, volume=null, sleepSeconds=null) {
// logTrace("searchMusic(${searchPhrase}, ${providerId})")
if(isCommandTypeAllowed(getCommandTypeForProvider(providerId))) {
doSearchMusicCmd(searchPhrase, providerId, volume, sleepSeconds)
} else { logWarn("searchMusic not supported for ${providerId}", true) }
}
String getCommandTypeForProvider(String providerId) {
def commandType = providerId
switch (providerId) {
case "AMAZON_MUSIC":
commandType = "amazonMusic"
break
case "APPLE_MUSIC":
commandType = "appleMusic"
break
case "TUNEIN":
commandType = "tuneInRadio"
break
case "PANDORA":
commandType = "pandoraRadio"
break
case "SIRIUSXM":
commandType = "siriusXm"
break
case "SPOTIFY":
commandType = "spotify"
break
// case "TIDAL":
// commandType = "tidal"
// break
case "I_HEART_RADIO":
commandType = "iHeartRadio"
break
}
return commandType
}
def searchAmazonMusic(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("amazonMusic")) {
doSearchMusicCmd(searchPhrase, "AMAZON_MUSIC", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchAmazonMusic")
}
}
def searchAppleMusic(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("appleMusic")) {
doSearchMusicCmd(searchPhrase, "APPLE_MUSIC", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchAppleMusic")
}
}
def searchTuneIn(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("tuneInRadio")) {
doSearchMusicCmd(searchPhrase, "TUNEIN", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchTuneInRadio")
}
}
def searchPandora(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("pandoraRadio")) {
doSearchMusicCmd(searchPhrase, "PANDORA", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchPandoraRadio")
}
}
def searchSiriusXm(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("siriusXm")) {
doSearchMusicCmd(searchPhrase, "SIRIUSXM", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchSiriusXmRadio")
}
}
def searchSpotify(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("spotify")) {
doSearchMusicCmd(searchPhrase, "SPOTIFY", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchSpotifyMusic")
}
}
// def searchTidal(String searchPhrase, volume=null, sleepSeconds=null) {
// if(isCommandTypeAllowed("tidal")) {
// doSearchMusicCmd(searchPhrase, "TIDAL", volume, sleepSeconds)
// incrementCntByKey("use_cnt_searchTidal")
// }
// }
def searchIheart(String searchPhrase, volume=null, sleepSeconds=null) {
if(isCommandTypeAllowed("iHeartRadio")) {
doSearchMusicCmd(searchPhrase, "I_HEART_RADIO", volume, sleepSeconds)
incrementCntByKey("use_cnt_searchIheartRadio")
}
}
private doSequenceCmd(cmdType, seqCmd, seqVal="") {
if(state?.serialNumber) {
logDebug("Sending (${cmdType}) | Command: ${seqCmd} | Value: ${seqVal}")
sendSequenceCommand(cmdType, seqCmd, seqVal)
} else { logWarn("doSequenceCmd Error | You are missing one of the following... SerialNumber: ${state?.serialNumber}", true) }
}
private doSearchMusicCmd(searchPhrase, musicProvId, volume=null, sleepSeconds=null) {
if(state?.serialNumber && searchPhrase && musicProvId) {
playMusicProvider(searchPhrase, musicProvId, volume, sleepSeconds)
// incrementCntByKey("use_cnt_searchMusic")
} else { logWarn("doSearchMusicCmd Error | You are missing one of the following... SerialNumber: ${state?.serialNumber} | searchPhrase: ${searchPhrase} | musicProvider: ${musicProvId}", true) }
}
private Map validateMusicSearch(searchPhrase, providerId, sleepSeconds=null) {
Map validObj = [
type: "Alexa.Music.PlaySearchPhrase",
operationPayload: [
deviceType: state?.deviceType,
deviceSerialNumber: state?.serialNumber,
customerId: state?.deviceOwnerCustomerId,
locale: (state?.regionLocale ?: "en-US"),
musicProviderId: providerId,
searchPhrase: searchPhrase
]
]
if(sleepSeconds) { validObj?.operationPayload?.waitTimeInSeconds = sleepSeconds }
validObj?.operationPayload = new groovy.json.JsonOutput().toJson(validObj?.operationPayload)
Map params = [
uri: getAmazonUrl(),
path: "/api/behaviors/operation/validate",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: new groovy.json.JsonOutput().toJson(validObj)
]
Map result = null
try {
httpPost(params) { resp->
Map rData = resp?.data ?: null
if(resp?.status == 200) {
if (rData?.result != "VALID") {
logError("Amazon the Music Search Request as Invalid | MusicProvider: [${providerId}] | Search Phrase: (${searchPhrase})")
result = null
} else { result = rData }
} else { logError("validateMusicSearch Request failed with status: (${resp?.status}) | MusicProvider: [${providerId}] | Search Phrase: (${searchPhrase})") }
}
} catch (ex) {
respExceptionHandler(ex, "validateMusicSearch")
}
return result
}
private getMusicSearchObj(String searchPhrase, String providerId, sleepSeconds=null) {
if (searchPhrase == "") { logError("getMusicSearchObj Searchphrase empty"); return; }
Map validObj = [type: "Alexa.Music.PlaySearchPhrase", "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"]
Map validResp = validateMusicSearch(searchPhrase, providerId, sleepSeconds)
if(validResp && validResp?.operationPayload) {
validObj?.operationPayload = validResp?.operationPayload
} else {
logError("Something went wrong with the Music Search | MusicProvider: [${providerId}] | Search Phrase: (${searchPhrase})")
validObj = null
}
return validObj
}
private playMusicProvider(searchPhrase, providerId, volume=null, sleepSeconds=null) {
logTrace("playMusicProvider() command received... | searchPhrase: $searchPhrase | providerId: $providerId | sleepSeconds: $sleepSeconds")
Map validObj = getMusicSearchObj(searchPhrase, providerId, sleepSeconds)
if(!validObj) { return }
Map seqJson = ["@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": validObj]
seqJson?.startNode["@type"] = "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"
if(volume) {
sendMultiSequenceCommand([ [command: "volume", value: volume], [command: validObj] ], "playMusicProvider(${providerId})", true)
} else { sendSequenceCommand("playMusicProvider(${providerId})", seqJson, null) }
}
def setWakeWord(String newWord) {
logTrace("setWakeWord($newWord) command received...")
String oldWord = device?.currentValue('alexaWakeWord')
def wwList = device?.currentValue('wakeWords') ?: []
logDebug("newWord: $newWord | oldWord: $oldWord | wwList: $wwList (${wwList?.contains(newWord.toString()?.toUpperCase())})")
if(oldWord && newWord && wwList && wwList?.contains(newWord.toString()?.toUpperCase())) {
sendAmazonCommand("PUT", [
uri: getAmazonUrl(),
path: "/api/wake-word/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [
active: true,
deviceSerialNumber: state?.serialNumber,
deviceType: state?.deviceType,
displayName: oldWord,
midFieldState: null,
wakeWord: newWord
]
], [cmdDesc: "SetWakeWord(${newWord})"])
incrementCntByKey("use_cnt_setWakeWord")
sendEvent(name: "alexaWakeWord", value: newWord?.toString()?.toUpperCase(), display: true, displayed: true)
} else { logWarn("setWakeWord is Missing a Required Parameter!!!", true) }
}
def createAlarm(String alarmLbl, String alarmDate, String alarmTime) {
logTrace("createAlarm($alarmLbl, $alarmDate, $alarmTime) command received...")
if(alarmLbl && alarmDate && alarmTime) {
createNotification("Alarm", [
cmdType: "CreateAlarm",
label: alarmLbl?.toString()?.replaceAll(" ", ""),
date: alarmDate,
time: alarmTime,
type: "Alarm"
])
incrementCntByKey("use_cnt_createAlarm")
} else { logWarn("createAlarm is Missing a Required Parameter!!!", true) }
}
def createReminder(String remLbl, String remDate, String remTime) {
logTrace("createReminder($remLbl, $remDate, $remTime) command received...")
if(isCommandTypeAllowed("alarms")) {
if(remLbl && remDate && remTime) {
createNotification("Reminder", [
cmdType: "CreateReminder",
label: remLbl?.toString(),
date: remDate?.toString(),
time: remTime?.toString(),
type: "Reminder"
])
incrementCntByKey("use_cnt_createReminder")
} else { logWarn("createReminder is Missing the Required (id) Parameter!!!", true) }
}
}
def removeNotification(String id) {
logTrace("removeNotification($id) command received...")
if(isCommandTypeAllowed("alarms") || isCommandTypeAllowed("reminders", true)) {
if(id) {
sendAmazonCommand("DELETE", [
uri: getAmazonUrl(),
path: "/api/notifications/${id}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: []
], [cmdDesc: "RemoveNotification"])
incrementCntByKey("use_cnt_removeNotification")
} else { logWarn("removeNotification is Missing the Required (id) Parameter!!!", true) }
}
}
private createNotification(type, options) {
def now = new Date()
def createdDate = now.getTime()
def addSeconds = new Date(createdDate + 1 * 60000);
def alarmTime = type != "Timer" ? addSeconds.getTime() : 0
// log.debug "addSeconds: $addSeconds | alarmTime: $alarmTime"
Map params = [
uri: getAmazonUrl(),
path: "/api/notifications/create${type}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [
type: type,
status: "ON",
alarmTime: alarmTime,
createdDate: createdDate,
originalTime: type != "Timer" ? "${options?.time}:00.000" : null,
originalDate: type != "Timer" ? options?.date : null,
timeZoneId: null,
reminderIndex: null,
sound: null,
deviceSerialNumber: state?.serialNumber,
deviceType: state?.deviceType,
timeZoneId: null,
recurringPattern: type != "Timer" ? '' : null,
alarmLabel: type == "Alarm" ? options?.label : null,
reminderLabel: type == "Reminder" ? options?.label : null,
reminderSubLabel: "Echo Speaks",
timerLabel: type == "Timer" ? options?.label : null,
skillInfo: null,
isSaveInFlight: type != "Timer" ? true : null,
triggerTime: 0,
id: "create${type}",
isRecurring: false,
remainingDuration: type != "Timer" ? 0 : options?.timerDuration
]
]
sendAmazonCommand("PUT", params, [cmdDesc: "Create${type}"])
}
def renameDevice(newName) {
logTrace("renameDevice($newName) command received...")
if(!state?.deviceAccountId) { logError("renameDevice Failed because deviceAccountId is not found..."); return; }
sendAmazonCommand("PUT", [
uri: getAmazonUrl(),
path: "/api/devices-v2/device/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [
serialNumber: state?.serialNumber,
deviceType: state?.deviceType,
deviceAccountId: state?.deviceAccountId,
accountName: newName
]
], [cmdDesc: "renameDevice(${newName})"])
incrementCntByKey("use_cnt_renameDevice")
}
def connectBluetooth(String btNameOrAddr) {
logTrace("connectBluetooth(${btName}) command received...")
if(isCommandTypeAllowed("bluetoothControl")) {
String curBtAddr = getBtAddrByAddrOrName(btNameOrAddr as String)
if(curBtAddr) {
sendAmazonCommand("POST", [
uri: getAmazonUrl(),
path: "/api/bluetooth/pair-sink/${state?.deviceType}/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [ bluetoothDeviceAddress: curBtAddr ]
], [cmdDesc: "connectBluetooth($btNameOrAddr)"])
incrementCntByKey("use_cnt_connectBluetooth")
sendEvent(name: "btDeviceConnected", value: btNameOrAddr, display: true, displayed: true)
} else { logError("ConnectBluetooth Error: Unable to find the connected bluetooth device address...") }
}
}
def disconnectBluetooth() {
logTrace("disconnectBluetooth() command received...")
if(isCommandTypeAllowed("bluetoothControl")) {
String curBtAddr = getBtAddrByAddrOrName(device?.currentValue("btDeviceConnected") as String)
if(curBtAddr) {
sendAmazonCommand("POST", [
uri: getAmazonUrl(),
path: "/api/bluetooth/disconnect-sink/${state?.deviceType}/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [ bluetoothDeviceAddress: curBtAddr ]
], [cmdDesc: "disconnectBluetooth"])
incrementCntByKey("use_cnt_disconnectBluetooth")
} else { logError("DisconnectBluetooth Error: Unable to find the connected bluetooth device address...") }
}
}
def removeBluetooth(String btNameOrAddr) {
logTrace("removeBluetooth(${btNameOrAddr}) command received...")
if(isCommandTypeAllowed("bluetoothControl")) {
String curBtAddr = getBtAddrByAddrOrName(btNameOrAddr)
if(curBtAddr) {
sendAmazonCommand("POST", [
uri: getAmazonUrl(),
path: "/api/bluetooth/unpair-sink/${state?.deviceType}/${state?.serialNumber}",
headers: [ Cookie: getCookieVal(), csrf: getCsrfVal(), Connection: "keep-alive", DNT: "1" ],
contentType: "application/json",
body: [ bluetoothDeviceAddress: curBtAddr, bluetoothDeviceClass: "OTHER" ]
], [cmdDesc: "removeBluetooth(${btNameOrAddr})"])
incrementCntByKey("use_cnt_removeBluetooth")
} else { logError("RemoveBluetooth Error: Unable to find the connected bluetooth device address...") }
}
}
def sendAlexaAppNotification(String text) {
// log.debug "sendAlexaAppNotification(${text})"
doSequenceCmd("AlexaAppNotification", "pushnotification", text)
incrementCntByKey("use_cnt_alexaAppNotification")
}
def getRandomItem(items) {
def list = new ArrayList();
items?.each { list?.add(it) }
return list?.get(new Random().nextInt(list?.size()));
}
def replayText() {
logTrace("replayText() command received...")
String lastText = device?.currentValue("lastSpeakCmd")?.toString()
if(lastText) { speak(lastText) } else { log.warn "Last Text was not found" }
}
def playText(String msg) {
logTrace("playText(msg: $msg) command received...")
speak(msg as String)
}
def playTrackAndResume(uri, duration, volume=null) {
if(isCommandTypeAllowed("TTS")) {
String tts = uriSpeechParser(uri)
if (tts) {
logDebug("playTrackAndResume($uri, $volume) | Attempting to parse out message from trackUri. This might not work in all scenarios...")
if(volume) {
def restVolume = device?.currentValue("level")?.toInteger()
setVolumeSpeakAndRestore(volume as Integer, text as String, restVolume as Integer)
} else { speak(tts as String) }
} else {
logWarn("Uh-Oh... The playTrackAndResume($uri, $volume) Command is NOT Supported by this Device!!!")
}
}
}
def playTextAndResume(text, volume=null) {
logTrace("The playTextAndResume(text: $text, volume: $volume) command received...")
def restVolume = device?.currentValue("level")?.toInteger()
if (volume != null) {
setVolumeSpeakAndRestore(volume as Integer, text as String, restVolume as Integer)
} else { speak(text as String) }
}
def playTrackAndRestore(uri, duration, volume=null) {
if(isCommandTypeAllowed("TTS")) {
String tts = uriSpeechParser(uri)
if (tts) {
logDebug("playTrackAndRestore($uri, $volume) | Attempting to parse out message from trackUri. This might not work in all scenarios...")
if(volume) {
def restVolume = device?.currentValue("level")?.toInteger()
setVolumeSpeakAndRestore(volume as Integer, text as String, restVolume as Integer)
} else { speak(tts as String) }
} else {
logWarn("Uh-Oh... The playTrackAndRestore(uri: $uri, duration: $duration, volume: $volume) Command is NOT Supported by this Device!!!")
}
}
}
def playTextAndRestore(text, volume=null) {
logTrace("The playTextAndRestore($text, $volume) command received...")
def restVolume = device?.currentValue("level")?.toInteger()
if (volume != null) {
setVolumeSpeakAndRestore(volume as Integer, text as String, restVolume as Integer)
} else { speak(text as String) }
}
def playURL(uri) {
if(isCommandTypeAllowed("TTS")) {
String tts = uriSpeechParser(uri)
if (tts) {
logDebug("playURL($uri) | Attempting to parse out message from trackUri. This might not work in all scenarios...")
speak(tts as String)
} else {
logWarn("Uh-Oh... The playUrl($uri) Command is NOT Supported by this Device!!!")
}
}
}
def playSoundAndTrack(soundUri, duration, trackData, volume=null) {
logWarn("Uh-Oh... The playSoundAndTrack(soundUri: $soundUri, duration: $duration, trackData: $trackData, volume: $volume) Command is NOT Supported by this Device!!!", true)
}
String uriSpeechParser(uri) {
// Thanks @fkrlaframboise for this idea. It never for one second occurred to me to parse out the trackUri...
if (uri?.toString()?.contains("/")) {
Integer sInd = uri?.lastIndexOf("/") + 1
uri = uri?.substring(sInd, uri?.size())?.toLowerCase()?.replace(".mp3", "")
logDebug("uriSpeechParser | tts: $uri")
return uri
}
return null
}
def speechTest(ttsMsg) {
List items = [
"Testing Testing 1, 2, 3",
"Yay!, I'm Alive... Hopefully you can hear me speaking?",
"Everybody have fun tonight. Everybody have fun tonight. Everybody Wang Chung tonight. Everybody have fun.",
"Being able to make me say whatever you want is the coolest thing since sliced bread!",
"I said a hip hop, Hippie to the hippie, The hip, hip a hop, and you don't stop, a rock it out, Bubba to the bang bang boogie, boobie to the boogie To the rhythm of the boogie the beat, Now, what you hear is not a test, I'm rappin' to the beat",
"This is how we do it!. It's Friday night, and I feel alright. The party is here on the West side. So I reach for my 40 and I turn it up. Designated driver take the keys to my truck, Hit the shore 'cause I'm faded, Honeys in the street say, Monty, yo we made it!. It feels so good in my hood tonight, The summertime skirts and the guys in Khannye.",
"Teenage Mutant Ninja Turtles, Teenage Mutant Ninja Turtles, Teenage Mutant Ninja Turtles, Heroes in a half-shell Turtle power!... They're the world's most fearsome fighting team (We're really hip!), They're heroes in a half-shell and they're green (Hey - get a grip!), When the evil Shredder attacks!!!, These Turtle boys don't cut him no slack!."
]
if(!ttsMsg) { ttsMsg = getRandomItem(items) }
speak(ttsMsg as String)
}
def speak(String msg) {
logTrace("speak() command received...")
if(isCommandTypeAllowed("TTS")) {
if(!msg) { logWarn("No Message sent with speak($msg) command", true) }
// msg = cleanString(msg, true)
speechCmd([cmdDesc: "SpeakCommand", message: msg, newVolume: (state?.newVolume ?: null), oldVolume: (state?.oldVolume ?: null), cmdDt: now()])
incrementCntByKey("use_cnt_speak")
}
}
String cleanString(str, frcTrans=false) {
if(!str) { return null }
//Cleans up characters from message
str?.replaceAll(~/[^a-zA-Z0-9-?%°., ]+/, "")?.replaceAll(/\s\s+/, " ")
str = textTransform(str, frcTrans)
// log.debug "cleanString: $str"
return str
}
private String textTransform(String str, force=false) {
if(!force && settings?.disableTextTransform == true) { return str; }
// Converts F temp values to readable text "19F"
str = str?.replaceAll(/([+-]?\d+)\s?([CcFf])/) { return "${it[0]?.toString()?.replaceAll("[-]", "minus ")?.replaceAll("[FfCc]", " degrees")}" }
str = str?.replaceAll(/(\sWSW\s)/, " west southwest ")?.replaceAll(/(\sWNW\s)/, " west northwest ")?.replaceAll(/(\sESE\s)/, " east southeast ")?.replaceAll(/(\sENE\s)/, " east northeast ")
str = str?.replaceAll(/(\sSSE\s)/, " south southeast ")?.replaceAll(/(\sSSW\s)/, " south southwest ")?.replaceAll(/(\sNNE\s)/, " north northeast ")?.replaceAll(/(\sNNW\s)/, " north northwest ")
str = str?.replaceAll(/(\sNW\s)/, " northwest ")?.replaceAll(/(\sNE\s)/, " northeast ")?.replaceAll(/(\sSW\s)/, " southwest ")?.replaceAll(/(\sSE\s)/, " southeast ")
str = str?.replaceAll(/(\sE\s)/," east ")?.replaceAll(/(\sS\s)/," south ")?.replaceAll(/(\sN\s)/," north ")?.replaceAll(/(\sW\s)/," west ")
str = str?.replaceAll("%"," percent ")
str = str?.replaceAll("°"," degree ")
return str
}
Integer getStringLen(str) { return (str && str?.toString()?.length()) ? str?.toString()?.length() : 0 }
private List msgSeqBuilder(String str) {
// log.debug "msgSeqBuilder: $str"
List seqCmds = []
List strArr = []
Boolean isSSML = (str?.toString()?.startsWith("") && str?.toString()?.endsWith(""))
if(str?.toString()?.length() < 450) {
seqCmds?.push([command: (isSSML ? "ssml": "speak"), value: str as String])
} else {
List msgItems = str?.split()
msgItems?.each { wd->
if((getStringLen(strArr?.join(" ")) + wd?.length()) <= 430) {
// log.debug "CurArrLen: ${(getStringLen(strArr?.join(" ")))} | CurStrLen: (${wd?.length()})"
strArr?.push(wd as String)
} else { seqCmds?.push([command: (isSSML ? "ssml": "speak"), value: strArr?.join(" ")]); strArr = []; strArr?.push(wd as String); }
if(wd == msgItems?.last()) { seqCmds?.push([command: (isSSML ? "ssml": "speak"), value: strArr?.join(" ")]) }
}
}
// log.debug "seqCmds: $seqCmds"
return seqCmds
}
def sendTestAnnouncement() {
playAnnouncement("Echo Speaks announcement test on ${device?.label?.replace("Echo - ", "")}")
}
def sendTestAnnouncementAll() {
playAnnouncementAll("Echo Speaks Announcement Test on All devices")
}
def sendTestAlexaMsg() {
sendAlexaAppNotification("Test Alexa Notification from ${device?.displayName}")
}
Map seqItemsAvail() {
return [
other: [
"weather":null, "traffic":null, "flashbriefing":null, "goodmorning":null, "goodnight":null, "cleanup":null,
"singasong":null, "tellstory":null, "funfact":null, "joke":null, "playsearch":null, "calendartoday":null,
"calendartomorrow":null, "calendarnext":null, "stop":null, "stopalldevices":null,
"dnd_duration": "2H30M", "dnd_time": "00:30", "dnd_all_duration": "2H30M", "dnd_all_time": "00:30",
"dnd_duration":"2H30M", "dnd_time":"00:30",
"cannedtts_random": ["goodbye", "confirmations", "goodmorning", "compliments", "birthday", "goodnight", "iamhome"],
"wait": "value (seconds)", "volume": "value (0-100)", "speak": "message", "announcement": "message",
"announcementall": "message", "pushnotification": "message", "email": null
],
music: [
"amazonmusic": "AMAZON_MUSIC", "applemusic": "APPLE_MUSIC", "iheartradio": "I_HEART_RADIO", "pandora": "PANDORA",
"spotify": "SPOTIFY", "tunein": "TUNEIN", "cloudplayer": "CLOUDPLAYER"
],
musicAlt: [
"amazonmusic": "amazonMusic", "applemusic": "appleMusic", "iheartradio": "iHeartRadio", "pandora": "pandoraRadio",
"spotify": "spotify", "tunein": "tuneInRadio", "cloudplayer": "cloudPlayer"
]
]
}
def executeSequenceCommand(String seqStr) {
if(seqStr) {
List seqList = seqStr?.split(",,")
// log.debug "seqList: ${seqList}"
List seqItems = []
if(seqList?.size()) {
seqList?.each {
def li = it?.toString()?.split("::")
// log.debug "li: $li"
if(li?.size()) {
String cmd = li[0]?.trim()?.toString()?.toLowerCase() as String
Boolean isValidCmd = (seqItemsAvail()?.other?.containsKey(cmd) || (seqItemsAvail()?.music?.containsKey(cmd)) || (seqItemsAvail()?.dnd?.containsKey(cmd)))
Boolean isMusicCmd = (seqItemsAvail()?.music?.containsKey(cmd) && !seqItemsAvail()?.other?.containsKey(cmd) && !seqItemsAvail()?.dnd?.containsKey(cmd))
// log.debug "cmd: $cmd | isValidCmd: $isValidCmd | isMusicCmd: $isMusicCmd"
if(!isValidCmd) { logError("executeSequenceCommand command ($cmd) is not a valid sequence command!!!"); return; }
if(isMusicCmd) {
List valObj = (li[1]?.trim()?.toString()?.contains("::")) ? li[1]?.trim()?.split("::") : [li[1]?.trim() as String]
String provID = seqItemsAvail()?.music[cmd]
if(!isCommandTypeAllowed(seqItemsAvail()?.musicAlt[cmd])) { logError("Current Music Sequence command ($cmd) not allowed... "); return; }
if (!valObj || valObj[0] == "") { logError("Play Music Sequence it Searchphrase empty"); return; }
Map validObj = getMusicSearchObj(valObj[0], provID, valObj[1] ?: null)
if(!validObj) { return }
seqItems?.push([command: validObj])
} else {
if(li?.size() == 1) {
seqItems?.push([command: cmd])
} else if(li?.size() == 2) {
seqItems?.push([command: cmd, value: li[1]?.trim()])
}
}
}
}
}
logDebug("executeSequenceCommand Items: $seqItems | seqStr: ${seqStr}")
if(seqItems?.size()) {
sendMultiSequenceCommand(seqItems, "executeSequenceCommand")
incrementCntByKey("use_cnt_executeSequenceCommand")
}
}
}
/*******************************************************************
Speech Queue Logic
*******************************************************************/
Integer getRecheckDelay(Integer msgLen=null, addRandom=false) {
def random = new Random()
Integer randomInt = random?.nextInt(5) //Was using 7
Integer twd = ttsWordDelay ? ttsWordDelay?.toInteger() : 2
if(!msgLen) { return 30 }
def v = (msgLen <= 14 ? twd : (msgLen / 14)) as Integer
// logTrace("getRecheckDelay($msgLen) | delay: $v + $randomInt")
return addRandom ? (v + randomInt) : v+2
}
Integer getLastTtsCmdSec() { return !state?.lastTtsCmdDt ? 1000 : GetTimeDiffSeconds(state?.lastTtsCmdDt).toInteger() }
Integer getLastQueueCheckSec() { return !state?.q_lastCheckDt ? 1000 : GetTimeDiffSeconds(state?.q_lastCheckDt).toInteger() }
Integer getCmdExecutionSec(timeVal) { return !timeVal ? null : GetTimeDiffSeconds(timeVal).toInteger() }
private getQueueSize() {
Map cmdQueue = state?.findAll { it?.key?.toString()?.startsWith("qItem_") }
return (cmdQueue?.size() ?: 0)
}
private getQueueSizeStr() {
Integer size = getQueueSize()
return "($size) Item${size>1 || size==0 ? "s" : ""}"
}
private processLogItems(String t, List ll, es=false, ee=true) {
if(t && ll?.size() && settings?.logDebug) {
if(ee) { "log${t?.capitalize()}"(" ") }
"log${t?.capitalize()}"("└─────────────────────────────")
ll?.each { "log${t?.capitalize()}"(it) }
if(es) { "log${t?.capitalize()}"(" ") }
}
}
private stateCleanup() {
if(state?.lastVolume) { state?.oldVolume = state?.lastVolume }
List items = ["qBlocked", "qCmdCycleCnt", "useThisVolume", "lastVolume", "lastQueueCheckDt", "loopChkCnt", "speakingNow",
"cmdQueueWorking", "firstCmdFlag", "recheckScheduled", "cmdQIndexNum", "curMsgLen", "lastTtsCmdDelay",
"lastQueueMsg", "lastTtsMsg"
]
items?.each { si-> if(state?.containsKey(si as String)) { state?.remove(si)} }
}
def resetQueue() {
logTrace("resetQueue()")
Map cmdQueue = state?.findAll { it?.key?.toString()?.startsWith("qItem_") }
cmdQueue?.each { cmdKey, cmdData -> state?.remove(cmdKey) }
unschedule("queueCheck")
unschedule("checkQueue")
state?.q_blocked = false
state?.q_cmdCycleCnt = null
state?.newVolume = null
state?.q_lastCheckDt = null
state?.q_loopChkCnt = null
state?.q_speakingNow = false
state?.q_cmdWorking = false
state?.q_firstCmdFlag = false
state?.q_recheckScheduled = false
state?.q_cmdIndexNum = null
state?.q_curMsgLen = null
state?.q_lastTtsCmdDelay = null
state?.q_lastTtsMsg = null
state?.q_lastMsg = null
}
Integer getNextQueueIndex() { return state?.q_cmdIndexNum ? state?.q_cmdIndexNum+1 : 1 }
Integer getCurrentQueueIndex() { return state?.q_cmdIndexNum ?: 1 }
String getAmazonDomain() { return state?.amazonDomain ?: parent?.settings?.amazonDomain }
String getAmazonUrl() {return "https://alexa.${getAmazonDomain()}"}
Map getQueueItems() { return state?.findAll { it?.key?.toString()?.startsWith("qItem_") } }
private queueCheckSchedHealth() {
Integer cmdCnt = state?.q_cmdCycleCnt
Integer lastChk = getLastQueueCheckSec()
Integer qSize = getQueueSize()
logDebug("queueCheckSchedHealth | Qsize: ${qSize} | LastChk: ${lastChk}")
if(qSize >= 2 && lastChk > 120) {
schedQueueCheck(4, true, null, "queueCheck(missed schedule)")
logDebug("queueCheck | Scheduling Queue Check for (4 sec) | Possible Lost Recheck Schedule")
}
}
private schedQueueCheck(Integer delay=30, overwrite=true, data=null, src) {
Map opts = [:]
opts["overwrite"] = overwrite
if(data) { opts["data"] = data }
runIn(delay, "queueCheck", opts)
state?.q_recheckScheduled = true
// log.debug "Scheduled Queue Check for (${delay}sec) | Overwrite: (${overwrite}) | q_recheckScheduled: (${state?.q_recheckScheduled}) | Source: (${src})"
}
public queueEchoCmd(type, msgLen, headers, body=null, firstRun=false) {
Integer qSize = getQueueSize()
if(state?.q_blocked == true) { log.warn "│ Queue Temporarily Blocked (${qSize} Items): | Working: (${state?.q_cmdWorking}) | Recheck: (${state?.q_recheckScheduled})"; return; }
List logItems = []
Map dupItems = state?.findAll { it?.key?.toString()?.startsWith("qItem_") && it?.value?.type == type && it?.value?.headers && it?.value?.headers?.message == headers?.message }
logItems?.push("│ Queue Active: (${state?.q_cmdWorking}) | Recheck: (${state?.q_recheckScheduled}) ")
if(dupItems?.size()) {
if(headers?.message) { logItems?.push("│ Message(${msgLen} char): ${headers?.message?.take(190)?.trim()}${msgLen > 190 ? "..." : ""}") }
logItems?.push("│ Ignoring (${headers?.cmdType}) Command... It Already Exists in QUEUE!!!")
logItems?.push("┌────────── Echo Queue Warning ──────────")
processLogItems("warn", logItems, true, true)
return
}
Integer qIndNum = getNextQueueIndex()
// log.debug "qIndexNum: $qIndNum"
state?.q_cmdIndexNum = qIndNum
headers?.qId = qIndNum
state?."qItem_${qIndNum}" = [type: type, headers: headers, body: body, newVolume: (headers?.newVolume ?: null), oldVolume: (headers?.oldVolume ?: null)]
state?.newVolume = null
state?.oldVolume = null
if(headers?.volume) { logItems?.push("│ Volume (${headers?.volume})") }
if(headers?.message) { logItems?.push("│ Message(Len: ${headers?.message?.toString()?.length()}): ${headers?.message?.take(200)?.trim()}${headers?.message?.toString()?.length() > 200 ? "..." : ""}") }
if(headers?.cmdType) { logItems?.push("│ CmdType: (${headers?.cmdType})") }
logItems?.push("┌───── Added Echo Queue Item (${state?.q_cmdIndexNum}) ─────")
// queueCheckSchedHealth()
if(!firstRun) {
processLogItems("trace", logItems, false, true)
}
}
private queueCheck(data) {
// log.debug "queueCheck | ${data}"
Integer qSize = getQueueSize()
Boolean qEmpty = (qSize == 0)
state?.q_lastCheckDt = getDtNow()
if(!qEmpty) {
if(qSize && qSize >= 10) {
state?.q_blocked = true
if (qSize < 20) {
logWarn("queueCheck | Queue Item Count (${qSize}) is filling up... Blocking Queue Additions Until Queue Size Drops below 10!!!", true)
schedQueueCheck(delay, true, null, "queueCheck(filling)")
} else {
logWarn("queueCheck | Queue Item Count (${qSize}) is abnormally high... Resetting Queue", true)
resetQueue()
return
}
} else { state?.q_blocked = false }
if(data && data?.rateLimited == true) {
Integer delay = data?.delay as Integer ?: getRecheckDelay(state?.q_curMsgLen)
schedQueueCheck(delay, true, null, "queueCheck(rate-limit)")
logDebug("queueCheck | Scheduling Queue Check for (${delay} sec) | Recheck for RateLimiting")
}
processCmdQueue()
return
} else {
logDebug("queueCheck | Nothing in the Queue | Performing Queue Reset...")
resetQueue()
return
}
}
void processCmdQueue() {
state?.q_cmdWorking = true
Integer q_cmdCycleCnt = state?.q_cmdCycleCnt
state?.q_cmdCycleCnt = q_cmdCycleCnt ? q_cmdCycleCnt+1 : 1
Map cmdQueue = getQueueItems()
if(cmdQueue?.size()) {
state?.q_recheckScheduled = false
def cmdKey = cmdQueue?.keySet()?.sort(false) { it.tokenize('_')[-1] as Integer }?.first()
Map cmdData = state[cmdKey as String]
// logDebug("processCmdQueue | Key: ${cmdKey} | Queue Items: (${getQueueItems()})")
cmdData?.headers["queueKey"] = cmdKey
Integer q_loopChkCnt = state?.q_loopChkCnt ?: 0
if(state?.q_lastTtsMsg == cmdData?.headers?.message && (getLastTtsCmdSec() <= 10)) { state?.q_loopChkCnt = (q_loopChkCnt >= 1) ? q_loopChkCnt++ : 1 }
// log.debug "q_loopChkCnt: ${state?.q_loopChkCnt}"
if(state?.q_loopChkCnt && (state?.q_loopChkCnt > 4) && (getLastTtsCmdSec() <= 10)) {
state?.remove(cmdKey as String)
logWarn("processCmdQueue | Possible loop detected... Last message was the same as message sent <10 seconds ago. This message will be removed from the queue")
schedQueueCheck(2, true, null, "processCmdQueue(removed duplicate)")
state?.q_cmdWorking = false
} else {
state?.q_lastMsg = cmdData?.headers?.message
speechCmd(cmdData?.headers, true)
}
} else { state?.q_cmdWorking = false }
}
Integer getAdjCmdDelay(elap, reqDelay) {
if(elap && reqDelay) {
Integer res = (elap - reqDelay)?.abs()
// log.debug "getAdjCmdDelay | reqDelay: $reqDelay | elap: $elap | res: ${res+3}"
return res < 3 ? 3 : res+3
}
return 5
}
def testMultiCmd() {
sendMultiSequenceCommand([[command: "volume", value: 60], [command: "speak", value: "super duper test message 1, 2, 3"], [command: "volume", value: 30]], "testMultiCmd")
}
private speechCmd(headers=[:], isQueueCmd=false) {
//TODO: Look into adding an expiration timestamp for automatic removal from the queue
// if(isQueueCmd) log.warn "Blocked: ${state?.q_blocked} | cycleCnt: ${state?.q_cmdCycleCnt} | isQCmd: ${isQueueCmd}"
state?.q_speakingNow = true
def tr = "speechCmd (${headers?.cmdDesc}) | Msg: ${headers?.message}"
tr += headers?.newVolume ? " | SetVolume: (${headers?.newVolume})" : ""
tr += headers?.oldVolume ? " | Restore Volume: (${headers?.oldVolume})" : ""
tr += headers?.msgDelay ? " | RecheckSeconds: (${headers?.msgDelay})" : ""
tr += headers?.queueKey ? " | QueueItem: [${headers?.queueKey}]" : ""
tr += headers?.cmdDt ? " | CmdDt: (${headers?.cmdDt})" : ""
logTrace("${tr}")
def random = new Random()
def randCmdId = random?.nextInt(300)
Map queryMap = [:]
List logItems = []
String healthStatus = getHealthStatus()
if(!headers || !(healthStatus in ["ACTIVE", "ONLINE"])) {
if(!headers) { logError("speechCmd | Error${!headers ? " | headers are missing" : ""} ") }
if(!(healthStatus in ["ACTIVE", "ONLINE"])) { logWarn("Command Ignored... Device is current in OFFLINE State", true) }
return
}
Boolean isTTS = true
// headers?.message = cleanString(headers?.message)
Integer lastTtsCmdSec = getLastTtsCmdSec()
Integer msgLen = headers?.message?.toString()?.length()
Integer recheckDelay = getRecheckDelay(msgLen)
headers["msgDelay"] = recheckDelay
headers["cmdId"] = randCmdId
if(!settings?.disableQueue) {
logItems?.push("│ Last TTS Sent: (${lastTtsCmdSec} seconds) ")
Boolean isFirstCmd = (state?.q_firstCmdFlag != true)
if(isFirstCmd) {
logItems?.push("│ First Command: (${isFirstCmd})")
headers["queueKey"] = "qItem_1"
state?.q_firstCmdFlag = true
}
Boolean sendToQueue = (isFirstCmd || (lastTtsCmdSec < 3) || (!isQueueCmd && state?.q_speakingNow == true))
if(!isQueueCmd) { logItems?.push("│ SentToQueue: (${sendToQueue})") }
// log.warn "speechCmd - QUEUE DEBUG | sendToQueue: (${sendToQueue?.toString()?.capitalize()}) | isQueueCmd: (${isQueueCmd?.toString()?.capitalize()})() | lastTtsCmdSec: [${lastTtsCmdSec}] | isFirstCmd: (${isFirstCmd?.toString()?.capitalize()}) | q_speakingNow: (${state?.q_speakingNow?.toString()?.capitalize()}) | RecheckDelay: [${recheckDelay}]"
if(sendToQueue) {
queueEchoCmd("Speak", msgLen, headers, body, isFirstCmd)
runIn((settings?.autoResetQueue ?: 180), "resetQueue")
if(!isFirstCmd) { return }
}
}
try {
Map headerMap = [cookie: getCookieVal(), csrf: getCsrfVal()]
headers?.each { k,v-> headerMap[k] = v }
Integer qSize = getQueueSize()
logItems?.push("│ Queue Items: (${qSize>=1 ? qSize-1 : 0}) │ Working: (${state?.q_cmdWorking})")
if(headers?.message) {
state?.q_curMsgLen = msgLen
state?.q_lastTtsCmdDelay = recheckDelay
schedQueueCheck(recheckDelay, true, null, "speechCmd(sendCloudCommand)")
logItems?.push("│ Rechecking: (${recheckDelay} seconds)")
logItems?.push("│ Message(${msgLen} char): ${headers?.message?.take(190)?.trim()}${msgLen > 190 ? "..." : ""}")
state?.q_lastTtsMsg = headers?.message
// state?.lastTtsCmdDt = getDtNow()
}
if(headerMap?.oldVolume) {logItems?.push("│ Restore Volume: (${headerMap?.oldVolume}%)") }
if(headerMap?.newVolume) {logItems?.push("│ New Volume: (${headerMap?.newVolume}%)") }
logItems?.push("│ Current Volume: (${device?.currentValue("volume")}%)")
Boolean isSSML = (headers?.message?.toString()?.startsWith("") && headers?.message?.toString()?.endsWith(""))
logItems?.push("│ Command: (SpeakCommand)${isSSML ? " | (SSML)" : ""}")
try {
def bodyObj = null
List seqCmds = []
if(headerMap?.newVolume) { seqCmds?.push([command: "volume", value: headerMap?.newVolume]) }
seqCmds = seqCmds + msgSeqBuilder(headerMap?.message)
if(headerMap?.oldVolume) { seqCmds?.push([command: "volume", value: headerMap?.oldVolume]) }
bodyObj = new groovy.json.JsonOutput().toJson(multiSequenceBuilder(seqCmds))
Map params = [
uri: getAmazonUrl(),
path: "/api/behaviors/preview",
headers: headerMap,
contentType: "application/json",
body: bodyObj
]
Map extData = [
cmdDt:(headerMap?.cmdDt ?: null), queueKey: (headerMap?.queueKey ?: null), cmdDesc: (headerMap?.cmdDesc ?: null), msgLen: msgLen, isSSML: isSSML, deviceId: device?.getDeviceNetworkId(), msgDelay: (headerMap?.msgDelay ?: null),
message: (headerMap?.message ? (isST() && msgLen > 700 ? headerMap?.message?.take(700) : headerMap?.message) : null), newVolume: (headerMap?.newVolume ?: null), oldVolume: (headerMap?.oldVolume ?: null), cmdId: (headerMap?.cmdId ?: null),
qId: (headerMap?.qId ?: null)
]
httpPost(params) { response->
def sData = response?.data ?: null
extData["amznReqId"] = response?.headers["x-amz-rid"] ?: null
postCmdProcess(sData, response?.status, extData)
}
} catch (ex) {
respExceptionHandler(ex, "speechCmd")
incrementCntByKey("err_speech_command")
}
logItems?.push("┌─────── Echo Command ${isQueueCmd && !settings?.disableQueue ? " (From Queue) " : ""} ────────")
processLogItems("debug", logItems)
} catch (ex) {
logError("speechCmd Exception: ${ex}")
incrementCntByKey("err_speech_command")
}
}
private postCmdProcess(resp, statusCode, data) {
if(data && data?.deviceId && (data?.deviceId == device?.getDeviceNetworkId())) {
String respMsg = resp?.message ?: null
String respMsgLow = resp?.message ? resp?.message?.toString()?.toLowerCase() : null
if(statusCode == 200) {
def execTime = data?.cmdDt ? (now()-data?.cmdDt) : 0
if(data?.queueKey) {
logDebug("Command Completed | Removing Queue Item: ${data?.queueKey}")
state?.remove(data?.queueKey as String)
}
def pi = "${data?.cmdDesc ? "${data?.cmdDesc}" : "Command"}"
pi += data?.isSSML ? " (SSML)" : ""
pi += " Sent"
pi += " | (${data?.message})"
pi += logDebug && !logInfo && data?.msgLen ? " | Length: (${data?.msgLen}) " : ""
pi += data?.msgDelay ? " | Runtime: (${data?.msgDelay} sec)" : ""
pi += logDebug && data?.amznReqId ? " | Amazon Request ID: ${data?.amznReqId}" : ""
pi += logDebug && data?.qId ? " | QueueID: (${data?.qId})" : ""
pi += " | QueueItems: (${getQueueSize()})"
pi += " | Execution Time: (${execTime}ms)"
logInfo("${pi}")
if(data?.cmdDesc && data?.cmdDesc == "SpeakCommand" && data?.message) {
state?.lastTtsCmdDt = getDtNow()
String lastMsg = data?.message as String ?: "Nothing to Show Here..."
sendEvent(name: "lastSpeakCmd", value: "${lastMsg}", descriptionText: "Last Speech text: ${lastMsg}", display: true, displayed: true)
sendEvent(name: "lastCmdSentDt", value: "${state?.lastTtsCmdDt}", descriptionText: "Last Command Timestamp: ${state?.lastTtsCmdDt}", display: false, displayed: false)
if(data?.oldVolume || data?.newVolume) {
sendEvent(name: "level", value: (data?.oldVolume ?: data?.newVolume) as Integer, display: false, displayed: false)
sendEvent(name: "volume", value: (data?.oldVolume ?: data?.newVolume) as Integer, display: false, displayed: false)
}
schedQueueCheck(getAdjCmdDelay(getLastTtsCmdSec(), data?.msgDelay), true, null, "postCmdProcess(adjDelay)")
logSpeech(data?.message, statusCode, null)
}
return
} else if((statusCode?.toInteger() in [400, 429]) && respMsgLow && (respMsgLow in ["rate exceeded", "too many requests"])) {
switch(respMsgLow) {
case "rate exceeded":
Integer rDelay = 3
logWarn("You've been rate-limited by Amazon for sending too many consectutive commands to your devices... | Device will retry again in ${rDelay} seconds", true)
schedQueueCheck(rDelay, true, [rateLimited: true, delay: data?.msgDelay], "postCmdProcess(Rate-Limited)")
break
case "too many requests":
Integer rDelay = 5
logWarn("You've sent too many consectutive commands to your devices... | Device will retry again in ${rDelay} seconds", true)
schedQueueCheck(rDelay, true, [rateLimited: false, delay: data?.msgDelay], "postCmdProcess(Too-Many-Requests)")
break
}
logSpeech(data?.message, statusCode, respMsg)
return
} else {
logError("postCmdProcess Error | status: ${statusCode} | Msg: ${respMsg}")
logSpeech(data?.message, statusCode, respMsg)
incrementCntByKey("err_cloud_commandPost")
resetQueue()
return
}
}
}
/*****************************************************
HELPER FUNCTIONS
******************************************************/
String getAppImg(imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/${isBeta() ? "beta" : "master"}/resources/icons/$imgName" }
Integer versionStr2Int(str) { return str ? str.toString()?.replaceAll("\\.", "")?.toInteger() : null }
Boolean checkMinVersion() { return (versionStr2Int(devVersion()) < parent?.minVersions()["echoDevice"]) }
def getDtNow() {
def now = new Date()
return formatDt(now, false)
}
def getIsoDtNow() {
def tf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
if(location?.timeZone) { tf.setTimeZone(location?.timeZone) }
return tf.format(new Date());
}
def formatDt(dt, mdy = true) {
def formatVal = mdy ? "MMM d, yyyy - h:mm:ss a" : "E MMM dd HH:mm:ss z yyyy"
def tf = new java.text.SimpleDateFormat(formatVal)
if(location?.timeZone) { tf.setTimeZone(location?.timeZone) }
return tf.format(dt)
}
def GetTimeDiffSeconds(strtDate, stpDate=null) {
if((strtDate && !stpDate) || (strtDate && stpDate)) {
def now = new Date()
def stopVal = stpDate ? stpDate.toString() : formatDt(now, false)
def start = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate)?.getTime()
def stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal)?.getTime()
def diff = (int) (long) (stop - start) / 1000
return diff
} else { return null }
}
def parseDt(dt, dtFmt) {
return Date.parse(dtFmt, dt)
}
Boolean ok2Notify() {
return (parent?.getOk2Notify())
}
private logSpeech(msg, status, error=null) {
Map o = [:]
if(status) o?.code = status
if(error) o?.error = error
addToLogHistory("speechHistory", msg, o, 5)
}
Integer stateSize() { def j = new groovy.json.JsonOutput().toJson(state); return j?.toString().length(); }
Integer stateSizePerc() { return (int) ((stateSize() / 100000)*100).toDouble().round(0); }
private addToLogHistory(String logKey, msg, statusData, Integer max=10) {
Boolean ssOk = (stateSizePerc() > 70)
List eData = state?.containsKey(logKey as String) ? state[logKey as String] : []
if(eData?.find { it?.message == msg }) { return; }
if(status) { eData.push([dt: getDtNow(), message: msg, status: statusData]) }
else { eData.push([dt: getDtNow(), message: msg]) }
if(!ssOK || eData?.size() > max) { eData = eData?.drop( (eData?.size()-max) ) }
state[logKey as String] = eData
}
private logDebug(msg) { if(settings?.logDebug == true) { log.debug "Echo (v${devVersion()}) | ${msg}" } }
private logInfo(msg) { if(settings?.logInfo != false) { log.info " Echo (v${devVersion()}) | ${msg}" } }
private logTrace(msg) { if(settings?.logTrace == true) { log.trace "Echo (v${devVersion()}) | ${msg}" } }
private logWarn(msg, noHist=false) { if(settings?.logWarn != false) { log.warn " Echo (v${devVersion()}) | ${msg}"; }; if(!noHist) { addToLogHistory("warnHistory", msg, null, 15); } }
private logError(msg, noHist=false) { if(settings?.logError != false) { log.error "Echo (v${devVersion()}) | ${msg}"; }; if(noHist) { addToLogHistory("errorHistory", msg, null, 15); } }
Map getLogHistory() {
return [ warnings: state?.warnHistory ?: [], errors: state?.errorHistory ?: [], speech: state?.speechHistory ?: [] ]
}
public clearLogHistory() {
state?.warnHistory = []
state?.errorHistory = []
state?.speechHistory = []
}
private incrementCntByKey(String key) {
long evtCnt = state?."${key}" ?: 0
evtCnt++
state?."${key}" = evtCnt?.toLong()
}
String getObjType(obj) {
if(obj instanceof String) {return "String"}
else if(obj instanceof GString) {return "GString"}
else if(obj instanceof Map) {return "Map"}
else if(obj instanceof LinkedHashMap) {return "LinkedHashMap"}
else if(obj instanceof HashMap) {return "HashMap"}
else if(obj instanceof List) {return "List"}
else if(obj instanceof ArrayList) {return "ArrayList"}
else if(obj instanceof Integer) {return "Integer"}
else if(obj instanceof BigInteger) {return "BigInteger"}
else if(obj instanceof Long) {return "Long"}
else if(obj instanceof Boolean) {return "Boolean"}
else if(obj instanceof BigDecimal) {return "BigDecimal"}
else if(obj instanceof Float) {return "Float"}
else if(obj instanceof Byte) {return "Byte"}
else { return "unknown"}
}
public Map getDeviceMetrics() {
Map out = [:]
def cntItems = state?.findAll { it?.key?.startsWith("use_") }
def errItems = state?.findAll { it?.key?.startsWith("err_") }
if(cntItems?.size()) {
out["usage"] = [:]
cntItems?.each { k,v -> out?.usage[k?.toString()?.replace("use_", "") as String] = v as Integer ?: 0 }
}
if(errItems?.size()) {
out["errors"] = [:]
errItems?.each { k,v -> out?.errors[k?.toString()?.replace("err_", "") as String] = v as Integer ?: 0 }
}
return out
}
private getPlatform() {
String p = "SmartThings"
if(state?.hubPlatform == null) {
try { [dummy: "dummyVal"]?.encodeAsJson(); } catch (e) { p = "Hubitat" }
// if (location?.hubs[0]?.id?.toString()?.length() > 5) { p = "SmartThings" } else { p = "Hubitat" }
state?.hubPlatform = p
logDebug("hubPlatform: (${state?.hubPlatform})")
}
return state?.hubPlatform
}
Map sequenceBuilder(cmd, val) {
def seqJson = null
if (cmd instanceof Map) {
seqJson = cmd?.sequence ?: cmd
} else { seqJson = ["@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": createSequenceNode(cmd, val)] }
Map seqObj = [behaviorId: (seqJson?.sequenceId ? cmd?.automationId : "PREVIEW"), sequenceJson: new groovy.json.JsonOutput().toJson(seqJson) as String, status: "ENABLED"]
return seqObj
}
Map multiSequenceBuilder(commands, parallel=false) {
String seqType = parallel ? "ParallelNode" : "SerialNode"
List nodeList = []
commands?.each { cmdItem-> nodeList?.push(createSequenceNode(cmdItem?.command, cmdItem?.value)) }
Map seqJson = [ "sequence": [ "@type": "com.amazon.alexa.behaviors.model.Sequence", "startNode": [ "@type": "com.amazon.alexa.behaviors.model.${seqType}", "name": null, "nodesToExecute": nodeList ] ] ]
Map seqObj = sequenceBuilder(seqJson, null)
return seqObj
}
Map createSequenceNode(command, value, devType=null, devSerial=null) {
try {
Boolean remDevSpecifics = false
Map seqNode = [
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode",
"operationPayload": [
"deviceType": devType ?: state?.deviceType,
"deviceSerialNumber": devSerial ?: state?.serialNumber,
"locale": (state?.regionLocale ?: "en-US"),
"customerId": state?.deviceOwnerCustomerId
]
]
switch (command?.toString()?.toLowerCase()) {
case "weather":
seqNode?.type = "Alexa.Weather.Play"
break
case "traffic":
seqNode?.type = "Alexa.Traffic.Play"
break
case "flashbriefing":
seqNode?.type = "Alexa.FlashBriefing.Play"
break
case "goodmorning":
seqNode?.type = "Alexa.GoodMorning.Play"
break
case "goodnight":
seqNode?.type = "Alexa.GoodNight.Play"
break
case "cleanup":
seqNode?.type = "Alexa.CleanUp.Play"
break
case "singasong":
seqNode?.type = "Alexa.SingASong.Play"
break
case "tellstory":
seqNode?.type = "Alexa.TellStory.Play"
break
case "funfact":
seqNode?.type = "Alexa.FunFact.Play"
break
case "joke":
seqNode?.type = "Alexa.Joke.Play"
break
case "calendartomorrow":
seqNode?.type = "Alexa.Calendar.PlayTomorrow"
break
case "calendartoday":
seqNode?.type = "Alexa.Calendar.PlayToday"
break
case "calendarnext":
seqNode?.type = "Alexa.Calendar.PlayNext"
break
case "stop":
seqNode?.type = "Alexa.DeviceControls.Stop"
break
case "stopalldevices":
remDevSpecifics = true
seqNode?.type = "Alexa.DeviceControls.Stop"
seqNode?.operationPayload?.devices = [ [deviceType: "ALEXA_ALL_DEVICE_TYPE", deviceSerialNumber: "ALEXA_ALL_DSN"] ]
seqNode?.operationPayload?.isAssociatedDevice = false
break
case "cannedtts_random":
List okVals = ["goodbye", "confirmations", "goodmorning", "compliments", "birthday", "goodnight", "iamhome"]
if(!(value in okVals)) { return null }
seqNode?.type = "Alexa.CannedTts.Speak"
seqNode?.operationPayload?.cannedTtsStringId = "alexa.cannedtts.speak.curatedtts-category-${value}/alexa.cannedtts.speak.curatedtts-random"
break
case "cannedtts":
List okVals = ["goodbye", "confirmations", "goodmorning", "compliments", "birthday", "goodnight", "iamhome"]
if(!(value in okVals)) { return null }
seqNode?.type = "Alexa.CannedTts.Speak"
List valObj = (value?.toString()?.contains("::")) ? value?.split("::") : [value as String, value as String]
seqNode?.operationPayload?.cannedTtsStringId = "alexa.cannedtts.speak.curatedtts-category-${valObj[0]}/alexa.cannedtts.speak.curatedtts-${valObj[1]}"
break
case "wait":
remDevSpecifics = true
seqNode?.operationPayload?.remove('customerId')
seqNode?.type = "Alexa.System.Wait"
seqNode?.operationPayload?.waitTimeInSeconds = value?.toInteger() ?: 5
break
case "volume":
seqNode?.type = "Alexa.DeviceControls.Volume"
seqNode?.operationPayload?.value = value;
break
case "dnd_duration":
case "dnd_time":
case "dnd_all_duration":
case "dnd_all_time":
remDevSpecifics = true
seqNode?.type = "Alexa.DeviceControls.DoNotDisturb"
seqNode?.skillId = "amzn1.ask.1p.alexadevicecontrols"
seqNode?.operationPayload?.customerId = state?.deviceOwnerCustomerId
if(command == "dnd_all_time" || command == "dnd_all_duration") {
seqNode?.operationPayload?.devices = [ [deviceType: "ALEXA_ALL_DEVICE_TYPE", deviceSerialNumber: "ALEXA_ALL_DSN"] ]
}
if(command == "dnd_time" || command == "dnd_duration") {
seqNode?.operationPayload?.devices = [ [deviceAccountId: state?.deviceAccountId, deviceType: state?.deviceType, deviceSerialNumber: state?.serialNumber] ]
}
seqNode?.operationPayload?.action = "Enable"
if(command == "dnd_time" || command == "dnd_all_time") {
seqNode?.operationPayload?.until = "TIME#T${value}"
} else if (command == "dnd_duration" || command == "dnd_all_duration") { seqNode?.operationPayload?.duration = "DURATION#PT${value}" }
seqNode?.operationPayload?.timeZoneId = "America/Detroit" //location?.timeZone?.ID ?: null
break
case "speak":
seqNode?.type = "Alexa.Speak"
value = cleanString(value as String)
seqNode?.operationPayload?.textToSpeak = value as String
break
case "ssml":
case "announcement":
case "announcementall":
case "announcement_devices":
remDevSpecifics = true
seqNode?.type = "AlexaAnnouncement"
seqNode?.operationPayload?.expireAfter = "PT5S"
List valObj = (value?.toString()?.contains("::")) ? value?.split("::") : ["Echo Speaks", value as String]
// log.debug "valObj(size: ${valObj?.size()}): $valObj"
seqNode?.operationPayload?.content = [[ locale: (state?.regionLocale ?: "en-US"), display: [ title: valObj[0], body: valObj[1]?.toString().replaceAll(/<[^>]+>/, '') ], speak: [ type: (command == "ssml" ? "ssml" : "text"), value: valObj[1] as String ] ] ]
seqNode?.operationPayload?.target = [ customerId : state?.deviceOwnerCustomerId ]
if(!(command in ["announcementall", "announcement_devices"])) {
seqNode?.operationPayload?.target?.devices = [ [ deviceTypeId: state?.deviceType, deviceSerialNumber: state?.serialNumber ] ]
} else if(command == "announcement_devices" && valObj?.size() && valObj[2] != null) {
List devObjs = new groovy.json.JsonSlurper().parseText(valObj[2])
seqNode?.operationPayload?.target?.devices = devObjs
}
break
case "pushnotification":
remDevSpecifics = true
seqNode?.type = "Alexa.Notifications.SendMobilePush"
seqNode?.skillId = "amzn1.ask.1p.alexanotifications"
seqNode?.operationPayload?.notificationMessage = value as String
seqNode?.operationPayload?.alexaUrl = "#v2/behaviors"
seqNode?.operationPayload?.title = "Echo Speaks"
break
case "email":
seqNode?.type = "Alexa.Operation.SkillConnections.Email.EmailSummary"
seqNode?.skillId = "amzn1.ask.1p.email"
seqNode?.operationPayload?.targetDevice = [deviceType: state?.deviceType, deviceSerialNumber: state?.serialNumber ]
seqNode?.operationPayload?.connectionRequest = [uri: "connection://AMAZON.Read.EmailSummary/amzn1.alexa-speechlet-client.DOMAIN:ALEXA_CONNECT", input: [:] ]
seqNode?.operationPayload?.remove('deviceType')
seqNode?.operationPayload?.remove('deviceSerialNumber')
break
default:
return null
}
if(remDevSpecifics) {
seqNode?.operationPayload?.remove('deviceType')
seqNode?.operationPayload?.remove('deviceSerialNumber')
seqNode?.operationPayload?.remove('locale')
}
return seqNode
} catch (ex) {
logError("createSequenceNode Exception: ${ex}")
return [:]
}
}