definition(
name: "BigTalker3-Parent-DEV",
namespace: "rayzurbock",
author: "rayzur@rayzurbock.com",
description: "Spoken and/or text announcements for mode changes, switches, motions, and more.",
category: "Fun & Social",
//singleInstance: true,
iconUrl: "https://lowrance.cc/ST/icons/BigTalker.png",
iconX2Url: "https://lowrance.cc/ST/icons/BigTalker@2x.png",
iconX3Url: "https://lowrance.cc/ST/icons/BigTalker@2x.png")
preferences {
page(name: "pageStart")
page(name: "pageTalkNow")
page(name: "pageConfigureDefaults")
page(name: "pageHelpPhraseTokens")
//End preferences
}
def pageStart(){
state.childAppName = "BigTalker3-Child-DEV"
state.parentAppName = "BigTalker3-Parent-DEV"
state.namespace = "rayzurbock"
setVersion()
state.hubType = getHubType()
LOGDEBUG("Hub Type: ${state.hubType}")
setFormatting()
state.supportedVoices = ["Ivy(en-us)","Joanna(en-us)","Joey(en-us)","Justin(en-us)","Matthew(en-us)","Kendra(en-us)","Kimberly(en-us)","Salli(en-us)","Amy(en-gb)","Brian(en-gb)","Emma(en-gb)","Miguel(es-us)","Penelope(es-us)"]
if (checkConfig()) {
// Do nothing here, but run checkConfig()
}
dynamicPage(name: "pageStart", title: "Big Talker", install: false, uninstall: (app.getInstallationState() == "COMPLETE")){
def formatSettingRootStart = state.formatSettingRootStart
def formatSettingRootEnd = state.formatSettingRootEnd
def formatSettingOptionalStart = state.formatSettingOptionalStart
def formatSettingOptionalEnd = state.formatSettingOptionalEnd
def formatUlStart = state.formatUlStart
def formatUlEnd = state.formatUlEnd
def formatLiStart = state.formatLiStart
def formatLiEnd = state.formatLiEnd
def formatIStart = state.formatIStart
def formatIEnd = state.formatIEnd
def formatStrongStart = state.formatStrongStart
def formatStrongEnd = state.formatStrongEnd
def formatHr = state.formatHr
def formatBr = state.formatBr
def formatCenterStart = state.formatCenterStart
def formatCenterEnd = state.formatCenterEnd
section(){
LOGDEBUG("install state=${app.getInstallationState()}.")
if (!(state.configOK)) {
href "pageConfigureDefaults", title:"Configure", description:"Tap to configure"
} else {
//V1Method href "pageConfigureEvents", title: "Configure Events", description: "Tap to configure events"
//href "pageStatus", title:"Status", description:"Tap to view status"
href "pageConfigureDefaults", title: "Configure Defaults", description: "Tap to configure defaults"
href "pageTalkNow", title:"Talk Now", description:"Tap to setup talk now"
}
}
section("Event Groups") {}
section(){
def apps = getChildApps()?.sort{ it.label }
if (state.configOK) {
if (apps?.size() == 0) {
paragraph "You have not configured any event groups yet."
app(name: "BTEvt-", appName: state.childAppName, namespace: state.namespace, title: "Add Event Group", description: "Tap to configure event triggers", multiple: true, uninstall: false)
} else {
app(name: "BTEvt-", appName: state.childAppName, namespace: state.namespace, title: "Add Event Group", description: "Tap to configure event triggers", multiple: true, uninstall: false)
}
}
}
section(){
if ((settings?.debugmode == true) && (settings?.resumePlay == true)) {
input name: "debug_pollnow", type: "bool", title: "DEBUG: Poll Now (simply toggle)", multiple: false, required: false, submitOnChange: true, defaultValue: false
if ((settings?.debug_pollnow == true) && (state?.lastDebugPollNow != true)) { poll() }
state.lastDebugPollNow = settings?.debug_pollnow
}
}
section("About"){
def AboutApp = ""
AboutApp += "${formatHr}Big Talker is a SmartApp that can make your house talk depending on various triggered events — out loud through speakers, or as text on your phone.${formatBr}${formatBr}"
AboutApp += "Pair with a Hubitat compatible audio device such as Sonos, Ubi, LANnouncer, Echo, Google Home Speaker and/or VLC Thing (running on your computer or Raspberry Pi) for spoken announcements, or pick notification devices (Hubitat mobile app, Telegram, Pushover) to receive announcements as text.${formatBr}"
paragraph(AboutApp)
updateCheck()
//checkButtons()
displayVersionStatus()
}
section("${formatHr}${formatHr}${formatStrongStart}${formatCenterStart}Donations${formatCenterEnd}${formatStrongEnd}"){
def DonateOptions = ""
DonateOptions += "Big Talker is provided to the community for free. It takes a lot of time to build and support any complex app. If you wish to support the time and effort put into development you may submit a donation with one of the following:${formatBr}"
DonateOptions += "${formatUlStart}"
DonateOptions += "${formatLiStart}${formatStrongStart}Cash App${formatStrongEnd} = https://cash.app/\$Lowrance (use a debit card, it\'s free for both of us)${formatLiEnd}"
DonateOptions += "${formatLiStart}${formatStrongStart}Venmo${formatStrongEnd} = @BrianLowrance${formatLiEnd}"
DonateOptions += "${formatLiStart}${formatStrongStart}Paypal.me${formatStrongEnd} = https://paypal.me/brianlowrance (They take a little since I setup my account as a business account)${formatLiEnd}"
paragraph(DonateOptions)
}
}
}
def pageTalkNow(){
dynamicPage(name: "pageTalkNow", title: "Talk Now", install: false, uninstall: false){
section(""){
def myTalkNowResume = false
paragraph ("Speak the following phrase:\nNote: must differ from the last spoken phrase\n")
if (true) {
input name: "talkNowVolume", type: "number", title: "Set volume to (overrides default):", required: false, submitOnChange: true
input name: "talkNowResume", type: "bool", title: "Enable audio resume", multiple: true, required: false, submitOnChange: true, defaultValue: (settings?.resumePlay == false) ? false : true
input name: "talkNowVoice", type: "enum", title: "Select custom voice:", options: state.supportedVoices, required: false, submitOnChange: true
myTalkNowResume = settings.talkNowResume
}
input name: "speechTalkNow", type: "text", title: "Speak phrase", required: false, submitOnChange: true
input name: "talkNowSpeechDeviceMusicPlayer", type: "capability.musicPlayer", title: "Music players (e.g. Sonos, VLCThing)", multiple: true, required: false, submitOnChange: true
input name: "talkNowSpeechDeviceSpeechSynthesis", type: "capability.speechSynthesis", title: "Speech speakers (e.g. Echo, Google Home, LANnouncer)", multiple: true, required: false, submitOnChange: true
input name: "talkNowSpeechDeviceAudioNotification", type: "capability.audioNotification", title: "Audio notification devices", multiple: true, required: false, submitOnChange: true
input name: "talkNowTextDevice", type: "capability.notification", title: "Text Only devices (e.g. Hubitat mobile app, Telegram, Pushover)", multiple: true, required: false, submitOnChange: true
//LOGDEBUG("previoustext=${state.lastTalkNow} New=${settings.speechTalkNow}")
def talkNowDevs = []
if (settings.talkNowSpeechDeviceMusicPlayer) { talkNowDevs = talkNowDevs + settings.talkNowSpeechDeviceMusicPlayer }
if (settings.talkNowSpeechDeviceSpeechSynthesis) { talkNowDevs = talkNowDevs + settings.talkNowSpeechDeviceSpeechSynthesis }
if (settings.talkNowSpeechDeviceAudioNotification) { talkNowDevs = talkNowDevs + settings.talkNowSpeechDeviceAudioNotification }
if (((!(state.lastTalkNow == settings.speechTalkNow)) && (talkNowDevs || settings.talkNowTextDevice)) || (settings.speechTalkNow?.contains("%askalexa%"))){
//Say stuff!
if (true) {
myTalkNowResume = (myTalkNowResume == "") ? settings.resumeAudio : true //use global setting if TalkNow is not set
if (settings?.talkNowResume == null) {myTalkNowResume = true} //default to true if not set.
} else {
//speechSynthesis does not use the following settings; make sure they are not null though
myVolume = "100"
myTalkNowResume = false
personality = false
myVoice = ""
}
def customevent = [displayName: 'BigTalker:TalkNow', name: 'TalkNow', value: 'TalkNow', descriptionText: "Talk Now"]
def myVolume = getDesiredVolume(settings?.talkNowVolume)
def myVoice = getMyVoice(settings.talkNowVoice)
//def myVoice = (!(talkNowVoice == null || talkNowVoice == "")) ? talkNowVoice : (settings?.speechVoice ? settings.speechVoice : "Sallie(en-us)")
def personality = false
LOGDEBUG ("TalkNow Voice=${myVoice}")
if (talkNowDevs || settings.speechTalkNow?.contains("%askalexa%")) { Talk("Talk Now", settings.speechTalkNow, talkNowDevs, myVolume, myTalkNowResume, personality, myVoice, customevent) }
Notify("Talk Now", settings.speechTalkNow, settings.talkNowTextDevice, customevent)
state.lastTalkNow = settings.speechTalkNow
}
}
section("Help"){
href "pageHelpPhraseTokens", title:"Phrase Tokens", description:"Tap for a list of phrase tokens"
}
}
}
def getMyVoice(deviceVoice){
def myVoice = "Not Used"
if (true) {
LOGDEBUG("getMyVoice[parent]: deviceVoice=${deviceVoice ? deviceVoice : "Not selected"}")
LOGDEBUG("getMyVoice[parent]: settings.speechVoice=${settings?.speechVoice}")
myVoice = (!(deviceVoice == null || deviceVoice == "")) ? deviceVoice : (settings?.speechVoice ? settings?.speechVoice : "Salli(en-us)")
}
return myVoice
}
def pageHelpPhraseTokens(){
//KEEP IN SYNC WITH CHILD!
dynamicPage(name: "pageHelpPhraseTokens", title: "Available Phrase Tokens", install: false, uninstall:false){
def formatSettingRootStart = state.formatSettingRootStart
def formatSettingRootEnd = state.formatSettingRootEnd
def formatSettingOptionalStart = state.formatSettingOptionalStart
def formatSettingOptionalEnd = state.formatSettingOptionalEnd
def formatUlStart = state.formatUlStart
def formatUlEnd = state.formatUlEnd
def formatLiStart = state.formatLiStart
def formatLiEnd = state.formatLiEnd
def formatIStart = state.formatIStart
def formatIEnd = state.formatIEnd
def formatStrongStart = state.formatStrongStart
def formatStrongEnd = state.formatStrongEnd
section("The following tokens can be used in your event phrases and will be replaced as listed:"){
def AvailTokens = ""
AvailTokens += "${formatUlStart}"
AvailTokens += "${formatLiStart}${formatStrongStart}%askalexa%${formatStrongEnd} = Send phrase to AskAlexa message queue${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%groupname%${formatStrongEnd} = Name that you gave for the event group${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%date%${formatStrongEnd} = Current date; January 01 20xx${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%day%${formatStrongEnd} = Current day; Monday${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%devicename%${formatStrongEnd} = Triggering devices display name${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%devicetype%${formatStrongEnd} = Triggering device type; motion, switch, etc${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%devicechange%${formatStrongEnd} = State change that occurred; on/off, active/inactive, etc...${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%description%${formatStrongEnd} = The description of the event that is to be displayed to the user in the mobile application.${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%locationname%${formatStrongEnd} = Hub location name; home, work, etc${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%lastmode%${formatStrongEnd} = Last hub mode; home, away, etc${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%mode%${formatStrongEnd} = Current hub mode; home, away, etc${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%mp3(url)%${formatStrongEnd} = Play hosted MP3 file; URL should be http://www.domain.com/path/file.mp3"
AvailTokens += "${formatUlStart}${formatLiStart}${formatIStart}No other tokens or phrases can be used with %mp3(url)%${formatIEnd}${formatLiEnd}${formatUlEnd}"
AvailTokens += "${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%time%${formatStrongEnd} = Current hub time; HH:mm am/pm${formatStrongEnd}${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%value%${formatStrongEnd} = Wattage value; only applicable in Power Meter events${formatLiEnd}"
AvailTokens += "${formatUlEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%weathercurrent%${formatStrongEnd} = Current weather based on hub location${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%weathertoday%${formatStrongEnd} = Today's weather forecast* based on hub location${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%weathertonight%${formatStrongEnd} = Tonight's weather forecast* based on hub location${formatLiEnd}"
AvailTokens += "${formatLiStart}${formatStrongStart}%weathertomorrow%${formatStrongEnd} = Tomorrow's weather forecast* based on hub location${formatLiEnd}"
paragraph(AvailTokens)
}
}
}
def pageConfigureDefaults(){
unsubscribe()
myRunIn(3, initSubscribe)
state.mode = location.mode
state.lastMode = state.mode
if (state?.installed == true) {
state.dynPageProperties = [
name: "pageConfigureDefaults",
title: "Configure Defaults",
install: false,
uninstall: false,
//nextPage: "pageConfigureEvents"
]
} else {
//During initial setup this is the install page. NOTE: Hubitat's Done button
//always returns to the Apps list (nextPage does not redirect it -- verified
//2026-11-06 on a fresh install), so the page tells the user to reopen the
//app from Apps to reach the main menu and add Event Groups.
state.dynPageProperties = [
name: "pageConfigureDefaults",
title: "Configure Defaults",
install: true,
uninstall: false
]
}
return dynamicPage(state.dynPageProperties) {
//dynamicPage(name: "pageConfigureDefaults", title: "Configure Defaults", nextPage: "${myNextPage}", install: false, uninstall: false) {
section("${state.formatSettingRootStart}Talk with:${state.formatSettingRootEnd}"){
input "speechDeviceDefaultMusicPlayer", "capability.musicPlayer", title: "Music players (default, e.g. Sonos, VLCThing)", multiple: true, required: false, submitOnChange: false
input "speechDeviceDefaultSpeechSynthesis", "capability.speechSynthesis", title: "Speech speakers (default, e.g. Echo, Google Home, LANnouncer)", multiple: true, required: false, submitOnChange: false
input "speechDeviceDefaultAudioNotification", "capability.audioNotification", title: "Audio notification devices (default)", multiple: true, required: false, submitOnChange: false
}
section("${state.formatSettingRootStart}Text Only (default):${state.formatSettingRootEnd}"){
paragraph "Notification devices receive a text copy of announcements (e.g. Hubitat mobile app, Telegram, Pushover). Pick them here as the app default; each event group and device type can override this."
input "textDeviceDefault", "capability.notification", title: "Text Only devices (default)", multiple: true, required: false, submitOnChange: false
}
if (true) {
section ("${state.formatSettingRootStart}Adjust volume during announcement (optional; Supports: Sonos, VLC-Thing):${state.formatSettingRootEnd}"){
input "speechMinimumVolume", "number", title: "Minimum volume for announcement (0-100%, Default: 50%):", required: false
input "speechVolume", "number", title: "Set volume during announcement (0-100%):", required: false
input "speechVoice", "enum", title: "Select voice:", options: state.supportedVoices, required: true, defaultValue: "Salli(en-us)"
}
section ("${state.formatSettingRootStart}Attempt to resume playing audio (optional; Supports: Sonos, VLC-Thing):${state.formatSettingRootEnd}"){
input "resumePlay", "bool", title: "Resume Play:", required: true, defaultValue: true
input "allowScheduledPoll", "bool", title: "Enable polling device status (recommended)", required: true, defaultValue: true
}
}
section (""){
input "speechModesDefault", "mode", title: "${state.formatSettingRootStart}Talk only while in these modes (default)${state.formatSettingRootEnd}", multiple: true, required: true, submitOnChange: false
}
section ("${state.formatSettingRootStart}Only between these times:${state.formatSettingRootEnd}"){
input "defaultStartTime", "time", title: "Don't talk before: ", required: false, submitOnChange: true
input "defaultEndTime", "time", title: "Don't talk after: ", required: (!(settings.defaultStartTime == null)), submitOnChange: true
}
section(){
input "personalityMode", "bool", title: "${state.formatSettingRootStart}Allow Personality?${state.formatSettingRootEnd}", required: true, defaultValue: false
input "customPersonalityPre", "text", title: "${state.formatSettingRootStart}Custom personality phrases spoken BEFORE the announcement (comma separated, optional)${state.formatSettingRootEnd}", required: false
input "customPersonalityPost", "text", title: "${state.formatSettingRootStart}Custom personality phrases spoken AFTER the announcement (comma separated, optional)${state.formatSettingRootEnd}", required: false
input "debugmode", "bool", title: "${state.formatSettingRootStart}Enable debug logging${state.formatSettingRootEnd}", required: true, defaultValue: false, submitOnChange: true
input "allowAskAlexa", "bool", title: "${state.formatSettingRootStart}Allow the %askalexa% token to enqueue messages for AskAlexa${state.formatSettingRootEnd}", required: false, defaultValue: false
if (settings.debugmode != state?.lastDebugmodeSetting) {
state.lastDebugmodeSetting = settings.debugmode
state.debugAutoOff = false
unschedule("disableDebug")
if (debugmode) {
state.debugMode = true; myRunIn(1800, disableDebug); LOGTRACE("Debug logging has been enabled. Will auto-disable in 30 minutes.")
} else {
state.debugMode = false; LOGTRACE("Debug logging is not enabled.")
}
} else if (state?.debugAutoOff == true && debugmode) {
//Auto-off already took effect; stay off until the user toggles the setting again
} else if (state?.debugMode == null) {
state.debugMode = (debugmode == true)
}
}
if (!(state?.installed == true)) {
section("What's next?"){
paragraph "After tapping Done, Hubitat returns you to the Apps list — that's normal. Reopen BigTalker from your Apps list and you'll land on the main menu, where you can add your Event Groups."
}
}
}
}
def installed() {
state.installed = true
//LOGTRACE("Installed with settings: ${settings}")
LOGTRACE("Installed (Parent Version: ${state.version})")
initialize()
if (settings?.allowScheduledPoll != false){
myRunIn(60, poll)
}
//End installed()
}
def updated() {
unschedule()
state.installed = true
//LOGTRACE("Updated with settings: ${settings}")
LOGTRACE("Updated settings (Parent Version: ${state.version})")
unsubscribe()
initialize()
if (settings?.allowScheduledPoll != false){
myRunIn(60, poll)
}
//End updated()
}
def checkConfig() {
def configErrorList = ""
if (!(state.speechDeviceType)){
state.speechDeviceType = "auto" //Set a default if the app was updated and didn't contain settings.speechDeviceType
}
if (settings?.allowScheduledPoll == true && settings?.resumePlay == true) { state.allowScheduledPoll = true }
if (settings?.allowScheduledPoll == null || settings?.resumePlay == null) { state.allowScheduledPoll = true }
if (settings?.allowScheduledPoll == false || settings?.resumePlay == false) { state.allowScheduledPoll = false}
// if (!(getDefaultSpeechDevices())){
// configErrorList += " ** Default speech device(s) not selected,"
// }
if (!(state.installed == true)) {
configErrorList += " ** state.installed not True,"
}
if (!(configErrorList == "")) {
LOGDEBUG ("checkConfig() returning FALSE (${configErrorList})")
state.configOK = false
return false //Errors occurred. Config check failed.
} else {
LOGDEBUG ("checkConfig() returning TRUE (${configErrorList})")
state.configOK = true
return true
}
}
def initialize() {
if (!(checkConfig())) {
def msg = ""
msg = "ERROR: App not properly configured! Can't start.\n"
msg += "ERRORs:\n${state.configErrorList}"
LOGTRACE(msg)
state.polledDevices = ""
return //App not properly configured, exit, don't subscribe
}
version()
LOGTRACE("Initialized (Parent Version: ${state.version})")
state.lastMode = location.mode
state.lastTalkNow = settings.speechTalkNow
initSubscribe()
//End initialize()
}
def initSubscribe() {
//Subscribe Mode
LOGDEBUG("Subscribed")
subscribe(location, "mode", onModeChangeEvent) //Keep track of mode changes
}
def processPhraseVariables(appname, phrase, evt, mode = null){
if (mode == null || mode == "") { mode = "auto" }
try {
def zipCode = location.zipCode
def mp3Url = ""
if (phrase.toLowerCase().contains("%mp3(")) {
if (phrase.toLowerCase().contains(".mp3)%")) {
def phraseMP3Start = (phrase.toLowerCase().indexOf("%mp3(") + 5)
def phraseMP3End = (phrase.toLowerCase().indexOf(".mp3)%"))
mp3Url = phrase.substring(phraseMP3Start, phraseMP3End)
LOGDEBUG("MP3 URL: ${mp3Url}")
phrase = phrase.replace("%mp3(","")
phrase = phrase.replace(".mp3)%", ".mp3")
phrase = phrase.replace (" ", "%20")
phrase = phrase.replace ("+", "%2B")
phrase = phrase.replace ("-", "%2D")
} else {
phrase = "Invalid M P 3 URL found in M P 3 token"
}
if (!isSafeUrl(mp3Url)) {
phrase = "Rejected unsafe MP3 URL"
}
return phrase
}
if (phrase.toLowerCase().contains(" percent ")) { phrase = phrase.replace(" percent ","%") }
if (phrase.toLowerCase().contains("%groupname%")) {
phrase = replaceTokenIgnoreCase(phrase, '%groupname%', appname)
}
if (phrase.toLowerCase().contains("%devicename%")) {
try {
phrase = replaceTokenIgnoreCase(phrase, '%devicename%', evt.displayName) //User given name of the device triggering the event
}
catch (ex) {
LOGDEBUG("evt.displayName failed; trying evt.device.displayName")
try {
phrase = replaceTokenIgnoreCase(phrase, '%devicename%', evt.device.displayName) //User given name of the device triggering the event
}
catch (ex2) {
LOGDEBUG("evt.device.displayName filed; trying evt.device.name")
try {
phrase = replaceTokenIgnoreCase(phrase, '%devicename%', evt.device.name) //Driver name for the device triggering the event
}
catch (ex3) {
LOGDEBUG("evt.device.name filed; Giving up")
phrase = replaceTokenIgnoreCase(phrase, '%devicename%', "Device Name Unknown")
}
}
}
}
if (phrase.toLowerCase().contains("%devicetype%")) {phrase = replaceTokenIgnoreCase(phrase, '%devicetype%', evt.name)} //Device type: motion, switch, etc...
if (phrase.toLowerCase().contains("%devicechange%")) {phrase = replaceTokenIgnoreCase(phrase, '%devicechange%', evt.value)} //State change that occurred: on/off, active/inactive, etc...
if (phrase.toLowerCase().contains("%description%")) {phrase = replaceTokenIgnoreCase(phrase, '%description%', evt.descriptionText)} //Description of the event which occurred via device-specific text`
if (phrase.toLowerCase().contains("%locationname%")) {phrase = replaceTokenIgnoreCase(phrase, '%locationname%', location.name)}
if (phrase.toLowerCase().contains("%lastmode%")) {phrase = replaceTokenIgnoreCase(phrase, '%lastmode%', state.lastMode)}
if (phrase.toLowerCase().contains("%mode%")) {phrase = replaceTokenIgnoreCase(phrase, '%mode%', location.mode)}
if (phrase.toLowerCase().contains("%time%")) {
phrase = replaceTokenIgnoreCase(phrase, '%time%', getTimeFromCalendar(false,true))
if ((phrase.toLowerCase().contains("00:")) && (phrase.toLowerCase().contains("am"))) {phrase = replaceTokenIgnoreCase(phrase, '00:', "12:")}
if ((phrase.toLowerCase().contains("24:")) && (phrase.toLowerCase().contains("am"))) {phrase = replaceTokenIgnoreCase(phrase, '24:', "12:")}
if ((phrase.toLowerCase().contains("0:")) && (!phrase.toLowerCase().contains("10:")) && (phrase.toLowerCase().contains("am"))) {phrase = replaceTokenIgnoreCase(phrase, '0:', "12:")}
}
if (phrase.toLowerCase().contains("%weathercurrent%")) {phrase = replaceTokenIgnoreCase(phrase, '%weathercurrent%', getWeather("current", zipCode)); phrase = adjustWeatherPhrase(phrase)}
if (phrase.toLowerCase().contains("%weathertoday%")) {phrase = replaceTokenIgnoreCase(phrase, '%weathertoday%', getWeather("today", zipCode)); phrase = adjustWeatherPhrase(phrase)}
if (phrase.toLowerCase().contains("%weathertonight%")) {phrase = replaceTokenIgnoreCase(phrase, '%weathertonight%', getWeather("tonight", zipCode));phrase = adjustWeatherPhrase(phrase)}
if (phrase.toLowerCase().contains("%weathertomorrow%")) {phrase = replaceTokenIgnoreCase(phrase, '%weathertomorrow%', getWeather("tomorrow", zipCode));phrase = adjustWeatherPhrase(phrase)}
if (mode == "capability.speechSynthesis" || mode == "capability.audioNotification"){
//Some TTS engines pronounce commas as "Dash", so convert them to dashes
if (phrase.contains(",")) { phrase = phrase.replace(","," - ") }
//if (phrase.contains(".")) { phrase = phrase.replace("."," - ") }
}
if (phrase.contains('"')) { phrase = phrase.replace('"',"") }
if (phrase.contains("'")) { phrase = phrase.replace("'","") }
if (phrase.toLowerCase().contains("10s")) { phrase = replaceTokenIgnoreCase(phrase, "10s","tens") }
if (phrase.toLowerCase().contains("20s")) { phrase = replaceTokenIgnoreCase(phrase, "20s","twenties") }
if (phrase.toLowerCase().contains("30s")) { phrase = replaceTokenIgnoreCase(phrase, "30s","thirties") }
if (phrase.toLowerCase().contains("40s")) { phrase = replaceTokenIgnoreCase(phrase, "40s","fourties") }
if (phrase.toLowerCase().contains("50s")) { phrase = replaceTokenIgnoreCase(phrase, "50s","fifties") }
if (phrase.toLowerCase().contains("60s")) { phrase = replaceTokenIgnoreCase(phrase, "60s","sixties") }
if (phrase.toLowerCase().contains("70s")) { phrase = replaceTokenIgnoreCase(phrase, "70s","seventies") }
if (phrase.toLowerCase().contains("80s")) { phrase = replaceTokenIgnoreCase(phrase, "80s","eighties") }
if (phrase.toLowerCase().contains("90s")) { phrase = replaceTokenIgnoreCase(phrase, "90s","nineties") }
if (phrase.toLowerCase().contains("100s")) { phrase = replaceTokenIgnoreCase(phrase, "100s","one hundreds") }
if (phrase.toLowerCase().contains("%askalexa%")) {
phrase=replaceTokenIgnoreCase(phrase, "%askalexa%","")
if (!(phrase == "") && (!(phrase == null))){
LOGTRACE("Sending to AskAlexa: ${phrase}.")
sendLocationEvent(name: "AskAlexaMsgQueue", value: "BigTalker", isStateChange: true, descriptionText: phrase.replaceAll(/<[^>]*>?/, ''))
}else{
LOGERROR("Phrase only contained %askalexa%. Nothing to say/send.")
}
}
if (phrase.toLowerCase().contains("%date%")) {
phrase=replaceTokenIgnoreCase(phrase, "%date%",(new Date().format( 'MMMM dd' )))
}
if (phrase.toLowerCase().contains("%day%")) {
phrase=replaceTokenIgnoreCase(phrase, "%day%",(new Date().format('EEEE',location.timeZone)))
}
if (phrase.contains("%")) { phrase = phrase.replace("%"," percent ") }
return phrase
} catch(ex) {
LOGTRACE("There was a problem processing your desired phrase: ${phrase}. ${ex}")
phrase = "Sorry, there was a problem processing your desired BigTalker phrase token."
return phrase
}
}
def addPersonalityToPhrase(phrase, evt){
LOGDEBUG("addPersonalityToPhrase(${phrase},${evt})")
def response = new String[20]
response[0] = ""
def options = 0
def genericresponse = new String[20]
genericresponse[0] = ""
def genericoptions = 0
def myRandom = 0
//SWITCHES BEGIN
if (evt.value == "on") {
if (phrase.contains("light")){
options = 16
response[1] = "{POST}please don't forget to turn the light off"
response[2] = "{POST}night vision goggles would do the same but I guess they are more expensive."
response[3] = "{POST}Thanks Thomas Edison!"
response[4] = "{POST}Wow, this is bright!"
response[5] = "{POST}Where are my sunglasses."
response[6] = "{POST}there goes the electricity bill!"
response[7] = "{POST}the same old thing everyday."
response[8] = "{POST}It is about time it was awfully dark!"
response[9] = "{POST}Glad you are here, I was lonely"
response[10] = "{POST}Is it time for us to play?"
response[13] = "{PRE}Well, hello there"
response[14] = "{PRE}Look who's back"
response[15] = "{POST}I could get used to this"
response[16] = "{POST}let there be light!"
response[11] = "{PRE}Oh, Hi"
response[12] = "{PRE}Oh, Hi there"
} else {
//Something turned on, but it wasn't a light
options = 8
response[1] = "{POST}there goes the electricity bill!"
response[2] = "{POST}the same old thing everyday."
response[3] = "{PRE}Oh, Hi"
response[4] = "{PRE}Oh, Hi there"
response[5] = "{PRE}Well, hello there"
response[6] = "{PRE}Look who's back"
response[7] = "{POST}I could get used to this"
response[8] = "{POST}it's about time!"
}
}
if (evt.value == "off") {
if (phrase.contains("light")){
options = 16
response[1] = "{POST}It's about time!"
response[2] = "{POST}time to save some money!"
response[3] = "{POST}wow, it's dark"
response[4] = "{POST}going green are we?"
response[5] = "{POST}I'll still be here, in the dark."
response[6] = "{POST}Hey! You know I am afraid of the dark."
response[7] = "{POST}Please don't leave me alone in the dark."
response[8] = "{POST}Good thing you turned that off it was hurting my eyes!"
response[9] = "{POST}You really like saving money!"
response[10] = "{POST}Is it time to go to sleep?"
response[11] = "{PRE}Oh, Hi"
response[12] = "{PRE}Oh, Hi there"
response[13] = "{PRE}Going somewhere?"
response[14] = "{PRE}Nighty night"
response[15] = "{POST}I hope you come back soon"
response[16] = "{POST}Finally, some peace and quiet"
} else {
//Something turned off, but it wasn't a light
options = 9
response[1] = "{POST}It's about time!"
response[2] = "{POST}time to save some money!"
response[3] = "{POST}going green are we?"
response[4] = "{PRE}Oh, Hi"
response[5] = "{PRE}Oh, Hi there"
response[6] = "{PRE}Going somewhere?"
response[7] = "{PRE}Well, hello there"
response[8] = "{POST}I could get used to this"
response[9] = "{POST}Finally, some peace and quiet"
}
}
//SWITCHES END
def UseGenericRandom = 0
myRandom = (new Random().nextInt(10))
if (myRandom == 1 || myRandom == 4 || myRandom == 7) {
//GENERIC RESPONSES BEGIN
genericoptions = 8
genericresponse[1] = "{PRE}Hey there"
genericresponse[2] = "{PRE}Don't mean to bother but"
genericresponse[3] = "{PRE}All I know is"
genericresponse[4] = "{POST}that is all I know."
genericresponse[5] = "{PRE}Just so you know,"
genericresponse[6] = "{PRE}Heads up!"
genericresponse[7] = "{PRE}By the way,"
genericresponse[8] = "{POST}you're welcome."
//Append user-defined custom personality phrases
def customPre = 8
if (settings?.customPersonalityPre) {
settings.customPersonalityPre.split(',').each {
customPre++
if (customPre <= 19) { genericresponse[customPre] = "{PRE}${it.trim()}" }
}
}
def customPostStart = customPre
if (settings?.customPersonalityPost) {
settings.customPersonalityPost.split(',').each {
customPostStart++
if (customPostStart <= 19) { genericresponse[customPostStart] = "{POST}${it.trim()}" }
}
}
genericoptions = Math.max(8, Math.max(customPre, customPostStart))
//GENERIC RESPONSES END
myRandom = (new Random().nextInt(genericoptions))
LOGDEBUG("genericoptions=${genericoptions};myRandom=${myRandom};phrase=${genericresponse[myRandom]}")
if (genericresponse[myRandom].contains("{PRE}")) {
genericresponse[myRandom] = genericresponse[myRandom].replace("{PRE}", "")
phrase = genericresponse[myRandom] + ", " + phrase
}
if (genericresponse[myRandom].contains("{POST}")) {
genericresponse[myRandom] = genericresponse[myRandom].replace("{POST}", "")
phrase = phrase + ", " + genericresponse[myRandom]
}
return phrase
}
if (options == 0) { return phrase }
myRandom = (new Random().nextInt(options))
LOGDEBUG("options=${options};myRandom=${myRandom};phrase=${response[myRandom]}")
if (response[myRandom].contains("{PRE}")) {
response[myRandom] = response[myRandom].replace("{PRE}", "")
phrase = response[myRandom] + ", " + phrase
}
if (response[myRandom].contains("{POST}")) {
response[myRandom] = response[myRandom].replace("{POST}", "")
phrase = phrase + ", " + response[myRandom]
}
return phrase
}
def adjustWeatherPhrase(phraseIn){
def phraseOut = ""
phraseOut = phraseIn.toUpperCase()
phraseOut = phraseOut.replace(" N ", " North ")
phraseOut = phraseOut.replace(" S ", " South ")
phraseOut = phraseOut.replace(" E ", " East ")
phraseOut = phraseOut.replace(" W ", " West ")
phraseOut = phraseOut.replace(" NNE ", " North Northeast ")
phraseOut = phraseOut.replace(" NNW ", " North Northwest ")
phraseOut = phraseOut.replace(" SSE ", " South Southeast ")
phraseOut = phraseOut.replace(" SSW ", " South Southwest ")
phraseOut = phraseOut.replace(" ENE ", " East Northeast ")
phraseOut = phraseOut.replace(" ESE ", " East Southeast ")
phraseOut = phraseOut.replace(" WNW ", " West Northeast ")
phraseOut = phraseOut.replace(" WSW ", " West Southwest ")
phraseOut = phraseOut.replace(" MPH", " Miles Per Hour")
phraseOut = phraseOut.replace(" MM)", " Milimeters ")
LOGDEBUG ("Adjust Weather: In=${phraseIn} Out=${phraseOut}")
return phraseOut
}
def Talk(appname, phrase, customSpeechDevice, volume, resume, personality, voice, evt, mode = null){
if (mode == null || mode == "") { mode = "auto" }
if (mode == "auto") {
//Auto: split picked devices by capability and send each the right way
def candidateDevices = customSpeechDevice ?: getDefaultSpeechDevices()
def sSDevs = []
def aNDevs = []
def mPDevs = []
candidateDevices.each { d ->
if (d.hasCommand("speak")) {
sSDevs = sSDevs + d
} else if ((d.hasCommand("playTrack") || d.hasCommand("playTrackAndRestore")) && (d.hasAttribute("status") || d.hasAttribute("level") || d.hasAttribute("mute"))) {
mPDevs = mPDevs + d
} else if (d.hasCommand("playTrack") || d.hasCommand("playTrackAndRestore")) {
aNDevs = aNDevs + d
} else {
LOGERROR("Talk: ${d.displayName} does not support a usable audio capability - skipped.")
}
}
if (sSDevs) { Talk(appname, phrase, sSDevs, volume, resume, personality, voice, evt, "capability.speechSynthesis") }
if (aNDevs) { Talk(appname, phrase, aNDevs, volume, resume, personality, voice, evt, "capability.audioNotification") }
if (mPDevs) { Talk(appname, phrase, mPDevs, volume, resume, personality, voice, evt, "capability.musicPlayer") }
return
}
def myDelay = 100
def myVoice = settings?.speechVoice
if (myVoice == "" || myVoice == null) { myVoice = "Salli(en-us)" }
if (!(voice == "" || voice == null)) {
myVoice = voice
}
myVoice = myVoice.replace("(en-us)","")
myVoice = myVoice.replace("(en-gb)","")
myVoice = myVoice.replace("(es-us)","")
if (mode == "capability.musicPlayer") {
myDelay = TalkQueue(appname, phrase, customSpeechDevice, volume, resume, personality, voice, evt)
atomicState.lastTalkTime = now()
}
def currentSpeechDevices = []
def smartAppSpeechDevice = false
def playAudioFile = false
def spoke = false
LOGDEBUG ("TALK(app=${appname},customdevice=${customSpeechDevice},volume=${volume},resume=${resume},personality=${personality},myDelay=${myDelay},voice=${myVoice},evt=${evt},phraseLen=${phrase?.length() ?: 0})")
if ((phrase?.toLowerCase())?.contains("%askalexa%")) {smartAppSpeechDevice = true}
if (!(phrase == null) && !(phrase == "")) {
phrase = processPhraseVariables(appname, phrase, evt, mode)
if (personality && !(phrase.toLowerCase().contains(".mp3"))) { phrase = addPersonalityToPhrase(phrase, evt) }
}
if (phrase == null || phrase == "") {
LOGERROR(processPhraseVariables(appname, "BigTalker - Check configuration. Phrase is empty for %devicename%", evt))
}
if (resume == null) { resume = true }
if ((mode == "capability.musicPlayer") && (!( phrase==null ) && !(phrase==""))){
state.sound = ""
state.ableToTalk = false
if (true) {
LOGTRACE("TALK(${appname}.${evt.name})|mP@|${volume} >> ${phrase}")
if (resume) { LOGTRACE("TALK(${appname}.${evt.name})|mP| Resume is desired") } else { LOGTRACE("TALK(${appname}.${evt.name})|mP| Resume is not desired") }
if (!(phrase.toLowerCase().contains(".mp3"))){
try {
state.sound = textToSpeech(phrase instanceof List ? phrase[0] : phrase, myVoice)
state.ableToTalk = true
} catch(e) {
LOGERROR("TALK(${appname}.${evt.name})|mP| Platform issue (textToSpeech)? ${e}")
//Try Again
try {
LOGTRACE("TALK(${appname}.${evt.name})|mP| Trying textToSpeech function again...")
state.sound = textToSpeech(phrase instanceof List ? phrase[0] : phrase, (settings?.speechVoice ?: "Salli(en-us)").replaceAll(/\(.*\)/, ''))
state.ableToTalk = true
} catch(ex) {
LOGERROR("TALK(${appname}.${evt.name})|mP| Platform issue (textToSpeech)? I tried textToSpeech() twice, ${state.hubType} wouldn't convert/process. I give up, Sorry..")
} //try again before final error(ableToTalk)
} //try (ableToTalk)
} else {
if (isSafeUrl(phrase)) {
LOGTRACE("TALK(${appname}.${evt.name})|mP| MP3=${phrase}")
def sound = [uri:phrase, duration:10]
state.sound = sound
playAudioFile = true
state.ableToTalk = true
LOGTRACE("Sound=${state.sound}")
} else {
LOGERROR("TALK(${appname}.${evt.name})|mP| Rejected unsafe MP3 URL: ${phrase}")
}
}
if ((state?.allowScheduledPoll != false) && (resume)) {
unschedule("poll")
LOGDEBUG("TALK(${appname}.${evt.name})|mP| Delaying polling for 120 seconds")
myRunIn(120, poll)
}
if (state.ableToTalk){
state.sound.duration = (state.sound.duration.toInteger() + 5).toString() //Try to prevent cutting out, add seconds to the duration
if (!(customSpeechDevice == null)) {
currentSpeechDevices = customSpeechDevice
} else {
//Use Default Speech Device
currentSpeechDevices = getDefaultSpeechDevices()
} //if (!(customSpeechDevice == null))
LOGTRACE("TALK(${appname}.${evt.name})|mP| Last poll: ${state.lastPoll}")
//Iterate Speech Devices and talk
def attrs = currentSpeechDevices.supportedAttributes
currentSpeechDevices.each(){
LOGDEBUG("TALK(${appname}.${evt.name})|mP| attrs=${attrs}")
def currentStatus = ""
try {
currentStatus = it?.latestValue("status")
} catch (ex) { LOGDEBUG("ERROR getting device currentStatus") }
def currentTrack = ""
try {
currentTrack = it?.latestState("trackData")?.jsonValue
} catch (ex) { LOGDEBUG("ERROR getting device currentTrack") }
def currentVolume = 0
if (it.hasAttribute("level")) { try { currentVolume = it.latestValue("level")?.toString()?.toInteger() ?: 0 } catch (ex) { currentVolume = 0 } }
if (it.hasAttribute("volume")) { try { currentVolume = it.latestValue("volume")?.toString()?.toInteger() ?: 0 } catch (ex) { currentVolume = 0 } }
if (currentVolume == 0) { LOGDEBUG("ERROR getting device currentVolume") }
def minimumVolume = 50
if (settings?.speechMinimumVolume >= 0) {minimumVolume = settings.speechMinimumVolume}
if (minimumVolume > 100) {minimumVolume = 100}
def desiredVolume = volume
//try {
// desiredVolume = settings?.speechVolume
//} catch (ex) { LOGDEBUG("ERROR getting desired default volume"); desiredVolume = -1 }
if (desiredVolume > 100) {desiredVolume = 100}
LOGDEBUG("TALK(${appname}.${evt.name})|mP| currentStatus:${currentStatus}")
LOGDEBUG("TALK(${appname}.${evt.name})|mP| currentTrack:${currentTrack}")
LOGDEBUG("TALK(${appname}.${evt.name})|mP| currentVolume:${currentVolume}")
LOGDEBUG("TALK(${appname}.${evt.name})|mP| Sound: ${state.sound.uri} , ${state.sound.duration}")
if (desiredVolume > -1){
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | Volume: ${currentVolume}, Desired Volume: ${desiredVolume}")
} else {
if (!(currentVolume >= minimumVolume)) {
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | Volume: ${currentVolume}, Minimum Volume: ${minimumVolume}; adjusting.")
} else {
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | Volume: ${currentVolume}, Minimum Volume: ${minimumVolume}; acceptable.")
}
}
if (!(currentTrack == null)){
//currentTrack has data
if (!(currentTrack?.status == null)) { LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | Current Status: ${currentStatus}, CurrentTrack: ${currentTrack}, CurrentTrack.Status: ${currentTrack.status}.") }
if (currentTrack?.status == null) { LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | Current Status: ${currentStatus}, CurrentTrack: ${currentTrack}.") }
if ((currentStatus == 'playing' || currentTrack?.status == 'playing') && (!((currentTrack?.status == 'stopped') || (currentTrack?.status == 'paused')))) { //Give currentTrack.status presidence if it exists, it seems more accurate
if (resume) {
LOGTRACE ("Sending playTrackandResume() 1")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT<>null | cS/cT=playing | Sending playTrackAndResume() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndResume(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndResume(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
} //if (desiredVolume)
} else {
//resume is not desired
LOGTRACE ("Sending playTrackandRestore() 2 - ${it?.displayName} - cVol = ${currentVolume}")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT<>null | cS/cT=playing | NoResume! | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
} // if (desiredVolume)
} // if (resume)
} else {
if (!(currentTrack?.status == 'playing') && (currentStatus == 'playing')) {
LOGDEBUG "TALK(${appname}.${evt.name})|mP| ${it?.displayName} | Discrepency in CS/CT, going with CT! | CS= ${currentStatus} CT=${currentTrack.status}"
}
LOGTRACE ("Sending playTrackandRestore() 3 - to ${it?.displayName} - cVol = ${currentVolume}")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT<>null | cS/cT<>playing | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}// if (desiredVolume)
}// if ((currentStatus == 'playing' || currentTrack?.status == 'playing') && (!((currentTrack?.status == 'stopped') || (currentTrack?.status == 'paused'))))
} else {
//currentTrack==null. currentTrack doesn't have data or is not supported on this device
if (!(currentStatus == null)) {
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | (2) Current Status: ${currentStatus}.")
if (currentStatus == "disconnected") {
//VLCThing?
if (resume) {
LOGTRACE ("Sending playTrackandResume() 4")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT=null | cS=disconnected | Sending playTrackAndResume() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndResume(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndResume(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}
} else {
//resume is not desired
LOGTRACE ("Sending playTrackandRestore() 5")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT=null | cS=disconnected | No Resume! | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}// if (desiredVolume)
}// if (resume)
} else {
if (currentStatus == "playing") {
if (resume) {
LOGTRACE ("Sending playTrackandResume() 6")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT=null | cS=playing | Sending playTrackAndResume() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndResume(it,state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndResume(it,state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndResume(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}// if (desiredVolume)
} else {
//resume not desired
LOGTRACE ("Sending playTrackandRestore() 7")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT=null | cS=playing | No Resume! | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}// if (desiredVolume)
}// if (resume)
} else {
//currentStatus <> "playing"
LOGTRACE ("Sending playTrackandRestore() 8")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it?.displayName} | cT=null | cS<>playing | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it,state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it,state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it,state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { playTrackAndRestore(it, state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
}// if (desiredVolume)
}// if (currentStatus == "playing")
}// if (currentStatus == "disconnected"))
} else {
//currentTrack and currentStatus are both null
LOGTRACE ("Sending playTrackandRestore() 9")
LOGTRACE("TALK(${appname}.${evt.name})|mP| ${it.displayName} | (3) cT=null | cS=null | Sending playTrackAndRestore() | CVol=${currentVolume} | SVol=${desiredVolume}")
if (desiredVolume > -1) {
//if (desiredVolume == currentVolume){playTrackAndRestore(it, state.sound.uri, state.sound.duration, myDelay)}
//if (!(desiredVolume == currentVolume)){playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)}
playTrackAndRestore(it, state.sound.uri, state.sound.duration, desiredVolume, myDelay, phrase)
spoke = true
} else {
//if (currentVolume >= minimumVolume) { it.playTrackAndRestore(state.sound.uri, state.sound.duration, myDelay) }
if (currentVolume >= minimumVolume) { it.playTrackAndRestore(state.sound.uri, state.sound.duration, currentVolume, myDelay, phrase) }
if (currentVolume < minimumVolume) { it.playTrackAndRestore(state.sound.uri, state.sound.duration, minimumVolume, myDelay, phrase) }
spoke = true
} //if (desiredVolume)
} //currentStatus == null
} //currentTrack == null
} //currentSpeechDevices.each()
} //state.ableToTalk
} //if (!(settings.speechDeviceDefault == null) || !(customSpeechDevice == null))
}// if (state.speechDeviceType=="capability.musicPlayer")
if ((mode == "capability.speechSynthesis") && (!( phrase==null ) && !(phrase==""))){
//capability.speechSynthesis is in use
//Prevent overlapping speech announcements by enforcing a minimum gap
def minGapMs = 6000
if (atomicState.lastTalkTimeS != null && (now() - atomicState.lastTalkTimeS) < minGapMs) {
//Prevent overlapping speech announcements; wait out the gap
pauseExecution((minGapMs - (now() - atomicState.lastTalkTimeS)) as long)
}
atomicState.lastTalkTimeS = now()
if (true) {
LOGTRACE("TALK(${appname}.${evt.name}) |sS| >> ${phrase}")
if (!(customSpeechDevice == null)) {
currentSpeechDevices = customSpeechDevice
} else {
//Use Default Speech Device
currentSpeechDevices = getDefaultSpeechDevices()
}// If (!(customSpeechDevice == null))
//Iterate Speech Devices and talk
def attrs = currentSpeechDevices.supportedAttributes
currentSpeechDevices.each(){
// Determine device name either by it.displayName or it.device.displayName (whichever works)
LOGDEBUG("FINGERPRINT ${it.displayName} cap:${it.getSupportedCapabilities} comm:${it.getSupportedCommands()} att:${it.getSupportedAttributes()}")
try {
LOGTRACE("TALK(${appname}.${evt.name}) |sS| ${it.displayName} | Sending speak().")
}
catch (ex) {
LOGDEBUG("TALK(${appname}.${evt.name}) |sS| it.displayName failed, trying it.device.displayName")
try {
LOGTRACE("TALK(${appname}.${evt.name}) |sS| ${it.device.displayName} | Sending speak().")
}
catch (ex2) {
LOGDEBUG("TALK(${appname}.${evt.name}) |sS| it.device.displayName failed, trying it.device.name")
LOGTRACE("TALK(${appname}.${evt.name}) |sS| ${it.device.name} | Sending speak().")
}
}
spoke = true
if (it.hasCommand("speak")) {
it.speak(phrase)
} else {
LOGERROR("TALK(${appname}.${evt.name}) |sS| ${it.displayName} does not support speak() - skipped.")
}
}// currentSpeechDevices.each()
} //if (!(settings.speechDeviceDefault == null) || !(customSpeechDevice == null))
} //if (mode == "capability.speechSynthesis")
if ((mode == "capability.audioNotification") && (!( phrase==null ) && !(phrase==""))){
//capability.audioNotification is in use
//Prevent overlapping audio notifications by enforcing a minimum gap
def minGapMs = 6000
if (atomicState.lastTalkTimeA != null && (now() - atomicState.lastTalkTimeA) < minGapMs) {
//Prevent overlapping audio notifications; wait out the gap
pauseExecution((minGapMs - (now() - atomicState.lastTalkTimeA)) as long)
}
atomicState.lastTalkTimeA = now()
if (true) {
LOGTRACE("TALK(${appname}.${evt.name}) |aN| >> ${phrase}")
if (!(customSpeechDevice == null)) {
currentSpeechDevices = customSpeechDevice
} else {
currentSpeechDevices = getDefaultSpeechDevices()
}
state.sound = null
state.ableToTalk = false
if (!(phrase.toLowerCase().contains(".mp3"))){
try {
state.sound = textToSpeech(phrase instanceof List ? phrase[0] : phrase, myVoice)
state.ableToTalk = true
} catch(e) {
LOGERROR("TALK(${appname}.${evt.name})|aN| Platform issue (textToSpeech)? ${e}")
try {
def fallbackVoice = (settings?.speechVoice ?: "Salli(en-us)").replaceAll(/\(.*\)/, '')
state.sound = textToSpeech(phrase instanceof List ? phrase[0] : phrase, fallbackVoice)
state.ableToTalk = true
} catch(ex) {
LOGERROR("TALK(${appname}.${evt.name})|aN| textToSpeech() failed twice. Giving up.")
}
}
} else {
if (isSafeUrl(phrase)) {
state.sound = [uri: phrase, duration: 10]
state.ableToTalk = true
} else {
LOGERROR("TALK(${appname}.${evt.name})|aN| Rejected unsafe MP3 URL: ${phrase}")
}
}
if (state.ableToTalk) {
def desiredVolume = getDesiredVolume(volume)
if (desiredVolume > 100) {desiredVolume = 100}
if (desiredVolume < 0) {desiredVolume = 50}
def soundUri = state.sound.uri
for (dev in currentSpeechDevices) {
if (dev.hasCommand("playTrackAndRestore")) {
try { dev.playTrackAndRestore(soundUri, desiredVolume); spoke = true } catch (ex) { LOGERROR("TALK(${appname}.${evt.name})|aN| ${dev.displayName} playTrackAndRestore failed: ${ex}") }
} else if (dev.hasCommand("playTrack")) {
try { dev.playTrack(soundUri, desiredVolume); spoke = true } catch (ex) { LOGERROR("TALK(${appname}.${evt.name})|aN| ${dev.displayName} playTrack failed: ${ex}") }
} else {
LOGERROR("TALK(${appname}.${evt.name})|aN| ${dev.displayName} does not support playTrack or playTrackAndRestore")
}
}
}
}
} //if (mode == "capability.audioNotification")
if ((!(smartAppSpeechDevice) && !(spoke)) && (!(phrase == null) && !(phrase == "")) && !(playAudioFile)) {
//No musicPlayer, speechSynthesis, or smartAppSpeechDevices selected. No route to export speech!
LOGTRACE("TALK(${appname}.${evt.name}) |ERROR| No selected speech device or smartAppSpeechDevice token in phrase. ${phrase}")
} else {
if ((smartAppSpeechDevice && !spoke) && (!(phrase == null) && !(phrase == ""))){
LOGTRACE("TALK(${appname}.${evt.name}) |sA| Sent to another smartApp.")
}
}
phrase = ""
}//Talk()
def getTimeFromDateString(inputtime, includeAmPm){
//I couldn't find the way to do this in ST / Groovy, so I made my own function
//Obtains the time from a supplied specifically formatted date string (ie: from a preference of type "time")
//LOGDEBUG "InputTime: ${inputtime}"
def outputtime = inputtime
def am_pm = "??"
outputtime = inputtime.substring(11,16)
if (includeAmPm) {
if ((outputtime.substring(0,2)).toInteger() < 12) {
am_pm = "am"
} else {
am_pm = "pm"
def newHH = ((outputtime.substring(0,2)).toInteger() - 12)
outputtime = newHH + outputtime.substring(2,5)
}
outputtime += am_pm
}
//LOGDEBUG "OutputTime: ${outputtime}"
return outputtime
}
def getTimeFromCalendar(includeSeconds, includeAmPm){
//Obtains the current time: HH:mm:ss am/pm
def calendar = Calendar.getInstance()
calendar.setTimeZone(location.timeZone)
def timeHH = calendar.get(Calendar.HOUR)
def timemm = calendar.get(Calendar.MINUTE)
def timess = calendar.get(Calendar.SECOND)
def timeampm = calendar.get(Calendar.AM_PM) ? "pm" : "am"
def timestring = "${timeHH}:${timemm}"
if (includeSeconds) { timestring += ":${timess}" }
if (includeAmPm) { timestring += " ${timeampm}" }
return timestring
}
//myRunIn from ST:Geko / Statusbits SmartAlarm app http://statusbits.github.io/smartalarm/
private def myRunIn(delay_s, func) {
//LOGDEBUG("myRunIn(${delay_s},${func})")
if (delay_s > 0) {
def tms = now() + (delay_s * 1000)
def date = new Date(tms)
runOnce(date, func)
//LOGDEBUG("'${func}' scheduled to run at ${date}")
}
}
def Notify(appname, phrase, customTextDevices, evt){
//Text Only announcements: the same phrase as speech, delivered as text to notification devices (Hubitat mobile app, Telegram, Pushover, etc.)
def devices = customTextDevices ?: getDefaultTextDevices()
if (!devices) { return }
if (phrase == null || phrase == "") { return }
def text = processPhraseVariables(appname, phrase, evt, null)
if (text == null || text == "") { return }
if (text.toLowerCase().contains(".mp3") || text.toLowerCase().contains("%askalexa%")) {
LOGDEBUG("Notify: phrase is audio-only (MP3/AskAlexa); skipping text notification", true)
return
}
devices.each { d ->
try {
if (d.hasCommand("deviceNotification")) {
d.deviceNotification(text)
LOGTRACE("NOTIFY(${appname})|${d.displayName} >> ${text}")
} else {
LOGERROR("Notify: ${d.displayName} does not support deviceNotification - skipped.")
}
} catch (ex) {
LOGERROR("Notify: failed to send text to ${d.displayName}: ${ex}")
}
}
}
def TalkQueue(appname, phrase, customSpeechDevice, volume, resume, personality, voice, evt){
//IN DEVELOPMENT
// Already talking or just recently (within x seconds) started talking
// Queue up current request(s), give time for current action to complete, then speak and flush queue
def threshold = 0
def minDelay = 6 //Minimum seconds between talking
try {
if (!(state?.sound?.duration == null)) {
threshold = state.sound.duration.toInteger() //Use the last musicPlayer sound duration from the last Talk call as the minimum delay
}
} catch (exception) {
threshold = 10
}
def durationFromLastTalkReq = 9999
//if (!(state.lastTalkTime == null)) { durationFromLastTalk = ((now() - state?.lastTalkTime)/1000).intValue() }
if (!(atomicState.lastTalkRequest == null)) { durationFromLastTalkReq = ((now() - atomicState.lastTalkRequest)/1000).intValue() }
atomicState.lastTalkRequest = now()
def tooSoon = (durationFromLastTalkReq < threshold)
def neededDelay = (((threshold - durationFromLastTalkReq) * 1000) + 1000)
LOGDEBUG ("TALKQUEUE(Threshold=${threshold},DurationFromLastTalkReq=${durationFromLastTalkReq},lastTalkReq=${atomicState.lastTalkRequest},lastTalkTime=${atomicState.lastTalkTime}, TooSoon=${tooSoon}, Calc=${neededDelay}")
if (tooSoon) {
if (neededDelay < 0) {
neededDelay = 0
} else {
if (neededDelay < (minDelay * 1000)) { neededDelay = (minDelay * 1000) }
}
LOGDEBUG("TALKQUEUE()-Spoke too recently; delaying ${(neededDelay / 1000)} seconds.")
return neededDelay
} else {
LOGDEBUG("TALKQUEUE()-OK to speak; (${(durationFromLastTalkReq)})")
return 0
}
}
def getWeather(mode, zipCode) {
//Uses the free, global, key-less Open-Meteo API (works US/UK/worldwide)
def msg = ""
//Use the cached forecast if it's fresh (<10 min)
if (state?.weatherCacheTime != null && state?.weatherCacheMode == mode && (now() - state.weatherCacheTime) < 600000 && state?.weatherCacheMsg) {
LOGDEBUG("getWeather: returning cached msg for mode=${mode}")
return state.weatherCacheMsg
}
if ((state?.latitude == null || state?.latitude == "") && settings?.latitude == null) { state.latitude = "${location.latitude}" }
if ((state?.longitude == null || state?.longitude == "") && settings?.longitude == null) { state.longitude = "${location.longitude}" }
def glat = null
def glon = null
try { glat = state.latitude?.toString()?.toDouble() } catch (ex) {}
try { glon = state.longitude?.toString()?.toDouble() } catch (ex) {}
if (glat == null || glon == null || glat < -90 || glat > 90 || glon < -180 || glon > 180) {
return "The weather is currently unavailable."
}
state.latitude = glat
state.longitude = glon
def tempUnit = (location.temperatureScale == "C") ? "celsius" : "fahrenheit"
def wxUri = "https://api.open-meteo.com/v1/forecast?latitude=${state.latitude}&longitude=${state.longitude}¤t_weather=true&daily=weathercode,temperature_2m_max,temperature_2m_min&timezone=auto&forecast_days=2&temperature_unit=${tempUnit}"
LOGTRACE("DEBUG wxURI: ${wxUri}")
def requestParams = [
uri: wxUri,
requestContentType: "application/json",
contentType: "application/json"
]
try {
httpGet(requestParams) { response ->
if (response?.status == 200) {
LOGDEBUG("getWeather: response=${response.data}")
def cw = response.data?.current_weather
def daily = response.data?.daily
def todayHigh = daily?.temperature_2m_max ? daily.temperature_2m_max[0] : null
def todayLow = daily?.temperature_2m_min ? daily.temperature_2m_min[0] : null
def tomorrowHigh = (daily?.temperature_2m_max && daily.temperature_2m_max.size() > 1) ? daily.temperature_2m_max[1] : null
def tomorrowLow = (daily?.temperature_2m_min && daily.temperature_2m_min.size() > 1) ? daily.temperature_2m_min[1] : null
def todayCode = daily?.weathercode ? daily.weathercode[0] : (cw?.weathercode ?: null)
def tomorrowCode = (daily?.weathercode && daily.weathercode.size() > 1) ? daily.weathercode[1] : null
def tUnit = tempUnit == "celsius" ? "celsius" : "fahrenheit"
if (mode == "current") {
msg = "The current temperature is ${cw?.temperature} degrees ${tUnit} with ${weatherCodeToText(cw?.weathercode)}."
}
if (mode == "today") {
msg = "Today the forecast is ${weatherCodeToText(todayCode)} with a high of ${todayHigh} and a low of ${todayLow} degrees ${tUnit}."
}
if (mode == "tonight") {
msg = "Tonight the forecast is ${weatherCodeToText(todayCode)} with a low of ${todayLow} degrees ${tUnit}."
}
if (mode == "tomorrow") {
msg = "Tomorrow the forecast is ${weatherCodeToText(tomorrowCode)} with a high of ${tomorrowHigh} and a low of ${tomorrowLow} degrees ${tUnit}."
}
}
}
} catch (ex) {
LOGERROR("getWeather(): ${ex}")
}
if (msg == "" || msg == null) {
if (state?.weatherCacheMsg) { msg = state.weatherCacheMsg } else { msg = "The weather is currently unavailable." }
} else {
state.weatherCacheMsg = msg
state.weatherCacheTime = now()
state.weatherCacheMode = mode
}
return msg
}
//Only allow plain http/https URLs (prevents file://, javascript:, data: injection),
//and only allow plain http for local/trusted hosts.
def isSafeUrl(url) {
if (url == null) { return false }
def match = url.toString().trim() =~ /(?i)^(https?):\/\/([^\/?#]+)(?:[\/?#].*)?$/
if (!match.matches()) { return false }
def protocol = match[0][1].toLowerCase()
def authority = match[0][2]
if (authority.contains('@') || authority.any { it <= ' ' }) { return false }
def host = authority
def port = null
if (authority.startsWith('[')) {
def closingBracket = authority.indexOf(']')
if (closingBracket < 0) { return false }
host = authority.substring(1, closingBracket).toLowerCase()
def remainder = authority.substring(closingBracket + 1)
if (remainder != '') {
if (!(remainder ==~ /:[0-9]{1,5}/)) { return false }
port = remainder.substring(1).toInteger()
}
if (!(host ==~ /(?i)[0-9a-f:.]+/)) { return false }
} else {
def colon = authority.lastIndexOf(':')
if (colon >= 0) {
if (authority.indexOf(':') != colon) { return false }
def portText = authority.substring(colon + 1)
if (!(portText ==~ /[0-9]{1,5}/)) { return false }
port = portText.toInteger()
host = authority.substring(0, colon)
}
host = host.toLowerCase()
if (!(host ==~ /[a-z0-9.-]+/)) { return false }
}
if (port != null && (port < 1 || port > 65535)) { return false }
if (protocol == 'https') { return true }
if (host == 'localhost' || host == '::1' || host.endsWith('.local')) { return true }
if (host ==~ /[0-9]{1,3}(\.[0-9]{1,3}){3}/) {
def octets = host.split('\\.').collect { it.toInteger() }
if (octets.any { it > 255 }) { return false }
return octets[0] == 10 || octets[0] == 127 ||
(octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31) ||
(octets[0] == 192 && octets[1] == 168) ||
(octets[0] == 169 && octets[1] == 254)
}
return host ==~ /(?i)fe[89ab][0-9a-f]*:.*/ || host ==~ /(?i)fec[0-3]:.*/
}
def weatherCodeToText(code) {
//WMO weather code -> plain text for speech
if (code == null) { return "unknown conditions" }
def m = [
0: "clear sky",
1: "mainly clear",
2: "partly cloudy",
3: "overcast",
45: "fog",
48: "depositing rime fog",
51: "light drizzle",
53: "moderate drizzle",
55: "dense drizzle",
56: "light freezing drizzle",
57: "dense freezing drizzle",
61: "slight rain",
63: "moderate rain",
65: "heavy rain",
66: "light freezing rain",
67: "heavy freezing rain",
71: "slight snowfall",
73: "moderate snowfall",
75: "heavy snowfall",
77: "snow grains",
80: "slight rain showers",
81: "moderate rain showers",
82: "violent rain showers",
85: "slight snow showers",
86: "heavy snow showers",
95: "thunderstorm",
96: "thunderstorm with slight hail",
99: "thunderstorm with heavy hail"
]
return m[code as Integer] ?: "unknown conditions"
}
def poll(){
if (settings?.resumePlay == true || settings?.resumePlay == null) {
unschedule("poll")
//LOGDEBUG("poll() settings=${settings?.allowScheduledPoll}")
//LOGDEBUG("poll() state=${state?.allowScheduledPoll}")
//LOGDEBUG("poll() resumePlay=${settings?.resumePlay}")
if (settings?.allowScheduledPoll != false) {
state.allowScheduledPoll = true
} else {
state.allowScheduledPoll = false
LOGDEBUG("Polling is not desired, disabling after this poll.")
}
if (true) {
LOGDEBUG("Polling speech device(s) for latest status")
state.polledDevices = ""
try {
if (!(settings?.speechDeviceDefaultMusicPlayer == null)) {dopoll(settings.speechDeviceDefaultMusicPlayer)}
} catch(e) {
LOGERROR("One of your speech devices is not responding. Poll failed.")
LOGDEBUG("BT_poll() Error: ${e}")
}
state.lastPoll = getTimeFromCalendar(true,true)
//LOGDEBUG("poll: state.polledDevices == ${state?.polledDevices}")
if (!(state?.polledDevices == "")) {
//Reschedule next poll
if (settings?.allowScheduledPoll != false) {
LOGDEBUG("Rescheduling Poll")
myRunIn(60, poll)
}
} else {
LOGDEBUG("No speech devices polled. Cancelling polling.")
}
}
}
}
def dopoll(pollSpeechDevice){
pollSpeechDevice.each(){
def devicename = ""
try {
devicename = it.displayName
} catch (ex) {}
if (devicename == "") {
try {
devicename = it.device.displayName
} catch (ex) {}
}
if (devicename == "") {
LOGERROR("dopoll(${pollSpeechDevice}) - Unable to get devicename")
}
if (!(state?.polledDevices?.find("|${devicename}|"))) {
state.polledDevices = state?.polledDevices + "|${devicename}|"
LOGDEBUG("dopoll(${devicename}) Polling ")
state.refresh = false
state.poll = false
try {
//LOGTRACE("refresh()")
it.refresh()
state.refresh = true
}
catch (ex) {
LOGDEBUG("ERROR(informational): it.refresh: ${ex}")
state.refresh = false
}
//LOGDEBUG("dopoll(${devicename}) after refresh() ")
if (!(state.refresh)) {
try {
//LOGTRACE("poll()")
it.poll()
state.poll = true
state.refresh = true
}
catch (ex) {
LOGDEBUG ("ERROR(informational): it.poll: ${ex}")
state.poll = false
state.refresh = false
}
}
//LOGDEBUG("dopoll(${devicename}) after poll()")
//LOGDEBUG("dopoll(${devicename}) refresh=${state.refresh} poll=${state.poll}")
//LOGDEBUG("dopoll(${it.displayName})cS=${it?.latestValue('status')},cT=${it?.latestState("trackData")?.jsonValue?.status},cV=${it?.latestState("level")?.integerValue ? it?.latestState("level")?.integerValue : 0}")
if (it?.latestValue('status') == "no_device_present") { LOGTRACE("During polling, the handler for ${devicename} indicated the device was not found.") } //VLCThing
}
LOGDEBUG("dopoll - polled devices: ${state?.polledDevices}")
}
}
def getSharedLastTalkMs() {
//Shared across all BigTalker child groups via a location event
try { return location.currentState("BigTalkerLastTalk")?.value?.toString()?.toLong() } catch (ex) { return null }
}
def noteSharedLastTalk() {
try { sendLocationEvent(name: "BigTalkerLastTalk", value: "${now()}") } catch (ex) {}
}
def replaceTokenIgnoreCase(String phrase, String needle, String replacement) {
if (phrase == null) { return phrase }
def needleRegex = needle.replaceAll(/[.\[\]{}()*+?^$\\|]/, '\\$0')
return phrase.replaceAll(/(?i)${needleRegex}/, java.util.regex.Matcher.quoteReplacement(replacement?.toString() ?: ''))
}
def getDefaultSpeechDevices(){
//Default devices picked in the app defaults page, all capability types
def devs = []
if (settings?.speechDeviceDefaultMusicPlayer) { devs = devs + settings.speechDeviceDefaultMusicPlayer }
if (settings?.speechDeviceDefaultSpeechSynthesis) { devs = devs + settings.speechDeviceDefaultSpeechSynthesis }
if (settings?.speechDeviceDefaultAudioNotification) { devs = devs + settings.speechDeviceDefaultAudioNotification }
return devs
}
def getDefaultTextDevices(){
//Default Text Only devices picked in the app defaults page
def devs = []
if (settings?.textDeviceDefault) { devs = devs + settings.textDeviceDefault }
return devs
}
def getDesiredVolume(invol) {
def globalVolume = settings?.speechVolume
def globalMinimumVolume = settings?.speechMinimumVolume
def myVolume = invol
def finalVolume = -1
if (myVolume > 0) {
finalVolume = myVolume
} else {
if (globalVolume > 0) {
finalVolume = globalVolume
} else {
if (globalMinimumVolume > 0) {
finalVolume = globalMinimumVolume
} else {
finalVolume = 50 //Default if no volume parameters are set
}
}
}
if (true) {
LOGDEBUG("finalVolume: ${finalVolume}")
}
return finalVolume
}
def setMode(mode){
// Remove this function
}
def onModeChangeEvent(evt){
state.lastMode = state.mode
state.mode = location.mode
LOGDEBUG("LastMode=${state.lastMode} Mode=${location.mode}")
}
def disableDebug(){
LOGTRACE("Debug timer has expired. Disabling debugging")
state.debugMode = false
state.debugAutoOff = true
unschedule("disableDebug")
}
def LOGDEBUG(txt){
if (state.debugMode) {
def msgfrom = "[PARENT] "
def appLabel = (app?.label == null) ? state.InternalName : app.label //Some child calls to parent.LOGDEBUG result in app.label being null, correct
appLabel = appLabel.replace(" ","")
appLabel.toUpperCase()
if (txt?.contains("[CHILD:")) { msgfrom = "" }
txt = txt?.toString()?.replaceAll(/[\r\n]+/, ' ')
try {
log.debug("${appLabel}(${state.version}) || ${msgfrom}${txt}")
} catch(ex) {
log.error("LOGDEBUG unable to output requested data! || err:${ex}")
}
}
}
def LOGTRACE(txt){
def msgfrom = "[PARENT] "
def appLabel = (app?.label == null) ? state.InternalName : app.label //Some child calls to parent.LOGTRACE result in app.label being null, correct
appLabel = appLabel.replace(" ","")
appLabel.toUpperCase()
if (txt?.contains("[CHILD:")) { msgfrom = "" }
txt = txt?.toString()?.replaceAll(/[\r\n]+/, ' ')
try {
log.trace("${appLabel}(${state.version}) || ${msgfrom}${txt}")
} catch(ex) {
log.error("LOGTRACE unable to output requested data!")
}
}
def LOGERROR(txt){
def msgfrom = "[PARENT] "
def appLabel = (app?.label == null) ? state.InternalName : app.label //Some child calls to parent.LOGERROR result in app.label being null, correct
appLabel = appLabel.replace(" ","")
appLabel.toUpperCase()
if (txt?.contains("[CHILD:")) { msgfrom = "" }
txt = txt?.toString()?.replaceAll(/[\r\n]+/, ' ')
try {
log.error("${appLabel}(${state.version}) || ${msgfrom}ERROR: ${txt}")
} catch(ex) {
log.error("LOGERROR unable to output requested data!")
}
}
def getHubType(){
return "Hubitat"
}
//Escape user/remote-supplied text for safe rendering inside HTML
def escapeHtml(sText) {
if (sText == null) { return "" }
def s = sText.toString()
s = s.replaceAll(/&/, '&')
s = s.replaceAll(/, '<')
s = s.replaceAll(/>/, '>')
s = s.replaceAll(/"/, '"')
s = s.replaceAll(/'/, ''')
return s
}
def returnVar(var) {
def dataType = "String"
def returnValue
if (!(settings."${var}" == null)) { returnValue = settings."${var}" }
if (!(state."${var}" == null)) { returnValue = state."${var}" }
if (!(atomicState."${var}" == null)) { returnValue = atomicState."${var}" }
def dateTest = returnValue =~ /\d\d\d\d-\d\d-\d\dT\d\d:/
if (dateTest) { dataType = "Date" }
if (returnValue == "true") { dataType = "Boolean" }
if (returnValue == "false") { dataType = "Boolean" }
if (returnValue == true) { dataType = "Boolean" }
if (returnValue == false) { dataType = "Boolean" }
if (dataType == "Date") {returnValue = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", returnValue)}
//LOGDEBUG ("returnVar(${var}), DataType:${dataType}, Value: ${returnValue}")
if (returnValue == null || returnValue == "") {}
return returnValue
}
def playTrackAndRestore(device, uri, duration, volume, myDelay, phrase) {
LOGDEBUG("playTrackAndRestore(${device.displayName},${uri},${duration},${volume},${myDelay})")
if (state.hubType == "Hubitat") {
def isEchoSpeaksLike = device.hasCommand("setVolumeAndSpeak")
def isSonosLike = device.hasCommand("playTrackAndRestore") //Hubitat only supports playTrackAndRestore(uri,volume) for Sonos
def supportsPlayTrack = device.hasCommand("playTrack")
def supportsSetLevel = device.hasCommand("setLevel")
def supportsSetVolume = device.hasCommand("setVolume")
def supportsUnmute = device.hasCommand("unmute")
def supportsATTRLevel = device.hasAttribute("level")
def supportsATTRVolume = device.hasAttribute("volume")
def curVol = -100
if (supportsATTRLevel){ curVol = device.latestValue("level")?.toString()?.toInteger() ?: -100 }
if (supportsATTRVolume){ curVol = device.latestValue("volume")?.toString()?.toInteger() ?: -100 }
LOGDEBUG("FINGERPRINT ${device.displayName} cap:${device.getSupportedCapabilities} comm:${device.getSupportedCommands()} att:${device.getSupportedAttributes()}")
if (isSonosLike){ //NEED ADDITIONAL SUPPORTED COMMANDS/ATTRIBUTES TO FINGERPRINT!
// SONOS SUPPORT
LOGDEBUG("playTrackAndRestore: Sonos like device detected, Please send previous debug log entry to Rayzurbock for Fingerprinting.")
//LOGDEBUG("playTrackAndRestore: Sonos like device detected, calling playTrackAndRestore(uri,vol)")
//device.playTrackAndRestore(uri, volume) //Hubitat only supports playTrackAndRestore(uri,volume) for Sonos
//return
}
if (isEchoSpeaksLike) {
// ECHOSPEAKS SUPPORT
// setVolumeSpeakAndRestore - Sets volume, plays message, restores volume
LOGDEBUG("playTrackAndRestore: EchoSpeaks like device detected, calling setVolumeSpeakAndRestore(volume, message)")
setVolumeSpeakAndRestore(volume, phrase)
return
}
if (supportsPlayTrack) {
def curTrack = device.trackData
if (supportsUnmute){
LOGDEBUG("playTrackAndRestore[playTrack]: unmute()")
device.unmute()
pauseExecution(250)
} else {
LOGDEBUG("playTrackAndRestore[playTrack]: unmute() is not supported by this device (${device.displayName})")
}
if (supportsSetLevel || supportsSetVolume){
if (supportsSetLevel){
LOGDEBUG("playTrackAndRestore[playTrack]: setLevel(${volume})")
device.setLevel(volume)
pauseExecution(250)
}
if (supportsSetVolume && !supportsSetLevel){
LOGDEBUG("playTrackAndRestore[playTrack]: setVolume(${volume})")
device.setVolume(volume)
pauseExecution(250)
}
} else {
LOGDEBUG("playTrackAndRestore[playTrack]: setLevel/setVolume is not supported by this device (${device.displayName})")
}
LOGDEBUG("playTrackAndRestore[playTrack]: playTrack(${uri})")
device.playTrack(uri)
pauseExecution((duration.toInteger() * 1000))
if (supportsSetLevel || supportsSetVolume){
if ((curVol > -100) && supportsSetLevel){
LOGDEBUG("playTrackAndRestore[playTrack]: setLevel(${curVol})")
device.setLevel(curVol)
pauseExecution(250)
}
if ((curVol > -100) && (supportsSetVolume && !supportsSetLevel)){
LOGDEBUG("playTrackAndRestore[playTrack]: setVolume(${curVol})")
device.setVolume(curVol)
pauseExecution(250)
}
} else {
LOGDEBUG("playTrackAndRestore[playTrack]: setLevel/setVolume is not supported by this device (${device.displayName})")
}
if (!curTrack?.uri == null){
LOGDEBUG("playTrackAndRestore[playTrack]: restoreTrack(${curTrack.uri})")
if (device.hasCommand("restoreTrack")){
device.restoreTrack(curTrack.uri)
} else {
LOGDEBUG("playTrackAndRestore[playTrack]: restoreTrack() is not supported by this device (${device.displayName})")
}
}
}
}
}
def playTrackAndResume(device, uri, duration, volume, myDelay, phrase) {
LOGDEBUG("playTrackAndResume(${device.displayName},${uri},${duration},${volume},${myDelay})")
if (state.hubType == "Hubitat") {
def isEchoSpeaksLike = device.hasCommand("setVolumeAndSpeak")
def isSonosLike = device.hasCommand("playTrackAndRestore") //Hubitat only supports playTrackAndRestore(uri,volume) for Sonos
def supportsPlayTrack = device.hasCommand("playTrack")
def supportsSetLevel = device.hasCommand("setLevel")
def supportsSetVolume = device.hasCommand("setVolume")
def supportsUnmute = device.hasCommand("unmute")
def supportsATTRLevel = device.hasAttribute("level")
def supportsATTRVolume = device.hasAttribute("volume")
def curVol = -100
if (supportsATTRLevel){ curVol = device.latestValue("level")?.toString()?.toInteger() ?: -100 }
if (supportsATTRVolume){ curVol = device.latestValue("volume")?.toString()?.toInteger() ?: -100 }
LOGDEBUG("FINGERPRINT ${device.displayName} cap:${device.getSupportedCapabilities} comm:${device.getSupportedCommands()} att:${device.getSupportedAttributes()}")
if (isSonosLike){ //NEED ADDITIONAL SUPPORTED COMMANDS/ATTRIBUTES TO FINGERPRINT!
// SONOS SUPPORT
LOGDEBUG("playTrackAndRestore: Sonos like device detected, Please send previous debug log entry to Rayzurbock for Fingerprinting.")
//LOGDEBUG("playTrackAndRestore: Sonos like device detected, calling playTrackAndRestore(uri,vol)")
//device.playTrackAndRestore(uri, volume) //Hubitat only supports playTrackAndRestore(uri,volume) for Sonos
//return
}
if (isEchoSpeaksLike) {
// ECHOSPEAKS SUPPORT
// setVolumeSpeakAndRestore - Sets volume, plays message, restores volume
LOGDEBUG("playTrackAndResume: EchoSpeaks like device detected, calling setVolumeSpeakAndRestore(volume, message); Resume track not supported")
setVolumeSpeakAndRestore(volume, phrase)
return
}
if (supportsPlayTrack) {
def curTrack = device.trackData
if (supportsUnmute){
LOGDEBUG("playTrackAndResume[playTrack]: unmute()")
device.unmute()
pauseExecution(250)
} else {
LOGDEBUG("playTrackAndResume[playTrack]: unmute() is not supported by this device (${device.displayName})")
}
if (supportsSetLevel || supportsSetVolume){
if (supportsSetLevel){
LOGDEBUG("playTrackAndResume[playTrack]: setLevel(${volume})")
device.setLevel(volume)
pauseExecution(250)
}
if (supportsSetVolume && !supportsSetLevel){
LOGDEBUG("playTrackAndResume[playTrack]: setVolume(${volume})")
device.setVolume(volume)
pauseExecution(250)
}
} else {
LOGDEBUG("playTrackAndResume[playTrack]: setLevel/setVolume is not supported by this device (${device.displayName})")
}
LOGDEBUG("playTrackAndResume[playTrack]: playTrack(${uri})")
device.playTrack(uri)
pauseExecution((duration.toInteger() * 1000))
if (supportsSetLevel || supportsSetVolume){
if ((curVol > -100) && supportsSetLevel){
LOGDEBUG("playTrackAndResume[playTrack]: setLevel(${curVol})")
device.setLevel(curVol)
pauseExecution(250)
}
if ((curVol > -100) && (supportsSetVolume && !supportsSetLevel)){
LOGDEBUG("playTrackAndResume[playTrack]: setVolume(${curVol})")
device.setVolume(volume)
pauseExecution(250)
}
} else {
LOGDEBUG("playTrackAndResume[playTrack]: setLevel/setVolume is not supported by this device (${device.displayName})")
}
if (!curTrack?.uri == null){
LOGDEBUG("playTrackAndResume[playTrack]: resumeTrack(${curTrack.uri})")
if (device.hasCommand("resumeTrack")){
device.resumeTrack(curTrack.uri)
} else {
LOGDEBUG("playTrackAndRestore[playTrack]: restoreTrack() is not supported by this device (${device.displayName})")
if (device.hasCommand("restoreTrack")){
device.restoreTrack(curTrack.uri)
}
}
}
}
}
}
def version(){
//Cobra update code, modified by Rayzurbock
resetBtnName()
//schedule("0 0 9 ? * FRI *", updateCheck) // Check for updates at 9am every Friday
updateCheck()
//checkButtons()
//pauseOrNot()
}
def checkButtons(){
//Cobra update code
LOGDEBUG("Running checkButtons")
appButtonHandler("updateBtn")
}
def appButtonHandler(btn){
//Cobra update code
state.btnCall = btn
if(state.btnCall == "updateBtn"){
log.info "Checking for updates now..."
updateCheck(true) //Manual re-check: 1-minute throttle so the button can't be spammed
//The hub's httpGet is async-only, so wait for the response
//to land before the page re-renders with the fresh result.
pause(3000)
state.btnName = state.versionBtn
runIn(2, resetBtnName)
}
if(state.btnCall == "updateBtn1"){
state.btnName = "Update Available - refresh page"
//httpGet("https://github.com/CobraVmax/Hubitat/tree/master/Apps' target='_blank")
}
}
def resetBtnName(){
//Cobra update code
// log.info "Resetting Update Button Name"
if(state.versionStatus != "Current"){
state.btnName = state.versionBtn
}
else{
state.btnName = "Check For Update"
}
}
def displayVersionStatus(){
//Cobra update code, modified by Rayzurbock
def formatSettingRootStart = state.formatSettingRootStart
def formatSettingRootEnd = state.formatSettingRootEnd
def formatSettingOptionalStart = state.formatSettingOptionalStart
def formatSettingOptionalEnd = state.formatSettingOptionalEnd
def formatUlStart = state.formatUlStart
def formatUlEnd = state.formatUlEnd
def formatLiStart = state.formatLiStart
def formatLiEnd = state.formatLiEnd
def formatIStart = state.formatIStart
def formatIEnd = state.formatIEnd
def formatStrongStart = state.formatStrongStart
def formatStrongEnd = state.formatStrongEnd
def formatHr = state.formatHr
def formatBr = state.formatBr
def formatCenterStart = state.formatCenterStart
def formatCenterEnd = state.formatCenterEnd
def versionInfo = ""
versionInfo = "${formatHr}${formatHr}${formatStrongStart}${formatCenterStart}Version Information${formatCenterEnd}${formatStrongEnd}${formatBr}"
if(state.versionStatus){
if (state.hubType == "Hubitat") {
versionInfo += "![]()
${state.ExternalName} - Version: ${state.version}
${state.Copyright ?: ""} "
}
}
//The Check For Update button is always shown: it is the manual
//re-check control, not just an update-available indicator. It used
//to live inside the update-available condition below, so it vanished
//whenever the app was up to date.
input "updateBtn", "button", title: "${state.btnName}"
if((state.versionStatus != "${formatStrongStart}** This app is no longer supported by $state.author **${formatStrongEnd}") & (state.versionStatus != "Current")){
// section(){
// log.info "app.label = $app.label"
// input "pause1", "bool", title: "Pause This App", required: true, submitOnChange: true, defaultValue: false
// }
// pauseOrNot()
//if(state.versionStatus != "Current"){
//updateURI and UpdateInfo are HTML from our own manifest (the download
//link and the release-notes list); escaping them shows the raw tags as
//text instead of rendering them, so they go in unescaped. versionStatus
//is plain text and stays escaped.
versionInfo += "${formatStrongStart}${escapeHtml(state.versionStatus ?: "")}${formatStrongEnd}${formatBr}${state.updateURI ?: ""}${formatBr}${formatStrongStart}${formatIStart}Release Notes:${formatIEnd}${formatStrongEnd}${state.UpdateInfo ?: ""}${formatBr}"
//}
//section(" ") {
// input "updateNotification", "bool", title: "Send a 'Pushover' message when an update is available", required: true, defaultValue: false, submitOnChange: true
// if(updateNotification == true){ input "speaker", "capability.speechSynthesis", title: "PushOver Device", required: true, multiple: true}
//}
} else {
versionInfo += "${formatIStart}App is up to date${formatIEnd}"
}
paragraph versionInfo
}
def updateCheck(manual = false){
setVersion()
def lastKnownVersionStatus = state.versionStatus
if (state?.versionStatus == null) { state.versionStatus = "Unknown" }
def paramsUD = [uri: "https://lowrance.cc/ST/manifests/RayzurCodeHE.json"]
if (updateCheckAllowed(manual) || lastKnownVersionStatus == "Unknown" || lastKnownVersionStatus == null){
state.Copyright = ""
state.updateURI = ""
state.UpdateInfo = ""
state.author = ""
state.versionBtn = ""
def newVerRaw = ""
def newVer = ""
def currentVer = ""
try {
//The hub only offers the async httpGet form (closure). There is
//no synchronous httpGet(Map) on Hubitat — calling it throws
//MissingMethodException, so the closure form is required.
httpGet(paramsUD) { respUD ->
//log.warn " Version Checking - Response Data: ${respUD.data}" // Troubleshooting Debug Code
def copyrightRead = (respUD.data.copyright)
//Manifest content is our own; keep any HTML entity intact for display and roll the copyright year forward
state.Copyright = copyrightRead?.replaceFirst(/\b(19|20)\d{2}\b/, new Date().format("yyyy"))
def updateUri = (respUD.data.versions.UpdateInfo.GithubFiles.(state.InternalName))
state.updateURI = updateUri
newVerRaw = (respUD.data.versions.Application.(state.InternalName))
if (newVerRaw == null) {
//Not listed in the update manifest (e.g. DEV builds); nothing to compare against
state.versionStatus = "Current"
log.info "updateCheck(): ${state.InternalName} not found in update manifest; skipping version comparison"
return
}
newVer = newVerRaw
def newVerCompare = newVer.toString().replaceAll(/[^0-9.]/, '')
currentVer = state.version.toString().replaceAll(/[^0-9.]/, '')
state.UpdateInfo = (respUD.data.versions.UpdateInfo.Application.(state.InternalName))
state.author = (respUD.data.author)
log.debug "currentVer=${currentVer}, newVer=${newVer}"
state.updateCheckFailures = 0
if(newVer == "NLS"){
state.versionStatus = "** This app is no longer supported by ${state.author} **"
log.warn "** This app is no longer supported by ${state.author} **"
}
else if(isNewerVersion(newVerCompare, currentVer)){
state.versionStatus = "New Version Available (Version: ${newVerRaw})"
log.warn "** There is a newer version of this app available (Version: ${newVerRaw}) **"
log.warn "** ${escapeHtml(state.UpdateInfo ?: "")} **"
def updateMsg = "There is a new version of '${state.ExternalName}' available (Version: ${newVerRaw})"
//pushOverNow(updateMsg)
}
else{
state.versionStatus = "Current"
log.info "You are using the current version of this app"
}
}
} catch (e) {
state.updateCheckFailures = (state?.updateCheckFailures ?: 0) + 1
log.error "Something went wrong: CHECK THE JSON FILE AND IT'S URI - $e"
}
//The button only re-checks; availability is shown in the version
//information below it, so the button never claims an update itself.
state.versionBtn = "Check For Update"
} else {
return
}
}
def isNewerVersion(newVer, currentVer){
//Guard: only accept dotted-numeric version strings
if (!(newVer.toString() ==~ /[0-9]+(\.[0-9]+)*/)) { return false }
if (!(currentVer.toString() ==~ /[0-9]+(\.[0-9]+)*/)) { return false }
//Beta versions have four segments (3.0.1.1); anything with fewer
//segments is an old-scheme version. An old-scheme version is never
//newer than a beta, and a beta is always newer than an old-scheme
//version: the old "-a12" suffix strips to "012", which would
//otherwise compare as newer than 3.0.1.1.
def newPeriods = newVer.toString().count('.')
def curPeriods = currentVer.toString().count('.')
if (newPeriods < 3 && curPeriods == 3) { return false }
if (newPeriods == 3 && curPeriods < 3) { return true }
def newParts = newVer.toString().tokenize('.')*.toInteger()
def curParts = currentVer.toString().tokenize('.')*.toInteger()
for (int i = 0; i < Math.max(newParts.size(), curParts.size()); i++) {
int n = i < newParts.size() ? newParts[i] : 0
int c = i < curParts.size() ? curParts[i] : 0
if (n > c) { return true }
if (n < c) { return false }
}
return false
}
def updateCheckAllowed(manual = false){
// rayzurbock code, used with Cobra update code
//Automatic checks (page load) run at most every 30 minutes; the
//button re-check runs at most every 1 minute so it can't be spammed.
//Each path keeps its own next-check time, so a page load doesn't block
//the button and a button press doesn't reset the 30-minute clock.
def intervalMin = manual ? 1 : 30
def nextCheckKey = manual ? "updateNextManualCheckDate" : "updateNextCheckDate"
def proceed = false
def updateCheckCurrentDate = new Date().getTime()
def updateCheckIntervalInMil = (intervalMin * 60000)
if ((state?.updateCheckFailures ?: 0) >= 3) { updateCheckIntervalInMil = updateCheckIntervalInMil * 4 }
if (state[nextCheckKey] == null) { state[nextCheckKey] = new Date().getTime() }
def timeDiff = state[nextCheckKey] - updateCheckCurrentDate
if (timeDiff <= 0 || timeDiff == null) { proceed = true }
if (state[nextCheckKey] == null) { proceed = true }
//if (!(proceed == true)) { log.debug "updateCheckAllowed() result: not allowed to proceed"}
LOGDEBUG("updateCheckAllowed() timeDiff=${timeDiff}, proceed=${proceed}")
if (proceed) {
//log.debug("updateCheckAllowed(): result: proceed")
state[nextCheckKey] = new Date().getTime() + updateCheckIntervalInMil
return true
} else {
//log.debug("updateCheckAllowed(): result: do not proceed (${timeDiff})")
return false
}
}
def setFormatting(){
state.formatSettingRootStart = ""
state.formatSettingRootEnd = ""
state.formatSettingOptionalStart = ""
state.formatSettingOptionalEnd = ""
state.formatUlStart = ""
state.formatUlEnd = "
"
state.formatLiStart = "
"
state.formatBr = "
"
state.formatCenterStart = "