From 60e90d25b32245b16bf19e17bce7c3203085bea8 Mon Sep 17 00:00:00 2001 From: imnotbob Date: Fri, 22 Jan 2021 19:39:58 -0500 Subject: [PATCH 01/15] fix for type check (#553) --- apps/echo-speaks.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/echo-speaks.groovy b/apps/echo-speaks.groovy index 5b44182b6..dbeca76f8 100644 --- a/apps/echo-speaks.groovy +++ b/apps/echo-speaks.groovy @@ -1051,7 +1051,7 @@ def speechPage() { paragraph pTS("This feature has been known to have issues and may not work because it's not supported by all Alexa devices. To test each device individually I suggest using the device interface and press Test Speech or Test Announcement") Map devs = getDeviceList(true, [tts]) input "test_speechDevices", sENUM, title: inTS("Select Devices to Test the Speech"), description: sTTS, options: (devs ? devs?.sort{it?.value} : []), multiple: true, required: false, submitOnChange: true - if((List)settings.test_speechDevices?.size() >= 3) { paragraph "Amazon will Rate Limit more than 3 device commands at a time. There will be a delay in the other devices but they should play the test after a few seconds", state: sNULL} + if(((List)settings.test_speechDevices)?.size() >= 3) { paragraph "Amazon will Rate Limit more than 3 device commands at a time. There will be a delay in the other devices but they should play the test after a few seconds", state: sNULL} input "test_speechVolume", "number", title: inTS("Speak at this volume"), description: "Enter number", range: "0..100", defaultValue: 30, required: false, submitOnChange: true input "test_speechRestVolume", "number", title: inTS("Restore to this volume after"), description: "Enter number", range: "0..100", defaultValue: null, required: false, submitOnChange: true input "test_speechMessage", "text", title: inTS("Message to Speak"), defaultValue: "This is a speech test for your Echo speaks device!!!", required: true, submitOnChange: true From 013c5d313f85381e91d8c22ad47bafb42d076898 Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Fri, 22 Jan 2021 21:24:33 -0500 Subject: [PATCH 02/15] Update CHANGELOG.md --- CHANGELOG.md | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 839cdf390..f44b1b863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,21 @@ -## v4.0.0.1 -### Echo Speaks App +# v4.0.1.0 +## Echo Speaks App +- [UPDATE] Updated minimum code versions for Echo Device and WebSocket. + +## Echo Speaks Device +- [FIX] VoiceActivity should now update the state correctly. + +## Echo Speaks WebSocket +- [FIX] Fixed Issue with WebSocket not working in v4.x. + +# v4.0.0.1 +## Echo Speaks App - [FIX] Heroku app.json missing when using Beta branch. -## v4.0.0.0 +# v4.0.0.0 +## Echo Speaks App - [UPDATE] Reduced overhead and resource usage - [UPDATE] Add new echo devices - -### Echo Speaks App - [UPDATE] Optimizations - [UPDATE] Reduced accesses to AWS Alexa APIs by caching results for re-use - [UPDATE] Removed a lot of state and atomicState accesses @@ -21,7 +30,7 @@ - [ADDED] Restored Zone and Action duplications. - [UPDATE] Added support for new Echo devices -### Echo Speaks Actions +## Echo Speaks Actions - [UPDATE] Integration with lock codes and security keypads, humidity sensors - [UPDATE] Ability to filter actions on specific security code usage - [UPDATE] Updates for operation with HSM @@ -35,16 +44,16 @@ - [UPDATE] Removed a lot of state and atomicState accesses - [UPDATE] Many JVM optimizations to reduce memory use in HE -### Echo Speaks Zones +## Echo Speaks Zones - [UPDATE] Optimizations - [UPDATE] Improved time handling and transitioning - [UPDATE] Improved UI -### Echo Speaks Device +## Echo Speaks Device - [UPDATE] Improved labeling for command inputs - [UPDATE] Less overhead - [NEW] Added new voiceCmdAsText() command to execute commands as if they are spoken to the device -### Echo Speaks WebSocket +## Echo Speaks WebSocket - [UPDATE] Less overhead -- [UPDATE] Improved operations with websockets \ No newline at end of file +- [UPDATE] Improved operations with websockets From a9bec8fa8e2414c39ab4f8711591a648a2f793fa Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Fri, 22 Jan 2021 23:57:00 -0500 Subject: [PATCH 03/15] Update CHANGELOG.md --- CHANGELOG.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6c391c2..56ca2170a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,32 @@ -# v4.0.1.0 -## Echo Speaks App +# **v4.0.1.0** +## **Echo Speaks App** - [UPDATE] Updated minimum code versions for Echo Device and WebSocket. - [UPDATE] Lot's of code cleanups - [FIX] Fixed guardData response error when guard is not enabled on Alexa account. -## Echo Speaks Actions +## **Echo Speaks Actions** - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with being able to unpause action. - [FIX] Fixed issue with voicecmd actiontypes. -## Echo Speaks Zones +## **Echo Speaks Zones** - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with relaying voicecmds. -## Echo Speaks Device +## **Echo Speaks Device** - [FIX] VoiceActivity should now update the state correctly. - [FIX] WebSocket fixes. - [UPDATE] Rename voicecmd sequence string to voicecmdtxt. -## Echo Speaks WebSocket +## **Echo Speaks WebSocket** - [FIX] Fixed Issue with WebSocket not working in v4.x. -# v4.0.0.1 -## Echo Speaks App +# **v4.0.0.1** +## **Echo Speaks App** - [FIX] Heroku app.json missing when using Beta branch. -# v4.0.0.0 -## Echo Speaks App +# **v4.0.0.0** +## **Echo Speaks App** - [UPDATE] Reduced overhead and resource usage - [UPDATE] Add new echo devices - [UPDATE] Optimizations @@ -43,7 +43,7 @@ - [ADDED] Restored Zone and Action duplications. - [UPDATE] Added support for new Echo devices -## Echo Speaks Actions +## **Echo Speaks Actions** - [UPDATE] Integration with lock codes and security keypads, humidity sensors - [UPDATE] Ability to filter actions on specific security code usage - [UPDATE] Updates for operation with HSM @@ -57,16 +57,16 @@ - [UPDATE] Removed a lot of state and atomicState accesses - [UPDATE] Many JVM optimizations to reduce memory use in HE -## Echo Speaks Zones +## **Echo Speaks Zones** - [UPDATE] Optimizations - [UPDATE] Improved time handling and transitioning - [UPDATE] Improved UI -## Echo Speaks Device +## **Echo Speaks Device** - [UPDATE] Improved labeling for command inputs - [UPDATE] Less overhead - [NEW] Added new voiceCmdAsText() command to execute commands as if they are spoken to the device -## Echo Speaks WebSocket +## **Echo Speaks WebSocket** - [UPDATE] Less overhead - [UPDATE] Improved operations with websockets From a0ea32916a2c15421f0f676c5d8780f95e3d9f95 Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Sat, 23 Jan 2021 00:02:46 -0500 Subject: [PATCH 04/15] Update CHANGELOG.md --- CHANGELOG.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ca2170a..c11859c01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,32 @@ -# **v4.0.1.0** -## **Echo Speaks App** +# _**v4.0.1.0**_ +## _**Echo Speaks App**_ - [UPDATE] Updated minimum code versions for Echo Device and WebSocket. - [UPDATE] Lot's of code cleanups - [FIX] Fixed guardData response error when guard is not enabled on Alexa account. -## **Echo Speaks Actions** +## _**Echo Speaks Actions**_ - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with being able to unpause action. - [FIX] Fixed issue with voicecmd actiontypes. -## **Echo Speaks Zones** +## _**Echo Speaks Zones**_ - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with relaying voicecmds. -## **Echo Speaks Device** +## _**Echo Speaks Device**_ - [FIX] VoiceActivity should now update the state correctly. - [FIX] WebSocket fixes. - [UPDATE] Rename voicecmd sequence string to voicecmdtxt. -## **Echo Speaks WebSocket** +## _**Echo Speaks WebSocket**_ - [FIX] Fixed Issue with WebSocket not working in v4.x. -# **v4.0.0.1** -## **Echo Speaks App** +# _**v4.0.0.1**_ +## _**Echo Speaks App**_ - [FIX] Heroku app.json missing when using Beta branch. -# **v4.0.0.0** -## **Echo Speaks App** +# _**v4.0.0.0**_ +## _**Echo Speaks App**_ - [UPDATE] Reduced overhead and resource usage - [UPDATE] Add new echo devices - [UPDATE] Optimizations @@ -43,7 +43,7 @@ - [ADDED] Restored Zone and Action duplications. - [UPDATE] Added support for new Echo devices -## **Echo Speaks Actions** +## _**Echo Speaks Actions**_ - [UPDATE] Integration with lock codes and security keypads, humidity sensors - [UPDATE] Ability to filter actions on specific security code usage - [UPDATE] Updates for operation with HSM @@ -57,16 +57,16 @@ - [UPDATE] Removed a lot of state and atomicState accesses - [UPDATE] Many JVM optimizations to reduce memory use in HE -## **Echo Speaks Zones** +## _**Echo Speaks Zones**_ - [UPDATE] Optimizations - [UPDATE] Improved time handling and transitioning - [UPDATE] Improved UI -## **Echo Speaks Device** +## _**Echo Speaks Device**_ - [UPDATE] Improved labeling for command inputs - [UPDATE] Less overhead - [NEW] Added new voiceCmdAsText() command to execute commands as if they are spoken to the device -## **Echo Speaks WebSocket** +## _**Echo Speaks WebSocket**_ - [UPDATE] Less overhead - [UPDATE] Improved operations with websockets From bf4182213daf241d1eac3d247a80079859d35ae0 Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Sat, 23 Jan 2021 00:16:27 -0500 Subject: [PATCH 05/15] Update CHANGELOG.md --- CHANGELOG.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c11859c01..74d67ac13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,32 +1,32 @@ -# _**v4.0.1.0**_ -## _**Echo Speaks App**_ +## _**(January 22nd, 2021)**_ +### _**Echo Speaks App (v4.0.1.0)**_ - [UPDATE] Updated minimum code versions for Echo Device and WebSocket. - [UPDATE] Lot's of code cleanups - [FIX] Fixed guardData response error when guard is not enabled on Alexa account. -## _**Echo Speaks Actions**_ +### _**Echo Speaks Actions (v4.0.1.0)**_ - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with being able to unpause action. - [FIX] Fixed issue with voicecmd actiontypes. -## _**Echo Speaks Zones**_ +### _**Echo Speaks Zones (v4.0.1.0)**_ - [UPDATE] Lot's of code cleanups - [FIX] Fixed issue with relaying voicecmds. -## _**Echo Speaks Device**_ +### _**Echo Speaks Device (v4.0.1.0)**_ - [FIX] VoiceActivity should now update the state correctly. - [FIX] WebSocket fixes. - [UPDATE] Rename voicecmd sequence string to voicecmdtxt. -## _**Echo Speaks WebSocket**_ +### _**Echo Speaks WebSocket (v4.0.1.0)**_ - [FIX] Fixed Issue with WebSocket not working in v4.x. -# _**v4.0.0.1**_ -## _**Echo Speaks App**_ +## _**(January 21st, 2021)**_ +### _**Echo Speaks App (v4.0.0.1)**_ - [FIX] Heroku app.json missing when using Beta branch. -# _**v4.0.0.0**_ -## _**Echo Speaks App**_ +## _**(January 21st, 2021)**_ +### _**Echo Speaks App (v4.0.0.0)**_ - [UPDATE] Reduced overhead and resource usage - [UPDATE] Add new echo devices - [UPDATE] Optimizations @@ -43,7 +43,7 @@ - [ADDED] Restored Zone and Action duplications. - [UPDATE] Added support for new Echo devices -## _**Echo Speaks Actions**_ +### _**Echo Speaks Actions (v4.0.0.0)**_ - [UPDATE] Integration with lock codes and security keypads, humidity sensors - [UPDATE] Ability to filter actions on specific security code usage - [UPDATE] Updates for operation with HSM @@ -57,16 +57,16 @@ - [UPDATE] Removed a lot of state and atomicState accesses - [UPDATE] Many JVM optimizations to reduce memory use in HE -## _**Echo Speaks Zones**_ +### _**Echo Speaks Zones (v4.0.0.0)**_ - [UPDATE] Optimizations - [UPDATE] Improved time handling and transitioning - [UPDATE] Improved UI -## _**Echo Speaks Device**_ +### _**Echo Speaks Device (v4.0.0.0)**_ - [UPDATE] Improved labeling for command inputs - [UPDATE] Less overhead - [NEW] Added new voiceCmdAsText() command to execute commands as if they are spoken to the device -## _**Echo Speaks WebSocket**_ +### _**Echo Speaks WebSocket (v4.0.0.0)**_ - [UPDATE] Less overhead -- [UPDATE] Improved operations with websockets +- [UPDATE] Improved operations with websockets. From 63c9cb13f229d7f578ae0b8595fa3349ae418493 Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Sat, 30 Jan 2021 11:48:14 -0500 Subject: [PATCH 06/15] Merge latest fixes (#584) * fix for type warning (#578) * another fix for state upgrade (#579) Co-authored-by: imnotbob --- apps/echo-speaks.groovy | 4 ++-- drivers/echo-speaks-ws.groovy | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/echo-speaks.groovy b/apps/echo-speaks.groovy index 689e0a43c..4b1b97b9c 100644 --- a/apps/echo-speaks.groovy +++ b/apps/echo-speaks.groovy @@ -3433,8 +3433,8 @@ void appUpdateNotify() { Boolean on=false if(res) { on = ((Boolean)settings.sendAppUpdateMsg) - updW = settings.updNotifyWaitVal - if(updW == null) { updW = 43200; settingUpdate("updNotifyWaitVal", 43200) } + updW = settings.updNotifyWaitVal?.toInteger() + if(updW == null) { updW = 43200; settingUpdate("updNotifyWaitVal", "43200", sENUM) } secs=getLastTsValSecs("lastUpdMsgDt") if(secs > updW && on) { String str = sBLANK diff --git a/drivers/echo-speaks-ws.groovy b/drivers/echo-speaks-ws.groovy index b6dba4f3f..df63990b2 100644 --- a/drivers/echo-speaks-ws.groovy +++ b/drivers/echo-speaks-ws.groovy @@ -95,9 +95,8 @@ def initialize() { state.remove('warnHistory'); state.remove('errorHistory') if(settings.autoConnectWs != false) { + if(!state.cookie || !(state.cookie instanceof Map)) state.cookie = (Map)parent?.getCookieMap() String cookS = getCookieVal() //state.cookie = parent?.getCookieVal() - if(!cookS || !state.cookie || state.cookie instanceof String) state.cookie = (Map)parent?.getCookieMap() - cookS = getCookieVal() if(cookS) { if(!state.amazonDomain) { state.amazonDomain = parent?.getAmazonDomain() From 31ea7436382228f926ab25962e9f0b5d4f82eb94 Mon Sep 17 00:00:00 2001 From: Anthony Santilli Date: Thu, 25 Feb 2021 10:52:30 -0500 Subject: [PATCH 07/15] fixed changelog typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3193002..9dc51f225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## _**(February 25th, 2021)**_ ### _**Echo Speaks App (v4.0.7.0)**_ - [NEW] Moved to a centralized queue for all speech commands to eliminate Amazon rate limiting. -- - [FIX] Optimizations to allow for grouping of commands to multiple zones +- [FIX] Optimizations to allow for grouping of commands to multiple zones - [NEW] Control Alexa Guard with the state of a single switch (On=Armed | Off=Disarmed). - [FIX] Lot's of UI design updates From 265677ce4074ea3800cffe32a223ab093830f33d Mon Sep 17 00:00:00 2001 From: imnotbob Date: Fri, 12 Mar 2021 08:29:27 -0500 Subject: [PATCH 08/15] fix caller (#724) --- apps/echo-speaks.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/echo-speaks.groovy b/apps/echo-speaks.groovy index caec41888..f2fa4cf32 100644 --- a/apps/echo-speaks.groovy +++ b/apps/echo-speaks.groovy @@ -3661,7 +3661,7 @@ void sendDevObjCmd(List odevObj, String myCmd, String title, String newmsg, queueMultiSequenceCommand(mainSeq, myMsg) devObj.each { dev-> def child = getChildDevice((String)dev.dni) - child.finishAnnounce(origMsg) + child.finishAnnounce(origMsg, volume, restoreVolume) } } From ec9e4264284946c88c887f004c3fc57a3a3bfc02 Mon Sep 17 00:00:00 2001 From: tonesto7 Date: Mon, 18 Apr 2022 16:44:24 -0400 Subject: [PATCH 09/15] Add files via upload --- apps/echo-speaks-actions.groovy | 11770 +++++++++++------------ apps/echo-speaks-zones.groovy | 4726 +++++----- apps/echo-speaks.groovy | 15218 +++++++++++++++--------------- 3 files changed, 15857 insertions(+), 15857 deletions(-) diff --git a/apps/echo-speaks-actions.groovy b/apps/echo-speaks-actions.groovy index 0ba87edea..f9f03ab4f 100644 --- a/apps/echo-speaks-actions.groovy +++ b/apps/echo-speaks-actions.groovy @@ -1,5885 +1,5885 @@ -/** - * Echo Speaks Actions (Hubitat) - * - * Copyright 2018, 2019, 2020, 2021, 2022 Anthony Santilli - * Code Contributions by @nh.schottfam - * - * 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. - * - * --------------------------------------------- - TODO: Custom Reports for multiple builtin in routine items. Reports for home status like temp, contact, alarm status. - */ -//file:noinspection GroovyUnusedAssignment -//file:noinspection GroovySillyAssignment -//file:noinspection GroovyDoubleNegation -//file:noinspection GrMethodMayBeStatic -//file:noinspection unused - - -import groovy.json.JsonOutput -import groovy.transform.Field - -import java.text.SimpleDateFormat -import java.util.concurrent.Semaphore - -//************************************************ -//* STATIC VARIABLES * -//************************************************ -@Field static final String appVersionFLD = '4.2.0.2' -@Field static final String appModifiedFLD = '2022-04-18' -@Field static final Boolean devModeFLD = false -@Field static final String sNULL = (String)null -@Field static final String sBLANK = '' -@Field static final String sSPACE = ' ' -@Field static final String s2SPACE = ' ' -@Field static final String s3SPACE = ' ' -@Field static final String s4SPACE = ' ' -@Field static final String s5SPACE = ' ' -@Field static final String s6SPACE = ' ' -@Field static final String sBULLET = '\u2022' -@Field static final String sBULLETINV = '\u25E6' -//@Field static final String sSQUARE = '\u29C8' -@Field static final String sPLUS = '\u002B' -//@Field static final String sRIGHTARR = '\u02C3' -@Field static final String okSymFLD = "\u2713" -@Field static final String notOkSymFLD = "\u2715" -@Field static final String sPAUSESymFLD = "\u275A\u275A" -@Field static final String sLINEBR = '
' -@Field static final String sFALSE = 'false' -@Field static final String sTRUE = 'true' -@Field static final String sBOOL = 'bool' -@Field static final String sENUM = 'enum' -@Field static final String sNUMBER = 'number' -@Field static final String sRESET = 'reset' -@Field static final String sTEXT = 'text' -@Field static final String sTIME = 'time' -@Field static final String sMODE = 'mode' -@Field static final String sCOMPLT = 'complete' -@Field static final String sMEDIUM = 'medium' -@Field static final String sSMALL = 'small' -@Field static final String sCLR4D9 = '#2784D9' -@Field static final String sCLRRED = 'red' -@Field static final String sCLRRED2 = '#cc2d3b' -@Field static final String sCLRGRY = 'gray' -//@Field static final String sCLRGRN = 'green' -@Field static final String sCLRGRN2 = '#43d843' -@Field static final String sCLRORG = 'orange' -@Field static final String sTTM = 'Tap to modify...' -@Field static final String sTTC = 'Tap to configure...' -@Field static final String sTTCR = 'Tap to configure (Required)' -@Field static final String sTTP = 'Tap to proceed...' -@Field static final String sEXTNRL = 'external' -@Field static final String sDEBUG = 'debug' -@Field static final String sCHKBOX = 'checkbox' -@Field static final String sCOMMAND = 'command' -@Field static final String sANY = 'any' -@Field static final String sARE = 'are' -@Field static final String sBETWEEN = 'between' -@Field static final String sNBETWEEN = 'not_between' -@Field static final String sBELOW = 'below' -@Field static final String sABOVE = 'above' -@Field static final String sEQUALS = 'equals' -@Field static final String sANN = 'announcement' -@Field static final String sANNI = 'announcement_ignDND' -@Field static final String sANNIT = 'announcement_ignDND_tiered' -@Field static final String sANNT = 'announcement_tiered' -@Field static final String sSPEAK = 'speak' -@Field static final String sSPEAKP = 'speak_parallel' -@Field static final String sSPEAKI = 'speak_ignDND' -@Field static final String sSPEAKT = 'speak_tiered' -@Field static final String sSPEAKPT = 'speak_parallel_tiered' -@Field static final String sSPEAKIP = 'speak_ignDND_parallel' -@Field static final String sSPEAKIT = 'speak_ignDND_tiered' -@Field static final String sSPEAKIPT = 'speak_ignDND_parallel_tiered' -@Field static final String sWEATH = 'weather' -@Field static final String sCSUNRISE = 'Sunrise' -@Field static final String sCSUNSET = 'Sunset' -@Field static final String sSTTIME = 'start_time' -@Field static final String sSPDKNB = 'speed_knob' -@Field static final String sSWITCH = 'switch' -@Field static final String sTEMP = 'temperature' -@Field static final String sTHERMTEMP = 'thermostatTemperature' -@Field static final String sCOOLSP = 'coolingSetpoint' -@Field static final String sHEATSP = 'heatingSetpoint' -@Field static final String sMOTION = 'motion' -@Field static final String sLEVEL = 'level' -@Field static final String sBATT = 'battery' -@Field static final String sTHERM = 'thermostat' -@Field static final String sTHERMMODE = 'thermostatMode' -@Field static final String sTHERMOS = 'thermostatOperatingState' -@Field static final String sTHERMFM = 'thermostatFanMode' -@Field static final String sSCHED = 'scheduled' -@Field static final String sHUMID = 'humidity' -@Field static final String sKEYPAD = 'keypad' -@Field static final String sLOCK = 'lock' -@Field static final String sCONTACT = 'contact' -@Field static final String sWATER = 'water' -@Field static final String sSMOKE = 'smoke' -@Field static final String sPOWER = 'power' -@Field static final String sVALVE = 'valve' -@Field static final String sCHGTO = 'changes to' -@Field static final String sTTS = 'TTS' -@Field static final String sQUES = 'question' -@Field static final String sDELAYT = 'delay_time' -@Field static final String sEQ = 'equal' -@Field static final String sPUSHED = 'pushed' -@Field static final String sRELEASED = 'released' -@Field static final String sHELD = 'held' -@Field static final String sDBLTAP = 'doubleTapped' -@Field static final String sALRMSYSST = 'alarmSystemStatus' -@Field static final String sPISTNEXEC = 'pistonExecuted' -@Field static final String sLRM = "light_restore_map" -@Field static final String sTRIG = "trig_" -@Field static final String sACT = 'act_' -@Field static final String sACT_START = 'act_tier_start_' -@Field static final String sACT_STOP = 'act_tier_stop_' -@Field static final String sACT_ITEM = 'act_tier_item_' -@Field static final String sRECURRING = 'Recurring' -@Field static final String sONETIME = 'One-Time' -@Field static final List lONOFF = ['on', 'off'] -@Field static final List lANY = ['any'] -@Field static final List lOPNCLS = ['open', 'closed'] -@Field static final List lACTINACT = ['active', 'inactive'] -@Field static final List lSUNRISESET = ['sunrise', 'sunset'] -@Field static final List lWETDRY = ['wet', 'dry'] -@Field static final List lLOCKUNL = ['locked', 'unlocked'] -@Field static final List lDETECTCLR = ['detected', 'clear', 'tested'] -@Field static final List lPRES = ['present', 'not present'] -@Field static final List lSEC = ['disarmed', 'armed home', 'armed away', 'unknown'] - -//************************************************ -//* IN-MEMORY ONLY VARIABLES * -//* (Cleared only on HUB REBOOT or CODE UPDATES) * -//************************************************ -@Field volatile static Map historyMapFLD = [:] -// @Field volatile static String gitBranchFLD = null - -static String appVersion() { return appVersionFLD } -static String appVersionDt() { return appModifiedFLD } - -definition( - name : 'Echo Speaks - Actions', - namespace : 'tonesto7', - author : 'Anthony Santilli', - description : 'DO NOT INSTALL FROM MARKETPLACE\n\nAllows you to create echo device actions based on device/location events in your home.', - category : 'My Apps', - parent : 'tonesto7:Echo Speaks', - iconUrl : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', - iconX2Url : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', - iconX3Url : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', - importUrl : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/apps/echo-speaks-actions.groovy', - documentationLink : documentationUrl(), - videoLink : videoUrl() -) - -preferences { - page(name: 'startPage') - page(name: 'uhOhPage') - page(name: 'codeUpdatePage') - page(name: 'mainPage') - page(name: 'prefsPage') - page(name: 'triggersPage') - page(name: 'conditionsPage') - page(name: 'condTimePage') - page(name: 'actionsPage') - page(name: 'actionTiersPage') - page(name: 'actionTiersConfigPage') - page(name: 'actTrigTasksPage') - page(name: 'actTierStartTasksPage') - page(name: 'actTierStopTasksPage') - page(name: 'actNotifPage') - page(name: 'actNotifTimePage') - page(name: 'actionHistoryPage') - page(name: 'searchTuneInResultsPage') - page(name: 'uninstallPage') - page(name: 'namePage') -} - -def startPage() { - if(parent != null) { - if(!(Boolean)state.isInstalled && !(Boolean)parent?.childInstallOk()) { return uhOhPage() } - else { - updDeviceInputs() - return (minVersionFailed()) ? codeUpdatePage() : mainPage() } - } else { return uhOhPage() } -} - -def codeUpdatePage() { - return dynamicPage(name: "codeUpdatePage", title: "Update is Required", install: false, uninstall: false) { - section() { paragraph spanSmBld("Looks like your Action App needs an update

Please make sure all app and device code is updated to the most current version

Once updated your actions will resume normal operation.", sCLRRED) } - } -} - -def uhOhPage() { - return dynamicPage(name: "uhOhPage", title: "This install Method is Not Allowed", install: false, uninstall: true) { - section() { - paragraph spanBld("HOUSTON WE HAVE A PROBLEM!

Echo Speaks - Actions can't be directly installed from the Marketplace.

Please use the Echo Speaks SmartApp to configure them.", sCLRRED) - } - } -} - -def appInfoSect() { - String instDt = state.dateInstalled ? fmtTime((String)state.dateInstalled, "MMM dd '@' h:mm a", true) : sNULL - String str = spanBld((String)app.name, "black", "es_groups") - str += ( app.label && (String)app.label != (String)app.name) ? spanBldBr(" (${app.label.replace(" (A)", sBLANK)})") : sLINEBR - str += spanSmBld("Version: ") + spanSmBr(appVersionFLD) - str += instDt ? spanSmBld("Installed: ") + spanSmBr(instDt) : sBLANK - section() { paragraph divSm(str, sCLRGRY) } -} - -private Map buildTriggerEnum() { - Map> buildItems = [:] - buildItems["Date/Time"] = [(sSCHED):"Scheduled Time"].sort{ it?.key } - buildItems["Location"] = [(sMODE):"Modes", (sPISTNEXEC):"Pistons"].sort{ it?.key } - if(!settings.enableWebCoRE) { buildItems.Location.remove(sPISTNEXEC) } - - buildItems["Sensor Devices"] = [(sCONTACT):"Contacts | Doors | Windows", (sBATT):"Battery Level", (sMOTION):"Motion", "illuminance": "Illuminance/Lux", "presence":"Presence", (sTEMP):"Temperature", (sHUMID):"Humidity", (sWATER):"Water", (sPOWER):"Power", "acceleration":"Accelerometers"].sort{ it?.value } - buildItems["Actionable Devices"] = [(sLOCK):"Locks", "securityKeypad":"Keypads", (sSWITCH):"Switches/Outlets", (sLEVEL):"Dimmers/Level", "door":"Garage Door Openers", (sVALVE):"Valves", "windowShade":"Window Shades"].sort{ it?.value } - buildItems["Thermostat Devices"] = [(sCOOLSP):"Thermostat Cooling Setpoint", (sHEATSP):"Thermostat Heating Setpoint", (sTHERMTEMP):"Thermostat Ambient Temp", (sTHERMOS):"Thermostat Operating State", (sTHERMMODE):"Thermostat Mode", (sTHERMFM):"Thermostat Fan Mode"].sort{ it?.value } - buildItems["Button Devices"] = [(sPUSHED):"Button (Pushable)", (sRELEASED):"Button (Releasable)", (sHELD):"Button (Holdable)", (sDBLTAP):"Button (Double Tapable)"].sort{ it?.value } -// TODO siren (capability.alarm, attr alarm - ENUM ["strobe", "off", "both", "siren"] - buildItems["Safety & Security"] = [(sALRMSYSST): getAlarmSystemName(), (sSMOKE):"Fire/Smoke", "carbonMonoxide":"Carbon Monoxide", "guard":"Alexa Guard"].sort{ it?.value } - if(!parent?.guardAutoConfigured()) { buildItems["Safety & Security"].remove("guard") } - return buildItems.collectEntries { it?.value }?.sort { it?.value } -} - -private static Map buildActTypeEnum() { - Map> buildItems = [:] - buildItems["Speech"] = [(sSPEAK):"Speak", (sANN):"Announcement", (sSPEAKP):"Speak (Parallel)", (sSPEAKI): "Speak Ignore Dnd", (sSPEAKIP): "Speak Ignore Dnd (Parallel)", - (sSPEAKT):"Speak (Tiered)", (sSPEAKPT):"Speak Parallel (Tiered)", (sSPEAKIT): "Speak Ignore Dnd (Tiered)", (sSPEAKIPT): "Speak Parallel Ignore Dnd (Tiered)", - (sANNI): "Announcement Ignore DND", (sANNT):"Announcement (Tiered)", (sANNIT): "Announcement Ignore DND (Tiered)" ].sort{ it?.key } - buildItems["Built-in Sounds"] = ["sounds":"Play a Sound"].sort{ it?.key } - buildItems["Built-in Responses"] = [(sWEATH):"Weather Report", "builtin":"Birthday, Compliments, Facts, Jokes, News, Stories, Traffic, and more...", "calendar":"Read Calendar Events"].sort{ it?.key } - buildItems["Media/Playback"] = ["music":"Play Music/Playlists", "playback":"Playback/Volume Control"].sort{ it?.key } - buildItems["Alarms/Reminders"] = ["alarm":"Create Alarm", "reminder":"Create Reminder"].sort{ it?.key } - buildItems["Devices Settings"] = ["wakeword":"Change Wake Word", "dnd":"Set Do Not Disturb", "bluetooth":"Bluetooth Control"].sort{ it?.key } - buildItems["Custom"] = ["voicecmd":"Execute a voice command","sequence":"Execute Sequence", "alexaroutine": "Execute Alexa Routine(s)"].sort{ it?.key } - return buildItems.collectEntries { it?.value }?.sort { it?.value } -} - -def mainPage() { - Boolean newInstall = !(Boolean)state.isInstalled - return dynamicPage(name: "mainPage", nextPage: (!newInstall ? sBLANK : "namePage"), uninstall: newInstall, install: !newInstall) { - Boolean dup = ((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup) - if(dup) { - state.dupOpenedByUser = true - section() { paragraph spanBld("This Action was created from an existing action.

Please review the settings and save to activate...
${state.badMode ?: sBLANK}", sCLRORG, "pause_orange") } - } - - if(settings.enableWebCoRE) { - if(!webCoREFLD) webCoRE_init() - webCoRE_poll() - } - appInfoSect() - Boolean paused = isPaused() - Boolean trigConf - //Boolean condConf - Boolean actConf - Boolean allOk - - if(paused) { - section() { - paragraph spanSmBld("This Action is currently in a paused state...
To edit the please un-pause", sCLRRED, "pause_orange") - } - } else { - section() { paragraph divSm(getOverallDesc(), sCLRGRY) } - - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } - trigConf = triggersConfigured() - //condConf = conditionsConfigured() - actConf = executionConfigured() - allOk = (Boolean) ((String)settings.actionType && trigConf && actConf) - section(sectHead("Configuration: Part 1")) { - input "actionType", sENUM, title: inTS1("Action Type", "list"), description: sBLANK, options: buildActTypeEnum(), multiple: false, required: true, submitOnChange: true - } - if (newInstall) { - section(sectHead("Configuration: Part 2")) { - paragraph spanBld("Further Options will be configured once you save this automation.
Please save and return to complete", sNULL, getAppImg("info")) - } - } else { - section (sectHead("Configuration: Part 2")) { - if((String)settings.actionType) { - href "triggersPage", title: inTS1("Action Triggers", "trigger"), description: spanSm(getTriggersDesc(), sCLR4D9) - } else { paragraph spanBld("These options will be shown once the action type is configured.", sNULL, getAppImg("info")) } - } - section(sectHead("Configuration: Part 3")) { - if((String)settings.actionType && trigConf) { - href "conditionsPage", title: inTS1("Conditions/Restrictions", "conditions") + optPrefix(), description: divSm(getConditionsDesc(true), sCLR4D9) - } else { paragraph spanBld("These options will be shown once the triggers are configured.", sNULL, getAppImg("info")) } - } - section(sectHead("Configuration: Part 4")) { - if((String)settings.actionType && trigConf) { - href "actionsPage", title: inTS1("Execution Config", "es_actions"), description: spanSm(getActionDesc(true), sCLR4D9) - } else { paragraph spanBld("These options will be shown once the triggers are configured.", sNULL, getAppImg("info")) } - } - if(allOk) { - section(sectHead("Notifications:")) { - String t0 = getAppNotifDesc() - href "actNotifPage", title: inTS1("Send Notifications", "notification2"), description: t0 ? divSm(t0 + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC) - } - - getTierStatusSection() - - section(sectHead("Action History")) { - href "actionHistoryPage", title: inTS1("View Action History", "tasks"), description: sBLANK - } - } - } - } - - section(sectHead("Preferences")) { - href "prefsPage", title: inTS1("Debug/Preferences", "settings"), description: sBLANK - if(!newInstall) { - input "actionPause", sBOOL, title: inTS1("Pause Action?", "pause_orange"), defaultValue: false, submitOnChange: true - if((Boolean)settings.actionPause) { unsubscribe() } - else { - input "actTestRun", sBOOL, title: inTS1("Test this action?", "testing"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.actTestRun) { executeActTest() } - } - } - } - - if(!newInstall) { - section(sectHead("Name this Action:")) { - input "appLbl", sTEXT, title: inTS1("Action Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true - } - section(sectHead("Remove Action:")) { - href "uninstallPage", title: inTS1("Remove this Action", "uninstall"), description: inputFooter("Tap to Remove...", sCLRGRY, true) - } - if(allOk) { - section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { - String issueUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28ACTIONS+BUG%29+&projects=echo-speaks%2F6" - String featUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BActions+Feature+Request%5D&projects=echo-speaks%2F6" - String ttob = inactFoot("Tap to open browser") - href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "www"), description: ttob - href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "www"), description: ttob - } - appFooter() - } - } - } -} - -def prefsPage() { - return dynamicPage(name: "prefsPage", install: false, uninstall: false) { - section(sectHead("Logging:")) { - input "logInfo", sBOOL, title: inTS1("Show Info Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logWarn", sBOOL, title: inTS1("Show Warning Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logError", sBOOL, title: inTS1("Show Error Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logDebug", sBOOL, title: inTS1("Show Debug Logs?", sDEBUG), required: false, defaultValue: false, submitOnChange: true - input "logTrace", sBOOL, title: inTS1("Show Detailed Logs?", sDEBUG), required: false, defaultValue: false, submitOnChange: true - } - if((Boolean)state.isInstalled) { - if(advLogsActive()) { logsEnabled() } - section(sectHead("Other:")) { - input "clrEvtHistory", sBOOL, title: inTS1("Clear Device Event History?", "reset"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.clrEvtHistory) { clearEvtHistory() } - } - } - } -} - -def namePage() { - return dynamicPage(name: "namePage", install: true, uninstall: false) { - section(sectHead("Name this Automation:")) { - input "appLbl", sTEXT, title: inTS1("Label this Action", "name_tag"), description: sBLANK, required:true, submitOnChange: true - } - } -} - -def actionHistoryPage() { - return dynamicPage(name: "actionHistoryPage", title: "Action History", install: false, uninstall: false) { - section() { - getActionHistory() - } - if( ((List)getMemStoreItem("actionHistory")).size() ) { - section(sBLANK) { - input "clearActionHistory", sBOOL, title: inTS1("Clear Action History?", sRESET), description: spanSm("Clears Stored Action History.", sCLRGRY), defaultValue: false, submitOnChange: true - if((Boolean)settings.clearActionHistory) { - settingUpdate("clearActionHistory", sFALSE, sBOOL) - clearActHistory() - } - } - } - } -} -/* -// TODO: Add flag to check for the old schedule settings and pause the action, and notify the user. -private scheduleConvert() { - if(settings.trig_scheduled_time || settings.trig_scheduled_sunState && !settings.trig_scheduled_type) { - if(settings.trig_scheduled_sunState) { settingUpdate("trig_scheduled_type", "${settings.trig_scheduled_sunState}", sENUM); settingRemove("trig_scheduled_sunState") } - else if(settings.trig_scheduled_time && settings.trig_scheduled_recurrence) { - if(settings.trig_scheduled_recurrence == "Once") { settingUpdate("trig_scheduled_type", sONETIME, sENUM) } - if(settings.trig_scheduled_recurrence in ["Daily", "Weekly", "Monthly"]) { settingUpdate("trig_scheduled_type", sRECURRING, sENUM) } - } - } -} */ - -@SuppressWarnings('GroovyFallthrough') -def triggersPage() { - return dynamicPage(name: "triggersPage", nextPage: "mainPage", title: "Action Triggers", uninstall: false, install: false) { -// Boolean isTierAct = isTierAction() - String a = getTriggersDesc(false, false) - if(a) { - section() { - paragraph spanSm(a, sCLR4D9) - } - } - section (sectHead("Enable webCoRE Integration:")) { - input "enableWebCoRE", sBOOL, title: inTS1("Enable webCoRE Integration", webCore_icon()), required: false, defaultValue: false, submitOnChange: true - } - if(settings.enableWebCoRE) { - if(!webCoREFLD) webCoRE_init() - webCoRE_poll() - } - Boolean showSpeakEvtVars = false - section (sectHead("Select Capabilities")) { - input "triggerEvents", sENUM, title: inTS1("Select Trigger Event(s)", "trigger"), options: buildTriggerEnum(), multiple: true, required: true, submitOnChange: true - } - Integer trigEvtCnt = ((List)settings.triggerEvents)?.size() - if (trigEvtCnt) { - Boolean fnd = false - //if( sSCHED in (List)settings.triggerEvents || - // sWEATH in (List)settings.triggerEvents) { fnd = true } - /*((List)settings.triggerEvents)?.each { String tr -> - if(tr in [sSCHED, sWEATH]) { fnd = true } - } */ - //if(!((List)settings.triggerEvents in [sSCHED, sWEATH])) { showSpeakEvtVars = true } - if(!fnd) { showSpeakEvtVars = true } - - if (valTrigEvt(sSCHED)) { - section(sectHead("Time Based Events"), hideable: true) { - List schedTypes = [sONETIME, sRECURRING, sCSUNRISE, sCSUNSET] - input "trig_scheduled_type", sENUM, title: inTS1("Schedule Type?", sCHKBOX), options: schedTypes, multiple: false, required: true, submitOnChange: true - String schedType = (String)settings.trig_scheduled_type - if(schedType) { - switch(schedType) { - case sONETIME: - case sRECURRING: - input "trig_scheduled_time", sTIME, title: inTS1("Trigger Time?", "clock"), required: false, submitOnChange: true - if(settings.trig_scheduled_time && schedType == sRECURRING) { - List recurOpts = ["Daily", "Weekly", "Monthly"] // "Yearly" - input "trig_scheduled_recurrence", sENUM, title: inTS1("Recurrence?", "day_calendar"), description: sBLANK, multiple: false, required: true, submitOnChange: true, options: recurOpts, defaultValue: "Once" - // TODO: Build out the scheduling some more with quick items like below - /* - At 6 pm on the last day of every month: (0 0 18 L * ?) - At 6 pm on the 3rd to last day of every month: (0 0 18 L-3 * ?) - At 10:30 am on the last Thursday of every month: (0 30 10 ? * 5L) - At 6 pm on the last Friday of every month during the years 2015, 2016 and 2017: (0 0 18 ? * 6L 2015-2017) - At 10 am on the third Monday of every month: (0 0 10 ? * 2#3) - At 12 am midnight on every day for five days starting on the 10th day of the month: (0 0 0 10/5 * ?) - */ - String schedRecur = (String)settings.trig_scheduled_recurrence - if(schedRecur) { - switch(schedRecur) { - case "Daily": - input "trig_scheduled_weekdays", sENUM, title: inTS1("Only of these Days of the Week?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: daysOfWeekMap() - break - - case "Weekly": - input "trig_scheduled_weekdays", sENUM, title: inTS1("Days of the Week?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: true, submitOnChange: true, options: daysOfWeekMap() - input "trig_scheduled_weeks", sENUM, title: inTS1("Only these Weeks on the Month?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: weeksOfMonthMap() - input "trig_scheduled_months", sENUM, title: inTS1("Only on these Months?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: monthMap() - break - - case "Monthly": - input "trig_scheduled_daynums", sENUM, title: inTS1("Days of the Month?", "day_calendar"), description: (!settings.trig_scheduled_weeks ? optPrefix(): sBLANK), multiple: true, required: (!settings.trig_scheduled_weeks), submitOnChange: true, options: (1..31)?.collect { it as String } - if(!settings.trig_scheduled_daynums) { - input "trig_scheduled_weeks", sENUM, title: inTS1("Weeks of the Month?", "day_calendar"), description: (!settings.trig_scheduled_daynums ? optPrefix(): sBLANK), multiple: true, required: (!settings.trig_scheduled_daynums), submitOnChange: true, options: weeksOfMonthMap() - } - input "trig_scheduled_months", sENUM, title: inTS1("Only on these Months?", "day_calendar"), description: optPrefix(), multiple: true, required: false, submitOnChange: true, options: monthMap() - break - } - } - } - break - case sCSUNRISE: - case sCSUNSET: - input "trig_scheduled_sunState_offset", sNUMBER, range: "*..*", title: inTS1("Offset ${schedType} this number of minutes (+/-)", schedType?.toLowerCase()), required: false - break - } - triggerMsgInput(sSCHED) - } - } - } - - if (valTrigEvt(sALRMSYSST)) { - section (sectHead("${getAlarmSystemName()} (${getAlarmSystemName(true)}) Events"), hideable: true) { - String inT = sALRMSYSST - input "trig_${inT}", sENUM, title: inTS1("${getAlarmSystemName()} Modes", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: true, submitOnChange: true - if("alerts" in (List)settings."trig_${inT}") { - input "trig_${inT}_events", sENUM, title: inTS1("${getAlarmSystemName()} Alert Events", "alarm_home"), options: getAlarmSystemAlertOptions(), multiple: true, required: true, submitOnChange: true - } - if((List)settings."trig_${inT}") { - input "trig_${inT}_once", sBOOL, title: inTS1("Only alert once a day? (per type: mode)", sQUES), required: false, defaultValue: false, submitOnChange: true - input "trig_${inT}_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(sALRMSYSST) - } - } - } - - if (valTrigEvt("guard")) { - section (sectHead("Alexa Guard Events"), hideable: true) { - input "trig_guard", sENUM, title: inTS1("Alexa Guard Modes", "alarm_home"), options: ["ARMED_STAY", "ARMED_AWAY", sANY], multiple: true, required: true, submitOnChange: true - if(settings.trig_guard) { - // input "trig_guard_once", sBOOL, title: inTS1("Only alert once a day?\n(per mode)", sQUES), required: false, defaultValue: false, submitOnChange: true - // input "trig_guard_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)\n(Optional)", sDELAYT), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput("guard") - } - } - } - - if (valTrigEvt(sMODE)) { - section (sectHead("Mode Events"), hideable: true) { - input "trig_mode", sMODE, title: inTS1("Location Modes", sMODE), multiple: true, required: true, submitOnChange: true - if((List)settings.trig_mode) { - input "trig_mode_once", sBOOL, title: inTS1("Only alert once a day? (per type: mode)", sQUES), required: false, defaultValue: false, submitOnChange: true - input "trig_mode_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(sMODE) - } - } - } - - if(valTrigEvt(sPISTNEXEC)) { - section(sectHead("webCoRE Piston Executed Events"), hideable: true) { - String inT = sPISTNEXEC - input "trig_${inT}", sENUM, title: inTS1("Pistons", webCore_icon()), options: webCoRE_list(), multiple: true, required: true, submitOnChange: true - if(settings."trig_${inT}") { - paragraph pTS("webCoRE settings must be enabled to send events for Piston Execution (not enabled by default in webCoRE)", sNULL, false, sCLRGRY) - input "trig_${inT}_once", sBOOL, title: inTS1("Only alert once a day?\n(per type: piston)", sQUES), required: false, defaultValue: false, submitOnChange: true - input "trig_${inT}_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(sPISTNEXEC) - } - } - } - - if (valTrigEvt(sSWITCH)) { - trigNonNumSect(sSWITCH, sSWITCH, "Switches", "Switches", lONOFF+lANY, "are turned", lONOFF, sSWITCH) - } - - if (valTrigEvt(sLEVEL)) { - trigNumValSect(sLEVEL, "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level is", sSPDKNB) - } - - if (valTrigEvt(sBATT)) { - trigNumValSect(sBATT, sBATT, "Battery Level", "Batteries", "Level is", sSPDKNB) - } - - if (valTrigEvt(sMOTION)) { - trigNonNumSect(sMOTION, "motionSensor", "Motion Sensors", "Motion Sensors", lACTINACT+lANY, "become", lACTINACT, sMOTION) - } - - if (valTrigEvt("presence")) { - trigNonNumSect("presence", "presenceSensor", "Presence Sensors", "Presence Sensors", lPRES +lANY, sCHGTO, lPRES, "presence") - } - - if (valTrigEvt(sCONTACT)) { - trigNonNumSect(sCONTACT, "contactSensor", "Contacts, Doors, Windows", "Contacts, Doors, Windows", lOPNCLS+lANY, sCHGTO, lOPNCLS, sCONTACT) - } - - if (valTrigEvt("acceleration")) { - trigNonNumSect("acceleration", "accelerationSensor", "Accelerometers", "Accelerometers", lACTINACT+lANY, sCHGTO, lACTINACT, "acceleration") - } - - if (valTrigEvt("door")) { - trigNonNumSect("door", "garageDoorControl", "Garage Door Openers", "Garage Doors", lOPNCLS+["opening", "closing", sANY], sCHGTO, lOPNCLS, "garage_door") - } - - if (valTrigEvt(sLOCK)) { - trigNonNumSect(sLOCK, sLOCK, "Locks", "Smart Locks", lLOCKUNL + lANY, sCHGTO, lLOCKUNL, sLOCK, (!!(List)settings.trig_lock_Codes), (((String)settings.trig_lock && (String)settings.trig_lock_cmd in ["unlocked", sANY]) ? this.&handleCodeSect : this.&dummy), "Unlocked" ) - } - - if (valTrigEvt("securityKeypad")) { - trigNonNumSect("securityKeypad", "securityKeypad", "Security Keypad", "Security Keypad", lSEC + lANY, sCHGTO, lSEC, sKEYPAD, (!!(List)settings.trig_securityKeypad_Codes), (((String)settings.trig_securityKeypad && (String)settings.trig_securityKeypad_cmd in ["disarmed", sANY]) ? this.&handleCodeSect : this.&dummy), "Keypad Disarmed" ) - } - - if (valTrigEvt(sPUSHED)) { - trigButtonSect(sPUSHED, "pushableButton", "Button Pushed Events", "Pushable Buttons", "button_push", sPUSHED) - } - - if (valTrigEvt(sRELEASED)) { - trigButtonSect(sRELEASED, "releasableButton", "Button Released Events", "Releasable Buttons", "button_released", sRELEASED) - } - - if (valTrigEvt(sHELD)) { - trigButtonSect(sHELD, "holdableButton", "Button Held Events", "Holdable Buttons", "button_held", sHELD) - } - - if (valTrigEvt(sDBLTAP)) { - trigButtonSect(sDBLTAP, "doubleTapableButton", "Button Double Tap Events", "Double Tap Buttons", "button_double", sDBLTAP) - } - - if (valTrigEvt(sTEMP)) { - trigNumValSect(sTEMP, "temperatureMeasurement", "Temperature Sensor", "Temperature Sensors", "Temperature", sTEMP) - } - - if (valTrigEvt(sHUMID)) { - trigNumValSect(sHUMID, "relativeHumidityMeasurement", "Humidity Sensors", "Relative Humidity Sensors", "Relative Humidity (%)", sHUMID) - } - - if (valTrigEvt(sWATER)) { - trigNonNumSect(sWATER, "waterSensor", "Water Sensors", "Water/Moisture Sensors", lWETDRY + lANY, sCHGTO, lWETDRY, sWATER) - } - - if (valTrigEvt(sPOWER)) { - trigNumValSect(sPOWER, "powerMeter", "Power Events", "Power Meters", "Power Level (W)", sPOWER) - } - - if (valTrigEvt("carbonMonoxide")) { - trigNonNumSect("carbonMonoxide", "carbonMonoxideDetector", "Carbon Monoxide Events", "Carbon Monoxide Detectors", lDETECTCLR+lANY, sCHGTO, lDETECTCLR, "co") - } - - if (valTrigEvt(sSMOKE)) { - trigNonNumSect(sSMOKE, "smokeDetector", "Smoke Detector Events", "Smoke Detectors", lDETECTCLR+lANY, sCHGTO, lDETECTCLR, sSMOKE) - } - - if (valTrigEvt("illuminance")) { - trigNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Events", "Illuminance Sensors", "Lux Level (%)", "illuminance") - } - - if (valTrigEvt("windowShade")) { - trigNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", lOPNCLS+["opening", "closing", sANY], sCHGTO, lOPNCLS, "shade") - } - - if (valTrigEvt(sVALVE)) { - trigNonNumSect(sVALVE, sVALVE, "Valves", "Valves", lOPNCLS+lANY, sCHGTO, lOPNCLS, sVALVE) - } - - if (valTrigEvt(sCOOLSP)) { - trigNumValSect(sCOOLSP, sTHERM, "Thermostat Cooling Setpoint Events", "Thermostat (Cooling Setpoint)", "Setpoint temp", sTHERM) - } - - if (valTrigEvt(sHEATSP)) { - trigNumValSect(sHEATSP, sTHERM, "Thermostat Heating Setpoint Events", "Thermostat (Heating Setpoint)", "Setpoint temp", sTHERM) - } - - if (valTrigEvt(sTHERMTEMP)) { - trigNumValSect(sTHERMTEMP, sTHERM, "Thermostat Ambient Temperature Events", "Thermostat (Ambient Temperature)", "Ambient Temp", sTHERM) - } - - if (valTrigEvt(sTHERMOS)) { - trigNonNumSect(sTHERMOS, sTHERM, "Thermostat Operating State Events", "Thermostat (Operating State)", getThermOperStOpts()+lANY, sCHGTO, getThermOperStOpts(), sTHERM) - } - - if (valTrigEvt(sTHERMMODE)) { - trigNonNumSect(sTHERMMODE, sTHERM, "Thermostat Mode Events", "Thermostat (Mode)", getThermModeOpts()+lANY, sCHGTO, getThermModeOpts(), sTHERM) - } - - if (valTrigEvt(sTHERMFM)) { - trigNonNumSect(sTHERMFM, sTHERM, "Thermostat Fan Mode Events", "Thermostat (Fan Mode)", getThermFanOpts()+lANY, sCHGTO, getThermFanOpts(), sTHERM) - } - - if(triggersConfigured()) { - section(sBLANK) { - paragraph spanMdBldBr("Trigger Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") - } - } - } - state.showSpeakEvtVars = showSpeakEvtVars - } -} - -def handleCodeSect(String typ, String lbl) { - Map lockCodes = getCodes((List)settings."trig_${typ}") - //log.debug "lockCodes: ${lockCodes}" - if(lockCodes) { - Map codeOpts = lockCodes.collectEntries { [((String)it.key): it.value?.name ? "Name: "+ (String)it.value.name : "Code Number ${(String)it.key}: (${(String)it.value?.code})"] } - input "trig_${typ}_Codes", sENUM, title: inTS1("Filter ${lbl} codes...", sCOMMAND), options: codeOpts, multiple: true, required: false, submitOnChange: true - } -} - -Map getCodes(List devs, String code=sNULL) { - // lockCodes are: - // ["":["code":"", "name":""],"":["code":"", "name":""]] - Map result = [:] - try { - String lockCodes = devs && devs.size() == 1 ? devs[0]?.currentValue("lockCodes") : (code ?: sNULL) - if (lockCodes) { - //decrypt codes if they're encrypted - if (lockCodes[0] == "{") result = parseJson(lockCodes) - else result = parseJson(decrypt(lockCodes)) - if(devModeFLD) log.debug "lockCodes: ${result}" - } - } catch(ex) { logError("getCodes error", true, ex) } - return result -} - -def dummy(a,b) {} - -def trigNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, cmdAfterOpts, String image, Boolean devReq=true, Closure extraMeth=this.&dummy, String extraStr=sNULL) { - section (sectHead(sectStr), hideable: true) { - input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), multiple: true, required: devReq, submitOnChange: true - if (settings."trig_${inType}") { - input "trig_${inType}_cmd", sENUM, title: spanSmBld("${cmdTitle}...", sNULL, sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true - if((String)settings."trig_${inType}_cmd") { - if(inType == sLOCK) { - String lMsg = "This will prevent duplicate speech events on Zigbee Locks that send two events per lock and unlock." - input "trig_${inType}_ign_empty_type", sBOOL, title: spanSmBld("Ignore ${devTitle} Events with No Type?", sNULL, sQUES) + optPrefix() + lineBr() + spanSm(lMsg, sCLRGRY), required: false, defaultValue: false, submitOnChange: true - } - if (settings."trig_${inType}"?.size() > 1 && (String)settings."trig_${inType}_cmd" != sANY) { - input "trig_${inType}_all", sBOOL, title: spanSmBld("Require ALL ${devTitle} to be (${settings."trig_${inType}_cmd"})?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - extraMeth(inType, extraStr) - - if(!isTierAction() && (String)settings."trig_${inType}_cmd" in cmdAfterOpts) { - doAftInput(inType) - } - input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true - input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)", sNULL, sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(inType) - } - } - } -} - -def trigButtonSect(String inType, String capType, String sectStr, String devTitle, String image, String cmd){ - section (sectHead(sectStr), hideable: true) { - input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), required: true, multiple: true, submitOnChange: true - if (settings."trig_${inType}") { - settingUpdate("trig_${inType}_cmd", cmd, sENUM) - //input "trig_${inType}_cmd", sENUM, title: inTS1("Pushed changes", sCOMMAND), options: [sPUSHED], required: true, multiple: false, defaultValue: sPUSHED, submitOnChange: true - input "trig_${inType}_nums", sENUM, title: inTS1("Button numbers?", sCOMMAND), options: 1..8, required: true, multiple: true, submitOnChange: true - if(settings."trig_${inType}_nums") { - input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true - input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)", sNULL, sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(cmd) - } - } - } -} - -def trigNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image, Boolean devReq=true) { - Boolean done = false - section (sectHead(sectStr), hideable: true) { - input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), multiple: true, submitOnChange: true, required: devReq - if(settings."trig_${inType}") { - input "trig_${inType}_cmd", sENUM, title: spanSmBld("${cmdTitle} is...", sNULL, sCOMMAND), options: numOpts(), required: true, multiple: false, submitOnChange: true - if (settings."trig_${inType}_cmd") { - if ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN, sBELOW]) { - String snot = (String)settings."trig_${inType}_cmd" in [sNBETWEEN] ? "Not " : sBLANK - input "trig_${inType}_low", sNUMBER, title: spanSmBld(((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN] ? "${snot}Between a Low" : "a low") + " ${cmdTitle} of...", sNULL, "low"), required: true, submitOnChange: true - } - if(settings."trig_${inType}_low"!=null && ((String)settings."trig_${inType}_cmd" in [sBELOW]) ) done=true - - if ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN, sABOVE]) { - input "trig_${inType}_high", sNUMBER, title: spanSmBld(((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN] ? "and a High of..." : "a High") + " ${cmdTitle} of...", sNULL, "high"), required: true, submitOnChange: true - } - if(settings."trig_${inType}_high"!=null && ((String)settings."trig_${inType}_cmd" in [sABOVE])) done=true - - if(settings."trig_${inType}_low"!=null && settings."trig_${inType}_high"!=null && ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN])) done=true - - if ((String)settings."trig_${inType}_cmd" == sEQUALS) { - input "trig_${inType}_equal", sNUMBER, title: spanSmBld("a ${cmdTitle} of...", sNULL, sEQ), required: true, submitOnChange: true - if(settings."trig_${inType}_equal"!=null) done=true - } - if(done) { - if (settings."trig_${inType}"?.size() > 1) { - input "trig_${inType}_all", sBOOL, title: spanSmBld("Require ALL devices to be (${settings."trig_${inType}_cmd"}) values?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true - if(!(Boolean)settings."trig_${inType}_all") { - input "trig_${inType}_avg", sBOOL, title: spanSmBld("Use the average of all selected device values?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - doAftInput(inType) - input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day? (per type: ${inType})", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true - input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - triggerMsgInput(inType) - } - } - } - } -} - -def doAftInput(String inType) { - input "trig_${inType}_after", sNUMBER, title: spanSmBld("Only after (${settings."trig_${inType}_cmd"}) for (xx) (0..7200) seconds?", sNULL, sDELAYT), required: false, defaultValue: null, submitOnChange: true - Integer aft = (Integer)settings."trig_${inType}_after" - if(aft != null) { - if(aft < 0 || aft > 7200) { settingUpdate("trig_${inType}_after", 10) } - input "trig_${inType}_after_repeat", sNUMBER, title: spanSmBld("Repeat every (xx) (10..7200) seconds until it's not ${settings."trig_${inType}_cmd"}?", sNULL, sDELAYT), required: false, defaultValue: null, submitOnChange: true - Integer aftR = (Integer)settings."trig_${inType}_after_repeat" - if(aftR != null) { - input "trig_${inType}_after_repeat_cnt", sNUMBER, title: spanSmBld("Only repeat this many times (2..20000)?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: null, submitOnChange: true - if(aftR < 10 || aftR > 7200) { settingUpdate("trig_${inType}_after_repeat", 10) } - // triggerMsgInput(inType) - } - Integer aftRC = (Integer)settings."trig_${inType}_after_repeat_cnt" - if(aftRC != null) { - if(aftRC < 2 || aftRC > 20000) { settingUpdate("trig_${inType}_after_repeat_cnt", 120) } - // settingRemove("trig_${inType}_once") - // settingRemove("trig_${inType}_wait") - } - } else { - settingRemove("trig_${inType}_after_repeat") - settingRemove("trig_${inType}_after_repeat_cnt") - settingRemove("trig_${inType}_after_repeat_txt") - } -} - -def triggerMsgInput(String inType /*, Boolean showRepInputs=false, Integer itemCnt=0 */) { - if((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI]) { - String str = spanSmBldBr("Response Options", sCLR4D9) - String a=s3SPACE+sBULLET+sSPACE - str += spanSmBr("Available Options:") - str += spanSmBr("${a}${strUnder("1")}: Leave the text empty below and text will be generated for each ${inType} trigger event.") - str += spanSmBr("${a}${strUnder("2")}: Wait till the Execution config step and define a single global response for all triggers selected.") - str += spanSmBr("${a}${strUnder("3")}: Use the response builder below and create custom responses for each individual trigger type. (Supports randomization when multiple responses are configured)") - paragraph divSm(str, sCLRGRY, "info") - //Custom Text Options - Integer resp1cnt = 1 - String desc1 = sBLANK - String[] resps1 = ((String)settings."trig_${inType}_txt")?.split(";") ?: [] - if(resps1.size() > 1) { - resps1.each { String i-> desc1 += spanSmBld("(${resp1cnt})") + sSPACE + spanSmBr(i, sCLR4D9); resp1cnt++ } - } else { - desc1 += spanSmBr((String)settings."trig_${inType}_txt" ?: sBLANK, sCLR4D9) - } - desc1 += (String)settings."trig_${inType}_txt" ? lineBr() + spanSmBld("Edit in Response Designer...", sCLRGRY) : spanSm("Open Response Designer...", sCLRGRY) - href url: parent?.getTextEditorPath(app?.id as String, "trig_${inType}_txt"), style: sEXTNRL, required: false, title: inTS1("Custom ${inType?.capitalize()} Responses", sTEXT) + optPrefix(), description: desc1//, submitOnChange: true - - if((Integer)settings."trig_${inType}_after_repeat") { - //Custom Repeat Text Options - paragraph divSm(spanSmBldBr("Repeat Response Options", sCLR4D9) + spanSm("Add custom responses for the ${inType} events that are repeated.", sCLRGRY), sNULL, getAppImg("info")) - Integer resp2cnt = 1 - String desc2 = sBLANK - String[] resps2 = ((String)settings."trig_${inType}_after_repeat_txt")?.split(";") ?: [] - if(resps2.size() > 1) { - resps2.each { String i-> desc2 += spanSmBld("(${resp2cnt})") + sSPACE + spanSmBr(i, sCLR4D9); resp2cnt++ } - } else { - desc2 += spanSmBr((String)settings."trig_${inType}_after_repeat_txt" ?: sBLANK, sCLR4D9) - } - desc2 += (String)settings."trig_${inType}_after_repeat_txt" ? lineBr() + spanSmBld("Edit in Response Designer...", sCLRGRY) : spanSm("Open Response Designer...", sCLRGRY) - href url: parent?.getTextEditorPath(app?.id as String, "trig_${inType}_after_repeat_txt"), style: sEXTNRL, title: inTS1("Custom ${inType?.capitalize()} Repeat Responses", sTEXT) + optPrefix(), description: desc2//, submitOnChange: true - } - } -} - -Boolean locationTriggers() { - return (valTrigEvt(sMODE) && (List)settings.trig_mode) || (valTrigEvt(sALRMSYSST) && (List)settings.trig_alarmSystemStatus) || - (valTrigEvt(sPISTNEXEC) && settings.trig_pistonExecuted) || - (valTrigEvt("guard") && settings.trig_guard) -} - -Boolean deviceTriggers() { - return (settings.trig_windowShade && settings.trig_windowShade_cmd) || (settings.trig_door && settings.trig_door_cmd) || (settings.trig_valve && settings.trig_valve_cmd) || - (settings.trig_switch && settings.trig_switch_cmd) || (settings.trig_level && settings.trig_level_cmd) || (settings.trig_lock && settings.trig_lock_cmd) || - (settings.trig_securityKeypad && settings.trig_securityKeypad_cmd) || -// (settings.trig_button && settings.trig_button_cmd) || - (settings.trig_pushed && settings.trig_pushed_cmd && settings.trig_pushed_nums) || - (settings.trig_released && settings.trig_released_cmd && settings.trig_released_nums) || - (settings.trig_held && settings.trig_held_cmd && settings.trig_held_nums) || - (settings.trig_doubleTapped && settings.trig_doubleTapped_cmd && settings.trig_doubleTapped_nums) || - (thermostatTriggers()) -} - -Boolean thermostatTriggers() { - List okList = [] - [sCOOLSP, sHEATSP, sTHERMTEMP].each { String att-> // Thermostat number value validation - if(valTrigEvt(att)) { okList.push((Boolean)(settings."trig_${att}" && settings."trig_${att}_cmd" && (settings."trig_${att}_low"!=null || settings."trig_${att}_high"!=null || settings."trig_${att}_equal"!=null))) } - } - [sTHERMMODE, sTHERMOS, sTHERMFM].each { String att-> // Thermostat non number validation - if(valTrigEvt(att)) { okList.push((Boolean)(settings."trig_${att}" && settings."trig_${att}_cmd")) } - } - // log.debug "thermostatTriggers | okList: ${okList} | Every: ${(okList.every { it == true })}" - return (okList.size()) ? (okList.every { it == true }) : false -} - -Boolean sensorTriggers() { - return (settings.trig_temperature && settings.trig_temperature_cmd) || (settings.trig_carbonMonoxide && settings.trig_carbonMonoxide_cmd) || (settings.trig_humidity && settings.trig_humidity_cmd) || - (settings.trig_battery && settings.trig_battery_cmd) || - (settings.trig_water && settings.trig_water_cmd) || (settings.trig_smoke && settings.trig_smoke_cmd) || (settings.trig_presence && settings.trig_presence_cmd) || (settings.trig_motion && settings.trig_motion_cmd) || - (settings.trig_contact && settings.trig_contact_cmd) || (settings.trig_power && settings.trig_power_cmd) || (settings.trig_illuminance && settings.trig_illuminance_low!=null && settings.trig_illuminance_high!=null) || - (settings.trig_acceleration && settings.trig_acceleration_cmd) -} - -Boolean triggersConfigured() { - Boolean sched = schedulesConfigured() - Boolean loc = locationTriggers() - Boolean dev = deviceTriggers() - Boolean sen = sensorTriggers() - // log.debug "sched: $sched | loc: $loc | dev: $dev | sen: $sen" - return (sched || loc || dev || sen) -} - -/****************************************************************************** - CONDITIONS SELECTION PAGE -******************************************************************************/ - -def conditionsPage() { - return dynamicPage(name: "conditionsPage", nextPage: "mainPage", title: "Conditions/Restrictions", install: false, uninstall: false) { - Boolean cra = !!(Boolean)settings.cond_require_all - Boolean multiConds = multipleConditions() - if(!multiConds && cra) { cra=false; settingUpdate("cond_require_all", sFALSE, sBOOL) } - String a = getConditionsDesc(false) - if(a) { section() { paragraph spanSm(a, sCLR4D9) } } - - section() { - if(multiConds) { - input "cond_require_all", sBOOL, title: inTS1("Require All Selected Conditions to Pass Before Action Runs?", sCHKBOX), required: false, defaultValue: true, submitOnChange: true - cra = (Boolean)settings.cond_require_all - } else { cra=false; settingUpdate("cond_require_all", sFALSE, sBOOL) } - Boolean allR = ( multiConds && cra ) // Boolean reqAllCond() - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm(allR ? "All selected conditions must pass before for this action to operate." : "Any condition will allow this action to operate.", sCLR4D9) - } - section(sectHead("Time/Date")) { - href "condTimePage", title: inTS1("Time Schedule", "clock"), description: spanSm(getTimeCondDesc(false), sCLR4D9) - input "cond_days", sENUM, title: inTS1("Days of the week", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: weekDaysEnum() - input "cond_months", sENUM, title: inTS1("Months of the year", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: monthEnum() - } - section (sectHead("Mode Conditions")) { - input "cond_mode", sMODE, title: inTS1("Location Modes...", sMODE), multiple: true, required: false, submitOnChange: true - if((List)settings.cond_mode) { - input "cond_mode_cmd", sENUM, title: inTS1("are...", sCOMMAND), options: ["not":"Not in these modes", (sARE):"In these Modes"], required: true, multiple: false, submitOnChange: true - } - } - section (sectHead("Alarm Conditions")) { - input "cond_alarmSystemStatus", sENUM, title: inTS1("${getAlarmSystemName()} is...", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: false, submitOnChange: true - } - - condNonNumSect(sSWITCH, sSWITCH, "Switches/Outlets Conditions", "Switches/Outlets", lONOFF, sARE, sSWITCH) - - condNonNumSect(sMOTION, "motionSensor", "Motion Conditions", "Motion Sensors", lACTINACT, sARE, sMOTION) - - condNonNumSect("presence", "presenceSensor", "Presence Conditions", "Presence Sensors", lPRES, sARE, "presence") - - condNonNumSect(sCONTACT, "contactSensor", "Door, Window, Contact Sensors Conditions", "Contact Sensors", lOPNCLS, sARE, sCONTACT) - - condNonNumSect("acceleration", "accelerationSensor", "Accelerometer Conditions", "Accelerometer Sensors", lACTINACT, sARE, "acceleration") - - condNonNumSect(sLOCK, sLOCK, "Lock Conditions", "Smart Locks", lLOCKUNL, sARE, sLOCK) - - condNonNumSect("securityKeypad", "securityKeypad", "Security Keypad Conditions", "Security Kepads", lSEC, sARE, sKEYPAD) - - condNonNumSect("door", "garageDoorControl", "Garage Door Conditions", "Garage Doors", lOPNCLS, sARE, "garage_door") - - condNumValSect(sTEMP, "temperatureMeasurement", "Temperature Conditions", "Temperature Sensors", "Temperature", sTEMP) - - condNumValSect(sHUMID, "relativeHumidityMeasurement", "Humidity Conditions", "Relative Humidity Sensors", "Relative Humidity (%)", sHUMID) - - condNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Conditions", "Illuminance Sensors", "Lux Level (%)", "illuminance") - - condNumValSect(sLEVEL, "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level (%)", sSPDKNB) - - condNonNumSect(sWATER, "waterSensor", "Water Sensors", "Water Sensors", lWETDRY, sARE, sWATER) - - condNonNumSect(sSMOKE, "smokeDetector", "Smoke Detectors", "Smoke Detectors", lDETECTCLR, sARE, sSMOKE) - - condNonNumSect("carbonMonoxide", "carbonMonoxideDetector", "Carbon Monoxide Detectors", "Carbon Monoxide Detectors", lDETECTCLR, sARE, "co") - - condNumValSect(sPOWER, "powerMeter", "Power Events", "Power Meters", "Power Level (W)", sPOWER) - - condNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", lOPNCLS, sARE, "shade") - - condNonNumSect(sVALVE, sVALVE, "Valves", "Valves", lOPNCLS, sARE, sVALVE) - - condNumValSect(sBATT, sBATT, "Battery Level Conditions", "Batteries", "Level (%)", sBATT) - - condNonNumSectM(sTHERMMODE, sTHERM, "Thermostat Modes", "Thermostat Mode", getThermModeOpts(), sTHERM) - - condNonNumSectM(sTHERMOS, sTHERM, "Thermostat Operating States", "Thermostat Operating State", getThermOperStOpts(), sTHERM) - - condNonNumSectM(sTHERMFM, sTHERM, "Thermostat Fan Modes", "Thermostat Fan Mode", getThermFanOpts(), sTHERM) - - condNumValSect(sCOOLSP, sTHERM, "Cooling Setpoint", "Thermostat Cooling SetPoint", "Temperature", sTEMP) - - condNumValSect(sHEATSP, sTHERM, "Heating Setpoint", "Thermostat Heating SetPoint", "Temperature", sTEMP) - } -} - -static List getThermModeOpts() { - return ["auto", "cool", " heat", "emergency heat", "off"] -} - -static List getThermOperStOpts() { - return ["cooling", "heating", "idle", "fan only"] -} - -static List getThermFanOpts() { - return ["on", "circulate", "auto"] -} - -// allows multiple:true for _cmd -def condNonNumSectM(String inType, String capType, String sectStr, String devTitle, List cmdOpts, String image) { - section (sectHead(sectStr)) { - input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false - if (settings."cond_${inType}") { - input "cond_${inType}_cmd", sENUM, title: inTS1("${devTitle} is...", sCOMMAND), options: cmdOpts, multiple: true, required: true, submitOnChange: true - if (((List)settings."cond_${inType}")?.size() > 1 && settings."cond_${inType}_cmd"?.size() == 1) { - input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } else settingUpdate("cond_${inType}_all", sFALSE, sBOOL) - } - } -} - -def condNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, String image) { - section (sectHead(sectStr)) { - input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false - if (settings."cond_${inType}") { - input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle}...", sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true - if ((String)settings."cond_${inType}_cmd" && settings."cond_${inType}"?.size() > 1) { - input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - } -} - -static List numOpts() { - return [sBETWEEN, sNBETWEEN, sBELOW, sABOVE, sEQUALS] -} - -def condNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image) { - section (sectHead(sectStr)) { - input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required: false - if((List)settings."cond_${inType}") { - input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle} is...", sCOMMAND), options: numOpts(), required: true, multiple: false, submitOnChange: true - String c_cmd = (String)settings."cond_${inType}_cmd" - if (c_cmd) { - if (c_cmd in [sNBETWEEN, sBETWEEN, sBELOW]) { - input "cond_${inType}_low", sNUMBER, title: inTS1("a ${c_cmd in [sNBETWEEN, sBETWEEN] ? "Low " : sBLANK}${cmdTitle} of...", "low"), required: true, submitOnChange: true - } - if (c_cmd in [sNBETWEEN, sBETWEEN, sABOVE]) { - input "cond_${inType}_high", sNUMBER, title: inTS1("${c_cmd in [sNBETWEEN, sBETWEEN] ? "and a high " : "a "}${cmdTitle} of...", "high"), required: true, submitOnChange: true - } - if (c_cmd == sEQUALS) { - input "cond_${inType}_equal", sNUMBER, title: inTS1("a ${cmdTitle} of...", sEQ), required: true, submitOnChange: true - } - if (((List)settings."cond_${inType}")?.size() > 1) { - input "cond_${inType}_all", sBOOL, title: inTS1("Require ALL devices to be (${c_cmd}) values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - if(!(Boolean)settings."cond_${inType}_all") { - input "cond_${inType}_avg", sBOOL, title: inTS1("Use the average of all selected device values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - } - } - } -} - -private Map devsSupportVolume(List devs) { - // this will never return a zone virtual device - List noSupport = [] - List supported = [] - if(devs?.size()) { - devs.each { dev-> - if(dev?.hasAttribute("permissions") && dev?.currentPermissions?.toString()?.contains("volumeControl")) { - supported.push((String)dev.label) - } else { noSupport.push((String)dev.label) } - } - } - return [s:supported, n:noSupport] -} - -def actVariableDesc(String actType, Boolean hideUserTxt=false) { - Map> txtItems = customTxtItems() - if(!isTierAction()) { - if(!txtItems?.size() && state.showSpeakEvtVars && !(String)settings."act_${actType}_txt") { - String str = "NOTICE:
You can choose to leave the response field empty and generic text will be generated for each event type, or return to Step 2 and define responses for each trigger." - paragraph spanSm(str, sCLR4D9, getAppImg("info")) - } - if(!hideUserTxt) { - if(txtItems?.size()) { - String str = spanSmBld("NOTICE: (Custom Text Defined)", sCLRORG) - txtItems?.each { i-> - i?.value?.each { i2-> str += lineBr() + spanSmBld("${sBULLET} ${i?.key?.capitalize()} ${i2?.key?.capitalize()}: ") + spanSm("(${i2?.value?.size()} Response${pluralizeStr(i2?.value)})") } - } - paragraph spanSm(str, sCLR4D9) - paragraph spanSmBld("WARNING:
Entering text below will override the text you defined for the trigger types under Step 2.", sCLRRED) - } - } - } -} - -@Field static final Map descsFLD = [ - speak: "Speak any message on your Echo Devices.", - speak_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken.", - speak_parallel: "Speak any message in parallel on your Echo Devices.", - speak_parallel_tiered: "Allows you to create parallel tiered responses. Each tier can have a different delay before the next message is spoken.", - speak_ignDND: "Speak any message on your Echo Devices, ignoring Do Not Disturb.", - speak_ignDND_parallel: "Speak any message on your Echo Devices, ignoring Do Not Disturb, in parallel.", - speak_ignDND_parallel_tiered:"Allows you to create parallel tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", - speak_ignDND_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", - announcement: "Plays a brief tone and speaks the message you define. If you select multiple devices it will be a synchronized broadcast.", - announcement_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is announced. Plays a brief tone and announces the message you define. If you select multiple devices it will be a synchronized broadcast.", - announcement_ignDND_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", - announcement_ignDND: "Plays a brief tone and speaks the message you define. If you select multiple devices it will be a synchronized broadcast, ignoring Do Not Disturb.", - sequence: "Sequences are a custom command where you can string different alexa actions which are sent to Amazon as a single command. The command is then processed by amazon sequentially or in parallel.", - weather: "Plays a very basic weather report.", - playback: "Allows you to control the media playback state or volume level of your Echo devices.", - builtin: "Builtin items are things like Sing a Song, Tell a Joke, Say Goodnight, etc.", - sounds: "Plays a selected amazon sound item.", - voicecmd: "Executes a text based command just as if it was spoken to the device", - music: "Allows playback of various Songs/Radio using any connected music provider", - calendar: "This will read out events in your calendar (Requires accounts to be configured in the alexa app. Must not have PIN.)", - alarm: "This will allow you to create Alexa alarm clock notifications.", - reminder: "This will allow you to create Alexa reminder notifications.", - dnd: "This will allow you to enable/disable Do Not Disturb mode", - alexaroutine: "This will allow you to run your configured Alexa Routines", - wakeword: "This will allow you to change the Wake Word of your Echo devices.", - bluetooth: "This will allow you to connect or disconnect bluetooth devices paired to your echo devices." -] - -String actionTypeDesc() { - String a = (String)settings.actionType - return descsFLD.containsKey(a) ? (String)descsFLD[a] : "No Description Found..." -} - -def actionTiersPage() { - return dynamicPage(name: "actionTiersPage", title: sBLANK, install: false, uninstall: false) { - section() { - input "act_tier_cnt", sNUMBER, title: inTS1("How many Tiers?", sEQ), required: true, submitOnChange: true - } - Integer tierCnt = (Integer)settings.act_tier_cnt - if(tierCnt) { - (1..tierCnt)?.each { Integer ti-> - String tstr = sACT_ITEM+"${ti}" - section(sectHead("Tier Item (${ti}) Config:")) { - if(ti > 1) { - input "${tstr}_delay", sNUMBER, title: inTS1("Delay after Tier ${ti-1}\n(seconds)", sEQ), defaultValue: null, required: true, submitOnChange: true - } - if(ti==1 || settings."${tstr}_delay") { - String inTxt = (String)settings."${tstr}_txt" - href url: parent?.getTextEditorPath(app?.id as String, "${tstr}_txt"), style: sEXTNRL, required: true, title: inTS1("Tier Item ${ti} Response", sTEXT) + (!inTxt ? spanSm(" (Required)", sCLRRED) : sBLANK), - description: inTxt ? spanSm(inTxt, sCLR4D9) : inactFoot("Open Response Designer...") - } - input "${tstr}_volume_change", sNUMBER, title: inTS1("Tier Item Volume", sSPDKNB) + optPrefix(), defaultValue: null, required: false, submitOnChange: true - input "${tstr}_volume_restore", sNUMBER, title: inTS1("Tier Item Volume Restore", sSPDKNB) + optPrefix(), defaultValue: null, required: false, submitOnChange: true - } - } - if(isTierActConfigured()) { - section(sBLANK) { - paragraph spanMdBldBr("Tier Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") - } - } - } - } -} - -String getTierRespDesc() { - Map tierMap = getTierMap() ?: [:] - String str = sBLANK - str += tierMap?.size() ? spanSmBr("Tiered Responses: (${tierMap?.size()})") : sBLANK - tierMap?.each { k,v-> - String tstr = sACT_ITEM+"${k}" - if((String)settings."${tstr}_txt") { - str += (settings."${tstr}_delay") ? spanSmBr(" ${sBULLET} Tier ${k} delay: (${settings."${tstr}_delay"} sec)") : sBLANK - str += (settings."${tstr}_volume_change") ? spanSmBr(" ${sBULLET} Tier ${k} volume: (${settings."${tstr}_volume_change"})") : sBLANK - str += (settings."${tstr}_volume_restore") ? spanSmBr(" ${sBULLET} Tier ${k} restore: (${settings."${tstr}_volume_restore"})") : sBLANK - } - } - return str != sBLANK ? str : sBLANK -} - -Boolean isTierAction() { - return ((String)settings.actionType in [sSPEAKT, sSPEAKPT, sSPEAKIT, sSPEAKIPT, sANNT, sANNIT]) -} - -Boolean isTierActConfigured() { - if(!isTierAction()) { return false } - Integer cnt = (Integer)settings.act_tier_cnt - List tierKeys = settings.findAll { it?.key?.startsWith(sACT_ITEM) && it?.key?.endsWith("_txt") }?.collect { (String)it?.key } - return (tierKeys?.size() == cnt) -} - -Map getTierMap() { - Map exec = [:] - Integer cnt = (Integer)settings.act_tier_cnt - if(isTierActConfigured() && cnt) { - List tiers = (1..cnt) - tiers?.each { Integer t-> - String tstr = sACT_ITEM+"${t}" - exec[t] = [ - message: (String)settings["${tstr}_txt"], - delay: settings[sACT_ITEM+"${(t+1)}_delay"], - volume: [ - change: settings["${tstr}_volume_change"], - restore: settings["${tstr}_volume_restore"] - ] - ] - } - } - return (exec?.size()) ? exec : null -} - -void tierItemCleanup() { - List rem = [] - Boolean isTierAct = isTierAction() - Integer tierCnt = (Integer)settings.act_tier_cnt - List tierKeys = settings.findAll { it?.key?.startsWith(sACT_ITEM) }?.collect { it.key as String } - List tierIds = isTierAct && tierCnt ? (1..tierCnt) : [] - // if(!isTierAct() || !tierCnt) { return } - tierKeys?.each { String k-> - List id = k?.tokenize("_") ?: [] - if(!isTierAct || (id?.size() && id?.size() < 4) || !id[3]?.isNumber() || !(id[3]?.toInteger() in tierIds)) { rem.push(k) } - } - if(rem.size()) { logDebug("tierItemCleanup | Removing: ${rem}"); rem.each { settingRemove(it) } } -} - -def actTextOrTiersInput(String type, Boolean req=false) { - if(isTierAction()) { - String tDesc = getTierRespDesc() - href "actionTiersPage", title: inTS1("Create Tiered Responses?", sTEXT, (tDesc ? sCLR4D9 : sCLRRED)), description: (tDesc ? divSm(tDesc + inputFooter(sTTM), sCLR4D9) : inputFooter(sTTC, sCLRGRY)) - input "act_tier_stop_on_clear", sBOOL, title: inTS1("Stop responses when trigger is cleared?"), required: false, defaultValue: false, submitOnChange: true - } else { - String textUrl = parent?.getTextEditorPath(app?.id as String, type) - String addr = !req ? optPrefix() : sBLANK - href url: textUrl, style: sEXTNRL, title: inTS1("Default Action Response", sTEXT) + addr, description: (String)settings."${type}" ? spanSm((String)settings."${type}", sCLR4D9) : inputFooter("Open Response Designer...", sCLRGRY, true) - } -} - -@SuppressWarnings('GroovyFallthrough') -def actionsPage() { - return dynamicPage(name: "actionsPage", nextPage: "mainPage", title: "Action Execution Configuration", install: false, uninstall: false) { - String a = getActionDesc(false) - if(a) { - section() { - paragraph spanSm(a, sCLR4D9) - } - } - Boolean done = false - Map actionExecMap = [configured: false] - String myactionType = (String)settings.actionType - if(myactionType) { - actionExecMap.actionType = myactionType - actionExecMap.config = [:] - List devices = parent?.getDevicesFromList(settings.act_EchoDevices) - String actTypeDesc = spanBldBr("[${myactionType.tokenize("_")?.collect { it?.capitalize() }?.join(sSPACE)}]", sCLR4D9, "info") + spanSm(actionTypeDesc(), sCLRGRY) - Boolean isTierAct = isTierAction() - switch(myactionType) { - case sSPEAK: - case sSPEAKI: - case sSPEAKP: - case sSPEAKIP: - case sSPEAKT: - case sSPEAKIT: - case sSPEAKPT: - case sSPEAKIPT: - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - section(sectHead("Action Type Config:")) { - actVariableDesc(myactionType) - actTextOrTiersInput("act_speak_txt") - } - if(!(myactionType in [sSPEAKP, sSPEAKPT, sSPEAKIP, sSPEAKIPT])) actionVolumeInputs(devices) - actionExecMap.config[myactionType] = [text: (String)settings.act_speak_txt, evtText: ((state.showSpeakEvtVars && !(String)settings.act_speak_txt) || hasUserDefinedTxt()), tiers: getTierMap()] - done = state.showSpeakEvtVars || (String)settings.act_speak_txt || (isTierAct && isTierActConfigured()) - } else { done = false } - break - - case sANN: - case sANNI: - case sANNT: - case sANNIT: - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm("announce") - if(settings.act_EchoDevices || settings.act_EchoZones) { - section(sectHead("Action Type Config:")) { - actVariableDesc(myactionType) - actTextOrTiersInput("act_announcement_txt") - } - actionVolumeInputs(devices) - actionExecMap.config[myactionType] = [text: (String)settings.act_announcement_txt, evtText: ((state.showSpeakEvtVars && !(String)settings.act_announcement_txt) || hasUserDefinedTxt()), tiers: getTierMap()] - if(settings.act_EchoDevices?.size() > 1) { - List devObj = [] - devices?.each { devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]) } - // log.debug "devObj: $devObj" - actionExecMap.config[myactionType].deviceObjs = devObj - } - done = state.showSpeakEvtVars || (String)settings.act_announcement_txt || (isTierAct && isTierActConfigured()) - } else { done = false } - break - - case "voicecmd": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices) { - section(sectHead("Action Type Config:")) { - input "act_voicecmd_txt", sTEXT, title: inTS1("Enter voice command text", sTEXT), submitOnChange: true, required: false - } - actionExecMap.config.voicecmd = [text: (String)settings.act_voicecmd_txt, cmd: "voiceCmdAsText"] - if((String)settings.act_voicecmd_txt) { done = true } else { done = false } - } else { done = false } - break - - case "sequence": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - Map seqItemsAvail = parent?.seqItemsAvail() - section(sectHead("Sequence Options Legend:"), hideable: true, hidden: false) { - String str1 = "Sequence Options:" - seqItemsAvail?.other?.sort()?.each { k, v-> - str1 += "${bulletItem(str1, "${k}${v != null ? "::${v}" : sBLANK}")}" - } - String str4 = "DoNotDisturb Options:" - seqItemsAvail?.dnd?.sort()?.each { k, v-> - str4 += "${bulletItem(str4, "${k}${v != null ? "::${v}" : sBLANK}")}" - } - String str2 = "Music Options:" - seqItemsAvail?.music?.sort()?.each { k, v-> - str2 += "${bulletItem(str2, "${k}${v != null ? "::${v}" : sBLANK}")}" - } - String str3 = "Canned TTS Options:" - seqItemsAvail?.speech?.sort()?.each { k, v-> - def newV = v - if(v instanceof List) { newV = sBLANK; v?.sort()?.each { newV += s5SPACE+dashItem(newV, "${it}", true) } } - str3 += "${bulletItem(str3, "${k}${newV != null ? "::${newV}" : sBLANK}")}" - } - paragraph spanSm(str1, sCLR4D9) - // paragraph spanSm(str4, sCLR4D9) - paragraph spanSm(str2, sCLR4D9) - paragraph spanSm(str3, sCLR4D9) - paragraph spanSm("Enter the command in a format exactly like this:
volume::40,, speak::this is so silly,, wait::60,, weather,, cannedtts_random::goodbye,, traffic,, amazonmusic::green day,, volume::30

Each command needs to be separated by a double comma `,,` and the separator between the command and value must be command::value.", "violet") - } - section(sectHead("Action Type Config:")) { - input "act_sequence_txt", sTEXT, title: inTS1("Enter sequence text", sTEXT), submitOnChange: true, required: false - } - actionExecMap.config.sequence = [text: (String)settings.act_sequence_txt, cmd: "executeSequenceCommand"] - if((String)settings.act_sequence_txt) { done = true } else { done = false } - } else { done = false } - break - - case sWEATH: - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - actionVolumeInputs(devices) - done = true - actionExecMap.config.weather = [cmd: "playWeather"] - } else { done = false } - break - - case "playback": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm("mediaPlayer") - if(settings.act_EchoDevices || settings.act_EchoZones) { - Map playbackOpts = [ - "pause":"Pause", "stop":"Stop", "play":"Play", "nextTrack":"Next Track", "previousTrack":"Previous Track", - "mute":"Mute", "volume":"Volume" - ] - section(sectHead("Playback Config:")) { - input "act_playback_cmd", sENUM, title: inTS1("Select Playback Action", sCOMMAND), description: sBLANK, options: playbackOpts, required: true, submitOnChange: true - } - if(settings.act_playback_cmd == "volume") { actionVolumeInputs(devices, true) } - actionExecMap.config?.playback = [cmd: settings.act_playback_cmd] - if(settings.act_playback_cmd) { done = true } else { done = false } - } else { done = false } - break - - case "sounds": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - section(sectHead("BuiltIn Sounds Config:")) { - input "act_sounds_cmd", sENUM, title: inTS1("Select Sound Type", sCOMMAND), description: sBLANK, options: parent?.getAvailableSounds()?.collect { it?.key as String }, required: true, submitOnChange: true - } - actionVolumeInputs(devices) - actionExecMap.config.sounds = [cmd: "playSoundByName", name: settings.act_sounds_cmd] - done = (settings.act_sounds_cmd != null) - } else { done = false } - break - - case "builtin": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - Map builtinOpts = [ - "playSingASong": "Sing a Song", "playFlashBrief": "Flash Briefing (News)", "playGoodNews": "Good News Only", "playFunFact": "Fun Fact", "playTraffic": "Traffic", "playJoke": "Joke", - "playTellStory": "Tell Story", "sayGoodbye": "Say Goodbye", "sayGoodNight": "Say Goodnight", "sayBirthday": "Happy Birthday", - "sayCompliment": "Give Compliment", "sayGoodMorning": "Good Morning", "sayWelcomeHome": "Welcome Home" - ] - section(sectHead("BuiltIn Speech Config:")) { - input "act_builtin_cmd", sENUM, title: inTS1("Select Builtin Speech Type", sCOMMAND), description: sBLANK, options: builtinOpts, required: true, submitOnChange: true - } - actionVolumeInputs(devices) - actionExecMap.config.builtin = [cmd: settings.act_builtin_cmd] - if(settings.act_builtin_cmd) { done = true } else { done = false } - } else { done = false } - break - - case "music": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm("mediaPlayer") - if(settings.act_EchoDevices || settings.act_EchoZones) { - List musicProvs = devices[0]?.hasAttribute("supportedMusic") ? devices[0]?.currentValue("supportedMusic")?.split(",")?.collect { "${it?.toString()?.trim()}"} : [] - logDebug("Music Providers: ${musicProvs}") - if(musicProvs) { - section(sectHead("Music Providers:")) { - input "act_music_provider", sENUM, title: inTS1("Select Music Provider", "music"), description: sBLANK, options: musicProvs, multiple: false, required: true, submitOnChange: true - } - if(settings.act_music_provider) { - if(settings.act_music_provider == "TuneIn") { - section(sectHead("TuneIn Search Results:")) { - paragraph spanSm("Enter a search phrase to query TuneIn to help you find the right search term to use in searchTuneIn() command.") - input "tuneinSearchQuery", sTEXT, title: inTS1("Enter search phrase for TuneIn", "tunein"), defaultValue: null, required: false, submitOnChange: true - if(settings.tuneinSearchQuery) { - href "searchTuneInResultsPage", title: inTS1("View search results!", "search"), description: sTTP - } - } - } - section(sectHead("Action Type Config:")) { - input "act_music_txt", sTEXT, title: inTS1("Enter Music Search text", sTEXT), submitOnChange: true, required: false - } - actionVolumeInputs(devices) - } - } - actionExecMap.config.music = [cmd: "searchMusic", provider: settings.act_music_provider, search: (String)settings.act_music_txt] - done = settings.act_music_provider && (String)settings.act_music_txt - } else { done = false } - break - - case "calendar": - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm(sTTS) - if(settings.act_EchoDevices || settings.act_EchoZones) { - section(sectHead("Action Type Config:")) { - input "act_calendar_cmd", sENUM, title: inTS1("Select Calendar Action", sCOMMAND), description: sBLANK, options: ["playCalendarToday":"Today", "playCalendarTomorrow":"Tomorrow", "playCalendarNext":"Next Events"], - required: true, submitOnChange: true - } - actionVolumeInputs(devices) - actionExecMap.config.calendar = [cmd: settings.act_calendar_cmd] - if(act_calendar_cmd) { done = true } else { done = false } - } else { done = false } - break - - case "alarm": - //TODO: Offer to remove alarm after event. - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm("alarms") - if(settings.act_EchoDevices) { -// Map repeatOpts = ["everyday":"Everyday", "weekends":"Weekends", "weekdays":"Weekdays", "daysofweek":"Days of the Week", "everyxdays":"Every Nth Day"] - String rptType = sNULL - def rptTypeOpts = null - section(sectHead("Action Type Config:")) { - input "act_alarm_label", sTEXT, title: inTS1("Alarm Label", "name_tag"), submitOnChange: true, required: true - input "act_alarm_date", sTEXT, title: inTS1("Alarm Date\n(yyyy-mm-dd)", "day_calendar"), submitOnChange: true, required: true - input "act_alarm_time", sTIME, title: inTS1("Alarm Time", "clock"), submitOnChange: true, required: true - // if(act_alarm_label && act_alarm_date && act_alarm_time) { - // input "act_alarm_rt", sENUM, title: inTS1("Repeat (Optional)", sCOMMAND), description: sBLANK, options: repeatOpts, required: true, submitOnChange: true - // if(settings."act_alarm_rt") { - // rptType = settings.act_alarm_rt - // if(settings.act_alarm_rt == "daysofweek") { - // input "act_alarm_rt_wd", sENUM, title: inTS1("Weekday", sCHKBOX), description: sBLANK, options: weekDaysAbrvEnum(), multiple: true, required: true, submitOnChange: true - // if(settings.act_alarm_rt_wd) rptTypeOpts = settings.act_alarm_rt_wd - // } - // if(settings."act_alarm_rt" == "everyxdays") { - // input "act_alarm_rt_ed", sNUMBER, title: inTS1("Every X Days", sCHKBOX), description: sBLANK, range: "1..31", required: true, submitOnChange: true - // if(settings.act_alarm_rt_ed) rptTypeOpts = settings.act_alarm_rt_ed - // } - // } - // } - // input "act_alarm_remove", sBOOL, title: "Remove Alarm when done", defaultValue: true, submitOnChange: true, required: false - } - actionVolumeInputs(devices, false, true) - String newTime = settings.act_alarm_time ? parseFmtDt("yyyy-MM-dd'T'HH:mm:ss.SSSZ", 'HH:mm', settings.act_alarm_time) : sNULL - actionExecMap.config.alarm = [cmd: "createAlarm", label: settings.act_alarm_label, date: settings.act_alarm_date, (sTIME): newTime, recur: [type: rptType, opts: rptTypeOpts], remove: settings.act_alarm_remove] - done = settings.act_alarm_label && settings.act_alarm_date && settings.act_alarm_time - } else { done = false } - break - - case "reminder": - //TODO: Offer to remove reminder after event. - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - echoDevicesInputByPerm("reminders") - if(settings.act_EchoDevices) { -// Map repeatOpts = ["everyday":"Everyday", "weekends":"Weekends", "weekdays":"Weekdays", "daysofweek":"Days of the Week", "everyxdays":"Every Nth Day"] - String rptType = sNULL - def rptTypeOpts = null - section(sectHead("Action Type Config:")) { - input "act_reminder_label", sTEXT, title: inTS1("Reminder Label", "name_tag"), submitOnChange: true, required: true - input "act_reminder_date", sTEXT, title: inTS1("Reminder Date\n(yyyy-mm-dd)", "day_calendar"), submitOnChange: true, required: true - input "act_reminder_time", sTIME, title: inTS1("Reminder Time", "clock"), submitOnChange: true, required: true - // if(settings.act_reminder_label && settings.act_reminder_date && settings.act_reminder_time) { - // input "act_reminder_rt", sENUM, title: inTS1("Repeat (Optional)", sCOMMAND), description: sBLANK, options: repeatOpts, required: true, submitOnChange: true - // if(settings."act_reminder_rt") { - // rptType = settings.act_reminder_rt - // if(settings."act_reminder_rt" == "daysofweek") { - // input "act_reminder_rt_wd", sENUM, title: inTS1("Weekday", sCHKBOX), description: sBLANK, options: weekDaysAbrvEnum(), multiple: true, required: true, submitOnChange: true - // if(settings.act_reminder_rt_wd) rptTypeOpts = settings.act_reminder_rt_wd - // } - // if(settings."act_reminder_rt" && settings."act_reminder_rt" == "everyxdays") { - // input "act_reminder_rt_ed", sNUMBER, title: inTS1("Every X Days (1-31)", sCHKBOX), description: sBLANK, range: "1..31", required: true, submitOnChange: true - // if(settings.act_reminder_rt_ed) rptTypeOpts = settings.act_reminder_rt_ed - // } - // } - // } - // input "act_reminder_remove", sBOOL, title: "Remove Reminder when done", defaultValue: true, submitOnChange: true, required: false - } - actionVolumeInputs(devices, false, true) - String newTime = settings.act_reminder_time ? parseFmtDt("yyyy-MM-dd'T'HH:mm:ss.SSSZ", 'HH:mm', settings.act_reminder_time) : sNULL - actionExecMap.config.reminder = [cmd: "createReminderNew", label: settings.act_reminder_label, date: settings.act_reminder_date, (sTIME): newTime, recur: [type: rptType, opts: rptTypeOpts], remove: settings.act_reminder_remove] - done = settings.act_reminder_label && settings.act_reminder_date && settings.act_reminder_time - } else { done = false } - break - - case "dnd": - echoDevicesInputByPerm("doNotDisturb") - if(settings.act_EchoDevices) { - Map dndOpts = ["doNotDisturbOn":"Enable", "doNotDisturbOff":"Disable"] - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - section(sectHead("Action Type Config:")) { - input "act_dnd_cmd", sENUM, title: inTS1("Select Do Not Disturb Action", sCOMMAND), description: sBLANK, options: dndOpts, required: true, submitOnChange: true - } - actionExecMap.config.dnd = [cmd: settings.act_dnd_cmd] - if(settings.act_dnd_cmd) { done = true } else { done = false } - } else { done = false } - break - - case "alexaroutine": - echoDevicesInputByPerm("wakeWord") - if(settings.act_EchoDevices) { - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - Map t0 = parent?.getAlexaRoutines() - Map routinesAvail = t0 ?: [:] - // logDebug("routinesAvail: $routinesAvail") - section(sectHead("Action Type Config:")) { - input "act_alexaroutine_cmd", sENUM, title: inTS1("Select Alexa Routine", sCOMMAND), description: sBLANK, options: routinesAvail, multiple: false, required: true, submitOnChange: true - } - actionExecMap.config.alexaroutine = [cmd: "executeRoutineId", routineId: settings.act_alexaroutine_cmd] - if(settings.act_alexaroutine_cmd) { done = true } else { done = false } - } else { done = false } - break - - case "wakeword": - echoDevicesInputByPerm("wakeWord") - if(settings.act_EchoDevices) { - Integer devsCnt = settings.act_EchoDevices.size() ?: 0 - List devsObj = [] - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - if(devsCnt >= 1) { - List wakeWords = devices[0]?.hasAttribute("wakeWords") ? devices[0]?.currentValue("wakeWords")?.replaceAll('"', sBLANK)?.split(",") : [] - // logDebug("WakeWords: ${wakeWords}") - devices?.each { cDev-> - section(sectHead("${cDev?.getLabel()}:")) { - if(wakeWords?.size()) { - paragraph "Current Wake Word: ${cDev?.hasAttribute("alexaWakeWord") ? cDev?.currentValue("alexaWakeWord") : "Unknown"}" - input "act_wakeword_device_${cDev?.id}", sENUM, title: inTS1("New Wake Word", "list"), description: sBLANK, options: wakeWords, required: true, submitOnChange: true - devsObj.push([device: cDev?.id as String, wakeword: settings."act_wakeword_device_${cDev?.id}", cmd: "setWakeWord"]) - } else { paragraph spanSmBld("Oops...
No Wake Words have been found!

Please Remove the device from selection.", sCLRRED) } - } - } - } - actionExecMap.config.wakeword = [ devices: devsObj] - done = settings.findAll { it?.key?.startsWith("act_wakeword_device_") && it?.value }?.size() == devsCnt - } else { done = false } - break - - case "bluetooth": - echoDevicesInputByPerm("bluetoothControl") - if(settings.act_EchoDevices) { - Integer devsCnt = settings.act_EchoDevices?.size() ?: 0 - List devsObj = [] - section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } - if(devsCnt >= 1) { - devices?.each { cDev-> - def btData = cDev?.hasAttribute("btDevicesPaired") ? cDev?.currentValue("btDevicesPaired") : null - List btDevs = (btData) ? parseJson(btData)?.names : [] - // log.debug "btDevs: $btDevs" - section(sectHead("${cDev?.getLabel()}:")) { - if(btDevs?.size()) { - input "act_bluetooth_device_${cDev?.id}", sENUM, title: inTS1("BT device to use", "bluetooth"), description: sBLANK, options: btDevs, required: true, submitOnChange: true - input "act_bluetooth_action_${cDev?.id}", sENUM, title: inTS1("BT action to take", sCOMMAND), description: sBLANK, options: ["connectBluetooth":"connect", "disconnectBluetooth":"disconnect"], required: true, submitOnChange: true - devsObj?.push([device: cDev?.id as String, btDevice: settings."act_bluetooth_device_${cDev?.id}", cmd: settings."act_bluetooth_action_${cDev?.id}"]) - } else { paragraph spanSmBldBr("Oops...") + spanSm("No Bluetooth devices are paired to this Echo Device!
Please Remove the device from selection.", sCLRRED) } - } - } - } - actionExecMap.config.bluetooth = [devices: devsObj] - done = settings.findAll { it?.key?.startsWith("act_bluetooth_device_") && it?.value }?.size() == devsCnt && - settings.findAll { it?.key?.startsWith("act_bluetooth_action_") && it?.value }?.size() == devsCnt - } else { done = false } - break - default: - paragraph spanSmBld("Unknown Action Type Defined...", sCLRRED, getAppImg("error")) - break - } - if(done) { - section(sectHead("Delay Config:")) { - input "act_delay", sNUMBER, title: inTS1("Delay Action in seconds", sDELAYT) + optPrefix(), required: false, submitOnChange: true - } - if(isTierAct && (Integer)settings.act_tier_cnt > 1) { - section(sectHead("Tier Action Start Tasks:")) { - href "actTrigTasksPage", title: inTS1("Tiered Tasks to Perform on Tier Start?", "tasks"), description: spanSm(actTaskDesc(sACT_START, true), sCLR4D9), params:[type: sACT_START] - } - section(sectHead("Tier Action Stop Tasks:")) { - href "actTrigTasksPage", title: inTS1("Tiered Tasks to Perform on Tier Stop?", "tasks"), description: spanSm(actTaskDesc(sACT_STOP, true), sCLR4D9), params:[type: sACT_STOP] - } - } else { - section(sectHead("Action Triggered Tasks:")) { - href "actTrigTasksPage", title: inTS1("Tasks to Perform when Triggered?", "tasks"), description: spanSm(actTaskDesc(sACT, true), sCLR4D9), params:[type: sACT] - } - } - actionSimulationSect() - section(sBLANK) { - paragraph spanMdBldBr("Execution Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") - } - actionExecMap.config.volume = [change: settings.act_volume_change, restore: settings.act_volume_restore, alarm: settings.act_alarm_volume] - actionExecMap.config.zoneVolume = getZoneVolumeMap() - // log.debug "zoneVolume: ${actionExecMap.config.zoneVolume}" - actionExecMap.delay = settings.act_delay - actionExecMap.configured = true - actionCleanup() - //TODO: Check Cleanup of non selected inputs - } else { actionExecMap = [configured: false] } - } - Map t1 = (done && (Boolean)actionExecMap.configured) ? actionExecMap : [configured: false] - state.actionExecMap = t1 - updConfigStatusMap() - logDebug("actionExecMap: ${t1}") - } -} - -def actTrigTasksPage(params) { - String t = params?.type - if(t) { - atomicState.curPageParams = params - } else { t = atomicState.curPageParams?.type } - return dynamicPage(name: "actTrigTasksPage", title: sBLANK, install: false, uninstall: false) { - Map dMap = [:] - section() { - switch(t) { - case sACT: - dMap = [def: sBLANK, delay: "tasks"] - paragraph spanSm("These tasks will be performed when the action is triggered.
(Delay is optional)", sCLR4D9) - break - case sACT_START: - dMap = [def: " with Tier start", delay: "Tier Start tasks"] - paragraph spanSm("These tasks will be performed with when the first tier of action is triggered.
(Delay is optional)", sCLR4D9) - break - case sACT_STOP: - dMap = [def: " with Tier stop", delay: "Tier Stop tasks"] - paragraph spanSm("These tasks will be performed with when the last tier of action is triggered.
(Delay is optional)", sCLR4D9) - break - default: - logWarn("bad actTaskDesc t: ${t}", true) - } - } - - if(!settings.enableWebCoRE) { - section (sectHead("Enable webCoRE Integration:")) { - input "enableWebCoRE", sBOOL, title: inTS("Enable webCoRE Integration", webCore_icon()), required: false, defaultValue: false, submitOnChange: true - } - } - if(settings.enableWebCoRE) { - if(!webCoREFLD) webCoRE_init() - webCoRE_poll() - } - section(sectHead("Control Devices:")) { - input "${t}switches_on", "capability.switch", title: inTS1("Turn ON these Switches${dMap?.def}", sSWITCH) + optPrefix(), multiple: true, required: false, submitOnChange: true - input "${t}switches_off", "capability.switch", title: inTS1("Turn OFF these Switches${dMap?.def}", sSWITCH) + optPrefix(), multiple: true, required: false, submitOnChange: true - } - - section(sectHead("Control Dimmers/Lights:")) { - input "${t}lights", "capability.switch", title: inTS1("Turn ON these Lights${dMap?.def}", "light") + optPrefix(), multiple: true, required: false, submitOnChange: true - if((List)settings."${t}lights") { - List lights = (List)settings."${t}lights" - List noLevelDevs = lights?.findAll { i-> (!(i?.hasCommand("setLevel") ) ) } - List noColorDevs = lights?.findAll { i-> ( !(i?.hasCommand("setColor") ) )} - - // Detemine if any of the devices supports level - Boolean lvlSupportAny = lights?.any { i-> (i?.hasCommand("setLevel")) } - Boolean lvlSupportAll = lights?.every { i-> (i?.hasCommand("setLevel")) } - if (lvlSupportAny) { - input "${t}lights_leveln", sNUMBER, title: inTS1("At this level?", sSPDKNB) + optPrefix(), multiple: false, options: dimmerLevelEnum(), required: false, submitOnChange: true - if(!lvlSupportAll && noLevelDevs?.size() > 0) { - paragraph spanSmBldBr("NOTICE: The following selected devices do not support level: ", sCLRRED) + spanSm(noLevelDevs.collect { " ${sBULLET} ${it}" }.join(lineBr()).toString(), sCLRRED) - } - } - - // Detemine if any of the devices supports color control - Boolean clrSupportAny = lights?.any { i-> (i?.hasCommand("setColor")) } - Boolean clrSupportAll = lights?.every { i-> (i?.hasCommand("setColor")) } - if(clrSupportAny) { - input "${t}lights_colort", sENUM, title: inTS1("To this color?", "light_color") + optPrefix(), multiple: false, options: colorSettingsListFLD?.name, required: false, submitOnChange: true - if (!clrSupportAll && noColorDevs.size() > 0) { - paragraph spanSmBldBr("NOTICE: The following selected devices do not support color: ", sCLRRED) + spanSm(noColorDevs.collect { " ${sBULLET} ${it}" }.join(lineBr()).toString(), sCLRRED) - } - } - - input "${t}lights_trig_clear_restore", sBOOL, title: inTS1("Restore original Light states when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true - input "${t}lights_restore_delay", sNUMBER, title: inTS1("Restore original light states after (x) seconds?", sDELAYT) + optPrefix(), required: false, submitOnChange: true - } - - } - - section(sectHead("Control Locks:")) { - input "${t}locks_lock", "capability.lock", title: inTS1("Lock these Locks${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true - input "${t}locks_unlock", "capability.lock", title: inTS1("Unlock these Locks${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true - if(settings."${t}locks_lock" || settings."${t}locks_unlock") { - input "${t}locks_trig_clear_restore", sBOOL, title: inTS1("Restore original lock state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true - } - } - - section(sectHead("Control Keypads:")) { - input "${t}securityKeypads_disarm", "capability.securityKeypad", title: inTS1("Disarm these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true - input "${t}securityKeypads_armHome", "capability.securityKeypad", title: inTS1("Arm Home these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true - input "${t}securityKeypads_armAway", "capability.securityKeypad", title: inTS1("Arm Away these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true - } - - section(sectHead("Control Doors:")) { - input "${t}doors_close", "capability.garageDoorControl", title: inTS1("Close these Garage Doors${dMap?.def}", "garage_door") + optPrefix(), multiple: true, required: false, submitOnChange: true - input "${t}doors_open", "capability.garageDoorControl", title: inTS1("Open these Garage Doors${dMap?.def}", "garage_door") + optPrefix(), multiple: true, required: false, submitOnChange: true - if(settings."${t}doors_close" || settings."${t}doors_open") { - input "${t}doors_trig_clear_restore", sBOOL, title: inTS1("Restore original door state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true - } - } - - section(sectHead("Control Siren:")) { - input "${t}sirens", "capability.alarm", title: inTS1("Activate these Sirens${dMap?.def}", "siren") + optPrefix(), multiple: true, required: false, submitOnChange: true - if(settings."${t}sirens") { - input "${t}siren_cmd", sENUM, title: inTS1("Alarm action to take${dMap?.def}", sCOMMAND) + optPrefix(), options: ["both": "Siren & Stobe", "strobe":"Strobe Only", "siren":"Siren Only"], multiple: false, required: true, submitOnChange: true - if(settings."${t}sirens" && settings."${t}siren_cmd") { - input "${t}siren_trig_clear_restore", sBOOL, title: inTS1("Restore HSM state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true - input "${t}siren_time", sNUMBER, title: inTS1("Stop after (x) seconds...", sDELAYT), required: (!(Boolean)settings."${t}siren_trig_clear_restore"), submitOnChange: true - } - - } - } - section(sectHead("Location Actions:")) { - input "${t}mode_run", sENUM, title: inTS1("Set Location Mode${dMap?.def}", sMODE) + optPrefix(), options: getLocationModes(true), multiple: false, required: false, submitOnChange: true - input "${t}alarm_run", sENUM, title: inTS1("Set ${getAlarmSystemName()} mode${dMap?.def}", "alarm_home") + optPrefix(), options: getAlarmSystemStatusActions(), multiple: false, required: false, submitOnChange: true - if(settings."${t}alarm_run") { - input "${t}alarm_run_trig_clear_restore", sBOOL, title: inTS1("Restore HSM state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true - } - - if(settings.enableWebCoRE) { - input "${t}piston_run", sENUM, title: inTS("Execute a piston${dMap?.def}", webCore_icon()) + optPrefix(), options: webCoRE_list(), multiple: false, required: false, submitOnChange: true - } - } - - if(actTasksConfiguredByType(t)) { - section("Delay before running Tasks: ") { - input "${t}tasks_delay", sNUMBER, title: inTS1("Delay running ${dMap?.delay} in seconds", sDELAYT) + optPrefix(), required: false, submitOnChange: true - } - } - } -} - -Boolean actTasksConfiguredByType(String pType) { - String p = pType - return (String)settings."${p}mode_run" || (String)settings."${p}alarm_run" || settings."${p}switches_off" || settings."${p}switches_on" || - (settings.enableWebCoRE && (String)settings."${p}piston_run") || - settings."${p}lights" || settings."${p}locks_lock" || settings."${p}locks_unlock" || settings."${p}sirens" || settings."${p}doors_close" || settings."${p}doors_open" || - settings."${p}securityKeypads_disarm" || settings."${p}securityKeypads_armHome" || settings."${p}securityKeypads_armAway" -} - -private executeTaskCommands(data) { - String p = data?.type ?: sNULL - if(isTrc())logTrace("executeTaskCommands | type: ${p} actions: ${actTaskDesc(p)}") - - if((String)settings."${p}mode_run") { setLocationMode((String)settings."${p}mode_run") } - if((String)settings."${p}alarm_run") { sendLocationEvent(name: "hsmSetArm", value: (String)settings."${p}alarm_run") } - if(settings.enableWebCoRE && (String)settings."${p}piston_run") { webCoRE_execute((String)settings."${p}piston_run") } - - if(settings."${p}switches_off") { settings."${p}switches_off"*.off() } - if(settings."${p}switches_on") { settings."${p}switches_on"*.on() } - if(settings."${p}locks_lock") { settings."${p}locks_lock"*.lock() } - if(settings."${p}locks_unlock") { settings."${p}locks_unlock"*.unlock() } - if(settings."${p}securityKeypads_disarm") { settings."${p}securityKeypads_disarm"*.disarm() } - if(settings."${p}securityKeypads_armHome") { settings."${p}securityKeypads_armHome"*.armHome() } - if(settings."${p}securityKeypads_armAway") { settings."${p}securityKeypads_armAway"*.armAway() } - if(settings."${p}doors_close") { settings."${p}doors_close"*.close() } - if(settings."${p}doors_open") { settings."${p}doors_open"*.open() } - if(settings."${p}sirens" && (String)settings."${p}siren_cmd") { - String cmd= (String)settings."${p}siren_cmd" - settings."${p}sirens"*."${cmd}"() - if(settings."${p}siren_time") runIn(settings."${p}siren_time", "postTaskCommands", [data:[type: p]]) - } - if(settings."${p}lights") { - if(settings."${p}lights_trig_clear_restore" || settings."${p}lights_restore_delay") { captureLightState((List)settings."${p}lights", p) } - settings."${p}lights"*.on() - if(settings."${p}lights_leveln") { - List hasLvlDevs = settings."${p}lights"*.filter { it.hasCommand("setLevel") } - hasLvlDevs*.setLevel(settings."${p}lights_leveln") - } - if(settings."${p}lights_colort") { - List hasClrDevs = settings."${p}lights"*.filter { it.hasCommand("setColor") } - hasClrDevs*.setColor(getColorName(settings."${p}lights_colort")) - } - if(settings."${p}lights_trig_clear_restore" && !settings."${p}lights_restore_delay") { - restoreLightState((List)settings."${p}lights", p) - } - if(settings."${p}lights_restore_delay") { runIn(settings."${p}lights_restore_delay", "restoreLights", [data:[type: p]]) } - } -} - -String actTaskDesc(String t, Boolean isInpt=false) { - String str = sBLANK - if(actTasksConfiguredByType(t)) { - switch(t) { - case sACT: - str += (isInpt ? sBLANK : lineBr()) + strUnder('Triggered Tasks:') - break - case sACT_START: - str += (isInpt ? sBLANK : lineBr()) + strUnder('Tiered Start Tasks:') - break - case sACT_STOP: - str += (isInpt ? sBLANK : lineBr()) + strUnder('Tiered Stop Tasks:') - break - } - String aStr = "\n ${sBULLET} " - str += settings."${t}switches_on" ? aStr + "Switches On: (${settings."${t}switches_on"?.size()})" : sBLANK - str += settings."${t}switches_off" ? aStr + "Switches Off: (${settings."${t}switches_off"?.size()})" : sBLANK - str += settings."${t}lights" ? aStr + "Lights: (${settings."${t}lights"?.size()})" : sBLANK - str += settings."${t}lights" && settings."${t}lights_leveln" ? "\n - Level: (${settings."${t}lights_leveln"}%)" : sBLANK - str += settings."${t}lights" && settings."${t}lights_colort" ? "\n - Color: (${settings."${t}lights_colort"})" : sBLANK - str += settings."${t}lights" && settings."${t}lights_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK - str += settings."${t}lights" && settings."${t}lights_restore_delay" ? "\n - Restore After: (${settings."${t}lights_restore_delay"} sec.)" : sBLANK - str += settings."${t}locks_unlock" ? aStr + "Locks Unlock: (${settings."${t}locks_unlock"?.size()})" : sBLANK - str += settings."${t}locks_lock" ? aStr + "Locks Lock: (${settings."${t}locks_lock"?.size()})" : sBLANK - str += (settings."${t}locks_lock" || settings."${t}locks_unlock") && settings."${t}locks_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK - str += settings."${t}securityKeypads_disarm" ? aStr + "KeyPads Disarm: (${settings."${t}securityKeypads_disarm".size()})" : sBLANK - str += settings."${t}securityKeypads_armHome" ? aStr + "KeyPads Arm Home: (${settings."${t}securityKeypads_armHome".size()})" : sBLANK - str += settings."${t}securityKeypads_armAway" ? aStr + "KeyPads Arm Away: (${settings."${t}securityKeypads_armAway".size()})" : sBLANK - str += settings."${t}doors_open" ? aStr + "Garages Open: (${settings."${t}doors_open"?.size()})" : sBLANK - str += settings."${t}doors_close" ? aStr + "Garages Close: (${settings."${t}doors_close"?.size()})" : sBLANK - str += (settings."${t}doors_open" || settings."${t}doors_close") && settings."${t}doors_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK - str += settings."${t}sirens" ? aStr + "Sirens On: (${settings."${t}sirens"?.size()})${settings."${t}sirens_delay" ? "(${settings."${t}sirens_delay"} sec)" : sBLANK}" : sBLANK - str += settings."${t}sirens" && settings."${t}siren_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK - - str += (String)settings."${t}mode_run" ? aStr + "Set Mode:\n - ${(String)settings."${t}mode_run"}" : sBLANK - str += (String)settings."${t}alarm_run" ? aStr + "Set Alarm:\n - ${(String)settings."${t}alarm_run"}" : sBLANK - str += settings."${t}alarm_run" && settings."${t}alarm_run_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK -// str += settings."${t}routine_run" ? aStr+"Execute Routine:\n - ${getRoutineById(settings."${t}routine_run")?.label}" : sBLANK - str += (settings.enableWebCoRE && (String)settings."${t}piston_run") ? aStr + "Execute webCoRE Piston:\n - " + getPistonById((String)settings."${t}piston_run") : sBLANK - } - return str != sBLANK ? (isInpt ? divSm(spanSmBr(str) + inputFooter(sTTM), sCLR4D9) : str) : (isInpt ? spanSm("On trigger control devices, set mode, set alarm state, execute WebCore Pistons", sCLRGRY) + inactFoot(sTTC) : sNULL) -} - -Boolean isActDevContConfigured() { - return isTierAction() ? (settings.act_tier_start_switches_off || settings.act_tier_start_switches_on || settings.act_tier_stop_switches_off || settings.act_tier_stop_switches_on) : (settings.act_switches_off || settings.act_switches_on) -} - -def actionSimulationSect() { - section(sectHead("Simulate Action")) { - String str = spanSmBldBr("Test this action and see the results.", "black", "testing") - String a=s2SPACE+sBULLET+sSPACE - str += spanSmBldBr("NOTE: ") + spanSmBr("${a}When global text is not defined, this will generate a random event based on your trigger selections.") - str += settings.act_EchoZones ? spanSm("${a}Testing with zones requires you to save the app and come back in to test.") : sBLANK - paragraph spanSm(str, sCLRGRY) - input "actTestRun", sBOOL, title: inTS1("Test this action?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.actTestRun) { executeActTest() } - } -} - -Boolean customMsgRequired() { return (!((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI])) } -Boolean customMsgConfigured() { return (settings.notif_use_custom && settings.notif_custom_message) } - -def actNotifPage() { - return dynamicPage(name: "actNotifPage", title: "Action Notifications", install: false, uninstall: false) { - String a = getAppNotifDesc() - String b = spanSmBldBr("Notification Overview:", sCLR4D9) - b += (!a) ? " ${sBULLET} Notifications not enabled" : a - section() { - paragraph spanSm(b, sCLR4D9) - } - - section (sectHead("Notification Devices:")) { - input "notif_devs", "capability.notification", title: inTS1("Send to Notification devices?", "notification"), description: ((!settings?.notif_devs) ? inactFoot(sTTC) : sBLANK), required: false, multiple: true, submitOnChange: true - } - section (sectHead("Alexa Mobile Notification:")) { - paragraph spanSmBldBr("Description:", sCLRGRY) + spanSmBld("This will send a push notification the Alexa Mobile app.", sCLRGRY) - input "notif_alexa_mobile", sBOOL, title: inTS1("Send message to Alexa App?", "notification"), required: false, defaultValue: false, submitOnChange: true - } - - if((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) { - section (sectHead("Message Customization:")) { - Boolean custMsgReq = customMsgRequired() - if(custMsgReq) { - paragraph pTS("The selected action (${(String)settings.actionType}) requires a custom notification message if notifications are enabled.", sNULL, false, sCLRGRY) - if(!settings.notif_use_custom) { settingUpdate("notif_use_custom", sTRUE, sBOOL) } - } else { - paragraph pTS("When using speak or announcement actions custom notification is optional and a notification will be sent with speech text.", sNULL, false, sCLRGRY) - } - input "notif_use_custom", sBOOL, title: inTS1("Send a custom notification...", sQUES), required: false, defaultValue: false, submitOnChange: true - if(settings.notif_use_custom || custMsgReq) { - input "notif_custom_message", sTEXT, title: inTS1("Enter custom message...", sTEXT), required: custMsgReq, submitOnChange: true - } - } - } else { - List sets = settings.findAll { it?.key?.startsWith("notif_") }?.collect { it?.key as String } - sets?.each { String sI-> settingRemove(sI) } - } - - if(isActNotifConfigured()) { - section(sectHead("Notification Restrictions:")) { - String nsd = getNotifSchedDesc() - href "actNotifTimePage", title: inTS1("Quiet Restrictions", "restriction"), description: (nsd ? divSm(spanSmBr(nsd) + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC)) - } - if(!(Boolean)state.notif_message_tested) { - def actDevices = (Boolean)settings.notif_alexa_mobile ? parent?.getDevicesFromList(settings.act_EchoDevices) : [] - def aMsgDev = actDevices?.size() && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null - if(sendNotifMsg("Info", "Action Notification Test Successful. Notifications Enabled for ${app?.getLabel()}", aMsgDev, true)) { state.notif_message_tested = true } - } - } else state.remove("notif_message_tested") - } -} - -def actNotifTimePage() { - return dynamicPage(name:"actNotifTimePage", title: sBLANK, install: false, uninstall: false) { - // String a = getNotifSchedDesc() - // if(a) { - // section() { - // paragraph spanSmBldBr("Restrictions Status:", sCLR4D9) + spanSm(a, sCLR4D9) - // paragraph spanSmBldBr("NOTICE: All selected restrictions must be ${strUnder("INACTIVE")} for notifications to be sent.", sCLRORG) - // paragraph htmlLine() - // } - // } - String pre = "notif" - Boolean timeReq = (settings["${pre}_time_start"] || settings["${pre}_time_stop"]) - section(sectHead("Quiet Start Time:")) { - input "${pre}_time_start_type", sENUM, title: inTS1("Starting at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true - if(settings."${pre}_time_start_type" == sTIME) { - input "${pre}_time_start", sTIME, title: inTS1("Start time", sSTTIME), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_start_type" in lSUNRISESET) { - input "${pre}_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true - } - } - section(sectHead("Quiet Stop Time:")) { - input "${pre}_time_stop_type", sENUM, title: inTS1("Stopping at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true - if(settings."${pre}_time_stop_type" == sTIME) { - input "${pre}_time_stop", sTIME, title: inTS1("Stop time", sSTTIME), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_stop_type" in lSUNRISESET) { - input "${pre}_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true - } - } - section(sectHead("Allowed Days:")) { - input "${pre}_days", sENUM, title: inTS1("Only on these week days", "day_calendar"), multiple: true, required: false, options: weekDaysEnum() - } - section(sectHead("Allowed Modes:")) { - input "${pre}_modes", sMODE, title: inTS1("Only in these Modes", sMODE), multiple: true, submitOnChange: true, required: false - } - } -} - -def ssmlInfoSection() { - String ssmlTestUrl = "https://topvoiceapps.com/ssml" - String ssmlDocsUrl = "https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html" - String ssmlSoundsUrl = "https://developer.amazon.com/docs/custom-skills/ask-soundlibrary.html" - String ssmlSpeechConsUrl = "https://developer.amazon.com/en-US/docs/alexa/custom-skills/speechcon-reference-interjections.html" - section(sectHead("SSML Documentation:"), hideable: true, hidden: true) { - paragraph spanSmBldBr("What is SSML?", sCLR4D9) + spanSm("SSML allows for changes in tone, speed, voice, emphasis. As well as using MP3, and access to the Sound Library", sCLR4D9) - String ttob = inactFoot("Tap to open browser") - href url: ssmlDocsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon SSML Docs", "www"), description: ttob - href url: ssmlSoundsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon Sound Library", "www"), description: ttob - href url: ssmlSpeechConsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon SpeechCons", "www"), description: ttob - href url: ssmlTestUrl, style: sEXTNRL, required: false, title: inTS1("SSML Designer and Tester", "www"), description: ttob - } -} - -private Map> customTxtItems() { - Map> items = [:] - ((List)settings.triggerEvents)?.each { String tr-> - String a=(String)settings."trig_${tr}_txt" - if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].event = a.tokenize(";") } - a = (String)settings."trig_${tr}_after_repeat_txt" - if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].repeat = a.tokenize(";") } - } - return items -} -/* -private Boolean hasRepeatTriggers() { - Map> items = [:] - ((List)settings.triggerEvents)?.each { String tr-> - String a=(String)settings."trig_${tr}_after_repeat_txt" - if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].repeat = a.tokenize(";") } - } - return (items.size() > 0) -} */ - -Boolean hasUserDefinedTxt() { -/* Boolean fnd = false - ((List)settings.triggerEvents)?.each { String tr -> - if((String)settings."trig_${tr}_txt") { fnd = true } - if((String)settings."trig_${tr}_after_repeat_txt") { fnd = true } - if(fnd) return true - } - return fnd */ - List myL = (List)settings.triggerEvents - String res = sNULL - res = myL?.find { String tr -> - (String)settings."trig_${tr}_txt" || (String)settings."trig_${tr}_after_repeat_txt" - } - return (res!=sNULL) -} - -Boolean executionConfigured() { - Boolean opts = (state.actionExecMap && state.actionExecMap.configured == true) - Boolean devs = (settings.act_EchoDevices || settings.act_EchoZones) - return (opts && devs) -} - -private echoDevicesInputByPerm(String type) { - List echoDevs = parent?.getChildDevicesByCap(type) - Boolean capOk = (type in [sTTS, "announce"]) - Boolean zonesOk = ((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT, sANN, sANNI, sANNT, sANNIT, "voicecmd", "sequence", sWEATH, "calendar", "music", "sounds", "builtin"]) - Map echoZones = (capOk && zonesOk) ? getZones() : [:] - section(sectHead("${echoZones?.size() ? "Zones & " : sBLANK}Alexa Devices:")) { - if(echoZones?.size()) { - if(!settings.act_EchoZones) { paragraph spanSmBldBr("What are Zones?") + spanSm("Zones are used to direct the speech output based on the conditions in the zone (Motion, presence, etc).
When both Zones and Echo devices are selected, the zone will take priority over the echo device setting.") } - input "act_EchoZones", sENUM, title: inTS1("Zone(s) to Use", "es_groups"), description: spanSm("Select the Zone(s)", sCLRGRY), options: echoZones?.collectEntries { [(it?.key): it?.value?.name?.replace(" (Z)", sBLANK) as String] }, multiple: true, required: (!settings.act_EchoDevices), submitOnChange: true - } - if(settings.act_EchoZones?.size() && echoDevs?.size() && !settings.act_EchoDevices?.size()) { - paragraph spanSm("There may be scenarios when none of your zones are active at the triggered action execution.\nYou have the option to select echo devices to use when no zones are available.", sCLR4D9) - } - if(echoDevs?.size()) { - Boolean devsOpt = (settings.act_EchoZones?.size()) - def eDevsMap = echoDevs?.collectEntries { [(it?.getId()): [label: it?.getLabel(), lsd: (it?.currentWasLastSpokenToDevice?.toString() == sTRUE)]] }?.sort { a,b -> b?.value?.lsd <=> a?.value?.lsd ?: a?.value?.label <=> b?.value?.label } - input "act_EchoDevices", sENUM, title: inTS1("Echo Speaks Devices", "echo_gen1") + (devsOpt ? spanSm(" (Optional Zone Backup)", "violet") : sBLANK), description: spanSm(devsOpt ? "These devices are used when all zones are inactive." : "Select your devices", sCLRGRY), options: eDevsMap?.collectEntries { [(it?.key): "${it?.value?.label}${(it?.value?.lsd == true) ? "\n(Last Spoken To)" : sBLANK}"] }, multiple: true, required: (!settings.act_EchoZones), submitOnChange: true - updDeviceInputs() - } else { paragraph spanSmBld("No devices were found with support for ($type)", sCLRRED) } - } -} - -private actionVolumeInputs(List devices, Boolean showVolOnly=false, Boolean showAlrmVol=false) { - if(showAlrmVol) { - section(sectHead("Volume Options:")) { - input "act_alarm_volume", sNUMBER, title: inTS1("Alarm Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true - } - } else { - if((devices || settings.act_EchoZones) && (String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI, sWEATH, "calendar", "music", "sounds", "builtin", "playback"]) { - Map volMap = devsSupportVolume(devices) - Integer volMapSiz = volMap?.n?.size() - Integer devSiz = devices?.size() - if(settings.act_EchoZones?.size() > 1) { - section(sectHead("(Per Zone) Volume Options:"), hideable: true) { - input "act_EchoZones_vol_per_zone", sBOOL, title: inTS1("Set Per Zone Volume?", sQUES), defaultValue: false, submitOnChange: true - if((Boolean)settings.act_EchoZones_vol_per_zone) { - Map echoZones = (Map)((Map)getZones() ?: [:]).findAll { it.key in settings.act_EchoZones } - paragraph htmlLine(sCLR4D9) - echoZones.each { String znId, Map znData-> - paragraph spanBld("Zone: ${znData.name}") - input "act_EchoZones_${znId}_volume_change", sNUMBER, title: inTS1("Volume Level (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true - if(!showVolOnly) { - input "act_EchoZones_${znId}_volume_restore", sNUMBER, title: inTS1("Restore Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true - } - } - } - } - } - section(sectHead("Volume Options:")) { - if(devSiz == volMapSiz) { paragraph spanSmBld("None of the selected devices support volume control", sCLRORG) } - else { - if(volMapSiz > 0 && volMapSiz < devSiz) { paragraph spanSmBld("NOTICE:
Some of the selected devices do not support volume control", sCLRORG) } - input "act_volume_change", sNUMBER, title: inTS1("Volume Level (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true - if(!showVolOnly) { - input "act_volume_restore", sNUMBER, title: inTS1("Restore Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true - } - } - } - } - } -} - -private Map getZoneVolumeMap() { - Map znMap = [:] - if((Boolean)settings.act_EchoZones_vol_per_zone) { - Map echoZones = (Map)((Map)getZones() ?: [:]).findAll { it.key in settings.act_EchoZones } - echoZones.each { String zId, Map zData -> - Map aa = [:] - aa.change = settings."act_EchoZones_${zId}_volume_change" ? settings."act_EchoZones_${zId}_volume_change" : null - aa.restore = settings."act_EchoZones_${zId}_volume_restore" ? settings."act_EchoZones_${zId}_volume_restore" : null - if(aa.keySet().size()) { znMap[zId] = aa } - } - } - return znMap -} - -def condTimePage() { - return dynamicPage(name:"condTimePage", title: sBLANK, install: false, uninstall: false) { - Boolean timeReq = (settings["cond_time_start"] || settings["cond_time_stop"]) - section(sectHead("Condition Start Time:")) { - input "cond_time_start_type", sENUM, title: inTS1("Starting at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true - if(settings.cond_time_start_type == sTIME) { - input "cond_time_start", sTIME, title: inTS1("Start time", sSTTIME), required: timeReq, submitOnChange: true - } else if((String)settings.cond_time_start_type in lSUNRISESET) { - input "cond_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true - } - } - section(sectHead("Condition Stop Time:")) { - input "cond_time_stop_type", sENUM, title: inTS1("Stopping at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true - if(settings.cond_time_stop_type == sTIME) { - input "cond_time_stop", sTIME, title: inTS1("Stop time", sSTTIME), required: timeReq, submitOnChange: true - } else if((String)settings.cond_time_stop_type in lSUNRISESET) { - input "cond_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true - } - } - } -} - -def uninstallPage() { - return dynamicPage(name: "uninstallPage", title: sBLANK, install: false , uninstall: true) { - section(sectHead("Uninstall this Action?")) { paragraph spanSmBld("This will delete this Echo Speaks Action.", sCLRORG) } - } -} - -private void updDeviceInputs() { - // log.trace "updDeviceInputs..." - List aa = (List)settings.act_EchoDevices - List devIds - Boolean updList = false - try { - updList = (aa.size() && aa[0].id != null) - devIds = aa.collect { it?.id?.toString() } - // log.debug "updList(try): ${devIds.unique()}" - } catch (ignored) { - devIds = aa.collect { it?.toString() } - } - if(updList && devIds) { - if(devModeFLD) log.debug "updList: $devIds" - app.updateSetting( "act_EchoDevices", [type: "enum", value: devIds.unique()]) - } - if(devIds) { app.updateSetting( "act_EchoDeviceList", [type: "capability", value: devIds.unique()]) } // this won't take effect until next execution -} - -static Boolean wordInString(String findStr, String fullStr) { - List parts = fullStr?.split(sSPACE)?.collect { String it -> it?.toLowerCase() } - return (findStr in parts) -} - -def installed() { - logInfo("Installed Event Received...") - state.dateInstalled = getDtNow() - if((Boolean)settings.duplicateFlag && !(Boolean)state.dupPendingSetup) { - Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") - state.dupPendingSetup = true - runIn(2, "processDuplication") - if(maybeDup) logInfo("installed found maybe a dup... ${(Boolean)settings.duplicateFlag}") - } else { - if(!(Boolean)state.dupPendingSetup) initialize() - } -} - -@Field static final String dupMSGFLD = "This action is duplicated and has not had configuration completed... Please open action and configure to complete setup..." - -def updated() { - logInfo("Updated Event Received...") - Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") - if(maybeDup) logInfo("updated found maybe a dup... ${(Boolean)settings.duplicateFlag}") - if((Boolean)settings.duplicateFlag) { - if((Boolean)state.dupOpenedByUser) { state.dupPendingSetup = false } - if((Boolean)state.dupPendingSetup){ - logInfo(dupMSGFLD) - return - } - logInfo("removing duplicate status") - settingRemove('duplicateFlag'); settingRemove('duplicateSrcId') - state.remove('dupOpenedByUser'); state.remove('dupPendingSetup'); state.remove('badMode') - } - initialize() -} - -def initialize() { - logInfo("Initialize Event Received...") - if((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup){ - logInfo(dupMSGFLD) - return - } - Boolean multiConds = multipleConditions() - if(!multiConds && (Boolean)settings.cond_require_all) { settingUpdate("cond_require_all", sFALSE, sBOOL) } - - updDeviceInputs() - -// TODO go away at some point -// convert old alarm / hsm settings to new; as attribute 'alarm' belongs to capability.alarm TODO this will go away if we offer alarm attribute - if(settings.trig_alarm && !settings.trig_alarmSystemStatus){ - settingUpdate("trig_alarmSystemStatus", settings.trig_alarm, sENUM) - if(settings.trig_alarm_events && !settings.trig_alarmSystemStatus_events){ - settingUpdate("trig_alarmSystemStatus_events", settings.trig_alarm_events, sENUM) - } - settingRemove("trig_alarm") - settingRemove("trig_alarm_events") - - List tl = [sNUMBER, sBOOL, sENUM, sBOOL, sNUMBER, sNUMBER, sNUMBER, sTEXT, sTEXT] - Integer i = 0 - ["wait", "all", "cmd", "once", "after", "after_repeat", "after_repeat_cnt", "txt", "after_repeat_txt" ]?.each { ei-> - if(settings."trig_alarm_${ei}") settingUpdate("trig_alarmSystemStatus_${ei}".toString(), settings."trig_alarm_${ei}", tl[i]) - settingRemove("trig_alarm_${ei}".toString()) - i++ - } - } - if(settings.cond_alarm && !settings.cond_alarmSystemStatus){ - settingUpdate("cond_alarmSystemStatus", settings.cond_alarm, sENUM) - settingRemove("cond_alarm") - } -// convert carbon to attribute - if(valTrigEvt('carbon')) { - List a = (List)settings.triggerEvents - a.remove(a.indexOf('carbon')) - a.push('carbonMonoxide') - settingUpdate("triggerEvents", a, sENUM) - } - - unsubscribe() - unschedule() - state.isInstalled = true - state.afterEvtCheckWatcherSched = false - atomicState.tierSchedActive = false - updAppLabel() - if(advLogsActive()) { logsEnabled() } - runIn(3, "actionCleanup") - if(!isPaused(true)) { - runIn(7, "subscribeToEvts") - runEvery1Hour("healthCheck") - if(settings.enableWebCoRE){ - remTsVal(sLASTWU) - webCoRE_init() - } - updConfigStatusMap() - resumeTierJobs() - } -} - -private void processDuplication() { - String al = (String)app?.getLabel() - String newLbl = "${al}${al?.contains(" (Dup)") ? "" : " (Dup)"}" - app?.updateLabel(newLbl) - state.dupPendingSetup = true - - String dupSrcId = settings.duplicateSrcId ? (String)settings.duplicateSrcId : sNULL - Map dupData = parent?.getChildDupeData("actions", dupSrcId) - if(devModeFLD) log.debug "dupData: ${dupData}" - if(dupData && dupData.state?.size()) { - dupData.state.each { String k,v-> state[k] = v } - } - - if(dupData && dupData.settings?.size()) { - dupData.settings.each { String k, Map v-> - if((String)v.type in [sENUM, sMODE]) { - settingRemove(k) - settingUpdate(k, (v.value != null ? v.value : null), (String)v.type) - } - } - } - - parent.childAppDuplicationFinished("actions", dupSrcId) - logInfo("Duplicated Action has been created... Please open action and configure to complete setup...") -} - -static 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 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 if(obj instanceof Date) {return "Date"} - else { return "unknown"} -} - -String getActionName() { return (String)settings.appLbl } - -private void updAppLabel() { - String newLbl = "${(String)settings.appLbl} (A${isPaused(true) ? " ${sPAUSESymFLD}" : sBLANK})".replaceAll(/ (Dup)/, sBLANK).replaceAll("\\s"," ") - if((String)settings.appLbl && (String)app?.getLabel() != newLbl) { app?.updateLabel(newLbl) } -} - -private void updConfigStatusMap() { - Map sMap = [:] - sMap.triggers = triggersConfigured() - sMap.conditions = conditionsConfigured() - sMap.actions = executionConfigured() - sMap.tiers = isTierActConfigured() - state.configStatusMap = sMap -} - -Boolean getConfStatusItem(String item) { - switch(item) { - case "tiers": return isTierActConfigured() - case "actions": return executionConfigured() - } - return (state.configStatusMap?.containsKey(item) && state.configStatusMap[item] == true) -} - -@SuppressWarnings('GroovyFallthrough') -private void actionCleanup() { - stateMapMigration() - // State Cleanup - - // keep actionExecMap configStatusMap schedTrigMap - List items = ["afterEvtMap", "afterEvtChkSchedMap", "actTierState", "tierSchedActive", "zoneStatusMap", sLRM] - updMemStoreItem("afterEvtMap", [:]) -// updMemStoreItem("afterEvtChkSchedMap", [:]) - updMemStoreItem("actTierState", [:]) - [sACT, sACT_START, sACT_STOP].each { String it -> items.push((it+sLRM)) } // cleanup light_restore_map - items.each { String si-> if(state.containsKey(si)) { state.remove(si)} } - - //convert old style input to new - [sACT, sACT_START, sACT_STOP].each { String it -> - String a=it+"lights_color" - String a1=it+"lights_colort" - if(settings."${a}") { - settingUpdate(a1, settings."${a}", sTEXT) - settingRemove(a) - } - String b=it+"lights_level" - String b1=it+"lights_leveln" - if(settings."${b}") { - settingUpdate(b1, settings."${b}", sNUMBER) - settingRemove(b) - } - } - - //Cleans up unused action setting items - List setItems = [] - List setIgn = ["act_delay", "act_volume_change", "act_volume_restore", "act_tier_cnt", "act_switches_off", "act_switches_on", "act_piston_run", "act_mode_run", "act_alarm_run"] - if(settings.act_EchoZones) { setIgn.push("act_EchoZones"); Map a = getActiveZones() } // to fill in FLD - else if(settings.act_EchoDevices) { setIgn.push("act_EchoDevices"); setIgn.push("act_EchoDeviceList") } - - tierItemCleanup() - if((String)settings.actionType) { - Boolean isTierAct = isTierAction() - ["act_lights", "act_locks", "act_securityKeypads", "act_doors", "act_sirens"].each { String it -> - ((Map)settings).each { sI -> - String k = (String)sI.key - if(k.startsWith(it)) { - isTierAct ? setItems.push(k) : setIgn.push(k) } - } - } - [sACT_START, sACT_STOP].each { String it -> - ((Map)settings).each { sI -> - String k = (String)sI.key - if(k.startsWith(it)) { - isTierAct ? setIgn.push(k) : setItems.push(k) } - } - } - ((Map)settings).each { si-> - String k = (String)si.key - if(!(k in setIgn) && k.startsWith(sACT) && !k.startsWith("act_${(String)settings.actionType}") && (!isTierAct && k.startsWith(sACT_ITEM))) { setItems.push(k) } - } - } - - // Cleanup Unused Condition settings... - List condKeys = ((Map)settings).findAll { it -> - String k = (String)it.key - k?.startsWith("cond_") && !(k?.tokenize("_")[1] in ['require']) }?.keySet()?.collect { ((String)it)?.tokenize("_")[1] }?.unique() - if(devModeFLD) log.debug("checking settings condition keys $condKeys" ) - if(condKeys?.size()) { - condKeys.each { String ck-> - if(!settings."cond_${ck}") { - if(devModeFLD) log.debug("found setting to cleanup: $ck") - setItems.push("cond_${ck}") - ["cmd", "all", "low", "high", "equal", "avg", "nums"].each { String ei-> - setItems.push("cond_${ck}_${ei}") - } - } - } - } - if(setItems) logDebug("settings to clean setItems: $setItems") - - // Cleanup Unused Trigger Types... - if((List)settings.triggerEvents) { - List trigKeys = ((Map)settings).findAll { it -> - String k = (String)it.key - k?.startsWith(sTRIG) && !(k?.tokenize("_")[1] in (List)settings.triggerEvents) }?.keySet()?.collect { ((String)it)?.tokenize("_")[1] }?.unique() - // log.debug "trigKeys: $trigKeys" - if(trigKeys?.size()) { - trigKeys.each { String tk-> - setItems.push("trig_${tk}") - ["events", "wait", "all", "avg", "cmd", "low", "high", "equal", "once", "after", "txt", "nums", "after_repeat", "after_repeat_cnt", "after_repeat_txt", "Codes"].each { String ei-> - setItems.push("trig_${tk}_${ei}") - } - } - } - } - - // Cleanup Unused Schedule Trigger Items - setItems = setItems + ["trig_scheduled_sunState"] - if(!settings.trig_scheduled_type) { - setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_type", "trig_scheduled_recurrence", "trig_scheduled_time", "trig_scheduled_weekdays", "trig_scheduled_weeks"] - } else { - switch(settings.trig_scheduled_type) { - case sONETIME: - case sCSUNRISE: - case sCSUNSET: - setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_recurrence", "trig_scheduled_weekdays", "trig_scheduled_weeks"] - if((String)settings.trig_scheduled_type in [sCSUNSET, sCSUNRISE]) { setItems.push("trig_scheduled_time") } - else setItems = setItems + ["trig_scheduled_sunState_offset"] - break - case sRECURRING: - switch(settings.trig_scheduled_recurrence) { - case "Daily": - setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_weeks"] - break - case "Weekly": - setItems.push("trig_scheduled_daynums") - break - case "Monthly": - setItems.push("trig_scheduled_weekdays") - if(settings.trig_scheduled_daynums && settings.trig_scheduled_weeks) { setItems.push("trig_scheduled_weeks") } - break - } - break - } - } - - ((Map)settings).each { si-> - String k = (String)si.key - ["broadcast", "musicTest", "announce", "sequence", "speechTest"].each { String it -> if (k?.startsWith(it)) setItems.push(k) } - // if(k?.startsWith("broadcast") || k?.startsWith("musicTest") || k?.startsWith("announce") || k?.startsWith("sequence") || k?.startsWith("speechTest")) { setItems.push(k) } } - } - if(state.webCoRE) { state.remove("webCoRE") } - if(!settings.enableWebCoRE) { setItems.push("webCorePistons"); setItems.push("act_piston_run") } - setItems = setItems + ["tuneinSearchQuery", "usePush", "smsNumbers", "pushoverSound", "pushoverDevices", "pushoverEnabled", "pushoverPriority", "alexaMobileMsg", "appDebug"] - // Performs the Setting Removal - // log.debug "setItems: $setItems" - setItems.unique()?.each { String sI-> if(settings.containsKey(sI)) { settingRemove(sI) } } -} - -Boolean isPaused(Boolean chkAll = false) { return (Boolean)settings.actionPause && (chkAll ? !(Boolean)state.dupPendingSetup : true) } - -public void triggerInitialize() { runIn(3, "initialize") } - -private Boolean valTrigEvt(String key) { return (key in (List)settings.triggerEvents) } - -public void updatePauseState(Boolean pause) { - if((Boolean)settings.actionPause != pause) { - logDebug("Received Request to Update Pause State to (${pause})") - settingUpdate("actionPause", "${pause}", sBOOL) - runIn(4, "updated") - } -} - -private healthCheck() { - if(isTrc())logTrace("healthCheck") - if(advLogsActive()) { logsDisable() } - if(settings.enableWebCoRE) webCoRE_poll() -} - -String cronBuilder() { - /**** - Cron Expression Format: ( ) - - * (all) – it is used to specify that event should happen for every time unit. For example, “*” in the field – means “for every minute” - ? (any) – it is utilized in the and fields to denote the arbitrary value – neglect the field value. For example, if we want to fire a script at “5th of every month” irrespective of what the day of the week falls on that date, then we specify a “?” in the field - – (range) – it is used to determine the value range. For example, “10-11” in field means “10th and 11th hours” - , (values) – it is used to specify multiple values. For example, “MON, WED, FRI” in field means on the days “Monday, Wednesday, and Friday” - / (increments) – it is used to specify the incremental values. For example, a “5/15” in the field, means at “5, 20, 35 and 50 minutes of an hour” - L (last) – it has different meanings when used in various fields. For example, if it's applied in the field, then it means last day of the month, i.e. “31st for January” and so on as per the calendar month. It can be used with an offset value, like “L-3“, which denotes the “third to last day of the calendar month”. In the , it specifies the “last day of a week”. It can also be used with another value in , like “6L“, which denotes the “last Friday” - W (weekday) – it is used to specify the weekday (Monday to Friday) nearest to a given day of the month. For example, if we specify “10W” in the field, then it means the “weekday near to 10th of that month”. So if “10th” is a Saturday, then the job will be triggered on “9th”, and if “10th” is a Sunday, then it will trigger on “11th”. If you specify “1W” in the and if “1st” is Saturday, then the job will be triggered on “3rd” which is Monday, and it will not jump back to the previous month - # – it is used to specify the “N-th” occurrence of a weekday of the month, for example, “3rd Friday of the month” can be indicated as “6#3“ - - At 12:00 pm (noon) every day during the year 2017: (0 0 12 * * ? 2017) - - Every 5 minutes starting at 1 pm and ending on 1:55 pm and then starting at 6 pm and ending at 6:55 pm, every day: (0 0/5 13,18 * * ?) - - Every minute starting at 1 pm and ending on 1:05 pm, every day: (0 0-5 13 * * ?) - - At 1:15 pm and 1:45 pm every Tuesday in the month of June: (0 15,45 13 ? 6 Tue) - - At 9:30 am every Monday, Tuesday, Wednesday, Thursday, and Friday: (0 30 9 ? * MON-FRI) - - At 9:30 am on 15th day of every month: (0 30 9 15 * ?) - - At 6 pm on the last day of every month: (0 0 18 L * ?) - - At 6 pm on the 3rd to last day of every month: (0 0 18 L-3 * ?) - - At 10:30 am on the last Thursday of every month: (0 30 10 ? * 5L) - - At 6 pm on the last Friday of every month during the years 2015, 2016 and 2017: (0 0 18 ? * 6L 2015-2017) - - At 10 am on the third Monday of every month: (0 0 10 ? * 2#3) - - At 12 am midnight on every day for five days starting on the 10th day of the month: (0 0 0 10/5 * ?) - ****/ - String cron = sNULL - //List schedTypes = [sONETIME, sRECURRING, sCSUNRISE, sCSUNSET] - String schedType = (String)settings.trig_scheduled_type - Boolean recur = (schedType == sRECURRING) - def time = settings.trig_scheduled_time ?: null - List dayNums = recur && settings.trig_scheduled_daynums ? settings.trig_scheduled_daynums?.collect { it as Integer }?.sort() : null -// List weekNums = recur && settings.trig_scheduled_weeks ? settings.trig_scheduled_weeks?.collect { it as Integer }?.sort() : null - List monthNums = recur && settings.trig_scheduled_months ? settings.trig_scheduled_months?.collect { it as Integer }?.sort() : null - if(time) { - String z = "0" - String hour = fmtTime(time, "HH") ?: z - String minute = fmtTime(time, "mm") ?: z - //String second = fmtTime(time, "ss") ?: z - String second = z - String daysOfWeek = settings.trig_scheduled_weekdays ? ((List)settings.trig_scheduled_weekdays)?.join(",") : sNULL - String daysOfMonth = dayNums?.size() ? (dayNums?.size() > 1 ? "${dayNums?.first()}-${dayNums?.last()}" : dayNums[0]) : sNULL - //String weeks = (weekNums && !dayNums) ? weekNums?.join(",") : sNULL - String months = monthNums ? monthNums?.sort()?.join(",") : sNULL - String year = sNULL - // log.debug "hour: ${hour} | m: ${minute} | s: ${second} | daysOfWeek: ${daysOfWeek} | daysOfMonth: ${daysOfMonth} | weeks: ${weeks} | months: ${months}" - if(hour || minute || second) { - List cItems = [] - cItems.push(second ?: "*") - cItems.push(minute ?: z) - cItems.push(hour ?: z) - cItems.push(daysOfMonth ?: (!daysOfWeek ? "*" : "?")) - cItems.push(months ?: "*") - cItems.push(daysOfWeek ? daysOfWeek?.replaceAll("\"", sBLANK) : "?") - if(year) { cItems.push(" ${year}") } - cron = cItems.join(" ") - } - } - logDebug("cronBuilder | Cron: ${cron}") - return cron -} - -Boolean schedulesConfigured() { - if((String)settings.trig_scheduled_type in [sCSUNRISE, sCSUNSET]) { return true } - else if((String)settings.trig_scheduled_type == sONETIME && settings.trig_scheduled_time) { return true } - else if((String)settings.trig_scheduled_type == sRECURRING && settings.trig_scheduled_time && settings.trig_scheduled_recurrence) { - if((String)settings.trig_scheduled_recurrence == "Daily") { return true } - else if ((String)settings.trig_scheduled_recurrence == "Weekly" && (List)settings.trig_scheduled_weekdays) { return true } - else if ((String)settings.trig_scheduled_recurrence == "Monthly" && (settings.trig_scheduled_daynums || settings.trig_scheduled_weeks)) { return true } - } - return false -} - -void scheduleSunriseSet() { - if(isPaused(true)) { logWarn("Action is PAUSED... No Events will be subscribed to or scheduled....", true); return } - Map sun = getSunriseAndSunset() - Long ltim = (String)settings.trig_scheduled_type in [sCSUNRISE] ? sun.sunrise.time : sun.sunset.time - Long offset = (settings.trig_scheduled_sunState_offset ?: 0L) * 60000L // minutes - Long t = now() - Long n = ltim+offset - if(t > n) { logDebug("Not scheduling sunrise, sunset - already past today"); return } - Long secs = Math.round((n - t)/1000.0D) + 1L - runIn(secs, "scheduleTrigEvt") - Date tt = new Date(n) - logDebug("Setting Schedule for ${epochToTime(tt)} in $secs's") -} - -void subscribeToEvts() { - state.handleGuardEvents = false - if(minVersionFailed()) { logError("CODE UPDATE required to RESUME operation. No events will be monitored.", true); return } - if(isPaused(true)) { logWarn("Action is PAUSED... No Events will be subscribed to or scheduled....", true); return } - ((List)settings.triggerEvents)?.each { String te-> - if(te == sSCHED || settings."trig_${te}") { - switch (te) { - case sSCHED: - // Scheduled Trigger Events - if (schedulesConfigured()) { - if((String)settings.trig_scheduled_type in [sCSUNRISE, sCSUNSET]) { - scheduleSunriseSet() - schedule('29 0 0 1/1 * ? * ', "scheduleSunriseSet") // run at 00:00:29 every day - } - else if((String)settings.trig_scheduled_type in [sONETIME, sRECURRING] && settings.trig_scheduled_time) { schedule(cronBuilder(), "scheduleTrigEvt") } - } - break - case "guard": - // Alexa Guard Status Events - if(settings.trig_guard) state.handleGuardEvents = true - break - case sALRMSYSST: - // Location Alarm Events - subscribe(location, "hsmStatus", alarmEvtHandler) - if("alerts" in (List)settings.trig_alarmSystemStatus) { subscribe(location, "hsmAlert", alarmEvtHandler) } // Only on Hubitat - break - case sMODE: - // Location Mode Events - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } - subscribe(location, sMODE, modeEvtHandler) - break - case sPISTNEXEC: - break - case sTHERMTEMP: - if (settings."trig_${te}_cmd") subscribe(settings."trig_${te}", sTEMP, getThermEvtHandlerName(te)) - break - default: - // Handles Remaining Device Events - subscribe(settings."trig_${te}", te, getDevEvtHandlerName(te)) - break - } - } - } -} - -private getDevEvtHandlerName(String type) { - return (type && (Integer)settings."trig_${type}_after"!=null) ? "devAfterEvtHandler" : "deviceEvtHandler" -} - -private getThermEvtHandlerName(String type) { - return (type && (Integer)settings."trig_${type}_after"!=null) ? "devAfterThermEvtHandler" : "thermostatEvtHandler" -} - -private executeActTest() { - logTrace("executeActTest STARTING") - try { - if((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT, sANN, sANNI, sANNT, sANNIT, sWEATH, "builtin", "calendar"]) { - Map evt = getRandomTrigEvt() - if(!evt) logWarn("no random event found") - else { - String te = (String)evt.name - - switch (te) { - case sSCHED: - // Scheduled Trigger Events - scheduleTrigEvt([date: new Date(), name: "test", value: "Stest", displayName: "Schedule Test"]) - break - case "webCoRE": // sPISTNEXEC: - webCoRE_handler(evt) - break - case sTHERMTEMP: - String hand = getThermEvtHandlerName(te) - evt.name = sTEMP - "${hand}"(evt) - break - case "guard": - // Alexa Guard Status Events - guardEventHandler((String)evt.value) - break - case sALRMSYSST: - // Location Alarm Events - alarmEvtHandler(evt) - // two cases hsmStatus (above) and hsmAlert TODO - //if("alerts" in (List)settings.trig_alarmSystemStatus) { subscribe(location, "hsmAlert", alarmEvtHandler) } // Only on Hubitat - break - case sMODE: - // Location Mode Events - modeEvtHandler(evt) - break - default: - String hand = getDevEvtHandlerName(te) - // Handles Remaining Device Events - "${hand}"(evt) - break - } - } - } else logWarn("TEST of Action type ${settings.actionType} SKIPPED") - } catch (ignored) {} - settingUpdate("actTestRun", sFALSE, sBOOL) - logTrace("executeActTest ENDING") -} - -Map getRandomTrigEvt() { - String trig = (String)getRandomItem((List)settings.triggerEvents) - List trigItems = settings."trig_${trig}" ?: null - def randItem = trigItems?.size() ? getRandomItem(trigItems) : null - def trigItem = randItem ? (randItem instanceof String ? [displayName: null, id: null] : - (trigItems?.size() ? trigItems?.find { it?.id?.toString() == randItem?.id?.toString() } : [displayName: null, id: null])) : null - if(devModeFLD) log.debug("trig: ${trig} | trigItem: ${trigItem} | ${trigItem?.displayName} | ${trigItem?.id} | trigItems: ${trigItems}") - Map evt = [ date: new Date(), name: trig ] - if( !(trig in [sSCHED]) ) { - Boolean isC = (getTemperatureScale()=="C") - Map attVal = [ - (sSWITCH): getRandomItem(lONOFF), - door: getRandomItem(lOPNCLS+["opening", "closing"]), - (sCONTACT): getRandomItem(lOPNCLS), - acceleration: getRandomItem(lACTINACT), - (sLOCK): getRandomItem(lLOCKUNL +["unlocked with timeout"]), - securityKeypad: getRandomItem(lSEC), - (sWATER): getRandomItem(lWETDRY), - presence: getRandomItem(lPRES), - (sMOTION): getRandomItem(lACTINACT), - (sVALVE): getRandomItem(lOPNCLS), - windowShade: getRandomItem(lOPNCLS+["opening", "closing"]), - (sPUSHED): getRandomItem([sPUSHED]), - (sRELEASED): getRandomItem([sRELEASED]), - (sHELD): getRandomItem([sHELD]), - (sDBLTAP): getRandomItem([sDBLTAP]), - (sSMOKE): getRandomItem(lDETECTCLR), - carbonMonoxide: getRandomItem(lDETECTCLR), - (sTEMP): isC ? getRandomItem(-1..29) : getRandomItem(30..80), - illuminance: getRandomItem(1..100), - (sHUMID): getRandomItem(1..100), - (sLEVEL): getRandomItem(1..100), - (sBATT): getRandomItem(1..100), - (sPOWER): getRandomItem(100..3000), - (sMODE): getRandomItem((List)location?.modes), - (sALRMSYSST): getRandomItem(getAlarmTrigOpts()?.collect {(String)it.key}), - guard: getRandomItem(["ARMED_AWAY", "ARMED_STAY"]), - (sTHERMTEMP): isC ? getRandomItem(10..33) : getRandomItem(50..90), - (sCOOLSP): isC ? getRandomItem(10..33) : getRandomItem(50..90), - (sHEATSP): isC ? getRandomItem(10..33) : getRandomItem(50..90), - (sTHERMMODE): getRandomItem(getThermModeOpts()), - (sTHERMFM): getRandomItem(getThermFanOpts()), - (sTHERMOS): getRandomItem(getThermOperStOpts()), - ] - if(settings.enableWebCoRE && trig == sPISTNEXEC) { - attVal.webCoRE = sPISTNEXEC - trig = 'webCoRE' - trigItem = [:] - trigItem.displayName = 'webcore piston executed' - String id = getRandomItem(getLocationPistons()) - evt.name=trig - evt = evt + [ jsonData: [id: id, name: getPistonById(id) ] ] - } - if(attVal.containsKey(trig)) { - evt = evt + [ displayName: trigItem?.displayName ?: sBLANK, value: attVal[trig], type: "digital", device: [id: trigItem?.id?.toString() ?: null ] ] - } else evt = null - } else { evt.name=sSCHED } - - if(devModeFLD) log.debug "getRandomTrigEvt | trig: ${trig} | Evt: ${evt}" - return evt -} - -@Field volatile static Map zoneStatusMapFLD = [:] - -def zoneStateHandler(evt) { - // TODO This is here as placeholder to prevent flooding the logs with errors after upgrading to v4.0 -} - -def zoneRemovedHandler(evt) { - // TODO This is here as placeholder to prevent flooding the logs with errors after upgrading to v4.0 -} - -public updZones(Map zoneMap) { - String myId=app.getId() - zoneStatusMapFLD[myId] = zoneMap - zoneStatusMapFLD = zoneStatusMapFLD -} - -public Map getZones(Boolean noCache = false) { - String myId=app.getId() - Map a = zoneStatusMapFLD[myId] - if(noCache || !a) { - a = parent.getZones(true) - } - String i = 'initialized' - if(a.containsKey(i))a.remove(i) - return a -} -/* -private Map getZoneState(Integer znId) { - Map zones = getZones() - if(zones) { - return zones[znId] - } - return null -} */ - -public Map getActiveZones() { - Map t0 = getZones() - Map zones = t0 ?: [:] - return zones.size() ? zones.findAll { it?.value?.active == true && !it?.value?.paused } : [:] -} - -public List getActiveZoneNames() { - Map t0 = getActiveZones() - Map zones = t0 ?: [:] - return zones.size() ? zones.collect { (String)it?.value?.name } : [] -} - -/*********************************************************************************************************** - EVENT HANDLER FUNCTIONS -************************************************************************************************************/ - -def scheduleTrigEvt(ievt=null) { - Map evt=ievt - if(!evt) { - Date adate= new Date() - String dt= dateTimeFmt(adate, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") - evt= [name: sSCHED, displayName: "Scheduled Trigger", value: fmtTime(dt), date: adate, device: [id: null]] - } - Long evtDelay = now() - ((Date)evt.date).getTime() - if(isTrc())logTrace("${(String)evt.name} Event | Device: ${(String)evt.displayName} | Value: (${strCapitalize(evt.value)}) with a delay of ${evtDelay}ms") - if (!schedulesConfigured()) { return } - String schedType = (String)settings.trig_scheduled_type - Boolean recur = schedType == sRECURRING - Map dateMap = getDateMap() - // log.debug "dateMap: $dateMap" - String srecur = recur ? (String)settings.trig_scheduled_recurrence : sNULL - List days = recur ? (List)settings.trig_scheduled_weekdays : null - List daynums = recur ? (List)settings.trig_scheduled_daynums : null - List weeks = recur ? (List)settings.trig_scheduled_weeks : null - List months = recur ? (List)settings.trig_scheduled_months : null - - getTheLock(sHMLF, "scheduleTrigEvt") - - Map sTrigMap = (Map)getMemStoreItem("schedTrigMap", [:]) - if(!sTrigMap) sTrigMap = (Map)state.schedTrigMap ?: [:] - - Boolean dChk = (sTrigMap?.lastRun?.day != dateMap.day) || (sTrigMap?.lastRun?.year != dateMap.year) || (sTrigMap?.lastRun?.month != dateMap.month) - - Boolean dOk = (srecur in ["Daily"]) ? (sTrigMap?.lastRun?.dayName != dateMap.dayName) : true - Boolean wdOk = (days && srecur in ["Daily", "Weekly"]) ? (dateMap.dayNameShort in days) : true - Boolean mdOk = daynums ? (dateMap.day in daynums) : true - Boolean wOk = (weeks && srecur in ["Weekly"]) ? (dateMap.week in weeks && sTrigMap?.lastRun?.week != dateMap.week) : true - Boolean mOk = (months && srecur in ["Weekly", "Monthly"]) ? (dateMap.month in months && sTrigMap?.lastRun?.month != dateMap.month) : true - Boolean ok = (dOk && wdOk && mdOk && wOk && mOk && dChk) - if(ok) { - sTrigMap.lastRun = dateMap - updMemStoreItem("schedTrigMap", sTrigMap) - state.schedTrigMap = sTrigMap - } - - releaseTheLock(sHMLF) - eventCompletion(evt, ok, false, null, "scheduleTrigEvt", evt?.value, (String)evt?.displayName) - if(!ok) logDebug("scheduleTrigEvt | SKIPPING | Comparison to lastrun: dateChange: $dChk | dayOk: $dOk | dayOfWeekOk: $wdOk | dayOfMonthOk: $mdOk | weekOk: $wOk | monthOk: $mOk") -} - -@SuppressWarnings('GroovyFallthrough') -def alarmEvtHandler(evt) { - Long evtDelay = now() - (Long)((Date)evt.date).getTime() - String eN = (String)evt.name - def eV = evt.value - if(isTrc())logTrace("${eN} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms") - String inT = sTRIG+sALRMSYSST - List lT = (List)settings."${inT}" - List lE = (List)settings."${inT}_events" - Boolean ok2Run = !!(lT) - if(ok2Run) { - switch(eN) { - case "hsmStatus": - case sALRMSYSST: - if(!(eV in lT)) ok2Run = false - break - case "hsmAlert": - if (!("alerts" in lT && eV in lE)) ok2Run = false - break - default: - ok2Run = false - } - } - Boolean dco = !!(Boolean)settings."${inT}_once" - Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null - eventCompletion(evt, ok2Run, dco, dcw, "alarmEvtHandler(${eN})", eV, (String)evt?.displayName) - if(!ok2Run) logDebug("alarmEvtHandler | Skipping event ${eN} value: ${eV}, did not match ${lT} ${lE}") -} - -public guardEventHandler(String guardState) { - Map evt = [name: "guard", displayName: "Alexa Guard", value: guardState, date: new Date(), device: [id: null]] - if(isTrc())logTrace("${evt.name} Event | Device: ${evt.displayName} | Value: (${strCapitalize(evt.value)})") - Boolean ok= ((Boolean)state.handleGuardEvents && settings.trig_guard && (sANY in (List)settings.trig_guard || guardState in (List)settings.trig_guard)) - eventCompletion(evt, ok, false, null, "guardEventHandler", guardState, (String)evt.displayName) - if(!ok) logDebug("guardEventHandler | Skipping event guard value: ${guardState}, did not match ${state.handleGuardEvents} ${settings.trig_guard}") -} - -void eventCompletion(evt, Boolean ok2Run, Boolean dco, Integer dcw, String meth, evtVal, - String evtDis, Boolean aftRepEvt=false, Boolean allDevsResp=false) { - Boolean evtWaitOk = ((dco || dcw!=null) ? evtWaitRestrictionOk([date: evt?.date, device: evt?.device, value: evtVal, name: evt?.name, displayName: evtDis], dco, dcw, aftRepEvt) : true) - Boolean ok = evtWaitOk && ok2Run - if(isDbg()) logDebug(meth+" | execOk: ${ok} | ok2Run :${ok2Run} | evtWaitOk: ${evtWaitOk}") - if(getConfStatusItem("tiers")) { - processTierTrigEvt(evt, ok) - } else { if(ok) executeAction(evt, meth, allDevsResp, aftRepEvt) } - if (!ok) logTrace(meth+" | Skipping event ${evtDis} ${evt?.name} value: ${evtVal} | ok2Run :${ok2Run} | evtWaitOk: ${evtWaitOk}") -} - -def webcoreEvtHandler(evt) { - String eN = (String)evt.name - def eV = evt.value - String disN = evt.jsonData?.name - String pId = evt.jsonData?.id - if(isTrc())logTrace("${eN.toUpperCase()} Event | Piston: ${disN} | pistonId: ${pId} | with a delay of ${now() - (Long)((Date)evt.date).getTime()}ms") - String inT = sTRIG+sPISTNEXEC - List lT = (List)settings."${inT}" - Boolean ok = (pId in lT) - Boolean dco = !!(Boolean)settings."${inT}_once" - Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null - eventCompletion(evt, ok, dco, dcw, "webcoreEvtHandler", disN, disN) - if(!ok) logTrace("webcoreEvtHandler | Skipping event ${eN} value: ${eV}, ${pId} did not match ${lT}") -} - -def modeEvtHandler(evt) { - String eN = (String)evt.name - def eV = evt.value - if(isTrc())logTrace("${eN.toUpperCase()} Event | Mode: (${strCapitalize(eV)}) with a delay of ${now() - (Long)((Date)evt.date).getTime()}ms") - String inT = sTRIG+sMODE - List lT = (List)settings."${inT}" - Boolean ok = (eV in lT) - Boolean dco = !!(Boolean)settings."${inT}_once" - Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null - eventCompletion(evt, ok, dco, dcw, "modeEvtHandler", eV, (String)evt?.displayName) - if(!ok) logTrace("modeEvtHandler | Skipping event ${eN} value: ${eV}, did not match ${lT}") -} - -void devAfterThermEvtHandler(evt) { - evt.name = (String)evt.name == sTEMP ? sTHERMTEMP : (String)evt.name - devAfterEvtHandler(evt) -} - -void devAfterEvtHandler(evt) { - Long evtT = (Long)((Date)evt.date).getTime() - Long now = now() - Long evtDelay = now - evtT - String eN = (String)evt?.name - def eV = evt?.value - String dc = settings."trig_${eN}_cmd" ?: null - Integer dcaf = (Integer)settings."trig_${eN}_after"!=null ? (Integer)settings."trig_${eN}_after" : 0 - Integer dcafr = (Integer)settings."trig_${eN}_after_repeat" ?: 0 - Integer dcafrc = (Integer)settings."trig_${eN}_after_repeat_cnt" ?: 0 - String eid = "${evt?.device?.id}_${eN}" - Boolean okpt1 = (dc && dcaf>=0) - Boolean okpt2 = okpt1 - if(!(eN in lDATTNUM)) { - okpt2 = (okpt1 && eV == dc) - } - String msg = "Device Event After | " - if(isTrc())logTrace(msg+"${eN?.toUpperCase()} | Name: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms | SchedCheck: (${okpt1}, ${okpt2}) dcaf: $dcaf, dcafr: $dcafr, dcafrc: $dcafrc") - - Boolean rem = false - - getTheLock(sHMLF, "scheduleTrigEvt") - - String mK = "afterEvtMap" - Map aEvtMap = (Map)getMemStoreItem(mK, [:]) - if(!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] - - if(aEvtMap.containsKey(eid) && !okpt2) { - aEvtMap.remove(eid) - rem = true - } - if(okpt2) { aEvtMap[eid] = [ - dt: formatDt((Date)evt.date), - device: [id: evt?.device?.id as String], - displayName: evt?.displayName, - name: eN, - value: eV, - type: evt?.type, - data: evt?.data, - triggerState: dc, // desired comparison value for non-numbers; or comparison type for number - nextT: now + (dcaf*1000L), - wait: dcaf, - //timeLeft: dcaf, - isRepeat: false, - repeatWait: dcafr ?: null, - repeatCnt: 0, - repeatCntMax: dcafrc ] - } - - updMemStoreItem(mK, aEvtMap) - state[mK] = aEvtMap - Integer sz = aEvtMap.size() - - releaseTheLock(sHMLF) - - if(rem) logDebug(msg+"Removing ${evt?.displayName} from AfterEvtCheckMap | Reason: (${eN?.toUpperCase()}) no longer has the state of (${dc}) | Remaining Items: (${sz})") - - if(okpt2) { - String msg1 = msg+"queued event | " - Boolean doRun=true - if (dcaf == 0) { - logTrace(msg1+"Running afterEvent") - afterEvtCheckHandler() - doRun = false - } - if(doRun) { - Integer ssec = dcaf >= 0 ? dcaf : 2 - runIn(ssec, "afterEvtCheckHandler") - logTrace(msg1+"Scheduled afterEvent in ${ssec} seconds") - } - // TODO remove - if(!(Boolean)state.afterEvtCheckWatcherSched) { - state.afterEvtCheckWatcherSched = true - logTrace(msg+"Scheduling afterEvtCheckWatcher...") - runEvery5Minutes("afterEvtCheckWatcher") - } - } else { - logDebug(msg+" did not queue | Passing to deviceEvtHandler") - deviceEvtHandler(evt, true, false) - } -} - -void afterEvtCheckHandler() { - unschedule("afterEvtCheckHandler") - updTsVal("lastAfterEvtCheck") - String meth = "afterEvtCheckHandler" - String msg = meth+" | " - logTrace(msg) - - getTheLock(sHMLF, meth) - Boolean hasLock = true - - String mK = "afterEvtMap" - Map aEvtMap = (Map)getMemStoreItem(mK, [:]) - if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] - - Long mnow = (Long)now() + 750L // anything in next 750ms runs now - Map newMap = aEvtMap.findAll { it -> (Long)it?.value?.nextT < mnow } - List sortList = newMap.collect { (Long)it?.value?.nextT }?.sort() - - sortList?.each { Long lit -> - if (!hasLock) { - getTheLock(sHMLF, meth) - hasLock = true - - aEvtMap = (Map)getMemStoreItem(mK, [:]) - if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] - } - def nextItem = aEvtMap.find { eM -> (Long)eM?.value?.nextT == lit } - Map nextVal = (Map)nextItem?.value ?: null - String eN = (String)nextVal.name - String edisplayN = (String)nextVal.displayName - String edId = (String)nextVal.device.id - String nextId = (edId && eN) ? "${edId}_${eN}" : sNULL - Boolean remEvt - Boolean runEvt = false - String msg1 = "Evaluating ${edisplayN} | (${eN?.capitalize()}) | Original Event Time: ${nextVal.dt} | " - String msg2 - if (nextVal && nextId && aEvtMap[nextId]) { - def devs = settings."trig_${eN}" ?: null - - def newVal = null // could be number or string - if (devs && eN) { - def dev = devs.find { it?.id?.toString() == edId } - String en = eN == sTHERMTEMP ? sTEMP : eN - if (dev) newVal = dev?.currentValue(en) - if (newVal != nextVal.value) msg1 = msg1 + "value changed old: ${nextVal.value} new: ${newVal} | " - } - Long origD = parseDate(nextVal.dt).getTime() - Map evt = [ - date : new Date(), - device : [id: edId], - displayName: edisplayN, - name : eN, - value : newVal,// ?: nextVal.value, - type : nextVal.type, - data : nextVal.data, - totalDur : (now() - origD)/1000 - ] - - Boolean skipEvt = true - if(eN in lDATTNUM) { - String dc = settings."trig_${eN}_cmd" // desired comparison for numbers - Boolean dca = !!(Boolean)settings."trig_${eN}_all" - Boolean dcavg = (!dca && !!(Boolean)settings."trig_${eN}_avg") - Double dcl = settings."trig_${eN}_low" - Double dch = settings."trig_${eN}_high" - Double dce = settings."trig_${eN}_equal" - Map valChk = deviceEvtProcNumValue(evt, devs, dc, dcl, dch, dce, dca, dcavg) - skipEvt = !(Boolean)valChk.evtOk - //evtAd = valChk.evtAd - } else { - if (nextVal.triggerState && edId && eN && devs) { - skipEvt = !devAttValEqual(devs, edId, eN, nextVal.triggerState) - } - } - - Boolean hasRepeat = ((Integer)settings."trig_${eN}_after_repeat" != null) - Boolean isRepeat = (Boolean)nextVal.isRepeat ?: false - - if (!skipEvt) { - runEvt = true - - if(hasRepeat) { - aEvtMap[nextId].nextT = now() + ((Integer)nextVal.repeatWait * 1000L) - updMemStoreItem(mK, aEvtMap) - state[mK] = aEvtMap - } - - if (isRepeat && hasRepeat) { - Integer repeatCnt = ((Integer)nextVal.repeatCnt >= 0) ? (Integer)nextVal.repeatCnt + 1 : 1 - Integer repeatCntMax = (Integer)nextVal.repeatCntMax ?: null - remEvt = (repeatCntMax && (repeatCnt == repeatCntMax)) - aEvtMap[nextId].repeatCnt = repeatCnt - updMemStoreItem(mK, aEvtMap) - state[mK] = aEvtMap - msg2 = "Issuing Repeat Event | RepeatWait: ${nextVal?.repeatWait} | isRepeat: ${isRepeat} | RepeatCnt: ${repeatCnt} | RepeatCntMax: ${repeatCntMax} " - if (remEvt) msg2 = "has repeated ${repeatCntMax} times | " + msg2 - - } else { // this is after - aEvtMap[nextId].isRepeat = true - updMemStoreItem(mK, aEvtMap) - state[mK] = aEvtMap - remEvt = !hasRepeat - msg2 = "Wait Threshold (${nextVal.wait} sec) Reached for ${edisplayN} (${eN?.capitalize()}) | Issuing held event | TriggerState: (${nextVal.triggerState}) | hasRepeat: ${hasRepeat} " - } - } else { - remEvt = true - msg2 = "state is no longer ${nextVal.triggerState} | Skipping Action... " - } - if (remEvt) { - aEvtMap.remove(nextId) - updMemStoreItem(mK, aEvtMap) - state[mK] = aEvtMap - msg2 = msg2 + "Removed event" - } - - releaseTheLock(sHMLF) - hasLock = false - - if(isDbg())logDebug(msg + msg1 + msg2) - if(runEvt) - deviceEvtHandler(evt, true, isRepeat) - } - } - - if (!hasLock) { - getTheLock(sHMLF, meth) - hasLock = true - - aEvtMap = (Map)getMemStoreItem(mK, [:]) - if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] - } - Integer sz = aEvtMap.size() - if (sz > 0) { - //newMap = aEvtMap.findAll { it -> it?.value?.nextT < now } - sortList = aEvtMap.collect { (Long)it?.value?.nextT }?.sort() - releaseTheLock(sHMLF) - hasLock = false - - Long myL = sortList.size() > 0 ? sortList[0] : null - if (myL != null) { - Integer ssecs = (myL - now() + 600L)/1000L - ssecs = ssecs > 1 ? Math.min(ssecs, 300).toInteger() : 2 - runIn(ssecs, meth) - logTrace(msg+"Scheduled afterEvent in ${ssecs} seconds; afterEvtMap: ${sz}") - } else logWarn(msg+"INCONSISTENT") - - } else { - releaseTheLock(sHMLF) - hasLock = false - clearAfterCheckSchedule() - } -} - -// TODO remove -Integer getLastAfterEvtCheck() { return getLastTsValSecs("lastAfterEvtCheck") } - -void afterEvtCheckWatcher() { - String meth = "afterEvtCheckWatcher" - getTheLock(sHMLF, meth) - - Map aEvtMap = (Map)getMemStoreItem("afterEvtMap", [:]) - if(!aEvtMap) aEvtMap = (Map)state.afterEvtMap ?: [:] - - releaseTheLock(sHMLF) - - if(aEvtMap.size() && getLastAfterEvtCheck() > 240) { - runIn(2, "afterEvtCheckHandler") - logDebug(meth+" scheduled afterEvtCheckHandler...") - } -} - -private clearAfterCheckSchedule() { - unschedule("afterEvtCheckHandler") - unschedule("afterEvtCheckWatcher") // TODO remove - state.afterEvtCheckWatcherSched = false - logTrace("Clearing After Event Check Schedule...") -} - -void thermostatEvtHandler(evt) { - evt.name = (String)evt.name == sTEMP ? sTHERMTEMP : (String)evt.name - deviceEvtHandler(evt) -} - -@SuppressWarnings('GroovyFallthrough') -void deviceEvtHandler(evt, Boolean aftEvt=false, Boolean aftRepEvt=false) { - Long evtDelay = now() - (Long)((Date)evt.date).getTime() - Boolean evtOk = false - Boolean evtAd = false - String meth = 'deviceEvtHandler' - String eN = (String)evt.name - def eV = evt?.value - List d = settings."trig_${eN}" - String aftMsg = "${aftEvt ? " | (aftEvt)" : sBLANK}${aftRepEvt ? " | (aftRepEvt)" : sBLANK}" - if(isTrc())logTrace("Device Event | ${eN.toUpperCase()} | Name: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms${aftMsg}") - String dc = settings."trig_${eN}_cmd" // desired attribute value - Boolean dca = !!(Boolean)settings."trig_${eN}_all" - Boolean dcavg = (!dca && !!(Boolean)settings."trig_${eN}_avg") - //Boolean dco = ((Integer)settings."trig_${eN}_after"==null && (Boolean)settings."trig_${eN}_once" == true) - //Integer dcw = ((Integer)settings."trig_${eN}_after"==null && (Integer)settings."trig_${eN}_wait") ? (Integer)settings."trig_${eN}_wait" : null - Boolean dco = !!(Boolean)settings."trig_${eN}_once" - Integer dcw = (Integer)settings."trig_${eN}_wait"!=null ? (Integer)settings."trig_${eN}_wait" : null - String extra = sBLANK - switch(eN) { - case sSWITCH: - case sLOCK: - case "securityKeypad": - case "door": - case sSMOKE: - case "carbonMonoxide": - case "windowShade": - case "presence": - case sCONTACT: - case "acceleration": - case sMOTION: - case sWATER: - case sVALVE: - case sTHERMFM: - case sTHERMOS: - case sTHERMMODE: - if(d?.size() && dc) { - if(dc == sANY) { evtOk = true } - else { - if(dca && (allDevAttValsEqual(d, dc, (String)eV))) { evtOk = true; evtAd = true } - else if(eV == dc) { evtOk=true } - } - } - Boolean locknt = (Boolean)settings."trig_${eN}_ign_empty_type" - if(locknt && evtOk && eN in [sLOCK] && (String)evt.value in lLOCKUNL && evt.type == null) { - evtOk = false - extra = " FILTER REMOVED ${eN} (${evt.value}), did not have type set (${evt.type})" - } - if(evtOk && eN in [sLOCK, "securityKeypad"] && (String)evt.value in ["disarmed", "unlocked"]) { - List dcn = settings."trig_${eN}_Codes" - if(dcn) { - String theCode = evt?.data - Map data = null - if(theCode) data = getCodes(null, theCode) -// log.debug "converted $theCode to ${data} starting with ${evt?.data}" - List theCList = data?.collect { (String)it.key } -// log.debug "found code $theCList" - if(!theCList || !(theCList[0] in dcn)) { - evtOk = false - extra = " FILTER REMOVED the received code (${theCode}), code did not match trig_${eN}_Codes (${dcn})" - } - } - } - break - case sPUSHED: - case sRELEASED: - case sHELD: - case sDBLTAP: - def dcn = settings."trig_${eN}_nums" - if(d?.size() && dc && dcn && dcn?.size() > 0) { - if(dcn?.contains(eV)) { evtOk = true } - } -//log.debug "deviceEvtHandler: dcn: $dcn | d: $d | dc: $dc | dco: $dco | dcw: $dcw | evt.value: ${eV}" - break - case sCOOLSP: - case sHEATSP: - case sTHERMTEMP: - case sHUMID: - case sTEMP: - case sPOWER: - case "illuminance": - case sLEVEL: - case sBATT: - Double dcl = settings."trig_${eN}_low" - Double dch = settings."trig_${eN}_high" - Double dce = settings."trig_${eN}_equal" - Map valChk = deviceEvtProcNumValue(evt, d, dc, dcl, dch, dce, dca, dcavg) - evtOk = valChk.evtOk - evtAd = valChk.evtAd - break - default: - logDebug(meth+" | unknown event ${evt?.displayName} ${eN} value: ${eV}") - } - if(isDbg())logDebug(meth+" | evtOk :${evtOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") - eventCompletion(evt, evtOk, dco, dcw, meth+"(${eN})", eV, (String)evt?.displayName, aftRepEvt, evtAd) -/* Boolean waitOk = ((dco || dcw!=null) ? evtWaitRestrictionOk(evt, dco, dcw, aftRepEvt) : true) - Boolean execOk = (evtOk && waitOk) - logDebug(meth+" | execOk: ${execOk} | evtOk :${evtOk} | devEvtWaitOk: ${waitOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") - //if(!waitOk) { return } - if(getConfStatusItem("tiers")) { - processTierTrigEvt(evt, execOk) - } else { - if (execOk) { executeAction(evt, meth+"(${eN})", evtAd, aftRepEvt) } - } - if (!execOk) logTrace(meth+" | Skipping event ${evt?.displayName} ${eN} value: ${eV} | evtOk :${evtOk} | devEvtWaitOk: ${waitOk}") - */ - if (!evtOk) logTrace(meth+" | Skipping event ${evt?.displayName} ${eN} value: ${eV} | evtOk :${evtOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") -} - -private void processTierTrigEvt(evt, Boolean evtOk) { - String meth = "processTierTrigEvt" - String msg = " | Name: ${evt?.name} | Value: ${evt?.value} | EvtOk: ${evtOk}" - if(isTrc())logTrace(meth + msg) - - String mK = "actTierState" - getTheLock(sHMLF, meth) - - Map aTierSt = (Map)getMemStoreItem(mK, [:]) - if(!aTierSt) aTierSt = (Map)state[mK] ?: [:] - - if (evtOk) { - if(aTierSt.size()) { - msg = meth + " found already active tier state ${aTierSt}"+msg - } else { - releaseTheLock(sHMLF) - - logDebug(meth + " activating tier state" + msg) - tierEvtHandler(evt) - return - } - } else if(!evtOk && settings.act_tier_stop_on_clear == true) { - def tierConf = aTierSt.evt - if(tierConf?.size() && tierConf?.name == evt?.name && tierConf?.device?.id == evt?.device?.id) { - updMemStoreItem(mK, [:]) - state[mK] = [:] - - releaseTheLock(sHMLF) - - logDebug(meth + " Tier Trigger no longer valid... Clearing TierState and Schedule..."+msg) - unschedule("tierEvtHandler") - atomicState.tierSchedActive = false - updTsVal("lastTierRespStopDt") - return - } else msg = meth + " Tier Trigger valid... exiting...${aTierSt}"+msg - } else msg = meth + " no action ${aTierSt}"+msg - - releaseTheLock(sHMLF) - - logDebug(msg) -} - -def getTierStatusSection() { - String str = sBLANK - if(isTierAction()) { - Map mTierMap = getTierMap() - Boolean tsa = (Boolean)atomicState.tierSchedActive == true - - getTheLock(sHMLF, "getTierStatusSection") - - Map aTierSt = (Map)getMemStoreItem("actTierState", [:]) - if(!aTierSt) aTierSt = (Map)state.actTierState ?: [:] - - Map tS = aTierSt - str += spanSm(" ${sBULLET} Tier Size: ", sCLR4D9) + spanSmBr("${mTierMap?.size()}", sCLRGRY) - str += spanSm(" ${sBULLET} Schedule Active: ", sCLR4D9) + spanSmBr("${tsa}", sCLRGRY) - str += tS?.cycle ? spanSm(" ${sBULLET} Tier Cycle: ", sCLR4D9) + spanSmBr("${tS?.cycle}", sCLRGRY) : sBLANK - str += tS?.schedDelay ? spanSm(" ${sBULLET} Next Delay: ", sCLR4D9) + spanSmBr("${tS?.schedDelay}", sCLRGRY) : sBLANK - str += tS?.lastMsg ? spanSm(" ${sBULLET} Is Last Cycle: ", sCLR4D9) + spanSmBr("${tS?.lastMsg == true}", sCLRGRY) : sBLANK - - releaseTheLock(sHMLF) - - String a = getTsVal("lastTierRespStartDt") - str += a ? spanSm(" ${sBULLET} Last Tier Start: ", sCLR4D9) + spanSmBr(a, sCLRGRY) : sBLANK - a = getTsVal("lastTierRespStopDt") - str += a ? spanSm(" ${sBULLET} Last Tier Stop: ", sCLR4D9) + spanSmBr(a, sCLRGRY) : sBLANK - - section() { - paragraph spanSmBldBr("Tier Response Status: ") + str - } - } -} - -private void resumeTierJobs() { - - getTheLock(sHMLF, "resumeTierJobs") - - Map aTierSt = (Map)getMemStoreItem("actTierState", [:]) - if(!aTierSt) aTierSt = (Map)state.actTierState ?: [:] - - if(aTierSt?.size()) { - releaseTheLock(sHMLF) - if((Boolean)atomicState.tierSchedActive) { - tierEvtHandler() - } - return - } - releaseTheLock(sHMLF) -} - -private void tierEvtHandler(evt=null) { - if(!evt) { - Date adate = new Date() - String dt = dateTimeFmt(adate, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") - evt = [name: "Tiered Schedule", displayName: "Scheduled Tiered Trigger", value: fmtTime(dt), date: adate, device: [id: null]] - } - if(isTrc())logTrace("tierEvtHandler: received event ${evt?.name} ${evt?.value}") - - Map t0 = getTierMap() - Map tierMap = t0 ?: [:] - - getTheLock(sHMLF, "tierEvtHandler") - - String mK = "actTierState" - Map aTierSt = (Map)getMemStoreItem(mK, [:]) - if(!aTierSt) aTierSt = (Map)state[mK] ?: [:] - - Map tierState = aTierSt - //log.debug "tierState: ${tierState}" - //log.debug "tierMap: ${tierMap}" - if(tierMap.size()) { - Map newEvt = (Map)tierState.evt ?: [name: evt?.name, displayName: evt?.displayName, value: evt?.value, unit: evt?.unit, device: [id: evt?.device?.id], date: evt?.date] - Integer curPass = (tierState.cycle && tierState.cycle.toString()?.isNumber()) ? tierState.cycle.toInteger()+1 : 1 - if(curPass <= tierMap.size()) { - tierState.cycle = curPass - tierState.schedDelay = tierMap[curPass]?.delay ?: null - tierState.message = tierMap[curPass]?.message ?: null - tierState.volume = [:] - if(tierMap[curPass]?.volume?.change) tierState.volume.change = tierMap[curPass]?.volume?.change ?: null - if(tierMap[curPass]?.volume?.restore) tierState.volume.restore = tierMap[curPass]?.volume?.restore ?: null - tierState.evt = newEvt - tierState.lastMsg = (curPass+1 > tierMap.size()) - - updMemStoreItem(mK, tierState) - state[mK] = tierState - - releaseTheLock(sHMLF) - - logTrace("tierEvtHandler: tierSize: (${tierMap.size()}) | cycle: ${tierState.cycle} | curPass: (${curPass}) | nextPass: ${curPass+1} | schedDelay: (${tierState.schedDelay}) | Message: (${tierState.message}) | LastMsg: (${tierState.lastMsg})") - - if(curPass == 1) { updTsVal("lastTierRespStartDt"); remTsVal("lastTierRespStopDt") } - - tierSchedHandler([sched: true, tierState: tierState]) - } else { - - updMemStoreItem(mK, [:]) - state[mK] = [:] - releaseTheLock(sHMLF) - atomicState.tierSchedActive = false - updTsVal("lastTierRespStopDt") - logDebug("tierEvtHandler: Tier Cycle has completed... Clearing TierState...") - } - } else releaseTheLock(sHMLF) -} - -private void tierSchedHandler(data) { - if(data && data.tierState?.size() && data.tierState?.message) { - logTrace("tierSchedHandler(${data})") - Map evt = data.tierState.evt - executeAction(evt, "tierSchedHandler", false, false, [msg: data?.tierState?.message as String, volume: data?.tierState?.volume, isFirst: (data?.tierState?.cycle == 1), isLast: (data?.tierState?.lastMsg == true)]) - if(data?.sched) { - if(data.tierState.schedDelay && data.tierState.lastMsg == false) { - logDebug("tierSchedHandler: Scheduling Next Tier Message for (${data.tierState?.schedDelay} seconds)") - runIn(data.tierState.schedDelay, "tierEvtHandler") - } else { - logDebug("tierSchedHandler: Scheduling cleanup for (5 seconds) as this was the last message") - runIn(5, "tierEvtHandler") - } - atomicState.tierSchedActive = true - } - } -} - -Map deviceEvtProcNumValue(evt, List devs=null, String cmd=sNULL, Double dcl=null, Double dch=null, Double dce=null, Boolean dca=false, Boolean dcavg=false) { - Boolean evtOk = false - Boolean evtAd = false - String en = (String)evt.name - String eV = evt.value?.toString() - Double evtValue = evt.value?.toDouble() - if(devs?.size() && cmd && eV?.isNumber()) { - en = en == sTHERMTEMP ? sTEMP : en - evtValue = dcavg ? getDevValueAvg(devs, en) : evtValue - Boolean not=false - //noinspection GroovyFallthrough - switch(cmd) { - case sEQUALS: - if(dce) { - if(!dca && dce == evtValue) evtOk=true - else if(dca && allDevAttNumValsEqual(devs, en, dce)) { evtOk=true; evtAd=true } - } - break - case sNBETWEEN: - not=true - case sBETWEEN: - if(dcl && dch) { - if(!dca) { - evtOk = (evtValue >= dcl) && (evtValue <= dch) - if(not) evtOk=!evtOk - } else if(dca && allDevAttNumValsBetween(devs, en, dcl, dch, not)) { evtOk=true; evtAd=true } - } - break - case sABOVE: - if(dch) { - if(!dca && (evtValue > dch)) evtOk=true - else if(dca && allDevAttNumValsAbove(devs, en, dch)) { evtOk=true; evtAd=true } - } - break - case sBELOW: - if(dcl) { - if(!dca && (evtValue < dcl)) evtOk=true - else if(dca && allDevAttNumValsBelow(devs, en, dcl)) { evtOk=true; evtAd=true } - } - break - } - } - Map result = [evtOk: evtOk, evtAd: evtAd] - if(isDbg())logDebug("deviceEvtProcNumValue | result: $result | evtName: ${evt.name} (${en}) | evtVal: $evtValue ($eV) | cmd: ${cmd} | low: ${dcl} | high: ${dch} | equal: ${dce} | all: ${dca} | avg: $dcavg") - return result -} - -static String evtValueCleanup(ival) { - def val=ival - // log.debug "val(in): ${val}" - val = (val?.toString()?.isNumber() && val?.toString()?.endsWith(".0")) ? val?.toDouble()?.round(0) : val - // log.debug "val(out): ${val}" - return val?.toString() -} - -private void clearEvtHistory() { - settingUpdate("clrEvtHistory", sFALSE, sBOOL) - String mK = "valEvtHistory" - - getTheLock(sHMLF, "clearEvtHistory") - - updMemStoreItem(mK, [:]) - state[mK] = [:] - - releaseTheLock(sHMLF) -} - -private Boolean evtWaitRestrictionOk(evt, Boolean once, Integer wait, Boolean aftRepEvt=false) { - Boolean ok = true - Long dur - Boolean waitOk = (wait!=null && wait >= 0) - String meth = "evtWaitRestrictionOk" - String msg = sBLANK - Boolean dayOk - String n = (String)evt?.name - String dID= evt?.device?.id as String - if(evt && n && dID) { - String kN= dID + '_' +n - msg = "Last ${n.capitalize()} Event for Device ($dID) | onceDaily ${once} | wait ${wait} | repeatEvt: ${aftRepEvt} | " - Date evtDt = (Date)evt.date - - getTheLock(sHMLF, meth) - - String mK = "valEvtHistory" - Map evtHistMap = (Map)getMemStoreItem(mK, [:]) - if (!evtHistMap) evtHistMap = (Map)state[mK] ?: [:] - - // log.debug "prevDt: ${evtHistMap[n]?.dt ? parseDate(evtHistMap[n]?.dt as String) : null} | evtDt: ${evtDt}" - if (evtHistMap.containsKey(kN) && evtHistMap[kN]?.dt) { - Date prevDt = parseDate((String)evtHistMap[kN].dt) - // log.debug "prevDt: ${prevDt.toString()}" - if (prevDt && evtDt) { - dur = Math.round(( (Long)evtDt.getTime() - (Long)prevDt.getTime()) / 1000.0D) - waitOk = waitOk ? (dur && (wait < dur)) : true - dayOk = !once || (once && !isDateToday(prevDt)) - ok = (waitOk && dayOk) - msg += "Occurred: (${dur} sec ago) | Desired Wait: (${wait} sec) - (waitOk $waitOk && dayOk $dayOk)" - } - } else msg += "No history found" - // check if same and don't save? - if (ok && !aftRepEvt) { - evtHistMap[kN] = [dt: formatDt(evtDt), value: evt.value, name: n, dID: dID] - updMemStoreItem(mK, evtHistMap) - state[mK] = evtHistMap - } - - releaseTheLock(sHMLF) - } else { - msg="missing evt, name ($n) or deviceId ($dID)" - } - msg += " Status: (${ok ? okSymFLD : notOkSymFLD})" - if(isDbg())logDebug(meth+": "+msg) - return ok -} - -@SuppressWarnings('GroovyFallthrough') -static String getAttrPostfix(String attr) { - switch(attr) { - case sHUMID: - case sLEVEL: - case sBATT: - return " percent" - case sTHERMTEMP: - case sTEMP: - return " degrees" - case "illuminance": - return " lux" - case sPOWER: - return " watts" - default: - return sBLANK - } -} - - -/*********************************************************************************************************** - CONDITIONS HANDLER -************************************************************************************************************/ -Boolean reqAllCond() { - Boolean mult = multipleConditions() - return ( mult && (Boolean)settings.cond_require_all ) -} - -Boolean timeCondOk() { - Date startTime - Date stopTime - Date now = new Date() - String startType = (String)settings.cond_time_start_type - String stopType = (String)settings.cond_time_stop_type - if(startType && stopType) { - startTime = startType == sTIME && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null - stopTime = stopType == sTIME && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null - - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L - Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - - if(startTime && stopTime) { - Boolean not = startTime.getTime() > stopTime.getTime() - Boolean isBtwn = timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) - isBtwn = not ? !isBtwn : isBtwn - state.startTime = formatDt(startTime) - state.stopTime = formatDt(stopTime) - if(isTrc())logTrace("timeCondOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not? startTime:stopTime})") - return isBtwn - } - } - if(isTrc())logTrace("timeCondOk | (null)") - state.startTime = sNULL - state.stopTime = sNULL - return null -} - -Boolean dateCondOk() { - Boolean result = null - Boolean dOk - Boolean mOk - if((List)settings.cond_days || (List)settings.cond_months) { - Boolean reqAll = reqAllCond() - dOk = settings.cond_days ? (isDayOfWeek((List)settings.cond_days)) : reqAll - mOk = settings.cond_months ? (isMonthOfYear((List)settings.cond_months)) : reqAll - result = reqAll ? (mOk && dOk) : (mOk || dOk) - } - if(isTrc()){ - //noinspection GroovyVariableNotAssigned - logTrace("dateCondOk | $result | monthOk: $mOk | daysOk: $dOk") - } - return result -} - -Boolean locationCondOk() { - Boolean result = null - Boolean mOk - Boolean aOk - List myL = (List)settings.cond_alarmSystemStatus - if((List)settings.cond_mode || (String)settings.cond_mode_cmd || myL) { - Boolean reqAll = reqAllCond() - mOk = ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : reqAll - aOk = myL ? isInAlarmMode(myL) : reqAll - result = reqAll ? (mOk && aOk) : (mOk || aOk) - } - if(isTrc()){ - //noinspection GroovyVariableNotAssigned - logTrace("locationCondOk | $result | modeOk: $mOk | alarmOk: $aOk") - } - return result -} - -Boolean checkDeviceCondOk(String att) { - List devs = (List)settings."cond_${att}" ?: null - def cmdVal = settings."cond_${att}_cmd" ?: null // list or string - Boolean all = !!(Boolean)settings."cond_${att}_all" - if( !(att && devs && cmdVal) ) { return true } - return all ? allDevAttValsEqual(devs, att, cmdVal) : anyDevAttValsEqual(devs, att, cmdVal) -} - -Boolean checkDeviceNumCondOk(String att) { - List devs = (List)settings."cond_${att}" ?: null - String cmd = settings."cond_${att}_cmd" ?: null - Double dcl = settings."cond_${att}_low"!=null ? settings."cond_${att}_low" : null - Double dch = settings."cond_${att}_high"!=null ? settings."cond_${att}_high" : null - Double dce = settings."cond_${att}_equal"!=null ? settings."cond_${att}_equal" : null - Boolean dca = (Boolean)settings."cond_${att}_all" ?: false - if( !(att && devs && cmd) ) { return true } - Boolean not=false - Boolean a = true - //noinspection GroovyFallthrough - switch(cmd) { - case sEQUALS: - if(dce) { - a = dca ? allDevAttNumValsEqual(devs, att, dce) : anyDevAttNumValEqual(devs, att, dce) - } - break - case sNBETWEEN: - not=true - case sBETWEEN: - if(dcl && dch) { - a = dca ? allDevAttNumValsBetween(devs, att, dcl, dch, not) : anyDevAttNumValBetween(devs, att, dcl, dch, not) - } - break - case sABOVE: - if(dch) { - a = dca ? allDevAttNumValsAbove(devs, att, dch) : anyDevAttNumValAbove(devs, att, dch) - } - break - case sBELOW: - if(dcl) { - a = dca ? allDevAttNumValsBelow(devs, att, dcl) : anyDevAttNumValBelow(devs, att, dcl) - } - break - } - return a -} - -@Field static final List lDATTSTR = ["switch", "motion", "presence", "contact", "acceleration", "lock", "securityKeypad", "door", "windowShade", "valve", "water", "thermostatMode", "thermostatOperatingState", "thermostatFanMode", "smoke", "carbonMonoxide"] -@Field static final List lDATTNUM = ["temperature", "humidity", "illuminance", "level", "power", "battery", "coolingSetpoint", "heatingSetpoint", "thermostatTemperature"] -// these are momentary triggers only: pushed released held doubleTapped - -Boolean deviceCondOk() { - List skipped = [] - List passed = [] - List failed = [] - lDATTSTR.each { String i-> - if(!(List)settings."cond_${i}") { skipped.push(i); return } - checkDeviceCondOk(i) ? passed.push(i) : failed.push(i) - } - lDATTNUM.each { String i-> - if(!(List)settings."cond_${i}") { skipped.push(i); return } - checkDeviceNumCondOk(i) ? passed.push(i) : failed.push(i) - } - Integer cndSize = (passed.size() + failed.size()) - Boolean result = null - if(cndSize != 0) result = reqAllCond() ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() > 0) - if(isTrc())logTrace("deviceCondOk | ${result} | Found: (${(passed?.size() + failed?.size())}) | Skipped: $skipped | Passed: $passed | Failed: $failed") - return result -} - -Map conditionStatus() { - Boolean reqAll = reqAllCond() - List failed = [] - List passed = [] - List skipped = [] - Boolean ok = true - if((Boolean)state.dupPendingSetup) ok = false - Integer cndSize = (Integer)null - if(ok) { - [sTIME, "date", "location", "device"].each { String i-> - def s = "${i}CondOk"() - if(s == null) { skipped.push(i); return } - s ? passed.push(i) : failed.push(i) - } - cndSize = (passed.size() + failed.size()) - ok = reqAll ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() > 0) - if(cndSize == 0) ok = true - } - if(isTrc())logTrace("conditionsStatus | ok: $ok | RequireAll: ${reqAll} | Found: (${cndSize}) | Skipped: $skipped | Passed: $passed | Failed: $failed") - return [ok: ok, passed: passed, blocks: failed] -} - -Boolean devCondConfigured(String att) { - return ((List)settings."cond_${att}" && settings."cond_${att}_cmd") -} - -Boolean devNumCondConfigured(String att) { - return (settings."cond_${att}_cmd" && (settings."cond_${att}_low"!=null || settings."cond_${att}_high"!=null || settings."trig_${att}_equal"!=null)) -} - -Boolean timeCondConfigured() { - Boolean startTime = ((String)settings.cond_time_start_type in lSUNRISESET || ((String)settings.cond_time_start_type == sTIME && settings.cond_time_start)) - Boolean stopTime = ((String)settings.cond_time_stop_type in lSUNRISESET || ((String)settings.cond_time_stop_type == sTIME && settings.cond_time_stop)) - return (startTime && stopTime) -} - -Boolean dateCondConfigured() { - Boolean days = (settings.cond_days) - Boolean months = (settings.cond_months) - return (days || months) -} - -Boolean locationModeConfigured() { - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } - return ((List)settings.cond_mode && (String)settings.cond_mode_cmd) -} - -Boolean locationAlarmConfigured() { - return ((List)settings.cond_alarmSystemStatus) -} - -Boolean deviceCondConfigured() { - return (deviceCondCount() > 0) -} - -Integer deviceCondCount() { - List devAtts = lDATTSTR + lDATTNUM - List items = [] - devAtts.each { String dc-> if(devCondConfigured(dc)) { items.push(dc) } } - return items.size() -} - -Boolean conditionsConfigured() { - return (timeCondConfigured() || dateCondConfigured() || locationModeConfigured() || locationAlarmConfigured() || deviceCondConfigured()) -} - -Boolean multipleConditions() { - Integer cnt = 0 - if(timeCondConfigured()) cnt++ - if(dateCondConfigured()) cnt++ - if(locationModeConfigured()) cnt++ - if(locationAlarmConfigured()) cnt++ - cnt = cnt + deviceCondCount() - return (cnt>1) -} - -/*********************************************************************************************************** - ACTION EXECUTION -************************************************************************************************************/ - -static String convEvtType(String type) { - Map typeConv = [ - (sPISTNEXEC): "Piston", - (sALRMSYSST): "Alarm system status", - "hsmStatus": "Alarm system status", - "hsmAlert": "Alarm system alert" - ] - return (type && typeConv.containsKey(type)) ? typeConv[type] : type -} - -String decodeVariables(evt, String str) { - if(!str) return str - if(evt) { - // log.debug "str: ${str} | vars: ${(str =~ /%[a-z]+%/)}" - String typS = "%type%" - String namS = "%name%" - if(str.contains(typS) && (String)evt.name) { - if(str.contains(namS)) { - str = str.replaceAll(typS, !(String)evt.displayName?.toLowerCase()?.contains((String)evt.name) ? convEvtType((String)evt.name) : sBLANK) - } else { - str = str.replaceAll(typS, convEvtType((String)evt.name)) - } - } - str = (str.contains(namS)) ? str.replaceAll(namS, (String)evt.displayName) : str - str = (str.contains("%unit%") && (String)evt.name) ? str.replaceAll("%unit%", getAttrPostfix((String)evt.name)) : str - str = (str.contains("%value%") && evt.value != null) ? str.replaceAll("%value%", evt.value?.toString()?.isNumber() ? evtValueCleanup(evt?.value) : evt?.value) : str - if(!(evt instanceof com.hubitat.hub.domain.Event) && evt.totalDur) { - Integer mins = durationToMinutes(evt.totalDur) - str = (str.contains("%duration%")) ? str.replaceAll("%duration%", "${evt.totalDur} second${evt.totalDur > 1 ? "s" : sBLANK} ago") : str - str = (str.contains("%duration_min%")) ? str.replaceAll("%duration_min%", "${mins} minute${mins > 1 ? "s" : sBLANK} ago") : str - str = (str.contains("%durationmin%")) ? str.replaceAll("%durationmin%", "${mins} minute${mins > 1 ? "s" : sBLANK} ago") : str - str = (str.contains("%durationval%")) ? str.replaceAll("%durationval%", "${evt.totalDur} second${evt.totalDur > 1 ? "s" : sBLANK}") : str - str = (str.contains("%durationvalmin%")) ? str.replaceAll("%durationvalmin%", "${mins} minute${mins > 1 ? "s" : sBLANK}") : str - } - } - Date adate = (Date)evt.date ?: new Date() - str = (str.contains("%date%")) ? str.replaceAll("%date%", convToDate(adate)) : str - str = (str.contains("%time%")) ? str.replaceAll("%time%", convToTime(adate)) : str - str = (str.contains("%datetime%")) ? str.replaceAll("%datetime%", convToDateTime(adate)) : str - return str -} - -static Integer durationToMinutes(dur) { - if(dur && dur>=60) return (dur/60)?.toInteger() - return 0 -} - -static Integer durationToHours(dur) { - if(dur && dur>= 3600) return (dur / 3600).toInteger() - return 0 -} -/* -private List decamelizeStr(String str) { - return (List) str.split("(? eTxtItems = glbText.tokenize(";") - return decodeVariables(evt, (String)getRandomItem(eTxtItems)) - } else if(tierMsg) { - List eTxtItems = tierMsg.tokenize(";") - return decodeVariables(evt, (String)getRandomItem(eTxtItems)) - } else { - List devs = settings."trig_${eN}" ?: [] - // String dc = (String)settings."trig_${eN}_cmd" - // Boolean dca = ((Boolean)settings."trig_${eN}_all" == true) - String dct = (String)settings."trig_${eN}_txt" ?: sNULL - String dcart = (Integer)settings."trig_${eN}_after_repeat" && (String)settings."trig_${eN}_after_repeat_txt" ? (String)settings."trig_${eN}_after_repeat_txt" : sNULL - List eTxtItems = dct ? dct.tokenize(";") : [] - List rTxtItems = dcart ? dcart.tokenize(";") : [] - List testItems = eTxtItems + rTxtItems - if(testMode && testItems.size()) { - return decodeVariables(evt, (String)getRandomItem(testItems)) - } else if(!testMode && isRepeat && rTxtItems?.size()) { - return decodeVariables(evt, (String)getRandomItem(rTxtItems)) - } else if(!testMode && eTxtItems?.size()) { - return decodeVariables(evt, (String)getRandomItem(eTxtItems)) - } else { - //noinspection GroovyFallthrough - switch(eN) { - case sMODE: - return "The location mode is now set to ${eV}" - case sPISTNEXEC: - return "The ${evt?.displayName} piston was just executed!." - case "scene": - return "The ${eV} scene was just executed!." - case "hsmStatus": - case sALRMSYSST: - return "The ${getAlarmSystemName()} is now set to ${eV}" - case "guard": - return "Alexa Guard is now set to ${eV}" - case "hsmAlert": - return "A ${getAlarmSystemName()} ${evt?.displayName} alert with ${eV} has occurred." - case "sunriseTime": - case "sunsetTime": - return "The ${getAlarmSystemName()} is now set to ${eV}" - case sSCHED: - return "Your scheduled event has occurred at ${eV}" - case sPUSHED: - case sHELD: - case sRELEASED: - case sDBLTAP: - return "Button ${eV} was ${eN == sDBLTAP ? "double tapped" : eN} on ${evt?.displayName}" - - case sTHERMTEMP: - eN = sTEMP - /* - case sCOOLSP: - case sHEATSP: - case sTHERMMODE: - case sTHERMFM: - case sTHERMOS: - case sSMOKE: - case "carbonMonoxide": - return "${eN} is ${eV} on ${evt?.displayName}!" - */ - default: - String t0 = getAttrPostfix(eN) - String postfix = t0 ?: sBLANK - if(evtAd && devs?.size()>1) { - return "All ${devs.size()}${!evt?.displayName?.toLowerCase()?.contains(eN) ? " ${eN}" : sBLANK} devices are ${eV} ${postfix}" - } else { - return "${evt?.displayName}${!evt?.displayName?.toLowerCase()?.contains(eN) ? " ${eN}" : sBLANK} is ${eV} ${postfix}" - } - break - } - } - } - return "Invalid Text Received... Please verify Action configuration..." -} - -public getActionHistory(Boolean asObj=false) { - List eHist = (List)getMemStoreItem("actionHistory") - List output = [] - if(eHist.size()) { - eHist.sort { a,b-> b.dt <=> a.dt }.each { Map h-> - List hList = [] - hList.push([name: "Trigger:", val: h?.evtName]) - hList.push([name: "Device:", val: h?.evtDevice]) - hList.push([name: "Value:", val: h?.evtValue]) - hList.push([name: "Condition Status:", val: (h?.active ? "Passed" : "Failed")]) - hList.push([name: "Conditions Passed:", val: h?.passed]) - hList.push([name: "Conditions Blocks:", val: h?.blocks]) - if(h?.test) hList.push([name: "Test Mode:", val: true]) - if(h?.isRepeat) hList.push([name: "Repeat:", val: true]) - hList.push([name: "Source:", val: h?.src]) - hList.push([name: "DateTime:", val: h?.dt]) - if(hList.size()) { - output.push(spanSm(kvListToHtmlTable(hList, sCLR4D9), sCLRGRY)) - } - } - } else { output.push("No History Items Found...") } - if(!asObj) { - output.each { String i-> paragraph spanSm(i) } - } else { - return output - } -} - -private static String kvListToHtmlTable(List tabList, String color=sCLRGRY) { - String str = sBLANK - if(tabList?.size()) { - str += "" - tabList.each { it-> - str += "" - } - str += "
${spanSmBld((String)it.name)}${spanSmBr("${it.val}")}
" - } - return str -} - -private addToActHistory(evt, data, Integer max=10) { - getTheLock(sHMLF, "addToActHistory") - - List eData = (List)getMemStoreItem("actionHistory") - if(eData == null) { eData = [] } - eData.push([ - dt: getDtNow(), - active: (data?.status?.ok == true), - evtName: evt?.name, - evtValue: evt?.value, - evtDevice: evt?.displayName, - blocks: data?.status?.blocks, - passed: data?.status?.passed, - test: data?.test, - isTierCmd: data?.isTier, - isRepeat: data?.isRepeat, - src: data?.src - ]) - Integer lsiz = eData.size() - if(lsiz > max) { eData = eData.drop( (lsiz-max) ) } - // log.debug "actionHistory Size: ${eData?.size()}" - updMemStoreItem("actionHistory", eData) - - releaseTheLock(sHMLF) -} - -void clearActHistory(){ - getTheLock(sHMLF, "clearActHistory") - - updMemStoreItem("actionHistory", []) - - releaseTheLock(sHMLF) -} - -private void executeAction(evt = null, String src=sNULL, Boolean allDevsResp=false, Boolean isRptAct=false, Map tierData=null) { - String meth="executeAction" - Long startTime = now() - Boolean testMode = false - if((Boolean)settings.actTestRun) { testMode = true } - if(isTrc())logTrace(meth+" ${src ? '('+src+')' : sBLANK}${testMode ? " | [TestMode]" : sBLANK}${allDevsResp ? " | [AllDevsResp]" : sBLANK}${isRptAct ? " | [RepeatEvt]" : sBLANK}") - if(isPaused(true)) { logWarn("Action is PAUSED... Skipping Action Execution...", true); return } - Map condStatus = conditionStatus() - // log.debug "condStatus: ${condStatus}" - addToActHistory(evt, [status: condStatus, test: testMode, src: src, isRepeat: isRptAct, isTier: (tierData != null)] ) - Map actMap = (Map)state.actionExecMap - - Integer actDevSiz = settings.act_EchoDevices ? settings.act_EchoDevices.size() : 0 -// List actDevices // = settings.act_EchoDevices ? parent?.getDevicesFromList(settings.act_EchoDevices) : [] -// Integer actDevSiz = actDevices.size() - - Map activeZones = settings.act_EchoZones ? getActiveZones() : [:] - activeZones = activeZones.size() ? activeZones.findAll { it?.key in settings.act_EchoZones } : [:] - Integer actZonesSiz = activeZones.size() - -// log.debug "activeZones: $activeZones" - - String actMsgTxt = sNULL - String actType = (String)settings.actionType - Boolean firstTierMsg = (tierData && tierData.isFirst == true) - Boolean lastTierMsg = (tierData && tierData.isLast == true) - Boolean actOk = getConfStatusItem("actions") - if(actOk && actType) { -// def alexaMsgDev = actDevSiz && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null - if(!(Boolean)condStatus.ok) { logWarn(meth+" | Skipping execution because ${condStatus.blocks} conditions have not been met", true); return } - if(!actMap || !actMap?.size()) { logError(meth+" Error | The ActionExecutionMap is not found or is empty", true); return } - if(settings.act_EchoZones && actZonesSiz == 0 && actDevSiz == 0) { logWarn(meth+" | No Active Zones Available and No Alternate Echo Devices Selected.", true); return } - if(actDevSiz == 0 && !settings.act_EchoZones) { logError(meth+" Error | Echo Device List not found or is empty", true); return } - if(!actMap.actionType) { logError(meth+" Error | The ActionType is missing or is empty", true); return } - Map actConf = (Map)actMap.config - Integer actDelay = (Integer)actMap.delay ?: 0 - Integer actDelayMs = actDelay*1000 - Integer changeVol = actConf?.volume?.change != null ? (Integer)actConf?.volume?.change : null - Integer restoreVol = actConf?.volume?.restore != null ? (Integer)actConf?.volume?.restore : null - //Integer alarmVol = actConf?.volume?.alarm ?: null - Map zoneVolumeMap = (Map)actConf?.zoneVolume ?: null - Boolean chkDnd=true - //noinspection GroovyFallthrough - switch(actType) { - case sSPEAKI: - case sSPEAKIT: - case sSPEAKIP: - case sSPEAKIPT: - case sANNI: - case sANNIT: - chkDnd=false - case sSPEAK: - case sSPEAKP: - case sSPEAKT: - case sSPEAKPT: - case sANN: - case sANNT: - if(actConf[actType]) { - if(tierData?.volume && tierData?.volume?.change || tierData?.volume?.restore) { - if(tierData?.volume?.change) changeVol = tierData?.volume?.change - if(tierData?.volume?.restore) restoreVol = tierData?.volume?.restore - } - String txt = getResponseItem(evt, (String)tierData?.msg, allDevsResp, isRptAct, testMode) ?: sNULL - // log.debug "txt: $txt" - if(!txt) { txt = "Invalid Text Received... Please verify Action configuration..." } - actMsgTxt = txt - - String mCmd = actType.replaceAll("_tiered", sBLANK) - if(actZonesSiz) { - parent?.sendZoneCmd([zones: activeZones.collect { (String)it?.key }, cmd: mCmd, title: getActionName(), message: txt, changeVol: changeVol, restoreVol: restoreVol, zoneVolumes: zoneVolumeMap, delay: actDelayMs]) - if(isDbg())logDebug("Sending ${mCmd} Command: (${txt}) to Zones via Parent (${activeZones.collect { it?.value?.name }.join(',')})${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - - } else { - if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - List devObjs = [] - // log.debug "actDevices: ${actDevices}" - String cmd = mCmd in [sSPEAK, sSPEAKP, sSPEAKI, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT] ? sTTS : "announce" - actDevices?.each { - Map devInfo = it?.getEchoDevInfo(cmd, true) // ignores dnd setting in device - if(devInfo) { - Boolean dnd = chkDnd ? parent.getDndEnabled((String)devInfo.deviceSerialNumber) : false - //if(!dnd) devObjs?.push([deviceTypeId: devInfo.deviceTypeId, deviceSerialNumber: devInfo.deviceSerialNumber, dni: devInfo.dni]) - if(!dnd) devObjs?.push(devInfo) - } - } - parent?.sendDevObjCmd(devObjs, mCmd, getActionName(), txt, changeVol, restoreVol) - if(isDbg())logDebug("Sending ${mCmd} Command: (${txt}) to devices via Parent (${devObjs})${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } - } - } - break - case "voicecmd": - case "sequence": - String mCmd= actConf[actType] ? (String)actConf[actType].cmd : sNULL - String mText= actConf[actType] ? (String)actConf[actType].text : sNULL - if(mText != sNULL) { - if(actZonesSiz) { - sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { (String)it?.key }, cmd: actType, message: mText, delay: actDelayMs], isStateChange: true, display: false, displayed: false) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mText}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } else if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices.each { dev-> - dev?."${mCmd}"(mText) - } - if(isDbg())logDebug("Sending ${mAct} Command: (${mText}) to devices (${actDevices})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } - } - break - case "playback": - case "dnd": - String mCmd= actConf[actType] ? (String)actConf[actType].cmd : sNULL - if(mCmd != sNULL) { - if(actZonesSiz) { - sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[zones: activeZones.collect { (String)it?.key }, cmd: mCmd, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mCmd}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } else if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices.each { dev-> - if(mCmd != "volume") dev?."${mCmd}"() - else if(mCmd == "volume" && changeVol != null) dev?.setVolume(changeVol) - } - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mCmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol != null ? " | Volume: ${changeVol}" : sBLANK}") - } - } - break - - case "builtin": - case "calendar": - case sWEATH: - if(actConf[actType] && actConf[actType].cmd) { - if(actZonesSiz) { - sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { (String)it?.key }, cmd: actConf[actType].cmd, message: actConf[actType].text, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } else if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices.each { dev-> - dev?."${actConf[actType].cmd}"(changeVol, restoreVol) - } - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } - } - break - - case "sounds": - if(actConf[actType] && actConf[actType].cmd && actConf[actType].name) { - if(actZonesSiz) { - sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { it?.key as String }, cmd: actConf[actType].cmd, message: actConf[actType].name, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd} | Name: ${actConf[actType].name}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } else if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices.each { dev-> - dev?."${actConf[actType].cmd}"(actConf[actType].name, changeVol, restoreVol) - } - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd} | Name: ${actConf[actType].name}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } - } - break - - case "alarm": - case "reminder": - if(actConf[actType] && actConf[actType].cmd && actConf[actType].label && actConf[actType].date && actConf[actType].time) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices?.each { dev-> - dev?."${actConf[actType].cmd}"(actConf[actType].label, actConf[actType].date, actConf[actType].time) - // dev?."${actConf[actType]?.cmd}"(actConf[actType]?.label, actConf[actType]?.date, actConf[actType]?.time, actConf[actType]?.recur?.type, actConf[actType]?.recur?.opt) - } - if(isDbg())logDebug("Sending ${actType?.toString()?.capitalize()} Command: (${actConf[actType]?.cmd}) to ${actDevices} | Label: ${actConf[actType]?.label} | Date: ${actConf[actType]?.date} | Time: ${actConf[actType]?.time}") - } - break - - case "music": - if(actConf[actType] && actConf[actType]?.cmd && actConf[actType]?.provider && actConf[actType]?.search) { - if(isDbg())logDebug("musicProvider: ${actConf[actType]?.provider} | ${convMusicProvider((String)actConf[actType]?.provider)}") - if(actZonesSiz) { - sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { it?.key as String }, cmd: actType, search: actConf[actType]?.search, provider: convMusicProvider((String)actConf[actType]?.provider), changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: true, displayed: true) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${txt}) to Zones (${activeZones.collect { it?.value?.name }} | Provider: ${actConf[actType]?.provider} | Search: ${actConf[actType]?.search} | Command: (${actConf[actType]?.cmd})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } else if(actDevSiz) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices.each { dev-> - dev?."${actConf[actType]?.cmd}"(actConf[actType]?.search, convMusicProvider((String)actConf[actType]?.provider), changeVol, restoreVol) - } - if(isDbg())logDebug("Sending ${actType.capitalize()} | Provider: ${actConf[actType]?.provider} | Search: ${actConf[actType]?.search} | Command: (${actConf[actType]?.cmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") - } - } - break - - case "alexaroutine": - if(actConf[actType] && actConf[actType].cmd && actConf[actType].routineId) { - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - actDevices[0]?."${actConf[actType].cmd}"((String)actConf[actType].routineId) - if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) | RoutineId: ${actConf[actType].routineId} to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } - break - - case "wakeword": - if(actConf[actType] && actConf[actType]?.devices && actConf[actType]?.devices?.size()) { - actConf[actType]?.devices?.each { d-> - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - def aDev = actDevices?.find { it?.id?.toString() == d?.device?.toString() } // TODO - if(aDev) { - aDev?."${d?.cmd}"(d?.wakeword) - if(isDbg())logDebug("Sending WakeWord: (${d?.wakeword}) | Command: (${d?.cmd}) to ${aDev}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } - } - } - break - - case "bluetooth": - if(actConf[actType] && actConf[actType]?.devices && actConf[actType]?.devices?.size()) { - actConf[actType]?.devices?.each { d-> - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - def aDev = actDevices?.find { it?.id?.toString() == d?.device?.toString() } - if(aDev) { - if(d?.cmd == "disconnectBluetooth") { - aDev?."${d?.cmd}"() - } else { - aDev?."${d?.cmd}"(d?.btDevice) - } - if(isDbg())logDebug("Sending ${d?.cmd} | Bluetooth Device: ${d?.btDevice} to ${aDev}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") - } - } - } - break - } - if(isActNotifConfigured()) { - Boolean ok2SendNotif = true - if(customMsgConfigured()) { actMsgTxt = settings.notif_custom_message } - if(customMsgRequired() && !customMsgConfigured()) { ok2SendNotif = false } - if(ok2SendNotif && actMsgTxt) { - def alexaMsgDev = null - if(actDevSiz && (Boolean)settings.notif_alexa_mobile){ - List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) - alexaMsgDev = actDevices[0] - } - if(sendNotifMsg(getActionName(), actMsgTxt, alexaMsgDev, false)) { logTrace("Sent Action Notification...") } - } - } - String mmS = "scheduled executeTaskCommands in " - String cmd = "executeTaskCommands" - - if(tierData?.size() && (Integer)settings.act_tier_cnt > 1) { - if(isDbg())logDebug("firstTierMsg: ${firstTierMsg} | lastTierMsg: ${lastTierMsg}") - if(firstTierMsg) { - Integer del = (Integer)settings.act_tier_start_delay - if(del) { - logTrace(mmS+"$del seconds - start delay") - runIn(del, cmd, [data:[type: sACT_START]]) - } else { executeTaskCommands([type:sACT_START]) } - } - if(lastTierMsg) { - Integer del = (Integer)settings.act_tier_stop_delay - if(del) { - logTrace(mmS+"$del seconds - stop delay") - runIn(del, cmd, [data:[type: sACT_STOP]]) - } else { executeTaskCommands([type:sACT_STOP]) } - } - } else { - Integer del = (Integer)settings.act_tasks_delay - if(del) { - logTrace(mmS+"$del seconds - action tasks delay") - runIn(del, cmd, [data:[type: sACT]]) - } else { executeTaskCommands([type: sACT]) } - } - } - if(isTrc())logTrace(meth+" Finished | ProcessTime: (${now()-startTime}ms)") -} - -private postTaskCommands(data) { - String p = data?.type ?: sNULL - if(p && settings."${p}sirens" && (String)settings."${p}siren_cmd") { settings."${p}sirens"*.off() } -} - - -// parent calls for editing - -public Map getInputData(String inName) { - String desc = "
  • Add custom responses to use when this action is executed.
  • " - String title - String tmplt = '%name% %type% is now %value% %unit%' - String vDesc = "
  • Example: %name% %type% is now %value% would translate into The garage door contact is now open
  • To make beep sounds use: wop, wop, wop (equals 3 beeps)
  • " - switch(inName) { - case "act_speak_txt": - title = "Global Action Speech Event" - break - case "act_announcement_txt": - title = "Global Announcement Speech Event" - break - default: - if(inName?.startsWith(sTRIG)) { - List i = inName.tokenize("_") - String s = i[1]?.capitalize() - title = "(${s}) Trigger " - desc = "
  • Add custom responses for ${s} " - if(i.contains("repeat")) { - title += "Repeat " - desc += "events which have to be repeated" - } else { - desc += "trigger events" - } - title += "Events" - desc += ".
  • ${vDesc}" - } - else if(inName?.startsWith(sACT_ITEM) && inName?.endsWith("_txt")) { - List i = inName.tokenize("_") - String s = i[3] - title = "Tier Response (${s})" - desc = "
  • Add custom responses to use when this action is executed.
  • " - tmplt = "Custom tier ${s} message here." - } else { - desc = sNULL - title = sNULL - tmplt = sNULL - } - break - } - Map o = [ - val: settings."${inName}" ? settings."${inName}"?.toString()?.replaceAll("\'", sBLANK) : null, - desc: """
      ${desc}
    """, - title: title, - template: tmplt, - version: appVersionFLD - ] - if(devModeFLD) log.debug "getInputData inName: $inName result: $o" - return o -} - -public Boolean updateTxtEntry(obj) { - // log.debug "updateTxtEntry | Obj: $obj" - if(obj?.name && obj?.type) { - settingUpdate("${obj?.name}", obj?.val ?: sBLANK, obj?.type as String) - return true - } - return false -} - -public getSettingInputVal(String inName) { - // log.debug "getSettingInputVal: ${inName}" - return settings."${inName}" ?: null -} - - -/*********************************************************************************************************** - HELPER UTILITES -************************************************************************************************************/ - -Boolean advLogsActive() { return (isDbg() || isTrc()) } -public void logsEnabled() { if(advLogsActive() && !getTsVal("logsEnabled")) { logTrace("enabling logging timer"); updTsVal("logsEnabled") } } -public void logsDisable() { - if(advLogsActive()) { - Integer dtSec = getLastTsValSecs("logsEnabled", null) - if(dtSec && (dtSec > 3600*6)) { - settingUpdate("logDebug", sFALSE, sBOOL) - settingUpdate("logTrace", sFALSE, sBOOL) - remTsVal("logsEnabled") - log.debug "Disabling debug logs" - } - } -} - -@Field volatile static Map tsDtMapFLD=[:] - -private void updTsVal(String key, String dt=sNULL) { - String appId=app.getId() - Map data=tsDtMapFLD[appId] ?: [:] - if(!data) data = (Map)state.tsDtMap ?: [:] - if(key) { data[key] = dt ?: getDtNow() } - tsDtMapFLD[appId]=data - tsDtMapFLD=tsDtMapFLD - - state.tsDtMap = data -} - -private void remTsVal(key) { - String appId=app.getId() - Map data=tsDtMapFLD[appId] ?: [:] - if(!data) data = (Map)state.tsDtMap ?: [:] - - if(key) { - if(key instanceof List) { - List t = (List)key - t.each { String k-> - if(data.containsKey(k)) { data.remove(k) } - } - } else { - String k = key - if(data.containsKey(k)) { data.remove(k) } - } - } - - tsDtMapFLD[appId]=data - tsDtMapFLD=tsDtMapFLD - state.tsDtMap = data -} - -String getTsVal(String key) { - String appId=app.getId() - Map tsMap=tsDtMapFLD[appId] - if(!tsMap) tsMap = (Map)state.tsDtMap ?: [:] - if(key && tsMap && tsMap[key]) { return (String)tsMap[key] } - return sNULL -} - -Integer getLastTsValSecs(String val, Integer nullVal=1000000) { - return (val && getTsVal(val)) ? GetTimeDiffSeconds(getTsVal(val)).toInteger() : nullVal -} - -private void updAppFlag(String key, val) { - Map t0 = atomicState.appFlagsMap - Map data = t0 ?: [:] - if(key) { data[key] = val } - atomicState.appFlagsMap = data -} - -private void remAppFlag(key) { - Map t0 = atomicState.appFlagsMap - Map data = t0 ?: [:] - if(key) { - if(key instanceof List) { - List t = (List)key - t.each { String k-> - if(data.containsKey(k)) { data.remove(k) } - } - } else { - String k = key - if(data.containsKey(k)) { data.remove(k) } - } - } - atomicState.appFlagsMap = data -} - -Boolean getAppFlag(String val) { - Map aMap = atomicState.appFlagsMap - if(val && aMap && aMap[val]) { return aMap[val] } - return false -} - -private void stateMapMigration() { - //Timestamp State Migrations - Map tsItems = ["lastAfterEvtCheck":"lastAfterEvtCheck", "lastNotifMsgDt":"lastNotifMsgDt"] - tsItems.each { String k, String v-> if(state.containsKey(k)) { updTsVal(v, (String)state[k]); state.remove(k) } } - - //App Flag Migrations - Map flagItems = [:] - flagItems.each { String k, String v-> if(state.containsKey(k)) { updAppFlag(v, state[k]); state.remove(k) } } - updAppFlag("stateMapConverted", true) -} - -void settingUpdate(String name, value, String type=sNULL) { - if(name && type) { app?.updateSetting(name, [type: type, value: value]) } - else if (name && type == sNULL) { app?.updateSetting(name, value) } -} - -void settingRemove(String name) { - if(isTrc())logTrace("settingRemove($name)...") - if(name && settings.containsKey(name)) { app?.removeSetting(name) } -} - -static List weekDaysEnum() { return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] } -static Map weekDaysAbrvEnum() { return ["MO":"Monday", "TU":"Tuesday", "WE":"Wednesday", "TH":"Thursday", "FR":"Friday", "SA":"Saturday", "SU":"Sunday"] } -static List monthEnum() { return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] } -static Map daysOfWeekMap() { return ["MON":"Monday", "TUE":"Tuesday", "WED":"Wednesday", "THU":"Thursday", "FRI":"Friday", "SAT":"Saturday", "SUN":"Sunday"] } -static Map weeksOfMonthMap() { return ["1":"1st Week", "2":"2nd Week", "3":"3rd Week", "4":"4th Week", "5":"5th Week"] } -static Map monthMap() { return ["1":"January", "2":"February", "3":"March", "4":"April", "5":"May", "6":"June", "7":"July", "8":"August", "9":"September", "10":"October", "11":"November", "12":"December"] } - -static Map getAlarmTrigOpts() { - return ["armedAway":"Armed Away", "armingAway":"Arming Away Pending exit delay","armedHome":"Armed Home","armingHome":"Arming Home pending exit delay", "armedNight":"Armed Night", "armingNight":"Arming Night pending exit delay","disarmed":"Disarmed", "allDisarmed":"All Disarmed","alerts":"Alerts"] -} - -private static Map getAlarmSystemAlertOptions(){ - return [ - intrusion : "Intrusion Away", - "intrusion-home" : "Intrusion Home", - "intrusion-night": "Intrusion Night", - (sSMOKE) : "Smoke", - (sWATER) : "Water", - rule : "Rule", - cancel : "Alerts cancelled", - arming : "Arming failure" - ] -} - -private static Map getAlarmSystemStatusActions(){ - return [ - armAway : "Arm Away", - armHome : "Arm Home", - armNight : "Arm Night", - disarm : "Disarm", - armRules : "Arm Monitor Rules", - disarmRules : "Disarm Monitor Rules", - disarmAll : "Disarm All", - armAll : "Arm All", - cancelAlerts: "Cancel Alerts" - ] -} - -public Map getActionMetrics() { - Map out = [:] - out.version = appVersionFLD - out.type = (String)settings.actionType ?: sNULL - out.delay = settings.actDelay ?: 0 - out.triggers = (List)settings.triggerEvents ?: [] - out.echoZones = (List)settings.act_EchoZones ?: [] - out.echoDevices = (List)settings.act_EchoDevices ?: [] - out.usingCustomText = (customTxtItems()?.size()) - Integer tDevCnt = 0 - ((List)settings.triggerEvents)?.each { if(settings."trig_${it}" && settings."trig_${it}"?.size()) { tDevCnt = tDevCnt+settings."trig_${it}"?.size() } } - out.triggerDeviceCnt = tDevCnt - return out -} - - -Integer getLastNotifMsgSec() { return getLastTsValSecs("lastNotifMsgDt") } -//Integer getLastChildInitRefreshSec() { return getLastTsValSecs("lastChildInitRefreshDt", 3600) } -//Integer getLastNotifMsgSec() { return !state.lastNotifMsgDt ? 100000 : GetTimeDiffSeconds(state.lastNotifMsgDt, "getLastMsgSec").toInteger() } - -Boolean getOk2Notify() { - Boolean smsOk // = (settings.notif_sms_numbers?.toString()?.length()>=10) - Boolean pushOk // = (settings.notif_send_push) - Boolean pushOver // = (settings.notif_pushover && settings.notif_pushover_devices) - Boolean alexaMsg = ((Boolean)settings.notif_alexa_mobile) - Boolean notifDevsOk = (((List)settings.notif_devs)?.size()) - Boolean daysOk = (List)settings.notif_days ? (isDayOfWeek((List)settings.notif_days)) : true - Boolean timeOk = notifTimeOk() - Boolean modesOk = (List)settings.notif_modes ? (isInMode((List)settings.notif_modes)) : true - Boolean result = true - if(!(smsOk || pushOk || alexaMsg || notifDevsOk || pushOver)) { result = false } - if(!(daysOk && modesOk && timeOk)) { result = false } - if(isDbg()){ - //noinspection GroovyVariableNotAssigned - logDebug("getOk2Notify() RESULT: $result | notifDevsOk: $notifDevsOk |smsOk: $smsOk | pushOk: $pushOk | pushOver: $pushOver | alexaMsg: $alexaMsg || daysOk: $daysOk | timeOk: $timeOk | modesOk: $modesOk") - } - return result -} - -Boolean notifTimeOk() { - Date startTime - Date stopTime - // these are quiet time start/stop - String startType = settings.notif_time_start_type - String stopType = settings.notif_time_stop_type - if(startType && stopType) { - startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null - stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null - } else { return true } - - Date now = new Date() - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L - Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - - if(startTime && stopTime) { - Boolean not = startTime.getTime() > stopTime.getTime() - Boolean isBtwn = !timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) - isBtwn = not ? !isBtwn : isBtwn - if(isTrc())logTrace("NotifTimeOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not ? startTime:stopTime})") - return isBtwn - } else { return true } -} - -// Sends the notifications based on app settings -Boolean sendNotifMsg(String msgTitle, String msg, alexaDev=null, Boolean showEvt=true) { - logTrace("sendNotifMsg() | msgTitle: ${msgTitle}, msg: ${msg}, $alexaDev, showEvt: ${showEvt}") - List sentSrc = [] // ["Push"] - Boolean sent = false - try { - String newMsg = msgTitle+': '+msg - String flatMsg = newMsg.replaceAll("\n", sSPACE) - if(!getOk2Notify()) { - logInfo( "sendNotifMsg: Notification not configured or Message Skipped During Quiet Time ($flatMsg)") - // if(showEvt) { sendNotificationEvent(newMsg) } - } else { - if((List)settings.notif_devs) { - sentSrc.push("Notification Devices") - ((List)settings.notif_devs)?.each { it?.deviceNotification(newMsg) } - sent = true - } - if((Boolean)settings.notif_alexa_mobile && alexaDev) { - alexaDev?.sendAlexaAppNotification(newMsg) - sentSrc.push("Alexa Mobile App") - sent = true - } - if(sent) { - state.lastNotificationMsg = flatMsg - updTsVal("lastNotifMsgDt") - if(isDbg())logDebug("sendNotifMsg: Sent ${sentSrc} (${flatMsg})") - } - } - } catch (ex) { - logError("sendNotifMsg $sentSrc Exception: ${ex}", false, ex) - } - return sent -} - -Boolean isActNotifConfigured() { - if(customMsgRequired() && !(settings.notif_use_custom && settings.notif_custom_message)) { return false } - return ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) -} - -static Integer versionStr2Int(String str) { return str ? str.toString()?.replaceAll("\\.", sBLANK)?.toInteger() : null } -Boolean minVersionFailed() { - try { - Integer t0 = (Integer)parent?.minVersions()["actionApp"] - Integer minDevVer = t0 ?: null - return minDevVer != null && versionStr2Int(appVersionFLD) < minDevVer - } catch (ignored) { - return false - } -} - - -/************************************************************************************************************ - webCoRE Integration -************************************************************************************************************/ -private static String webCoRE_handle(){return'webCoRE'} - -private void webCoRE_init(pistonExecutedCbk=null){ - subscribe(location,webCoRE_handle(),webCoRE_handler) -// if(pistonExecutedCbk)subscribe(location,"${webCoRE_handle()}.pistonExecuted",webCoRE_handler); - if(!webCoREFLD) { - webCoREFLD = [:] + [cbk:true] // pistonExecutedCbk] - } - webCoRE_poll(true) -} - -@Field static final String sLASTWU = 'lastwebCoREUpdDt' -@Field volatile static Map webCoREFLD = [:] - -private void webCoRE_poll(Boolean anow=false){ - Long rUpd = (Long)webCoREFLD.updated - if(rUpd && (now() > (rUpd+300000L))) { - Date aa = new Date(rUpd) - updTsVal(sLASTWU, formatDt(aa)) - } - Integer lastUpd = getLastTsValSecs(sLASTWU) - if (!webCoREFLD || (lastUpd > (3600*24)) || (anow && lastUpd > 300)) { - sendLocationEvent([name: "${webCoRE_handle()}.poll",value:'poll',isStateChange:true,displayed:false]) - updTsVal(sLASTWU) - } -} - -private void webCoRE_execute(String pistonIdOrName,Map data=[:]) { - String i = pistonIdOrName - if(i){sendLocationEvent([name:i,value:app.label,isStateChange:true,displayed:false,data:data])} -} - -private static List webCoRE_list(){ - List ret = ((List)webCoREFLD?.pistons)?.sort {it?.name}?.collect { [(it?.id): it?.aname?.replaceAll("<[^>]*>", sBLANK)] } - return ret -} -/* -private static String getPistonByName(String pistonIdOrName) { - String i=((List)webCoREFLD?.pistons ?: []).find{((String)it.name==pistonIdOrName)||((String)it.id==pistonIdOrName)}?.id -} */ - -private static String getPistonById(String rId) { - Map a = webCoRE_list()?.find { it.containsKey(rId) } - String aaa = (String)a?."${rId}" - return aaa ?: "Refresh to display piston name..." -} - -void webCoRE_handler(evt){ - logTrace("${evt?.name?.toUpperCase()} Event | Value: (${strCapitalize(evt?.value)}) with a delay of ${now() - evt?.date?.getTime()}ms") - switch((String)evt.value){ - case 'pistonList': - getTheLock(sHMLF, "webCoRE_Handler") - Long rUpd = (Long)webCoREFLD.updated - if(rUpd && (now() < (rUpd+1000L))) { - releaseTheLock(sHMLF) - break - } - List p=(List)webCoREFLD?.pistons ?: [] - Map d=(Map)evt.jsonData?:[:] - if(d.id && d.pistons && (d.pistons instanceof List)){ - p.removeAll{it.iid==d.id} - p+=d.pistons.collect{[iid:d.id]+it}.sort{it.name} - Boolean a = (Boolean)webCoREFLD?.cbk - - webCoREFLD = [cbk: a, updated: now(), pistons: p] - releaseTheLock(sHMLF) - - updTsVal(sLASTWU) - } else releaseTheLock(sHMLF) - break - case sPISTNEXEC: - if(valTrigEvt(sPISTNEXEC) && settings.trig_pistonExecuted) { - webcoreEvtHandler(evt) - } - break - } -} - -static String webCore_icon(){ return "https://raw.githubusercontent.com/ady624/webCoRE/master/resources/icons/app-CoRE.png" } - -/****************************************** -| Restriction validators -*******************************************/ - -Double getDevValueAvg(List devs, String attr) { - List vals = devs?.findAll { - Double t = it?.currentValue(attr)?.toDouble() - (t != null /* && t != 0 */ ) }?.collect { t } - return vals?.size() ? (vals.sum()/vals.size()).round(1).toDouble() : null -} - -String getCurrentMode() { - return location?.mode -} - -List getLocationModes(Boolean sorted=false) { - List modes = location?.modes*.name - // log.debug "modes: ${modes}" - return (sorted) ? modes?.sort() : modes -} - -static List getLocationPistons() { - List aa = ((List)webCoREFLD?.pistons ?: []).findAll { it.id }.collect { (String)it.id } - return aa ?: [] -} -/* -private getModeById(mId) { - return location?.getModes()?.find{it?.id == mId} -} */ - -Boolean isInMode(List modes, Boolean not=false) { - Boolean fnd = false - if(modes) { - fnd = (getCurrentMode() in modes) - fnd = (not ? !fnd : fnd) - } - return fnd -} - -Boolean isInAlarmMode(List modes) { - String a = (String)location?.hsmStatus ?: "disarmed" - //return (modes) ? (parent?.getAlarmSystemStatus() in modes) : false - return (modes) ? (a in modes) : false -} -/* -Boolean areAllDevsSame(List devs, String attr, val) { - if(devs && attr) { return (devs?.findAll { it?.currentValue(attr) == val as String }?.size() == devs?.size()) } - return false -} */ - -Boolean allDevAttValsEqual(List devs, String att, val) { - if(devs && att) { - if(val instanceof List) return (devs.findAll { it?.currentValue(att) in val }?.size() == devs?.size()) - else return (devs.findAll { it?.currentValue(att) == val }?.size() == devs?.size()) - } - return false -} - -Boolean anyDevAttValsEqual(List devs, String att, val) { - if(devs && att) { - if(val instanceof List) return (devs.findAll { it?.currentValue(att) in (List)val }?.size() >= 1) - else return (devs.findAll { it?.currentValue(att) == val }?.size() >= 1) - } - return false -} - -Boolean anyDevAttNumValAbove(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() >= 1) : false -} -Boolean anyDevAttNumValBelow(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() >= 1) : false -} -Boolean anyDevAttNumValBetween(List devs, String att, Double low, Double high, Boolean not) { - return (devs && att && (low < high)) ? (devs?.findAll { - Double t = it?.currentValue(att)?.toDouble() - if(not) ( (t < low) || (t > high) ) - else ( (t >= low) && (t <= high) ) }?.size() >= 1) : false -} -Boolean anyDevAttNumValEqual(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() >= 1) : false -} - -Boolean allDevAttNumValsAbove(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsBelow(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsBetween(List devs, String att, Double low, Double high, Boolean not) { - return (devs && att && (low < high)) ? (devs?.findAll { - Double t = it?.currentValue(att)?.toDouble() - if(not) ( (t < low) || (t > high) ) - else ( (t >= low) && (t <= high) ) }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsEqual(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() == devs?.size()) : false -} - -Boolean devAttValEqual(List devs, String devId, String att, val) { - if(devs && att) { - def dev = devs.find { it?.id?.toString() == devId } - if(dev) return (dev?.currentValue(att) == val) - } - return false -} - -static String getAlarmSystemName(Boolean abbr=false) { - return (abbr ? "HSM" : "Hubitat Safety Monitor") -} -/****************************************** -| Time and Date Conversion Functions -*******************************************/ -String formatDt(Date dt, Boolean tzChg=true) { - return dateTimeFmt(dt, "E MMM dd HH:mm:ss z yyyy", tzChg) -} - -String dateTimeFmt(Date dt, String fmt, Boolean tzChg=true) { -// if(!(dt instanceof Date)) { try { dt = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dt?.toString()) } catch(e) { dt = Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } } - SimpleDateFormat tf = new SimpleDateFormat(fmt) - if(tzChg && location?.timeZone) { tf.setTimeZone((TimeZone)location.timeZone) } - return (String)tf.format(dt) -} - -String convToTime(Date dt) { - String newDt = dateTimeFmt(dt, "h:mm a") - if(newDt?.contains(":00 ")) { newDt?.replaceAll(":00 ", sSPACE) } - return newDt -} - -String convToDate(Date dt) { - String newDt = dateTimeFmt(dt, "EEE, MMM d") - return newDt -} - -String convToDateTime(Date dt) { - String t = dateTimeFmt(dt, "h:mm a") - String d = dateTimeFmt(dt, "EEE, MMM d") - return d+', '+t -} - -static Date parseDate(dt) { return Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } - -//static Boolean isDateToday(Date dt) { return (dt && dt.clearTime().compareTo(new Date().clearTime()) >= 0) } -static Boolean isDateToday(Date dt) { return (dt && dt.clearTime() >= new Date().clearTime()) } - -static String strCapitalize(str) { return str ? str.toString().capitalize() : sNULL } - -static String pluralizeStr(List obj, Boolean para=true) { return (obj?.size() > 1) ? "${para ? "(s)": "s"}" : sBLANK } - -String parseFmtDt(String parseFmt, String newFmt, dt) { - Date newDt = Date.parse(parseFmt, dt?.toString()) - return dateTimeFmt(newDt, newFmt) -} - -String getDtNow() { - Date now = new Date() - return formatDt(now) -} - -String epochToTime(Date dt) { - return dateTimeFmt(dt, "h:mm a") -} - -String fmtTime(t, String fmt="h:mm a", Boolean altFmt=false) { - if(!t) return sNULL - Date dt = new Date().parse(altFmt ? "E MMM dd HH:mm:ss z yyyy" : "yyyy-MM-dd'T'HH:mm:ss.SSSZ", t.toString()) - return dateTimeFmt(dt, fmt) -} - -Long GetTimeDiffSeconds(String lastDate, String sender=sNULL) { - try { - if(lastDate?.contains("dtNow")) { return 10000 } - Date lastDt = parseDate(lastDate) // Date.parse("E MMM dd HH:mm:ss z yyyy", lastDate) - Long start = lastDt.getTime() - Long stop = now() - Long diff = Math.round((stop - start) / 1000L) - return diff.abs() - } - catch (ex) { - logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : sBLANK}lastDate: $lastDate): ${ex?.message}", false, ex) - return 10000L - } -} - -Map getDateMap() { - Map m = [:] - Date d = new Date() - m.dayOfYear = dateTimeFmt(d, "DD") - m.dayNameShort = dateTimeFmt(d, "EEE").toUpperCase() - m.dayName = dateTimeFmt(d, "EEEE") - m.day = dateTimeFmt(d, "d") - m.week = dateTimeFmt(d, "W") - m.weekOfYear = dateTimeFmt(d, "w") - m.monthName = dateTimeFmt(d, "MMMMM") - m.month = dateTimeFmt(d, "MM") - m.year = dateTimeFmt(d, "yyyy") - m.hour = dateTimeFmt(d, "hh") - m.minute = dateTimeFmt(d, "mm") - m.second = dateTimeFmt(d, "ss") - return m -} - -Boolean isDayOfWeek(List opts) { - SimpleDateFormat df = new SimpleDateFormat("EEEE") - df?.setTimeZone((TimeZone)location?.timeZone) - String day = df?.format(new Date()) - return opts?.contains(day) -} - -Boolean isMonthOfYear(List opts) { - Map dtMap = getDateMap() - return ( opts?.contains((String)dtMap.monthName) ) -} - -Boolean isTimeBetween(String startTime, String stopTime, Date curTime= new Date()) { - if(!startTime && !stopTime) { return true } - Date st = toDateTime(startTime) - Date et = toDateTime(stopTime) - return timeOfDayIsBetween(st, et, curTime, (TimeZone)location?.timeZone) -} - -/****************************************** -| App Input Description Functions -*******************************************/ - -static Map dimmerLevelEnum() { return [100:"Set Level to 100%", 90:"Set Level to 90%", 80:"Set Level to 80%", 70:"Set Level to 70%", 60:"Set Level to 60%", 50:"Set Level to 50%", 40:"Set Level to 40%", 30:"Set Level to 30%", 20:"Set Level to 20%", 10:"Set Level to 10%"] } - -/* -String unitStr(type) { - switch(type) { - case "temp": - return "\u00b0${getTemperatureScale() ?: "F"}" - case sHUMID: - return "%" - default: - return sBLANK - } -} */ - -String getAppNotifDesc(Boolean hide=false) { - String str = sBLANK - if(isActNotifConfigured()) { - Boolean ok = getOk2Notify() - str += hide ? sBLANK : spanSmBr(strUnder("Send allowed: ") + getOkOrNotSymHTML(ok)) - str += ((List)settings.notif_devs) ? spanSmBr(" ${sBULLET} Notification Device${pluralizeStr((List)settings.notif_devs)} (${((List)settings.notif_devs).size()})") : sBLANK - str += (Boolean)settings.notif_alexa_mobile ? spanSmBr(" ${sBULLET} Alexa Mobile App") : sBLANK - String res = getNotifSchedDesc(true) - str += res ? spanSmBr(res) : sBLANK - } - return str != sBLANK ? str : sNULL -} - -List getQuietDays() { - List allDays = weekDaysEnum() - List curDays = (List)settings.notif_days ?: [] - return allDays.findAll { (!curDays?.contains(it)) } -} - -@SuppressWarnings('GroovyVariableNotAssigned') -String getNotifSchedDesc(Boolean min=false) { - String startType = settings.notif_time_start_type - Date startTime - String stopType = settings.notif_time_stop_type - Date stopTime - List dayInput = (List)settings.notif_days - List modeInput = (List)settings.notif_modes - String str = sBLANK - - if(startType && stopType) { - startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null - stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null - } - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L - Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - Boolean timeOk = notifTimeOk() - Boolean daysOk = dayInput ? isDayOfWeek(dayInput) : true - Boolean modesOk = modeInput ? isInMode(modeInput) : true - Boolean rest = !(daysOk && modesOk && timeOk) - String startLbl = startTime ? epochToTime(startTime) : sBLANK - String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK - String a=s5SPACE+sBULLET+sSPACE - str += (startLbl && stopLbl) ? spanSmBr("${a}Restricted Times: ${startLbl} - ${stopLbl} " + getOkOrNotSymHTML(!timeOk)) : sBLANK - List qDays = getQuietDays() - str += dayInput && qDays ? spanSmBr("${a}Restricted Day${pluralizeStr(qDays, false)}: (${qDays?.join(", ")}) " + getOkOrNotSymHTML(!daysOk)) : sBLANK - str += modeInput ? spanSm("${a}Allowed Mode${pluralizeStr(modeInput, false)}: (${modeInput?.join(", ")}) " + getOkOrNotSymHTML(!modesOk)) : sBLANK - str = str ? spanSmBr(" ${sBULLET} Restrictions Active: " + getOkOrNotSymHTML(rest)) + spanSm(str) : sBLANK - return (str != sBLANK) ? divSm(str, sCLR4D9) : sNULL -} - -String getTriggersDesc(Boolean hideDesc=false, Boolean addFoot=true) { - Boolean confd = triggersConfigured() - List setItem = (List)settings.triggerEvents - String sPre = sTRIG - if(confd && setItem?.size()) { - if(!hideDesc) { - String str = spanSmBldBr("Triggers${!addFoot ? " for ("+(String)buildActTypeEnum()."${(String)settings.actionType}" + ")" : sBLANK}:", sNULL) - String btstr = s4SPACE+sBULLETINV+sSPACE - setItem.each { String evt-> - String adder = sBLANK - String eH = sPre+evt - List myL = (List)settings."${eH}" - String tstr = s4SPACE+spanSmBld(sPLUS)+sSPACE - //noinspection GroovyFallthrough - switch(evt) { - case sSCHED: - String schedTyp = settings."${eH}_type" ?: sNULL - str += spanSmBr(" ${sBULLET} ${strUnder(evt?.capitalize())}${schedTyp ? " (${schedTyp})" : ""}") - if(schedTyp == sRECURRING) { - str += settings."${eH}_recurrence" ? spanSmBr(btstr+"Recurrence: (${settings."${eH}_recurrence"})") : sBLANK - str += settings."${eH}_time" ? spanSmBr(btstr+"Time: (${fmtTime(settings."${eH}_time")})") : sBLANK - str += settings."${eH}_weekdays" ? spanSmBr(btstr+"Week Days: (${settings."${eH}_weekdays"?.join(",")})") : sBLANK - str += settings."${eH}_daynums" ? spanSmBr(btstr+"Days of Month: (${settings."${eH}_daynums"?.size()})") : sBLANK - str += settings."${eH}_weeks" ? spanSmBr(btstr+"Weeks of Month: (${settings."${eH}_weeks"?.join(",")})") : sBLANK - str += settings."${eH}_months" ? spanSmBr(btstr+"Months: (${settings."${eH}_months"?.join(",")})") : sBLANK - } - if(schedTyp == sONETIME) { - str += settings."${eH}_time" ? spanSmBr(btstr+"Time: (${fmtTime(settings."${eH}_time")}))") : sBLANK - } - if(schedTyp in [sCSUNRISE, sCSUNSET]) { - str += settings."${eH}_sunState_offset" ? spanSmBr(btstr+"Offset: (${settings."${eH}_sunState_offset"})") : sBLANK - } - break - case sALRMSYSST: - str += spanSmBr(" ${sBULLET} ${strUnder(evt?.capitalize())} (${getAlarmSystemName(true)})" + myL ? " (${myL?.size()} Selected)" : sBLANK) - if ("alerts" in myL) str += (List)settings."${eH}_events" ? spanSmBr(btstr+"Alert Events: (${(List)settings."${eH}_events"})") : sBLANK - str += (Boolean)settings."${eH}_once" ? spanSmBr(btstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK - str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(btstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK - break - case sPISTNEXEC: - case sMODE: -// case "scene": - String typ = evt == sMODE ? "Mode" : "Piston" - str += myL ? spanSmBr(" ${sBULLET} "+ strUnder(typ) + pluralizeStr(myL, false) + " (${myL?.size()})") : sBLANK - str += (Boolean)settings."${eH}_once" ? spanSmBr(btstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK - str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(btstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK - break - case sPUSHED: - case sRELEASED: - case sHELD: - case sDBLTAP: - adder = "Button " - default: - str += spanSmBr(" ${sBULLET} ${adder}${strUnder(evt?.capitalize())}${myL ? " (${myL?.size()} Device" + pluralizeStr(myL, false) + ")" : sBLANK}") - String t_cmd = (String)settings."${eH}_cmd" - if(t_cmd in numOpts()) { - if (t_cmd in [sBETWEEN, sNBETWEEN]) { - str += spanSmBr(tstr+"Trigger Value ${t_cmd.capitalize()}: (${settings."${eH}_low"} - ${settings."${eH}_high"})") - } else { - str += (t_cmd == sABOVE && settings."${eH}_high"!=null) ? spanSmBr(tstr+"Trigger Value Above: (${settings."${eH}_high"})") : sBLANK - str += (t_cmd == sBELOW && settings."${eH}_low"!=null) ? spanSmBr(tstr+"Trigger Value Below: (${settings."${eH}_low"})") : sBLANK - str += (t_cmd == sEQUALS && settings."${eH}_equal"!=null) ? spanSmBr(tstr+"Trigger Value Equals: (${settings."${eH}_equal"})") : sBLANK - } - } else { - str += t_cmd ? spanSmBr(tstr+"Trigger State: (${t_cmd})") : sBLANK - } - str += settings."${eH}_nums" ? spanSmBr(tstr+"Button Numbers: ${settings."${eH}_nums"}") : sBLANK - str += (Integer)settings."${eH}_after"!=null ? spanSmBr(tstr+"Only After: (${settings."${eH}_after"} sec)") : sBLANK - str += (Integer)settings."${eH}_after_repeat" ? spanSmBr(tstr+"Repeat Every: (${settings."${eH}_after_repeat"} sec)") : sBLANK - str += (Integer)settings."${eH}_after_repeat_cnt" ? spanSmBr(tstr+"Repeat Count: (${settings."${eH}_after_repeat_cnt"})") : sBLANK - str += (Boolean)settings."${eH}_all" ? spanSmBr(tstr+"Require All: (${settings."${eH}_all"})") : sBLANK - str += (Boolean)settings."${eH}_ign_empty_type" ? spanSmBr(tstr+"Ignore Empty Event Type: (${settings."${eH}_ign_empty_type"})") : sBLANK - str += (Boolean)settings."${eH}_once" ? spanSmBr(tstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK - str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(tstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK - str += ((String)settings."${eH}_txt" || (String)settings."${eH}_after_repeat_txt") ? spanSmBr(tstr+"Custom Responses:") : sBLANK - str += (String)settings."${eH}_txt" ? spanSmBr(s3SPACE+tstr+"Events: (${((String)settings."${eH}_txt")?.tokenize(";")?.size()} Items)") : sBLANK - str += (String)settings."${eH}_after_repeat_txt" ? spanSmBr(s3SPACE+tstr+"Repeats: (${((String)settings."${eH}_after_repeat_txt")?.tokenize(";")?.size()} Items)") : sBLANK - break - } - } - str += addFoot ? inputFooter(sTTM) : sBLANK - return str - } else { return addFoot ? inputFooter(sTTM) : sBLANK } - } else { - return addFoot ? inputFooter("Tap to configure (Required!)", sCLRRED, true) : sBLANK - } -} - -String getOverallDesc() { - Map condStatus = conditionStatus() - return spanSmBld("Action Condition Status: ") + spanSmBr( ((Boolean)condStatus.ok ? "Active " : "Inactive ") + getOkOrNotSymHTML((Boolean)condStatus.ok)) -} - -String getConditionsDesc(Boolean addFoot=true) { - Boolean confd = conditionsConfigured() - String sPre = "cond_" - String str = sBLANK - String btstr = s4SPACE+sBULLETINV+sSPACE - if(confd) { - str = getOverallDesc() - str += spanSmBr(" ${sBULLET} " + spanSmBld("${reqAllCond() ? "All Conditions Required" : "Any Condition Allowed"}")) - if(timeCondConfigured()) { - str += spanSmBr(" ${sBULLET} Time Between Allowed: " + getOkOrNotSymHTML(timeCondOk())) - str += spanSmBr(btstr+"${getTimeCondDesc(false)}") - } - if(dateCondConfigured()) { - str += spanSmBr(" ${sBULLET} Date:") - str += (List)settings.cond_days ? spanSmBr(btstr+"Days Allowed: ${(List)settings.cond_days} " + getOkOrNotSymHTML(isDayOfWeek((List)settings.cond_days))) : sBLANK - str += (List)settings.cond_months ? spanSmBr(btstr+"Months Allowed: ${(List)settings.cond_months} " + getOkOrNotSymHTML(isMonthOfYear((List)settings.cond_months))) : sBLANK - } - - Boolean mC = locationModeConfigured() - Boolean aC = locationAlarmConfigured() - if(aC || mC) { - str += spanSmBr(" ${sBULLET} Location: " + getOkOrNotSymHTML(locationCondOk())) - if(aC) { - String a = (String)location?.hsmStatus ?: "disarmed" - List myL = (List)settings.cond_alarmSystemStatus - str += myL ? spanSmBr(btstr+"Alarm Mode ${a} in: ${myL} " + getOkOrNotSymHTML(isInAlarmMode(myL))) : sBLANK - } - if(mC) { - Boolean not = ((String)settings.cond_mode_cmd == "not") - str += (List)settings.cond_mode ? spanSmBr(btstr+"Mode ${getCurrentMode()} (${not ? "not in" : "in"}): ${(List)settings.cond_mode} " + getOkOrNotSymHTML(isInMode((List)settings.cond_mode, not))) : sBLANK - } - } - - if(deviceCondConfigured()) { - List devConds = lDATTSTR + lDATTNUM - devConds.each { String evt-> - String eH = sPre+evt - if(devCondConfigured(evt)) { - Boolean condOk = false - if(evt in lDATTSTR) { condOk = checkDeviceCondOk(evt) } - else if(evt in lDATTNUM) { condOk = checkDeviceNumCondOk(evt) } - - List devs = settings."${eH}" ?: null - if(devs){ - List myV = [] - if(!addFoot) devs.each { dev -> myV.push(dev?.currentValue(evt)?.toString()) } - str += spanSmBr(" ${sBULLET} ${evt?.capitalize()} (${settings."${eH}"?.size()}) ${!addFoot ? myV : sBLANK} " + getOkOrNotSymHTML(condOk)) - } - - String a = btstr+"Desired Value: " - String aG = (Boolean)settings."${eH}_avg" ? "(Avg)" : sBLANK - String cmd = settings."${eH}_cmd" ?: sNULL - if(cmd in numOpts()) { - def cmdLow = settings."${eH}_low"!=null ? settings."${eH}_low" : null - def cmdHigh = settings."${eH}_high"!=null ? settings."${eH}_high" : null - def cmdEq = settings."${eH}_equal"!=null ? settings."${eH}_equal" : null - String aU = attUnit(evt) + ')' + aG - str += (cmd == sEQUALS && cmdEq) ? spanSmBr(a+"( =${cmdEq}"+aU) : sBLANK - str += (cmd in [sBETWEEN, sNBETWEEN] && cmdLow && cmdHigh) ? spanSmBr(a+cmd.capitalize()+" (${cmdLow}-${cmdHigh}"+aU) : sBLANK - str += (cmd == sABOVE && cmdHigh) ? spanSmBr(a+"( >${cmdHigh}"+aU) : sBLANK - str += (cmd == sBELOW && cmdLow) ? spanSmBr(a+"( <${cmdLow}"+aU) : sBLANK - } else { - str += cmd ? spanSmBr(a+"(${cmd})" + aG) : sBLANK - } - str += (Boolean)settings."${eH}_all" ? spanSmBr(btstr+"Require All: (${settings."${eH}_all"})") : sBLANK - } - } - } - str += addFoot ? inputFooter(sTTM) : sBLANK - } else { - str += addFoot ? inputFooter(sTTC, sCLRGRY, true) : sBLANK - } - return str -} - -@SuppressWarnings('GroovyFallthrough') -static String attUnit(String attr) { - switch(attr) { - case sHUMID: - case sLEVEL: - case sBATT: - return " percent" - case sTEMP: - return " degrees" - case "illuminance": - return " lux" - case sPOWER: - return " watts" - default: - return sBLANK - } -} - -Map getZoneStatus() { - List echoZones = (List)settings.act_EchoZones ?: [] - Map res = [:] - if(echoZones.size()) { - Map allZones = getZones() - echoZones.each { String k -> if(allZones?.containsKey(k)) { res[k] = allZones[k] } } - } - return res -} - -String getZoneVolDesc(zone, MapvolMap) { - String str = sBLANK - String k = (String)zone.key - str += spanSm(" ${sBULLET} ${zone?.value?.name} ${zone?.value?.active == true ? spanSm(" (Active)", sCLRGRN2) : spanSm(" (Inactive)", sCLRGRY)}") - if((Boolean)settings.act_EchoZones_vol_per_zone && volMap && volMap[k]) { - String btstr = s4SPACE+sBULLETINV+sSPACE - str += volMap[k].change ? lineBr() + spanSm(btstr+"New Volume: ${volMap[k].change}") : sBLANK - str += volMap[k].restore ? lineBr() + spanSm(btstr+"Restore Volume: ${volMap[k].restore}") : sBLANK - } - return str -} - -String getActionDesc(Boolean addFoot=true) { - Boolean confd = executionConfigured() - String str = sBLANK - Map actMap = (Map)state.actionExecMap - Map znVolMap = actMap && actMap.config && actMap.config.zoneVolume ? (Map)actMap.config.zoneVolume : null - str += addFoot ? spanSmBr(strUnder("Action Type:")) : sBLANK - str += addFoot ? spanSmBr(" ${sBULLET} " + (String)buildActTypeEnum()."${(String)settings.actionType}") + lineBr() : sBLANK - if((String)settings.actionType && confd) { - Boolean isTierAct = isTierAction() - List eDevs = parent?.getDevicesFromList(settings.act_EchoDevices) - Map zones = getZoneStatus() - String tierDesc = isTierAct ? getTierRespDesc() : sNULL - String tierStart = isTierAct ? actTaskDesc(sACT_START) : sNULL - String tierStop = isTierAct ? actTaskDesc(sACT_STOP) : sNULL - str += zones?.size() ? spanSmBr(strUnder("Echo Zones:")) : sBLANK - str += zones?.size() ? spanSmBr(zones?.collect { getZoneVolDesc(it, znVolMap) }?.join(sLINEBR)) + (eDevs?.size() ? sLINEBR : sBLANK) : sBLANK - str += eDevs?.size() ? spanSm(strUnder("Alexa Devices:")) + (zones?.size() ? spanSm(" (Inactive Zone Default)", sCLRGRY) : sBLANK) + lineBr() + spanSmBr(eDevs?.collect { " ${sBULLET} ${it?.displayName?.toString()?.replace("Echo - ", sBLANK)}" }?.join(sLINEBR)) : sBLANK - str += tierDesc ? sLINEBR + spanSm(tierDesc) + (tierStart || tierStop ? sBLANK : sLINEBR) : sBLANK - str += tierStart ? spanSmBr(tierStart) : sBLANK - str += tierStop ? spanSmBr(tierStop) : sBLANK - str += settings.act_volume_change != null ? spanSmBr(" - New Volume: (${settings.act_volume_change})") : sBLANK - str += settings.act_volume_restore != null ? spanSmBr(" - Restore Volume: (${settings.act_volume_restore})") : sBLANK - str += settings.act_delay ? spanSmBr("Delay: (${settings.act_delay})") : sBLANK - - String mT = (String)settings.actionType - if(mT in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP]) mT = sSPEAK - if(mT in [sANN, sANNI]) mT = sANN - str += (mT in [sSPEAK, sANN] || isTierAct) && (String)settings."act_${mT}_txt" ? spanSmBr("Using Default Response: (True)") : sBLANK - - String trigTasks = !isTierAct ? actTaskDesc(sACT) + lineBr() : sNULL - str += trigTasks ? spanSm(trigTasks) : sBLANK - str += addFoot ? inputFooter(sTTM) : sBLANK - } - str += !confd && addFoot ? inputFooter("Tap to configure (Required!)", sCLRRED, true) : sBLANK - return divSm(str.replaceAll("\n\n\n", "\n\n"), sCLR4D9) -} - -@SuppressWarnings('GroovyVariableNotAssigned') -String getTimeCondDesc(Boolean addPre=true) { - Date startTime - Date stopTime - String startType = settings.cond_time_start_type - String stopType = settings.cond_time_stop_type - if(startType && stopType) { - startTime = startType == sTIME && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null - stopTime = stopType == sTIME && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null - } - - String startLbl1 = sBLANK - String stopLbl1 = sBLANK - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L - Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - startLbl1 = startType.capitalize() + sSPACE + startoffset ? "with offset " : sBLANK - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - stopLbl1 = stopType.capitalize() + sSPACE + stopoffset ? "with offset " : sBLANK - } - } - String startLbl = startTime ? epochToTime(startTime) : sBLANK - String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK - - return startLbl && stopLbl ? "${addPre ? "Time Condition:\n" : sBLANK}(${startLbl1}${startLbl} - ${stopLbl1}${stopLbl})" : inactFoot(sTTC) -} - -static String getInputToStringDesc(inpt, addSpace = null) { - Integer cnt = 0 - String str = sBLANK - if(inpt) { - inpt.sort().each { item -> - cnt = cnt+1 - str += item ? (((cnt < 1) || (inpt?.size() > 1)) ? "\n ${item}" : "${addSpace ? s6SPACE : sBLANK}${item}") : sBLANK - } - } - //log.debug "str: $str" - return (str != sBLANK) ? str : sNULL -} - -static String randomString(Integer len) { - def pool = ["a".."z", 0..9].flatten() - Random rand = new Random(new Date().getTime()) - def randChars = (0..len).collect { pool[rand.nextInt(pool.size())] } - // logDebug("randomString: ${randChars?.join()}") - return randChars.join() -} - -static def getRandomItem(List items) { - List list = [] // new ArrayList() - items?.each { list.add(it) } - return list.get(new Random().nextInt(list.size())) -} - -// public String gitBranch() { -// if(gitBranchFLD == sNULL) { gitBranchFLD = parent?.gitBranch() } -// return (String)gitBranchFLD -// } - -Boolean showChgLogOk() { return ((Boolean)state.isInstalled && !(Boolean)state.shownChgLog) } - -static String getAppImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/${imgName}.png" } - -static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } - -static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

    ${i ? " " : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}

    " } - -static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

    ${i ? " " : sBLANK} ${t?.replaceAll("\n", "
    ")}

    ${st ? "${st}" : sBLANK}" } - -static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
    " : sBLANK}${bold ? "" : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : ""}${color ? "
    " : ""}" } - -static String inTS1(String str, String img = sNULL, String clr=sNULL) { return spanSmBldUnd(str, clr, img) } -static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str?.replaceAll("\n", " ")?.replaceAll("
    ", " "), und), clr, img) } - -// Root HTML Objects -static String sectHead(String str, String img = sNULL) { return str ? "

    " + spanImgStr(img) + span(str, "darkorange", sNULL, true) + "

    " + "
    " : sBLANK } -static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } -static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
    ${str}
    ${br ? sLINEBR : sBLANK}" : sBLANK } -static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } -static String divImgStr(String str, String img=sNULL) { return str ? div(img ? spanImg(img) + span(str) : str) : sBLANK } -static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } -static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } -static String htmlLine(String color=sCLR4D9) { return "
    " } -static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } -static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? ((noBr ? sBLANK : lineBr()) + divSmBld(str, clr)) : sBLANK } -static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } -static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } -static String optPrefix() { return spanSm(" (Optional)", "violet") } -// - -// Custom versions of the root objects above -static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } -static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } -static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } -static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } -static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } -static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } -static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } -static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } -static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } -static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } -static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } -static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } - - -static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } -static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } -static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } -static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } -static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } -static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } -static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } - -static String textDonateUrl() { return "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HWBN4LB9NMHZ4" } -def appFooter() { - section() { - paragraph htmlLine("orange") - paragraph "
    Echo Speaks


    Please consider donating if you find this integration useful.
    " - } -} - -static String documentationUrl() { return "https://tonesto7.github.io/echo-speaks-docs" } -static String videoUrl() { return "https://www.youtube.com/watch?v=wQPPlTFaGb4&ab_channel=SimplySmart123%E2%9C%85" } - -static String bulletItem(String inStr, String strVal) { return "${inStr == sBLANK ? sBLANK : "\n"} \u2022 ${strVal}" } -static String dashItem(String inStr, String strVal, Boolean newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : "\n"} - ${strVal}" } - -Integer stateSize() { - String j = new JsonOutput().toJson((Map)state) - return j.length() -} - -Integer stateSizePerc() { return (int) ((stateSize() / 100000)*100).toDouble().round(0) } -/* -String debugStatus() { return !settings.appDebug ? "Off" : "On" } -String deviceDebugStatus() { return !settings.childDebug ? "Off" : "On" } -Boolean isAppDebug() { return (settings.appDebug == true) } -Boolean isChildDebug() { return (settings.childDebug == true) } - -String getAppDebugDesc() { - String str = sBLANK - str += isAppDebug() ? "App Debug: (${debugStatus()})" : sBLANK - str += isChildDebug() && str != sBLANK ? "\n" : sBLANK - str += isChildDebug() ? "Device Debug: (${deviceDebugStatus()})" : sBLANK - return (str != sBLANK) ? str : sNULL -}*/ - -private addToLogHistory(String logKey, String data, Integer max=10) { - Boolean ssOk = true // (stateSizePerc() > 70) - - getTheLock(sHMLF, "addToHistory(${logKey})") - - - List eData = (List)getMemStoreItem(logKey) - if(!(eData.find { it?.data == data })) { - eData.push([dt: getDtNow(), data: data]) - Integer lsiz = eData.size() - if (!ssOk || lsiz > max) { - eData = eData.drop((lsiz - max)) - } - updMemStoreItem(logKey, eData) - } - releaseTheLock(sHMLF) -} - -public Map getLogConfigs() { - return [ - info: (Boolean) settings.logInfo, - warn: (Boolean) settings.logWarn, - error: (Boolean) settings.logError, - debug: (Boolean) settings.logDebug, - trace: (Boolean) settings.logTrace - ] -} - -public void enableDebugLog() { settingUpdate("logDebug", sTRUE, sBOOL); logInfo("Debug Logs Enabled From Main App...") } -public void disableDebugLog() { settingUpdate("logDebug", sFALSE, sBOOL); logInfo("Debug Logs Disabled From Main App...") } -public void enableTraceLog() { settingUpdate("logTrace", sTRUE, sBOOL); logInfo("Trace Logs Enabled From Main App...") } -public void disableTraceLog() { settingUpdate("logTrace", sFALSE, sBOOL); logInfo("Trace Logs Disabled From Main App...") } - -private void logDebug(String msg) { if(isDbg()) { log.debug logPrefix(msg, "purple") } } -private void logInfo(String msg) { if((Boolean)settings.logInfo != false) { log.info sSPACE + logPrefix(msg, "#0299b1") } } -private void logTrace(String msg) { if(isTrc()) { log.trace logPrefix(msg, sCLRGRY) } } -private void logWarn(String msg, Boolean noHist=false) { if((Boolean)settings.logWarn != false) { log.warn sSPACE + logPrefix(msg, sCLRORG) }; if(!noHist) { addToLogHistory("warnHistory", msg, 15) } } - -private Boolean isTrc(){ return (Boolean)settings.logTrace } -private Boolean isDbg(){ return (Boolean)settings.logDebug } - -void logError(String msg, Boolean noHist=false, ex=null) { - if((Boolean)settings.logError != false) { - log.error logPrefix(msg, sCLRRED) - String a - try { - if (ex) a = getExceptionMessageWithLine(ex) - } catch (ignored) { - } - if(a) log.error logPrefix(a, sCLRRED) - } - if(!noHist) { addToLogHistory("errorHistory", msg, 15) } -} - -static String logPrefix(String msg, String color = sNULL) { - return span("Action (v" + appVersionFLD + ") | ", sCLRGRY) + span(msg, color) -} - -Map getLogHistory() { - getTheLock(sHMLF, "getLogHistory") - - List warn = (List)getMemStoreItem("warnHistory") - List errs = (List)getMemStoreItem("errorHistory") - releaseTheLock(sHMLF) - return [ warnings: []+warn, errors: []+errs ] -} - -/* -private void clearLogHistory() { - getTheLock(sHMLF, "clearLogHistory") - - updMemStoreItem("warnHistory", []) - updMemStoreItem("errorHistory", []) - releaseTheLock(sHMLF) -} */ - -// FIELD VARIABLE FUNCTIONS -private void updMemStoreItem(String key, val) { - String appId = app.getId() - Map memStore = historyMapFLD[appId] ?: [:] - memStore[key] = val - historyMapFLD[appId] = memStore - historyMapFLD = historyMapFLD - // log.debug("updMemStoreItem(${key}): ${memStore[key]}") -} - -private getMemStoreItem(String key, defVal=[]){ - String appId = app.getId() - Map memStore = historyMapFLD[appId] ?: [:] - return memStore[key] ?: defVal -} - -/* -// Memory Barrier -@Field static java.util.concurrent.Semaphore theMBLockFLD=new java.util.concurrent.Semaphore(0) -static void mb(String meth=sNULL){ - if((Boolean)theMBLockFLD.tryAcquire()){ - theMBLockFLD.release() - } -} */ - -@Field static final String sHMLF = 'theHistMapLockFLD' -@Field static Semaphore histMapLockFLD = new Semaphore(1) - -private Integer getSemaNum(String name) { - if(name==sHMLF) return 0 - log.warn "unrecognized lock name..." - return 0 - // Integer stripes=22 - // if(name.isNumber()) return name.toInteger()%stripes - // Integer hash=smear(name.hashCode()) - // return Math.abs(hash)%stripes - // log.info "sema $name # $sema" -} - -Semaphore getSema(Integer snum){ - switch(snum) { - case 0: return histMapLockFLD - default: log.error "bad hash result $snum" - return null - } -} - -@Field volatile static Map lockTimesFLD = [:] -@Field volatile static Map lockHolderFLD = [:] - -void getTheLock(String qname, String meth=sNULL, Boolean longWait=false) { - Long waitT = longWait ? 1000L : 60L -// Boolean wait = false - Integer semaNum = getSemaNum(qname) - String semaSNum = semaNum.toString() - Semaphore sema = getSema(semaNum) - while(!((Boolean)sema.tryAcquire())) { - // did not get the lock - Long timeL = lockTimesFLD[semaSNum] - if(timeL == null){ - timeL = now() - lockTimesFLD[semaSNum] = timeL - lockTimesFLD = lockTimesFLD - } - if(devModeFLD) log.warn "waiting for ${qname} ${semaSNum} lock access, $meth, long: $longWait, holder: ${(String)lockHolderFLD[semaSNum]}" - pauseExecution(waitT) -// wait = true - if((now() - timeL) > 30000L) { - releaseTheLock(qname) - if(devModeFLD) log.warn "overriding lock $meth" - } - } - lockTimesFLD[semaSNum] = (Long)now() - lockTimesFLD = lockTimesFLD - lockHolderFLD[semaSNum] = "${app.getId()} ${meth}".toString() - lockHolderFLD = lockHolderFLD -// return wait -} - -void releaseTheLock(String qname){ - Integer semaNum=getSemaNum(qname) - String semaSNum=semaNum.toString() - Semaphore sema=getSema(semaNum) - lockTimesFLD[semaSNum]=null - lockTimesFLD=lockTimesFLD - lockHolderFLD[semaSNum]=sNULL - lockHolderFLD=lockHolderFLD - sema.release() -} - -static String convMusicProvider(String prov) { - switch (prov) { - case "My Library": - return "CLOUDPLAYER" - case "Amazon Music": - return "AMAZON_MUSIC" - case "Apple Music": - return "APPLE_MUSIC" - case "TuneIn": - return "TUNEIN" - case "Pandora": - return "PANDORA" - case "SiriusXM": - return "SIRIUSXM" - case "Spotify": - return "SPOTIFY" - case "Tidal": - return "TIDAL" - case "iHeartRadio": - return "I_HEART_RADIO" - default: return sBLANK - } -} - -/************************************************ - SEQUENCE TEST LOGIC -*************************************************/ - -def searchTuneInResultsPage() { - return dynamicPage(name: "searchTuneInResultsPage", uninstall: false, install: false) { - Map results = parent.executeTuneInSearch((String)settings.tuneinSearchQuery) - section(sectHead("Search Results: (Query: ${(String)settings.tuneinSearchQuery})")) { - if(results?.browseList && results?.browseList?.size()) { - results?.browseList?.eachWithIndex { item, i-> - if(i < 25) { - if(item?.browseList != null && item?.browseList?.size()) { - item?.browseList?.eachWithIndex { item2, i2-> - String str = sBLANK - str += "ContentType: (${item2?.contentType})" - str += "\nId: (${item2?.id})" - str += "\nDescription: ${item2?.description}" - href "searchTuneInResultsPage", title: pTS((String)item2?.name?.take(75), (String)item2?.image), description: str - } - } else { - String str = sBLANK - str += "ContentType: (${item?.contentType})" - str += "\nId: (${item?.id})" - str += "\nDescription: ${item?.description}" - href "searchTuneInResultsPage", title: pTS( (String)((String)item?.name)?.take(75), (String)item?.image), description: str - } - } - } - } else { paragraph "No Results found..." } - } - } -} - -private getColorName(desiredColor, level=null) { - String desC = desiredColor?.toLowerCase() - for (color in colorSettingsListFLD) { - if (((String)color.name)?.toLowerCase() == desC) { - Integer hue = Math.round((Integer)color.h / 3.6).toInteger() - Integer lvl = level ?: (Integer)color.l - return [hue: hue, saturation: (Integer)color.s, level: lvl] - } - } - logWarn("getColorName ${desC} not found") -} - -// if there is no colorMaps, then flash lights -private void startFlashLights(String p, Integer cycles, Integer cycleT, Map color1Map=null, Map color2Map=null) { - logDebug("startFlashLights ${p} | cycles: ${cycles} | cycle time: ${cycleT} | color1Map: ${color1Map} | color2Map: ${color2Map}") - Map data = [type: p, first: true, cycles: cycles, cycleT: cycleT, color1Map: color1Map, colorMap2: color2Map ] - flashLights(data) -} - -private void flashLights(Map data) { - String msg = "flashLights | " - if(devModeFLD) logTrace(msg+"state: ${data}") - - String p = data?.type - if(!data || !p) return - List devs = (List)settings."${p}lights" - Integer cT = (Integer)data.cycleT - Integer cS = (Integer)data.cycles - if(!devs || !cT || !cS || cT < 1 || cS < 1) return - if(cT > 60) data.cycleT=60 - if(cS > 200) data.cycles = 200 - - if((Boolean)data.first) { - data.first = false - logTrace(msg+"capturing state") - captureLightState(devs, p) - if(data.color1Map && data.color2Map) { - data.state = "a" - devs*.on() - } else data.state = "off" - data.cycle = 1 - } - - switch(data.state){ - case "off": - data.state = "on"; devs*.on() - break - case "on": - data.state = "off"; devs*.off() - data.cycle = data.cycle + 1 - break - case "a": - data.state = "b"; devs*.setColor(data.color1Map) - break - case "b": - data.state = "a"; devs*.setColor(data.color2Map) - data.cycle = data.cycle + 1 - break - } - if(data.cycle <= cS ) { - runIn(cT, "flashLights", [data: data]) - } else { - logTrace(msg+"restoring state") - restoreLightState(devs, p) - logDebug(msg+"Ending") - } -} - -private restoreLights(data) { - String p = data?.type ?: sNULL - if(p && settings."${p}lights") { restoreLightState((List)settings."${p}lights", p) } -} - -void captureLightState(List devs, String p) { - String e = p+sLRM - String msg = "captureLightState | ${e} | " - logTrace(msg+"${devs}") - Map sMap = [:] - if(devs && p) { - devs.each { dev-> - String dId = dev?.id - sMap[dId] = [:] - [sSWITCH, sLEVEL, "hue", "saturation", "colorTemperature", "color"].each { String att -> - if(dev?.hasAttribute(att)) { sMap[dId]."${att}" = dev?.currentValue(att) } - } - } - state."${e}" = sMap - } - if(devModeFLD) logDebug(msg+"sMap: $sMap") - //logDebug(msg+ "sMap: $sMap") -} - -void restoreLightState(List devs, String p) { - String e = p+sLRM - String msg = "restoreLightState | ${e} | " - logTrace(msg+"${devs}") - if(devs && p) { - Boolean doP = true // do pauses - Long paus = 100L - Boolean dco = false // disable command optimization - - Map sMap = state."${e}" - if(devModeFLD) logDebug(msg+"sMap: $sMap") - if(sMap?.size()) { - devs.each { dev-> - String dId = dev?.id - if(sMap.containsKey(dId)) { - Map sD = sMap[dId] - if(devModeFLD) logDebug(msg+"${sD}") - Boolean wOn = (sD.switch == "on") - Boolean isOn = (dev.currentValue(sSWITCH) == "on") - Boolean chgLvl = sD.level ? (dev.currentValue(sLEVEL) != sD.level) : false - Boolean chgHSL = (sD.level && sD.saturation && sD.hue && dev.hasCommand("setColor")) - Boolean chgCtemp = sD.colorTemperature ? (dev.currentValue("colorTemperature") != sD.colorTemperature) : false - // CHECK IF ANYTHING NEEDS CHANGES? - if((sD.level || sD.colorTemperature) && (dco || !isOn)) { - dev.on() - isOn = true - if(doP) pauseExecution(paus) - } - if(sD.level) { - if(chgHSL) { - dev?.setColor([h: sD.hue, s: sD.saturation, l: sD.level]) - if(doP) pauseExecution(paus) - } else{ - if((dco || chgLvl) && dev.hasCommand("setLevel")) { - dev?.setLevel(sD.level) - if(!wOn && doP) pauseExecution(paus) - } - } - } - if(sD.colorTemperature) { - if((dco || chgCtemp) && dev.hasCommand("setColorTemperature")) { - dev?.setColorTemperature(sD.colorTemperature) - if(!wOn && doP) pauseExecution(paus) - } - } - if(dco || (wOn != isOn)) dev?."${sD.switch}"() - } else logWarn(msg+"no saved state for device found") - } - } else logWarn(msg+"no saved state found") - state.remove(e) - } -} - -@Field static final List colorSettingsListFLD = [ - [name: "Soft White", rgb: "#B6DA7C", h: 83, s: 44, l: 67], [name: "Warm White", rgb: "#DAF17E", h: 51, s: 20, l: 100], [name: "Very Warm White", rgb: "#DAF17E", h: 51, s: 60, l: 51], - [name: "Daylight White", rgb: "#CEF4FD", h: 191, s: 9, l: 90], [name: "Daylight", rgb: "#CEF4FD", h: 191, s: 9, l: 90], [name: "Cool White", rgb: "#F3F6F7", h: 187, s: 19, l: 96], - [name: "White", rgb: "#FFFFFF", h: 0, s: 0, l: 100], [name: "Alice Blue", rgb: "#F0F8FF", h: 208, s: 100, l: 97], [name: "Antique White", rgb: "#FAEBD7", h: 34, s: 78, l: 91], - [name: "Aqua", rgb: "#00FFFF", h: 180, s: 100, l: 50], [name: "Aquamarine", rgb: "#7FFFD4", h: 160, s: 100, l: 75], [name: "Azure", rgb: "#F0FFFF", h: 180, s: 100, l: 97], - [name: "Beige", rgb: "#F5F5DC", h: 60, s: 56, l: 91], [name: "Bisque", rgb: "#FFE4C4", h: 33, s: 100, l: 88], [name: "Blanched Almond", rgb: "#FFEBCD", h: 36, s: 100, l: 90], - [name: "Blue", rgb: "#0000FF", h: 240, s: 100, l: 50], [name: "Blue Violet", rgb: "#8A2BE2", h: 271, s: 76, l: 53], [name: "Brown", rgb: "#A52A2A", h: 0, s: 59, l: 41], - [name: "Burly Wood", rgb: "#DEB887", h: 34, s: 57, l: 70], [name: "Cadet Blue", rgb: "#5F9EA0", h: 182, s: 25, l: 50], [name: "Chartreuse", rgb: "#7FFF00", h: 90, s: 100, l: 50], - [name: "Chocolate", rgb: "#D2691E", h: 25, s: 75, l: 47], [name: "Coral", rgb: "#FF7F50", h: 16, s: 100, l: 66], [name: "Corn Flower Blue", rgb: "#6495ED", h: 219, s: 79, l: 66], - [name: "Corn Silk", rgb: "#FFF8DC", h: 48, s: 100, l: 93], [name: "Crimson", rgb: "#DC143C", h: 348, s: 83, l: 58], [name: "Cyan", rgb: "#00FFFF", h: 180, s: 100, l: 50], - [name: "Dark Blue", rgb: "#00008B", h: 240, s: 100, l: 27], [name: "Dark Cyan", rgb: "#008B8B", h: 180, s: 100, l: 27], [name: "Dark Golden Rod", rgb: "#B8860B", h: 43, s: 89, l: 38], - [name: "Dark Gray", rgb: "#A9A9A9", h: 0, s: 0, l: 66], [name: "Dark Green", rgb: "#006400", h: 120, s: 100, l: 20], [name: "Dark Khaki", rgb: "#BDB76B", h: 56, s: 38, l: 58], - [name: "Dark Magenta", rgb: "#8B008B", h: 300, s: 100, l: 27], [name: "Dark Olive Green", rgb: "#556B2F", h: 82, s: 39, l: 30], [name: "Dark Orange", rgb: "#FF8C00", h: 33, s: 100, l: 50], - [name: "Dark Orchid", rgb: "#9932CC", h: 280, s: 61, l: 50], [name: "Dark Red", rgb: "#8B0000", h: 0, s: 100, l: 27], [name: "Dark Salmon", rgb: "#E9967A", h: 15, s: 72, l: 70], - [name: "Dark Sea Green", rgb: "#8FBC8F", h: 120, s: 25, l: 65], [name: "Dark Slate Blue", rgb: "#483D8B", h: 248, s: 39, l: 39], [name: "Dark Slate Gray", rgb: "#2F4F4F", h: 180, s: 25, l: 25], - [name: "Dark Turquoise", rgb: "#00CED1", h: 181, s: 100, l: 41], [name: "Dark Violet", rgb: "#9400D3", h: 282, s: 100, l: 41], [name: "Deep Pink", rgb: "#FF1493", h: 328, s: 100, l: 54], - [name: "Deep Sky Blue", rgb: "#00BFFF", h: 195, s: 100, l: 50], [name: "Dim Gray", rgb: "#696969", h: 0, s: 0, l: 41], [name: "Dodger Blue", rgb: "#1E90FF", h: 210, s: 100, l: 56], - [name: "Fire Brick", rgb: "#B22222", h: 0, s: 68, l: 42], [name: "Floral White", rgb: "#FFFAF0", h: 40, s: 100, l: 97], [name: "Forest Green", rgb: "#228B22", h: 120, s: 61, l: 34], - [name: "Fuchsia", rgb: "#FF00FF", h: 300, s: 100, l: 50], [name: "Gainsboro", rgb: "#DCDCDC", h: 0, s: 0, l: 86], [name: "Ghost White", rgb: "#F8F8FF", h: 240, s: 100, l: 99], - [name: "Gold", rgb: "#FFD700", h: 51, s: 100, l: 50], [name: "Golden Rod", rgb: "#DAA520", h: 43, s: 74, l: 49], [name: "Gray", rgb: "#808080", h: 0, s: 0, l: 50], - [name: "Green", rgb: "#008000", h: 120, s: 100, l: 25], [name: "Green Yellow", rgb: "#ADFF2F", h: 84, s: 100, l: 59], [name: "Honeydew", rgb: "#F0FFF0", h: 120, s: 100, l: 97], - [name: "Hot Pink", rgb: "#FF69B4", h: 330, s: 100, l: 71], [name: "Indian Red", rgb: "#CD5C5C", h: 0, s: 53, l: 58], [name: "Indigo", rgb: "#4B0082", h: 275, s: 100, l: 25], - [name: "Ivory", rgb: "#FFFFF0", h: 60, s: 100, l: 97], [name: "Khaki", rgb: "#F0E68C", h: 54, s: 77, l: 75], [name: "Lavender", rgb: "#E6E6FA", h: 240, s: 67, l: 94], - [name: "Lavender Blush", rgb: "#FFF0F5", h: 340, s: 100, l: 97], [name: "Lawn Green", rgb: "#7CFC00", h: 90, s: 100, l: 49], [name: "Lemon Chiffon", rgb: "#FFFACD", h: 54, s: 100, l: 90], - [name: "Light Blue", rgb: "#ADD8E6", h: 195, s: 53, l: 79], [name: "Light Coral", rgb: "#F08080", h: 0, s: 79, l: 72], [name: "Light Cyan", rgb: "#E0FFFF", h: 180, s: 100, l: 94], - [name: "Light Golden Rod Yellow", rgb: "#FAFAD2", h: 60, s: 80, l: 90], [name: "Light Gray", rgb: "#D3D3D3", h: 0, s: 0, l: 83], [name: "Light Green", rgb: "#90EE90", h: 120, s: 73, l: 75], - [name: "Light Pink", rgb: "#FFB6C1", h: 351, s: 100, l: 86], [name: "Light Salmon", rgb: "#FFA07A", h: 17, s: 100, l: 74], [name: "Light Sea Green", rgb: "#20B2AA", h: 177, s: 70, l: 41], - [name: "Light Sky Blue", rgb: "#87CEFA", h: 203, s: 92, l: 75], [name: "Light Slate Gray", rgb: "#778899", h: 210, s: 14, l: 53], [name: "Light Steel Blue", rgb: "#B0C4DE", h: 214, s: 41, l: 78], - [name: "Light Yellow", rgb: "#FFFFE0", h: 60, s: 100, l: 94], [name: "Lime", rgb: "#00FF00", h: 120, s: 100, l: 50], [name: "Lime Green", rgb: "#32CD32", h: 120, s: 61, l: 50], - [name: "Linen", rgb: "#FAF0E6", h: 30, s: 67, l: 94], [name: "Maroon", rgb: "#800000", h: 0, s: 100, l: 25], [name: "Medium Aquamarine", rgb: "#66CDAA", h: 160, s: 51, l: 60], - [name: "Medium Blue", rgb: "#0000CD", h: 240, s: 100, l: 40], [name: "Medium Orchid", rgb: "#BA55D3", h: 288, s: 59, l: 58], [name: "Medium Purple", rgb: "#9370DB", h: 260, s: 60, l: 65], - [name: "Medium Sea Green", rgb: "#3CB371", h: 147, s: 50, l: 47], [name: "Medium Slate Blue", rgb: "#7B68EE", h: 249, s: 80, l: 67], [name: "Medium Spring Green", rgb: "#00FA9A", h: 157, s: 100, l: 49], - [name: "Medium Turquoise", rgb: "#48D1CC", h: 178, s: 60, l: 55], [name: "Medium Violet Red", rgb: "#C71585", h: 322, s: 81, l: 43], [name: "Midnight Blue", rgb: "#191970", h: 240, s: 64, l: 27], - [name: "Mint Cream", rgb: "#F5FFFA", h: 150, s: 100, l: 98], [name: "Misty Rose", rgb: "#FFE4E1", h: 6, s: 100, l: 94], [name: "Moccasin", rgb: "#FFE4B5", h: 38, s: 100, l: 85], - [name: "Navajo White", rgb: "#FFDEAD", h: 36, s: 100, l: 84], [name: "Navy", rgb: "#000080", h: 240, s: 100, l: 25], [name: "Old Lace", rgb: "#FDF5E6", h: 39, s: 85, l: 95], - [name: "Olive", rgb: "#808000", h: 60, s: 100, l: 25], [name: "Olive Drab", rgb: "#6B8E23", h: 80, s: 60, l: 35], [name: "Orange", rgb: "#FFA500", h: 39, s: 100, l: 50], - [name: "Orange Red", rgb: "#FF4500", h: 16, s: 100, l: 50], [name: "Orchid", rgb: "#DA70D6", h: 302, s: 59, l: 65], [name: "Pale Golden Rod", rgb: "#EEE8AA", h: 55, s: 67, l: 80], - [name: "Pale Green", rgb: "#98FB98", h: 120, s: 93, l: 79], [name: "Pale Turquoise", rgb: "#AFEEEE", h: 180, s: 65, l: 81], [name: "Pale Violet Red", rgb: "#DB7093", h: 340, s: 60, l: 65], - [name: "Papaya Whip", rgb: "#FFEFD5", h: 37, s: 100, l: 92], [name: "Peach Puff", rgb: "#FFDAB9", h: 28, s: 100, l: 86], [name: "Peru", rgb: "#CD853F", h: 30, s: 59, l: 53], - [name: "Pink", rgb: "#FFC0CB", h: 350, s: 100, l: 88], [name: "Plum", rgb: "#DDA0DD", h: 300, s: 47, l: 75], [name: "Powder Blue", rgb: "#B0E0E6", h: 187, s: 52, l: 80], - [name: "Purple", rgb: "#800080", h: 300, s: 100, l: 25], [name: "Red", rgb: "#FF0000", h: 0, s: 100, l: 50], [name: "Rosy Brown", rgb: "#BC8F8F", h: 0, s: 25, l: 65], - [name: "Royal Blue", rgb: "#4169E1", h: 225, s: 73, l: 57], [name: "Saddle Brown", rgb: "#8B4513", h: 25, s: 76, l: 31], [name: "Salmon", rgb: "#FA8072", h: 6, s: 93, l: 71], - [name: "Sandy Brown", rgb: "#F4A460", h: 28, s: 87, l: 67], [name: "Sea Green", rgb: "#2E8B57", h: 146, s: 50, l: 36], [name: "Sea Shell", rgb: "#FFF5EE", h: 25, s: 100, l: 97], - [name: "Sienna", rgb: "#A0522D", h: 19, s: 56, l: 40], [name: "Silver", rgb: "#C0C0C0", h: 0, s: 0, l: 75], [name: "Sky Blue", rgb: "#87CEEB", h: 197, s: 71, l: 73], - [name: "Slate Blue", rgb: "#6A5ACD", h: 248, s: 53, l: 58], [name: "Slate Gray", rgb: "#708090", h: 210, s: 13, l: 50], [name: "Snow", rgb: "#FFFAFA", h: 0, s: 100, l: 99], - [name: "Spring Green", rgb: "#00FF7F", h: 150, s: 100, l: 50], [name: "Steel Blue", rgb: "#4682B4", h: 207, s: 44, l: 49], [name: "Tan", rgb: "#D2B48C", h: 34, s: 44, l: 69], - [name: "Teal", rgb: "#008080", h: 180, s: 100, l: 25], [name: "Thistle", rgb: "#D8BFD8", h: 300, s: 24, l: 80], [name: "Tomato", rgb: "#FF6347", h: 9, s: 100, l: 64], - [name: "Turquoise", rgb: "#40E0D0", h: 174, s: 72, l: 56], [name: "Violet", rgb: "#EE82EE", h: 300, s: 76, l: 72], [name: "Wheat", rgb: "#F5DEB3", h: 39, s: 77, l: 83], - [name: "White Smoke", rgb: "#F5F5F5", h: 0, s: 0, l: 96], [name: "Yellow", rgb: "#FFFF00", h: 60, s: 100, l: 50], [name: "Yellow Green", rgb: "#9ACD32", h: 80, s: 61, l: 50] -] - -//******************************************************************* -// CLONE CHILD LOGIC -//******************************************************************* -public Map getSettingsAndStateMap() { - Map typeObj = parent?.getAppDuplTypes() - Map setObjs = [:] - if(typeObj) { - ((Map>)typeObj.stat).each { sk, sv-> - sv?.each { svi-> if(settings.containsKey(svi)) { setObjs[svi] = [type: sk, value: settings[svi] ] } } - } - ((Map>)typeObj.ends).each { ek, ev-> - ev?.each { evi-> - settings.findAll { it?.key?.endsWith(evi) }?.each { String fk, fv-> - def vv = settings[fk] - if(ek==sTIME) vv = dateTimeFmt((Date)toDateTime(vv), "HH:mm") - setObjs[fk] = [type: ek, value: vv] - } - } - } - ((Map)typeObj.caps).each { ck, cv-> - settings.findAll { it.key.endsWith(ck) }?.each { String fk, fv-> setObjs[fk] = [type: "capability", value: (fv instanceof List ? fv?.collect { it?.id?.toString() } : it?.id?.toString ) ] } - } - ((Map)typeObj.dev).each { dk, dv-> - settings.findAll { it.key.endsWith(dk) }?.each { String fk, fv-> setObjs[fk] = [type: "device", value: (fv instanceof List ? fv.collect { it?.id?.toString() } : it?.id?.toString() ) ] } - } - } - Map data = [:] - String newlbl = app?.getLabel()?.toString()?.replace(" (A ${sPAUSESymFLD})", sBLANK) - data.label = newlbl?.replace(" (A)", sBLANK) - data.settings = setObjs - - List stateSkip = [ - /* "isInstalled", "isParent", */ - "lastNotifMsgDt", "lastNotificationMsg", "setupComplete", "valEvtHistory", "warnHistory", "errorHistory", - "appData", "actionHistory", "authValidHistory", "deviceRefreshInProgress", "noticeData", "installData", "herokuName", "zoneHistory", - // actions - "tierSchedActive", - // zones - "zoneConditionsOk", "configStatusMap", "tsDtMap", "dateInstalled", "handleGuardEvents", "startTime", "stopTime", "alexaGuardState", "appFlagsMap", - "dupPendingSetup", "dupOpenedByUser" - - ] - data.state = state?.findAll { !((String)it?.key in stateSkip) } - return data -} +/** + * Echo Speaks Actions (Hubitat) + * + * Copyright 2018, 2019, 2020, 2021, 2022 Anthony Santilli + * Code Contributions by @nh.schottfam + * + * 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. + * + * --------------------------------------------- + TODO: Custom Reports for multiple builtin in routine items. Reports for home status like temp, contact, alarm status. + */ +//file:noinspection GroovyUnusedAssignment +//file:noinspection GroovySillyAssignment +//file:noinspection GroovyDoubleNegation +//file:noinspection GrMethodMayBeStatic +//file:noinspection unused + + +import groovy.json.JsonOutput +import groovy.transform.Field + +import java.text.SimpleDateFormat +import java.util.concurrent.Semaphore + +//************************************************ +//* STATIC VARIABLES * +//************************************************ +@Field static final String appVersionFLD = '4.2.0.2' +@Field static final String appModifiedFLD = '2022-04-18' +@Field static final Boolean devModeFLD = false +@Field static final String sNULL = (String)null +@Field static final String sBLANK = '' +@Field static final String sSPACE = ' ' +@Field static final String s2SPACE = ' ' +@Field static final String s3SPACE = ' ' +@Field static final String s4SPACE = ' ' +@Field static final String s5SPACE = ' ' +@Field static final String s6SPACE = ' ' +@Field static final String sBULLET = '\u2022' +@Field static final String sBULLETINV = '\u25E6' +//@Field static final String sSQUARE = '\u29C8' +@Field static final String sPLUS = '\u002B' +//@Field static final String sRIGHTARR = '\u02C3' +@Field static final String okSymFLD = "\u2713" +@Field static final String notOkSymFLD = "\u2715" +@Field static final String sPAUSESymFLD = "\u275A\u275A" +@Field static final String sLINEBR = '
    ' +@Field static final String sFALSE = 'false' +@Field static final String sTRUE = 'true' +@Field static final String sBOOL = 'bool' +@Field static final String sENUM = 'enum' +@Field static final String sNUMBER = 'number' +@Field static final String sRESET = 'reset' +@Field static final String sTEXT = 'text' +@Field static final String sTIME = 'time' +@Field static final String sMODE = 'mode' +@Field static final String sCOMPLT = 'complete' +@Field static final String sMEDIUM = 'medium' +@Field static final String sSMALL = 'small' +@Field static final String sCLR4D9 = '#2784D9' +@Field static final String sCLRRED = 'red' +@Field static final String sCLRRED2 = '#cc2d3b' +@Field static final String sCLRGRY = 'gray' +//@Field static final String sCLRGRN = 'green' +@Field static final String sCLRGRN2 = '#43d843' +@Field static final String sCLRORG = 'orange' +@Field static final String sTTM = 'Tap to modify...' +@Field static final String sTTC = 'Tap to configure...' +@Field static final String sTTCR = 'Tap to configure (Required)' +@Field static final String sTTP = 'Tap to proceed...' +@Field static final String sEXTNRL = 'external' +@Field static final String sDEBUG = 'debug' +@Field static final String sCHKBOX = 'checkbox' +@Field static final String sCOMMAND = 'command' +@Field static final String sANY = 'any' +@Field static final String sARE = 'are' +@Field static final String sBETWEEN = 'between' +@Field static final String sNBETWEEN = 'not_between' +@Field static final String sBELOW = 'below' +@Field static final String sABOVE = 'above' +@Field static final String sEQUALS = 'equals' +@Field static final String sANN = 'announcement' +@Field static final String sANNI = 'announcement_ignDND' +@Field static final String sANNIT = 'announcement_ignDND_tiered' +@Field static final String sANNT = 'announcement_tiered' +@Field static final String sSPEAK = 'speak' +@Field static final String sSPEAKP = 'speak_parallel' +@Field static final String sSPEAKI = 'speak_ignDND' +@Field static final String sSPEAKT = 'speak_tiered' +@Field static final String sSPEAKPT = 'speak_parallel_tiered' +@Field static final String sSPEAKIP = 'speak_ignDND_parallel' +@Field static final String sSPEAKIT = 'speak_ignDND_tiered' +@Field static final String sSPEAKIPT = 'speak_ignDND_parallel_tiered' +@Field static final String sWEATH = 'weather' +@Field static final String sCSUNRISE = 'Sunrise' +@Field static final String sCSUNSET = 'Sunset' +@Field static final String sSTTIME = 'start_time' +@Field static final String sSPDKNB = 'speed_knob' +@Field static final String sSWITCH = 'switch' +@Field static final String sTEMP = 'temperature' +@Field static final String sTHERMTEMP = 'thermostatTemperature' +@Field static final String sCOOLSP = 'coolingSetpoint' +@Field static final String sHEATSP = 'heatingSetpoint' +@Field static final String sMOTION = 'motion' +@Field static final String sLEVEL = 'level' +@Field static final String sBATT = 'battery' +@Field static final String sTHERM = 'thermostat' +@Field static final String sTHERMMODE = 'thermostatMode' +@Field static final String sTHERMOS = 'thermostatOperatingState' +@Field static final String sTHERMFM = 'thermostatFanMode' +@Field static final String sSCHED = 'scheduled' +@Field static final String sHUMID = 'humidity' +@Field static final String sKEYPAD = 'keypad' +@Field static final String sLOCK = 'lock' +@Field static final String sCONTACT = 'contact' +@Field static final String sWATER = 'water' +@Field static final String sSMOKE = 'smoke' +@Field static final String sPOWER = 'power' +@Field static final String sVALVE = 'valve' +@Field static final String sCHGTO = 'changes to' +@Field static final String sTTS = 'TTS' +@Field static final String sQUES = 'question' +@Field static final String sDELAYT = 'delay_time' +@Field static final String sEQ = 'equal' +@Field static final String sPUSHED = 'pushed' +@Field static final String sRELEASED = 'released' +@Field static final String sHELD = 'held' +@Field static final String sDBLTAP = 'doubleTapped' +@Field static final String sALRMSYSST = 'alarmSystemStatus' +@Field static final String sPISTNEXEC = 'pistonExecuted' +@Field static final String sLRM = "light_restore_map" +@Field static final String sTRIG = "trig_" +@Field static final String sACT = 'act_' +@Field static final String sACT_START = 'act_tier_start_' +@Field static final String sACT_STOP = 'act_tier_stop_' +@Field static final String sACT_ITEM = 'act_tier_item_' +@Field static final String sRECURRING = 'Recurring' +@Field static final String sONETIME = 'One-Time' +@Field static final List lONOFF = ['on', 'off'] +@Field static final List lANY = ['any'] +@Field static final List lOPNCLS = ['open', 'closed'] +@Field static final List lACTINACT = ['active', 'inactive'] +@Field static final List lSUNRISESET = ['sunrise', 'sunset'] +@Field static final List lWETDRY = ['wet', 'dry'] +@Field static final List lLOCKUNL = ['locked', 'unlocked'] +@Field static final List lDETECTCLR = ['detected', 'clear', 'tested'] +@Field static final List lPRES = ['present', 'not present'] +@Field static final List lSEC = ['disarmed', 'armed home', 'armed away', 'unknown'] + +//************************************************ +//* IN-MEMORY ONLY VARIABLES * +//* (Cleared only on HUB REBOOT or CODE UPDATES) * +//************************************************ +@Field volatile static Map historyMapFLD = [:] +// @Field volatile static String gitBranchFLD = null + +static String appVersion() { return appVersionFLD } +static String appVersionDt() { return appModifiedFLD } + +definition( + name : 'Echo Speaks - Actions', + namespace : 'tonesto7', + author : 'Anthony Santilli', + description : 'DO NOT INSTALL FROM MARKETPLACE\n\nAllows you to create echo device actions based on device/location events in your home.', + category : 'My Apps', + parent : 'tonesto7:Echo Speaks', + iconUrl : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', + iconX2Url : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', + iconX3Url : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_actions.png', + importUrl : 'https://raw.githubusercontent.com/tonesto7/echo-speaks/master/apps/echo-speaks-actions.groovy', + documentationLink : documentationUrl(), + videoLink : videoUrl() +) + +preferences { + page(name: 'startPage') + page(name: 'uhOhPage') + page(name: 'codeUpdatePage') + page(name: 'mainPage') + page(name: 'prefsPage') + page(name: 'triggersPage') + page(name: 'conditionsPage') + page(name: 'condTimePage') + page(name: 'actionsPage') + page(name: 'actionTiersPage') + page(name: 'actionTiersConfigPage') + page(name: 'actTrigTasksPage') + page(name: 'actTierStartTasksPage') + page(name: 'actTierStopTasksPage') + page(name: 'actNotifPage') + page(name: 'actNotifTimePage') + page(name: 'actionHistoryPage') + page(name: 'searchTuneInResultsPage') + page(name: 'uninstallPage') + page(name: 'namePage') +} + +def startPage() { + if(parent != null) { + if(!(Boolean)state.isInstalled && !(Boolean)parent?.childInstallOk()) { return uhOhPage() } + else { + updDeviceInputs() + return (minVersionFailed()) ? codeUpdatePage() : mainPage() } + } else { return uhOhPage() } +} + +def codeUpdatePage() { + return dynamicPage(name: "codeUpdatePage", title: "Update is Required", install: false, uninstall: false) { + section() { paragraph spanSmBld("Looks like your Action App needs an update

    Please make sure all app and device code is updated to the most current version

    Once updated your actions will resume normal operation.", sCLRRED) } + } +} + +def uhOhPage() { + return dynamicPage(name: "uhOhPage", title: "This install Method is Not Allowed", install: false, uninstall: true) { + section() { + paragraph spanBld("HOUSTON WE HAVE A PROBLEM!

    Echo Speaks - Actions can't be directly installed from the Marketplace.

    Please use the Echo Speaks SmartApp to configure them.", sCLRRED) + } + } +} + +def appInfoSect() { + String instDt = state.dateInstalled ? fmtTime((String)state.dateInstalled, "MMM dd '@' h:mm a", true) : sNULL + String str = spanBld((String)app.name, "black", "es_groups") + str += ( app.label && (String)app.label != (String)app.name) ? spanBldBr(" (${app.label.replace(" (A)", sBLANK)})") : sLINEBR + str += spanSmBld("Version: ") + spanSmBr(appVersionFLD) + str += instDt ? spanSmBld("Installed: ") + spanSmBr(instDt) : sBLANK + section() { paragraph divSm(str, sCLRGRY) } +} + +private Map buildTriggerEnum() { + Map> buildItems = [:] + buildItems["Date/Time"] = [(sSCHED):"Scheduled Time"].sort{ it?.key } + buildItems["Location"] = [(sMODE):"Modes", (sPISTNEXEC):"Pistons"].sort{ it?.key } + if(!settings.enableWebCoRE) { buildItems.Location.remove(sPISTNEXEC) } + + buildItems["Sensor Devices"] = [(sCONTACT):"Contacts | Doors | Windows", (sBATT):"Battery Level", (sMOTION):"Motion", "illuminance": "Illuminance/Lux", "presence":"Presence", (sTEMP):"Temperature", (sHUMID):"Humidity", (sWATER):"Water", (sPOWER):"Power", "acceleration":"Accelerometers"].sort{ it?.value } + buildItems["Actionable Devices"] = [(sLOCK):"Locks", "securityKeypad":"Keypads", (sSWITCH):"Switches/Outlets", (sLEVEL):"Dimmers/Level", "door":"Garage Door Openers", (sVALVE):"Valves", "windowShade":"Window Shades"].sort{ it?.value } + buildItems["Thermostat Devices"] = [(sCOOLSP):"Thermostat Cooling Setpoint", (sHEATSP):"Thermostat Heating Setpoint", (sTHERMTEMP):"Thermostat Ambient Temp", (sTHERMOS):"Thermostat Operating State", (sTHERMMODE):"Thermostat Mode", (sTHERMFM):"Thermostat Fan Mode"].sort{ it?.value } + buildItems["Button Devices"] = [(sPUSHED):"Button (Pushable)", (sRELEASED):"Button (Releasable)", (sHELD):"Button (Holdable)", (sDBLTAP):"Button (Double Tapable)"].sort{ it?.value } +// TODO siren (capability.alarm, attr alarm - ENUM ["strobe", "off", "both", "siren"] + buildItems["Safety & Security"] = [(sALRMSYSST): getAlarmSystemName(), (sSMOKE):"Fire/Smoke", "carbonMonoxide":"Carbon Monoxide", "guard":"Alexa Guard"].sort{ it?.value } + if(!parent?.guardAutoConfigured()) { buildItems["Safety & Security"].remove("guard") } + return buildItems.collectEntries { it?.value }?.sort { it?.value } +} + +private static Map buildActTypeEnum() { + Map> buildItems = [:] + buildItems["Speech"] = [(sSPEAK):"Speak", (sANN):"Announcement", (sSPEAKP):"Speak (Parallel)", (sSPEAKI): "Speak Ignore Dnd", (sSPEAKIP): "Speak Ignore Dnd (Parallel)", + (sSPEAKT):"Speak (Tiered)", (sSPEAKPT):"Speak Parallel (Tiered)", (sSPEAKIT): "Speak Ignore Dnd (Tiered)", (sSPEAKIPT): "Speak Parallel Ignore Dnd (Tiered)", + (sANNI): "Announcement Ignore DND", (sANNT):"Announcement (Tiered)", (sANNIT): "Announcement Ignore DND (Tiered)" ].sort{ it?.key } + buildItems["Built-in Sounds"] = ["sounds":"Play a Sound"].sort{ it?.key } + buildItems["Built-in Responses"] = [(sWEATH):"Weather Report", "builtin":"Birthday, Compliments, Facts, Jokes, News, Stories, Traffic, and more...", "calendar":"Read Calendar Events"].sort{ it?.key } + buildItems["Media/Playback"] = ["music":"Play Music/Playlists", "playback":"Playback/Volume Control"].sort{ it?.key } + buildItems["Alarms/Reminders"] = ["alarm":"Create Alarm", "reminder":"Create Reminder"].sort{ it?.key } + buildItems["Devices Settings"] = ["wakeword":"Change Wake Word", "dnd":"Set Do Not Disturb", "bluetooth":"Bluetooth Control"].sort{ it?.key } + buildItems["Custom"] = ["voicecmd":"Execute a voice command","sequence":"Execute Sequence", "alexaroutine": "Execute Alexa Routine(s)"].sort{ it?.key } + return buildItems.collectEntries { it?.value }?.sort { it?.value } +} + +def mainPage() { + Boolean newInstall = !(Boolean)state.isInstalled + return dynamicPage(name: "mainPage", nextPage: (!newInstall ? sBLANK : "namePage"), uninstall: newInstall, install: !newInstall) { + Boolean dup = ((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup) + if(dup) { + state.dupOpenedByUser = true + section() { paragraph spanBld("This Action was created from an existing action.

    Please review the settings and save to activate...
    ${state.badMode ?: sBLANK}", sCLRORG, "pause_orange") } + } + + if(settings.enableWebCoRE) { + if(!webCoREFLD) webCoRE_init() + webCoRE_poll() + } + appInfoSect() + Boolean paused = isPaused() + Boolean trigConf + //Boolean condConf + Boolean actConf + Boolean allOk + + if(paused) { + section() { + paragraph spanSmBld("This Action is currently in a paused state...
    To edit the please un-pause", sCLRRED, "pause_orange") + } + } else { + section() { paragraph divSm(getOverallDesc(), sCLRGRY) } + + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } + trigConf = triggersConfigured() + //condConf = conditionsConfigured() + actConf = executionConfigured() + allOk = (Boolean) ((String)settings.actionType && trigConf && actConf) + section(sectHead("Configuration: Part 1")) { + input "actionType", sENUM, title: inTS1("Action Type", "list"), description: sBLANK, options: buildActTypeEnum(), multiple: false, required: true, submitOnChange: true + } + if (newInstall) { + section(sectHead("Configuration: Part 2")) { + paragraph spanBld("Further Options will be configured once you save this automation.
    Please save and return to complete", sNULL, getAppImg("info")) + } + } else { + section (sectHead("Configuration: Part 2")) { + if((String)settings.actionType) { + href "triggersPage", title: inTS1("Action Triggers", "trigger"), description: spanSm(getTriggersDesc(), sCLR4D9) + } else { paragraph spanBld("These options will be shown once the action type is configured.", sNULL, getAppImg("info")) } + } + section(sectHead("Configuration: Part 3")) { + if((String)settings.actionType && trigConf) { + href "conditionsPage", title: inTS1("Conditions/Restrictions", "conditions") + optPrefix(), description: divSm(getConditionsDesc(true), sCLR4D9) + } else { paragraph spanBld("These options will be shown once the triggers are configured.", sNULL, getAppImg("info")) } + } + section(sectHead("Configuration: Part 4")) { + if((String)settings.actionType && trigConf) { + href "actionsPage", title: inTS1("Execution Config", "es_actions"), description: spanSm(getActionDesc(true), sCLR4D9) + } else { paragraph spanBld("These options will be shown once the triggers are configured.", sNULL, getAppImg("info")) } + } + if(allOk) { + section(sectHead("Notifications:")) { + String t0 = getAppNotifDesc() + href "actNotifPage", title: inTS1("Send Notifications", "notification2"), description: t0 ? divSm(t0 + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC) + } + + getTierStatusSection() + + section(sectHead("Action History")) { + href "actionHistoryPage", title: inTS1("View Action History", "tasks"), description: sBLANK + } + } + } + } + + section(sectHead("Preferences")) { + href "prefsPage", title: inTS1("Debug/Preferences", "settings"), description: sBLANK + if(!newInstall) { + input "actionPause", sBOOL, title: inTS1("Pause Action?", "pause_orange"), defaultValue: false, submitOnChange: true + if((Boolean)settings.actionPause) { unsubscribe() } + else { + input "actTestRun", sBOOL, title: inTS1("Test this action?", "testing"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true + if((Boolean)settings.actTestRun) { executeActTest() } + } + } + } + + if(!newInstall) { + section(sectHead("Name this Action:")) { + input "appLbl", sTEXT, title: inTS1("Action Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true + } + section(sectHead("Remove Action:")) { + href "uninstallPage", title: inTS1("Remove this Action", "uninstall"), description: inputFooter("Tap to Remove...", sCLRGRY, true) + } + if(allOk) { + section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { + String issueUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28ACTIONS+BUG%29+&projects=echo-speaks%2F6" + String featUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BActions+Feature+Request%5D&projects=echo-speaks%2F6" + String ttob = inactFoot("Tap to open browser") + href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "www"), description: ttob + href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "www"), description: ttob + } + appFooter() + } + } + } +} + +def prefsPage() { + return dynamicPage(name: "prefsPage", install: false, uninstall: false) { + section(sectHead("Logging:")) { + input "logInfo", sBOOL, title: inTS1("Show Info Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logWarn", sBOOL, title: inTS1("Show Warning Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logError", sBOOL, title: inTS1("Show Error Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logDebug", sBOOL, title: inTS1("Show Debug Logs?", sDEBUG), required: false, defaultValue: false, submitOnChange: true + input "logTrace", sBOOL, title: inTS1("Show Detailed Logs?", sDEBUG), required: false, defaultValue: false, submitOnChange: true + } + if((Boolean)state.isInstalled) { + if(advLogsActive()) { logsEnabled() } + section(sectHead("Other:")) { + input "clrEvtHistory", sBOOL, title: inTS1("Clear Device Event History?", "reset"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true + if((Boolean)settings.clrEvtHistory) { clearEvtHistory() } + } + } + } +} + +def namePage() { + return dynamicPage(name: "namePage", install: true, uninstall: false) { + section(sectHead("Name this Automation:")) { + input "appLbl", sTEXT, title: inTS1("Label this Action", "name_tag"), description: sBLANK, required:true, submitOnChange: true + } + } +} + +def actionHistoryPage() { + return dynamicPage(name: "actionHistoryPage", title: "Action History", install: false, uninstall: false) { + section() { + getActionHistory() + } + if( ((List)getMemStoreItem("actionHistory")).size() ) { + section(sBLANK) { + input "clearActionHistory", sBOOL, title: inTS1("Clear Action History?", sRESET), description: spanSm("Clears Stored Action History.", sCLRGRY), defaultValue: false, submitOnChange: true + if((Boolean)settings.clearActionHistory) { + settingUpdate("clearActionHistory", sFALSE, sBOOL) + clearActHistory() + } + } + } + } +} +/* +// TODO: Add flag to check for the old schedule settings and pause the action, and notify the user. +private scheduleConvert() { + if(settings.trig_scheduled_time || settings.trig_scheduled_sunState && !settings.trig_scheduled_type) { + if(settings.trig_scheduled_sunState) { settingUpdate("trig_scheduled_type", "${settings.trig_scheduled_sunState}", sENUM); settingRemove("trig_scheduled_sunState") } + else if(settings.trig_scheduled_time && settings.trig_scheduled_recurrence) { + if(settings.trig_scheduled_recurrence == "Once") { settingUpdate("trig_scheduled_type", sONETIME, sENUM) } + if(settings.trig_scheduled_recurrence in ["Daily", "Weekly", "Monthly"]) { settingUpdate("trig_scheduled_type", sRECURRING, sENUM) } + } + } +} */ + +@SuppressWarnings('GroovyFallthrough') +def triggersPage() { + return dynamicPage(name: "triggersPage", nextPage: "mainPage", title: "Action Triggers", uninstall: false, install: false) { +// Boolean isTierAct = isTierAction() + String a = getTriggersDesc(false, false) + if(a) { + section() { + paragraph spanSm(a, sCLR4D9) + } + } + section (sectHead("Enable webCoRE Integration:")) { + input "enableWebCoRE", sBOOL, title: inTS1("Enable webCoRE Integration", webCore_icon()), required: false, defaultValue: false, submitOnChange: true + } + if(settings.enableWebCoRE) { + if(!webCoREFLD) webCoRE_init() + webCoRE_poll() + } + Boolean showSpeakEvtVars = false + section (sectHead("Select Capabilities")) { + input "triggerEvents", sENUM, title: inTS1("Select Trigger Event(s)", "trigger"), options: buildTriggerEnum(), multiple: true, required: true, submitOnChange: true + } + Integer trigEvtCnt = ((List)settings.triggerEvents)?.size() + if (trigEvtCnt) { + Boolean fnd = false + //if( sSCHED in (List)settings.triggerEvents || + // sWEATH in (List)settings.triggerEvents) { fnd = true } + /*((List)settings.triggerEvents)?.each { String tr -> + if(tr in [sSCHED, sWEATH]) { fnd = true } + } */ + //if(!((List)settings.triggerEvents in [sSCHED, sWEATH])) { showSpeakEvtVars = true } + if(!fnd) { showSpeakEvtVars = true } + + if (valTrigEvt(sSCHED)) { + section(sectHead("Time Based Events"), hideable: true) { + List schedTypes = [sONETIME, sRECURRING, sCSUNRISE, sCSUNSET] + input "trig_scheduled_type", sENUM, title: inTS1("Schedule Type?", sCHKBOX), options: schedTypes, multiple: false, required: true, submitOnChange: true + String schedType = (String)settings.trig_scheduled_type + if(schedType) { + switch(schedType) { + case sONETIME: + case sRECURRING: + input "trig_scheduled_time", sTIME, title: inTS1("Trigger Time?", "clock"), required: false, submitOnChange: true + if(settings.trig_scheduled_time && schedType == sRECURRING) { + List recurOpts = ["Daily", "Weekly", "Monthly"] // "Yearly" + input "trig_scheduled_recurrence", sENUM, title: inTS1("Recurrence?", "day_calendar"), description: sBLANK, multiple: false, required: true, submitOnChange: true, options: recurOpts, defaultValue: "Once" + // TODO: Build out the scheduling some more with quick items like below + /* + At 6 pm on the last day of every month: (0 0 18 L * ?) + At 6 pm on the 3rd to last day of every month: (0 0 18 L-3 * ?) + At 10:30 am on the last Thursday of every month: (0 30 10 ? * 5L) + At 6 pm on the last Friday of every month during the years 2015, 2016 and 2017: (0 0 18 ? * 6L 2015-2017) + At 10 am on the third Monday of every month: (0 0 10 ? * 2#3) + At 12 am midnight on every day for five days starting on the 10th day of the month: (0 0 0 10/5 * ?) + */ + String schedRecur = (String)settings.trig_scheduled_recurrence + if(schedRecur) { + switch(schedRecur) { + case "Daily": + input "trig_scheduled_weekdays", sENUM, title: inTS1("Only of these Days of the Week?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: daysOfWeekMap() + break + + case "Weekly": + input "trig_scheduled_weekdays", sENUM, title: inTS1("Days of the Week?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: true, submitOnChange: true, options: daysOfWeekMap() + input "trig_scheduled_weeks", sENUM, title: inTS1("Only these Weeks on the Month?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: weeksOfMonthMap() + input "trig_scheduled_months", sENUM, title: inTS1("Only on these Months?", "day_calendar") + optPrefix(), description: sBLANK, multiple: true, required: false, submitOnChange: true, options: monthMap() + break + + case "Monthly": + input "trig_scheduled_daynums", sENUM, title: inTS1("Days of the Month?", "day_calendar"), description: (!settings.trig_scheduled_weeks ? optPrefix(): sBLANK), multiple: true, required: (!settings.trig_scheduled_weeks), submitOnChange: true, options: (1..31)?.collect { it as String } + if(!settings.trig_scheduled_daynums) { + input "trig_scheduled_weeks", sENUM, title: inTS1("Weeks of the Month?", "day_calendar"), description: (!settings.trig_scheduled_daynums ? optPrefix(): sBLANK), multiple: true, required: (!settings.trig_scheduled_daynums), submitOnChange: true, options: weeksOfMonthMap() + } + input "trig_scheduled_months", sENUM, title: inTS1("Only on these Months?", "day_calendar"), description: optPrefix(), multiple: true, required: false, submitOnChange: true, options: monthMap() + break + } + } + } + break + case sCSUNRISE: + case sCSUNSET: + input "trig_scheduled_sunState_offset", sNUMBER, range: "*..*", title: inTS1("Offset ${schedType} this number of minutes (+/-)", schedType?.toLowerCase()), required: false + break + } + triggerMsgInput(sSCHED) + } + } + } + + if (valTrigEvt(sALRMSYSST)) { + section (sectHead("${getAlarmSystemName()} (${getAlarmSystemName(true)}) Events"), hideable: true) { + String inT = sALRMSYSST + input "trig_${inT}", sENUM, title: inTS1("${getAlarmSystemName()} Modes", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: true, submitOnChange: true + if("alerts" in (List)settings."trig_${inT}") { + input "trig_${inT}_events", sENUM, title: inTS1("${getAlarmSystemName()} Alert Events", "alarm_home"), options: getAlarmSystemAlertOptions(), multiple: true, required: true, submitOnChange: true + } + if((List)settings."trig_${inT}") { + input "trig_${inT}_once", sBOOL, title: inTS1("Only alert once a day? (per type: mode)", sQUES), required: false, defaultValue: false, submitOnChange: true + input "trig_${inT}_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(sALRMSYSST) + } + } + } + + if (valTrigEvt("guard")) { + section (sectHead("Alexa Guard Events"), hideable: true) { + input "trig_guard", sENUM, title: inTS1("Alexa Guard Modes", "alarm_home"), options: ["ARMED_STAY", "ARMED_AWAY", sANY], multiple: true, required: true, submitOnChange: true + if(settings.trig_guard) { + // input "trig_guard_once", sBOOL, title: inTS1("Only alert once a day?\n(per mode)", sQUES), required: false, defaultValue: false, submitOnChange: true + // input "trig_guard_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)\n(Optional)", sDELAYT), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput("guard") + } + } + } + + if (valTrigEvt(sMODE)) { + section (sectHead("Mode Events"), hideable: true) { + input "trig_mode", sMODE, title: inTS1("Location Modes", sMODE), multiple: true, required: true, submitOnChange: true + if((List)settings.trig_mode) { + input "trig_mode_once", sBOOL, title: inTS1("Only alert once a day? (per type: mode)", sQUES), required: false, defaultValue: false, submitOnChange: true + input "trig_mode_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(sMODE) + } + } + } + + if(valTrigEvt(sPISTNEXEC)) { + section(sectHead("webCoRE Piston Executed Events"), hideable: true) { + String inT = sPISTNEXEC + input "trig_${inT}", sENUM, title: inTS1("Pistons", webCore_icon()), options: webCoRE_list(), multiple: true, required: true, submitOnChange: true + if(settings."trig_${inT}") { + paragraph pTS("webCoRE settings must be enabled to send events for Piston Execution (not enabled by default in webCoRE)", sNULL, false, sCLRGRY) + input "trig_${inT}_once", sBOOL, title: inTS1("Only alert once a day?\n(per type: piston)", sQUES), required: false, defaultValue: false, submitOnChange: true + input "trig_${inT}_wait", sNUMBER, title: inTS1("Wait between each report (in seconds)", sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(sPISTNEXEC) + } + } + } + + if (valTrigEvt(sSWITCH)) { + trigNonNumSect(sSWITCH, sSWITCH, "Switches", "Switches", lONOFF+lANY, "are turned", lONOFF, sSWITCH) + } + + if (valTrigEvt(sLEVEL)) { + trigNumValSect(sLEVEL, "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level is", sSPDKNB) + } + + if (valTrigEvt(sBATT)) { + trigNumValSect(sBATT, sBATT, "Battery Level", "Batteries", "Level is", sSPDKNB) + } + + if (valTrigEvt(sMOTION)) { + trigNonNumSect(sMOTION, "motionSensor", "Motion Sensors", "Motion Sensors", lACTINACT+lANY, "become", lACTINACT, sMOTION) + } + + if (valTrigEvt("presence")) { + trigNonNumSect("presence", "presenceSensor", "Presence Sensors", "Presence Sensors", lPRES +lANY, sCHGTO, lPRES, "presence") + } + + if (valTrigEvt(sCONTACT)) { + trigNonNumSect(sCONTACT, "contactSensor", "Contacts, Doors, Windows", "Contacts, Doors, Windows", lOPNCLS+lANY, sCHGTO, lOPNCLS, sCONTACT) + } + + if (valTrigEvt("acceleration")) { + trigNonNumSect("acceleration", "accelerationSensor", "Accelerometers", "Accelerometers", lACTINACT+lANY, sCHGTO, lACTINACT, "acceleration") + } + + if (valTrigEvt("door")) { + trigNonNumSect("door", "garageDoorControl", "Garage Door Openers", "Garage Doors", lOPNCLS+["opening", "closing", sANY], sCHGTO, lOPNCLS, "garage_door") + } + + if (valTrigEvt(sLOCK)) { + trigNonNumSect(sLOCK, sLOCK, "Locks", "Smart Locks", lLOCKUNL + lANY, sCHGTO, lLOCKUNL, sLOCK, (!!(List)settings.trig_lock_Codes), (((String)settings.trig_lock && (String)settings.trig_lock_cmd in ["unlocked", sANY]) ? this.&handleCodeSect : this.&dummy), "Unlocked" ) + } + + if (valTrigEvt("securityKeypad")) { + trigNonNumSect("securityKeypad", "securityKeypad", "Security Keypad", "Security Keypad", lSEC + lANY, sCHGTO, lSEC, sKEYPAD, (!!(List)settings.trig_securityKeypad_Codes), (((String)settings.trig_securityKeypad && (String)settings.trig_securityKeypad_cmd in ["disarmed", sANY]) ? this.&handleCodeSect : this.&dummy), "Keypad Disarmed" ) + } + + if (valTrigEvt(sPUSHED)) { + trigButtonSect(sPUSHED, "pushableButton", "Button Pushed Events", "Pushable Buttons", "button_push", sPUSHED) + } + + if (valTrigEvt(sRELEASED)) { + trigButtonSect(sRELEASED, "releasableButton", "Button Released Events", "Releasable Buttons", "button_released", sRELEASED) + } + + if (valTrigEvt(sHELD)) { + trigButtonSect(sHELD, "holdableButton", "Button Held Events", "Holdable Buttons", "button_held", sHELD) + } + + if (valTrigEvt(sDBLTAP)) { + trigButtonSect(sDBLTAP, "doubleTapableButton", "Button Double Tap Events", "Double Tap Buttons", "button_double", sDBLTAP) + } + + if (valTrigEvt(sTEMP)) { + trigNumValSect(sTEMP, "temperatureMeasurement", "Temperature Sensor", "Temperature Sensors", "Temperature", sTEMP) + } + + if (valTrigEvt(sHUMID)) { + trigNumValSect(sHUMID, "relativeHumidityMeasurement", "Humidity Sensors", "Relative Humidity Sensors", "Relative Humidity (%)", sHUMID) + } + + if (valTrigEvt(sWATER)) { + trigNonNumSect(sWATER, "waterSensor", "Water Sensors", "Water/Moisture Sensors", lWETDRY + lANY, sCHGTO, lWETDRY, sWATER) + } + + if (valTrigEvt(sPOWER)) { + trigNumValSect(sPOWER, "powerMeter", "Power Events", "Power Meters", "Power Level (W)", sPOWER) + } + + if (valTrigEvt("carbonMonoxide")) { + trigNonNumSect("carbonMonoxide", "carbonMonoxideDetector", "Carbon Monoxide Events", "Carbon Monoxide Detectors", lDETECTCLR+lANY, sCHGTO, lDETECTCLR, "co") + } + + if (valTrigEvt(sSMOKE)) { + trigNonNumSect(sSMOKE, "smokeDetector", "Smoke Detector Events", "Smoke Detectors", lDETECTCLR+lANY, sCHGTO, lDETECTCLR, sSMOKE) + } + + if (valTrigEvt("illuminance")) { + trigNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Events", "Illuminance Sensors", "Lux Level (%)", "illuminance") + } + + if (valTrigEvt("windowShade")) { + trigNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", lOPNCLS+["opening", "closing", sANY], sCHGTO, lOPNCLS, "shade") + } + + if (valTrigEvt(sVALVE)) { + trigNonNumSect(sVALVE, sVALVE, "Valves", "Valves", lOPNCLS+lANY, sCHGTO, lOPNCLS, sVALVE) + } + + if (valTrigEvt(sCOOLSP)) { + trigNumValSect(sCOOLSP, sTHERM, "Thermostat Cooling Setpoint Events", "Thermostat (Cooling Setpoint)", "Setpoint temp", sTHERM) + } + + if (valTrigEvt(sHEATSP)) { + trigNumValSect(sHEATSP, sTHERM, "Thermostat Heating Setpoint Events", "Thermostat (Heating Setpoint)", "Setpoint temp", sTHERM) + } + + if (valTrigEvt(sTHERMTEMP)) { + trigNumValSect(sTHERMTEMP, sTHERM, "Thermostat Ambient Temperature Events", "Thermostat (Ambient Temperature)", "Ambient Temp", sTHERM) + } + + if (valTrigEvt(sTHERMOS)) { + trigNonNumSect(sTHERMOS, sTHERM, "Thermostat Operating State Events", "Thermostat (Operating State)", getThermOperStOpts()+lANY, sCHGTO, getThermOperStOpts(), sTHERM) + } + + if (valTrigEvt(sTHERMMODE)) { + trigNonNumSect(sTHERMMODE, sTHERM, "Thermostat Mode Events", "Thermostat (Mode)", getThermModeOpts()+lANY, sCHGTO, getThermModeOpts(), sTHERM) + } + + if (valTrigEvt(sTHERMFM)) { + trigNonNumSect(sTHERMFM, sTHERM, "Thermostat Fan Mode Events", "Thermostat (Fan Mode)", getThermFanOpts()+lANY, sCHGTO, getThermFanOpts(), sTHERM) + } + + if(triggersConfigured()) { + section(sBLANK) { + paragraph spanMdBldBr("Trigger Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") + } + } + } + state.showSpeakEvtVars = showSpeakEvtVars + } +} + +def handleCodeSect(String typ, String lbl) { + Map lockCodes = getCodes((List)settings."trig_${typ}") + //log.debug "lockCodes: ${lockCodes}" + if(lockCodes) { + Map codeOpts = lockCodes.collectEntries { [((String)it.key): it.value?.name ? "Name: "+ (String)it.value.name : "Code Number ${(String)it.key}: (${(String)it.value?.code})"] } + input "trig_${typ}_Codes", sENUM, title: inTS1("Filter ${lbl} codes...", sCOMMAND), options: codeOpts, multiple: true, required: false, submitOnChange: true + } +} + +Map getCodes(List devs, String code=sNULL) { + // lockCodes are: + // ["":["code":"", "name":""],"":["code":"", "name":""]] + Map result = [:] + try { + String lockCodes = devs && devs.size() == 1 ? devs[0]?.currentValue("lockCodes") : (code ?: sNULL) + if (lockCodes) { + //decrypt codes if they're encrypted + if (lockCodes[0] == "{") result = parseJson(lockCodes) + else result = parseJson(decrypt(lockCodes)) + if(devModeFLD) log.debug "lockCodes: ${result}" + } + } catch(ex) { logError("getCodes error", true, ex) } + return result +} + +def dummy(a,b) {} + +def trigNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, cmdAfterOpts, String image, Boolean devReq=true, Closure extraMeth=this.&dummy, String extraStr=sNULL) { + section (sectHead(sectStr), hideable: true) { + input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), multiple: true, required: devReq, submitOnChange: true + if (settings."trig_${inType}") { + input "trig_${inType}_cmd", sENUM, title: spanSmBld("${cmdTitle}...", sNULL, sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true + if((String)settings."trig_${inType}_cmd") { + if(inType == sLOCK) { + String lMsg = "This will prevent duplicate speech events on Zigbee Locks that send two events per lock and unlock." + input "trig_${inType}_ign_empty_type", sBOOL, title: spanSmBld("Ignore ${devTitle} Events with No Type?", sNULL, sQUES) + optPrefix() + lineBr() + spanSm(lMsg, sCLRGRY), required: false, defaultValue: false, submitOnChange: true + } + if (settings."trig_${inType}"?.size() > 1 && (String)settings."trig_${inType}_cmd" != sANY) { + input "trig_${inType}_all", sBOOL, title: spanSmBld("Require ALL ${devTitle} to be (${settings."trig_${inType}_cmd"})?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + extraMeth(inType, extraStr) + + if(!isTierAction() && (String)settings."trig_${inType}_cmd" in cmdAfterOpts) { + doAftInput(inType) + } + input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true + input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)", sNULL, sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(inType) + } + } + } +} + +def trigButtonSect(String inType, String capType, String sectStr, String devTitle, String image, String cmd){ + section (sectHead(sectStr), hideable: true) { + input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), required: true, multiple: true, submitOnChange: true + if (settings."trig_${inType}") { + settingUpdate("trig_${inType}_cmd", cmd, sENUM) + //input "trig_${inType}_cmd", sENUM, title: inTS1("Pushed changes", sCOMMAND), options: [sPUSHED], required: true, multiple: false, defaultValue: sPUSHED, submitOnChange: true + input "trig_${inType}_nums", sENUM, title: inTS1("Button numbers?", sCOMMAND), options: 1..8, required: true, multiple: true, submitOnChange: true + if(settings."trig_${inType}_nums") { + input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true + input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)", sNULL, sDELAYT) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(cmd) + } + } + } +} + +def trigNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image, Boolean devReq=true) { + Boolean done = false + section (sectHead(sectStr), hideable: true) { + input "trig_${inType}", "capability.${capType}", title: spanSmBld(devTitle, sNULL, image), multiple: true, submitOnChange: true, required: devReq + if(settings."trig_${inType}") { + input "trig_${inType}_cmd", sENUM, title: spanSmBld("${cmdTitle} is...", sNULL, sCOMMAND), options: numOpts(), required: true, multiple: false, submitOnChange: true + if (settings."trig_${inType}_cmd") { + if ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN, sBELOW]) { + String snot = (String)settings."trig_${inType}_cmd" in [sNBETWEEN] ? "Not " : sBLANK + input "trig_${inType}_low", sNUMBER, title: spanSmBld(((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN] ? "${snot}Between a Low" : "a low") + " ${cmdTitle} of...", sNULL, "low"), required: true, submitOnChange: true + } + if(settings."trig_${inType}_low"!=null && ((String)settings."trig_${inType}_cmd" in [sBELOW]) ) done=true + + if ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN, sABOVE]) { + input "trig_${inType}_high", sNUMBER, title: spanSmBld(((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN] ? "and a High of..." : "a High") + " ${cmdTitle} of...", sNULL, "high"), required: true, submitOnChange: true + } + if(settings."trig_${inType}_high"!=null && ((String)settings."trig_${inType}_cmd" in [sABOVE])) done=true + + if(settings."trig_${inType}_low"!=null && settings."trig_${inType}_high"!=null && ((String)settings."trig_${inType}_cmd" in [sNBETWEEN, sBETWEEN])) done=true + + if ((String)settings."trig_${inType}_cmd" == sEQUALS) { + input "trig_${inType}_equal", sNUMBER, title: spanSmBld("a ${cmdTitle} of...", sNULL, sEQ), required: true, submitOnChange: true + if(settings."trig_${inType}_equal"!=null) done=true + } + if(done) { + if (settings."trig_${inType}"?.size() > 1) { + input "trig_${inType}_all", sBOOL, title: spanSmBld("Require ALL devices to be (${settings."trig_${inType}_cmd"}) values?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true + if(!(Boolean)settings."trig_${inType}_all") { + input "trig_${inType}_avg", sBOOL, title: spanSmBld("Use the average of all selected device values?", sNULL, sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + doAftInput(inType) + input "trig_${inType}_once", sBOOL, title: spanSmBld("Only alert once a day? (per type: ${inType})", sNULL, sQUES) + optPrefix(), required: false, defaultValue: false, submitOnChange: true + input "trig_${inType}_wait", sNUMBER, title: spanSmBld("Wait between each report (in seconds)?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + triggerMsgInput(inType) + } + } + } + } +} + +def doAftInput(String inType) { + input "trig_${inType}_after", sNUMBER, title: spanSmBld("Only after (${settings."trig_${inType}_cmd"}) for (xx) (0..7200) seconds?", sNULL, sDELAYT), required: false, defaultValue: null, submitOnChange: true + Integer aft = (Integer)settings."trig_${inType}_after" + if(aft != null) { + if(aft < 0 || aft > 7200) { settingUpdate("trig_${inType}_after", 10) } + input "trig_${inType}_after_repeat", sNUMBER, title: spanSmBld("Repeat every (xx) (10..7200) seconds until it's not ${settings."trig_${inType}_cmd"}?", sNULL, sDELAYT), required: false, defaultValue: null, submitOnChange: true + Integer aftR = (Integer)settings."trig_${inType}_after_repeat" + if(aftR != null) { + input "trig_${inType}_after_repeat_cnt", sNUMBER, title: spanSmBld("Only repeat this many times (2..20000)?", sNULL, sQUES) + optPrefix(), required: false, defaultValue: null, submitOnChange: true + if(aftR < 10 || aftR > 7200) { settingUpdate("trig_${inType}_after_repeat", 10) } + // triggerMsgInput(inType) + } + Integer aftRC = (Integer)settings."trig_${inType}_after_repeat_cnt" + if(aftRC != null) { + if(aftRC < 2 || aftRC > 20000) { settingUpdate("trig_${inType}_after_repeat_cnt", 120) } + // settingRemove("trig_${inType}_once") + // settingRemove("trig_${inType}_wait") + } + } else { + settingRemove("trig_${inType}_after_repeat") + settingRemove("trig_${inType}_after_repeat_cnt") + settingRemove("trig_${inType}_after_repeat_txt") + } +} + +def triggerMsgInput(String inType /*, Boolean showRepInputs=false, Integer itemCnt=0 */) { + if((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI]) { + String str = spanSmBldBr("Response Options", sCLR4D9) + String a=s3SPACE+sBULLET+sSPACE + str += spanSmBr("Available Options:") + str += spanSmBr("${a}${strUnder("1")}: Leave the text empty below and text will be generated for each ${inType} trigger event.") + str += spanSmBr("${a}${strUnder("2")}: Wait till the Execution config step and define a single global response for all triggers selected.") + str += spanSmBr("${a}${strUnder("3")}: Use the response builder below and create custom responses for each individual trigger type. (Supports randomization when multiple responses are configured)") + paragraph divSm(str, sCLRGRY, "info") + //Custom Text Options + Integer resp1cnt = 1 + String desc1 = sBLANK + String[] resps1 = ((String)settings."trig_${inType}_txt")?.split(";") ?: [] + if(resps1.size() > 1) { + resps1.each { String i-> desc1 += spanSmBld("(${resp1cnt})") + sSPACE + spanSmBr(i, sCLR4D9); resp1cnt++ } + } else { + desc1 += spanSmBr((String)settings."trig_${inType}_txt" ?: sBLANK, sCLR4D9) + } + desc1 += (String)settings."trig_${inType}_txt" ? lineBr() + spanSmBld("Edit in Response Designer...", sCLRGRY) : spanSm("Open Response Designer...", sCLRGRY) + href url: parent?.getTextEditorPath(app?.id as String, "trig_${inType}_txt"), style: sEXTNRL, required: false, title: inTS1("Custom ${inType?.capitalize()} Responses", sTEXT) + optPrefix(), description: desc1//, submitOnChange: true + + if((Integer)settings."trig_${inType}_after_repeat") { + //Custom Repeat Text Options + paragraph divSm(spanSmBldBr("Repeat Response Options", sCLR4D9) + spanSm("Add custom responses for the ${inType} events that are repeated.", sCLRGRY), sNULL, getAppImg("info")) + Integer resp2cnt = 1 + String desc2 = sBLANK + String[] resps2 = ((String)settings."trig_${inType}_after_repeat_txt")?.split(";") ?: [] + if(resps2.size() > 1) { + resps2.each { String i-> desc2 += spanSmBld("(${resp2cnt})") + sSPACE + spanSmBr(i, sCLR4D9); resp2cnt++ } + } else { + desc2 += spanSmBr((String)settings."trig_${inType}_after_repeat_txt" ?: sBLANK, sCLR4D9) + } + desc2 += (String)settings."trig_${inType}_after_repeat_txt" ? lineBr() + spanSmBld("Edit in Response Designer...", sCLRGRY) : spanSm("Open Response Designer...", sCLRGRY) + href url: parent?.getTextEditorPath(app?.id as String, "trig_${inType}_after_repeat_txt"), style: sEXTNRL, title: inTS1("Custom ${inType?.capitalize()} Repeat Responses", sTEXT) + optPrefix(), description: desc2//, submitOnChange: true + } + } +} + +Boolean locationTriggers() { + return (valTrigEvt(sMODE) && (List)settings.trig_mode) || (valTrigEvt(sALRMSYSST) && (List)settings.trig_alarmSystemStatus) || + (valTrigEvt(sPISTNEXEC) && settings.trig_pistonExecuted) || + (valTrigEvt("guard") && settings.trig_guard) +} + +Boolean deviceTriggers() { + return (settings.trig_windowShade && settings.trig_windowShade_cmd) || (settings.trig_door && settings.trig_door_cmd) || (settings.trig_valve && settings.trig_valve_cmd) || + (settings.trig_switch && settings.trig_switch_cmd) || (settings.trig_level && settings.trig_level_cmd) || (settings.trig_lock && settings.trig_lock_cmd) || + (settings.trig_securityKeypad && settings.trig_securityKeypad_cmd) || +// (settings.trig_button && settings.trig_button_cmd) || + (settings.trig_pushed && settings.trig_pushed_cmd && settings.trig_pushed_nums) || + (settings.trig_released && settings.trig_released_cmd && settings.trig_released_nums) || + (settings.trig_held && settings.trig_held_cmd && settings.trig_held_nums) || + (settings.trig_doubleTapped && settings.trig_doubleTapped_cmd && settings.trig_doubleTapped_nums) || + (thermostatTriggers()) +} + +Boolean thermostatTriggers() { + List okList = [] + [sCOOLSP, sHEATSP, sTHERMTEMP].each { String att-> // Thermostat number value validation + if(valTrigEvt(att)) { okList.push((Boolean)(settings."trig_${att}" && settings."trig_${att}_cmd" && (settings."trig_${att}_low"!=null || settings."trig_${att}_high"!=null || settings."trig_${att}_equal"!=null))) } + } + [sTHERMMODE, sTHERMOS, sTHERMFM].each { String att-> // Thermostat non number validation + if(valTrigEvt(att)) { okList.push((Boolean)(settings."trig_${att}" && settings."trig_${att}_cmd")) } + } + // log.debug "thermostatTriggers | okList: ${okList} | Every: ${(okList.every { it == true })}" + return (okList.size()) ? (okList.every { it == true }) : false +} + +Boolean sensorTriggers() { + return (settings.trig_temperature && settings.trig_temperature_cmd) || (settings.trig_carbonMonoxide && settings.trig_carbonMonoxide_cmd) || (settings.trig_humidity && settings.trig_humidity_cmd) || + (settings.trig_battery && settings.trig_battery_cmd) || + (settings.trig_water && settings.trig_water_cmd) || (settings.trig_smoke && settings.trig_smoke_cmd) || (settings.trig_presence && settings.trig_presence_cmd) || (settings.trig_motion && settings.trig_motion_cmd) || + (settings.trig_contact && settings.trig_contact_cmd) || (settings.trig_power && settings.trig_power_cmd) || (settings.trig_illuminance && settings.trig_illuminance_low!=null && settings.trig_illuminance_high!=null) || + (settings.trig_acceleration && settings.trig_acceleration_cmd) +} + +Boolean triggersConfigured() { + Boolean sched = schedulesConfigured() + Boolean loc = locationTriggers() + Boolean dev = deviceTriggers() + Boolean sen = sensorTriggers() + // log.debug "sched: $sched | loc: $loc | dev: $dev | sen: $sen" + return (sched || loc || dev || sen) +} + +/****************************************************************************** + CONDITIONS SELECTION PAGE +******************************************************************************/ + +def conditionsPage() { + return dynamicPage(name: "conditionsPage", nextPage: "mainPage", title: "Conditions/Restrictions", install: false, uninstall: false) { + Boolean cra = !!(Boolean)settings.cond_require_all + Boolean multiConds = multipleConditions() + if(!multiConds && cra) { cra=false; settingUpdate("cond_require_all", sFALSE, sBOOL) } + String a = getConditionsDesc(false) + if(a) { section() { paragraph spanSm(a, sCLR4D9) } } + + section() { + if(multiConds) { + input "cond_require_all", sBOOL, title: inTS1("Require All Selected Conditions to Pass Before Action Runs?", sCHKBOX), required: false, defaultValue: true, submitOnChange: true + cra = (Boolean)settings.cond_require_all + } else { cra=false; settingUpdate("cond_require_all", sFALSE, sBOOL) } + Boolean allR = ( multiConds && cra ) // Boolean reqAllCond() + paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm(allR ? "All selected conditions must pass before for this action to operate." : "Any condition will allow this action to operate.", sCLR4D9) + } + section(sectHead("Time/Date")) { + href "condTimePage", title: inTS1("Time Schedule", "clock"), description: spanSm(getTimeCondDesc(false), sCLR4D9) + input "cond_days", sENUM, title: inTS1("Days of the week", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: weekDaysEnum() + input "cond_months", sENUM, title: inTS1("Months of the year", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: monthEnum() + } + section (sectHead("Mode Conditions")) { + input "cond_mode", sMODE, title: inTS1("Location Modes...", sMODE), multiple: true, required: false, submitOnChange: true + if((List)settings.cond_mode) { + input "cond_mode_cmd", sENUM, title: inTS1("are...", sCOMMAND), options: ["not":"Not in these modes", (sARE):"In these Modes"], required: true, multiple: false, submitOnChange: true + } + } + section (sectHead("Alarm Conditions")) { + input "cond_alarmSystemStatus", sENUM, title: inTS1("${getAlarmSystemName()} is...", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: false, submitOnChange: true + } + + condNonNumSect(sSWITCH, sSWITCH, "Switches/Outlets Conditions", "Switches/Outlets", lONOFF, sARE, sSWITCH) + + condNonNumSect(sMOTION, "motionSensor", "Motion Conditions", "Motion Sensors", lACTINACT, sARE, sMOTION) + + condNonNumSect("presence", "presenceSensor", "Presence Conditions", "Presence Sensors", lPRES, sARE, "presence") + + condNonNumSect(sCONTACT, "contactSensor", "Door, Window, Contact Sensors Conditions", "Contact Sensors", lOPNCLS, sARE, sCONTACT) + + condNonNumSect("acceleration", "accelerationSensor", "Accelerometer Conditions", "Accelerometer Sensors", lACTINACT, sARE, "acceleration") + + condNonNumSect(sLOCK, sLOCK, "Lock Conditions", "Smart Locks", lLOCKUNL, sARE, sLOCK) + + condNonNumSect("securityKeypad", "securityKeypad", "Security Keypad Conditions", "Security Kepads", lSEC, sARE, sKEYPAD) + + condNonNumSect("door", "garageDoorControl", "Garage Door Conditions", "Garage Doors", lOPNCLS, sARE, "garage_door") + + condNumValSect(sTEMP, "temperatureMeasurement", "Temperature Conditions", "Temperature Sensors", "Temperature", sTEMP) + + condNumValSect(sHUMID, "relativeHumidityMeasurement", "Humidity Conditions", "Relative Humidity Sensors", "Relative Humidity (%)", sHUMID) + + condNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Conditions", "Illuminance Sensors", "Lux Level (%)", "illuminance") + + condNumValSect(sLEVEL, "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level (%)", sSPDKNB) + + condNonNumSect(sWATER, "waterSensor", "Water Sensors", "Water Sensors", lWETDRY, sARE, sWATER) + + condNonNumSect(sSMOKE, "smokeDetector", "Smoke Detectors", "Smoke Detectors", lDETECTCLR, sARE, sSMOKE) + + condNonNumSect("carbonMonoxide", "carbonMonoxideDetector", "Carbon Monoxide Detectors", "Carbon Monoxide Detectors", lDETECTCLR, sARE, "co") + + condNumValSect(sPOWER, "powerMeter", "Power Events", "Power Meters", "Power Level (W)", sPOWER) + + condNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", lOPNCLS, sARE, "shade") + + condNonNumSect(sVALVE, sVALVE, "Valves", "Valves", lOPNCLS, sARE, sVALVE) + + condNumValSect(sBATT, sBATT, "Battery Level Conditions", "Batteries", "Level (%)", sBATT) + + condNonNumSectM(sTHERMMODE, sTHERM, "Thermostat Modes", "Thermostat Mode", getThermModeOpts(), sTHERM) + + condNonNumSectM(sTHERMOS, sTHERM, "Thermostat Operating States", "Thermostat Operating State", getThermOperStOpts(), sTHERM) + + condNonNumSectM(sTHERMFM, sTHERM, "Thermostat Fan Modes", "Thermostat Fan Mode", getThermFanOpts(), sTHERM) + + condNumValSect(sCOOLSP, sTHERM, "Cooling Setpoint", "Thermostat Cooling SetPoint", "Temperature", sTEMP) + + condNumValSect(sHEATSP, sTHERM, "Heating Setpoint", "Thermostat Heating SetPoint", "Temperature", sTEMP) + } +} + +static List getThermModeOpts() { + return ["auto", "cool", " heat", "emergency heat", "off"] +} + +static List getThermOperStOpts() { + return ["cooling", "heating", "idle", "fan only"] +} + +static List getThermFanOpts() { + return ["on", "circulate", "auto"] +} + +// allows multiple:true for _cmd +def condNonNumSectM(String inType, String capType, String sectStr, String devTitle, List cmdOpts, String image) { + section (sectHead(sectStr)) { + input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false + if (settings."cond_${inType}") { + input "cond_${inType}_cmd", sENUM, title: inTS1("${devTitle} is...", sCOMMAND), options: cmdOpts, multiple: true, required: true, submitOnChange: true + if (((List)settings."cond_${inType}")?.size() > 1 && settings."cond_${inType}_cmd"?.size() == 1) { + input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } else settingUpdate("cond_${inType}_all", sFALSE, sBOOL) + } + } +} + +def condNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, String image) { + section (sectHead(sectStr)) { + input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false + if (settings."cond_${inType}") { + input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle}...", sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true + if ((String)settings."cond_${inType}_cmd" && settings."cond_${inType}"?.size() > 1) { + input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + } +} + +static List numOpts() { + return [sBETWEEN, sNBETWEEN, sBELOW, sABOVE, sEQUALS] +} + +def condNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image) { + section (sectHead(sectStr)) { + input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required: false + if((List)settings."cond_${inType}") { + input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle} is...", sCOMMAND), options: numOpts(), required: true, multiple: false, submitOnChange: true + String c_cmd = (String)settings."cond_${inType}_cmd" + if (c_cmd) { + if (c_cmd in [sNBETWEEN, sBETWEEN, sBELOW]) { + input "cond_${inType}_low", sNUMBER, title: inTS1("a ${c_cmd in [sNBETWEEN, sBETWEEN] ? "Low " : sBLANK}${cmdTitle} of...", "low"), required: true, submitOnChange: true + } + if (c_cmd in [sNBETWEEN, sBETWEEN, sABOVE]) { + input "cond_${inType}_high", sNUMBER, title: inTS1("${c_cmd in [sNBETWEEN, sBETWEEN] ? "and a high " : "a "}${cmdTitle} of...", "high"), required: true, submitOnChange: true + } + if (c_cmd == sEQUALS) { + input "cond_${inType}_equal", sNUMBER, title: inTS1("a ${cmdTitle} of...", sEQ), required: true, submitOnChange: true + } + if (((List)settings."cond_${inType}")?.size() > 1) { + input "cond_${inType}_all", sBOOL, title: inTS1("Require ALL devices to be (${c_cmd}) values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + if(!(Boolean)settings."cond_${inType}_all") { + input "cond_${inType}_avg", sBOOL, title: inTS1("Use the average of all selected device values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + } + } + } +} + +private Map devsSupportVolume(List devs) { + // this will never return a zone virtual device + List noSupport = [] + List supported = [] + if(devs?.size()) { + devs.each { dev-> + if(dev?.hasAttribute("permissions") && dev?.currentPermissions?.toString()?.contains("volumeControl")) { + supported.push((String)dev.label) + } else { noSupport.push((String)dev.label) } + } + } + return [s:supported, n:noSupport] +} + +def actVariableDesc(String actType, Boolean hideUserTxt=false) { + Map> txtItems = customTxtItems() + if(!isTierAction()) { + if(!txtItems?.size() && state.showSpeakEvtVars && !(String)settings."act_${actType}_txt") { + String str = "NOTICE:
    You can choose to leave the response field empty and generic text will be generated for each event type, or return to Step 2 and define responses for each trigger." + paragraph spanSm(str, sCLR4D9, getAppImg("info")) + } + if(!hideUserTxt) { + if(txtItems?.size()) { + String str = spanSmBld("NOTICE: (Custom Text Defined)", sCLRORG) + txtItems?.each { i-> + i?.value?.each { i2-> str += lineBr() + spanSmBld("${sBULLET} ${i?.key?.capitalize()} ${i2?.key?.capitalize()}: ") + spanSm("(${i2?.value?.size()} Response${pluralizeStr(i2?.value)})") } + } + paragraph spanSm(str, sCLR4D9) + paragraph spanSmBld("WARNING:
    Entering text below will override the text you defined for the trigger types under Step 2.", sCLRRED) + } + } + } +} + +@Field static final Map descsFLD = [ + speak: "Speak any message on your Echo Devices.", + speak_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken.", + speak_parallel: "Speak any message in parallel on your Echo Devices.", + speak_parallel_tiered: "Allows you to create parallel tiered responses. Each tier can have a different delay before the next message is spoken.", + speak_ignDND: "Speak any message on your Echo Devices, ignoring Do Not Disturb.", + speak_ignDND_parallel: "Speak any message on your Echo Devices, ignoring Do Not Disturb, in parallel.", + speak_ignDND_parallel_tiered:"Allows you to create parallel tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", + speak_ignDND_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", + announcement: "Plays a brief tone and speaks the message you define. If you select multiple devices it will be a synchronized broadcast.", + announcement_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is announced. Plays a brief tone and announces the message you define. If you select multiple devices it will be a synchronized broadcast.", + announcement_ignDND_tiered: "Allows you to create tiered responses. Each tier can have a different delay before the next message is spoken, while ignoring Do Not Disturb.", + announcement_ignDND: "Plays a brief tone and speaks the message you define. If you select multiple devices it will be a synchronized broadcast, ignoring Do Not Disturb.", + sequence: "Sequences are a custom command where you can string different alexa actions which are sent to Amazon as a single command. The command is then processed by amazon sequentially or in parallel.", + weather: "Plays a very basic weather report.", + playback: "Allows you to control the media playback state or volume level of your Echo devices.", + builtin: "Builtin items are things like Sing a Song, Tell a Joke, Say Goodnight, etc.", + sounds: "Plays a selected amazon sound item.", + voicecmd: "Executes a text based command just as if it was spoken to the device", + music: "Allows playback of various Songs/Radio using any connected music provider", + calendar: "This will read out events in your calendar (Requires accounts to be configured in the alexa app. Must not have PIN.)", + alarm: "This will allow you to create Alexa alarm clock notifications.", + reminder: "This will allow you to create Alexa reminder notifications.", + dnd: "This will allow you to enable/disable Do Not Disturb mode", + alexaroutine: "This will allow you to run your configured Alexa Routines", + wakeword: "This will allow you to change the Wake Word of your Echo devices.", + bluetooth: "This will allow you to connect or disconnect bluetooth devices paired to your echo devices." +] + +String actionTypeDesc() { + String a = (String)settings.actionType + return descsFLD.containsKey(a) ? (String)descsFLD[a] : "No Description Found..." +} + +def actionTiersPage() { + return dynamicPage(name: "actionTiersPage", title: sBLANK, install: false, uninstall: false) { + section() { + input "act_tier_cnt", sNUMBER, title: inTS1("How many Tiers?", sEQ), required: true, submitOnChange: true + } + Integer tierCnt = (Integer)settings.act_tier_cnt + if(tierCnt) { + (1..tierCnt)?.each { Integer ti-> + String tstr = sACT_ITEM+"${ti}" + section(sectHead("Tier Item (${ti}) Config:")) { + if(ti > 1) { + input "${tstr}_delay", sNUMBER, title: inTS1("Delay after Tier ${ti-1}\n(seconds)", sEQ), defaultValue: null, required: true, submitOnChange: true + } + if(ti==1 || settings."${tstr}_delay") { + String inTxt = (String)settings."${tstr}_txt" + href url: parent?.getTextEditorPath(app?.id as String, "${tstr}_txt"), style: sEXTNRL, required: true, title: inTS1("Tier Item ${ti} Response", sTEXT) + (!inTxt ? spanSm(" (Required)", sCLRRED) : sBLANK), + description: inTxt ? spanSm(inTxt, sCLR4D9) : inactFoot("Open Response Designer...") + } + input "${tstr}_volume_change", sNUMBER, title: inTS1("Tier Item Volume", sSPDKNB) + optPrefix(), defaultValue: null, required: false, submitOnChange: true + input "${tstr}_volume_restore", sNUMBER, title: inTS1("Tier Item Volume Restore", sSPDKNB) + optPrefix(), defaultValue: null, required: false, submitOnChange: true + } + } + if(isTierActConfigured()) { + section(sBLANK) { + paragraph spanMdBldBr("Tier Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") + } + } + } + } +} + +String getTierRespDesc() { + Map tierMap = getTierMap() ?: [:] + String str = sBLANK + str += tierMap?.size() ? spanSmBr("Tiered Responses: (${tierMap?.size()})") : sBLANK + tierMap?.each { k,v-> + String tstr = sACT_ITEM+"${k}" + if((String)settings."${tstr}_txt") { + str += (settings."${tstr}_delay") ? spanSmBr(" ${sBULLET} Tier ${k} delay: (${settings."${tstr}_delay"} sec)") : sBLANK + str += (settings."${tstr}_volume_change") ? spanSmBr(" ${sBULLET} Tier ${k} volume: (${settings."${tstr}_volume_change"})") : sBLANK + str += (settings."${tstr}_volume_restore") ? spanSmBr(" ${sBULLET} Tier ${k} restore: (${settings."${tstr}_volume_restore"})") : sBLANK + } + } + return str != sBLANK ? str : sBLANK +} + +Boolean isTierAction() { + return ((String)settings.actionType in [sSPEAKT, sSPEAKPT, sSPEAKIT, sSPEAKIPT, sANNT, sANNIT]) +} + +Boolean isTierActConfigured() { + if(!isTierAction()) { return false } + Integer cnt = (Integer)settings.act_tier_cnt + List tierKeys = settings.findAll { it?.key?.startsWith(sACT_ITEM) && it?.key?.endsWith("_txt") }?.collect { (String)it?.key } + return (tierKeys?.size() == cnt) +} + +Map getTierMap() { + Map exec = [:] + Integer cnt = (Integer)settings.act_tier_cnt + if(isTierActConfigured() && cnt) { + List tiers = (1..cnt) + tiers?.each { Integer t-> + String tstr = sACT_ITEM+"${t}" + exec[t] = [ + message: (String)settings["${tstr}_txt"], + delay: settings[sACT_ITEM+"${(t+1)}_delay"], + volume: [ + change: settings["${tstr}_volume_change"], + restore: settings["${tstr}_volume_restore"] + ] + ] + } + } + return (exec?.size()) ? exec : null +} + +void tierItemCleanup() { + List rem = [] + Boolean isTierAct = isTierAction() + Integer tierCnt = (Integer)settings.act_tier_cnt + List tierKeys = settings.findAll { it?.key?.startsWith(sACT_ITEM) }?.collect { it.key as String } + List tierIds = isTierAct && tierCnt ? (1..tierCnt) : [] + // if(!isTierAct() || !tierCnt) { return } + tierKeys?.each { String k-> + List id = k?.tokenize("_") ?: [] + if(!isTierAct || (id?.size() && id?.size() < 4) || !id[3]?.isNumber() || !(id[3]?.toInteger() in tierIds)) { rem.push(k) } + } + if(rem.size()) { logDebug("tierItemCleanup | Removing: ${rem}"); rem.each { settingRemove(it) } } +} + +def actTextOrTiersInput(String type, Boolean req=false) { + if(isTierAction()) { + String tDesc = getTierRespDesc() + href "actionTiersPage", title: inTS1("Create Tiered Responses?", sTEXT, (tDesc ? sCLR4D9 : sCLRRED)), description: (tDesc ? divSm(tDesc + inputFooter(sTTM), sCLR4D9) : inputFooter(sTTC, sCLRGRY)) + input "act_tier_stop_on_clear", sBOOL, title: inTS1("Stop responses when trigger is cleared?"), required: false, defaultValue: false, submitOnChange: true + } else { + String textUrl = parent?.getTextEditorPath(app?.id as String, type) + String addr = !req ? optPrefix() : sBLANK + href url: textUrl, style: sEXTNRL, title: inTS1("Default Action Response", sTEXT) + addr, description: (String)settings."${type}" ? spanSm((String)settings."${type}", sCLR4D9) : inputFooter("Open Response Designer...", sCLRGRY, true) + } +} + +@SuppressWarnings('GroovyFallthrough') +def actionsPage() { + return dynamicPage(name: "actionsPage", nextPage: "mainPage", title: "Action Execution Configuration", install: false, uninstall: false) { + String a = getActionDesc(false) + if(a) { + section() { + paragraph spanSm(a, sCLR4D9) + } + } + Boolean done = false + Map actionExecMap = [configured: false] + String myactionType = (String)settings.actionType + if(myactionType) { + actionExecMap.actionType = myactionType + actionExecMap.config = [:] + List devices = parent?.getDevicesFromList(settings.act_EchoDevices) + String actTypeDesc = spanBldBr("[${myactionType.tokenize("_")?.collect { it?.capitalize() }?.join(sSPACE)}]", sCLR4D9, "info") + spanSm(actionTypeDesc(), sCLRGRY) + Boolean isTierAct = isTierAction() + switch(myactionType) { + case sSPEAK: + case sSPEAKI: + case sSPEAKP: + case sSPEAKIP: + case sSPEAKT: + case sSPEAKIT: + case sSPEAKPT: + case sSPEAKIPT: + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + section(sectHead("Action Type Config:")) { + actVariableDesc(myactionType) + actTextOrTiersInput("act_speak_txt") + } + if(!(myactionType in [sSPEAKP, sSPEAKPT, sSPEAKIP, sSPEAKIPT])) actionVolumeInputs(devices) + actionExecMap.config[myactionType] = [text: (String)settings.act_speak_txt, evtText: ((state.showSpeakEvtVars && !(String)settings.act_speak_txt) || hasUserDefinedTxt()), tiers: getTierMap()] + done = state.showSpeakEvtVars || (String)settings.act_speak_txt || (isTierAct && isTierActConfigured()) + } else { done = false } + break + + case sANN: + case sANNI: + case sANNT: + case sANNIT: + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm("announce") + if(settings.act_EchoDevices || settings.act_EchoZones) { + section(sectHead("Action Type Config:")) { + actVariableDesc(myactionType) + actTextOrTiersInput("act_announcement_txt") + } + actionVolumeInputs(devices) + actionExecMap.config[myactionType] = [text: (String)settings.act_announcement_txt, evtText: ((state.showSpeakEvtVars && !(String)settings.act_announcement_txt) || hasUserDefinedTxt()), tiers: getTierMap()] + if(settings.act_EchoDevices?.size() > 1) { + List devObj = [] + devices?.each { devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]) } + // log.debug "devObj: $devObj" + actionExecMap.config[myactionType].deviceObjs = devObj + } + done = state.showSpeakEvtVars || (String)settings.act_announcement_txt || (isTierAct && isTierActConfigured()) + } else { done = false } + break + + case "voicecmd": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices) { + section(sectHead("Action Type Config:")) { + input "act_voicecmd_txt", sTEXT, title: inTS1("Enter voice command text", sTEXT), submitOnChange: true, required: false + } + actionExecMap.config.voicecmd = [text: (String)settings.act_voicecmd_txt, cmd: "voiceCmdAsText"] + if((String)settings.act_voicecmd_txt) { done = true } else { done = false } + } else { done = false } + break + + case "sequence": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + Map seqItemsAvail = parent?.seqItemsAvail() + section(sectHead("Sequence Options Legend:"), hideable: true, hidden: false) { + String str1 = "Sequence Options:" + seqItemsAvail?.other?.sort()?.each { k, v-> + str1 += "${bulletItem(str1, "${k}${v != null ? "::${v}" : sBLANK}")}" + } + String str4 = "DoNotDisturb Options:" + seqItemsAvail?.dnd?.sort()?.each { k, v-> + str4 += "${bulletItem(str4, "${k}${v != null ? "::${v}" : sBLANK}")}" + } + String str2 = "Music Options:" + seqItemsAvail?.music?.sort()?.each { k, v-> + str2 += "${bulletItem(str2, "${k}${v != null ? "::${v}" : sBLANK}")}" + } + String str3 = "Canned TTS Options:" + seqItemsAvail?.speech?.sort()?.each { k, v-> + def newV = v + if(v instanceof List) { newV = sBLANK; v?.sort()?.each { newV += s5SPACE+dashItem(newV, "${it}", true) } } + str3 += "${bulletItem(str3, "${k}${newV != null ? "::${newV}" : sBLANK}")}" + } + paragraph spanSm(str1, sCLR4D9) + // paragraph spanSm(str4, sCLR4D9) + paragraph spanSm(str2, sCLR4D9) + paragraph spanSm(str3, sCLR4D9) + paragraph spanSm("Enter the command in a format exactly like this:
    volume::40,, speak::this is so silly,, wait::60,, weather,, cannedtts_random::goodbye,, traffic,, amazonmusic::green day,, volume::30

    Each command needs to be separated by a double comma `,,` and the separator between the command and value must be command::value.", "violet") + } + section(sectHead("Action Type Config:")) { + input "act_sequence_txt", sTEXT, title: inTS1("Enter sequence text", sTEXT), submitOnChange: true, required: false + } + actionExecMap.config.sequence = [text: (String)settings.act_sequence_txt, cmd: "executeSequenceCommand"] + if((String)settings.act_sequence_txt) { done = true } else { done = false } + } else { done = false } + break + + case sWEATH: + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + actionVolumeInputs(devices) + done = true + actionExecMap.config.weather = [cmd: "playWeather"] + } else { done = false } + break + + case "playback": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm("mediaPlayer") + if(settings.act_EchoDevices || settings.act_EchoZones) { + Map playbackOpts = [ + "pause":"Pause", "stop":"Stop", "play":"Play", "nextTrack":"Next Track", "previousTrack":"Previous Track", + "mute":"Mute", "volume":"Volume" + ] + section(sectHead("Playback Config:")) { + input "act_playback_cmd", sENUM, title: inTS1("Select Playback Action", sCOMMAND), description: sBLANK, options: playbackOpts, required: true, submitOnChange: true + } + if(settings.act_playback_cmd == "volume") { actionVolumeInputs(devices, true) } + actionExecMap.config?.playback = [cmd: settings.act_playback_cmd] + if(settings.act_playback_cmd) { done = true } else { done = false } + } else { done = false } + break + + case "sounds": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + section(sectHead("BuiltIn Sounds Config:")) { + input "act_sounds_cmd", sENUM, title: inTS1("Select Sound Type", sCOMMAND), description: sBLANK, options: parent?.getAvailableSounds()?.collect { it?.key as String }, required: true, submitOnChange: true + } + actionVolumeInputs(devices) + actionExecMap.config.sounds = [cmd: "playSoundByName", name: settings.act_sounds_cmd] + done = (settings.act_sounds_cmd != null) + } else { done = false } + break + + case "builtin": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + Map builtinOpts = [ + "playSingASong": "Sing a Song", "playFlashBrief": "Flash Briefing (News)", "playGoodNews": "Good News Only", "playFunFact": "Fun Fact", "playTraffic": "Traffic", "playJoke": "Joke", + "playTellStory": "Tell Story", "sayGoodbye": "Say Goodbye", "sayGoodNight": "Say Goodnight", "sayBirthday": "Happy Birthday", + "sayCompliment": "Give Compliment", "sayGoodMorning": "Good Morning", "sayWelcomeHome": "Welcome Home" + ] + section(sectHead("BuiltIn Speech Config:")) { + input "act_builtin_cmd", sENUM, title: inTS1("Select Builtin Speech Type", sCOMMAND), description: sBLANK, options: builtinOpts, required: true, submitOnChange: true + } + actionVolumeInputs(devices) + actionExecMap.config.builtin = [cmd: settings.act_builtin_cmd] + if(settings.act_builtin_cmd) { done = true } else { done = false } + } else { done = false } + break + + case "music": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm("mediaPlayer") + if(settings.act_EchoDevices || settings.act_EchoZones) { + List musicProvs = devices[0]?.hasAttribute("supportedMusic") ? devices[0]?.currentValue("supportedMusic")?.split(",")?.collect { "${it?.toString()?.trim()}"} : [] + logDebug("Music Providers: ${musicProvs}") + if(musicProvs) { + section(sectHead("Music Providers:")) { + input "act_music_provider", sENUM, title: inTS1("Select Music Provider", "music"), description: sBLANK, options: musicProvs, multiple: false, required: true, submitOnChange: true + } + if(settings.act_music_provider) { + if(settings.act_music_provider == "TuneIn") { + section(sectHead("TuneIn Search Results:")) { + paragraph spanSm("Enter a search phrase to query TuneIn to help you find the right search term to use in searchTuneIn() command.") + input "tuneinSearchQuery", sTEXT, title: inTS1("Enter search phrase for TuneIn", "tunein"), defaultValue: null, required: false, submitOnChange: true + if(settings.tuneinSearchQuery) { + href "searchTuneInResultsPage", title: inTS1("View search results!", "search"), description: sTTP + } + } + } + section(sectHead("Action Type Config:")) { + input "act_music_txt", sTEXT, title: inTS1("Enter Music Search text", sTEXT), submitOnChange: true, required: false + } + actionVolumeInputs(devices) + } + } + actionExecMap.config.music = [cmd: "searchMusic", provider: settings.act_music_provider, search: (String)settings.act_music_txt] + done = settings.act_music_provider && (String)settings.act_music_txt + } else { done = false } + break + + case "calendar": + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm(sTTS) + if(settings.act_EchoDevices || settings.act_EchoZones) { + section(sectHead("Action Type Config:")) { + input "act_calendar_cmd", sENUM, title: inTS1("Select Calendar Action", sCOMMAND), description: sBLANK, options: ["playCalendarToday":"Today", "playCalendarTomorrow":"Tomorrow", "playCalendarNext":"Next Events"], + required: true, submitOnChange: true + } + actionVolumeInputs(devices) + actionExecMap.config.calendar = [cmd: settings.act_calendar_cmd] + if(act_calendar_cmd) { done = true } else { done = false } + } else { done = false } + break + + case "alarm": + //TODO: Offer to remove alarm after event. + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm("alarms") + if(settings.act_EchoDevices) { +// Map repeatOpts = ["everyday":"Everyday", "weekends":"Weekends", "weekdays":"Weekdays", "daysofweek":"Days of the Week", "everyxdays":"Every Nth Day"] + String rptType = sNULL + def rptTypeOpts = null + section(sectHead("Action Type Config:")) { + input "act_alarm_label", sTEXT, title: inTS1("Alarm Label", "name_tag"), submitOnChange: true, required: true + input "act_alarm_date", sTEXT, title: inTS1("Alarm Date\n(yyyy-mm-dd)", "day_calendar"), submitOnChange: true, required: true + input "act_alarm_time", sTIME, title: inTS1("Alarm Time", "clock"), submitOnChange: true, required: true + // if(act_alarm_label && act_alarm_date && act_alarm_time) { + // input "act_alarm_rt", sENUM, title: inTS1("Repeat (Optional)", sCOMMAND), description: sBLANK, options: repeatOpts, required: true, submitOnChange: true + // if(settings."act_alarm_rt") { + // rptType = settings.act_alarm_rt + // if(settings.act_alarm_rt == "daysofweek") { + // input "act_alarm_rt_wd", sENUM, title: inTS1("Weekday", sCHKBOX), description: sBLANK, options: weekDaysAbrvEnum(), multiple: true, required: true, submitOnChange: true + // if(settings.act_alarm_rt_wd) rptTypeOpts = settings.act_alarm_rt_wd + // } + // if(settings."act_alarm_rt" == "everyxdays") { + // input "act_alarm_rt_ed", sNUMBER, title: inTS1("Every X Days", sCHKBOX), description: sBLANK, range: "1..31", required: true, submitOnChange: true + // if(settings.act_alarm_rt_ed) rptTypeOpts = settings.act_alarm_rt_ed + // } + // } + // } + // input "act_alarm_remove", sBOOL, title: "Remove Alarm when done", defaultValue: true, submitOnChange: true, required: false + } + actionVolumeInputs(devices, false, true) + String newTime = settings.act_alarm_time ? parseFmtDt("yyyy-MM-dd'T'HH:mm:ss.SSSZ", 'HH:mm', settings.act_alarm_time) : sNULL + actionExecMap.config.alarm = [cmd: "createAlarm", label: settings.act_alarm_label, date: settings.act_alarm_date, (sTIME): newTime, recur: [type: rptType, opts: rptTypeOpts], remove: settings.act_alarm_remove] + done = settings.act_alarm_label && settings.act_alarm_date && settings.act_alarm_time + } else { done = false } + break + + case "reminder": + //TODO: Offer to remove reminder after event. + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + echoDevicesInputByPerm("reminders") + if(settings.act_EchoDevices) { +// Map repeatOpts = ["everyday":"Everyday", "weekends":"Weekends", "weekdays":"Weekdays", "daysofweek":"Days of the Week", "everyxdays":"Every Nth Day"] + String rptType = sNULL + def rptTypeOpts = null + section(sectHead("Action Type Config:")) { + input "act_reminder_label", sTEXT, title: inTS1("Reminder Label", "name_tag"), submitOnChange: true, required: true + input "act_reminder_date", sTEXT, title: inTS1("Reminder Date\n(yyyy-mm-dd)", "day_calendar"), submitOnChange: true, required: true + input "act_reminder_time", sTIME, title: inTS1("Reminder Time", "clock"), submitOnChange: true, required: true + // if(settings.act_reminder_label && settings.act_reminder_date && settings.act_reminder_time) { + // input "act_reminder_rt", sENUM, title: inTS1("Repeat (Optional)", sCOMMAND), description: sBLANK, options: repeatOpts, required: true, submitOnChange: true + // if(settings."act_reminder_rt") { + // rptType = settings.act_reminder_rt + // if(settings."act_reminder_rt" == "daysofweek") { + // input "act_reminder_rt_wd", sENUM, title: inTS1("Weekday", sCHKBOX), description: sBLANK, options: weekDaysAbrvEnum(), multiple: true, required: true, submitOnChange: true + // if(settings.act_reminder_rt_wd) rptTypeOpts = settings.act_reminder_rt_wd + // } + // if(settings."act_reminder_rt" && settings."act_reminder_rt" == "everyxdays") { + // input "act_reminder_rt_ed", sNUMBER, title: inTS1("Every X Days (1-31)", sCHKBOX), description: sBLANK, range: "1..31", required: true, submitOnChange: true + // if(settings.act_reminder_rt_ed) rptTypeOpts = settings.act_reminder_rt_ed + // } + // } + // } + // input "act_reminder_remove", sBOOL, title: "Remove Reminder when done", defaultValue: true, submitOnChange: true, required: false + } + actionVolumeInputs(devices, false, true) + String newTime = settings.act_reminder_time ? parseFmtDt("yyyy-MM-dd'T'HH:mm:ss.SSSZ", 'HH:mm', settings.act_reminder_time) : sNULL + actionExecMap.config.reminder = [cmd: "createReminderNew", label: settings.act_reminder_label, date: settings.act_reminder_date, (sTIME): newTime, recur: [type: rptType, opts: rptTypeOpts], remove: settings.act_reminder_remove] + done = settings.act_reminder_label && settings.act_reminder_date && settings.act_reminder_time + } else { done = false } + break + + case "dnd": + echoDevicesInputByPerm("doNotDisturb") + if(settings.act_EchoDevices) { + Map dndOpts = ["doNotDisturbOn":"Enable", "doNotDisturbOff":"Disable"] + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + section(sectHead("Action Type Config:")) { + input "act_dnd_cmd", sENUM, title: inTS1("Select Do Not Disturb Action", sCOMMAND), description: sBLANK, options: dndOpts, required: true, submitOnChange: true + } + actionExecMap.config.dnd = [cmd: settings.act_dnd_cmd] + if(settings.act_dnd_cmd) { done = true } else { done = false } + } else { done = false } + break + + case "alexaroutine": + echoDevicesInputByPerm("wakeWord") + if(settings.act_EchoDevices) { + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + Map t0 = parent?.getAlexaRoutines() + Map routinesAvail = t0 ?: [:] + // logDebug("routinesAvail: $routinesAvail") + section(sectHead("Action Type Config:")) { + input "act_alexaroutine_cmd", sENUM, title: inTS1("Select Alexa Routine", sCOMMAND), description: sBLANK, options: routinesAvail, multiple: false, required: true, submitOnChange: true + } + actionExecMap.config.alexaroutine = [cmd: "executeRoutineId", routineId: settings.act_alexaroutine_cmd] + if(settings.act_alexaroutine_cmd) { done = true } else { done = false } + } else { done = false } + break + + case "wakeword": + echoDevicesInputByPerm("wakeWord") + if(settings.act_EchoDevices) { + Integer devsCnt = settings.act_EchoDevices.size() ?: 0 + List devsObj = [] + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + if(devsCnt >= 1) { + List wakeWords = devices[0]?.hasAttribute("wakeWords") ? devices[0]?.currentValue("wakeWords")?.replaceAll('"', sBLANK)?.split(",") : [] + // logDebug("WakeWords: ${wakeWords}") + devices?.each { cDev-> + section(sectHead("${cDev?.getLabel()}:")) { + if(wakeWords?.size()) { + paragraph "Current Wake Word: ${cDev?.hasAttribute("alexaWakeWord") ? cDev?.currentValue("alexaWakeWord") : "Unknown"}" + input "act_wakeword_device_${cDev?.id}", sENUM, title: inTS1("New Wake Word", "list"), description: sBLANK, options: wakeWords, required: true, submitOnChange: true + devsObj.push([device: cDev?.id as String, wakeword: settings."act_wakeword_device_${cDev?.id}", cmd: "setWakeWord"]) + } else { paragraph spanSmBld("Oops...
    No Wake Words have been found!

    Please Remove the device from selection.", sCLRRED) } + } + } + } + actionExecMap.config.wakeword = [ devices: devsObj] + done = settings.findAll { it?.key?.startsWith("act_wakeword_device_") && it?.value }?.size() == devsCnt + } else { done = false } + break + + case "bluetooth": + echoDevicesInputByPerm("bluetoothControl") + if(settings.act_EchoDevices) { + Integer devsCnt = settings.act_EchoDevices?.size() ?: 0 + List devsObj = [] + section(sectHead("Action Description:")) { paragraph spanSm(actTypeDesc, sCLR4D9) } + if(devsCnt >= 1) { + devices?.each { cDev-> + def btData = cDev?.hasAttribute("btDevicesPaired") ? cDev?.currentValue("btDevicesPaired") : null + List btDevs = (btData) ? parseJson(btData)?.names : [] + // log.debug "btDevs: $btDevs" + section(sectHead("${cDev?.getLabel()}:")) { + if(btDevs?.size()) { + input "act_bluetooth_device_${cDev?.id}", sENUM, title: inTS1("BT device to use", "bluetooth"), description: sBLANK, options: btDevs, required: true, submitOnChange: true + input "act_bluetooth_action_${cDev?.id}", sENUM, title: inTS1("BT action to take", sCOMMAND), description: sBLANK, options: ["connectBluetooth":"connect", "disconnectBluetooth":"disconnect"], required: true, submitOnChange: true + devsObj?.push([device: cDev?.id as String, btDevice: settings."act_bluetooth_device_${cDev?.id}", cmd: settings."act_bluetooth_action_${cDev?.id}"]) + } else { paragraph spanSmBldBr("Oops...") + spanSm("No Bluetooth devices are paired to this Echo Device!
    Please Remove the device from selection.", sCLRRED) } + } + } + } + actionExecMap.config.bluetooth = [devices: devsObj] + done = settings.findAll { it?.key?.startsWith("act_bluetooth_device_") && it?.value }?.size() == devsCnt && + settings.findAll { it?.key?.startsWith("act_bluetooth_action_") && it?.value }?.size() == devsCnt + } else { done = false } + break + default: + paragraph spanSmBld("Unknown Action Type Defined...", sCLRRED, getAppImg("error")) + break + } + if(done) { + section(sectHead("Delay Config:")) { + input "act_delay", sNUMBER, title: inTS1("Delay Action in seconds", sDELAYT) + optPrefix(), required: false, submitOnChange: true + } + if(isTierAct && (Integer)settings.act_tier_cnt > 1) { + section(sectHead("Tier Action Start Tasks:")) { + href "actTrigTasksPage", title: inTS1("Tiered Tasks to Perform on Tier Start?", "tasks"), description: spanSm(actTaskDesc(sACT_START, true), sCLR4D9), params:[type: sACT_START] + } + section(sectHead("Tier Action Stop Tasks:")) { + href "actTrigTasksPage", title: inTS1("Tiered Tasks to Perform on Tier Stop?", "tasks"), description: spanSm(actTaskDesc(sACT_STOP, true), sCLR4D9), params:[type: sACT_STOP] + } + } else { + section(sectHead("Action Triggered Tasks:")) { + href "actTrigTasksPage", title: inTS1("Tasks to Perform when Triggered?", "tasks"), description: spanSm(actTaskDesc(sACT, true), sCLR4D9), params:[type: sACT] + } + } + actionSimulationSect() + section(sBLANK) { + paragraph spanMdBldBr("Execution Configuration Complete", sNULL, getAppImg("done")) + spanSm("Press Next to Return to Main Page") + } + actionExecMap.config.volume = [change: settings.act_volume_change, restore: settings.act_volume_restore, alarm: settings.act_alarm_volume] + actionExecMap.config.zoneVolume = getZoneVolumeMap() + // log.debug "zoneVolume: ${actionExecMap.config.zoneVolume}" + actionExecMap.delay = settings.act_delay + actionExecMap.configured = true + actionCleanup() + //TODO: Check Cleanup of non selected inputs + } else { actionExecMap = [configured: false] } + } + Map t1 = (done && (Boolean)actionExecMap.configured) ? actionExecMap : [configured: false] + state.actionExecMap = t1 + updConfigStatusMap() + logDebug("actionExecMap: ${t1}") + } +} + +def actTrigTasksPage(params) { + String t = params?.type + if(t) { + atomicState.curPageParams = params + } else { t = atomicState.curPageParams?.type } + return dynamicPage(name: "actTrigTasksPage", title: sBLANK, install: false, uninstall: false) { + Map dMap = [:] + section() { + switch(t) { + case sACT: + dMap = [def: sBLANK, delay: "tasks"] + paragraph spanSm("These tasks will be performed when the action is triggered.
    (Delay is optional)", sCLR4D9) + break + case sACT_START: + dMap = [def: " with Tier start", delay: "Tier Start tasks"] + paragraph spanSm("These tasks will be performed with when the first tier of action is triggered.
    (Delay is optional)", sCLR4D9) + break + case sACT_STOP: + dMap = [def: " with Tier stop", delay: "Tier Stop tasks"] + paragraph spanSm("These tasks will be performed with when the last tier of action is triggered.
    (Delay is optional)", sCLR4D9) + break + default: + logWarn("bad actTaskDesc t: ${t}", true) + } + } + + if(!settings.enableWebCoRE) { + section (sectHead("Enable webCoRE Integration:")) { + input "enableWebCoRE", sBOOL, title: inTS("Enable webCoRE Integration", webCore_icon()), required: false, defaultValue: false, submitOnChange: true + } + } + if(settings.enableWebCoRE) { + if(!webCoREFLD) webCoRE_init() + webCoRE_poll() + } + section(sectHead("Control Devices:")) { + input "${t}switches_on", "capability.switch", title: inTS1("Turn ON these Switches${dMap?.def}", sSWITCH) + optPrefix(), multiple: true, required: false, submitOnChange: true + input "${t}switches_off", "capability.switch", title: inTS1("Turn OFF these Switches${dMap?.def}", sSWITCH) + optPrefix(), multiple: true, required: false, submitOnChange: true + } + + section(sectHead("Control Dimmers/Lights:")) { + input "${t}lights", "capability.switch", title: inTS1("Turn ON these Lights${dMap?.def}", "light") + optPrefix(), multiple: true, required: false, submitOnChange: true + if((List)settings."${t}lights") { + List lights = (List)settings."${t}lights" + List noLevelDevs = lights?.findAll { i-> (!(i?.hasCommand("setLevel") ) ) } + List noColorDevs = lights?.findAll { i-> ( !(i?.hasCommand("setColor") ) )} + + // Detemine if any of the devices supports level + Boolean lvlSupportAny = lights?.any { i-> (i?.hasCommand("setLevel")) } + Boolean lvlSupportAll = lights?.every { i-> (i?.hasCommand("setLevel")) } + if (lvlSupportAny) { + input "${t}lights_leveln", sNUMBER, title: inTS1("At this level?", sSPDKNB) + optPrefix(), multiple: false, options: dimmerLevelEnum(), required: false, submitOnChange: true + if(!lvlSupportAll && noLevelDevs?.size() > 0) { + paragraph spanSmBldBr("NOTICE: The following selected devices do not support level: ", sCLRRED) + spanSm(noLevelDevs.collect { " ${sBULLET} ${it}" }.join(lineBr()).toString(), sCLRRED) + } + } + + // Detemine if any of the devices supports color control + Boolean clrSupportAny = lights?.any { i-> (i?.hasCommand("setColor")) } + Boolean clrSupportAll = lights?.every { i-> (i?.hasCommand("setColor")) } + if(clrSupportAny) { + input "${t}lights_colort", sENUM, title: inTS1("To this color?", "light_color") + optPrefix(), multiple: false, options: colorSettingsListFLD?.name, required: false, submitOnChange: true + if (!clrSupportAll && noColorDevs.size() > 0) { + paragraph spanSmBldBr("NOTICE: The following selected devices do not support color: ", sCLRRED) + spanSm(noColorDevs.collect { " ${sBULLET} ${it}" }.join(lineBr()).toString(), sCLRRED) + } + } + + input "${t}lights_trig_clear_restore", sBOOL, title: inTS1("Restore original Light states when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true + input "${t}lights_restore_delay", sNUMBER, title: inTS1("Restore original light states after (x) seconds?", sDELAYT) + optPrefix(), required: false, submitOnChange: true + } + + } + + section(sectHead("Control Locks:")) { + input "${t}locks_lock", "capability.lock", title: inTS1("Lock these Locks${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true + input "${t}locks_unlock", "capability.lock", title: inTS1("Unlock these Locks${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true + if(settings."${t}locks_lock" || settings."${t}locks_unlock") { + input "${t}locks_trig_clear_restore", sBOOL, title: inTS1("Restore original lock state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true + } + } + + section(sectHead("Control Keypads:")) { + input "${t}securityKeypads_disarm", "capability.securityKeypad", title: inTS1("Disarm these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true + input "${t}securityKeypads_armHome", "capability.securityKeypad", title: inTS1("Arm Home these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true + input "${t}securityKeypads_armAway", "capability.securityKeypad", title: inTS1("Arm Away these Keypads${dMap?.def}", sLOCK) + optPrefix(), multiple: true, required: false, submitOnChange: true + } + + section(sectHead("Control Doors:")) { + input "${t}doors_close", "capability.garageDoorControl", title: inTS1("Close these Garage Doors${dMap?.def}", "garage_door") + optPrefix(), multiple: true, required: false, submitOnChange: true + input "${t}doors_open", "capability.garageDoorControl", title: inTS1("Open these Garage Doors${dMap?.def}", "garage_door") + optPrefix(), multiple: true, required: false, submitOnChange: true + if(settings."${t}doors_close" || settings."${t}doors_open") { + input "${t}doors_trig_clear_restore", sBOOL, title: inTS1("Restore original door state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true + } + } + + section(sectHead("Control Siren:")) { + input "${t}sirens", "capability.alarm", title: inTS1("Activate these Sirens${dMap?.def}", "siren") + optPrefix(), multiple: true, required: false, submitOnChange: true + if(settings."${t}sirens") { + input "${t}siren_cmd", sENUM, title: inTS1("Alarm action to take${dMap?.def}", sCOMMAND) + optPrefix(), options: ["both": "Siren & Stobe", "strobe":"Strobe Only", "siren":"Siren Only"], multiple: false, required: true, submitOnChange: true + if(settings."${t}sirens" && settings."${t}siren_cmd") { + input "${t}siren_trig_clear_restore", sBOOL, title: inTS1("Restore HSM state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true + input "${t}siren_time", sNUMBER, title: inTS1("Stop after (x) seconds...", sDELAYT), required: (!(Boolean)settings."${t}siren_trig_clear_restore"), submitOnChange: true + } + + } + } + section(sectHead("Location Actions:")) { + input "${t}mode_run", sENUM, title: inTS1("Set Location Mode${dMap?.def}", sMODE) + optPrefix(), options: getLocationModes(true), multiple: false, required: false, submitOnChange: true + input "${t}alarm_run", sENUM, title: inTS1("Set ${getAlarmSystemName()} mode${dMap?.def}", "alarm_home") + optPrefix(), options: getAlarmSystemStatusActions(), multiple: false, required: false, submitOnChange: true + if(settings."${t}alarm_run") { + input "${t}alarm_run_trig_clear_restore", sBOOL, title: inTS1("Restore HSM state when trigger is cleared?", "reset") + optPrefix(), required: false, defaultValue: false, submitOnChange: true + } + + if(settings.enableWebCoRE) { + input "${t}piston_run", sENUM, title: inTS("Execute a piston${dMap?.def}", webCore_icon()) + optPrefix(), options: webCoRE_list(), multiple: false, required: false, submitOnChange: true + } + } + + if(actTasksConfiguredByType(t)) { + section("Delay before running Tasks: ") { + input "${t}tasks_delay", sNUMBER, title: inTS1("Delay running ${dMap?.delay} in seconds", sDELAYT) + optPrefix(), required: false, submitOnChange: true + } + } + } +} + +Boolean actTasksConfiguredByType(String pType) { + String p = pType + return (String)settings."${p}mode_run" || (String)settings."${p}alarm_run" || settings."${p}switches_off" || settings."${p}switches_on" || + (settings.enableWebCoRE && (String)settings."${p}piston_run") || + settings."${p}lights" || settings."${p}locks_lock" || settings."${p}locks_unlock" || settings."${p}sirens" || settings."${p}doors_close" || settings."${p}doors_open" || + settings."${p}securityKeypads_disarm" || settings."${p}securityKeypads_armHome" || settings."${p}securityKeypads_armAway" +} + +private executeTaskCommands(data) { + String p = data?.type ?: sNULL + if(isTrc())logTrace("executeTaskCommands | type: ${p} actions: ${actTaskDesc(p)}") + + if((String)settings."${p}mode_run") { setLocationMode((String)settings."${p}mode_run") } + if((String)settings."${p}alarm_run") { sendLocationEvent(name: "hsmSetArm", value: (String)settings."${p}alarm_run") } + if(settings.enableWebCoRE && (String)settings."${p}piston_run") { webCoRE_execute((String)settings."${p}piston_run") } + + if(settings."${p}switches_off") { settings."${p}switches_off"*.off() } + if(settings."${p}switches_on") { settings."${p}switches_on"*.on() } + if(settings."${p}locks_lock") { settings."${p}locks_lock"*.lock() } + if(settings."${p}locks_unlock") { settings."${p}locks_unlock"*.unlock() } + if(settings."${p}securityKeypads_disarm") { settings."${p}securityKeypads_disarm"*.disarm() } + if(settings."${p}securityKeypads_armHome") { settings."${p}securityKeypads_armHome"*.armHome() } + if(settings."${p}securityKeypads_armAway") { settings."${p}securityKeypads_armAway"*.armAway() } + if(settings."${p}doors_close") { settings."${p}doors_close"*.close() } + if(settings."${p}doors_open") { settings."${p}doors_open"*.open() } + if(settings."${p}sirens" && (String)settings."${p}siren_cmd") { + String cmd= (String)settings."${p}siren_cmd" + settings."${p}sirens"*."${cmd}"() + if(settings."${p}siren_time") runIn(settings."${p}siren_time", "postTaskCommands", [data:[type: p]]) + } + if(settings."${p}lights") { + if(settings."${p}lights_trig_clear_restore" || settings."${p}lights_restore_delay") { captureLightState((List)settings."${p}lights", p) } + settings."${p}lights"*.on() + if(settings."${p}lights_leveln") { + List hasLvlDevs = settings."${p}lights"*.filter { it.hasCommand("setLevel") } + hasLvlDevs*.setLevel(settings."${p}lights_leveln") + } + if(settings."${p}lights_colort") { + List hasClrDevs = settings."${p}lights"*.filter { it.hasCommand("setColor") } + hasClrDevs*.setColor(getColorName(settings."${p}lights_colort")) + } + if(settings."${p}lights_trig_clear_restore" && !settings."${p}lights_restore_delay") { + restoreLightState((List)settings."${p}lights", p) + } + if(settings."${p}lights_restore_delay") { runIn(settings."${p}lights_restore_delay", "restoreLights", [data:[type: p]]) } + } +} + +String actTaskDesc(String t, Boolean isInpt=false) { + String str = sBLANK + if(actTasksConfiguredByType(t)) { + switch(t) { + case sACT: + str += (isInpt ? sBLANK : lineBr()) + strUnder('Triggered Tasks:') + break + case sACT_START: + str += (isInpt ? sBLANK : lineBr()) + strUnder('Tiered Start Tasks:') + break + case sACT_STOP: + str += (isInpt ? sBLANK : lineBr()) + strUnder('Tiered Stop Tasks:') + break + } + String aStr = "\n ${sBULLET} " + str += settings."${t}switches_on" ? aStr + "Switches On: (${settings."${t}switches_on"?.size()})" : sBLANK + str += settings."${t}switches_off" ? aStr + "Switches Off: (${settings."${t}switches_off"?.size()})" : sBLANK + str += settings."${t}lights" ? aStr + "Lights: (${settings."${t}lights"?.size()})" : sBLANK + str += settings."${t}lights" && settings."${t}lights_leveln" ? "\n - Level: (${settings."${t}lights_leveln"}%)" : sBLANK + str += settings."${t}lights" && settings."${t}lights_colort" ? "\n - Color: (${settings."${t}lights_colort"})" : sBLANK + str += settings."${t}lights" && settings."${t}lights_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK + str += settings."${t}lights" && settings."${t}lights_restore_delay" ? "\n - Restore After: (${settings."${t}lights_restore_delay"} sec.)" : sBLANK + str += settings."${t}locks_unlock" ? aStr + "Locks Unlock: (${settings."${t}locks_unlock"?.size()})" : sBLANK + str += settings."${t}locks_lock" ? aStr + "Locks Lock: (${settings."${t}locks_lock"?.size()})" : sBLANK + str += (settings."${t}locks_lock" || settings."${t}locks_unlock") && settings."${t}locks_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK + str += settings."${t}securityKeypads_disarm" ? aStr + "KeyPads Disarm: (${settings."${t}securityKeypads_disarm".size()})" : sBLANK + str += settings."${t}securityKeypads_armHome" ? aStr + "KeyPads Arm Home: (${settings."${t}securityKeypads_armHome".size()})" : sBLANK + str += settings."${t}securityKeypads_armAway" ? aStr + "KeyPads Arm Away: (${settings."${t}securityKeypads_armAway".size()})" : sBLANK + str += settings."${t}doors_open" ? aStr + "Garages Open: (${settings."${t}doors_open"?.size()})" : sBLANK + str += settings."${t}doors_close" ? aStr + "Garages Close: (${settings."${t}doors_close"?.size()})" : sBLANK + str += (settings."${t}doors_open" || settings."${t}doors_close") && settings."${t}doors_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK + str += settings."${t}sirens" ? aStr + "Sirens On: (${settings."${t}sirens"?.size()})${settings."${t}sirens_delay" ? "(${settings."${t}sirens_delay"} sec)" : sBLANK}" : sBLANK + str += settings."${t}sirens" && settings."${t}siren_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK + + str += (String)settings."${t}mode_run" ? aStr + "Set Mode:\n - ${(String)settings."${t}mode_run"}" : sBLANK + str += (String)settings."${t}alarm_run" ? aStr + "Set Alarm:\n - ${(String)settings."${t}alarm_run"}" : sBLANK + str += settings."${t}alarm_run" && settings."${t}alarm_run_trig_clear_restore" ? "\n - Restore After Trigger Clears: (True)" : sBLANK +// str += settings."${t}routine_run" ? aStr+"Execute Routine:\n - ${getRoutineById(settings."${t}routine_run")?.label}" : sBLANK + str += (settings.enableWebCoRE && (String)settings."${t}piston_run") ? aStr + "Execute webCoRE Piston:\n - " + getPistonById((String)settings."${t}piston_run") : sBLANK + } + return str != sBLANK ? (isInpt ? divSm(spanSmBr(str) + inputFooter(sTTM), sCLR4D9) : str) : (isInpt ? spanSm("On trigger control devices, set mode, set alarm state, execute WebCore Pistons", sCLRGRY) + inactFoot(sTTC) : sNULL) +} + +Boolean isActDevContConfigured() { + return isTierAction() ? (settings.act_tier_start_switches_off || settings.act_tier_start_switches_on || settings.act_tier_stop_switches_off || settings.act_tier_stop_switches_on) : (settings.act_switches_off || settings.act_switches_on) +} + +def actionSimulationSect() { + section(sectHead("Simulate Action")) { + String str = spanSmBldBr("Test this action and see the results.", "black", "testing") + String a=s2SPACE+sBULLET+sSPACE + str += spanSmBldBr("NOTE: ") + spanSmBr("${a}When global text is not defined, this will generate a random event based on your trigger selections.") + str += settings.act_EchoZones ? spanSm("${a}Testing with zones requires you to save the app and come back in to test.") : sBLANK + paragraph spanSm(str, sCLRGRY) + input "actTestRun", sBOOL, title: inTS1("Test this action?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true + if((Boolean)settings.actTestRun) { executeActTest() } + } +} + +Boolean customMsgRequired() { return (!((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI])) } +Boolean customMsgConfigured() { return (settings.notif_use_custom && settings.notif_custom_message) } + +def actNotifPage() { + return dynamicPage(name: "actNotifPage", title: "Action Notifications", install: false, uninstall: false) { + String a = getAppNotifDesc() + String b = spanSmBldBr("Notification Overview:", sCLR4D9) + b += (!a) ? " ${sBULLET} Notifications not enabled" : a + section() { + paragraph spanSm(b, sCLR4D9) + } + + section (sectHead("Notification Devices:")) { + input "notif_devs", "capability.notification", title: inTS1("Send to Notification devices?", "notification"), description: ((!settings?.notif_devs) ? inactFoot(sTTC) : sBLANK), required: false, multiple: true, submitOnChange: true + } + section (sectHead("Alexa Mobile Notification:")) { + paragraph spanSmBldBr("Description:", sCLRGRY) + spanSmBld("This will send a push notification the Alexa Mobile app.", sCLRGRY) + input "notif_alexa_mobile", sBOOL, title: inTS1("Send message to Alexa App?", "notification"), required: false, defaultValue: false, submitOnChange: true + } + + if((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) { + section (sectHead("Message Customization:")) { + Boolean custMsgReq = customMsgRequired() + if(custMsgReq) { + paragraph pTS("The selected action (${(String)settings.actionType}) requires a custom notification message if notifications are enabled.", sNULL, false, sCLRGRY) + if(!settings.notif_use_custom) { settingUpdate("notif_use_custom", sTRUE, sBOOL) } + } else { + paragraph pTS("When using speak or announcement actions custom notification is optional and a notification will be sent with speech text.", sNULL, false, sCLRGRY) + } + input "notif_use_custom", sBOOL, title: inTS1("Send a custom notification...", sQUES), required: false, defaultValue: false, submitOnChange: true + if(settings.notif_use_custom || custMsgReq) { + input "notif_custom_message", sTEXT, title: inTS1("Enter custom message...", sTEXT), required: custMsgReq, submitOnChange: true + } + } + } else { + List sets = settings.findAll { it?.key?.startsWith("notif_") }?.collect { it?.key as String } + sets?.each { String sI-> settingRemove(sI) } + } + + if(isActNotifConfigured()) { + section(sectHead("Notification Restrictions:")) { + String nsd = getNotifSchedDesc() + href "actNotifTimePage", title: inTS1("Quiet Restrictions", "restriction"), description: (nsd ? divSm(spanSmBr(nsd) + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC)) + } + if(!(Boolean)state.notif_message_tested) { + def actDevices = (Boolean)settings.notif_alexa_mobile ? parent?.getDevicesFromList(settings.act_EchoDevices) : [] + def aMsgDev = actDevices?.size() && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null + if(sendNotifMsg("Info", "Action Notification Test Successful. Notifications Enabled for ${app?.getLabel()}", aMsgDev, true)) { state.notif_message_tested = true } + } + } else state.remove("notif_message_tested") + } +} + +def actNotifTimePage() { + return dynamicPage(name:"actNotifTimePage", title: sBLANK, install: false, uninstall: false) { + // String a = getNotifSchedDesc() + // if(a) { + // section() { + // paragraph spanSmBldBr("Restrictions Status:", sCLR4D9) + spanSm(a, sCLR4D9) + // paragraph spanSmBldBr("NOTICE: All selected restrictions must be ${strUnder("INACTIVE")} for notifications to be sent.", sCLRORG) + // paragraph htmlLine() + // } + // } + String pre = "notif" + Boolean timeReq = (settings["${pre}_time_start"] || settings["${pre}_time_stop"]) + section(sectHead("Quiet Start Time:")) { + input "${pre}_time_start_type", sENUM, title: inTS1("Starting at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true + if(settings."${pre}_time_start_type" == sTIME) { + input "${pre}_time_start", sTIME, title: inTS1("Start time", sSTTIME), required: timeReq, submitOnChange: true + } else if(settings."${pre}_time_start_type" in lSUNRISESET) { + input "${pre}_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true + } + } + section(sectHead("Quiet Stop Time:")) { + input "${pre}_time_stop_type", sENUM, title: inTS1("Stopping at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true + if(settings."${pre}_time_stop_type" == sTIME) { + input "${pre}_time_stop", sTIME, title: inTS1("Stop time", sSTTIME), required: timeReq, submitOnChange: true + } else if(settings."${pre}_time_stop_type" in lSUNRISESET) { + input "${pre}_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true + } + } + section(sectHead("Allowed Days:")) { + input "${pre}_days", sENUM, title: inTS1("Only on these week days", "day_calendar"), multiple: true, required: false, options: weekDaysEnum() + } + section(sectHead("Allowed Modes:")) { + input "${pre}_modes", sMODE, title: inTS1("Only in these Modes", sMODE), multiple: true, submitOnChange: true, required: false + } + } +} + +def ssmlInfoSection() { + String ssmlTestUrl = "https://topvoiceapps.com/ssml" + String ssmlDocsUrl = "https://developer.amazon.com/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html" + String ssmlSoundsUrl = "https://developer.amazon.com/docs/custom-skills/ask-soundlibrary.html" + String ssmlSpeechConsUrl = "https://developer.amazon.com/en-US/docs/alexa/custom-skills/speechcon-reference-interjections.html" + section(sectHead("SSML Documentation:"), hideable: true, hidden: true) { + paragraph spanSmBldBr("What is SSML?", sCLR4D9) + spanSm("SSML allows for changes in tone, speed, voice, emphasis. As well as using MP3, and access to the Sound Library", sCLR4D9) + String ttob = inactFoot("Tap to open browser") + href url: ssmlDocsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon SSML Docs", "www"), description: ttob + href url: ssmlSoundsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon Sound Library", "www"), description: ttob + href url: ssmlSpeechConsUrl, style: sEXTNRL, required: false, title: inTS1("Amazon SpeechCons", "www"), description: ttob + href url: ssmlTestUrl, style: sEXTNRL, required: false, title: inTS1("SSML Designer and Tester", "www"), description: ttob + } +} + +private Map> customTxtItems() { + Map> items = [:] + ((List)settings.triggerEvents)?.each { String tr-> + String a=(String)settings."trig_${tr}_txt" + if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].event = a.tokenize(";") } + a = (String)settings."trig_${tr}_after_repeat_txt" + if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].repeat = a.tokenize(";") } + } + return items +} +/* +private Boolean hasRepeatTriggers() { + Map> items = [:] + ((List)settings.triggerEvents)?.each { String tr-> + String a=(String)settings."trig_${tr}_after_repeat_txt" + if(a) { if(!items[tr]) { items[tr] = [:] as Map }; items[tr].repeat = a.tokenize(";") } + } + return (items.size() > 0) +} */ + +Boolean hasUserDefinedTxt() { +/* Boolean fnd = false + ((List)settings.triggerEvents)?.each { String tr -> + if((String)settings."trig_${tr}_txt") { fnd = true } + if((String)settings."trig_${tr}_after_repeat_txt") { fnd = true } + if(fnd) return true + } + return fnd */ + List myL = (List)settings.triggerEvents + String res = sNULL + res = myL?.find { String tr -> + (String)settings."trig_${tr}_txt" || (String)settings."trig_${tr}_after_repeat_txt" + } + return (res!=sNULL) +} + +Boolean executionConfigured() { + Boolean opts = (state.actionExecMap && state.actionExecMap.configured == true) + Boolean devs = (settings.act_EchoDevices || settings.act_EchoZones) + return (opts && devs) +} + +private echoDevicesInputByPerm(String type) { + List echoDevs = parent?.getChildDevicesByCap(type) + Boolean capOk = (type in [sTTS, "announce"]) + Boolean zonesOk = ((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT, sANN, sANNI, sANNT, sANNIT, "voicecmd", "sequence", sWEATH, "calendar", "music", "sounds", "builtin"]) + Map echoZones = (capOk && zonesOk) ? getZones() : [:] + section(sectHead("${echoZones?.size() ? "Zones & " : sBLANK}Alexa Devices:")) { + if(echoZones?.size()) { + if(!settings.act_EchoZones) { paragraph spanSmBldBr("What are Zones?") + spanSm("Zones are used to direct the speech output based on the conditions in the zone (Motion, presence, etc).
    When both Zones and Echo devices are selected, the zone will take priority over the echo device setting.") } + input "act_EchoZones", sENUM, title: inTS1("Zone(s) to Use", "es_groups"), description: spanSm("Select the Zone(s)", sCLRGRY), options: echoZones?.collectEntries { [(it?.key): it?.value?.name?.replace(" (Z)", sBLANK) as String] }, multiple: true, required: (!settings.act_EchoDevices), submitOnChange: true + } + if(settings.act_EchoZones?.size() && echoDevs?.size() && !settings.act_EchoDevices?.size()) { + paragraph spanSm("There may be scenarios when none of your zones are active at the triggered action execution.\nYou have the option to select echo devices to use when no zones are available.", sCLR4D9) + } + if(echoDevs?.size()) { + Boolean devsOpt = (settings.act_EchoZones?.size()) + def eDevsMap = echoDevs?.collectEntries { [(it?.getId()): [label: it?.getLabel(), lsd: (it?.currentWasLastSpokenToDevice?.toString() == sTRUE)]] }?.sort { a,b -> b?.value?.lsd <=> a?.value?.lsd ?: a?.value?.label <=> b?.value?.label } + input "act_EchoDevices", sENUM, title: inTS1("Echo Speaks Devices", "echo_gen1") + (devsOpt ? spanSm(" (Optional Zone Backup)", "violet") : sBLANK), description: spanSm(devsOpt ? "These devices are used when all zones are inactive." : "Select your devices", sCLRGRY), options: eDevsMap?.collectEntries { [(it?.key): "${it?.value?.label}${(it?.value?.lsd == true) ? "\n(Last Spoken To)" : sBLANK}"] }, multiple: true, required: (!settings.act_EchoZones), submitOnChange: true + updDeviceInputs() + } else { paragraph spanSmBld("No devices were found with support for ($type)", sCLRRED) } + } +} + +private actionVolumeInputs(List devices, Boolean showVolOnly=false, Boolean showAlrmVol=false) { + if(showAlrmVol) { + section(sectHead("Volume Options:")) { + input "act_alarm_volume", sNUMBER, title: inTS1("Alarm Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true + } + } else { + if((devices || settings.act_EchoZones) && (String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sANN, sANNI, sWEATH, "calendar", "music", "sounds", "builtin", "playback"]) { + Map volMap = devsSupportVolume(devices) + Integer volMapSiz = volMap?.n?.size() + Integer devSiz = devices?.size() + if(settings.act_EchoZones?.size() > 1) { + section(sectHead("(Per Zone) Volume Options:"), hideable: true) { + input "act_EchoZones_vol_per_zone", sBOOL, title: inTS1("Set Per Zone Volume?", sQUES), defaultValue: false, submitOnChange: true + if((Boolean)settings.act_EchoZones_vol_per_zone) { + Map echoZones = (Map)((Map)getZones() ?: [:]).findAll { it.key in settings.act_EchoZones } + paragraph htmlLine(sCLR4D9) + echoZones.each { String znId, Map znData-> + paragraph spanBld("Zone: ${znData.name}") + input "act_EchoZones_${znId}_volume_change", sNUMBER, title: inTS1("Volume Level (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true + if(!showVolOnly) { + input "act_EchoZones_${znId}_volume_restore", sNUMBER, title: inTS1("Restore Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true + } + } + } + } + } + section(sectHead("Volume Options:")) { + if(devSiz == volMapSiz) { paragraph spanSmBld("None of the selected devices support volume control", sCLRORG) } + else { + if(volMapSiz > 0 && volMapSiz < devSiz) { paragraph spanSmBld("NOTICE:
    Some of the selected devices do not support volume control", sCLRORG) } + input "act_volume_change", sNUMBER, title: inTS1("Volume Level (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true + if(!showVolOnly) { + input "act_volume_restore", sNUMBER, title: inTS1("Restore Volume (0% - 100%)", sSPDKNB) + optPrefix(), description: "(0% - 100%)", range: "0..100", required: false, submitOnChange: true + } + } + } + } + } +} + +private Map getZoneVolumeMap() { + Map znMap = [:] + if((Boolean)settings.act_EchoZones_vol_per_zone) { + Map echoZones = (Map)((Map)getZones() ?: [:]).findAll { it.key in settings.act_EchoZones } + echoZones.each { String zId, Map zData -> + Map aa = [:] + aa.change = settings."act_EchoZones_${zId}_volume_change" ? settings."act_EchoZones_${zId}_volume_change" : null + aa.restore = settings."act_EchoZones_${zId}_volume_restore" ? settings."act_EchoZones_${zId}_volume_restore" : null + if(aa.keySet().size()) { znMap[zId] = aa } + } + } + return znMap +} + +def condTimePage() { + return dynamicPage(name:"condTimePage", title: sBLANK, install: false, uninstall: false) { + Boolean timeReq = (settings["cond_time_start"] || settings["cond_time_stop"]) + section(sectHead("Condition Start Time:")) { + input "cond_time_start_type", sENUM, title: inTS1("Starting at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true + if(settings.cond_time_start_type == sTIME) { + input "cond_time_start", sTIME, title: inTS1("Start time", sSTTIME), required: timeReq, submitOnChange: true + } else if((String)settings.cond_time_start_type in lSUNRISESET) { + input "cond_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true + } + } + section(sectHead("Condition Stop Time:")) { + input "cond_time_stop_type", sENUM, title: inTS1("Stopping at...", sSTTIME), options: [(sTIME):"Time of Day", "sunrise":sCSUNRISE, "sunset":sCSUNSET], required: false , submitOnChange: true + if(settings.cond_time_stop_type == sTIME) { + input "cond_time_stop", sTIME, title: inTS1("Stop time", sSTTIME), required: timeReq, submitOnChange: true + } else if((String)settings.cond_time_stop_type in lSUNRISESET) { + input "cond_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", sSTTIME), required: false, submitOnChange: true + } + } + } +} + +def uninstallPage() { + return dynamicPage(name: "uninstallPage", title: sBLANK, install: false , uninstall: true) { + section(sectHead("Uninstall this Action?")) { paragraph spanSmBld("This will delete this Echo Speaks Action.", sCLRORG) } + } +} + +private void updDeviceInputs() { + // log.trace "updDeviceInputs..." + List aa = (List)settings.act_EchoDevices + List devIds + Boolean updList = false + try { + updList = (aa.size() && aa[0].id != null) + devIds = aa.collect { it?.id?.toString() } + // log.debug "updList(try): ${devIds.unique()}" + } catch (ignored) { + devIds = aa.collect { it?.toString() } + } + if(updList && devIds) { + if(devModeFLD) log.debug "updList: $devIds" + app.updateSetting( "act_EchoDevices", [type: "enum", value: devIds.unique()]) + } + if(devIds) { app.updateSetting( "act_EchoDeviceList", [type: "capability", value: devIds.unique()]) } // this won't take effect until next execution +} + +static Boolean wordInString(String findStr, String fullStr) { + List parts = fullStr?.split(sSPACE)?.collect { String it -> it?.toLowerCase() } + return (findStr in parts) +} + +def installed() { + logInfo("Installed Event Received...") + state.dateInstalled = getDtNow() + if((Boolean)settings.duplicateFlag && !(Boolean)state.dupPendingSetup) { + Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") + state.dupPendingSetup = true + runIn(2, "processDuplication") + if(maybeDup) logInfo("installed found maybe a dup... ${(Boolean)settings.duplicateFlag}") + } else { + if(!(Boolean)state.dupPendingSetup) initialize() + } +} + +@Field static final String dupMSGFLD = "This action is duplicated and has not had configuration completed... Please open action and configure to complete setup..." + +def updated() { + logInfo("Updated Event Received...") + Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") + if(maybeDup) logInfo("updated found maybe a dup... ${(Boolean)settings.duplicateFlag}") + if((Boolean)settings.duplicateFlag) { + if((Boolean)state.dupOpenedByUser) { state.dupPendingSetup = false } + if((Boolean)state.dupPendingSetup){ + logInfo(dupMSGFLD) + return + } + logInfo("removing duplicate status") + settingRemove('duplicateFlag'); settingRemove('duplicateSrcId') + state.remove('dupOpenedByUser'); state.remove('dupPendingSetup'); state.remove('badMode') + } + initialize() +} + +def initialize() { + logInfo("Initialize Event Received...") + if((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup){ + logInfo(dupMSGFLD) + return + } + Boolean multiConds = multipleConditions() + if(!multiConds && (Boolean)settings.cond_require_all) { settingUpdate("cond_require_all", sFALSE, sBOOL) } + + updDeviceInputs() + +// TODO go away at some point +// convert old alarm / hsm settings to new; as attribute 'alarm' belongs to capability.alarm TODO this will go away if we offer alarm attribute + if(settings.trig_alarm && !settings.trig_alarmSystemStatus){ + settingUpdate("trig_alarmSystemStatus", settings.trig_alarm, sENUM) + if(settings.trig_alarm_events && !settings.trig_alarmSystemStatus_events){ + settingUpdate("trig_alarmSystemStatus_events", settings.trig_alarm_events, sENUM) + } + settingRemove("trig_alarm") + settingRemove("trig_alarm_events") + + List tl = [sNUMBER, sBOOL, sENUM, sBOOL, sNUMBER, sNUMBER, sNUMBER, sTEXT, sTEXT] + Integer i = 0 + ["wait", "all", "cmd", "once", "after", "after_repeat", "after_repeat_cnt", "txt", "after_repeat_txt" ]?.each { ei-> + if(settings."trig_alarm_${ei}") settingUpdate("trig_alarmSystemStatus_${ei}".toString(), settings."trig_alarm_${ei}", tl[i]) + settingRemove("trig_alarm_${ei}".toString()) + i++ + } + } + if(settings.cond_alarm && !settings.cond_alarmSystemStatus){ + settingUpdate("cond_alarmSystemStatus", settings.cond_alarm, sENUM) + settingRemove("cond_alarm") + } +// convert carbon to attribute + if(valTrigEvt('carbon')) { + List a = (List)settings.triggerEvents + a.remove(a.indexOf('carbon')) + a.push('carbonMonoxide') + settingUpdate("triggerEvents", a, sENUM) + } + + unsubscribe() + unschedule() + state.isInstalled = true + state.afterEvtCheckWatcherSched = false + atomicState.tierSchedActive = false + updAppLabel() + if(advLogsActive()) { logsEnabled() } + runIn(3, "actionCleanup") + if(!isPaused(true)) { + runIn(7, "subscribeToEvts") + runEvery1Hour("healthCheck") + if(settings.enableWebCoRE){ + remTsVal(sLASTWU) + webCoRE_init() + } + updConfigStatusMap() + resumeTierJobs() + } +} + +private void processDuplication() { + String al = (String)app?.getLabel() + String newLbl = "${al}${al?.contains(" (Dup)") ? "" : " (Dup)"}" + app?.updateLabel(newLbl) + state.dupPendingSetup = true + + String dupSrcId = settings.duplicateSrcId ? (String)settings.duplicateSrcId : sNULL + Map dupData = parent?.getChildDupeData("actions", dupSrcId) + if(devModeFLD) log.debug "dupData: ${dupData}" + if(dupData && dupData.state?.size()) { + dupData.state.each { String k,v-> state[k] = v } + } + + if(dupData && dupData.settings?.size()) { + dupData.settings.each { String k, Map v-> + if((String)v.type in [sENUM, sMODE]) { + settingRemove(k) + settingUpdate(k, (v.value != null ? v.value : null), (String)v.type) + } + } + } + + parent.childAppDuplicationFinished("actions", dupSrcId) + logInfo("Duplicated Action has been created... Please open action and configure to complete setup...") +} + +static 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 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 if(obj instanceof Date) {return "Date"} + else { return "unknown"} +} + +String getActionName() { return (String)settings.appLbl } + +private void updAppLabel() { + String newLbl = "${(String)settings.appLbl} (A${isPaused(true) ? " ${sPAUSESymFLD}" : sBLANK})".replaceAll(/ (Dup)/, sBLANK).replaceAll("\\s"," ") + if((String)settings.appLbl && (String)app?.getLabel() != newLbl) { app?.updateLabel(newLbl) } +} + +private void updConfigStatusMap() { + Map sMap = [:] + sMap.triggers = triggersConfigured() + sMap.conditions = conditionsConfigured() + sMap.actions = executionConfigured() + sMap.tiers = isTierActConfigured() + state.configStatusMap = sMap +} + +Boolean getConfStatusItem(String item) { + switch(item) { + case "tiers": return isTierActConfigured() + case "actions": return executionConfigured() + } + return (state.configStatusMap?.containsKey(item) && state.configStatusMap[item] == true) +} + +@SuppressWarnings('GroovyFallthrough') +private void actionCleanup() { + stateMapMigration() + // State Cleanup + + // keep actionExecMap configStatusMap schedTrigMap + List items = ["afterEvtMap", "afterEvtChkSchedMap", "actTierState", "tierSchedActive", "zoneStatusMap", sLRM] + updMemStoreItem("afterEvtMap", [:]) +// updMemStoreItem("afterEvtChkSchedMap", [:]) + updMemStoreItem("actTierState", [:]) + [sACT, sACT_START, sACT_STOP].each { String it -> items.push((it+sLRM)) } // cleanup light_restore_map + items.each { String si-> if(state.containsKey(si)) { state.remove(si)} } + + //convert old style input to new + [sACT, sACT_START, sACT_STOP].each { String it -> + String a=it+"lights_color" + String a1=it+"lights_colort" + if(settings."${a}") { + settingUpdate(a1, settings."${a}", sTEXT) + settingRemove(a) + } + String b=it+"lights_level" + String b1=it+"lights_leveln" + if(settings."${b}") { + settingUpdate(b1, settings."${b}", sNUMBER) + settingRemove(b) + } + } + + //Cleans up unused action setting items + List setItems = [] + List setIgn = ["act_delay", "act_volume_change", "act_volume_restore", "act_tier_cnt", "act_switches_off", "act_switches_on", "act_piston_run", "act_mode_run", "act_alarm_run"] + if(settings.act_EchoZones) { setIgn.push("act_EchoZones"); Map a = getActiveZones() } // to fill in FLD + else if(settings.act_EchoDevices) { setIgn.push("act_EchoDevices"); setIgn.push("act_EchoDeviceList") } + + tierItemCleanup() + if((String)settings.actionType) { + Boolean isTierAct = isTierAction() + ["act_lights", "act_locks", "act_securityKeypads", "act_doors", "act_sirens"].each { String it -> + ((Map)settings).each { sI -> + String k = (String)sI.key + if(k.startsWith(it)) { + isTierAct ? setItems.push(k) : setIgn.push(k) } + } + } + [sACT_START, sACT_STOP].each { String it -> + ((Map)settings).each { sI -> + String k = (String)sI.key + if(k.startsWith(it)) { + isTierAct ? setIgn.push(k) : setItems.push(k) } + } + } + ((Map)settings).each { si-> + String k = (String)si.key + if(!(k in setIgn) && k.startsWith(sACT) && !k.startsWith("act_${(String)settings.actionType}") && (!isTierAct && k.startsWith(sACT_ITEM))) { setItems.push(k) } + } + } + + // Cleanup Unused Condition settings... + List condKeys = ((Map)settings).findAll { it -> + String k = (String)it.key + k?.startsWith("cond_") && !(k?.tokenize("_")[1] in ['require']) }?.keySet()?.collect { ((String)it)?.tokenize("_")[1] }?.unique() + if(devModeFLD) log.debug("checking settings condition keys $condKeys" ) + if(condKeys?.size()) { + condKeys.each { String ck-> + if(!settings."cond_${ck}") { + if(devModeFLD) log.debug("found setting to cleanup: $ck") + setItems.push("cond_${ck}") + ["cmd", "all", "low", "high", "equal", "avg", "nums"].each { String ei-> + setItems.push("cond_${ck}_${ei}") + } + } + } + } + if(setItems) logDebug("settings to clean setItems: $setItems") + + // Cleanup Unused Trigger Types... + if((List)settings.triggerEvents) { + List trigKeys = ((Map)settings).findAll { it -> + String k = (String)it.key + k?.startsWith(sTRIG) && !(k?.tokenize("_")[1] in (List)settings.triggerEvents) }?.keySet()?.collect { ((String)it)?.tokenize("_")[1] }?.unique() + // log.debug "trigKeys: $trigKeys" + if(trigKeys?.size()) { + trigKeys.each { String tk-> + setItems.push("trig_${tk}") + ["events", "wait", "all", "avg", "cmd", "low", "high", "equal", "once", "after", "txt", "nums", "after_repeat", "after_repeat_cnt", "after_repeat_txt", "Codes"].each { String ei-> + setItems.push("trig_${tk}_${ei}") + } + } + } + } + + // Cleanup Unused Schedule Trigger Items + setItems = setItems + ["trig_scheduled_sunState"] + if(!settings.trig_scheduled_type) { + setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_type", "trig_scheduled_recurrence", "trig_scheduled_time", "trig_scheduled_weekdays", "trig_scheduled_weeks"] + } else { + switch(settings.trig_scheduled_type) { + case sONETIME: + case sCSUNRISE: + case sCSUNSET: + setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_recurrence", "trig_scheduled_weekdays", "trig_scheduled_weeks"] + if((String)settings.trig_scheduled_type in [sCSUNSET, sCSUNRISE]) { setItems.push("trig_scheduled_time") } + else setItems = setItems + ["trig_scheduled_sunState_offset"] + break + case sRECURRING: + switch(settings.trig_scheduled_recurrence) { + case "Daily": + setItems = setItems + ["trig_scheduled_daynums", "trig_scheduled_months", "trig_scheduled_weeks"] + break + case "Weekly": + setItems.push("trig_scheduled_daynums") + break + case "Monthly": + setItems.push("trig_scheduled_weekdays") + if(settings.trig_scheduled_daynums && settings.trig_scheduled_weeks) { setItems.push("trig_scheduled_weeks") } + break + } + break + } + } + + ((Map)settings).each { si-> + String k = (String)si.key + ["broadcast", "musicTest", "announce", "sequence", "speechTest"].each { String it -> if (k?.startsWith(it)) setItems.push(k) } + // if(k?.startsWith("broadcast") || k?.startsWith("musicTest") || k?.startsWith("announce") || k?.startsWith("sequence") || k?.startsWith("speechTest")) { setItems.push(k) } } + } + if(state.webCoRE) { state.remove("webCoRE") } + if(!settings.enableWebCoRE) { setItems.push("webCorePistons"); setItems.push("act_piston_run") } + setItems = setItems + ["tuneinSearchQuery", "usePush", "smsNumbers", "pushoverSound", "pushoverDevices", "pushoverEnabled", "pushoverPriority", "alexaMobileMsg", "appDebug"] + // Performs the Setting Removal + // log.debug "setItems: $setItems" + setItems.unique()?.each { String sI-> if(settings.containsKey(sI)) { settingRemove(sI) } } +} + +Boolean isPaused(Boolean chkAll = false) { return (Boolean)settings.actionPause && (chkAll ? !(Boolean)state.dupPendingSetup : true) } + +public void triggerInitialize() { runIn(3, "initialize") } + +private Boolean valTrigEvt(String key) { return (key in (List)settings.triggerEvents) } + +public void updatePauseState(Boolean pause) { + if((Boolean)settings.actionPause != pause) { + logDebug("Received Request to Update Pause State to (${pause})") + settingUpdate("actionPause", "${pause}", sBOOL) + runIn(4, "updated") + } +} + +private healthCheck() { + if(isTrc())logTrace("healthCheck") + if(advLogsActive()) { logsDisable() } + if(settings.enableWebCoRE) webCoRE_poll() +} + +String cronBuilder() { + /**** + Cron Expression Format: ( ) + + * (all) – it is used to specify that event should happen for every time unit. For example, “*” in the field – means “for every minute” + ? (any) – it is utilized in the and fields to denote the arbitrary value – neglect the field value. For example, if we want to fire a script at “5th of every month” irrespective of what the day of the week falls on that date, then we specify a “?” in the field + – (range) – it is used to determine the value range. For example, “10-11” in field means “10th and 11th hours” + , (values) – it is used to specify multiple values. For example, “MON, WED, FRI” in field means on the days “Monday, Wednesday, and Friday” + / (increments) – it is used to specify the incremental values. For example, a “5/15” in the field, means at “5, 20, 35 and 50 minutes of an hour” + L (last) – it has different meanings when used in various fields. For example, if it's applied in the field, then it means last day of the month, i.e. “31st for January” and so on as per the calendar month. It can be used with an offset value, like “L-3“, which denotes the “third to last day of the calendar month”. In the , it specifies the “last day of a week”. It can also be used with another value in , like “6L“, which denotes the “last Friday” + W (weekday) – it is used to specify the weekday (Monday to Friday) nearest to a given day of the month. For example, if we specify “10W” in the field, then it means the “weekday near to 10th of that month”. So if “10th” is a Saturday, then the job will be triggered on “9th”, and if “10th” is a Sunday, then it will trigger on “11th”. If you specify “1W” in the and if “1st” is Saturday, then the job will be triggered on “3rd” which is Monday, and it will not jump back to the previous month + # – it is used to specify the “N-th” occurrence of a weekday of the month, for example, “3rd Friday of the month” can be indicated as “6#3“ + + At 12:00 pm (noon) every day during the year 2017: (0 0 12 * * ? 2017) + + Every 5 minutes starting at 1 pm and ending on 1:55 pm and then starting at 6 pm and ending at 6:55 pm, every day: (0 0/5 13,18 * * ?) + + Every minute starting at 1 pm and ending on 1:05 pm, every day: (0 0-5 13 * * ?) + + At 1:15 pm and 1:45 pm every Tuesday in the month of June: (0 15,45 13 ? 6 Tue) + + At 9:30 am every Monday, Tuesday, Wednesday, Thursday, and Friday: (0 30 9 ? * MON-FRI) + + At 9:30 am on 15th day of every month: (0 30 9 15 * ?) + + At 6 pm on the last day of every month: (0 0 18 L * ?) + + At 6 pm on the 3rd to last day of every month: (0 0 18 L-3 * ?) + + At 10:30 am on the last Thursday of every month: (0 30 10 ? * 5L) + + At 6 pm on the last Friday of every month during the years 2015, 2016 and 2017: (0 0 18 ? * 6L 2015-2017) + + At 10 am on the third Monday of every month: (0 0 10 ? * 2#3) + + At 12 am midnight on every day for five days starting on the 10th day of the month: (0 0 0 10/5 * ?) + ****/ + String cron = sNULL + //List schedTypes = [sONETIME, sRECURRING, sCSUNRISE, sCSUNSET] + String schedType = (String)settings.trig_scheduled_type + Boolean recur = (schedType == sRECURRING) + def time = settings.trig_scheduled_time ?: null + List dayNums = recur && settings.trig_scheduled_daynums ? settings.trig_scheduled_daynums?.collect { it as Integer }?.sort() : null +// List weekNums = recur && settings.trig_scheduled_weeks ? settings.trig_scheduled_weeks?.collect { it as Integer }?.sort() : null + List monthNums = recur && settings.trig_scheduled_months ? settings.trig_scheduled_months?.collect { it as Integer }?.sort() : null + if(time) { + String z = "0" + String hour = fmtTime(time, "HH") ?: z + String minute = fmtTime(time, "mm") ?: z + //String second = fmtTime(time, "ss") ?: z + String second = z + String daysOfWeek = settings.trig_scheduled_weekdays ? ((List)settings.trig_scheduled_weekdays)?.join(",") : sNULL + String daysOfMonth = dayNums?.size() ? (dayNums?.size() > 1 ? "${dayNums?.first()}-${dayNums?.last()}" : dayNums[0]) : sNULL + //String weeks = (weekNums && !dayNums) ? weekNums?.join(",") : sNULL + String months = monthNums ? monthNums?.sort()?.join(",") : sNULL + String year = sNULL + // log.debug "hour: ${hour} | m: ${minute} | s: ${second} | daysOfWeek: ${daysOfWeek} | daysOfMonth: ${daysOfMonth} | weeks: ${weeks} | months: ${months}" + if(hour || minute || second) { + List cItems = [] + cItems.push(second ?: "*") + cItems.push(minute ?: z) + cItems.push(hour ?: z) + cItems.push(daysOfMonth ?: (!daysOfWeek ? "*" : "?")) + cItems.push(months ?: "*") + cItems.push(daysOfWeek ? daysOfWeek?.replaceAll("\"", sBLANK) : "?") + if(year) { cItems.push(" ${year}") } + cron = cItems.join(" ") + } + } + logDebug("cronBuilder | Cron: ${cron}") + return cron +} + +Boolean schedulesConfigured() { + if((String)settings.trig_scheduled_type in [sCSUNRISE, sCSUNSET]) { return true } + else if((String)settings.trig_scheduled_type == sONETIME && settings.trig_scheduled_time) { return true } + else if((String)settings.trig_scheduled_type == sRECURRING && settings.trig_scheduled_time && settings.trig_scheduled_recurrence) { + if((String)settings.trig_scheduled_recurrence == "Daily") { return true } + else if ((String)settings.trig_scheduled_recurrence == "Weekly" && (List)settings.trig_scheduled_weekdays) { return true } + else if ((String)settings.trig_scheduled_recurrence == "Monthly" && (settings.trig_scheduled_daynums || settings.trig_scheduled_weeks)) { return true } + } + return false +} + +void scheduleSunriseSet() { + if(isPaused(true)) { logWarn("Action is PAUSED... No Events will be subscribed to or scheduled....", true); return } + Map sun = getSunriseAndSunset() + Long ltim = (String)settings.trig_scheduled_type in [sCSUNRISE] ? sun.sunrise.time : sun.sunset.time + Long offset = (settings.trig_scheduled_sunState_offset ?: 0L) * 60000L // minutes + Long t = now() + Long n = ltim+offset + if(t > n) { logDebug("Not scheduling sunrise, sunset - already past today"); return } + Long secs = Math.round((n - t)/1000.0D) + 1L + runIn(secs, "scheduleTrigEvt") + Date tt = new Date(n) + logDebug("Setting Schedule for ${epochToTime(tt)} in $secs's") +} + +void subscribeToEvts() { + state.handleGuardEvents = false + if(minVersionFailed()) { logError("CODE UPDATE required to RESUME operation. No events will be monitored.", true); return } + if(isPaused(true)) { logWarn("Action is PAUSED... No Events will be subscribed to or scheduled....", true); return } + ((List)settings.triggerEvents)?.each { String te-> + if(te == sSCHED || settings."trig_${te}") { + switch (te) { + case sSCHED: + // Scheduled Trigger Events + if (schedulesConfigured()) { + if((String)settings.trig_scheduled_type in [sCSUNRISE, sCSUNSET]) { + scheduleSunriseSet() + schedule('29 0 0 1/1 * ? * ', "scheduleSunriseSet") // run at 00:00:29 every day + } + else if((String)settings.trig_scheduled_type in [sONETIME, sRECURRING] && settings.trig_scheduled_time) { schedule(cronBuilder(), "scheduleTrigEvt") } + } + break + case "guard": + // Alexa Guard Status Events + if(settings.trig_guard) state.handleGuardEvents = true + break + case sALRMSYSST: + // Location Alarm Events + subscribe(location, "hsmStatus", alarmEvtHandler) + if("alerts" in (List)settings.trig_alarmSystemStatus) { subscribe(location, "hsmAlert", alarmEvtHandler) } // Only on Hubitat + break + case sMODE: + // Location Mode Events + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } + subscribe(location, sMODE, modeEvtHandler) + break + case sPISTNEXEC: + break + case sTHERMTEMP: + if (settings."trig_${te}_cmd") subscribe(settings."trig_${te}", sTEMP, getThermEvtHandlerName(te)) + break + default: + // Handles Remaining Device Events + subscribe(settings."trig_${te}", te, getDevEvtHandlerName(te)) + break + } + } + } +} + +private getDevEvtHandlerName(String type) { + return (type && (Integer)settings."trig_${type}_after"!=null) ? "devAfterEvtHandler" : "deviceEvtHandler" +} + +private getThermEvtHandlerName(String type) { + return (type && (Integer)settings."trig_${type}_after"!=null) ? "devAfterThermEvtHandler" : "thermostatEvtHandler" +} + +private executeActTest() { + logTrace("executeActTest STARTING") + try { + if((String)settings.actionType in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT, sANN, sANNI, sANNT, sANNIT, sWEATH, "builtin", "calendar"]) { + Map evt = getRandomTrigEvt() + if(!evt) logWarn("no random event found") + else { + String te = (String)evt.name + + switch (te) { + case sSCHED: + // Scheduled Trigger Events + scheduleTrigEvt([date: new Date(), name: "test", value: "Stest", displayName: "Schedule Test"]) + break + case "webCoRE": // sPISTNEXEC: + webCoRE_handler(evt) + break + case sTHERMTEMP: + String hand = getThermEvtHandlerName(te) + evt.name = sTEMP + "${hand}"(evt) + break + case "guard": + // Alexa Guard Status Events + guardEventHandler((String)evt.value) + break + case sALRMSYSST: + // Location Alarm Events + alarmEvtHandler(evt) + // two cases hsmStatus (above) and hsmAlert TODO + //if("alerts" in (List)settings.trig_alarmSystemStatus) { subscribe(location, "hsmAlert", alarmEvtHandler) } // Only on Hubitat + break + case sMODE: + // Location Mode Events + modeEvtHandler(evt) + break + default: + String hand = getDevEvtHandlerName(te) + // Handles Remaining Device Events + "${hand}"(evt) + break + } + } + } else logWarn("TEST of Action type ${settings.actionType} SKIPPED") + } catch (ignored) {} + settingUpdate("actTestRun", sFALSE, sBOOL) + logTrace("executeActTest ENDING") +} + +Map getRandomTrigEvt() { + String trig = (String)getRandomItem((List)settings.triggerEvents) + List trigItems = settings."trig_${trig}" ?: null + def randItem = trigItems?.size() ? getRandomItem(trigItems) : null + def trigItem = randItem ? (randItem instanceof String ? [displayName: null, id: null] : + (trigItems?.size() ? trigItems?.find { it?.id?.toString() == randItem?.id?.toString() } : [displayName: null, id: null])) : null + if(devModeFLD) log.debug("trig: ${trig} | trigItem: ${trigItem} | ${trigItem?.displayName} | ${trigItem?.id} | trigItems: ${trigItems}") + Map evt = [ date: new Date(), name: trig ] + if( !(trig in [sSCHED]) ) { + Boolean isC = (getTemperatureScale()=="C") + Map attVal = [ + (sSWITCH): getRandomItem(lONOFF), + door: getRandomItem(lOPNCLS+["opening", "closing"]), + (sCONTACT): getRandomItem(lOPNCLS), + acceleration: getRandomItem(lACTINACT), + (sLOCK): getRandomItem(lLOCKUNL +["unlocked with timeout"]), + securityKeypad: getRandomItem(lSEC), + (sWATER): getRandomItem(lWETDRY), + presence: getRandomItem(lPRES), + (sMOTION): getRandomItem(lACTINACT), + (sVALVE): getRandomItem(lOPNCLS), + windowShade: getRandomItem(lOPNCLS+["opening", "closing"]), + (sPUSHED): getRandomItem([sPUSHED]), + (sRELEASED): getRandomItem([sRELEASED]), + (sHELD): getRandomItem([sHELD]), + (sDBLTAP): getRandomItem([sDBLTAP]), + (sSMOKE): getRandomItem(lDETECTCLR), + carbonMonoxide: getRandomItem(lDETECTCLR), + (sTEMP): isC ? getRandomItem(-1..29) : getRandomItem(30..80), + illuminance: getRandomItem(1..100), + (sHUMID): getRandomItem(1..100), + (sLEVEL): getRandomItem(1..100), + (sBATT): getRandomItem(1..100), + (sPOWER): getRandomItem(100..3000), + (sMODE): getRandomItem((List)location?.modes), + (sALRMSYSST): getRandomItem(getAlarmTrigOpts()?.collect {(String)it.key}), + guard: getRandomItem(["ARMED_AWAY", "ARMED_STAY"]), + (sTHERMTEMP): isC ? getRandomItem(10..33) : getRandomItem(50..90), + (sCOOLSP): isC ? getRandomItem(10..33) : getRandomItem(50..90), + (sHEATSP): isC ? getRandomItem(10..33) : getRandomItem(50..90), + (sTHERMMODE): getRandomItem(getThermModeOpts()), + (sTHERMFM): getRandomItem(getThermFanOpts()), + (sTHERMOS): getRandomItem(getThermOperStOpts()), + ] + if(settings.enableWebCoRE && trig == sPISTNEXEC) { + attVal.webCoRE = sPISTNEXEC + trig = 'webCoRE' + trigItem = [:] + trigItem.displayName = 'webcore piston executed' + String id = getRandomItem(getLocationPistons()) + evt.name=trig + evt = evt + [ jsonData: [id: id, name: getPistonById(id) ] ] + } + if(attVal.containsKey(trig)) { + evt = evt + [ displayName: trigItem?.displayName ?: sBLANK, value: attVal[trig], type: "digital", device: [id: trigItem?.id?.toString() ?: null ] ] + } else evt = null + } else { evt.name=sSCHED } + + if(devModeFLD) log.debug "getRandomTrigEvt | trig: ${trig} | Evt: ${evt}" + return evt +} + +@Field volatile static Map zoneStatusMapFLD = [:] + +def zoneStateHandler(evt) { + // TODO This is here as placeholder to prevent flooding the logs with errors after upgrading to v4.0 +} + +def zoneRemovedHandler(evt) { + // TODO This is here as placeholder to prevent flooding the logs with errors after upgrading to v4.0 +} + +public updZones(Map zoneMap) { + String myId=app.getId() + zoneStatusMapFLD[myId] = zoneMap + zoneStatusMapFLD = zoneStatusMapFLD +} + +public Map getZones(Boolean noCache = false) { + String myId=app.getId() + Map a = zoneStatusMapFLD[myId] + if(noCache || !a) { + a = parent.getZones(true) + } + String i = 'initialized' + if(a.containsKey(i))a.remove(i) + return a +} +/* +private Map getZoneState(Integer znId) { + Map zones = getZones() + if(zones) { + return zones[znId] + } + return null +} */ + +public Map getActiveZones() { + Map t0 = getZones() + Map zones = t0 ?: [:] + return zones.size() ? zones.findAll { it?.value?.active == true && !it?.value?.paused } : [:] +} + +public List getActiveZoneNames() { + Map t0 = getActiveZones() + Map zones = t0 ?: [:] + return zones.size() ? zones.collect { (String)it?.value?.name } : [] +} + +/*********************************************************************************************************** + EVENT HANDLER FUNCTIONS +************************************************************************************************************/ + +def scheduleTrigEvt(ievt=null) { + Map evt=ievt + if(!evt) { + Date adate= new Date() + String dt= dateTimeFmt(adate, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") + evt= [name: sSCHED, displayName: "Scheduled Trigger", value: fmtTime(dt), date: adate, device: [id: null]] + } + Long evtDelay = now() - ((Date)evt.date).getTime() + if(isTrc())logTrace("${(String)evt.name} Event | Device: ${(String)evt.displayName} | Value: (${strCapitalize(evt.value)}) with a delay of ${evtDelay}ms") + if (!schedulesConfigured()) { return } + String schedType = (String)settings.trig_scheduled_type + Boolean recur = schedType == sRECURRING + Map dateMap = getDateMap() + // log.debug "dateMap: $dateMap" + String srecur = recur ? (String)settings.trig_scheduled_recurrence : sNULL + List days = recur ? (List)settings.trig_scheduled_weekdays : null + List daynums = recur ? (List)settings.trig_scheduled_daynums : null + List weeks = recur ? (List)settings.trig_scheduled_weeks : null + List months = recur ? (List)settings.trig_scheduled_months : null + + getTheLock(sHMLF, "scheduleTrigEvt") + + Map sTrigMap = (Map)getMemStoreItem("schedTrigMap", [:]) + if(!sTrigMap) sTrigMap = (Map)state.schedTrigMap ?: [:] + + Boolean dChk = (sTrigMap?.lastRun?.day != dateMap.day) || (sTrigMap?.lastRun?.year != dateMap.year) || (sTrigMap?.lastRun?.month != dateMap.month) + + Boolean dOk = (srecur in ["Daily"]) ? (sTrigMap?.lastRun?.dayName != dateMap.dayName) : true + Boolean wdOk = (days && srecur in ["Daily", "Weekly"]) ? (dateMap.dayNameShort in days) : true + Boolean mdOk = daynums ? (dateMap.day in daynums) : true + Boolean wOk = (weeks && srecur in ["Weekly"]) ? (dateMap.week in weeks && sTrigMap?.lastRun?.week != dateMap.week) : true + Boolean mOk = (months && srecur in ["Weekly", "Monthly"]) ? (dateMap.month in months && sTrigMap?.lastRun?.month != dateMap.month) : true + Boolean ok = (dOk && wdOk && mdOk && wOk && mOk && dChk) + if(ok) { + sTrigMap.lastRun = dateMap + updMemStoreItem("schedTrigMap", sTrigMap) + state.schedTrigMap = sTrigMap + } + + releaseTheLock(sHMLF) + eventCompletion(evt, ok, false, null, "scheduleTrigEvt", evt?.value, (String)evt?.displayName) + if(!ok) logDebug("scheduleTrigEvt | SKIPPING | Comparison to lastrun: dateChange: $dChk | dayOk: $dOk | dayOfWeekOk: $wdOk | dayOfMonthOk: $mdOk | weekOk: $wOk | monthOk: $mOk") +} + +@SuppressWarnings('GroovyFallthrough') +def alarmEvtHandler(evt) { + Long evtDelay = now() - (Long)((Date)evt.date).getTime() + String eN = (String)evt.name + def eV = evt.value + if(isTrc())logTrace("${eN} Event | Device: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms") + String inT = sTRIG+sALRMSYSST + List lT = (List)settings."${inT}" + List lE = (List)settings."${inT}_events" + Boolean ok2Run = !!(lT) + if(ok2Run) { + switch(eN) { + case "hsmStatus": + case sALRMSYSST: + if(!(eV in lT)) ok2Run = false + break + case "hsmAlert": + if (!("alerts" in lT && eV in lE)) ok2Run = false + break + default: + ok2Run = false + } + } + Boolean dco = !!(Boolean)settings."${inT}_once" + Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null + eventCompletion(evt, ok2Run, dco, dcw, "alarmEvtHandler(${eN})", eV, (String)evt?.displayName) + if(!ok2Run) logDebug("alarmEvtHandler | Skipping event ${eN} value: ${eV}, did not match ${lT} ${lE}") +} + +public guardEventHandler(String guardState) { + Map evt = [name: "guard", displayName: "Alexa Guard", value: guardState, date: new Date(), device: [id: null]] + if(isTrc())logTrace("${evt.name} Event | Device: ${evt.displayName} | Value: (${strCapitalize(evt.value)})") + Boolean ok= ((Boolean)state.handleGuardEvents && settings.trig_guard && (sANY in (List)settings.trig_guard || guardState in (List)settings.trig_guard)) + eventCompletion(evt, ok, false, null, "guardEventHandler", guardState, (String)evt.displayName) + if(!ok) logDebug("guardEventHandler | Skipping event guard value: ${guardState}, did not match ${state.handleGuardEvents} ${settings.trig_guard}") +} + +void eventCompletion(evt, Boolean ok2Run, Boolean dco, Integer dcw, String meth, evtVal, + String evtDis, Boolean aftRepEvt=false, Boolean allDevsResp=false) { + Boolean evtWaitOk = ((dco || dcw!=null) ? evtWaitRestrictionOk([date: evt?.date, device: evt?.device, value: evtVal, name: evt?.name, displayName: evtDis], dco, dcw, aftRepEvt) : true) + Boolean ok = evtWaitOk && ok2Run + if(isDbg()) logDebug(meth+" | execOk: ${ok} | ok2Run :${ok2Run} | evtWaitOk: ${evtWaitOk}") + if(getConfStatusItem("tiers")) { + processTierTrigEvt(evt, ok) + } else { if(ok) executeAction(evt, meth, allDevsResp, aftRepEvt) } + if (!ok) logTrace(meth+" | Skipping event ${evtDis} ${evt?.name} value: ${evtVal} | ok2Run :${ok2Run} | evtWaitOk: ${evtWaitOk}") +} + +def webcoreEvtHandler(evt) { + String eN = (String)evt.name + def eV = evt.value + String disN = evt.jsonData?.name + String pId = evt.jsonData?.id + if(isTrc())logTrace("${eN.toUpperCase()} Event | Piston: ${disN} | pistonId: ${pId} | with a delay of ${now() - (Long)((Date)evt.date).getTime()}ms") + String inT = sTRIG+sPISTNEXEC + List lT = (List)settings."${inT}" + Boolean ok = (pId in lT) + Boolean dco = !!(Boolean)settings."${inT}_once" + Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null + eventCompletion(evt, ok, dco, dcw, "webcoreEvtHandler", disN, disN) + if(!ok) logTrace("webcoreEvtHandler | Skipping event ${eN} value: ${eV}, ${pId} did not match ${lT}") +} + +def modeEvtHandler(evt) { + String eN = (String)evt.name + def eV = evt.value + if(isTrc())logTrace("${eN.toUpperCase()} Event | Mode: (${strCapitalize(eV)}) with a delay of ${now() - (Long)((Date)evt.date).getTime()}ms") + String inT = sTRIG+sMODE + List lT = (List)settings."${inT}" + Boolean ok = (eV in lT) + Boolean dco = !!(Boolean)settings."${inT}_once" + Integer dcw = (Integer)settings."${inT}_wait"!=null ? (Integer)settings."${inT}_wait" : null + eventCompletion(evt, ok, dco, dcw, "modeEvtHandler", eV, (String)evt?.displayName) + if(!ok) logTrace("modeEvtHandler | Skipping event ${eN} value: ${eV}, did not match ${lT}") +} + +void devAfterThermEvtHandler(evt) { + evt.name = (String)evt.name == sTEMP ? sTHERMTEMP : (String)evt.name + devAfterEvtHandler(evt) +} + +void devAfterEvtHandler(evt) { + Long evtT = (Long)((Date)evt.date).getTime() + Long now = now() + Long evtDelay = now - evtT + String eN = (String)evt?.name + def eV = evt?.value + String dc = settings."trig_${eN}_cmd" ?: null + Integer dcaf = (Integer)settings."trig_${eN}_after"!=null ? (Integer)settings."trig_${eN}_after" : 0 + Integer dcafr = (Integer)settings."trig_${eN}_after_repeat" ?: 0 + Integer dcafrc = (Integer)settings."trig_${eN}_after_repeat_cnt" ?: 0 + String eid = "${evt?.device?.id}_${eN}" + Boolean okpt1 = (dc && dcaf>=0) + Boolean okpt2 = okpt1 + if(!(eN in lDATTNUM)) { + okpt2 = (okpt1 && eV == dc) + } + String msg = "Device Event After | " + if(isTrc())logTrace(msg+"${eN?.toUpperCase()} | Name: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms | SchedCheck: (${okpt1}, ${okpt2}) dcaf: $dcaf, dcafr: $dcafr, dcafrc: $dcafrc") + + Boolean rem = false + + getTheLock(sHMLF, "scheduleTrigEvt") + + String mK = "afterEvtMap" + Map aEvtMap = (Map)getMemStoreItem(mK, [:]) + if(!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] + + if(aEvtMap.containsKey(eid) && !okpt2) { + aEvtMap.remove(eid) + rem = true + } + if(okpt2) { aEvtMap[eid] = [ + dt: formatDt((Date)evt.date), + device: [id: evt?.device?.id as String], + displayName: evt?.displayName, + name: eN, + value: eV, + type: evt?.type, + data: evt?.data, + triggerState: dc, // desired comparison value for non-numbers; or comparison type for number + nextT: now + (dcaf*1000L), + wait: dcaf, + //timeLeft: dcaf, + isRepeat: false, + repeatWait: dcafr ?: null, + repeatCnt: 0, + repeatCntMax: dcafrc ] + } + + updMemStoreItem(mK, aEvtMap) + state[mK] = aEvtMap + Integer sz = aEvtMap.size() + + releaseTheLock(sHMLF) + + if(rem) logDebug(msg+"Removing ${evt?.displayName} from AfterEvtCheckMap | Reason: (${eN?.toUpperCase()}) no longer has the state of (${dc}) | Remaining Items: (${sz})") + + if(okpt2) { + String msg1 = msg+"queued event | " + Boolean doRun=true + if (dcaf == 0) { + logTrace(msg1+"Running afterEvent") + afterEvtCheckHandler() + doRun = false + } + if(doRun) { + Integer ssec = dcaf >= 0 ? dcaf : 2 + runIn(ssec, "afterEvtCheckHandler") + logTrace(msg1+"Scheduled afterEvent in ${ssec} seconds") + } + // TODO remove + if(!(Boolean)state.afterEvtCheckWatcherSched) { + state.afterEvtCheckWatcherSched = true + logTrace(msg+"Scheduling afterEvtCheckWatcher...") + runEvery5Minutes("afterEvtCheckWatcher") + } + } else { + logDebug(msg+" did not queue | Passing to deviceEvtHandler") + deviceEvtHandler(evt, true, false) + } +} + +void afterEvtCheckHandler() { + unschedule("afterEvtCheckHandler") + updTsVal("lastAfterEvtCheck") + String meth = "afterEvtCheckHandler" + String msg = meth+" | " + logTrace(msg) + + getTheLock(sHMLF, meth) + Boolean hasLock = true + + String mK = "afterEvtMap" + Map aEvtMap = (Map)getMemStoreItem(mK, [:]) + if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] + + Long mnow = (Long)now() + 750L // anything in next 750ms runs now + Map newMap = aEvtMap.findAll { it -> (Long)it?.value?.nextT < mnow } + List sortList = newMap.collect { (Long)it?.value?.nextT }?.sort() + + sortList?.each { Long lit -> + if (!hasLock) { + getTheLock(sHMLF, meth) + hasLock = true + + aEvtMap = (Map)getMemStoreItem(mK, [:]) + if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] + } + def nextItem = aEvtMap.find { eM -> (Long)eM?.value?.nextT == lit } + Map nextVal = (Map)nextItem?.value ?: null + String eN = (String)nextVal.name + String edisplayN = (String)nextVal.displayName + String edId = (String)nextVal.device.id + String nextId = (edId && eN) ? "${edId}_${eN}" : sNULL + Boolean remEvt + Boolean runEvt = false + String msg1 = "Evaluating ${edisplayN} | (${eN?.capitalize()}) | Original Event Time: ${nextVal.dt} | " + String msg2 + if (nextVal && nextId && aEvtMap[nextId]) { + def devs = settings."trig_${eN}" ?: null + + def newVal = null // could be number or string + if (devs && eN) { + def dev = devs.find { it?.id?.toString() == edId } + String en = eN == sTHERMTEMP ? sTEMP : eN + if (dev) newVal = dev?.currentValue(en) + if (newVal != nextVal.value) msg1 = msg1 + "value changed old: ${nextVal.value} new: ${newVal} | " + } + Long origD = parseDate(nextVal.dt).getTime() + Map evt = [ + date : new Date(), + device : [id: edId], + displayName: edisplayN, + name : eN, + value : newVal,// ?: nextVal.value, + type : nextVal.type, + data : nextVal.data, + totalDur : (now() - origD)/1000 + ] + + Boolean skipEvt = true + if(eN in lDATTNUM) { + String dc = settings."trig_${eN}_cmd" // desired comparison for numbers + Boolean dca = !!(Boolean)settings."trig_${eN}_all" + Boolean dcavg = (!dca && !!(Boolean)settings."trig_${eN}_avg") + Double dcl = settings."trig_${eN}_low" + Double dch = settings."trig_${eN}_high" + Double dce = settings."trig_${eN}_equal" + Map valChk = deviceEvtProcNumValue(evt, devs, dc, dcl, dch, dce, dca, dcavg) + skipEvt = !(Boolean)valChk.evtOk + //evtAd = valChk.evtAd + } else { + if (nextVal.triggerState && edId && eN && devs) { + skipEvt = !devAttValEqual(devs, edId, eN, nextVal.triggerState) + } + } + + Boolean hasRepeat = ((Integer)settings."trig_${eN}_after_repeat" != null) + Boolean isRepeat = (Boolean)nextVal.isRepeat ?: false + + if (!skipEvt) { + runEvt = true + + if(hasRepeat) { + aEvtMap[nextId].nextT = now() + ((Integer)nextVal.repeatWait * 1000L) + updMemStoreItem(mK, aEvtMap) + state[mK] = aEvtMap + } + + if (isRepeat && hasRepeat) { + Integer repeatCnt = ((Integer)nextVal.repeatCnt >= 0) ? (Integer)nextVal.repeatCnt + 1 : 1 + Integer repeatCntMax = (Integer)nextVal.repeatCntMax ?: null + remEvt = (repeatCntMax && (repeatCnt == repeatCntMax)) + aEvtMap[nextId].repeatCnt = repeatCnt + updMemStoreItem(mK, aEvtMap) + state[mK] = aEvtMap + msg2 = "Issuing Repeat Event | RepeatWait: ${nextVal?.repeatWait} | isRepeat: ${isRepeat} | RepeatCnt: ${repeatCnt} | RepeatCntMax: ${repeatCntMax} " + if (remEvt) msg2 = "has repeated ${repeatCntMax} times | " + msg2 + + } else { // this is after + aEvtMap[nextId].isRepeat = true + updMemStoreItem(mK, aEvtMap) + state[mK] = aEvtMap + remEvt = !hasRepeat + msg2 = "Wait Threshold (${nextVal.wait} sec) Reached for ${edisplayN} (${eN?.capitalize()}) | Issuing held event | TriggerState: (${nextVal.triggerState}) | hasRepeat: ${hasRepeat} " + } + } else { + remEvt = true + msg2 = "state is no longer ${nextVal.triggerState} | Skipping Action... " + } + if (remEvt) { + aEvtMap.remove(nextId) + updMemStoreItem(mK, aEvtMap) + state[mK] = aEvtMap + msg2 = msg2 + "Removed event" + } + + releaseTheLock(sHMLF) + hasLock = false + + if(isDbg())logDebug(msg + msg1 + msg2) + if(runEvt) + deviceEvtHandler(evt, true, isRepeat) + } + } + + if (!hasLock) { + getTheLock(sHMLF, meth) + hasLock = true + + aEvtMap = (Map)getMemStoreItem(mK, [:]) + if (!aEvtMap) aEvtMap = (Map)state[mK] ?: [:] + } + Integer sz = aEvtMap.size() + if (sz > 0) { + //newMap = aEvtMap.findAll { it -> it?.value?.nextT < now } + sortList = aEvtMap.collect { (Long)it?.value?.nextT }?.sort() + releaseTheLock(sHMLF) + hasLock = false + + Long myL = sortList.size() > 0 ? sortList[0] : null + if (myL != null) { + Integer ssecs = (myL - now() + 600L)/1000L + ssecs = ssecs > 1 ? Math.min(ssecs, 300).toInteger() : 2 + runIn(ssecs, meth) + logTrace(msg+"Scheduled afterEvent in ${ssecs} seconds; afterEvtMap: ${sz}") + } else logWarn(msg+"INCONSISTENT") + + } else { + releaseTheLock(sHMLF) + hasLock = false + clearAfterCheckSchedule() + } +} + +// TODO remove +Integer getLastAfterEvtCheck() { return getLastTsValSecs("lastAfterEvtCheck") } + +void afterEvtCheckWatcher() { + String meth = "afterEvtCheckWatcher" + getTheLock(sHMLF, meth) + + Map aEvtMap = (Map)getMemStoreItem("afterEvtMap", [:]) + if(!aEvtMap) aEvtMap = (Map)state.afterEvtMap ?: [:] + + releaseTheLock(sHMLF) + + if(aEvtMap.size() && getLastAfterEvtCheck() > 240) { + runIn(2, "afterEvtCheckHandler") + logDebug(meth+" scheduled afterEvtCheckHandler...") + } +} + +private clearAfterCheckSchedule() { + unschedule("afterEvtCheckHandler") + unschedule("afterEvtCheckWatcher") // TODO remove + state.afterEvtCheckWatcherSched = false + logTrace("Clearing After Event Check Schedule...") +} + +void thermostatEvtHandler(evt) { + evt.name = (String)evt.name == sTEMP ? sTHERMTEMP : (String)evt.name + deviceEvtHandler(evt) +} + +@SuppressWarnings('GroovyFallthrough') +void deviceEvtHandler(evt, Boolean aftEvt=false, Boolean aftRepEvt=false) { + Long evtDelay = now() - (Long)((Date)evt.date).getTime() + Boolean evtOk = false + Boolean evtAd = false + String meth = 'deviceEvtHandler' + String eN = (String)evt.name + def eV = evt?.value + List d = settings."trig_${eN}" + String aftMsg = "${aftEvt ? " | (aftEvt)" : sBLANK}${aftRepEvt ? " | (aftRepEvt)" : sBLANK}" + if(isTrc())logTrace("Device Event | ${eN.toUpperCase()} | Name: ${evt?.displayName} | Value: (${strCapitalize(eV)}) with a delay of ${evtDelay}ms${aftMsg}") + String dc = settings."trig_${eN}_cmd" // desired attribute value + Boolean dca = !!(Boolean)settings."trig_${eN}_all" + Boolean dcavg = (!dca && !!(Boolean)settings."trig_${eN}_avg") + //Boolean dco = ((Integer)settings."trig_${eN}_after"==null && (Boolean)settings."trig_${eN}_once" == true) + //Integer dcw = ((Integer)settings."trig_${eN}_after"==null && (Integer)settings."trig_${eN}_wait") ? (Integer)settings."trig_${eN}_wait" : null + Boolean dco = !!(Boolean)settings."trig_${eN}_once" + Integer dcw = (Integer)settings."trig_${eN}_wait"!=null ? (Integer)settings."trig_${eN}_wait" : null + String extra = sBLANK + switch(eN) { + case sSWITCH: + case sLOCK: + case "securityKeypad": + case "door": + case sSMOKE: + case "carbonMonoxide": + case "windowShade": + case "presence": + case sCONTACT: + case "acceleration": + case sMOTION: + case sWATER: + case sVALVE: + case sTHERMFM: + case sTHERMOS: + case sTHERMMODE: + if(d?.size() && dc) { + if(dc == sANY) { evtOk = true } + else { + if(dca && (allDevAttValsEqual(d, dc, (String)eV))) { evtOk = true; evtAd = true } + else if(eV == dc) { evtOk=true } + } + } + Boolean locknt = (Boolean)settings."trig_${eN}_ign_empty_type" + if(locknt && evtOk && eN in [sLOCK] && (String)evt.value in lLOCKUNL && evt.type == null) { + evtOk = false + extra = " FILTER REMOVED ${eN} (${evt.value}), did not have type set (${evt.type})" + } + if(evtOk && eN in [sLOCK, "securityKeypad"] && (String)evt.value in ["disarmed", "unlocked"]) { + List dcn = settings."trig_${eN}_Codes" + if(dcn) { + String theCode = evt?.data + Map data = null + if(theCode) data = getCodes(null, theCode) +// log.debug "converted $theCode to ${data} starting with ${evt?.data}" + List theCList = data?.collect { (String)it.key } +// log.debug "found code $theCList" + if(!theCList || !(theCList[0] in dcn)) { + evtOk = false + extra = " FILTER REMOVED the received code (${theCode}), code did not match trig_${eN}_Codes (${dcn})" + } + } + } + break + case sPUSHED: + case sRELEASED: + case sHELD: + case sDBLTAP: + def dcn = settings."trig_${eN}_nums" + if(d?.size() && dc && dcn && dcn?.size() > 0) { + if(dcn?.contains(eV)) { evtOk = true } + } +//log.debug "deviceEvtHandler: dcn: $dcn | d: $d | dc: $dc | dco: $dco | dcw: $dcw | evt.value: ${eV}" + break + case sCOOLSP: + case sHEATSP: + case sTHERMTEMP: + case sHUMID: + case sTEMP: + case sPOWER: + case "illuminance": + case sLEVEL: + case sBATT: + Double dcl = settings."trig_${eN}_low" + Double dch = settings."trig_${eN}_high" + Double dce = settings."trig_${eN}_equal" + Map valChk = deviceEvtProcNumValue(evt, d, dc, dcl, dch, dce, dca, dcavg) + evtOk = valChk.evtOk + evtAd = valChk.evtAd + break + default: + logDebug(meth+" | unknown event ${evt?.displayName} ${eN} value: ${eV}") + } + if(isDbg())logDebug(meth+" | evtOk :${evtOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") + eventCompletion(evt, evtOk, dco, dcw, meth+"(${eN})", eV, (String)evt?.displayName, aftRepEvt, evtAd) +/* Boolean waitOk = ((dco || dcw!=null) ? evtWaitRestrictionOk(evt, dco, dcw, aftRepEvt) : true) + Boolean execOk = (evtOk && waitOk) + logDebug(meth+" | execOk: ${execOk} | evtOk :${evtOk} | devEvtWaitOk: ${waitOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") + //if(!waitOk) { return } + if(getConfStatusItem("tiers")) { + processTierTrigEvt(evt, execOk) + } else { + if (execOk) { executeAction(evt, meth+"(${eN})", evtAd, aftRepEvt) } + } + if (!execOk) logTrace(meth+" | Skipping event ${evt?.displayName} ${eN} value: ${eV} | evtOk :${evtOk} | devEvtWaitOk: ${waitOk}") + */ + if (!evtOk) logTrace(meth+" | Skipping event ${evt?.displayName} ${eN} value: ${eV} | evtOk :${evtOk} | event requires all devices evtAd: $evtAd${aftMsg}${extra}") +} + +private void processTierTrigEvt(evt, Boolean evtOk) { + String meth = "processTierTrigEvt" + String msg = " | Name: ${evt?.name} | Value: ${evt?.value} | EvtOk: ${evtOk}" + if(isTrc())logTrace(meth + msg) + + String mK = "actTierState" + getTheLock(sHMLF, meth) + + Map aTierSt = (Map)getMemStoreItem(mK, [:]) + if(!aTierSt) aTierSt = (Map)state[mK] ?: [:] + + if (evtOk) { + if(aTierSt.size()) { + msg = meth + " found already active tier state ${aTierSt}"+msg + } else { + releaseTheLock(sHMLF) + + logDebug(meth + " activating tier state" + msg) + tierEvtHandler(evt) + return + } + } else if(!evtOk && settings.act_tier_stop_on_clear == true) { + def tierConf = aTierSt.evt + if(tierConf?.size() && tierConf?.name == evt?.name && tierConf?.device?.id == evt?.device?.id) { + updMemStoreItem(mK, [:]) + state[mK] = [:] + + releaseTheLock(sHMLF) + + logDebug(meth + " Tier Trigger no longer valid... Clearing TierState and Schedule..."+msg) + unschedule("tierEvtHandler") + atomicState.tierSchedActive = false + updTsVal("lastTierRespStopDt") + return + } else msg = meth + " Tier Trigger valid... exiting...${aTierSt}"+msg + } else msg = meth + " no action ${aTierSt}"+msg + + releaseTheLock(sHMLF) + + logDebug(msg) +} + +def getTierStatusSection() { + String str = sBLANK + if(isTierAction()) { + Map mTierMap = getTierMap() + Boolean tsa = (Boolean)atomicState.tierSchedActive == true + + getTheLock(sHMLF, "getTierStatusSection") + + Map aTierSt = (Map)getMemStoreItem("actTierState", [:]) + if(!aTierSt) aTierSt = (Map)state.actTierState ?: [:] + + Map tS = aTierSt + str += spanSm(" ${sBULLET} Tier Size: ", sCLR4D9) + spanSmBr("${mTierMap?.size()}", sCLRGRY) + str += spanSm(" ${sBULLET} Schedule Active: ", sCLR4D9) + spanSmBr("${tsa}", sCLRGRY) + str += tS?.cycle ? spanSm(" ${sBULLET} Tier Cycle: ", sCLR4D9) + spanSmBr("${tS?.cycle}", sCLRGRY) : sBLANK + str += tS?.schedDelay ? spanSm(" ${sBULLET} Next Delay: ", sCLR4D9) + spanSmBr("${tS?.schedDelay}", sCLRGRY) : sBLANK + str += tS?.lastMsg ? spanSm(" ${sBULLET} Is Last Cycle: ", sCLR4D9) + spanSmBr("${tS?.lastMsg == true}", sCLRGRY) : sBLANK + + releaseTheLock(sHMLF) + + String a = getTsVal("lastTierRespStartDt") + str += a ? spanSm(" ${sBULLET} Last Tier Start: ", sCLR4D9) + spanSmBr(a, sCLRGRY) : sBLANK + a = getTsVal("lastTierRespStopDt") + str += a ? spanSm(" ${sBULLET} Last Tier Stop: ", sCLR4D9) + spanSmBr(a, sCLRGRY) : sBLANK + + section() { + paragraph spanSmBldBr("Tier Response Status: ") + str + } + } +} + +private void resumeTierJobs() { + + getTheLock(sHMLF, "resumeTierJobs") + + Map aTierSt = (Map)getMemStoreItem("actTierState", [:]) + if(!aTierSt) aTierSt = (Map)state.actTierState ?: [:] + + if(aTierSt?.size()) { + releaseTheLock(sHMLF) + if((Boolean)atomicState.tierSchedActive) { + tierEvtHandler() + } + return + } + releaseTheLock(sHMLF) +} + +private void tierEvtHandler(evt=null) { + if(!evt) { + Date adate = new Date() + String dt = dateTimeFmt(adate, "yyyy-MM-dd'T'HH:mm:ss.SSSZ") + evt = [name: "Tiered Schedule", displayName: "Scheduled Tiered Trigger", value: fmtTime(dt), date: adate, device: [id: null]] + } + if(isTrc())logTrace("tierEvtHandler: received event ${evt?.name} ${evt?.value}") + + Map t0 = getTierMap() + Map tierMap = t0 ?: [:] + + getTheLock(sHMLF, "tierEvtHandler") + + String mK = "actTierState" + Map aTierSt = (Map)getMemStoreItem(mK, [:]) + if(!aTierSt) aTierSt = (Map)state[mK] ?: [:] + + Map tierState = aTierSt + //log.debug "tierState: ${tierState}" + //log.debug "tierMap: ${tierMap}" + if(tierMap.size()) { + Map newEvt = (Map)tierState.evt ?: [name: evt?.name, displayName: evt?.displayName, value: evt?.value, unit: evt?.unit, device: [id: evt?.device?.id], date: evt?.date] + Integer curPass = (tierState.cycle && tierState.cycle.toString()?.isNumber()) ? tierState.cycle.toInteger()+1 : 1 + if(curPass <= tierMap.size()) { + tierState.cycle = curPass + tierState.schedDelay = tierMap[curPass]?.delay ?: null + tierState.message = tierMap[curPass]?.message ?: null + tierState.volume = [:] + if(tierMap[curPass]?.volume?.change) tierState.volume.change = tierMap[curPass]?.volume?.change ?: null + if(tierMap[curPass]?.volume?.restore) tierState.volume.restore = tierMap[curPass]?.volume?.restore ?: null + tierState.evt = newEvt + tierState.lastMsg = (curPass+1 > tierMap.size()) + + updMemStoreItem(mK, tierState) + state[mK] = tierState + + releaseTheLock(sHMLF) + + logTrace("tierEvtHandler: tierSize: (${tierMap.size()}) | cycle: ${tierState.cycle} | curPass: (${curPass}) | nextPass: ${curPass+1} | schedDelay: (${tierState.schedDelay}) | Message: (${tierState.message}) | LastMsg: (${tierState.lastMsg})") + + if(curPass == 1) { updTsVal("lastTierRespStartDt"); remTsVal("lastTierRespStopDt") } + + tierSchedHandler([sched: true, tierState: tierState]) + } else { + + updMemStoreItem(mK, [:]) + state[mK] = [:] + releaseTheLock(sHMLF) + atomicState.tierSchedActive = false + updTsVal("lastTierRespStopDt") + logDebug("tierEvtHandler: Tier Cycle has completed... Clearing TierState...") + } + } else releaseTheLock(sHMLF) +} + +private void tierSchedHandler(data) { + if(data && data.tierState?.size() && data.tierState?.message) { + logTrace("tierSchedHandler(${data})") + Map evt = data.tierState.evt + executeAction(evt, "tierSchedHandler", false, false, [msg: data?.tierState?.message as String, volume: data?.tierState?.volume, isFirst: (data?.tierState?.cycle == 1), isLast: (data?.tierState?.lastMsg == true)]) + if(data?.sched) { + if(data.tierState.schedDelay && data.tierState.lastMsg == false) { + logDebug("tierSchedHandler: Scheduling Next Tier Message for (${data.tierState?.schedDelay} seconds)") + runIn(data.tierState.schedDelay, "tierEvtHandler") + } else { + logDebug("tierSchedHandler: Scheduling cleanup for (5 seconds) as this was the last message") + runIn(5, "tierEvtHandler") + } + atomicState.tierSchedActive = true + } + } +} + +Map deviceEvtProcNumValue(evt, List devs=null, String cmd=sNULL, Double dcl=null, Double dch=null, Double dce=null, Boolean dca=false, Boolean dcavg=false) { + Boolean evtOk = false + Boolean evtAd = false + String en = (String)evt.name + String eV = evt.value?.toString() + Double evtValue = evt.value?.toDouble() + if(devs?.size() && cmd && eV?.isNumber()) { + en = en == sTHERMTEMP ? sTEMP : en + evtValue = dcavg ? getDevValueAvg(devs, en) : evtValue + Boolean not=false + //noinspection GroovyFallthrough + switch(cmd) { + case sEQUALS: + if(dce) { + if(!dca && dce == evtValue) evtOk=true + else if(dca && allDevAttNumValsEqual(devs, en, dce)) { evtOk=true; evtAd=true } + } + break + case sNBETWEEN: + not=true + case sBETWEEN: + if(dcl && dch) { + if(!dca) { + evtOk = (evtValue >= dcl) && (evtValue <= dch) + if(not) evtOk=!evtOk + } else if(dca && allDevAttNumValsBetween(devs, en, dcl, dch, not)) { evtOk=true; evtAd=true } + } + break + case sABOVE: + if(dch) { + if(!dca && (evtValue > dch)) evtOk=true + else if(dca && allDevAttNumValsAbove(devs, en, dch)) { evtOk=true; evtAd=true } + } + break + case sBELOW: + if(dcl) { + if(!dca && (evtValue < dcl)) evtOk=true + else if(dca && allDevAttNumValsBelow(devs, en, dcl)) { evtOk=true; evtAd=true } + } + break + } + } + Map result = [evtOk: evtOk, evtAd: evtAd] + if(isDbg())logDebug("deviceEvtProcNumValue | result: $result | evtName: ${evt.name} (${en}) | evtVal: $evtValue ($eV) | cmd: ${cmd} | low: ${dcl} | high: ${dch} | equal: ${dce} | all: ${dca} | avg: $dcavg") + return result +} + +static String evtValueCleanup(ival) { + def val=ival + // log.debug "val(in): ${val}" + val = (val?.toString()?.isNumber() && val?.toString()?.endsWith(".0")) ? val?.toDouble()?.round(0) : val + // log.debug "val(out): ${val}" + return val?.toString() +} + +private void clearEvtHistory() { + settingUpdate("clrEvtHistory", sFALSE, sBOOL) + String mK = "valEvtHistory" + + getTheLock(sHMLF, "clearEvtHistory") + + updMemStoreItem(mK, [:]) + state[mK] = [:] + + releaseTheLock(sHMLF) +} + +private Boolean evtWaitRestrictionOk(evt, Boolean once, Integer wait, Boolean aftRepEvt=false) { + Boolean ok = true + Long dur + Boolean waitOk = (wait!=null && wait >= 0) + String meth = "evtWaitRestrictionOk" + String msg = sBLANK + Boolean dayOk + String n = (String)evt?.name + String dID= evt?.device?.id as String + if(evt && n && dID) { + String kN= dID + '_' +n + msg = "Last ${n.capitalize()} Event for Device ($dID) | onceDaily ${once} | wait ${wait} | repeatEvt: ${aftRepEvt} | " + Date evtDt = (Date)evt.date + + getTheLock(sHMLF, meth) + + String mK = "valEvtHistory" + Map evtHistMap = (Map)getMemStoreItem(mK, [:]) + if (!evtHistMap) evtHistMap = (Map)state[mK] ?: [:] + + // log.debug "prevDt: ${evtHistMap[n]?.dt ? parseDate(evtHistMap[n]?.dt as String) : null} | evtDt: ${evtDt}" + if (evtHistMap.containsKey(kN) && evtHistMap[kN]?.dt) { + Date prevDt = parseDate((String)evtHistMap[kN].dt) + // log.debug "prevDt: ${prevDt.toString()}" + if (prevDt && evtDt) { + dur = Math.round(( (Long)evtDt.getTime() - (Long)prevDt.getTime()) / 1000.0D) + waitOk = waitOk ? (dur && (wait < dur)) : true + dayOk = !once || (once && !isDateToday(prevDt)) + ok = (waitOk && dayOk) + msg += "Occurred: (${dur} sec ago) | Desired Wait: (${wait} sec) - (waitOk $waitOk && dayOk $dayOk)" + } + } else msg += "No history found" + // check if same and don't save? + if (ok && !aftRepEvt) { + evtHistMap[kN] = [dt: formatDt(evtDt), value: evt.value, name: n, dID: dID] + updMemStoreItem(mK, evtHistMap) + state[mK] = evtHistMap + } + + releaseTheLock(sHMLF) + } else { + msg="missing evt, name ($n) or deviceId ($dID)" + } + msg += " Status: (${ok ? okSymFLD : notOkSymFLD})" + if(isDbg())logDebug(meth+": "+msg) + return ok +} + +@SuppressWarnings('GroovyFallthrough') +static String getAttrPostfix(String attr) { + switch(attr) { + case sHUMID: + case sLEVEL: + case sBATT: + return " percent" + case sTHERMTEMP: + case sTEMP: + return " degrees" + case "illuminance": + return " lux" + case sPOWER: + return " watts" + default: + return sBLANK + } +} + + +/*********************************************************************************************************** + CONDITIONS HANDLER +************************************************************************************************************/ +Boolean reqAllCond() { + Boolean mult = multipleConditions() + return ( mult && (Boolean)settings.cond_require_all ) +} + +Boolean timeCondOk() { + Date startTime + Date stopTime + Date now = new Date() + String startType = (String)settings.cond_time_start_type + String stopType = (String)settings.cond_time_stop_type + if(startType && stopType) { + startTime = startType == sTIME && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null + stopTime = stopType == sTIME && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null + + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L + Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + + if(startTime && stopTime) { + Boolean not = startTime.getTime() > stopTime.getTime() + Boolean isBtwn = timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) + isBtwn = not ? !isBtwn : isBtwn + state.startTime = formatDt(startTime) + state.stopTime = formatDt(stopTime) + if(isTrc())logTrace("timeCondOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not? startTime:stopTime})") + return isBtwn + } + } + if(isTrc())logTrace("timeCondOk | (null)") + state.startTime = sNULL + state.stopTime = sNULL + return null +} + +Boolean dateCondOk() { + Boolean result = null + Boolean dOk + Boolean mOk + if((List)settings.cond_days || (List)settings.cond_months) { + Boolean reqAll = reqAllCond() + dOk = settings.cond_days ? (isDayOfWeek((List)settings.cond_days)) : reqAll + mOk = settings.cond_months ? (isMonthOfYear((List)settings.cond_months)) : reqAll + result = reqAll ? (mOk && dOk) : (mOk || dOk) + } + if(isTrc()){ + //noinspection GroovyVariableNotAssigned + logTrace("dateCondOk | $result | monthOk: $mOk | daysOk: $dOk") + } + return result +} + +Boolean locationCondOk() { + Boolean result = null + Boolean mOk + Boolean aOk + List myL = (List)settings.cond_alarmSystemStatus + if((List)settings.cond_mode || (String)settings.cond_mode_cmd || myL) { + Boolean reqAll = reqAllCond() + mOk = ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : reqAll + aOk = myL ? isInAlarmMode(myL) : reqAll + result = reqAll ? (mOk && aOk) : (mOk || aOk) + } + if(isTrc()){ + //noinspection GroovyVariableNotAssigned + logTrace("locationCondOk | $result | modeOk: $mOk | alarmOk: $aOk") + } + return result +} + +Boolean checkDeviceCondOk(String att) { + List devs = (List)settings."cond_${att}" ?: null + def cmdVal = settings."cond_${att}_cmd" ?: null // list or string + Boolean all = !!(Boolean)settings."cond_${att}_all" + if( !(att && devs && cmdVal) ) { return true } + return all ? allDevAttValsEqual(devs, att, cmdVal) : anyDevAttValsEqual(devs, att, cmdVal) +} + +Boolean checkDeviceNumCondOk(String att) { + List devs = (List)settings."cond_${att}" ?: null + String cmd = settings."cond_${att}_cmd" ?: null + Double dcl = settings."cond_${att}_low"!=null ? settings."cond_${att}_low" : null + Double dch = settings."cond_${att}_high"!=null ? settings."cond_${att}_high" : null + Double dce = settings."cond_${att}_equal"!=null ? settings."cond_${att}_equal" : null + Boolean dca = (Boolean)settings."cond_${att}_all" ?: false + if( !(att && devs && cmd) ) { return true } + Boolean not=false + Boolean a = true + //noinspection GroovyFallthrough + switch(cmd) { + case sEQUALS: + if(dce) { + a = dca ? allDevAttNumValsEqual(devs, att, dce) : anyDevAttNumValEqual(devs, att, dce) + } + break + case sNBETWEEN: + not=true + case sBETWEEN: + if(dcl && dch) { + a = dca ? allDevAttNumValsBetween(devs, att, dcl, dch, not) : anyDevAttNumValBetween(devs, att, dcl, dch, not) + } + break + case sABOVE: + if(dch) { + a = dca ? allDevAttNumValsAbove(devs, att, dch) : anyDevAttNumValAbove(devs, att, dch) + } + break + case sBELOW: + if(dcl) { + a = dca ? allDevAttNumValsBelow(devs, att, dcl) : anyDevAttNumValBelow(devs, att, dcl) + } + break + } + return a +} + +@Field static final List lDATTSTR = ["switch", "motion", "presence", "contact", "acceleration", "lock", "securityKeypad", "door", "windowShade", "valve", "water", "thermostatMode", "thermostatOperatingState", "thermostatFanMode", "smoke", "carbonMonoxide"] +@Field static final List lDATTNUM = ["temperature", "humidity", "illuminance", "level", "power", "battery", "coolingSetpoint", "heatingSetpoint", "thermostatTemperature"] +// these are momentary triggers only: pushed released held doubleTapped + +Boolean deviceCondOk() { + List skipped = [] + List passed = [] + List failed = [] + lDATTSTR.each { String i-> + if(!(List)settings."cond_${i}") { skipped.push(i); return } + checkDeviceCondOk(i) ? passed.push(i) : failed.push(i) + } + lDATTNUM.each { String i-> + if(!(List)settings."cond_${i}") { skipped.push(i); return } + checkDeviceNumCondOk(i) ? passed.push(i) : failed.push(i) + } + Integer cndSize = (passed.size() + failed.size()) + Boolean result = null + if(cndSize != 0) result = reqAllCond() ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() > 0) + if(isTrc())logTrace("deviceCondOk | ${result} | Found: (${(passed?.size() + failed?.size())}) | Skipped: $skipped | Passed: $passed | Failed: $failed") + return result +} + +Map conditionStatus() { + Boolean reqAll = reqAllCond() + List failed = [] + List passed = [] + List skipped = [] + Boolean ok = true + if((Boolean)state.dupPendingSetup) ok = false + Integer cndSize = (Integer)null + if(ok) { + [sTIME, "date", "location", "device"].each { String i-> + def s = "${i}CondOk"() + if(s == null) { skipped.push(i); return } + s ? passed.push(i) : failed.push(i) + } + cndSize = (passed.size() + failed.size()) + ok = reqAll ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() > 0) + if(cndSize == 0) ok = true + } + if(isTrc())logTrace("conditionsStatus | ok: $ok | RequireAll: ${reqAll} | Found: (${cndSize}) | Skipped: $skipped | Passed: $passed | Failed: $failed") + return [ok: ok, passed: passed, blocks: failed] +} + +Boolean devCondConfigured(String att) { + return ((List)settings."cond_${att}" && settings."cond_${att}_cmd") +} + +Boolean devNumCondConfigured(String att) { + return (settings."cond_${att}_cmd" && (settings."cond_${att}_low"!=null || settings."cond_${att}_high"!=null || settings."trig_${att}_equal"!=null)) +} + +Boolean timeCondConfigured() { + Boolean startTime = ((String)settings.cond_time_start_type in lSUNRISESET || ((String)settings.cond_time_start_type == sTIME && settings.cond_time_start)) + Boolean stopTime = ((String)settings.cond_time_stop_type in lSUNRISESET || ((String)settings.cond_time_stop_type == sTIME && settings.cond_time_stop)) + return (startTime && stopTime) +} + +Boolean dateCondConfigured() { + Boolean days = (settings.cond_days) + Boolean months = (settings.cond_months) + return (days || months) +} + +Boolean locationModeConfigured() { + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", sARE, sENUM) } + return ((List)settings.cond_mode && (String)settings.cond_mode_cmd) +} + +Boolean locationAlarmConfigured() { + return ((List)settings.cond_alarmSystemStatus) +} + +Boolean deviceCondConfigured() { + return (deviceCondCount() > 0) +} + +Integer deviceCondCount() { + List devAtts = lDATTSTR + lDATTNUM + List items = [] + devAtts.each { String dc-> if(devCondConfigured(dc)) { items.push(dc) } } + return items.size() +} + +Boolean conditionsConfigured() { + return (timeCondConfigured() || dateCondConfigured() || locationModeConfigured() || locationAlarmConfigured() || deviceCondConfigured()) +} + +Boolean multipleConditions() { + Integer cnt = 0 + if(timeCondConfigured()) cnt++ + if(dateCondConfigured()) cnt++ + if(locationModeConfigured()) cnt++ + if(locationAlarmConfigured()) cnt++ + cnt = cnt + deviceCondCount() + return (cnt>1) +} + +/*********************************************************************************************************** + ACTION EXECUTION +************************************************************************************************************/ + +static String convEvtType(String type) { + Map typeConv = [ + (sPISTNEXEC): "Piston", + (sALRMSYSST): "Alarm system status", + "hsmStatus": "Alarm system status", + "hsmAlert": "Alarm system alert" + ] + return (type && typeConv.containsKey(type)) ? typeConv[type] : type +} + +String decodeVariables(evt, String str) { + if(!str) return str + if(evt) { + // log.debug "str: ${str} | vars: ${(str =~ /%[a-z]+%/)}" + String typS = "%type%" + String namS = "%name%" + if(str.contains(typS) && (String)evt.name) { + if(str.contains(namS)) { + str = str.replaceAll(typS, !(String)evt.displayName?.toLowerCase()?.contains((String)evt.name) ? convEvtType((String)evt.name) : sBLANK) + } else { + str = str.replaceAll(typS, convEvtType((String)evt.name)) + } + } + str = (str.contains(namS)) ? str.replaceAll(namS, (String)evt.displayName) : str + str = (str.contains("%unit%") && (String)evt.name) ? str.replaceAll("%unit%", getAttrPostfix((String)evt.name)) : str + str = (str.contains("%value%") && evt.value != null) ? str.replaceAll("%value%", evt.value?.toString()?.isNumber() ? evtValueCleanup(evt?.value) : evt?.value) : str + if(!(evt instanceof com.hubitat.hub.domain.Event) && evt.totalDur) { + Integer mins = durationToMinutes(evt.totalDur) + str = (str.contains("%duration%")) ? str.replaceAll("%duration%", "${evt.totalDur} second${evt.totalDur > 1 ? "s" : sBLANK} ago") : str + str = (str.contains("%duration_min%")) ? str.replaceAll("%duration_min%", "${mins} minute${mins > 1 ? "s" : sBLANK} ago") : str + str = (str.contains("%durationmin%")) ? str.replaceAll("%durationmin%", "${mins} minute${mins > 1 ? "s" : sBLANK} ago") : str + str = (str.contains("%durationval%")) ? str.replaceAll("%durationval%", "${evt.totalDur} second${evt.totalDur > 1 ? "s" : sBLANK}") : str + str = (str.contains("%durationvalmin%")) ? str.replaceAll("%durationvalmin%", "${mins} minute${mins > 1 ? "s" : sBLANK}") : str + } + } + Date adate = (Date)evt.date ?: new Date() + str = (str.contains("%date%")) ? str.replaceAll("%date%", convToDate(adate)) : str + str = (str.contains("%time%")) ? str.replaceAll("%time%", convToTime(adate)) : str + str = (str.contains("%datetime%")) ? str.replaceAll("%datetime%", convToDateTime(adate)) : str + return str +} + +static Integer durationToMinutes(dur) { + if(dur && dur>=60) return (dur/60)?.toInteger() + return 0 +} + +static Integer durationToHours(dur) { + if(dur && dur>= 3600) return (dur / 3600).toInteger() + return 0 +} +/* +private List decamelizeStr(String str) { + return (List) str.split("(? eTxtItems = glbText.tokenize(";") + return decodeVariables(evt, (String)getRandomItem(eTxtItems)) + } else if(tierMsg) { + List eTxtItems = tierMsg.tokenize(";") + return decodeVariables(evt, (String)getRandomItem(eTxtItems)) + } else { + List devs = settings."trig_${eN}" ?: [] + // String dc = (String)settings."trig_${eN}_cmd" + // Boolean dca = ((Boolean)settings."trig_${eN}_all" == true) + String dct = (String)settings."trig_${eN}_txt" ?: sNULL + String dcart = (Integer)settings."trig_${eN}_after_repeat" && (String)settings."trig_${eN}_after_repeat_txt" ? (String)settings."trig_${eN}_after_repeat_txt" : sNULL + List eTxtItems = dct ? dct.tokenize(";") : [] + List rTxtItems = dcart ? dcart.tokenize(";") : [] + List testItems = eTxtItems + rTxtItems + if(testMode && testItems.size()) { + return decodeVariables(evt, (String)getRandomItem(testItems)) + } else if(!testMode && isRepeat && rTxtItems?.size()) { + return decodeVariables(evt, (String)getRandomItem(rTxtItems)) + } else if(!testMode && eTxtItems?.size()) { + return decodeVariables(evt, (String)getRandomItem(eTxtItems)) + } else { + //noinspection GroovyFallthrough + switch(eN) { + case sMODE: + return "The location mode is now set to ${eV}" + case sPISTNEXEC: + return "The ${evt?.displayName} piston was just executed!." + case "scene": + return "The ${eV} scene was just executed!." + case "hsmStatus": + case sALRMSYSST: + return "The ${getAlarmSystemName()} is now set to ${eV}" + case "guard": + return "Alexa Guard is now set to ${eV}" + case "hsmAlert": + return "A ${getAlarmSystemName()} ${evt?.displayName} alert with ${eV} has occurred." + case "sunriseTime": + case "sunsetTime": + return "The ${getAlarmSystemName()} is now set to ${eV}" + case sSCHED: + return "Your scheduled event has occurred at ${eV}" + case sPUSHED: + case sHELD: + case sRELEASED: + case sDBLTAP: + return "Button ${eV} was ${eN == sDBLTAP ? "double tapped" : eN} on ${evt?.displayName}" + + case sTHERMTEMP: + eN = sTEMP + /* + case sCOOLSP: + case sHEATSP: + case sTHERMMODE: + case sTHERMFM: + case sTHERMOS: + case sSMOKE: + case "carbonMonoxide": + return "${eN} is ${eV} on ${evt?.displayName}!" + */ + default: + String t0 = getAttrPostfix(eN) + String postfix = t0 ?: sBLANK + if(evtAd && devs?.size()>1) { + return "All ${devs.size()}${!evt?.displayName?.toLowerCase()?.contains(eN) ? " ${eN}" : sBLANK} devices are ${eV} ${postfix}" + } else { + return "${evt?.displayName}${!evt?.displayName?.toLowerCase()?.contains(eN) ? " ${eN}" : sBLANK} is ${eV} ${postfix}" + } + break + } + } + } + return "Invalid Text Received... Please verify Action configuration..." +} + +public getActionHistory(Boolean asObj=false) { + List eHist = (List)getMemStoreItem("actionHistory") + List output = [] + if(eHist.size()) { + eHist.sort { a,b-> b.dt <=> a.dt }.each { Map h-> + List hList = [] + hList.push([name: "Trigger:", val: h?.evtName]) + hList.push([name: "Device:", val: h?.evtDevice]) + hList.push([name: "Value:", val: h?.evtValue]) + hList.push([name: "Condition Status:", val: (h?.active ? "Passed" : "Failed")]) + hList.push([name: "Conditions Passed:", val: h?.passed]) + hList.push([name: "Conditions Blocks:", val: h?.blocks]) + if(h?.test) hList.push([name: "Test Mode:", val: true]) + if(h?.isRepeat) hList.push([name: "Repeat:", val: true]) + hList.push([name: "Source:", val: h?.src]) + hList.push([name: "DateTime:", val: h?.dt]) + if(hList.size()) { + output.push(spanSm(kvListToHtmlTable(hList, sCLR4D9), sCLRGRY)) + } + } + } else { output.push("No History Items Found...") } + if(!asObj) { + output.each { String i-> paragraph spanSm(i) } + } else { + return output + } +} + +private static String kvListToHtmlTable(List tabList, String color=sCLRGRY) { + String str = sBLANK + if(tabList?.size()) { + str += "" + tabList.each { it-> + str += "" + } + str += "
    ${spanSmBld((String)it.name)}${spanSmBr("${it.val}")}
    " + } + return str +} + +private addToActHistory(evt, data, Integer max=10) { + getTheLock(sHMLF, "addToActHistory") + + List eData = (List)getMemStoreItem("actionHistory") + if(eData == null) { eData = [] } + eData.push([ + dt: getDtNow(), + active: (data?.status?.ok == true), + evtName: evt?.name, + evtValue: evt?.value, + evtDevice: evt?.displayName, + blocks: data?.status?.blocks, + passed: data?.status?.passed, + test: data?.test, + isTierCmd: data?.isTier, + isRepeat: data?.isRepeat, + src: data?.src + ]) + Integer lsiz = eData.size() + if(lsiz > max) { eData = eData.drop( (lsiz-max) ) } + // log.debug "actionHistory Size: ${eData?.size()}" + updMemStoreItem("actionHistory", eData) + + releaseTheLock(sHMLF) +} + +void clearActHistory(){ + getTheLock(sHMLF, "clearActHistory") + + updMemStoreItem("actionHistory", []) + + releaseTheLock(sHMLF) +} + +private void executeAction(evt = null, String src=sNULL, Boolean allDevsResp=false, Boolean isRptAct=false, Map tierData=null) { + String meth="executeAction" + Long startTime = now() + Boolean testMode = false + if((Boolean)settings.actTestRun) { testMode = true } + if(isTrc())logTrace(meth+" ${src ? '('+src+')' : sBLANK}${testMode ? " | [TestMode]" : sBLANK}${allDevsResp ? " | [AllDevsResp]" : sBLANK}${isRptAct ? " | [RepeatEvt]" : sBLANK}") + if(isPaused(true)) { logWarn("Action is PAUSED... Skipping Action Execution...", true); return } + Map condStatus = conditionStatus() + // log.debug "condStatus: ${condStatus}" + addToActHistory(evt, [status: condStatus, test: testMode, src: src, isRepeat: isRptAct, isTier: (tierData != null)] ) + Map actMap = (Map)state.actionExecMap + + Integer actDevSiz = settings.act_EchoDevices ? settings.act_EchoDevices.size() : 0 +// List actDevices // = settings.act_EchoDevices ? parent?.getDevicesFromList(settings.act_EchoDevices) : [] +// Integer actDevSiz = actDevices.size() + + Map activeZones = settings.act_EchoZones ? getActiveZones() : [:] + activeZones = activeZones.size() ? activeZones.findAll { it?.key in settings.act_EchoZones } : [:] + Integer actZonesSiz = activeZones.size() + +// log.debug "activeZones: $activeZones" + + String actMsgTxt = sNULL + String actType = (String)settings.actionType + Boolean firstTierMsg = (tierData && tierData.isFirst == true) + Boolean lastTierMsg = (tierData && tierData.isLast == true) + Boolean actOk = getConfStatusItem("actions") + if(actOk && actType) { +// def alexaMsgDev = actDevSiz && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null + if(!(Boolean)condStatus.ok) { logWarn(meth+" | Skipping execution because ${condStatus.blocks} conditions have not been met", true); return } + if(!actMap || !actMap?.size()) { logError(meth+" Error | The ActionExecutionMap is not found or is empty", true); return } + if(settings.act_EchoZones && actZonesSiz == 0 && actDevSiz == 0) { logWarn(meth+" | No Active Zones Available and No Alternate Echo Devices Selected.", true); return } + if(actDevSiz == 0 && !settings.act_EchoZones) { logError(meth+" Error | Echo Device List not found or is empty", true); return } + if(!actMap.actionType) { logError(meth+" Error | The ActionType is missing or is empty", true); return } + Map actConf = (Map)actMap.config + Integer actDelay = (Integer)actMap.delay ?: 0 + Integer actDelayMs = actDelay*1000 + Integer changeVol = actConf?.volume?.change != null ? (Integer)actConf?.volume?.change : null + Integer restoreVol = actConf?.volume?.restore != null ? (Integer)actConf?.volume?.restore : null + //Integer alarmVol = actConf?.volume?.alarm ?: null + Map zoneVolumeMap = (Map)actConf?.zoneVolume ?: null + Boolean chkDnd=true + //noinspection GroovyFallthrough + switch(actType) { + case sSPEAKI: + case sSPEAKIT: + case sSPEAKIP: + case sSPEAKIPT: + case sANNI: + case sANNIT: + chkDnd=false + case sSPEAK: + case sSPEAKP: + case sSPEAKT: + case sSPEAKPT: + case sANN: + case sANNT: + if(actConf[actType]) { + if(tierData?.volume && tierData?.volume?.change || tierData?.volume?.restore) { + if(tierData?.volume?.change) changeVol = tierData?.volume?.change + if(tierData?.volume?.restore) restoreVol = tierData?.volume?.restore + } + String txt = getResponseItem(evt, (String)tierData?.msg, allDevsResp, isRptAct, testMode) ?: sNULL + // log.debug "txt: $txt" + if(!txt) { txt = "Invalid Text Received... Please verify Action configuration..." } + actMsgTxt = txt + + String mCmd = actType.replaceAll("_tiered", sBLANK) + if(actZonesSiz) { + parent?.sendZoneCmd([zones: activeZones.collect { (String)it?.key }, cmd: mCmd, title: getActionName(), message: txt, changeVol: changeVol, restoreVol: restoreVol, zoneVolumes: zoneVolumeMap, delay: actDelayMs]) + if(isDbg())logDebug("Sending ${mCmd} Command: (${txt}) to Zones via Parent (${activeZones.collect { it?.value?.name }.join(',')})${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + + } else { + if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + List devObjs = [] + // log.debug "actDevices: ${actDevices}" + String cmd = mCmd in [sSPEAK, sSPEAKP, sSPEAKI, sSPEAKIP, sSPEAKT, sSPEAKIT, sSPEAKPT, sSPEAKIPT] ? sTTS : "announce" + actDevices?.each { + Map devInfo = it?.getEchoDevInfo(cmd, true) // ignores dnd setting in device + if(devInfo) { + Boolean dnd = chkDnd ? parent.getDndEnabled((String)devInfo.deviceSerialNumber) : false + //if(!dnd) devObjs?.push([deviceTypeId: devInfo.deviceTypeId, deviceSerialNumber: devInfo.deviceSerialNumber, dni: devInfo.dni]) + if(!dnd) devObjs?.push(devInfo) + } + } + parent?.sendDevObjCmd(devObjs, mCmd, getActionName(), txt, changeVol, restoreVol) + if(isDbg())logDebug("Sending ${mCmd} Command: (${txt}) to devices via Parent (${devObjs})${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } + } + } + break + case "voicecmd": + case "sequence": + String mCmd= actConf[actType] ? (String)actConf[actType].cmd : sNULL + String mText= actConf[actType] ? (String)actConf[actType].text : sNULL + if(mText != sNULL) { + if(actZonesSiz) { + sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { (String)it?.key }, cmd: actType, message: mText, delay: actDelayMs], isStateChange: true, display: false, displayed: false) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mText}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } else if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices.each { dev-> + dev?."${mCmd}"(mText) + } + if(isDbg())logDebug("Sending ${mAct} Command: (${mText}) to devices (${actDevices})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } + } + break + case "playback": + case "dnd": + String mCmd= actConf[actType] ? (String)actConf[actType].cmd : sNULL + if(mCmd != sNULL) { + if(actZonesSiz) { + sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[zones: activeZones.collect { (String)it?.key }, cmd: mCmd, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mCmd}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } else if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices.each { dev-> + if(mCmd != "volume") dev?."${mCmd}"() + else if(mCmd == "volume" && changeVol != null) dev?.setVolume(changeVol) + } + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${mCmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol != null ? " | Volume: ${changeVol}" : sBLANK}") + } + } + break + + case "builtin": + case "calendar": + case sWEATH: + if(actConf[actType] && actConf[actType].cmd) { + if(actZonesSiz) { + sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { (String)it?.key }, cmd: actConf[actType].cmd, message: actConf[actType].text, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } else if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices.each { dev-> + dev?."${actConf[actType].cmd}"(changeVol, restoreVol) + } + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } + } + break + + case "sounds": + if(actConf[actType] && actConf[actType].cmd && actConf[actType].name) { + if(actZonesSiz) { + sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { it?.key as String }, cmd: actConf[actType].cmd, message: actConf[actType].name, changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: false, displayed: false) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd} | Name: ${actConf[actType].name}) to Zones (${activeZones.collect { it?.value?.name }})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } else if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices.each { dev-> + dev?."${actConf[actType].cmd}"(actConf[actType].name, changeVol, restoreVol) + } + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd} | Name: ${actConf[actType].name}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } + } + break + + case "alarm": + case "reminder": + if(actConf[actType] && actConf[actType].cmd && actConf[actType].label && actConf[actType].date && actConf[actType].time) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices?.each { dev-> + dev?."${actConf[actType].cmd}"(actConf[actType].label, actConf[actType].date, actConf[actType].time) + // dev?."${actConf[actType]?.cmd}"(actConf[actType]?.label, actConf[actType]?.date, actConf[actType]?.time, actConf[actType]?.recur?.type, actConf[actType]?.recur?.opt) + } + if(isDbg())logDebug("Sending ${actType?.toString()?.capitalize()} Command: (${actConf[actType]?.cmd}) to ${actDevices} | Label: ${actConf[actType]?.label} | Date: ${actConf[actType]?.date} | Time: ${actConf[actType]?.time}") + } + break + + case "music": + if(actConf[actType] && actConf[actType]?.cmd && actConf[actType]?.provider && actConf[actType]?.search) { + if(isDbg())logDebug("musicProvider: ${actConf[actType]?.provider} | ${convMusicProvider((String)actConf[actType]?.provider)}") + if(actZonesSiz) { + sendLocationEvent(name: "es3ZoneCmd", value: actType, data:[ zones: activeZones.collect { it?.key as String }, cmd: actType, search: actConf[actType]?.search, provider: convMusicProvider((String)actConf[actType]?.provider), changeVol: changeVol, restoreVol: restoreVol, delay: actDelayMs], isStateChange: true, display: true, displayed: true) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${txt}) to Zones (${activeZones.collect { it?.value?.name }} | Provider: ${actConf[actType]?.provider} | Search: ${actConf[actType]?.search} | Command: (${actConf[actType]?.cmd})${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } else if(actDevSiz) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices.each { dev-> + dev?."${actConf[actType]?.cmd}"(actConf[actType]?.search, convMusicProvider((String)actConf[actType]?.provider), changeVol, restoreVol) + } + if(isDbg())logDebug("Sending ${actType.capitalize()} | Provider: ${actConf[actType]?.provider} | Search: ${actConf[actType]?.search} | Command: (${actConf[actType]?.cmd}) to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}${changeVol!=null ? " | Volume: ${changeVol}" : sBLANK}${restoreVol!=null ? " | Restore Volume: ${restoreVol}" : sBLANK}") + } + } + break + + case "alexaroutine": + if(actConf[actType] && actConf[actType].cmd && actConf[actType].routineId) { + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + actDevices[0]?."${actConf[actType].cmd}"((String)actConf[actType].routineId) + if(isDbg())logDebug("Sending ${actType.capitalize()} Command: (${actConf[actType].cmd}) | RoutineId: ${actConf[actType].routineId} to ${actDevices}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } + break + + case "wakeword": + if(actConf[actType] && actConf[actType]?.devices && actConf[actType]?.devices?.size()) { + actConf[actType]?.devices?.each { d-> + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + def aDev = actDevices?.find { it?.id?.toString() == d?.device?.toString() } // TODO + if(aDev) { + aDev?."${d?.cmd}"(d?.wakeword) + if(isDbg())logDebug("Sending WakeWord: (${d?.wakeword}) | Command: (${d?.cmd}) to ${aDev}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } + } + } + break + + case "bluetooth": + if(actConf[actType] && actConf[actType]?.devices && actConf[actType]?.devices?.size()) { + actConf[actType]?.devices?.each { d-> + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + def aDev = actDevices?.find { it?.id?.toString() == d?.device?.toString() } + if(aDev) { + if(d?.cmd == "disconnectBluetooth") { + aDev?."${d?.cmd}"() + } else { + aDev?."${d?.cmd}"(d?.btDevice) + } + if(isDbg())logDebug("Sending ${d?.cmd} | Bluetooth Device: ${d?.btDevice} to ${aDev}${actDelay ? " | Delay: (${actDelay})" : sBLANK}") + } + } + } + break + } + if(isActNotifConfigured()) { + Boolean ok2SendNotif = true + if(customMsgConfigured()) { actMsgTxt = settings.notif_custom_message } + if(customMsgRequired() && !customMsgConfigured()) { ok2SendNotif = false } + if(ok2SendNotif && actMsgTxt) { + def alexaMsgDev = null + if(actDevSiz && (Boolean)settings.notif_alexa_mobile){ + List actDevices = parent?.getDevicesFromList(settings.act_EchoDevices) + alexaMsgDev = actDevices[0] + } + if(sendNotifMsg(getActionName(), actMsgTxt, alexaMsgDev, false)) { logTrace("Sent Action Notification...") } + } + } + String mmS = "scheduled executeTaskCommands in " + String cmd = "executeTaskCommands" + + if(tierData?.size() && (Integer)settings.act_tier_cnt > 1) { + if(isDbg())logDebug("firstTierMsg: ${firstTierMsg} | lastTierMsg: ${lastTierMsg}") + if(firstTierMsg) { + Integer del = (Integer)settings.act_tier_start_delay + if(del) { + logTrace(mmS+"$del seconds - start delay") + runIn(del, cmd, [data:[type: sACT_START]]) + } else { executeTaskCommands([type:sACT_START]) } + } + if(lastTierMsg) { + Integer del = (Integer)settings.act_tier_stop_delay + if(del) { + logTrace(mmS+"$del seconds - stop delay") + runIn(del, cmd, [data:[type: sACT_STOP]]) + } else { executeTaskCommands([type:sACT_STOP]) } + } + } else { + Integer del = (Integer)settings.act_tasks_delay + if(del) { + logTrace(mmS+"$del seconds - action tasks delay") + runIn(del, cmd, [data:[type: sACT]]) + } else { executeTaskCommands([type: sACT]) } + } + } + if(isTrc())logTrace(meth+" Finished | ProcessTime: (${now()-startTime}ms)") +} + +private postTaskCommands(data) { + String p = data?.type ?: sNULL + if(p && settings."${p}sirens" && (String)settings."${p}siren_cmd") { settings."${p}sirens"*.off() } +} + + +// parent calls for editing + +public Map getInputData(String inName) { + String desc = "
  • Add custom responses to use when this action is executed.
  • " + String title + String tmplt = '%name% %type% is now %value% %unit%' + String vDesc = "
  • Example: %name% %type% is now %value% would translate into The garage door contact is now open
  • To make beep sounds use: wop, wop, wop (equals 3 beeps)
  • " + switch(inName) { + case "act_speak_txt": + title = "Global Action Speech Event" + break + case "act_announcement_txt": + title = "Global Announcement Speech Event" + break + default: + if(inName?.startsWith(sTRIG)) { + List i = inName.tokenize("_") + String s = i[1]?.capitalize() + title = "(${s}) Trigger " + desc = "
  • Add custom responses for ${s} " + if(i.contains("repeat")) { + title += "Repeat " + desc += "events which have to be repeated" + } else { + desc += "trigger events" + } + title += "Events" + desc += ".
  • ${vDesc}" + } + else if(inName?.startsWith(sACT_ITEM) && inName?.endsWith("_txt")) { + List i = inName.tokenize("_") + String s = i[3] + title = "Tier Response (${s})" + desc = "
  • Add custom responses to use when this action is executed.
  • " + tmplt = "Custom tier ${s} message here." + } else { + desc = sNULL + title = sNULL + tmplt = sNULL + } + break + } + Map o = [ + val: settings."${inName}" ? settings."${inName}"?.toString()?.replaceAll("\'", sBLANK) : null, + desc: """
      ${desc}
    """, + title: title, + template: tmplt, + version: appVersionFLD + ] + if(devModeFLD) log.debug "getInputData inName: $inName result: $o" + return o +} + +public Boolean updateTxtEntry(obj) { + // log.debug "updateTxtEntry | Obj: $obj" + if(obj?.name && obj?.type) { + settingUpdate("${obj?.name}", obj?.val ?: sBLANK, obj?.type as String) + return true + } + return false +} + +public getSettingInputVal(String inName) { + // log.debug "getSettingInputVal: ${inName}" + return settings."${inName}" ?: null +} + + +/*********************************************************************************************************** + HELPER UTILITES +************************************************************************************************************/ + +Boolean advLogsActive() { return (isDbg() || isTrc()) } +public void logsEnabled() { if(advLogsActive() && !getTsVal("logsEnabled")) { logTrace("enabling logging timer"); updTsVal("logsEnabled") } } +public void logsDisable() { + if(advLogsActive()) { + Integer dtSec = getLastTsValSecs("logsEnabled", null) + if(dtSec && (dtSec > 3600*6)) { + settingUpdate("logDebug", sFALSE, sBOOL) + settingUpdate("logTrace", sFALSE, sBOOL) + remTsVal("logsEnabled") + log.debug "Disabling debug logs" + } + } +} + +@Field volatile static Map tsDtMapFLD=[:] + +private void updTsVal(String key, String dt=sNULL) { + String appId=app.getId() + Map data=tsDtMapFLD[appId] ?: [:] + if(!data) data = (Map)state.tsDtMap ?: [:] + if(key) { data[key] = dt ?: getDtNow() } + tsDtMapFLD[appId]=data + tsDtMapFLD=tsDtMapFLD + + state.tsDtMap = data +} + +private void remTsVal(key) { + String appId=app.getId() + Map data=tsDtMapFLD[appId] ?: [:] + if(!data) data = (Map)state.tsDtMap ?: [:] + + if(key) { + if(key instanceof List) { + List t = (List)key + t.each { String k-> + if(data.containsKey(k)) { data.remove(k) } + } + } else { + String k = key + if(data.containsKey(k)) { data.remove(k) } + } + } + + tsDtMapFLD[appId]=data + tsDtMapFLD=tsDtMapFLD + state.tsDtMap = data +} + +String getTsVal(String key) { + String appId=app.getId() + Map tsMap=tsDtMapFLD[appId] + if(!tsMap) tsMap = (Map)state.tsDtMap ?: [:] + if(key && tsMap && tsMap[key]) { return (String)tsMap[key] } + return sNULL +} + +Integer getLastTsValSecs(String val, Integer nullVal=1000000) { + return (val && getTsVal(val)) ? GetTimeDiffSeconds(getTsVal(val)).toInteger() : nullVal +} + +private void updAppFlag(String key, val) { + Map t0 = atomicState.appFlagsMap + Map data = t0 ?: [:] + if(key) { data[key] = val } + atomicState.appFlagsMap = data +} + +private void remAppFlag(key) { + Map t0 = atomicState.appFlagsMap + Map data = t0 ?: [:] + if(key) { + if(key instanceof List) { + List t = (List)key + t.each { String k-> + if(data.containsKey(k)) { data.remove(k) } + } + } else { + String k = key + if(data.containsKey(k)) { data.remove(k) } + } + } + atomicState.appFlagsMap = data +} + +Boolean getAppFlag(String val) { + Map aMap = atomicState.appFlagsMap + if(val && aMap && aMap[val]) { return aMap[val] } + return false +} + +private void stateMapMigration() { + //Timestamp State Migrations + Map tsItems = ["lastAfterEvtCheck":"lastAfterEvtCheck", "lastNotifMsgDt":"lastNotifMsgDt"] + tsItems.each { String k, String v-> if(state.containsKey(k)) { updTsVal(v, (String)state[k]); state.remove(k) } } + + //App Flag Migrations + Map flagItems = [:] + flagItems.each { String k, String v-> if(state.containsKey(k)) { updAppFlag(v, state[k]); state.remove(k) } } + updAppFlag("stateMapConverted", true) +} + +void settingUpdate(String name, value, String type=sNULL) { + if(name && type) { app?.updateSetting(name, [type: type, value: value]) } + else if (name && type == sNULL) { app?.updateSetting(name, value) } +} + +void settingRemove(String name) { + if(isTrc())logTrace("settingRemove($name)...") + if(name && settings.containsKey(name)) { app?.removeSetting(name) } +} + +static List weekDaysEnum() { return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] } +static Map weekDaysAbrvEnum() { return ["MO":"Monday", "TU":"Tuesday", "WE":"Wednesday", "TH":"Thursday", "FR":"Friday", "SA":"Saturday", "SU":"Sunday"] } +static List monthEnum() { return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] } +static Map daysOfWeekMap() { return ["MON":"Monday", "TUE":"Tuesday", "WED":"Wednesday", "THU":"Thursday", "FRI":"Friday", "SAT":"Saturday", "SUN":"Sunday"] } +static Map weeksOfMonthMap() { return ["1":"1st Week", "2":"2nd Week", "3":"3rd Week", "4":"4th Week", "5":"5th Week"] } +static Map monthMap() { return ["1":"January", "2":"February", "3":"March", "4":"April", "5":"May", "6":"June", "7":"July", "8":"August", "9":"September", "10":"October", "11":"November", "12":"December"] } + +static Map getAlarmTrigOpts() { + return ["armedAway":"Armed Away", "armingAway":"Arming Away Pending exit delay","armedHome":"Armed Home","armingHome":"Arming Home pending exit delay", "armedNight":"Armed Night", "armingNight":"Arming Night pending exit delay","disarmed":"Disarmed", "allDisarmed":"All Disarmed","alerts":"Alerts"] +} + +private static Map getAlarmSystemAlertOptions(){ + return [ + intrusion : "Intrusion Away", + "intrusion-home" : "Intrusion Home", + "intrusion-night": "Intrusion Night", + (sSMOKE) : "Smoke", + (sWATER) : "Water", + rule : "Rule", + cancel : "Alerts cancelled", + arming : "Arming failure" + ] +} + +private static Map getAlarmSystemStatusActions(){ + return [ + armAway : "Arm Away", + armHome : "Arm Home", + armNight : "Arm Night", + disarm : "Disarm", + armRules : "Arm Monitor Rules", + disarmRules : "Disarm Monitor Rules", + disarmAll : "Disarm All", + armAll : "Arm All", + cancelAlerts: "Cancel Alerts" + ] +} + +public Map getActionMetrics() { + Map out = [:] + out.version = appVersionFLD + out.type = (String)settings.actionType ?: sNULL + out.delay = settings.actDelay ?: 0 + out.triggers = (List)settings.triggerEvents ?: [] + out.echoZones = (List)settings.act_EchoZones ?: [] + out.echoDevices = (List)settings.act_EchoDevices ?: [] + out.usingCustomText = (customTxtItems()?.size()) + Integer tDevCnt = 0 + ((List)settings.triggerEvents)?.each { if(settings."trig_${it}" && settings."trig_${it}"?.size()) { tDevCnt = tDevCnt+settings."trig_${it}"?.size() } } + out.triggerDeviceCnt = tDevCnt + return out +} + + +Integer getLastNotifMsgSec() { return getLastTsValSecs("lastNotifMsgDt") } +//Integer getLastChildInitRefreshSec() { return getLastTsValSecs("lastChildInitRefreshDt", 3600) } +//Integer getLastNotifMsgSec() { return !state.lastNotifMsgDt ? 100000 : GetTimeDiffSeconds(state.lastNotifMsgDt, "getLastMsgSec").toInteger() } + +Boolean getOk2Notify() { + Boolean smsOk // = (settings.notif_sms_numbers?.toString()?.length()>=10) + Boolean pushOk // = (settings.notif_send_push) + Boolean pushOver // = (settings.notif_pushover && settings.notif_pushover_devices) + Boolean alexaMsg = ((Boolean)settings.notif_alexa_mobile) + Boolean notifDevsOk = (((List)settings.notif_devs)?.size()) + Boolean daysOk = (List)settings.notif_days ? (isDayOfWeek((List)settings.notif_days)) : true + Boolean timeOk = notifTimeOk() + Boolean modesOk = (List)settings.notif_modes ? (isInMode((List)settings.notif_modes)) : true + Boolean result = true + if(!(smsOk || pushOk || alexaMsg || notifDevsOk || pushOver)) { result = false } + if(!(daysOk && modesOk && timeOk)) { result = false } + if(isDbg()){ + //noinspection GroovyVariableNotAssigned + logDebug("getOk2Notify() RESULT: $result | notifDevsOk: $notifDevsOk |smsOk: $smsOk | pushOk: $pushOk | pushOver: $pushOver | alexaMsg: $alexaMsg || daysOk: $daysOk | timeOk: $timeOk | modesOk: $modesOk") + } + return result +} + +Boolean notifTimeOk() { + Date startTime + Date stopTime + // these are quiet time start/stop + String startType = settings.notif_time_start_type + String stopType = settings.notif_time_stop_type + if(startType && stopType) { + startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null + stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null + } else { return true } + + Date now = new Date() + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L + Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + + if(startTime && stopTime) { + Boolean not = startTime.getTime() > stopTime.getTime() + Boolean isBtwn = !timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) + isBtwn = not ? !isBtwn : isBtwn + if(isTrc())logTrace("NotifTimeOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not ? startTime:stopTime})") + return isBtwn + } else { return true } +} + +// Sends the notifications based on app settings +Boolean sendNotifMsg(String msgTitle, String msg, alexaDev=null, Boolean showEvt=true) { + logTrace("sendNotifMsg() | msgTitle: ${msgTitle}, msg: ${msg}, $alexaDev, showEvt: ${showEvt}") + List sentSrc = [] // ["Push"] + Boolean sent = false + try { + String newMsg = msgTitle+': '+msg + String flatMsg = newMsg.replaceAll("\n", sSPACE) + if(!getOk2Notify()) { + logInfo( "sendNotifMsg: Notification not configured or Message Skipped During Quiet Time ($flatMsg)") + // if(showEvt) { sendNotificationEvent(newMsg) } + } else { + if((List)settings.notif_devs) { + sentSrc.push("Notification Devices") + ((List)settings.notif_devs)?.each { it?.deviceNotification(newMsg) } + sent = true + } + if((Boolean)settings.notif_alexa_mobile && alexaDev) { + alexaDev?.sendAlexaAppNotification(newMsg) + sentSrc.push("Alexa Mobile App") + sent = true + } + if(sent) { + state.lastNotificationMsg = flatMsg + updTsVal("lastNotifMsgDt") + if(isDbg())logDebug("sendNotifMsg: Sent ${sentSrc} (${flatMsg})") + } + } + } catch (ex) { + logError("sendNotifMsg $sentSrc Exception: ${ex}", false, ex) + } + return sent +} + +Boolean isActNotifConfigured() { + if(customMsgRequired() && !(settings.notif_use_custom && settings.notif_custom_message)) { return false } + return ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) +} + +static Integer versionStr2Int(String str) { return str ? str.toString()?.replaceAll("\\.", sBLANK)?.toInteger() : null } +Boolean minVersionFailed() { + try { + Integer t0 = (Integer)parent?.minVersions()["actionApp"] + Integer minDevVer = t0 ?: null + return minDevVer != null && versionStr2Int(appVersionFLD) < minDevVer + } catch (ignored) { + return false + } +} + + +/************************************************************************************************************ + webCoRE Integration +************************************************************************************************************/ +private static String webCoRE_handle(){return'webCoRE'} + +private void webCoRE_init(pistonExecutedCbk=null){ + subscribe(location,webCoRE_handle(),webCoRE_handler) +// if(pistonExecutedCbk)subscribe(location,"${webCoRE_handle()}.pistonExecuted",webCoRE_handler); + if(!webCoREFLD) { + webCoREFLD = [:] + [cbk:true] // pistonExecutedCbk] + } + webCoRE_poll(true) +} + +@Field static final String sLASTWU = 'lastwebCoREUpdDt' +@Field volatile static Map webCoREFLD = [:] + +private void webCoRE_poll(Boolean anow=false){ + Long rUpd = (Long)webCoREFLD.updated + if(rUpd && (now() > (rUpd+300000L))) { + Date aa = new Date(rUpd) + updTsVal(sLASTWU, formatDt(aa)) + } + Integer lastUpd = getLastTsValSecs(sLASTWU) + if (!webCoREFLD || (lastUpd > (3600*24)) || (anow && lastUpd > 300)) { + sendLocationEvent([name: "${webCoRE_handle()}.poll",value:'poll',isStateChange:true,displayed:false]) + updTsVal(sLASTWU) + } +} + +private void webCoRE_execute(String pistonIdOrName,Map data=[:]) { + String i = pistonIdOrName + if(i){sendLocationEvent([name:i,value:app.label,isStateChange:true,displayed:false,data:data])} +} + +private static List webCoRE_list(){ + List ret = ((List)webCoREFLD?.pistons)?.sort {it?.name}?.collect { [(it?.id): it?.aname?.replaceAll("<[^>]*>", sBLANK)] } + return ret +} +/* +private static String getPistonByName(String pistonIdOrName) { + String i=((List)webCoREFLD?.pistons ?: []).find{((String)it.name==pistonIdOrName)||((String)it.id==pistonIdOrName)}?.id +} */ + +private static String getPistonById(String rId) { + Map a = webCoRE_list()?.find { it.containsKey(rId) } + String aaa = (String)a?."${rId}" + return aaa ?: "Refresh to display piston name..." +} + +void webCoRE_handler(evt){ + logTrace("${evt?.name?.toUpperCase()} Event | Value: (${strCapitalize(evt?.value)}) with a delay of ${now() - evt?.date?.getTime()}ms") + switch((String)evt.value){ + case 'pistonList': + getTheLock(sHMLF, "webCoRE_Handler") + Long rUpd = (Long)webCoREFLD.updated + if(rUpd && (now() < (rUpd+1000L))) { + releaseTheLock(sHMLF) + break + } + List p=(List)webCoREFLD?.pistons ?: [] + Map d=(Map)evt.jsonData?:[:] + if(d.id && d.pistons && (d.pistons instanceof List)){ + p.removeAll{it.iid==d.id} + p+=d.pistons.collect{[iid:d.id]+it}.sort{it.name} + Boolean a = (Boolean)webCoREFLD?.cbk + + webCoREFLD = [cbk: a, updated: now(), pistons: p] + releaseTheLock(sHMLF) + + updTsVal(sLASTWU) + } else releaseTheLock(sHMLF) + break + case sPISTNEXEC: + if(valTrigEvt(sPISTNEXEC) && settings.trig_pistonExecuted) { + webcoreEvtHandler(evt) + } + break + } +} + +static String webCore_icon(){ return "https://raw.githubusercontent.com/ady624/webCoRE/master/resources/icons/app-CoRE.png" } + +/****************************************** +| Restriction validators +*******************************************/ + +Double getDevValueAvg(List devs, String attr) { + List vals = devs?.findAll { + Double t = it?.currentValue(attr)?.toDouble() + (t != null /* && t != 0 */ ) }?.collect { t } + return vals?.size() ? (vals.sum()/vals.size()).round(1).toDouble() : null +} + +String getCurrentMode() { + return location?.mode +} + +List getLocationModes(Boolean sorted=false) { + List modes = location?.modes*.name + // log.debug "modes: ${modes}" + return (sorted) ? modes?.sort() : modes +} + +static List getLocationPistons() { + List aa = ((List)webCoREFLD?.pistons ?: []).findAll { it.id }.collect { (String)it.id } + return aa ?: [] +} +/* +private getModeById(mId) { + return location?.getModes()?.find{it?.id == mId} +} */ + +Boolean isInMode(List modes, Boolean not=false) { + Boolean fnd = false + if(modes) { + fnd = (getCurrentMode() in modes) + fnd = (not ? !fnd : fnd) + } + return fnd +} + +Boolean isInAlarmMode(List modes) { + String a = (String)location?.hsmStatus ?: "disarmed" + //return (modes) ? (parent?.getAlarmSystemStatus() in modes) : false + return (modes) ? (a in modes) : false +} +/* +Boolean areAllDevsSame(List devs, String attr, val) { + if(devs && attr) { return (devs?.findAll { it?.currentValue(attr) == val as String }?.size() == devs?.size()) } + return false +} */ + +Boolean allDevAttValsEqual(List devs, String att, val) { + if(devs && att) { + if(val instanceof List) return (devs.findAll { it?.currentValue(att) in val }?.size() == devs?.size()) + else return (devs.findAll { it?.currentValue(att) == val }?.size() == devs?.size()) + } + return false +} + +Boolean anyDevAttValsEqual(List devs, String att, val) { + if(devs && att) { + if(val instanceof List) return (devs.findAll { it?.currentValue(att) in (List)val }?.size() >= 1) + else return (devs.findAll { it?.currentValue(att) == val }?.size() >= 1) + } + return false +} + +Boolean anyDevAttNumValAbove(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() >= 1) : false +} +Boolean anyDevAttNumValBelow(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() >= 1) : false +} +Boolean anyDevAttNumValBetween(List devs, String att, Double low, Double high, Boolean not) { + return (devs && att && (low < high)) ? (devs?.findAll { + Double t = it?.currentValue(att)?.toDouble() + if(not) ( (t < low) || (t > high) ) + else ( (t >= low) && (t <= high) ) }?.size() >= 1) : false +} +Boolean anyDevAttNumValEqual(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() >= 1) : false +} + +Boolean allDevAttNumValsAbove(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsBelow(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsBetween(List devs, String att, Double low, Double high, Boolean not) { + return (devs && att && (low < high)) ? (devs?.findAll { + Double t = it?.currentValue(att)?.toDouble() + if(not) ( (t < low) || (t > high) ) + else ( (t >= low) && (t <= high) ) }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsEqual(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() == devs?.size()) : false +} + +Boolean devAttValEqual(List devs, String devId, String att, val) { + if(devs && att) { + def dev = devs.find { it?.id?.toString() == devId } + if(dev) return (dev?.currentValue(att) == val) + } + return false +} + +static String getAlarmSystemName(Boolean abbr=false) { + return (abbr ? "HSM" : "Hubitat Safety Monitor") +} +/****************************************** +| Time and Date Conversion Functions +*******************************************/ +String formatDt(Date dt, Boolean tzChg=true) { + return dateTimeFmt(dt, "E MMM dd HH:mm:ss z yyyy", tzChg) +} + +String dateTimeFmt(Date dt, String fmt, Boolean tzChg=true) { +// if(!(dt instanceof Date)) { try { dt = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dt?.toString()) } catch(e) { dt = Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } } + SimpleDateFormat tf = new SimpleDateFormat(fmt) + if(tzChg && location?.timeZone) { tf.setTimeZone((TimeZone)location.timeZone) } + return (String)tf.format(dt) +} + +String convToTime(Date dt) { + String newDt = dateTimeFmt(dt, "h:mm a") + if(newDt?.contains(":00 ")) { newDt?.replaceAll(":00 ", sSPACE) } + return newDt +} + +String convToDate(Date dt) { + String newDt = dateTimeFmt(dt, "EEE, MMM d") + return newDt +} + +String convToDateTime(Date dt) { + String t = dateTimeFmt(dt, "h:mm a") + String d = dateTimeFmt(dt, "EEE, MMM d") + return d+', '+t +} + +static Date parseDate(dt) { return Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } + +//static Boolean isDateToday(Date dt) { return (dt && dt.clearTime().compareTo(new Date().clearTime()) >= 0) } +static Boolean isDateToday(Date dt) { return (dt && dt.clearTime() >= new Date().clearTime()) } + +static String strCapitalize(str) { return str ? str.toString().capitalize() : sNULL } + +static String pluralizeStr(List obj, Boolean para=true) { return (obj?.size() > 1) ? "${para ? "(s)": "s"}" : sBLANK } + +String parseFmtDt(String parseFmt, String newFmt, dt) { + Date newDt = Date.parse(parseFmt, dt?.toString()) + return dateTimeFmt(newDt, newFmt) +} + +String getDtNow() { + Date now = new Date() + return formatDt(now) +} + +String epochToTime(Date dt) { + return dateTimeFmt(dt, "h:mm a") +} + +String fmtTime(t, String fmt="h:mm a", Boolean altFmt=false) { + if(!t) return sNULL + Date dt = new Date().parse(altFmt ? "E MMM dd HH:mm:ss z yyyy" : "yyyy-MM-dd'T'HH:mm:ss.SSSZ", t.toString()) + return dateTimeFmt(dt, fmt) +} + +Long GetTimeDiffSeconds(String lastDate, String sender=sNULL) { + try { + if(lastDate?.contains("dtNow")) { return 10000 } + Date lastDt = parseDate(lastDate) // Date.parse("E MMM dd HH:mm:ss z yyyy", lastDate) + Long start = lastDt.getTime() + Long stop = now() + Long diff = Math.round((stop - start) / 1000L) + return diff.abs() + } + catch (ex) { + logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : sBLANK}lastDate: $lastDate): ${ex?.message}", false, ex) + return 10000L + } +} + +Map getDateMap() { + Map m = [:] + Date d = new Date() + m.dayOfYear = dateTimeFmt(d, "DD") + m.dayNameShort = dateTimeFmt(d, "EEE").toUpperCase() + m.dayName = dateTimeFmt(d, "EEEE") + m.day = dateTimeFmt(d, "d") + m.week = dateTimeFmt(d, "W") + m.weekOfYear = dateTimeFmt(d, "w") + m.monthName = dateTimeFmt(d, "MMMMM") + m.month = dateTimeFmt(d, "MM") + m.year = dateTimeFmt(d, "yyyy") + m.hour = dateTimeFmt(d, "hh") + m.minute = dateTimeFmt(d, "mm") + m.second = dateTimeFmt(d, "ss") + return m +} + +Boolean isDayOfWeek(List opts) { + SimpleDateFormat df = new SimpleDateFormat("EEEE") + df?.setTimeZone((TimeZone)location?.timeZone) + String day = df?.format(new Date()) + return opts?.contains(day) +} + +Boolean isMonthOfYear(List opts) { + Map dtMap = getDateMap() + return ( opts?.contains((String)dtMap.monthName) ) +} + +Boolean isTimeBetween(String startTime, String stopTime, Date curTime= new Date()) { + if(!startTime && !stopTime) { return true } + Date st = toDateTime(startTime) + Date et = toDateTime(stopTime) + return timeOfDayIsBetween(st, et, curTime, (TimeZone)location?.timeZone) +} + +/****************************************** +| App Input Description Functions +*******************************************/ + +static Map dimmerLevelEnum() { return [100:"Set Level to 100%", 90:"Set Level to 90%", 80:"Set Level to 80%", 70:"Set Level to 70%", 60:"Set Level to 60%", 50:"Set Level to 50%", 40:"Set Level to 40%", 30:"Set Level to 30%", 20:"Set Level to 20%", 10:"Set Level to 10%"] } + +/* +String unitStr(type) { + switch(type) { + case "temp": + return "\u00b0${getTemperatureScale() ?: "F"}" + case sHUMID: + return "%" + default: + return sBLANK + } +} */ + +String getAppNotifDesc(Boolean hide=false) { + String str = sBLANK + if(isActNotifConfigured()) { + Boolean ok = getOk2Notify() + str += hide ? sBLANK : spanSmBr(strUnder("Send allowed: ") + getOkOrNotSymHTML(ok)) + str += ((List)settings.notif_devs) ? spanSmBr(" ${sBULLET} Notification Device${pluralizeStr((List)settings.notif_devs)} (${((List)settings.notif_devs).size()})") : sBLANK + str += (Boolean)settings.notif_alexa_mobile ? spanSmBr(" ${sBULLET} Alexa Mobile App") : sBLANK + String res = getNotifSchedDesc(true) + str += res ? spanSmBr(res) : sBLANK + } + return str != sBLANK ? str : sNULL +} + +List getQuietDays() { + List allDays = weekDaysEnum() + List curDays = (List)settings.notif_days ?: [] + return allDays.findAll { (!curDays?.contains(it)) } +} + +@SuppressWarnings('GroovyVariableNotAssigned') +String getNotifSchedDesc(Boolean min=false) { + String startType = settings.notif_time_start_type + Date startTime + String stopType = settings.notif_time_stop_type + Date stopTime + List dayInput = (List)settings.notif_days + List modeInput = (List)settings.notif_modes + String str = sBLANK + + if(startType && stopType) { + startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null + stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null + } + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L + Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + Boolean timeOk = notifTimeOk() + Boolean daysOk = dayInput ? isDayOfWeek(dayInput) : true + Boolean modesOk = modeInput ? isInMode(modeInput) : true + Boolean rest = !(daysOk && modesOk && timeOk) + String startLbl = startTime ? epochToTime(startTime) : sBLANK + String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK + String a=s5SPACE+sBULLET+sSPACE + str += (startLbl && stopLbl) ? spanSmBr("${a}Restricted Times: ${startLbl} - ${stopLbl} " + getOkOrNotSymHTML(!timeOk)) : sBLANK + List qDays = getQuietDays() + str += dayInput && qDays ? spanSmBr("${a}Restricted Day${pluralizeStr(qDays, false)}: (${qDays?.join(", ")}) " + getOkOrNotSymHTML(!daysOk)) : sBLANK + str += modeInput ? spanSm("${a}Allowed Mode${pluralizeStr(modeInput, false)}: (${modeInput?.join(", ")}) " + getOkOrNotSymHTML(!modesOk)) : sBLANK + str = str ? spanSmBr(" ${sBULLET} Restrictions Active: " + getOkOrNotSymHTML(rest)) + spanSm(str) : sBLANK + return (str != sBLANK) ? divSm(str, sCLR4D9) : sNULL +} + +String getTriggersDesc(Boolean hideDesc=false, Boolean addFoot=true) { + Boolean confd = triggersConfigured() + List setItem = (List)settings.triggerEvents + String sPre = sTRIG + if(confd && setItem?.size()) { + if(!hideDesc) { + String str = spanSmBldBr("Triggers${!addFoot ? " for ("+(String)buildActTypeEnum()."${(String)settings.actionType}" + ")" : sBLANK}:", sNULL) + String btstr = s4SPACE+sBULLETINV+sSPACE + setItem.each { String evt-> + String adder = sBLANK + String eH = sPre+evt + List myL = (List)settings."${eH}" + String tstr = s4SPACE+spanSmBld(sPLUS)+sSPACE + //noinspection GroovyFallthrough + switch(evt) { + case sSCHED: + String schedTyp = settings."${eH}_type" ?: sNULL + str += spanSmBr(" ${sBULLET} ${strUnder(evt?.capitalize())}${schedTyp ? " (${schedTyp})" : ""}") + if(schedTyp == sRECURRING) { + str += settings."${eH}_recurrence" ? spanSmBr(btstr+"Recurrence: (${settings."${eH}_recurrence"})") : sBLANK + str += settings."${eH}_time" ? spanSmBr(btstr+"Time: (${fmtTime(settings."${eH}_time")})") : sBLANK + str += settings."${eH}_weekdays" ? spanSmBr(btstr+"Week Days: (${settings."${eH}_weekdays"?.join(",")})") : sBLANK + str += settings."${eH}_daynums" ? spanSmBr(btstr+"Days of Month: (${settings."${eH}_daynums"?.size()})") : sBLANK + str += settings."${eH}_weeks" ? spanSmBr(btstr+"Weeks of Month: (${settings."${eH}_weeks"?.join(",")})") : sBLANK + str += settings."${eH}_months" ? spanSmBr(btstr+"Months: (${settings."${eH}_months"?.join(",")})") : sBLANK + } + if(schedTyp == sONETIME) { + str += settings."${eH}_time" ? spanSmBr(btstr+"Time: (${fmtTime(settings."${eH}_time")}))") : sBLANK + } + if(schedTyp in [sCSUNRISE, sCSUNSET]) { + str += settings."${eH}_sunState_offset" ? spanSmBr(btstr+"Offset: (${settings."${eH}_sunState_offset"})") : sBLANK + } + break + case sALRMSYSST: + str += spanSmBr(" ${sBULLET} ${strUnder(evt?.capitalize())} (${getAlarmSystemName(true)})" + myL ? " (${myL?.size()} Selected)" : sBLANK) + if ("alerts" in myL) str += (List)settings."${eH}_events" ? spanSmBr(btstr+"Alert Events: (${(List)settings."${eH}_events"})") : sBLANK + str += (Boolean)settings."${eH}_once" ? spanSmBr(btstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK + str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(btstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK + break + case sPISTNEXEC: + case sMODE: +// case "scene": + String typ = evt == sMODE ? "Mode" : "Piston" + str += myL ? spanSmBr(" ${sBULLET} "+ strUnder(typ) + pluralizeStr(myL, false) + " (${myL?.size()})") : sBLANK + str += (Boolean)settings."${eH}_once" ? spanSmBr(btstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK + str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(btstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK + break + case sPUSHED: + case sRELEASED: + case sHELD: + case sDBLTAP: + adder = "Button " + default: + str += spanSmBr(" ${sBULLET} ${adder}${strUnder(evt?.capitalize())}${myL ? " (${myL?.size()} Device" + pluralizeStr(myL, false) + ")" : sBLANK}") + String t_cmd = (String)settings."${eH}_cmd" + if(t_cmd in numOpts()) { + if (t_cmd in [sBETWEEN, sNBETWEEN]) { + str += spanSmBr(tstr+"Trigger Value ${t_cmd.capitalize()}: (${settings."${eH}_low"} - ${settings."${eH}_high"})") + } else { + str += (t_cmd == sABOVE && settings."${eH}_high"!=null) ? spanSmBr(tstr+"Trigger Value Above: (${settings."${eH}_high"})") : sBLANK + str += (t_cmd == sBELOW && settings."${eH}_low"!=null) ? spanSmBr(tstr+"Trigger Value Below: (${settings."${eH}_low"})") : sBLANK + str += (t_cmd == sEQUALS && settings."${eH}_equal"!=null) ? spanSmBr(tstr+"Trigger Value Equals: (${settings."${eH}_equal"})") : sBLANK + } + } else { + str += t_cmd ? spanSmBr(tstr+"Trigger State: (${t_cmd})") : sBLANK + } + str += settings."${eH}_nums" ? spanSmBr(tstr+"Button Numbers: ${settings."${eH}_nums"}") : sBLANK + str += (Integer)settings."${eH}_after"!=null ? spanSmBr(tstr+"Only After: (${settings."${eH}_after"} sec)") : sBLANK + str += (Integer)settings."${eH}_after_repeat" ? spanSmBr(tstr+"Repeat Every: (${settings."${eH}_after_repeat"} sec)") : sBLANK + str += (Integer)settings."${eH}_after_repeat_cnt" ? spanSmBr(tstr+"Repeat Count: (${settings."${eH}_after_repeat_cnt"})") : sBLANK + str += (Boolean)settings."${eH}_all" ? spanSmBr(tstr+"Require All: (${settings."${eH}_all"})") : sBLANK + str += (Boolean)settings."${eH}_ign_empty_type" ? spanSmBr(tstr+"Ignore Empty Event Type: (${settings."${eH}_ign_empty_type"})") : sBLANK + str += (Boolean)settings."${eH}_once" ? spanSmBr(tstr+"Once a Day: (${(Boolean)settings."${eH}_once"})") : sBLANK + str += (Integer)settings."${eH}_wait"!=null ? spanSmBr(tstr+"Wait (Sec): (${(Integer)settings."${eH}_wait"})") : sBLANK + str += ((String)settings."${eH}_txt" || (String)settings."${eH}_after_repeat_txt") ? spanSmBr(tstr+"Custom Responses:") : sBLANK + str += (String)settings."${eH}_txt" ? spanSmBr(s3SPACE+tstr+"Events: (${((String)settings."${eH}_txt")?.tokenize(";")?.size()} Items)") : sBLANK + str += (String)settings."${eH}_after_repeat_txt" ? spanSmBr(s3SPACE+tstr+"Repeats: (${((String)settings."${eH}_after_repeat_txt")?.tokenize(";")?.size()} Items)") : sBLANK + break + } + } + str += addFoot ? inputFooter(sTTM) : sBLANK + return str + } else { return addFoot ? inputFooter(sTTM) : sBLANK } + } else { + return addFoot ? inputFooter("Tap to configure (Required!)", sCLRRED, true) : sBLANK + } +} + +String getOverallDesc() { + Map condStatus = conditionStatus() + return spanSmBld("Action Condition Status: ") + spanSmBr( ((Boolean)condStatus.ok ? "Active " : "Inactive ") + getOkOrNotSymHTML((Boolean)condStatus.ok)) +} + +String getConditionsDesc(Boolean addFoot=true) { + Boolean confd = conditionsConfigured() + String sPre = "cond_" + String str = sBLANK + String btstr = s4SPACE+sBULLETINV+sSPACE + if(confd) { + str = getOverallDesc() + str += spanSmBr(" ${sBULLET} " + spanSmBld("${reqAllCond() ? "All Conditions Required" : "Any Condition Allowed"}")) + if(timeCondConfigured()) { + str += spanSmBr(" ${sBULLET} Time Between Allowed: " + getOkOrNotSymHTML(timeCondOk())) + str += spanSmBr(btstr+"${getTimeCondDesc(false)}") + } + if(dateCondConfigured()) { + str += spanSmBr(" ${sBULLET} Date:") + str += (List)settings.cond_days ? spanSmBr(btstr+"Days Allowed: ${(List)settings.cond_days} " + getOkOrNotSymHTML(isDayOfWeek((List)settings.cond_days))) : sBLANK + str += (List)settings.cond_months ? spanSmBr(btstr+"Months Allowed: ${(List)settings.cond_months} " + getOkOrNotSymHTML(isMonthOfYear((List)settings.cond_months))) : sBLANK + } + + Boolean mC = locationModeConfigured() + Boolean aC = locationAlarmConfigured() + if(aC || mC) { + str += spanSmBr(" ${sBULLET} Location: " + getOkOrNotSymHTML(locationCondOk())) + if(aC) { + String a = (String)location?.hsmStatus ?: "disarmed" + List myL = (List)settings.cond_alarmSystemStatus + str += myL ? spanSmBr(btstr+"Alarm Mode ${a} in: ${myL} " + getOkOrNotSymHTML(isInAlarmMode(myL))) : sBLANK + } + if(mC) { + Boolean not = ((String)settings.cond_mode_cmd == "not") + str += (List)settings.cond_mode ? spanSmBr(btstr+"Mode ${getCurrentMode()} (${not ? "not in" : "in"}): ${(List)settings.cond_mode} " + getOkOrNotSymHTML(isInMode((List)settings.cond_mode, not))) : sBLANK + } + } + + if(deviceCondConfigured()) { + List devConds = lDATTSTR + lDATTNUM + devConds.each { String evt-> + String eH = sPre+evt + if(devCondConfigured(evt)) { + Boolean condOk = false + if(evt in lDATTSTR) { condOk = checkDeviceCondOk(evt) } + else if(evt in lDATTNUM) { condOk = checkDeviceNumCondOk(evt) } + + List devs = settings."${eH}" ?: null + if(devs){ + List myV = [] + if(!addFoot) devs.each { dev -> myV.push(dev?.currentValue(evt)?.toString()) } + str += spanSmBr(" ${sBULLET} ${evt?.capitalize()} (${settings."${eH}"?.size()}) ${!addFoot ? myV : sBLANK} " + getOkOrNotSymHTML(condOk)) + } + + String a = btstr+"Desired Value: " + String aG = (Boolean)settings."${eH}_avg" ? "(Avg)" : sBLANK + String cmd = settings."${eH}_cmd" ?: sNULL + if(cmd in numOpts()) { + def cmdLow = settings."${eH}_low"!=null ? settings."${eH}_low" : null + def cmdHigh = settings."${eH}_high"!=null ? settings."${eH}_high" : null + def cmdEq = settings."${eH}_equal"!=null ? settings."${eH}_equal" : null + String aU = attUnit(evt) + ')' + aG + str += (cmd == sEQUALS && cmdEq) ? spanSmBr(a+"( =${cmdEq}"+aU) : sBLANK + str += (cmd in [sBETWEEN, sNBETWEEN] && cmdLow && cmdHigh) ? spanSmBr(a+cmd.capitalize()+" (${cmdLow}-${cmdHigh}"+aU) : sBLANK + str += (cmd == sABOVE && cmdHigh) ? spanSmBr(a+"( >${cmdHigh}"+aU) : sBLANK + str += (cmd == sBELOW && cmdLow) ? spanSmBr(a+"( <${cmdLow}"+aU) : sBLANK + } else { + str += cmd ? spanSmBr(a+"(${cmd})" + aG) : sBLANK + } + str += (Boolean)settings."${eH}_all" ? spanSmBr(btstr+"Require All: (${settings."${eH}_all"})") : sBLANK + } + } + } + str += addFoot ? inputFooter(sTTM) : sBLANK + } else { + str += addFoot ? inputFooter(sTTC, sCLRGRY, true) : sBLANK + } + return str +} + +@SuppressWarnings('GroovyFallthrough') +static String attUnit(String attr) { + switch(attr) { + case sHUMID: + case sLEVEL: + case sBATT: + return " percent" + case sTEMP: + return " degrees" + case "illuminance": + return " lux" + case sPOWER: + return " watts" + default: + return sBLANK + } +} + +Map getZoneStatus() { + List echoZones = (List)settings.act_EchoZones ?: [] + Map res = [:] + if(echoZones.size()) { + Map allZones = getZones() + echoZones.each { String k -> if(allZones?.containsKey(k)) { res[k] = allZones[k] } } + } + return res +} + +String getZoneVolDesc(zone, MapvolMap) { + String str = sBLANK + String k = (String)zone.key + str += spanSm(" ${sBULLET} ${zone?.value?.name} ${zone?.value?.active == true ? spanSm(" (Active)", sCLRGRN2) : spanSm(" (Inactive)", sCLRGRY)}") + if((Boolean)settings.act_EchoZones_vol_per_zone && volMap && volMap[k]) { + String btstr = s4SPACE+sBULLETINV+sSPACE + str += volMap[k].change ? lineBr() + spanSm(btstr+"New Volume: ${volMap[k].change}") : sBLANK + str += volMap[k].restore ? lineBr() + spanSm(btstr+"Restore Volume: ${volMap[k].restore}") : sBLANK + } + return str +} + +String getActionDesc(Boolean addFoot=true) { + Boolean confd = executionConfigured() + String str = sBLANK + Map actMap = (Map)state.actionExecMap + Map znVolMap = actMap && actMap.config && actMap.config.zoneVolume ? (Map)actMap.config.zoneVolume : null + str += addFoot ? spanSmBr(strUnder("Action Type:")) : sBLANK + str += addFoot ? spanSmBr(" ${sBULLET} " + (String)buildActTypeEnum()."${(String)settings.actionType}") + lineBr() : sBLANK + if((String)settings.actionType && confd) { + Boolean isTierAct = isTierAction() + List eDevs = parent?.getDevicesFromList(settings.act_EchoDevices) + Map zones = getZoneStatus() + String tierDesc = isTierAct ? getTierRespDesc() : sNULL + String tierStart = isTierAct ? actTaskDesc(sACT_START) : sNULL + String tierStop = isTierAct ? actTaskDesc(sACT_STOP) : sNULL + str += zones?.size() ? spanSmBr(strUnder("Echo Zones:")) : sBLANK + str += zones?.size() ? spanSmBr(zones?.collect { getZoneVolDesc(it, znVolMap) }?.join(sLINEBR)) + (eDevs?.size() ? sLINEBR : sBLANK) : sBLANK + str += eDevs?.size() ? spanSm(strUnder("Alexa Devices:")) + (zones?.size() ? spanSm(" (Inactive Zone Default)", sCLRGRY) : sBLANK) + lineBr() + spanSmBr(eDevs?.collect { " ${sBULLET} ${it?.displayName?.toString()?.replace("Echo - ", sBLANK)}" }?.join(sLINEBR)) : sBLANK + str += tierDesc ? sLINEBR + spanSm(tierDesc) + (tierStart || tierStop ? sBLANK : sLINEBR) : sBLANK + str += tierStart ? spanSmBr(tierStart) : sBLANK + str += tierStop ? spanSmBr(tierStop) : sBLANK + str += settings.act_volume_change != null ? spanSmBr(" - New Volume: (${settings.act_volume_change})") : sBLANK + str += settings.act_volume_restore != null ? spanSmBr(" - Restore Volume: (${settings.act_volume_restore})") : sBLANK + str += settings.act_delay ? spanSmBr("Delay: (${settings.act_delay})") : sBLANK + + String mT = (String)settings.actionType + if(mT in [sSPEAK, sSPEAKI, sSPEAKP, sSPEAKIP]) mT = sSPEAK + if(mT in [sANN, sANNI]) mT = sANN + str += (mT in [sSPEAK, sANN] || isTierAct) && (String)settings."act_${mT}_txt" ? spanSmBr("Using Default Response: (True)") : sBLANK + + String trigTasks = !isTierAct ? actTaskDesc(sACT) + lineBr() : sNULL + str += trigTasks ? spanSm(trigTasks) : sBLANK + str += addFoot ? inputFooter(sTTM) : sBLANK + } + str += !confd && addFoot ? inputFooter("Tap to configure (Required!)", sCLRRED, true) : sBLANK + return divSm(str.replaceAll("\n\n\n", "\n\n"), sCLR4D9) +} + +@SuppressWarnings('GroovyVariableNotAssigned') +String getTimeCondDesc(Boolean addPre=true) { + Date startTime + Date stopTime + String startType = settings.cond_time_start_type + String stopType = settings.cond_time_stop_type + if(startType && stopType) { + startTime = startType == sTIME && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null + stopTime = stopType == sTIME && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null + } + + String startLbl1 = sBLANK + String stopLbl1 = sBLANK + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L + Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + startLbl1 = startType.capitalize() + sSPACE + startoffset ? "with offset " : sBLANK + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + stopLbl1 = stopType.capitalize() + sSPACE + stopoffset ? "with offset " : sBLANK + } + } + String startLbl = startTime ? epochToTime(startTime) : sBLANK + String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK + + return startLbl && stopLbl ? "${addPre ? "Time Condition:\n" : sBLANK}(${startLbl1}${startLbl} - ${stopLbl1}${stopLbl})" : inactFoot(sTTC) +} + +static String getInputToStringDesc(inpt, addSpace = null) { + Integer cnt = 0 + String str = sBLANK + if(inpt) { + inpt.sort().each { item -> + cnt = cnt+1 + str += item ? (((cnt < 1) || (inpt?.size() > 1)) ? "\n ${item}" : "${addSpace ? s6SPACE : sBLANK}${item}") : sBLANK + } + } + //log.debug "str: $str" + return (str != sBLANK) ? str : sNULL +} + +static String randomString(Integer len) { + def pool = ["a".."z", 0..9].flatten() + Random rand = new Random(new Date().getTime()) + def randChars = (0..len).collect { pool[rand.nextInt(pool.size())] } + // logDebug("randomString: ${randChars?.join()}") + return randChars.join() +} + +static def getRandomItem(List items) { + List list = [] // new ArrayList() + items?.each { list.add(it) } + return list.get(new Random().nextInt(list.size())) +} + +// public String gitBranch() { +// if(gitBranchFLD == sNULL) { gitBranchFLD = parent?.gitBranch() } +// return (String)gitBranchFLD +// } + +Boolean showChgLogOk() { return ((Boolean)state.isInstalled && !(Boolean)state.shownChgLog) } + +static String getAppImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/${imgName}.png" } + +static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } + +static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

    ${i ? " " : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}

    " } + +static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

    ${i ? " " : sBLANK} ${t?.replaceAll("\n", "
    ")}

    ${st ? "${st}" : sBLANK}" } + +static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
    " : sBLANK}${bold ? "" : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : ""}${color ? "
    " : ""}" } + +static String inTS1(String str, String img = sNULL, String clr=sNULL) { return spanSmBldUnd(str, clr, img) } +static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str?.replaceAll("\n", " ")?.replaceAll("
    ", " "), und), clr, img) } + +// Root HTML Objects +static String sectHead(String str, String img = sNULL) { return str ? "

    " + spanImgStr(img) + span(str, "darkorange", sNULL, true) + "

    " + "
    " : sBLANK } +static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } +static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
    ${str}
    ${br ? sLINEBR : sBLANK}" : sBLANK } +static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } +static String divImgStr(String str, String img=sNULL) { return str ? div(img ? spanImg(img) + span(str) : str) : sBLANK } +static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } +static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } +static String htmlLine(String color=sCLR4D9) { return "
    " } +static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } +static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? ((noBr ? sBLANK : lineBr()) + divSmBld(str, clr)) : sBLANK } +static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } +static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } +static String optPrefix() { return spanSm(" (Optional)", "violet") } +// + +// Custom versions of the root objects above +static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } +static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } +static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } +static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } +static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } +static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } +static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } +static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } +static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } +static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } +static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } +static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } + + +static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } +static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } +static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } +static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } +static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } +static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } +static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } + +static String textDonateUrl() { return "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HWBN4LB9NMHZ4" } +def appFooter() { + section() { + paragraph htmlLine("orange") + paragraph "
    Echo Speaks


    Please consider donating if you find this integration useful.
    " + } +} + +static String documentationUrl() { return "https://tonesto7.github.io/echo-speaks-docs" } +static String videoUrl() { return "https://www.youtube.com/watch?v=wQPPlTFaGb4&ab_channel=SimplySmart123%E2%9C%85" } + +static String bulletItem(String inStr, String strVal) { return "${inStr == sBLANK ? sBLANK : "\n"} \u2022 ${strVal}" } +static String dashItem(String inStr, String strVal, Boolean newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : "\n"} - ${strVal}" } + +Integer stateSize() { + String j = new JsonOutput().toJson((Map)state) + return j.length() +} + +Integer stateSizePerc() { return (int) ((stateSize() / 100000)*100).toDouble().round(0) } +/* +String debugStatus() { return !settings.appDebug ? "Off" : "On" } +String deviceDebugStatus() { return !settings.childDebug ? "Off" : "On" } +Boolean isAppDebug() { return (settings.appDebug == true) } +Boolean isChildDebug() { return (settings.childDebug == true) } + +String getAppDebugDesc() { + String str = sBLANK + str += isAppDebug() ? "App Debug: (${debugStatus()})" : sBLANK + str += isChildDebug() && str != sBLANK ? "\n" : sBLANK + str += isChildDebug() ? "Device Debug: (${deviceDebugStatus()})" : sBLANK + return (str != sBLANK) ? str : sNULL +}*/ + +private addToLogHistory(String logKey, String data, Integer max=10) { + Boolean ssOk = true // (stateSizePerc() > 70) + + getTheLock(sHMLF, "addToHistory(${logKey})") + + + List eData = (List)getMemStoreItem(logKey) + if(!(eData.find { it?.data == data })) { + eData.push([dt: getDtNow(), data: data]) + Integer lsiz = eData.size() + if (!ssOk || lsiz > max) { + eData = eData.drop((lsiz - max)) + } + updMemStoreItem(logKey, eData) + } + releaseTheLock(sHMLF) +} + +public Map getLogConfigs() { + return [ + info: (Boolean) settings.logInfo, + warn: (Boolean) settings.logWarn, + error: (Boolean) settings.logError, + debug: (Boolean) settings.logDebug, + trace: (Boolean) settings.logTrace + ] +} + +public void enableDebugLog() { settingUpdate("logDebug", sTRUE, sBOOL); logInfo("Debug Logs Enabled From Main App...") } +public void disableDebugLog() { settingUpdate("logDebug", sFALSE, sBOOL); logInfo("Debug Logs Disabled From Main App...") } +public void enableTraceLog() { settingUpdate("logTrace", sTRUE, sBOOL); logInfo("Trace Logs Enabled From Main App...") } +public void disableTraceLog() { settingUpdate("logTrace", sFALSE, sBOOL); logInfo("Trace Logs Disabled From Main App...") } + +private void logDebug(String msg) { if(isDbg()) { log.debug logPrefix(msg, "purple") } } +private void logInfo(String msg) { if((Boolean)settings.logInfo != false) { log.info sSPACE + logPrefix(msg, "#0299b1") } } +private void logTrace(String msg) { if(isTrc()) { log.trace logPrefix(msg, sCLRGRY) } } +private void logWarn(String msg, Boolean noHist=false) { if((Boolean)settings.logWarn != false) { log.warn sSPACE + logPrefix(msg, sCLRORG) }; if(!noHist) { addToLogHistory("warnHistory", msg, 15) } } + +private Boolean isTrc(){ return (Boolean)settings.logTrace } +private Boolean isDbg(){ return (Boolean)settings.logDebug } + +void logError(String msg, Boolean noHist=false, ex=null) { + if((Boolean)settings.logError != false) { + log.error logPrefix(msg, sCLRRED) + String a + try { + if (ex) a = getExceptionMessageWithLine(ex) + } catch (ignored) { + } + if(a) log.error logPrefix(a, sCLRRED) + } + if(!noHist) { addToLogHistory("errorHistory", msg, 15) } +} + +static String logPrefix(String msg, String color = sNULL) { + return span("Action (v" + appVersionFLD + ") | ", sCLRGRY) + span(msg, color) +} + +Map getLogHistory() { + getTheLock(sHMLF, "getLogHistory") + + List warn = (List)getMemStoreItem("warnHistory") + List errs = (List)getMemStoreItem("errorHistory") + releaseTheLock(sHMLF) + return [ warnings: []+warn, errors: []+errs ] +} + +/* +private void clearLogHistory() { + getTheLock(sHMLF, "clearLogHistory") + + updMemStoreItem("warnHistory", []) + updMemStoreItem("errorHistory", []) + releaseTheLock(sHMLF) +} */ + +// FIELD VARIABLE FUNCTIONS +private void updMemStoreItem(String key, val) { + String appId = app.getId() + Map memStore = historyMapFLD[appId] ?: [:] + memStore[key] = val + historyMapFLD[appId] = memStore + historyMapFLD = historyMapFLD + // log.debug("updMemStoreItem(${key}): ${memStore[key]}") +} + +private getMemStoreItem(String key, defVal=[]){ + String appId = app.getId() + Map memStore = historyMapFLD[appId] ?: [:] + return memStore[key] ?: defVal +} + +/* +// Memory Barrier +@Field static java.util.concurrent.Semaphore theMBLockFLD=new java.util.concurrent.Semaphore(0) +static void mb(String meth=sNULL){ + if((Boolean)theMBLockFLD.tryAcquire()){ + theMBLockFLD.release() + } +} */ + +@Field static final String sHMLF = 'theHistMapLockFLD' +@Field static Semaphore histMapLockFLD = new Semaphore(1) + +private Integer getSemaNum(String name) { + if(name==sHMLF) return 0 + log.warn "unrecognized lock name..." + return 0 + // Integer stripes=22 + // if(name.isNumber()) return name.toInteger()%stripes + // Integer hash=smear(name.hashCode()) + // return Math.abs(hash)%stripes + // log.info "sema $name # $sema" +} + +Semaphore getSema(Integer snum){ + switch(snum) { + case 0: return histMapLockFLD + default: log.error "bad hash result $snum" + return null + } +} + +@Field volatile static Map lockTimesFLD = [:] +@Field volatile static Map lockHolderFLD = [:] + +void getTheLock(String qname, String meth=sNULL, Boolean longWait=false) { + Long waitT = longWait ? 1000L : 60L +// Boolean wait = false + Integer semaNum = getSemaNum(qname) + String semaSNum = semaNum.toString() + Semaphore sema = getSema(semaNum) + while(!((Boolean)sema.tryAcquire())) { + // did not get the lock + Long timeL = lockTimesFLD[semaSNum] + if(timeL == null){ + timeL = now() + lockTimesFLD[semaSNum] = timeL + lockTimesFLD = lockTimesFLD + } + if(devModeFLD) log.warn "waiting for ${qname} ${semaSNum} lock access, $meth, long: $longWait, holder: ${(String)lockHolderFLD[semaSNum]}" + pauseExecution(waitT) +// wait = true + if((now() - timeL) > 30000L) { + releaseTheLock(qname) + if(devModeFLD) log.warn "overriding lock $meth" + } + } + lockTimesFLD[semaSNum] = (Long)now() + lockTimesFLD = lockTimesFLD + lockHolderFLD[semaSNum] = "${app.getId()} ${meth}".toString() + lockHolderFLD = lockHolderFLD +// return wait +} + +void releaseTheLock(String qname){ + Integer semaNum=getSemaNum(qname) + String semaSNum=semaNum.toString() + Semaphore sema=getSema(semaNum) + lockTimesFLD[semaSNum]=null + lockTimesFLD=lockTimesFLD + lockHolderFLD[semaSNum]=sNULL + lockHolderFLD=lockHolderFLD + sema.release() +} + +static String convMusicProvider(String prov) { + switch (prov) { + case "My Library": + return "CLOUDPLAYER" + case "Amazon Music": + return "AMAZON_MUSIC" + case "Apple Music": + return "APPLE_MUSIC" + case "TuneIn": + return "TUNEIN" + case "Pandora": + return "PANDORA" + case "SiriusXM": + return "SIRIUSXM" + case "Spotify": + return "SPOTIFY" + case "Tidal": + return "TIDAL" + case "iHeartRadio": + return "I_HEART_RADIO" + default: return sBLANK + } +} + +/************************************************ + SEQUENCE TEST LOGIC +*************************************************/ + +def searchTuneInResultsPage() { + return dynamicPage(name: "searchTuneInResultsPage", uninstall: false, install: false) { + Map results = parent.executeTuneInSearch((String)settings.tuneinSearchQuery) + section(sectHead("Search Results: (Query: ${(String)settings.tuneinSearchQuery})")) { + if(results?.browseList && results?.browseList?.size()) { + results?.browseList?.eachWithIndex { item, i-> + if(i < 25) { + if(item?.browseList != null && item?.browseList?.size()) { + item?.browseList?.eachWithIndex { item2, i2-> + String str = sBLANK + str += "ContentType: (${item2?.contentType})" + str += "\nId: (${item2?.id})" + str += "\nDescription: ${item2?.description}" + href "searchTuneInResultsPage", title: pTS((String)item2?.name?.take(75), (String)item2?.image), description: str + } + } else { + String str = sBLANK + str += "ContentType: (${item?.contentType})" + str += "\nId: (${item?.id})" + str += "\nDescription: ${item?.description}" + href "searchTuneInResultsPage", title: pTS( (String)((String)item?.name)?.take(75), (String)item?.image), description: str + } + } + } + } else { paragraph "No Results found..." } + } + } +} + +private getColorName(desiredColor, level=null) { + String desC = desiredColor?.toLowerCase() + for (color in colorSettingsListFLD) { + if (((String)color.name)?.toLowerCase() == desC) { + Integer hue = Math.round((Integer)color.h / 3.6).toInteger() + Integer lvl = level ?: (Integer)color.l + return [hue: hue, saturation: (Integer)color.s, level: lvl] + } + } + logWarn("getColorName ${desC} not found") +} + +// if there is no colorMaps, then flash lights +private void startFlashLights(String p, Integer cycles, Integer cycleT, Map color1Map=null, Map color2Map=null) { + logDebug("startFlashLights ${p} | cycles: ${cycles} | cycle time: ${cycleT} | color1Map: ${color1Map} | color2Map: ${color2Map}") + Map data = [type: p, first: true, cycles: cycles, cycleT: cycleT, color1Map: color1Map, colorMap2: color2Map ] + flashLights(data) +} + +private void flashLights(Map data) { + String msg = "flashLights | " + if(devModeFLD) logTrace(msg+"state: ${data}") + + String p = data?.type + if(!data || !p) return + List devs = (List)settings."${p}lights" + Integer cT = (Integer)data.cycleT + Integer cS = (Integer)data.cycles + if(!devs || !cT || !cS || cT < 1 || cS < 1) return + if(cT > 60) data.cycleT=60 + if(cS > 200) data.cycles = 200 + + if((Boolean)data.first) { + data.first = false + logTrace(msg+"capturing state") + captureLightState(devs, p) + if(data.color1Map && data.color2Map) { + data.state = "a" + devs*.on() + } else data.state = "off" + data.cycle = 1 + } + + switch(data.state){ + case "off": + data.state = "on"; devs*.on() + break + case "on": + data.state = "off"; devs*.off() + data.cycle = data.cycle + 1 + break + case "a": + data.state = "b"; devs*.setColor(data.color1Map) + break + case "b": + data.state = "a"; devs*.setColor(data.color2Map) + data.cycle = data.cycle + 1 + break + } + if(data.cycle <= cS ) { + runIn(cT, "flashLights", [data: data]) + } else { + logTrace(msg+"restoring state") + restoreLightState(devs, p) + logDebug(msg+"Ending") + } +} + +private restoreLights(data) { + String p = data?.type ?: sNULL + if(p && settings."${p}lights") { restoreLightState((List)settings."${p}lights", p) } +} + +void captureLightState(List devs, String p) { + String e = p+sLRM + String msg = "captureLightState | ${e} | " + logTrace(msg+"${devs}") + Map sMap = [:] + if(devs && p) { + devs.each { dev-> + String dId = dev?.id + sMap[dId] = [:] + [sSWITCH, sLEVEL, "hue", "saturation", "colorTemperature", "color"].each { String att -> + if(dev?.hasAttribute(att)) { sMap[dId]."${att}" = dev?.currentValue(att) } + } + } + state."${e}" = sMap + } + if(devModeFLD) logDebug(msg+"sMap: $sMap") + //logDebug(msg+ "sMap: $sMap") +} + +void restoreLightState(List devs, String p) { + String e = p+sLRM + String msg = "restoreLightState | ${e} | " + logTrace(msg+"${devs}") + if(devs && p) { + Boolean doP = true // do pauses + Long paus = 100L + Boolean dco = false // disable command optimization + + Map sMap = state."${e}" + if(devModeFLD) logDebug(msg+"sMap: $sMap") + if(sMap?.size()) { + devs.each { dev-> + String dId = dev?.id + if(sMap.containsKey(dId)) { + Map sD = sMap[dId] + if(devModeFLD) logDebug(msg+"${sD}") + Boolean wOn = (sD.switch == "on") + Boolean isOn = (dev.currentValue(sSWITCH) == "on") + Boolean chgLvl = sD.level ? (dev.currentValue(sLEVEL) != sD.level) : false + Boolean chgHSL = (sD.level && sD.saturation && sD.hue && dev.hasCommand("setColor")) + Boolean chgCtemp = sD.colorTemperature ? (dev.currentValue("colorTemperature") != sD.colorTemperature) : false + // CHECK IF ANYTHING NEEDS CHANGES? + if((sD.level || sD.colorTemperature) && (dco || !isOn)) { + dev.on() + isOn = true + if(doP) pauseExecution(paus) + } + if(sD.level) { + if(chgHSL) { + dev?.setColor([h: sD.hue, s: sD.saturation, l: sD.level]) + if(doP) pauseExecution(paus) + } else{ + if((dco || chgLvl) && dev.hasCommand("setLevel")) { + dev?.setLevel(sD.level) + if(!wOn && doP) pauseExecution(paus) + } + } + } + if(sD.colorTemperature) { + if((dco || chgCtemp) && dev.hasCommand("setColorTemperature")) { + dev?.setColorTemperature(sD.colorTemperature) + if(!wOn && doP) pauseExecution(paus) + } + } + if(dco || (wOn != isOn)) dev?."${sD.switch}"() + } else logWarn(msg+"no saved state for device found") + } + } else logWarn(msg+"no saved state found") + state.remove(e) + } +} + +@Field static final List colorSettingsListFLD = [ + [name: "Soft White", rgb: "#B6DA7C", h: 83, s: 44, l: 67], [name: "Warm White", rgb: "#DAF17E", h: 51, s: 20, l: 100], [name: "Very Warm White", rgb: "#DAF17E", h: 51, s: 60, l: 51], + [name: "Daylight White", rgb: "#CEF4FD", h: 191, s: 9, l: 90], [name: "Daylight", rgb: "#CEF4FD", h: 191, s: 9, l: 90], [name: "Cool White", rgb: "#F3F6F7", h: 187, s: 19, l: 96], + [name: "White", rgb: "#FFFFFF", h: 0, s: 0, l: 100], [name: "Alice Blue", rgb: "#F0F8FF", h: 208, s: 100, l: 97], [name: "Antique White", rgb: "#FAEBD7", h: 34, s: 78, l: 91], + [name: "Aqua", rgb: "#00FFFF", h: 180, s: 100, l: 50], [name: "Aquamarine", rgb: "#7FFFD4", h: 160, s: 100, l: 75], [name: "Azure", rgb: "#F0FFFF", h: 180, s: 100, l: 97], + [name: "Beige", rgb: "#F5F5DC", h: 60, s: 56, l: 91], [name: "Bisque", rgb: "#FFE4C4", h: 33, s: 100, l: 88], [name: "Blanched Almond", rgb: "#FFEBCD", h: 36, s: 100, l: 90], + [name: "Blue", rgb: "#0000FF", h: 240, s: 100, l: 50], [name: "Blue Violet", rgb: "#8A2BE2", h: 271, s: 76, l: 53], [name: "Brown", rgb: "#A52A2A", h: 0, s: 59, l: 41], + [name: "Burly Wood", rgb: "#DEB887", h: 34, s: 57, l: 70], [name: "Cadet Blue", rgb: "#5F9EA0", h: 182, s: 25, l: 50], [name: "Chartreuse", rgb: "#7FFF00", h: 90, s: 100, l: 50], + [name: "Chocolate", rgb: "#D2691E", h: 25, s: 75, l: 47], [name: "Coral", rgb: "#FF7F50", h: 16, s: 100, l: 66], [name: "Corn Flower Blue", rgb: "#6495ED", h: 219, s: 79, l: 66], + [name: "Corn Silk", rgb: "#FFF8DC", h: 48, s: 100, l: 93], [name: "Crimson", rgb: "#DC143C", h: 348, s: 83, l: 58], [name: "Cyan", rgb: "#00FFFF", h: 180, s: 100, l: 50], + [name: "Dark Blue", rgb: "#00008B", h: 240, s: 100, l: 27], [name: "Dark Cyan", rgb: "#008B8B", h: 180, s: 100, l: 27], [name: "Dark Golden Rod", rgb: "#B8860B", h: 43, s: 89, l: 38], + [name: "Dark Gray", rgb: "#A9A9A9", h: 0, s: 0, l: 66], [name: "Dark Green", rgb: "#006400", h: 120, s: 100, l: 20], [name: "Dark Khaki", rgb: "#BDB76B", h: 56, s: 38, l: 58], + [name: "Dark Magenta", rgb: "#8B008B", h: 300, s: 100, l: 27], [name: "Dark Olive Green", rgb: "#556B2F", h: 82, s: 39, l: 30], [name: "Dark Orange", rgb: "#FF8C00", h: 33, s: 100, l: 50], + [name: "Dark Orchid", rgb: "#9932CC", h: 280, s: 61, l: 50], [name: "Dark Red", rgb: "#8B0000", h: 0, s: 100, l: 27], [name: "Dark Salmon", rgb: "#E9967A", h: 15, s: 72, l: 70], + [name: "Dark Sea Green", rgb: "#8FBC8F", h: 120, s: 25, l: 65], [name: "Dark Slate Blue", rgb: "#483D8B", h: 248, s: 39, l: 39], [name: "Dark Slate Gray", rgb: "#2F4F4F", h: 180, s: 25, l: 25], + [name: "Dark Turquoise", rgb: "#00CED1", h: 181, s: 100, l: 41], [name: "Dark Violet", rgb: "#9400D3", h: 282, s: 100, l: 41], [name: "Deep Pink", rgb: "#FF1493", h: 328, s: 100, l: 54], + [name: "Deep Sky Blue", rgb: "#00BFFF", h: 195, s: 100, l: 50], [name: "Dim Gray", rgb: "#696969", h: 0, s: 0, l: 41], [name: "Dodger Blue", rgb: "#1E90FF", h: 210, s: 100, l: 56], + [name: "Fire Brick", rgb: "#B22222", h: 0, s: 68, l: 42], [name: "Floral White", rgb: "#FFFAF0", h: 40, s: 100, l: 97], [name: "Forest Green", rgb: "#228B22", h: 120, s: 61, l: 34], + [name: "Fuchsia", rgb: "#FF00FF", h: 300, s: 100, l: 50], [name: "Gainsboro", rgb: "#DCDCDC", h: 0, s: 0, l: 86], [name: "Ghost White", rgb: "#F8F8FF", h: 240, s: 100, l: 99], + [name: "Gold", rgb: "#FFD700", h: 51, s: 100, l: 50], [name: "Golden Rod", rgb: "#DAA520", h: 43, s: 74, l: 49], [name: "Gray", rgb: "#808080", h: 0, s: 0, l: 50], + [name: "Green", rgb: "#008000", h: 120, s: 100, l: 25], [name: "Green Yellow", rgb: "#ADFF2F", h: 84, s: 100, l: 59], [name: "Honeydew", rgb: "#F0FFF0", h: 120, s: 100, l: 97], + [name: "Hot Pink", rgb: "#FF69B4", h: 330, s: 100, l: 71], [name: "Indian Red", rgb: "#CD5C5C", h: 0, s: 53, l: 58], [name: "Indigo", rgb: "#4B0082", h: 275, s: 100, l: 25], + [name: "Ivory", rgb: "#FFFFF0", h: 60, s: 100, l: 97], [name: "Khaki", rgb: "#F0E68C", h: 54, s: 77, l: 75], [name: "Lavender", rgb: "#E6E6FA", h: 240, s: 67, l: 94], + [name: "Lavender Blush", rgb: "#FFF0F5", h: 340, s: 100, l: 97], [name: "Lawn Green", rgb: "#7CFC00", h: 90, s: 100, l: 49], [name: "Lemon Chiffon", rgb: "#FFFACD", h: 54, s: 100, l: 90], + [name: "Light Blue", rgb: "#ADD8E6", h: 195, s: 53, l: 79], [name: "Light Coral", rgb: "#F08080", h: 0, s: 79, l: 72], [name: "Light Cyan", rgb: "#E0FFFF", h: 180, s: 100, l: 94], + [name: "Light Golden Rod Yellow", rgb: "#FAFAD2", h: 60, s: 80, l: 90], [name: "Light Gray", rgb: "#D3D3D3", h: 0, s: 0, l: 83], [name: "Light Green", rgb: "#90EE90", h: 120, s: 73, l: 75], + [name: "Light Pink", rgb: "#FFB6C1", h: 351, s: 100, l: 86], [name: "Light Salmon", rgb: "#FFA07A", h: 17, s: 100, l: 74], [name: "Light Sea Green", rgb: "#20B2AA", h: 177, s: 70, l: 41], + [name: "Light Sky Blue", rgb: "#87CEFA", h: 203, s: 92, l: 75], [name: "Light Slate Gray", rgb: "#778899", h: 210, s: 14, l: 53], [name: "Light Steel Blue", rgb: "#B0C4DE", h: 214, s: 41, l: 78], + [name: "Light Yellow", rgb: "#FFFFE0", h: 60, s: 100, l: 94], [name: "Lime", rgb: "#00FF00", h: 120, s: 100, l: 50], [name: "Lime Green", rgb: "#32CD32", h: 120, s: 61, l: 50], + [name: "Linen", rgb: "#FAF0E6", h: 30, s: 67, l: 94], [name: "Maroon", rgb: "#800000", h: 0, s: 100, l: 25], [name: "Medium Aquamarine", rgb: "#66CDAA", h: 160, s: 51, l: 60], + [name: "Medium Blue", rgb: "#0000CD", h: 240, s: 100, l: 40], [name: "Medium Orchid", rgb: "#BA55D3", h: 288, s: 59, l: 58], [name: "Medium Purple", rgb: "#9370DB", h: 260, s: 60, l: 65], + [name: "Medium Sea Green", rgb: "#3CB371", h: 147, s: 50, l: 47], [name: "Medium Slate Blue", rgb: "#7B68EE", h: 249, s: 80, l: 67], [name: "Medium Spring Green", rgb: "#00FA9A", h: 157, s: 100, l: 49], + [name: "Medium Turquoise", rgb: "#48D1CC", h: 178, s: 60, l: 55], [name: "Medium Violet Red", rgb: "#C71585", h: 322, s: 81, l: 43], [name: "Midnight Blue", rgb: "#191970", h: 240, s: 64, l: 27], + [name: "Mint Cream", rgb: "#F5FFFA", h: 150, s: 100, l: 98], [name: "Misty Rose", rgb: "#FFE4E1", h: 6, s: 100, l: 94], [name: "Moccasin", rgb: "#FFE4B5", h: 38, s: 100, l: 85], + [name: "Navajo White", rgb: "#FFDEAD", h: 36, s: 100, l: 84], [name: "Navy", rgb: "#000080", h: 240, s: 100, l: 25], [name: "Old Lace", rgb: "#FDF5E6", h: 39, s: 85, l: 95], + [name: "Olive", rgb: "#808000", h: 60, s: 100, l: 25], [name: "Olive Drab", rgb: "#6B8E23", h: 80, s: 60, l: 35], [name: "Orange", rgb: "#FFA500", h: 39, s: 100, l: 50], + [name: "Orange Red", rgb: "#FF4500", h: 16, s: 100, l: 50], [name: "Orchid", rgb: "#DA70D6", h: 302, s: 59, l: 65], [name: "Pale Golden Rod", rgb: "#EEE8AA", h: 55, s: 67, l: 80], + [name: "Pale Green", rgb: "#98FB98", h: 120, s: 93, l: 79], [name: "Pale Turquoise", rgb: "#AFEEEE", h: 180, s: 65, l: 81], [name: "Pale Violet Red", rgb: "#DB7093", h: 340, s: 60, l: 65], + [name: "Papaya Whip", rgb: "#FFEFD5", h: 37, s: 100, l: 92], [name: "Peach Puff", rgb: "#FFDAB9", h: 28, s: 100, l: 86], [name: "Peru", rgb: "#CD853F", h: 30, s: 59, l: 53], + [name: "Pink", rgb: "#FFC0CB", h: 350, s: 100, l: 88], [name: "Plum", rgb: "#DDA0DD", h: 300, s: 47, l: 75], [name: "Powder Blue", rgb: "#B0E0E6", h: 187, s: 52, l: 80], + [name: "Purple", rgb: "#800080", h: 300, s: 100, l: 25], [name: "Red", rgb: "#FF0000", h: 0, s: 100, l: 50], [name: "Rosy Brown", rgb: "#BC8F8F", h: 0, s: 25, l: 65], + [name: "Royal Blue", rgb: "#4169E1", h: 225, s: 73, l: 57], [name: "Saddle Brown", rgb: "#8B4513", h: 25, s: 76, l: 31], [name: "Salmon", rgb: "#FA8072", h: 6, s: 93, l: 71], + [name: "Sandy Brown", rgb: "#F4A460", h: 28, s: 87, l: 67], [name: "Sea Green", rgb: "#2E8B57", h: 146, s: 50, l: 36], [name: "Sea Shell", rgb: "#FFF5EE", h: 25, s: 100, l: 97], + [name: "Sienna", rgb: "#A0522D", h: 19, s: 56, l: 40], [name: "Silver", rgb: "#C0C0C0", h: 0, s: 0, l: 75], [name: "Sky Blue", rgb: "#87CEEB", h: 197, s: 71, l: 73], + [name: "Slate Blue", rgb: "#6A5ACD", h: 248, s: 53, l: 58], [name: "Slate Gray", rgb: "#708090", h: 210, s: 13, l: 50], [name: "Snow", rgb: "#FFFAFA", h: 0, s: 100, l: 99], + [name: "Spring Green", rgb: "#00FF7F", h: 150, s: 100, l: 50], [name: "Steel Blue", rgb: "#4682B4", h: 207, s: 44, l: 49], [name: "Tan", rgb: "#D2B48C", h: 34, s: 44, l: 69], + [name: "Teal", rgb: "#008080", h: 180, s: 100, l: 25], [name: "Thistle", rgb: "#D8BFD8", h: 300, s: 24, l: 80], [name: "Tomato", rgb: "#FF6347", h: 9, s: 100, l: 64], + [name: "Turquoise", rgb: "#40E0D0", h: 174, s: 72, l: 56], [name: "Violet", rgb: "#EE82EE", h: 300, s: 76, l: 72], [name: "Wheat", rgb: "#F5DEB3", h: 39, s: 77, l: 83], + [name: "White Smoke", rgb: "#F5F5F5", h: 0, s: 0, l: 96], [name: "Yellow", rgb: "#FFFF00", h: 60, s: 100, l: 50], [name: "Yellow Green", rgb: "#9ACD32", h: 80, s: 61, l: 50] +] + +//******************************************************************* +// CLONE CHILD LOGIC +//******************************************************************* +public Map getSettingsAndStateMap() { + Map typeObj = parent?.getAppDuplTypes() + Map setObjs = [:] + if(typeObj) { + ((Map>)typeObj.stat).each { sk, sv-> + sv?.each { svi-> if(settings.containsKey(svi)) { setObjs[svi] = [type: sk, value: settings[svi] ] } } + } + ((Map>)typeObj.ends).each { ek, ev-> + ev?.each { evi-> + settings.findAll { it?.key?.endsWith(evi) }?.each { String fk, fv-> + def vv = settings[fk] + if(ek==sTIME) vv = dateTimeFmt((Date)toDateTime(vv), "HH:mm") + setObjs[fk] = [type: ek, value: vv] + } + } + } + ((Map)typeObj.caps).each { ck, cv-> + settings.findAll { it.key.endsWith(ck) }?.each { String fk, fv-> setObjs[fk] = [type: "capability", value: (fv instanceof List ? fv?.collect { it?.id?.toString() } : it?.id?.toString ) ] } + } + ((Map)typeObj.dev).each { dk, dv-> + settings.findAll { it.key.endsWith(dk) }?.each { String fk, fv-> setObjs[fk] = [type: "device", value: (fv instanceof List ? fv.collect { it?.id?.toString() } : it?.id?.toString() ) ] } + } + } + Map data = [:] + String newlbl = app?.getLabel()?.toString()?.replace(" (A ${sPAUSESymFLD})", sBLANK) + data.label = newlbl?.replace(" (A)", sBLANK) + data.settings = setObjs + + List stateSkip = [ + /* "isInstalled", "isParent", */ + "lastNotifMsgDt", "lastNotificationMsg", "setupComplete", "valEvtHistory", "warnHistory", "errorHistory", + "appData", "actionHistory", "authValidHistory", "deviceRefreshInProgress", "noticeData", "installData", "herokuName", "zoneHistory", + // actions + "tierSchedActive", + // zones + "zoneConditionsOk", "configStatusMap", "tsDtMap", "dateInstalled", "handleGuardEvents", "startTime", "stopTime", "alexaGuardState", "appFlagsMap", + "dupPendingSetup", "dupOpenedByUser" + + ] + data.state = state?.findAll { !((String)it?.key in stateSkip) } + return data +} diff --git a/apps/echo-speaks-zones.groovy b/apps/echo-speaks-zones.groovy index 72b529871..77b0a96d6 100644 --- a/apps/echo-speaks-zones.groovy +++ b/apps/echo-speaks-zones.groovy @@ -1,2363 +1,2363 @@ -/** - * Echo Speaks - Zones (Hubitat) - * - * Copyright 2018, 2019, 2020, 2021, 2022 Anthony Santilli - * Code Contributions by @nh.schottfam - * - * 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. - * - */ -//file:noinspection GroovyUnusedAssignment -//file:noinspection unused -//file:noinspection GroovySillyAssignment -//file:noinspection GrMethodMayBeStatic - -import groovy.json.JsonOutput -import groovy.transform.Field - -import java.text.SimpleDateFormat -import java.util.concurrent.Semaphore - -//************************************************ -//* STATIC VARIABLES * -//************************************************ -@Field static final String appVersionFLD = '4.2.0.2' -@Field static final String appModifiedFLD = '2022-04-18' -@Field static final String sNULL = (String)null -@Field static final String sBLANK = '' -@Field static final String sSPACE = ' ' -@Field static final String sBULLET = '\u2022' -//@Field static final String sBULLETINV = '\u25E6' -//@Field static final String sSQUARE = '\u29C8' -//@Field static final String sPLUS = '\u002B' -//@Field static final String sRIGHTARR = '\u02C3' -@Field static final String okSymFLD = "\u2713" -@Field static final String notOkSymFLD = "\u2715" -@Field static final String sPAUSESymFLD = "\u275A\u275A" -@Field static final String sLINEBR = '
    ' -@Field static final String sFALSE = 'false' -@Field static final String sTRUE = 'true' -@Field static final String sBOOL = 'bool' -@Field static final String sENUM = 'enum' -@Field static final String sNUMBER = 'number' -@Field static final String sTEXT = 'text' -@Field static final String sTIME = 'time' -@Field static final String sMODE = 'mode' -//@Field static final String sCOMPLT = 'complete' -@Field static final String sMEDIUM = 'medium' -@Field static final String sSMALL = 'small' -@Field static final String sCLR4D9 = '#2784D9' -@Field static final String sCLRRED = 'red' -@Field static final String sCLRRED2 = '#cc2d3b' -@Field static final String sCLRGRY = 'gray' -//@Field static final String sCLRGRN = 'green' -@Field static final String sCLRGRN2 = '#43d843' -@Field static final String sCLRORG = 'orange' -@Field static final String sTTM = 'Tap to modify...' -@Field static final String sTTC = 'Tap to configure...' -//@Field static final String sTTCR = 'Tap to configure (Required)' -//@Field static final String sTTP = 'Tap to proceed...' -//@Field static final String sTTS = 'Tap to select...' -@Field static final String sSETTINGS = 'settings' -@Field static final String sRESET = 'reset' -@Field static final String sEXTNRL = 'external' -@Field static final String sDEBUG = 'debug' -@Field static final String sSWITCH = 'switch' -@Field static final String sCHKBOX = 'checkbox' -@Field static final String sCOMMAND = 'command' -@Field static final String zoneHistFLD = 'zoneHistory' -@Field static final List lSUNRISESET = ["sunrise", "sunset"] - -//************************************************ -//* IN-MEMORY ONLY VARIABLES * -//* (Cleared only on HUB REBOOT or CODE UPDATES) * -//************************************************ -@Field volatile static Map historyMapFLD = [:] -// @Field volatile static String gitBranchFLD = null - -static String appVersion() { return appVersionFLD } -static String appVersionDt() { return appModifiedFLD } - -definition( - name : "Echo Speaks - Zones", - namespace : "tonesto7", - author : "Anthony Santilli", - description : "DO NOT INSTALL FROM MARKETPLACE\n\nAllows you to create virtual broadcast zones based on your echo devices using device/location events to active the zone.", - category : "My Apps", - parent : "tonesto7:Echo Speaks", - iconUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", - iconX2Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", - iconX3Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", - importUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/apps/echo-speaks-zones.groovy", - documentationLink : documentationUrl(), - videoLink : videoUrl() -) - -preferences { - page(name: "startPage") - page(name: "uhOhPage") - page(name: "codeUpdatePage") - page(name: "mainPage") - page(name: "prefsPage") - page(name: "conditionsPage") - page(name: "condTimePage") - page(name: "zoneNotifPage") - page(name: "zoneNotifTimePage") - page(name: "zoneHistoryPage") - page(name: "uninstallPage") - page(name: "namePage") -} - -def startPage() { - if(parent != null) { - if(!(Boolean)state.isInstalled && !(Boolean)parent?.childInstallOk()) { return uhOhPage() } - else { - updDeviceInputs() - return (minVersionFailed()) ? codeUpdatePage() : mainPage() - } - } else { return uhOhPage() } -} - -def codeUpdatePage () { - return dynamicPage(name: "codeUpdatePage", title: "Update is Required", install: false, uninstall: false) { - section() { paragraph spanSmBld("Looks like your Zone App needs an update

    Please make sure all app and device code is updated to the most current version

    Once updated your zones will resume normal operation.", sCLRRED) } - } -} - -def uhOhPage () { - return dynamicPage(name: "uhOhPage", title: "This install Method is Not Allowed", install: false, uninstall: true) { - section() { - paragraph spanBld("HOUSTON WE HAVE A PROBLEM!

    Echo Speaks - Zones can't be directly installed from the Marketplace.

    Please use the Echo Speaks SmartApp to configure them.", sCLRRED) - } - } -} - -def appInfoSect() { - String instDt = state.dateInstalled ? fmtTime((String)state.dateInstalled, "MMM dd '@' h:mm a", true) : sNULL - String str = spanBld((String)app.name, "black", "es_groups") - str += (app.label && (String)app.label != (String)app.name) ? spanBldBr(" (${app.label.replace(" (Z)", sBLANK)})") : sLINEBR - str += spanSmBld("Version: ") + spanSmBr(appVersionFLD) - str += instDt ? spanSmBld("Installed: ") + spanSmBr(instDt) : sBLANK - section() { paragraph divSm(str, sCLRGRY) } -} - -def mainPage() { - Boolean newInstall = !(Boolean)state.isInstalled - return dynamicPage(name: "mainPage", nextPage: (!newInstall ? sBLANK : "namePage"), uninstall: newInstall, install: !newInstall) { - Boolean dup = ((Boolean)settings.duplicateFlag == true && (Boolean)state.dupPendingSetup) - if(dup) { - state.dupOpenedByUser = true - section() { paragraph spanBld("This Zone was created from an existing zone.

    Please review the settings and save to activate...
    ${state.badMode ?: sBLANK}", sCLRORG, "pause_orange") } - } - appInfoSect() - - Boolean paused = isPaused() - if(paused) { - section() { - paragraph spanSmBlr("This Zone is currently disabled...
    To edit the please re-enable it.", sCLRRED, "pause_orange") - } - } else { - section() { paragraph divSm(getOverallDesc(), sCLRGRY) } - - section(sectHead("Zone Devices:")) { - echoDevicesInputByPerm('announce') - } - if(settings.zone_EchoDevices) { - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } - Boolean condConf = conditionsConfigured() - section(sectHead("Zone Configuration:")) { - href "conditionsPage", title: inTS1("Zone Activation Conditions", "conditions") + optPrefix(), description: divSm(getConditionsDesc(true), sCLR4D9) - } - - if(condConf && settings.zone_EchoDevices) { - section(sectHead("Condition Delays:")) { - input "zone_active_delay", sNUMBER, title: inTS1("Delay Activation (In Seconds)", "delay_time") + optPrefix(), defaultValue: null, required: false, submitOnChange: true - input "zone_inactive_delay", sNUMBER, title: inTS1("Delay Deactivation (In Seconds)", "delay_time") + optPrefix(), defaultValue: null, required: false, submitOnChange: true - } - section(sectHead("Control Switches on Zone Active (Optional):")) { - input "zone_active_switches_on", "capability.switch", title: inTS1("Turn on Switches when Zone Active", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - input "zone_active_switches_off", "capability.switch", title: inTS1("Turn off Switches when Zone Active", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - } - section(sectHead("Control Switches on Zone Inactive (Optional):")) { - input "zone_inactive_switches_on", "capability.switch", title: inTS1("Turn on Switches when Zone Inactive", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - input "zone_inactive_switches_off", "capability.switch", title: inTS1("Turn off Switches when Zone Inactive", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - } - section(sectHead("Notifications:")) { - def t0 = getAppNotifDesc() - href "zoneNotifPage", title: inTS1("Send Notifications", "notification2"), description: t0 ? divSm(t0 + inputFooter(sTTM), sCLR4D9) : inputFooter(sTTC, sNULL, true) - } - } - } - updConfigStatusMap() - } - - section(sectHead("Zone History")) { - href "zoneHistoryPage", title: inTS1("View Zone History", "tasks"), description: sBLANK - } - - section(sectHead("Preferences")) { - href "prefsPage", title: inTS1("Logging Preferences", sSETTINGS), description: sBLANK - if((Boolean)state.isInstalled) { - input "zonePause", sBOOL, title: inTS1("Disable Zone?", "pause_orange"), defaultValue: false, submitOnChange: true - input "createZoneDevice", sBOOL, title: inTS1("Create a Virtual Device for this Zone?", "question"), defaultValue: false, submitOnChange: true - input "forceAnnounce", sBOOL, title: inTS1("Convert Zone speak commands to announcements?", "question"), defaultValue: true, submitOnChange: true - if((Boolean)settings.zonePause) { unsubscribe() } - } - } - //log.debug "myZoneStatus: ${myZoneStatus()}" - if((Boolean) state.isInstalled) { - section(sectHead("Name this Zone:")) { - input "appLbl", sTEXT, title: inTS1("Zone Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true - } - section(sectHead("Remove Zone:")) { - href "uninstallPage", title: inTS1("Remove this Zone", "uninstall"), description: inputFooter("Tap to Remove...", sCLRGRY, true) - } - section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { - String issueUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28ZONES+BUG%29+&projects=echo-speaks%2F6" - String featUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BZones+Feature+Request%5D&projects=echo-speaks%2F6" - href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "www"), description: spanSm("Tap to open browser", sCLRGRY) - href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "www"), description: spanSm("Tap to open browser", sCLRGRY) - } - } - } -} - -private echoDevicesInputByPerm(String type) { - List echoDevs = parent?.getChildDevicesByCap(type) - if(echoDevs?.size()) { - Map eDevsMap = echoDevs?.collectEntries { [(it.getId()): [label: (String)it.getLabel(), lsd: (it.currentWasLastSpokenToDevice?.toString() == sTRUE)]] }?.sort { a,b -> b?.value?.lsd <=> a?.value?.lsd ?: a?.value?.label <=> b?.value?.label } - Map moptions = eDevsMap?.collectEntries { [(it.key.toString()): "${it?.value?.label}${(it?.value?.lsd == true) ? " (Last Spoken To)" : sBLANK}".toString()] } - input "zone_EchoDevices", sENUM, title: inTS1("Echo Devices in Zone", "echo_gen1"), description: spanSm("Select the devices", sCLRGRY), options: moptions, multiple: true, required: true, submitOnChange: true - - // updDeviceInputs() - } else { paragraph spanSmBld("No devices were found with support for ($type)", sCLRRED) } -} - -def zoneHistoryPage() { - return dynamicPage(name: "zoneHistoryPage", title: "Zone History", install: false, uninstall: false) { - section() { - getZoneHistory() - } - List eData = (List)getMemStoreItem(zoneHistFLD) ?: [] - if(eData.size()) { - section(sBLANK) { - input "clearZoneHistory", sBOOL, title: inTS1("Clear Zone History?", sRESET), description: spanSm("Clears Stored Zone History.", sCLRGRY), defaultValue: false, submitOnChange: true - if(settings.clearZoneHistory) { settingUpdate("clearZoneHistory", sFALSE, sBOOL); updMemStoreItem(zoneHistFLD, []) } - } - } - } -} - -def prefsPage() { - return dynamicPage(name: "prefsPage", install: false, uninstall: false) { - section(sectHead("Logging:")) { - input "logInfo", sBOOL, title: inTS1("Show Info Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logWarn", sBOOL, title: inTS1("Show Warning Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logError", sBOOL, title: inTS1("Show Error Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logDebug", sBOOL, title: inTS1("Show Debug Logs?", sDEBUG), description: spanSm("Auto disables after 6 hours", sCLRGRY), required: false, defaultValue: false, submitOnChange: true - input "logTrace", sBOOL, title: inTS1("Show Detailed Logs?", sDEBUG), description: spanSm("Only enable when asked to (Auto disables after 6 hours)", sCLRGRY), required: false, defaultValue: false, submitOnChange: true - } - if((Boolean)state.isInstalled) { - if(advLogsActive()) { logsEnabled() } - } - } -} - -def namePage() { - return dynamicPage(name: "namePage", install: true, uninstall: false) { - section(sectHead("Name the Zone:")) { - input "appLbl", sTEXT, title: inTS1("Zone Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true - } - } -} - -def uninstallPage() { - return dynamicPage(name: "uninstallPage", title: "Uninstall", install: false , uninstall: true) { - section(sBLANK) { paragraph spanSmBld("This will delete this Echo Speaks Zone.", sCLRORG) } - } -} - -private void updDeviceInputs() { - // log.trace "updDeviceInputs..." - List aa = (List)settings.zone_EchoDevices - List devIds - Boolean updList = false - try { - updList = (aa.size() && aa[0].id != null) - devIds = aa.collect { it?.id?.toString() } - // log.debug "updList(try): ${devIds.unique()}" - } catch (ignored) { - // log.debug "ex: $ex" - devIds = aa.collect { it?.toString() } - } - if(updList && devIds) { - logDebug("updList: $devIds") - app.updateSetting( "zone_EchoDevices", [type: "enum", value: devIds.unique()]) - } - if(devIds) { app.updateSetting( "zone_EchoDeviceList", [type: "capability", value: devIds.unique()]) } // this won't take effect until next execution -} - -/****************************************************************************** - CONDITIONS SELECTION PAGE -******************************************************************************/ - -def conditionsPage() { - return dynamicPage(name: "conditionsPage", title: "Zone Activation Conditions", nextPage: "mainPage", install: false, uninstall: false) { - String desc = getConditionsDesc(false) - if(!desc) { desc= "No Conditions/Restrictions Set" } - section() { paragraph divSm(desc, sCLR4D9) } - - Boolean multiConds = multipleConditions() - section() { - if(multiConds) { - input "cond_require_all", sBOOL, title: inTS1("Require All Selected Conditions to Pass Before Activating Zone?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm(reqAllCond() ? "All selected conditions must pass before this zone is active." : "Any condition will allow this zone to become active.", sCLR4D9) - } - section(sectHead("Time/Date Restrictions")) { - href "condTimePage", title: inTS1("Time Schedule", "clock"), description: spanSm(getTimeCondDesc(false), sCLR4D9) - input "cond_days", sENUM, title: inTS1("Days of the week", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: weekDaysEnum() - input "cond_months", sENUM, title: inTS1("Months of the year", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: monthEnum() - } - - section (sectHead("Mode Conditions")) { - input "cond_mode", sMODE, title: inTS1("Location Modes...", sMODE), multiple: true, required: false, submitOnChange: true - if((List)settings.cond_mode) { - input "cond_mode_cmd", sENUM, title: inTS1("are...", sCOMMAND), options: ["not":"not in these modes", "are":"In these Modes"], required: true, multiple: false, submitOnChange: true - if((List)settings.cond_mode && (String)settings.cond_mode_cmd) { - input "cond_mode_db", sBOOL, title: inTS1("Deactivate Zone immediately when Mode condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - } - - section (sectHead("Alarm Conditions")) { - input "cond_alarm", sENUM, title: inTS1("${getAlarmSystemName()} is...", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: false, submitOnChange: true - if((List)settings.cond_alarm) { - input "cond_alarm_db", sBOOL, title: inTS1("Deactivate Zone immediately when Alarm condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - - condNonNumSect(sSWITCH, sSWITCH, "Switches/Outlets Conditions", "Switches/Outlets", ["on","off"], "are", sSWITCH) - - condNonNumSect("contact", "contactSensor", "Door, Window, Contact Sensors Conditions", "Contact Sensors", ["open","closed"], "are", "contact") - - condNonNumSect("motion", "motionSensor", "Motion Conditions", "Motion Sensors", ["active", "inactive"], "are", "motion") - - condNonNumSect("presence", "presenceSensor", "Presence Conditions", "Presence Sensors", ["present", "not present"], "are", "presence") - - condNonNumSect("acceleration", "accelerationSensor", "Accelerometer Conditions", "Accelerometer Sensors", ["active","inactive"], "are", "acceleration") - - condNumValSect("battery", "battery", "Battery Level Conditions", "Batteries", "Level (%)", "battery") - - condNumValSect("level", "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level (%)", "speed_knob") - - condNonNumSect("door", "garageDoorControl", "Garage Door Conditions", "Garage Doors", ["open", "closed"], "are", "garage_door") - - condNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Conditions", "Illuminance Sensors", "Lux Level (%)", "illuminance") - - condNumValSect("power", "powerMeter", "Power Events", "Power Meters", "Power Level (W)", "power") - - condNumValSect("humidity", "relativeHumidityMeasurement", "Humidity Conditions", "Relative Humidity Sensors", "Relative Humidity (%)", "humidity") - - condNonNumSect("securityKeypad", "securityKeypad", "Security Keypad Conditions", "Security Kepads", ["disarmed", "armed home", "armed away"], "are", "keypad") - - condNonNumSect("lock", "lock", "Lock Conditions", "Smart Locks", ["locked", "unlocked"], "are", "lock") - - condNumValSect("temperature", "temperatureMeasurement", "Temperature Conditions", "Temperature Sensors", "Temperature", "temperature") - - condNonNumSect("water", "waterSensor", "Water Sensors", "Water Sensors", ["wet", "dry"], "are", "water") - - condNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", ["open", "closed"], "are", "shade") - - condNonNumSect("valve", "valve", "Valves", "Valves", ["open", "closed"], "are", "valve") - } -} - -def condNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, String image) { - section(sectHead(sectStr) /*, hideWhenEmpty: true */) { - input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false //, hideWhenEmpty: true - if (settings."cond_${inType}") { - input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle}...", sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true - if ((String)settings."cond_${inType}_cmd" && ((List)settings."cond_${inType}")?.size() > 1) { - input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${(String)settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - input "cond_${inType}_db", sBOOL, title: inTS1("Deactivate Zone immediately when ${cmdTitle} condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } -} - -@SuppressWarnings('unused') -def condNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image, hideable= false) { - section(sectHead(sectStr) /*, hideWhenEmpty: true */) { - input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required: false //, hideWhenEmpty: true - if(settings."cond_${inType}") { - input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle} is...", sCOMMAND), options: [sBETWEEN, sBELOW, sABOVE, sEQUALS], required: true, multiple: false, submitOnChange: true - if ((String)settings."cond_${inType}_cmd") { - if ((String)settings."cond_${inType}_cmd" in [sBETWEEN, sBELOW]) { - input "cond_${inType}_low", sNUMBER, title: inTS1("a ${(String)settings."cond_${inType}_cmd" == sBETWEEN ? "Low " : sBLANK}${cmdTitle} of...", "low"), required: true, submitOnChange: true - } - if ((String)settings."cond_${inType}_cmd" in [sBETWEEN, sABOVE]) { - input "cond_${inType}_high", sNUMBER, title: inTS1("${(String)settings."cond_${inType}_cmd" == sBETWEEN ? "and a high " : "a "}${cmdTitle} of...", "high"), required: true, submitOnChange: true - } - if ((String)settings."cond_${inType}_cmd" == sEQUALS) { - input "cond_${inType}_equal", sNUMBER, title: inTS1("a ${cmdTitle} of...", "equal"), required: true, submitOnChange: true - } - if (settings."cond_${inType}"?.size() > 1) { - input "cond_${inType}_all", sBOOL, title: inTS1("Require ALL devices to be (${(String)settings."cond_${inType}_cmd"}) values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - if(!(Boolean)settings."cond_${inType}_all") { - input "cond_${inType}_avg", sBOOL, title: inTS1("Use the average of all selected device values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - input "cond_${inType}_db", sBOOL, title: inTS1("Deactivate Zone immediately when ${cmdTitle} condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true - } - } - } -} - -def condTimePage() { - return dynamicPage(name:"condTimePage", title: sBLANK, install: false, uninstall: false) { - Boolean timeReq = (settings["cond_time_start"] || settings["cond_time_stop"]) - section(sectHead("Start Time:")) { - input "cond_time_start_type", sENUM, title: inTS1("Starting at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(cond_time_start_type == sTIME) { - input "cond_time_start", sTIME, title: inTS1("Start time", "start_time"), required: timeReq, submitOnChange: true - } else if(cond_time_start_type in lSUNRISESET) { - input "cond_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - section(sectHead("Stop Time:")) { - input "cond_time_stop_type", sENUM, title: inTS1("Stopping at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(cond_time_stop_type == sTIME) { - input "cond_time_stop", sTIME, title: inTS1("Stop time", "start_time"), required: timeReq, submitOnChange: true - } else if(cond_time_stop_type in lSUNRISESET) { - input "cond_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - } -} - -def zoneNotifPage() { - return dynamicPage(name: "zoneNotifPage", title: "Zone Notifications", install: false, uninstall: false) { - String a = getAppNotifDesc() - if(!a) a = "Notifications not enabled" - section() { paragraph divSm(a, sCLR4D9) } - - section (sectHead("Notification Devices:")) { - input "notif_devs", "capability.notification", title: inTS1("Send to Notification devices?", "notification"), description: ((!settings?.notif_devs) ? inactFoot(sTTC) : sBLANK), required: false, multiple: true, submitOnChange: true - } - section (sectHead("Alexa Mobile Notification:")) { - paragraph spanSmBldBr("Description:", sCLRGRY) + spanSmBld("This will send a push notification the Alexa Mobile app.", sCLRGRY) - input "notif_alexa_mobile", sBOOL, title: inTS1("Send message to Alexa App?", "notification"), required: false, defaultValue: false, submitOnChange: true - } - - Boolean active = ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) - if(active) { - section (sectHead("Message Customization:")) { - paragraph spanSm("Configure either an Active or Inactive message to complete notification configuration.", sCLR4D9) - Boolean req = !((String)settings.notif_active_message || (String)settings.notif_inactive_message) - input "notif_active_message", sTEXT, title: inTS1("Active Message?", sTEXT), required: req, submitOnChange: true - input "notif_inactive_message", sTEXT, title: inTS1("Inactive Message?", sTEXT), required: req, submitOnChange: true - } - } else { - List sets = settings.findAll { it.key.startsWith("notif_") }?.collect { (String)it.key } - sets?.each { String sI-> settingRemove(sI) } - } - - if(isZoneNotifConfigured()) { - section(sectHead("Notification Restrictions:")) { - String nsd = getNotifSchedDesc() - href "zoneNotifTimePage", title: inTS1("Quiet Restrictions", "restriction"), description: (nsd ? divSm(spanSmBr(nsd) + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC)) - } - if(!(Boolean)state.notif_message_tested) { - List actDevices = (Boolean)settings.notif_alexa_mobile ? parent?.getDevicesFromList(settings.zone_EchoDevices) : [] - def aMsgDev = actDevices?.size() && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null - if(sendNotifMsg("Info", "Zone Notification Test Successful. Notifications Enabled for ${app?.getLabel()}", aMsgDev, true)) { state.notif_message_tested = true } - } - } else { state.remove("notif_message_tested") } - } -} - -def zoneNotifTimePage() { - return dynamicPage(name:"zoneNotifTimePage", title: sBLANK, install: false, uninstall: false) { - // String a = getNotifSchedDesc() - // if(a) { - // section() { - // paragraph spanSmBldBr("Restrictions Status:", sCLR4D9) + spanSm(a, sCLR4D9) - // paragraph spanSmBldBr("NOTICE: All selected restrictions must be ${strUnder("INACTIVE")} for notifications to be sent.", sCLRORG) - // paragraph htmlLine() - // } - // } - String pre = "notif" - Boolean timeReq = (settings["${pre}_time_start"] || settings["${pre}_time_stop"]) - section(sectHead("Quiet Start Time:")) { - input "${pre}_time_start_type", sENUM, title: inTS1("Starting at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(settings."${pre}_time_start_type" == sTIME) { - input "${pre}_time_start", sTIME, title: inTS1("Start time", "start_time"), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_start_type" in lSUNRISESET) { - input "${pre}_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - section(sectHead("Quiet Stop Time:")) { - input "${pre}_time_stop_type", sENUM, title: inTS1("Stopping at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(settings."${pre}_time_stop_type" == sTIME) { - input "${pre}_time_stop", sTIME, title: inTS1("Stop time", "start_time"), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_stop_type" in lSUNRISESET) { - input "${pre}_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - section(sectHead("Allowed Days:")) { - input "${pre}_days", sENUM, title: inTS1("Only on these week days", "day_calendar"), multiple: true, required: false, options: weekDaysEnum() - } - section(sectHead("Allowed Modes:")) { - input "${pre}_modes", sMODE, title: inTS1("Only in these Modes", sMODE), multiple: true, submitOnChange: true, required: false - } - } -} - - -//******************************************** -// CORE APP METHODS -//******************************************** - -def installed() { - logInfo("Installed Event Received...") - state.dateInstalled = getDtNow() - if((Boolean)settings.duplicateFlag && !(Boolean)state.dupPendingSetup) { - Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") - state.dupPendingSetup = true - runIn(2, "processDuplication") - if(maybeDup) logInfo("installed found maybe a dup... ${(Boolean)settings.duplicateFlag}") - } else { - if(!(Boolean)state.dupPendingSetup) initialize() - } -} - -@Field static final String dupMSGFLD = "This zone is duplicated and has not had configuration completed... Please open zone and configure to complete setup..." - -def updated() { - logInfo("Updated Event Received...") - Boolean maybeDup = app?.getLabel()?.toString()?.contains(" (Dup)") - if(maybeDup) logInfo("updated found maybe a dup... ${(Boolean)settings.duplicateFlag}") - if((Boolean)settings.duplicateFlag) { - if((Boolean)state.dupOpenedByUser) { state.dupPendingSetup = false } - if((Boolean)state.dupPendingSetup){ - logInfo(dupMSGFLD) - return - } - logInfo("removing duplicate status") - settingRemove('duplicateFlag'); settingRemove('duplicateSrcId') - state.remove('dupOpenedByUser'); state.remove('dupPendingSetup'); state.remove('badMode') - } - initialize() -} - -def initialize() { - logInfo("Initialize Event Received...") - if((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup){ - logInfo(dupMSGFLD) - return - } - updDeviceInputs() - unsubscribe() - unschedule() - state.isInstalled = true - updAppLabel() - if(advLogsActive()) { logsEnabled() } - runIn(3, "zoneCleanup") - if(!isPaused(true)) { - runIn(7, "subscribeToEvts") - runEvery1Hour("healthCheck") - updConfigStatusMap() - } - - handleZoneDevice() - checkZoneStatus([name: "initialize", displayName: "initialize"]) -} - -void handleZoneDevice() { - String dni = [app?.id, "echoSpeaks-Zone-Dev"].join("|") - def childDevice = getChildDevice(dni) - String devLabel = "EchoZone - ${app.getLabel().replace(" (Z)", sBLANK)}" - String childHandlerName = "Echo Speaks Zone Device" - if (!childDevice && (Boolean)settings.createZoneDevice) { - // log.debug "childDevice not found | autoCreateDevices: ${settings.autoCreateDevices}" - try{ - logInfo("Creating NEW Echo Speaks Zone Device!!! | Device Label: ($devLabel)") - childDevice = addChildDevice("tonesto7", childHandlerName, dni, null, [name: childHandlerName, label: devLabel, settings:[isZone: true], completedSetup: true]) - } catch(ex) { - logError("AddDevice Error! | ${ex}", false, ex) - } - } - if (childDevice && !(Boolean)settings.createZoneDevice) { - try { - deleteChildDevice(childDevice.deviceNetworkId) - } catch(e) { - logError("RemoveDevice Error! } ${e}", false, e) - } - } -} - -List getEzDevice() { - return ((List)getChildDevices())?.findAll { (Boolean)it?.isZone() } -} - -void updateChildZoneState(Boolean zoneActive, Boolean active) { - if(zoneActive != null) getEzDevice()?.each { it?.updStatus(zoneActive) } - if(active != null) getEzDevice()?.each { it?.updSocketStatus(active) } -} - -/******************************************************************* - To Device from parent -*******************************************************************/ -@SuppressWarnings('unused') -String relayDevVersion() { - String a - a = sNULL - getEzDevice().each { a = it.devVersion() } - return a -} - -@SuppressWarnings('unused') -void relayUpdChildSocketStatus(Boolean active) { - updateChildZoneState(null, active) -} - -@SuppressWarnings('unused') -void relayUpdateCookies(Map cookies, Boolean doInit){ - getEzDevice().each { it.updateCookies(cookies, doInit) } -} - -@SuppressWarnings('unused') -void relayRemoveCookies(Boolean isParent) { - getEzDevice().each { it.removeCookies(isParent) } -} - -@SuppressWarnings('unused') -void relayEnableDebugLog() { getEzDevice().each { it.enableDebugLog() } } -@SuppressWarnings('unused') -void relayDisableDebugLog() { getEzDevice().each { it.disableDebugLog() } } -@SuppressWarnings('unused') -void relayEnableTraceLog() { getEzDevice().each { it.enableTraceLog() } } -@SuppressWarnings('unused') -void relayDisableTraceLog() { getEzDevice().each { it.disableTraceLog() } } -@SuppressWarnings('unused') -void relayLogsOff() { getEzDevice().each { it.logsOff() } } -@SuppressWarnings('unused') -Map relayGetLogHistory() { Map a; a=null; getEzDevice().each { a = it.getLogHistory() }; return a ?: [:] } -@SuppressWarnings('unused') -void relayClearLogHistory() { getEzDevice().each { it.clearLogHistory() } } -@SuppressWarnings('unused') -void relayFinishAnnouncement(String msg, LinkedHashMap vmap) { getEzDevice().each { it.finishAnnounce(msg, vmap.vol, vmap.restvol) } } -@SuppressWarnings('unused') -void relayFinishSpeak(Map resp, Integer statucode, Map data) { getEzDevice().each { it.finishSendSpeakZ(resp, statuscode, data) } } -@SuppressWarnings('unused') -void relaySetOnline(Boolean onl) { getEzDevice().each { it.setOnlineStatus(onl) } } - - -/******************************************************************* - To Parent from Device Command FUNCTIONS -*******************************************************************/ -Boolean zoneOnline() { - if(isPaused(true) || !isActive()) { - logTrace("zone offline or inactive; relay failed") - return false - } - return true -} - -Map relayMinVersions() { - parent.minVersions() -} - -@SuppressWarnings('unused') -void relayMultiSeqCommand(List commands, String srcDesc, Boolean parallel=false, Map cmdMap=[:], String device=sNULL, String callback=sNULL) { - if(zoneOnline()) parent.queueMultiSequenceCommand(commands, srcDesc, parallel, cmdMap, device, callback) -} - -@SuppressWarnings('unused') -void relaySeqCommand(String type, String command, value=null, Map deviceData=[:], String device=sNULL, String callback=sNULL) { - if(zoneOnline()) parent.queueSequenceCommand(type, command, value, deviceData, device, callback) -} - -@SuppressWarnings('unused') -void relayAnnounceZone(String zoneId, String msg, Boolean parallel, Boolean ignoreDoNotDisturb) { - if(zoneOnline()) parent.sendZoneAnnounce(zoneId, msg, parallel, ignoreDoNotDisturb) -} - -@SuppressWarnings('unused') -void relaySpeakZone(String zoneId, String msg, Boolean parallel, Boolean ignoreDoNotDisturb) { - if(zoneOnline()) parent.sendZoneSpeak(zoneId, msg, parallel, ignoreDoNotDisturb) -} - -@SuppressWarnings('unused') -void relayNopCommand() { - if(zoneOnline()) parent.queueNopCommand() -} - -@SuppressWarnings('unused') -private Boolean relayGetWWebSocketStatus() { - parent.getWWebSocketStatus() -} - -@SuppressWarnings('unused') -void relayChildInitialtedRefresh() { - parent.childInitiatedRefresh() -} - -String relayGetAlexaGuardStatus() { - parent.getAlexaGuardStatus() -} -/* -Boolean RelayGetDndEnabled(String serial) { - parent.getDndEnabled(serial) -} */ - -@SuppressWarnings('unused') -private void processDuplication() { - String al = (String)app?.getLabel() - String newLbl = "${al}${al?.contains(" (Dup)") ? sBLANK : " (Dup)"}" - app?.updateLabel(newLbl) - state.dupPendingSetup = true - - String dupSrcId = settings.duplicateSrcId ? (String)settings.duplicateSrcId : sNULL - Map dupData = parent?.getChildDupeData("zones", dupSrcId) - log.debug "dupData: ${dupData}" - if(dupData && dupData.state?.size()) { - dupData.state.each { String k,v-> state[k] = v } - } - - if(dupData && dupData.settings?.size()) { - dupData.settings.each { String k, Map v-> - if((String)v.type in [sENUM, sMODE]) { - settingRemove(k) - settingUpdate(k, (v.value != null ? v.value : null), (String)v.type) - } - } - } - - parent.childAppDuplicationFinished("zones", dupSrcId) - sendZoneStatus() - subscribe(location, "es3ZoneRefresh", zoneRefreshHandler) - logInfo("Duplicated Zone has been created... Please open zone and configure to complete setup...") -} - -def uninstalled() { - sendZoneRemoved() -} - -String getZoneName() { return (String)settings.appLbl } - -private void updAppLabel() { - String newLbl = "${(String)settings.appLbl} (Z${isPaused(true) ? " ${sPAUSESymFLD}" : sBLANK})".replaceAll(/ (Dup)/, sBLANK).replaceAll("\\s"," ") - if((String)settings.appLbl && (String)app?.getLabel() != newLbl) { app?.updateLabel(newLbl); sendZoneStatus() } -} - -private void updConfigStatusMap() { - Map sMap = (Map)state.configStatusMap - sMap = sMap ?: [:] - sMap.conditions = conditionsConfigured() - sMap.devices = devicesConfigured() - state.configStatusMap = sMap -} - -Boolean devicesConfigured() { return (settings.zone_EchoDevices?.size() > 0) } - -//private Boolean getConfStatusItem(String item) { Map sMap = state.configStatusMap; return (sMap?.containsKey(item) && sMap[item] == true) } - -@SuppressWarnings('unused') -private void zoneCleanup() { - // State Cleanup - List items = [] - items.each { String si-> if(state.containsKey(si)) { state.remove(si)} } - //Cleans up unused Zone setting items - List setItems = ["zone_delay"] - setItems.each { String sI-> if(settings.containsKey(sI)) { settingRemove(sI) } } -} - -public void triggerInitialize() { runIn(3, "initialize") } - -public void updatePauseState(Boolean pause) { - if((Boolean)settings.zonePause != pause) { - logDebug("Received Request to Update Pause State to (${pause})") - settingUpdate("zonePause", "${pause}", sBOOL) - runIn(4, "updated") - } -} - -@SuppressWarnings('unused') -private healthCheck() { - // logTrace("healthCheck", true) - checkZoneStatus([name: "healthCheck", displayName: "healthCheck"]) - if(advLogsActive()) { logsDisable() } -} - -//private condItemSet(String key) { return (settings.containsKey("cond_${key}") && settings["cond_${key}"]) } - -@SuppressWarnings('GroovyVariableNotAssigned') -void scheduleCondition() { - if(isPaused(true)) { logWarn("Zone is PAUSED... No Events will be subscribed to or scheduled....", true); return } - Boolean tC = timeCondConfigured() - Boolean dC = dateCondConfigured() - if (tC || dC) { - String msg = 'scheduleCondition: ' - Date startTime - Date stopTime - if (tC) { - Boolean timeOk = timeCondOk() // this updates state variables - startTime = (String)state.startTime ? parseDate((String)state.startTime) : null - stopTime = (String)state.stopTime ? parseDate((String)state.stopTime) : null - } - if(!startTime && !stopTime && dC){ - startTime = timeToday('00:00', location.timeZone) - stopTime = timeTodayAfter('23:59', '00:00', location.timeZone) - } - if(startTime && stopTime){ - Long t = now() - Long lstart = startTime.getTime() - Long lstop = stopTime.getTime() - - Boolean not = lstart > lstop - Boolean isStart = true - Long nextEvtT = 0L - if(!not) { - if(t 0L) { - Long tt = Math.round((nextEvtT)/1000.0D) + 1L - tt=(tt<1L ? 1L:tt) - if(isStart) runIn(tt, "zoneTimeStartCondHandler") - else runIn(tt, "zoneTimeStopCondHandler") - Date ttt = isStart ? startTime: stopTime - msg += "Setting Schedule for ${isStart ? "Start Condition" : "Stop Condition"} in $tt's at ${epochToTime(ttt)}" - msg += msg1 - } else msg += "Nothing to"+msg1 - } else log.warn "Strange - no start ($startTime) or stop ($stopTime) time: $tC date: $dC" - logTrace(msg) - } -} - -void subscribeToEvts() { -// state.handleGuardEvents = false - if(minVersionFailed()) { logError("CODE UPDATE required to RESUME operation. No events will be monitored.", true); return } - if(isPaused(true)) { logWarn("Zone is PAUSED... No Events will be subscribed to or scheduled....", true); return } - List attItems = [sMODE, "alarm"] + lDATTSTR + lDATTNUM - - //SCHEDULING - if(timeCondConfigured() || dateCondConfigured()) { - scheduleCondition() - schedule('33 0 0 1/1 * ? * ', scheduleCondition) // run at 00:00:33 every day -// in case we have been stopped a while... - Map condStat = conditionStatus() - if((Boolean)condStat.ok) zoneTimeStartCondHandler() - else zoneTimeStopCondHandler() - } - - attItems.each { String si-> - if((List)settings."cond_${si}") { - switch(si) { - case "alarm": - subscribe(location, "hsmStatus", zoneEvtHandler) - break - case sMODE: - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } - subscribe(location, si, zoneEvtHandler) - break - default: - subscribe((List)settings."cond_${si}", si, zoneEvtHandler) - break - } - } - } - // Subscribes to Zone Location Command from Other Echo Speaks apps. - subscribe(location, "es3ZoneCmd", zoneCmdHandler) - subscribe(location, "es3ZoneRefresh", zoneRefreshHandler) - subscribe(location, "systemStart", zoneStartHandler) -} - -/*********************************************************************************************************** - CONDITIONS HANDLER -************************************************************************************************************/ -// Boolean reqAllCond() { Boolean a = multipleConditions(); return (!a || (a && (Boolean)settings.cond_require_all) ); } -Boolean reqAllCond() { - Boolean mult = multipleConditions() - return ( mult && (Boolean)settings.cond_require_all ) -} - -Boolean timeCondOk() { - Date startTime - Date stopTime - Date now = new Date() - String startType = (String)settings.cond_time_start_type - String stopType = (String)settings.cond_time_stop_type - if(startType && stopType) { - startTime = startType == 'time' && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null - stopTime = stopType == 'time' && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null - - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - def sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L - Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - - if(startTime && stopTime) { - Boolean not = startTime.getTime() > stopTime.getTime() - Boolean isBtwn = timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, location?.timeZone) - isBtwn = not ? !isBtwn : isBtwn - state.startTime = formatDt(startTime) - state.stopTime = formatDt(stopTime) - logTrace("timeCondOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not? startTime:stopTime})") - return isBtwn - } - } - logTrace("timeCondOk | (null)") - state.startTime = sNULL - state.stopTime = sNULL - return null -} - -@SuppressWarnings('GroovyVariableNotAssigned') -Boolean dateCondOk() { - Boolean result = null - Boolean dOk - Boolean mOk - if(((List)settings.cond_days || (List)settings.cond_months) ) { - Boolean reqAll = reqAllCond() - dOk = (List)settings.cond_days ? (isDayOfWeek((List)settings.cond_days)) : reqAll // true - mOk = (List)settings.cond_months ? (isMonthOfYear((List)settings.cond_months)) : reqAll //true - result = reqAll ? (mOk && dOk) : (mOk || dOk) - } - logTrace("dateCondOk | $result | monthOk: $mOk | daysOk: $dOk") - return result -} - -@SuppressWarnings('GroovyVariableNotAssigned') -Boolean locationCondOk() { - Boolean result = null - Boolean mOk - Boolean aOk - if((List)settings.cond_mode || (String)settings.cond_mode_cmd || (List)settings.cond_alarm) { - Boolean reqAll = reqAllCond() - mOk = ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : reqAll //true - aOk = (List)settings.cond_alarm ? isInAlarmMode((List)settings.cond_alarm) : reqAll //true - result = reqAll ? (mOk && aOk) : (mOk || aOk) - } - logTrace("locationCondOk | $result | modeOk: $mOk | alarmOk: $aOk") - return result -} - -Boolean checkDeviceCondOk(String att) { - List devs = (List)settings."cond_${att}" ?: null - def cmdVal = settings."cond_${att}_cmd" ?: null // list or string - Boolean all = ((Boolean)settings."cond_${att}_all" == true) - if( !(att && devs && cmdVal) ) { return true } - return all ? allDevAttValsEqual(devs, att, cmdVal) : anyDevAttValsEqual(devs, att, cmdVal) -} - -Boolean checkDeviceNumCondOk(String att) { - List devs = (List)settings."cond_${att}" ?: null - String cmd = (String)settings."cond_${att}_cmd" ?: sNULL - Double dcl = settings."cond_${att}_low" ?: null - Double dch = settings."cond_${att}_high" ?: null - Double dce = settings."cond_${att}_equal" ?: null - Boolean dca = ((Boolean)settings."cond_${att}_all" == true) ?: false - if( !(att && devs && cmd) ) { return true } - - switch(cmd) { - case "equals": - if(dce) { - if(dca) { return allDevAttNumValsEqual(devs, att, dce) } - else { return anyDevAttNumValEqual(devs, att, dce) } - } - return true - break - case "between": - if(dcl && dch) { - if(dca) { return allDevAttNumValsBetween(devs, att, dcl, dch) } - else { return anyDevAttNumValBetween(devs, att, dcl, dch) } - } - return true - break - case "above": - if(dch) { - if(dca) { return allDevAttNumValsAbove(devs, att, dch) } - else { return anyDevAttNumValAbove(devs, att, dch) } - } - return true - break - case "below": - if(dcl) { - if(dca) { return allDevAttNumValsBelow(devs, att, dcl) } - else { return anyDevAttNumValBelow(devs, att, dcl) } - } - return true - break - } - return false -} - -@Field static List lDATTSTR = ["switch", "motion", "presence", "contact", "acceleration", "lock", "securityKeypad", "windowShade", "door", "valve", "water" ] -@Field static List lDATTNUM = ["battery", "temperature", "illuminance", "level", "power", "humidity"] - -private Boolean isConditionOk(String att) { - if(lDATTSTR.contains(att)) { - if(!settings."cond_${att}") { return true } - return checkDeviceCondOk(att) - } else if(lDATTNUM.contains(att)) { - if(!settings."cond_${att}") { return true } - return checkDeviceNumCondOk(att) - } else if (att == sMODE) { - return ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : true - } else if (["hsmStatus", "alarmSystemStatus"].contains(att)) { - return (List)settings.cond_alarm ? isInAlarmMode((List)settings.cond_alarm) : true - } else { - return true - } -} - -Boolean deviceCondOk() { - List skipped = [] - List passed = [] - List failed = [] - lDATTSTR.each { String i-> - if(!settings."cond_${i}") { skipped.push(i); return } - checkDeviceCondOk(i) ? passed.push(i) : failed.push(i) - } - lDATTNUM.each { String i-> - if(!settings."cond_${i}") { skipped.push(i); return } - checkDeviceNumCondOk(i) ? passed.push(i) : failed.push(i) - } - Integer cndSize = (passed.size() + failed.size()) - Boolean result = null - if(cndSize != 0) result = reqAllCond() ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() >= 1) - logTrace("DeviceCondOk | ${result} | Found: (${(passed?.size() + failed?.size())}) | Skipped: $skipped | Passed: $passed | Failed: $failed") - return result -} - -Map conditionStatus() { - Boolean reqAll = reqAllCond() - List failed = [] - List passed = [] - List skipped = [] - Boolean ok = true - if((Boolean)state.dupPendingSetup) ok = false - if(!settings.zone_EchoDevices) ok = false - Integer cndSize=(Integer)null - if(ok) { - [sTIME, "date", "location", "device"]?.each { i-> - Boolean s = "${i}CondOk"() - if(s == null) { skipped.push(i); return } - s ? passed.push(i) : failed.push(i) - } - cndSize = passed.size() + failed.size() - ok = reqAll ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() >= 1) - if(cndSize == 0) ok = true - } - logTrace("conditionStatus | ok: $ok | RequireAll: ${reqAll} | Found: (${cndSize}) | Skipped: $skipped | Passed: $passed | Failed: $failed") - return [ok: ok, passed: passed, blocks: failed] -} - -Boolean devCondConfigured(String att) { - return ((List)settings."cond_${att}" && (String)settings."cond_${att}_cmd") -} - -Boolean devNumCondConfigured(String att) { - return ((String)settings."cond_${att}_cmd" && (settings."cond_${att}_low" || settings."cond_${att}_high" || settings."trig_${att}_equal")) -} - -Boolean timeCondConfigured() { - Boolean startTime = ((String)settings.cond_time_start_type in lSUNRISESET || ((String)settings.cond_time_start_type == sTIME && settings.cond_time_start)) - Boolean stopTime = ((String)settings.cond_time_stop_type in lSUNRISESET || ((String)settings.cond_time_stop_type == sTIME && settings.cond_time_stop)) - return (startTime && stopTime) -} - -Boolean dateCondConfigured() { - Boolean days = ((List)settings.cond_days) - Boolean months = ((List)settings.cond_months) - return (days || months) -} - -Boolean locationCondConfigured() { - if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } - Boolean mode = ((List)settings.cond_mode && (String)settings.cond_mode_cmd) - Boolean alarm = ((List)settings.cond_alarm) - return (mode || alarm) -} - -Boolean deviceCondConfigured() { - return (deviceCondCount() > 0) -} - -Integer deviceCondCount() { - List devConds = lDATTSTR + lDATTNUM - List items = [] - devConds.each { String dc-> if(devCondConfigured(dc)) { items.push(dc) } } - return items.size() -} - -Boolean conditionsConfigured() { - return (timeCondConfigured() || dateCondConfigured() || locationCondConfigured() || deviceCondConfigured()) -} - -Boolean multipleConditions() { - Integer cnt = 0 - if(timeCondConfigured()) cnt++ - if(dateCondConfigured()) cnt++ - if(locationCondConfigured()) cnt++ - cnt = cnt + deviceCondCount() - return (cnt>1) -} - -/*********************************************************************************************************** - EVENT HANDLER FUNCTIONS -************************************************************************************************************/ - -def zoneEvtHandler(evt) { - logTrace( "${(String)evt?.name} Event | Device: ${(String)evt?.displayName} | Value: (${strCapitalize(evt?.value?.toString())}) with a delay of ${now() - ((Date)evt?.date)?.getTime()}ms") - checkZoneStatus(evt) - scheduleCondition() -} - -void zoneTimeStartCondHandler() { - Map evt = [date: new Date(), name: "Time", displayName: "Condition Start Time", value: now()] - zoneEvtHandler(evt) -} - -void zoneTimeStopCondHandler() { - Map evt = [date: new Date(), name: "Time", displayName: "Condition Stop Time", value: now()] - zoneEvtHandler(evt) -} - -def zoneStartHandler(evt) { - zoneEvtHandler(evt) -// match states incase we were down - sendZoneStatus() -} - -private void addToZoneHistory(Map evt, Map condStatus, Integer max=10) { - Boolean ssOk = true //(stateSizePerc() <= 70) - List eData = getMemStoreItem(zoneHistFLD) ?: [] - eData.push([dt: getDtNow(), active: (condStatus.ok == true), evtName: evt.name, evtDevice: evt.displayName, blocks: condStatus.blocks, passed: condStatus.passed]) - Integer lsiz = eData.size() - if(!ssOk || lsiz > max) { eData = eData.drop( (lsiz-max)+1 ) } - updMemStoreItem(zoneHistFLD, eData) -} - -void checkZoneStatus(evt) { - Map condStatus = conditionStatus() - Boolean active = (Boolean)condStatus.ok - String delayType = active ? "active" : "inactive" - String nm = (String)evt?.name - String msg1 = " | Call from ${nm} / ${(String)evt?.displayName}".toString() - if((Boolean)state.zoneConditionsOk == active) { logTrace("checkZoneStatus: Zone: ${delayType} | No changes${msg1}"); return } - Boolean bypassDelay = false - Map data = [active: active, recheck: false, evtData: [name: nm, displayName: (String)evt?.displayName], condStatus: condStatus] - Integer delay = settings."zone_${delayType}_delay" ?: null - if(!active && settings."cond_${nm}_db" == true) { bypassDelay = !isConditionOk(nm) } - String msg = !bypassDelay && delay ? "in (${delay} sec)" : (bypassDelay ? "Bypassing Inactive Delay for (${nm}) Event..." : sBLANK) - logTrace("calling updateZoneStatus [${delayType}] "+msg+msg1) - if(!bypassDelay && delay) { - runIn(delay, "updateZoneStatus", [data: data]) - } else { - updateZoneStatus(data) - } -} - -public Boolean isZoneDeviceEnabled() { - return (Boolean)settings.createZoneDevice -} - -Map myZoneStatus() { - Map zoneDevs = getZoneDevices() - // log.debug "zoneDevs: $zoneDevs" - return [name: (String)app?.getLabel(), active: isActive(), paused: isPaused(true), id: app?.getId(), createZoneDevice: isZoneDeviceEnabled(), zoneDevices: (List)zoneDevs.devIds ?: []] -} - -void sendZoneStatus() { - Map zs = myZoneStatus() - sendLocationEvent(name: "es3ZoneState", value: app?.getId(), data: zs, isStateChange: true, display: false, displayed: false) - Boolean onl = (Boolean)zs.active && !(Boolean)zs.paused - relaySetOnline(onl) -} - -void sendZoneRemoved() { - sendLocationEvent(name: "es3ZoneRemoved", value: app?.getId(), data:[name: (String)app?.getLabel()], isStateChange: true, display: false, displayed: false) -} - -void updateZoneStatus(Map data) { - Boolean active = (Boolean)data.active - Map condStatus = (Map)data.condStatus - if((Boolean)data.recheck) { - condStatus = conditionStatus() - active = (Boolean)condStatus.ok - } - if(state.zoneConditionsOk != active) { - logInfo("Setting Zone (${getZoneName()}) Status to (${active ? "Active" : "Inactive"})") - state.zoneConditionsOk = active - addToZoneHistory((Map)data.evtData, condStatus) - sendZoneStatus() - if(isZoneNotifConfigured()) { - Boolean ok2Send = true - String msgTxt = active ? ((String)settings.notif_active_message ?: sNULL) : ((String)settings.notif_inactive_message ?: sNULL) - if(ok2Send && msgTxt) { - Map zoneDevices = getZoneDevices() - def alexaMsgDev = ((List)zoneDevices.devices)?.size() && (Boolean)settings.notif_alexa_mobile ? ((List)zoneDevices.devices)[0] : null - if(sendNotifMsg(getZoneName(), msgTxt, alexaMsgDev, false)) { logTrace("Sent Zone Notification...") } - } - } - if(active) { - if(settings.zone_active_switches_off) settings.zone_active_switches_off*.off() - if(settings.zone_active_switches_on) settings.zone_active_switches_on*.on() - } else { - if(settings.zone_inactive_switches_off) settings.zone_inactive_switches_off*.off() - if(settings.zone_inactive_switches_on) settings.zone_inactive_switches_on*.on() - } - } else logDebug("no change to Zone (${getZoneName()}) Status (${active ? "Active" : "Inactive"})") -} - -public getZoneHistory(Boolean asObj=false) { - List zHist = (List)getMemStoreItem(zoneHistFLD) ?: [] - List output = [] - if(zHist?.size()) { - zHist.sort { a,b-> b.dt <=> a.dt }.each { h-> - List hList = [] - hList.push([name: "Trigger:", val: h?.evtName]) - hList.push([name: "Device:", val: h?.evtDevice]) - // hList.push([name: "Value:", val: h?.evtValue]) - hList.push([name: "Zone Status:", val: (h?.active ? "Activate" : "Deactivate")]) - hList.push([name: "Conditions Passed:", val: h?.passed]) - hList.push([name: "Conditions Blocks:", val: h?.blocks]) - hList.push([name: "DateTime:", val: h?.dt]) - if(hList.size()) { - output.push(spanSm(kvListToHtmlTable(hList, sCLR4D9), sCLRGRY)) - } - } - } else { output.push("No History Items Found...") } - if(!asObj) { - output.each { i-> paragraph spanSm(i) } - } else { return output } -} - -private static String kvListToHtmlTable(List tabList, String color=sCLRGRY) { - String str = sBLANK - if(tabList?.size()) { - str += "" - tabList.each { it-> - str += "" - } - str += "
    ${spanSmBld((String)it.name)}${spanSmBr("${it.val}")}
    " - } - return str -} - -Map getZoneDevices(String cmd=sNULL, Boolean ignoreDoNotDisturb=false) { - // updDeviceInputs() - List devObj = [] - List devIds = [] - if(cmd==sNULL) cmd='announce' - List devices = parent?.getDevicesFromList(settings.zone_EchoDevices) - //devices?.each { devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]); devIds.push(it?.getId()); } - devices?.each { - Map devInfo = it?.getEchoDevInfo(cmd, ignoreDoNotDisturb) - if(devInfo) { - devObj?.push(devInfo) - devIds.push(it?.getId()) - } else { - logDebug("Uh Oh. did not get devinfo") - } - // devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]) - // devIds.push(it?.getId()) - } - return [devices: devices, devObj: devObj, devIds: devIds]//, jsonStr: new groovy.json.JsonOutput().toJson(devObj)] -} - -public zoneRefreshHandler(evt) { - String cmd = evt?.value - // Map data = evt?.jsonData - switch(cmd) { - case "checkStatus": - checkZoneStatus([name: "zoneRefresh", displayName: "zoneRefresh"]) - scheduleCondition() - break - case "sendStatus": - sendZoneStatus() - break - } -} - -/* - * handle commands sent to a zone - * caller could be actions (a broadcast location event) (actions calls parent for speak or announce to combine across zones) - * or a zone device handler (calling its parent) - */ - -@SuppressWarnings('GroovyFallthrough') -public zoneCmdHandler(evt, Boolean chldDev=false, Boolean ignoreDoNotDisturb=false) { - // log.warn "zoneCmdHandler $evt" - String cmd = evt?.value - Map data = evt?.jsonData - String appId = app?.getId() as String - - // log.warn "zoneCmdHandler cmd: $cmd data: $data appId: $appId" - if(cmd && data && appId && data.zones && data.zones.contains(appId) && data.cmd) { // is the broadcast for me? - if(isPaused(true) || !isActive()) { - logInfo( "zoneCmdHandler: zone is paused or inactive Skipping $evt") - return - } - - // log.trace "zoneCmdHandler | Cmd: $cmd | Data: $data" - if(cmd == 'speak'/* && zoneDevs?.size() >= 2 */ ) { - Boolean fA = true - if(settings.forceAnnounce != null) fA = (Boolean)settings.forceAnnounce - if(fA) cmd = "announcement" - } - - Map zoneDevMap = getZoneDevices(cmd == 'speak' ? 'TTS' : 'announce', ignoreDoNotDisturb) - List zoneDevs = (List)zoneDevMap.devices - - if(data.zoneVolumes && data.zoneVolumes?.size() && data.zoneVolumes[appId]) { - Map zVol = (Map)data.zoneVolumes[appId] - // log.debug "zoneVolume: ${zVol}" - data.changeVol = zVol.change ?: data.changeVol - data.restoreVol = zVol.restore ?: data.restoreVol - } - Integer delay = (Integer)data.delay // ?: null - - // log.warn "zoneCmdHandler cmd: $cmd data: $data zoneDevMap: $zoneDevMap zoneDevs: $zoneDevs" - if(cmd in [ 'speak', 'announcement', 'voicecmd', 'sequence'] && !data.message) { logWarning("Zone Command Message is missing", true); return } - - String tmsg = (String)data.message - String msgp1 = " to Zone (${getZoneName()})" - String msgv1 = "${data.changeVol!=null ? " | Volume: ${data.changeVol}" : sBLANK}${data.restoreVol!=null ? " | Restore Volume: ${data.restoreVol}" : sBLANK}" - String msgd1 = "${delay ? " | Delay: (${delay})" : sBLANK}" - Boolean doFin = false - switch(cmd) { - case "speak": - logDebug("Sending Speak Command: (${tmsg})${msgp1}${msgv1}${msgd1}") - if(data.changeVol!=null || data.restoreVol!=null) { - zoneDevs?.each { dev-> - dev?.setVolumeSpeakAndRestore(data.changeVol, tmsg, data.restoreVol) - } - /* call zone vdevice with finishSendSpeakZ */ - if(!chldDev) relayFinishSpeak([:], 200, [message: tmsg, oldVolume: data.restoreVol, newVolume: data.changeVol]) - } else { - zoneDevs?.each { dev-> - dev?.speak(tmsg) - } - } - break - case "announcement": - if(zoneDevs?.size() > 0 && (List)zoneDevMap.devObj) { - logDebug("Sending Announcement Command: (${tmsg})${msgp1}${msgv1}${msgd1}") - List valS = tmsg.contains("::") ? (List)tmsg.split("::") : [sNULL, tmsg] - String mymsg = valS[1] - String mtitle = data.title ?: (valS[0] ?: getZoneName()) - //NOTE: Only sends command to first device in the list | We send the list of devices to announce one and then Amazon does all the processing - zoneDevs[0]?.sendAnnouncementToDevices(mymsg, mtitle, (List)zoneDevMap.devObj, data.changeVol, data.restoreVol) - /* call zone vdevice with finishAnnounce */ - if(!chldDev) relayFinishAnnouncement(mtitle+'::'+mymsg, [vol: (Integer)data.changeVol, restvol: (Integer)data.restoreVol]) - } - break - - case "voicecmd": - logDebug("Sending VoiceCmdAsText Command: (${tmsg})${msgp1}${msgd1}") - zoneDevs?.each { dev-> - dev?.voiceCmdAsText(tmsg) - } - break - case "sequence": - logDebug("Sending Sequence Command: (${tmsg})${msgp1}${msgd1}") - zoneDevs?.each { dev-> - dev?.executeSequenceCommand(tmsg) - } - break - - case "alarmvolume": - case "volume": - logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgv1}${msgd1}") - if(data.changeVol != null) { - zoneDevs?.each { dev-> - dev?."${data.cmd}"(data.changeVol) - } - /* call zone vdevice with updateLevel */ - if(!chldDev) getEzDevice().each { it.updateLevel(null, data.changeVol) } - } - break - - case "playback": - case "dnd": - case "mute": - case "unmute": - logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgd1}") - zoneDevs?.each { dev-> - if(data.cmd) { dev?."${data.cmd}"() } - } - /* if(!chldDev) todo need to call zone vdevice with mute/unmute updates */ - // getEzDevice().each { it.updateCookies(cookies) } - break - - case "builtin": - case "calendar": - case "weather": - logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgv1}${msgd1}") - zoneDevs?.each { dev-> - if(data.cmd) { dev?."${data.cmd}"(data.changeVol, data.restoreVol) } - } - doFin=true - break - case "sounds": - logDebug("Sending ${data.cmd?.capitalize()} | Name: ${tmsg} Command${msgp1}${msgv1}${msgd1}") - zoneDevs?.each { dev-> - dev?."${data.cmd}"(tmsg, data.changeVol, data.restoreVol) - } - doFin=true - break - case "music": - logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1} | Provider: ${data.provider} | Search: ${data.search}${msgd1}${msgv1}") - zoneDevs?.each { dev -> - dev?."${data.cmd}"(data.search, data.provider, data.changeVol, data.restoreVol) - } - doFin=true - break - } - /* call zone vdevice with updateLevel */ - if(doFin && !chldDev) getEzDevice().each { it.updateLevel(data.restoreVol, data.changeVol) } - } -} -/****************************************** -| Restriction validators -*******************************************/ - -@SuppressWarnings('GroovyFallthrough') -static String attUnit(String attr) { - switch(attr) { - case "humidity": - case "level": - case "battery": - return " percent" - case "temperature": - return " degrees" - case "illuminance": - return " lux" - case "power": - return " watts" - default: - return sBLANK - } -} -/* -Double getDevValueAvg(devs, attr) { - List vals = devs?.findAll { it?.currentValue(attr)?.isNumber() }?.collect { it?.currentValue(attr).toDouble() } - return vals?.size() ? (vals?.sum()/vals?.size())?.round(1) as Double : null -} */ - -String getCurrentMode() { - return location?.mode -} - -List getLocationModes(Boolean sorted=false) { - List modes = location?.modes*.name - // log.debug "modes: ${modes}" - return (sorted) ? modes?.sort() : modes -} - -Boolean isInMode(List modes, Boolean not=false) { - if(modes instanceof String){ - modes = modes.toList() - log.warn("Found bad mode settings $modes, this setting needs to be updated to work properly") - } - return (modes) ? (not ? (!(getCurrentMode() in modes)) : (getCurrentMode() in modes)) : false -} - -Boolean isInAlarmMode(List modes) { - String a = location?.hsmStatus ?: "disarmed" - //return (modes) ? (parent?.getAlarmSystemStatus() in modes) : false - return (modes) ? (a in modes) : false -} -/* -Boolean areAllDevsSame(List devs, String attr, val) { - if(devs && attr) { return (devs?.findAll { it?.currentValue(attr) == val as String }?.size() == devs?.size()) } - return false -} */ - -Boolean allDevAttValsEqual(List devs, String att, val) { - if(devs && att) { - if(val instanceof List) return (devs.findAll { it?.currentValue(att) in val }?.size() == devs.size()) - else return (devs.findAll { it?.currentValue(att) == val }?.size() == devs.size()) - } - return false -} - -Boolean anyDevAttValsEqual(List devs, String att, val) { - if(devs && att) { - if(val instanceof List) return (devs.findAll { it?.currentValue(att) in (List)val }?.size() >= 1) - else return (devs.findAll { it?.currentValue(att) == val }?.size() >= 1) - } - return false -} - -Boolean anyDevAttNumValAbove(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() >= 1) : false -} -Boolean anyDevAttNumValBelow(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() >= 1) : false -} -Boolean anyDevAttNumValBetween(List devs, String att, Double low, Double high) { - return (devs && att && (low < high)) ? (devs?.findAll { - Double t = it?.currentValue(att)?.toDouble() - ( (t >= low) && (t <= high) ) }?.size() >= 1) : false -} -Boolean anyDevAttNumValEqual(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() >= 1) : false -} - -Boolean allDevAttNumValsAbove(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsBelow(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsBetween(List devs, String att, Double low, Double high) { - return (devs && att && (low < high)) ? (devs?.findAll { - Double t = it?.currentValue(att)?.toDouble() - ( (t >= low) && (t <= high) ) }?.size() == devs?.size()) : false -} -Boolean allDevAttNumValsEqual(List devs, String att, Double val) { - return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() == devs?.size()) : false -} -/* -Boolean devAttValEqual(List devs, String devId, String att, val) { - if(devs && att) { return (devs.find { it?.currentValue(att) == val }) } - return false -} */ - -static String getAlarmSystemName(Boolean abbr=false) { - return (abbr ? "HSM" : "Hubitat Safety Monitor") -} - -public Map getZoneMetrics() { - Map out = [:] - out.version = appVersionFLD - out.activeDelay = settings.zone_active_delay ?: 0 - out.inactiveDelay = settings.zone_inactive_delay ?: 0 - out.zoneDevices = settings.zone_EchoDevices ?: [] - out.activeSwitchesOnCnt = settings.zone_active_switches_on ?: [] - out.activeSwitchesOffCnt = settings.zone_active_switches_off ?: [] - out.inactiveSwitchesOnCnt = settings.zone_inactive_switches_on ?: [] - out.inactiveSwitchesOffCnt = settings.zone_inactive_switches_off ?: [] - return out -} - -/****************************************** -| Time and Date Conversion Functions -*******************************************/ -String formatDt(Date dt, Boolean tzChg=true) { - return dateTimeFmt(dt, "E MMM dd HH:mm:ss z yyyy", tzChg) -} - -String dateTimeFmt(Date dt, String fmt, Boolean tzChg=true) { -// if(!(dt instanceof Date)) { try { dt = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dt?.toString()) } catch(e) { dt = Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } } - def tf = new SimpleDateFormat(fmt) - if(tzChg && location?.timeZone) { tf.setTimeZone((TimeZone)location?.timeZone) } - return (String)tf.format(dt) -} - -String convToTime(Date dt) { - String newDt = dateTimeFmt(dt, "h:mm a") - if(newDt?.contains(":00 ")) { newDt?.toString()?.replaceAll(":00 ", sSPACE) } - return newDt -} - -static Date parseDate(String dt) { return Date.parse("E MMM dd HH:mm:ss z yyyy", dt) } - -//static Boolean isDateToday(Date dt) { return (dt && dt.clearTime().compareTo(new Date().clearTime()) >= 0) } -static Boolean isDateToday(Date dt) { return (dt && dt.clearTime() >= new Date().clearTime()) } - -static String strCapitalize(String str) { return str ? str.toString().capitalize() : sNULL } - -static String pluralizeStr(List obj, Boolean para=true) { return (obj?.size() > 1) ? "${para ? "(s)": "s"}" : sBLANK } - -String getDtNow() { - Date now = new Date() - return formatDt(now) -} - -String epochToTime(Date dt) { - return dateTimeFmt(dt, "h:mm a") -} - -String fmtTime(t, String fmt="h:mm a", Boolean altFmt=false) { - if(!t) return sNULL - Date dt = new Date().parse(altFmt ? "E MMM dd HH:mm:ss z yyyy" : "yyyy-MM-dd'T'HH:mm:ss.SSSZ", t.toString()) - return dateTimeFmt(dt, fmt) -} - -Long GetTimeDiffSeconds(String lastDate, String sender=sNULL) { - try { - if(lastDate?.contains("dtNow")) { return 10000 } - Date lastDt = parseDate(lastDate) // Date.parse("E MMM dd HH:mm:ss z yyyy", lastDate) - Long start = lastDt.getTime() - Long stop = now() - Long diff = Math.round((stop - start) / 1000L) - return diff.abs() - } - catch (ex) { - logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : sBLANK}lastDate: $lastDate): ${ex?.message}", false, ex) - return 10000L - } -} - -String getWeekDay() { - def df = new SimpleDateFormat("EEEE") - df.setTimeZone((TimeZone)location?.timeZone) - return (String)df.format(new Date()) -} -/* -String getWeekMonth() { - def df = new java.text.SimpleDateFormat("W") - df.setTimeZone(location?.timeZone) - return (String)df.format(new Date()) -} - -String getDay() { - def df = new java.text.SimpleDateFormat("D") - df.setTimeZone(location?.timeZone) - return (String)df.format(new Date()) -} - -String getYear() { - def df = new java.text.SimpleDateFormat("yyyy") - df.setTimeZone(location?.timeZone) - return (String)df.format(new Date()) -} -*/ -String getMonth() { - def df = new SimpleDateFormat("MMMMM") - df.setTimeZone((TimeZone)location?.timeZone) - return (String)df.format(new Date()) -} -/* -String getWeekYear() { - def df = new java.text.SimpleDateFormat("w") - df.setTimeZone(location?.timeZone) - return (String)df.format(new Date()) -} */ -/* -Map getDateMap() { - return [d: getWeekDay(), dm: getDay(), wm: getWeekMonth(), wy: getWeekYear(), m: getMonth(), y: getYear() ] -} */ - -Boolean isDayOfWeek(List opts) { - String day = getWeekDay() - return ( opts?.contains(day) ) -} - -Boolean isMonthOfYear(List opts) { - String mon = getMonth() - return ( opts?.contains(mon) ) -} - -Boolean isTimeOfDay(String startTime, String stopTime) { - if(!startTime && !stopTime) { return true } - Date st = toDateTime(startTime) - Date et = toDateTime(stopTime) - return timeOfDayIsBetween(st, et, new Date(), location.timeZone) -} - -Boolean advLogsActive() { return ((Boolean)settings.logDebug || (Boolean)settings.logTrace) } -public void logsEnabled() { if(advLogsActive() && !getTsVal("logsEnabled")) { logTrace("enabling logging timer"); updTsVal("logsEnabled") } } -public void logsDisable() { - if(advLogsActive()) { - Integer dtSec = getLastTsValSecs("logsEnabled", null) - if(dtSec && (dtSec > 3600*6)) { - settingUpdate("logDebug", sFALSE, sBOOL) - settingUpdate("logTrace", sFALSE, sBOOL) - remTsVal("logsEnabled") - log.debug "Disabling debug logs" - } - } -} - -public void enableDebugLog() { settingUpdate("logDebug", sTRUE, sBOOL); logInfo("Debug Logs Enabled From Main App...") } -public void disableDebugLog() { settingUpdate("logDebug", sFALSE, sBOOL); logInfo("Debug Logs Disabled From Main App...") } -public void enableTraceLog() { settingUpdate("logTrace", sTRUE, sBOOL); logInfo("Trace Logs Enabled From Main App...") } -public void disableTraceLog() { settingUpdate("logTrace", sFALSE, sBOOL); logInfo("Trace Logs Disabled From Main App...") } - -@Field volatile static Map tsDtMapFLD=[:] - -private void updTsVal(String key, String dt=sNULL) { - String appId=app.getId() - Map data=tsDtMapFLD[appId] ?: [:] - if(!data) data = (Map)state.tsDtMap ?: [:] - if(key) { data[key] = dt ?: getDtNow() } - tsDtMapFLD[appId]=data - tsDtMapFLD=tsDtMapFLD - - state.tsDtMap = data -} - -private void remTsVal(key) { - String appId=app.getId() - Map data=tsDtMapFLD[appId] ?: [:] - if(!data) data = (Map)state.tsDtMap ?: [:] - if(key) { - if(key instanceof List) { - List aa = (List)key - aa.each { String k-> - if(data.containsKey(k)) { data.remove(k) } - } - } else if(data.containsKey((String)key)) { data.remove((String)key) } - } - tsDtMapFLD[appId]=data - tsDtMapFLD=tsDtMapFLD - - state.tsDtMap = data -} - -String getTsVal(String key) { - String appId=app.getId() - Map tsMap=tsDtMapFLD[appId] - if(!tsMap) tsMap = (Map)state.tsDtMap ?: [:] - if(key && tsMap && tsMap[key]) { return (String)tsMap[key] } - return sNULL -} - -Integer getLastTsValSecs(String val, Integer nullVal=1000000) { - return (val && getTsVal(val)) ? GetTimeDiffSeconds(getTsVal(val)).toInteger() : nullVal -} - -private void updAppFlag(String key, Boolean val) { - def data = atomicState?.appFlagsMap ?: [:] - if(key) { data[key] = val } - atomicState?.appFlagsMap = data -} - -@SuppressWarnings('unused') -private void remAppFlag(key) { - Map data = atomicState?.appFlagsMap ?: [:] - if(key) { - if(key instanceof List) { - List aa = (List)key - aa.each { String k-> if(data.containsKey(k)) { data.remove(k) } } - } else { if(data.containsKey(key)) { data.remove(key) } } - atomicState.appFlagsMap = data - } -} - -Boolean getAppFlag(String val) { - Map aMap = atomicState?.appFlagsMap - if(val && aMap && aMap[val]) { return (Boolean)aMap[val] } - return false -} - -@SuppressWarnings('unused') -private stateMapMigration() { - //Timestamp State Migrations - Map tsItems = [:] - tsItems?.each { k, v-> if(state.containsKey(k)) { updTsVal(v, (String)state[k]); state.remove(k) } } - - //App Flag Migrations - Map flagItems = [:] - flagItems?.each { k, v-> if(state.containsKey(k)) { updAppFlag(v, (Boolean)state[k]); state.remove(k) } } - updAppFlag("stateMapConverted", true) -} - -/****************************************** -| App Input Description Functions -*******************************************/ -/* -String unitStr(String att) { - switch(att) { - case "temp": - return "\u00b0${getTemperatureScale() ?: "F"}" - case "humidity": - case "battery": - return "%" - default: - return sBLANK - } -} */ - -String getAppNotifDesc(Boolean hide=false) { - String str = sBLANK - if(isZoneNotifConfigured()) { - Boolean ok = getOk2Notify() - str += hide ? sBLANK : spanSmBr(strUnder("Send allowed: ") + getOkOrNotSymHTML(ok)) - str += ((List)settings.notif_devs) ? spanSmBr(" ${sBULLET} Notification Device${pluralizeStr((List)settings.notif_devs)} (${((List)settings.notif_devs).size()})") : sBLANK - str += (Boolean)settings.notif_alexa_mobile ? spanSmBr(" ${sBULLET} Alexa Mobile App") : sBLANK - String res = getNotifSchedDesc(true) - str += res ? spanSmBr(res) : sBLANK - } - return str != sBLANK ? str : sNULL -} - -List getQuietDays() { - List allDays = weekDaysEnum() - List curDays = (List)settings.notif_days ?: [] - return allDays?.findAll { (!curDays?.contains(it)) } -} - -@SuppressWarnings('GroovyVariableNotAssigned') -String getNotifSchedDesc(Boolean min=false) { - String startType = settings.notif_time_start_type - Date startTime - String stopType = settings.notif_time_stop_type - Date stopTime - List dayInput = (List)settings.notif_days - List modeInput = (List)settings.notif_modes - String str = sBLANK - - if(startType && stopType) { - startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null - stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null - } - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L - Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - Boolean timeOk = notifTimeOk() - Boolean daysOk = dayInput ? (isDayOfWeek(dayInput)) : true - Boolean modesOk = modeInput ? (isInMode(modeInput)) : true - Boolean rest = !(daysOk && modesOk && timeOk) - String startLbl = startTime ? epochToTime(startTime) : sBLANK - String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK - str += (startLbl && stopLbl) ? spanSmBr(" ${sBULLET} Restricted Times: ${startLbl} - ${stopLbl} " + getOkOrNotSymHTML(!timeOk)) : sBLANK - List qDays = getQuietDays() - str += dayInput && qDays ? spanSmBr(" ${sBULLET} Restricted Day${pluralizeStr(qDays, false)}: (${qDays?.join(", ")}) " + getOkOrNotSymHTML(!daysOk)) : sBLANK - str += modeInput ? spanSm(" ${sBULLET} Allowed Mode${pluralizeStr(modeInput, false)}: (${modeInput?.join(", ")}) " + getOkOrNotSymHTML(!modesOk)) : sBLANK - str = str ? spanSmBr(" ${sBULLET} Restrictions Active: " + getOkOrNotSymHTML(rest)) + spanSm(str) : sBLANK - return (str != sBLANK) ? divSm(str, sCLR4D9) : sNULL -} - -String getOverallDesc() { - Map condStatus = conditionStatus() - String str = spanSmBld("Zone is ") + spanSmBr( ((Boolean)condStatus.ok ? "Active " : "Inactive ") + getOkOrNotSymHTML((Boolean)condStatus.ok)) - return str -} - -String getConditionsDesc(Boolean addFoot=true) { - Boolean confd = conditionsConfigured() - String sPre = "cond_" - String str = sBLANK - if(confd) { - str = getOverallDesc() - str += spanSmBr(" ${sBULLET} " + spanSmBld("${reqAllCond() ? "All Conditions Required" : "Any Condition Allowed"}")) - if((Boolean)timeCondConfigured()) { - str += spanSmBr(" ${sBULLET} Time Between Allowed: " + getOkOrNotSymHTML(timeCondOk())) - str += spanSmBr(" - ${getTimeCondDesc(false)}") - } - if((Boolean)dateCondConfigured()) { - str += spanSmBr(" ${sBULLET} Date:") - str += (List)settings.cond_days ? spanSmBr(" - Days Allowed: " + getOkOrNotSymHTML(isDayOfWeek((List)settings.cond_days))) : sBLANK - str += (List)settings.cond_months ? spanSmBr(" - Months Allowed: " + getOkOrNotSymHTML(isMonthOfYear((List)settings.cond_months))) : sBLANK - } - if((List)settings.cond_alarm || (List)settings.cond_mode) { - str += spanSmBr(" ${sBULLET} Location: " + getOkOrNotSymHTML(locationCondOk())) - String a = location?.hsmStatus ?: "disarmed" - str += (List)settings.cond_alarm ? spanSmBr(" - Alarm Mode ${a} in: ${(List)settings.cond_alarm} " + getOkOrNotSymHTML(isInAlarmMode((List)settings.cond_alarm))) : sBLANK - Boolean not = ((String)settings.cond_mode_cmd == "not") - str += settings.cond_mode ? spanSmBr(" - Mode ${getCurrentMode()} (${not ? "not in" : "in"}): ${(List)settings.cond_mode} " + getOkOrNotSymHTML(isInMode((List)settings.cond_mode, not))) : sBLANK - } - if(deviceCondConfigured()) { - List devConds = lDATTSTR + lDATTNUM - devConds.each { String evt-> - if(devCondConfigured(evt)) { - Boolean condOk = false - if(evt in lDATTSTR) { condOk = checkDeviceCondOk(evt) } - else if(evt in lDATTNUM) { condOk = checkDeviceNumCondOk(evt) } - - List devs = settings."${sPre}${evt}" ?: null - if(devs){ - List myV = [] - if(!addFoot) devs.each { dev -> myV.push(dev?.currentValue(evt)) } - str += spanSmBr(" ${sBULLET} ${evt?.capitalize()} (${settings."${sPre}${evt}"?.size()}) ${!addFoot ? myV : sBLANK} " + getOkOrNotSymHTML(condOk)) - } - - String a = " - Desired Value: " - String cmd = (String)settings."${sPre}${evt}_cmd" ?: sNULL - if(cmd in [sBETWEEN, sBELOW, sABOVE, sEQUALS]) { - def cmdLow = settings."${sPre}${evt}_low" ?: null - def cmdHigh = settings."${sPre}${evt}_high" ?: null - def cmdEq = settings."${sPre}${evt}_equal" ?: null - str += (cmd == sEQUALS && cmdEq) ? spanSmBr(a+"( =${cmdEq}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK - str += (cmd == sBETWEEN && cmdLow && cmdHigh) ? spanSmBr(a+"(${cmdLow-cmdHigh}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK - str += (cmd == sABOVE && cmdHigh) ? spanSmBr(a+"( >${cmdHigh}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK - str += (cmd == sBELOW && cmdLow) ? spanSmBr(a+"( <${cmdLow}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK - } else { - str += cmd ? spanSmBr(a+"(${cmd})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK - } - str += ((Boolean)settings."${sPre}${evt}_all" == true) ? spanSmBr(" - Require All: (${settings."${sPre}${evt}_all"})") : sBLANK - } - } - } - str += addFoot ? inputFooter(sTTM) : sBLANK - } else { - str += addFoot ? inputFooter(sTTC, sCLRGRY, true) : sBLANK - } - return str -} - -String getZoneDesc() { - if(devicesConfigured() && conditionsConfigured()) { - List eDevs = parent?.getDevicesFromList(settings.zone_EchoDevices)?.collect { it?.displayName as String } - String str = eDevs?.size() ? "Echo Devices in Zone:\n${eDevs?.join("\n")}\n" : sBLANK - str += settings.zone_active_delay ? bulletItem(sBLANK, "Activate Delay: (${settings.zone_active_delay})\n)") : sBLANK - str += settings.zone_inactive_delay ? bulletItem(sBLANK, "Deactivate Delay: (${settings.zone_inactive_delay})\n") : sBLANK - str += "\n"+sTTM - return str - } else { - return sTTC - } -} - -@SuppressWarnings('GroovyVariableNotAssigned') -String getTimeCondDesc(Boolean addPre=true) { - Date startTime - Date stopTime - String startType = settings.cond_time_start_type - String stopType = settings.cond_time_stop_type - if(startType && stopType) { - startTime = startType == 'time' && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null - stopTime = stopType == 'time' && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null - } - - String startLbl1 = sBLANK - String stopLbl1 = sBLANK - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - def sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L - Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - startLbl1 = startType.capitalize() + sSPACE + "${startoffset ? "with offset " : sBLANK}" - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - stopLbl1 = stopType.capitalize() + sSPACE + "${stopoffset ? "with offset " : sBLANK}" - } - } - String startLbl = startTime ? epochToTime(startTime) : sBLANK - String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK - - return startLbl && stopLbl ? "${addPre ? "Time Condition:\n" : sBLANK}(${startLbl1}${startLbl} - ${stopLbl1}${stopLbl})" : sTTC -} - -static String getInputToStringDesc(inpt, addSpace = null) { - Integer cnt = 0 - String str = sBLANK - if(inpt) { - inpt.sort().each { item -> - cnt = cnt+1 - str += item ? (((cnt < 1) || (inpt.size() > 1)) ? "\n ${item}" : "${addSpace ? " " : sBLANK}${item}") : sBLANK - } - } - //log.debug "str: $str" - return str != sBLANK ? str : sNULL -} - -static String randomString(Integer len) { - def pool = ["a".."z",0..9].flatten() - Random rand = new Random(new Date().getTime()) - def randChars = (0..len).collect { pool[rand.nextInt(pool.size())] } - // logDebug("randomString: ${randChars?.join()}") - return randChars.join() -} - -static def getRandomItem(items) { - List list = new ArrayList() - items?.each { list.add(it) } - return list?.get(new Random().nextInt(list?.size())) -} - -/*********************************************************************************************************** - HELPER FUNCTIONS -************************************************************************************************************/ -void settingUpdate(String name, value, String type=sNULL) { - if(name && type) { app?.updateSetting(name, [type: type, value: value]) } - else if (name) { app?.updateSetting(name, value) } -} - -void settingRemove(String name) { - logTrace("settingRemove($name)...") - if(name && settings.containsKey(name)) { app?.removeSetting(name) } -} - -static List weekDaysEnum() { return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] } - -static Map daysOfWeekMap() { return ["MON":"Monday", "TUE":"Tuesday", "WED":"Wednesday", "THU":"Thursday", "FRI":"Friday", "SAT":"Saturday", "SUN":"Sunday"] } - -static List monthEnum() { return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] } - -static Map monthMap() { return ["1":"January", "2":"February", "3":"March", "4":"April", "5":"May", "6":"June", "7":"July", "8":"August", "9":"September", "10":"October", "11":"November", "12":"December"] } - -static Map getAlarmTrigOpts() { - return ["armedAway":"Armed Away", "armingAway":"Arming Away Pending exit delay","armedHome":"Armed Home","armingHome":"Arming Home pending exit delay", "armedNight":"Armed Night", "armingNight":"Arming Night pending exit delay","disarmed":"Disarmed", "allDisarmed":"All Disarmed","alerts":"Alerts"] -} - -Integer getLastNotifMsgSec() { return !state.lastNotifMsgDt ? 100000 : GetTimeDiffSeconds((String)state.lastNotifMsgDt, "getLastMsgSec").toInteger() } -Integer getLastChildInitRefreshSec() { return !state.lastChildInitRefreshDt ? 3600 : GetTimeDiffSeconds((String)state.lastChildInitRefreshDt, "getLastChildInitRefreshSec").toInteger() } - -Boolean getOk2Notify() { - Boolean smsOk = false // = (settings.notif_sms_numbers?.toString()?.length()>=10) - Boolean pushOk = false // = settings.notif_send_push - Boolean pushOver = false // = (settings.notif_pushover && settings.notif_pushover_devices) - Boolean alexaMsg = (Boolean)settings.notif_alexa_mobile - Boolean notifDevsOk = (((List)settings.notif_devs)?.size()) - Boolean daysOk = (List)settings.notif_days ? (isDayOfWeek((List)settings.notif_days)) : true - Boolean timeOk = notifTimeOk() - Boolean modesOk = (List)settings.notif_modes ? (isInMode((List)settings.notif_modes)) : true - Boolean result - result = (smsOk || pushOk || alexaMsg || notifDevsOk || pushOver) - if(!(daysOk && modesOk && timeOk)) { result = false } - logDebug("getOk2Notify() RESULT: $result | notifDevsOk: $notifDevsOk | smsOk: $smsOk | pushOk: $pushOk | pushOver: $pushOver | alexaMsg: $alexaMsg || daysOk: $daysOk | timeOk: $timeOk | modesOk: $modesOk") - return result -} - -Boolean notifTimeOk() { - Date startTime - Date stopTime - // these are quiet time start/stop - String startType = settings.notif_time_start_type - String stopType = settings.notif_time_stop_type - if(startType && stopType) { - startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null - stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null - } else { return true } - - Date now = new Date() - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L - Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - - if(startTime && stopTime) { - Boolean not = startTime.getTime() > stopTime.getTime() - Boolean isBtwn = !timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) - isBtwn = not ? !isBtwn : isBtwn - logTrace("NotifTimeOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not ? startTime:stopTime})") - return isBtwn - } else { return true } -} - -// Sends the notifications based on app settings -public Boolean sendNotifMsg(String msgTitle, String msg, alexaDev=null, Boolean showEvt=true) { - logTrace("sendNotifMsg() | msgTitle: ${msgTitle}, msg: ${msg}, $alexaDev, showEvt: ${showEvt}") - List sentSrc = [] // ["Push"] - Boolean sent = false - try { - String newMsg = "${msgTitle}: ${msg}" - String flatMsg = newMsg.toString().replaceAll("\n", sSPACE) - if(!getOk2Notify()) { - logInfo( "sendNotifMsg: Notification not configured or Message Skipped During Quiet Time ($flatMsg)") - //if(showEvt) { sendNotificationEvent(newMsg) } - } else { - if((List)settings.notif_devs) { - sentSrc.push("Notification Devices") - ((List)settings.notif_devs)?.each { it?.deviceNotification(newMsg) } - sent = true - } - if((Boolean)settings.notif_alexa_mobile && alexaDev) { - alexaDev?.sendAlexaAppNotification(newMsg) - sentSrc.push("Alexa Mobile App") - sent = true - } - if(sent) { - state.lastNotificationMsg = flatMsg - updTsVal("lastNotifMsgDt") - logDebug("sendNotifMsg: Sent ${sentSrc} (${flatMsg})") - } - } - } catch (ex) { - logError("sendNotifMsg $sentstr Exception: ${ex}", false, ex) - } - return sent -} - -Boolean isZoneNotifConfigured() { - return ((String)settings.notif_active_message || (String)settings.notif_inactive_message) && - ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) - -} - -static Integer versionStr2Int(String str) { return str ? str.replaceAll("\\.", sBLANK)?.toInteger() : null } - -Boolean minVersionFailed() { - try { - Integer minDevVer = (Integer)parent?.minVersions()["zoneApp"] - return minDevVer != null && versionStr2Int(appVersionFLD) < minDevVer - } catch (ignored) { - return false - } -} - -// public String gitBranch() { -// if(gitBranchFLD == sNULL) { gitBranchFLD = (String) parent?.gitBranch() } -// return (String)gitBranchFLD -// } - -Boolean isActive() { - Boolean st = (Boolean)state.zoneConditionsOk - return st != null ? st : (Boolean)conditionStatus().ok -} - -Boolean isPaused(Boolean chkAll = false) { return (Boolean)settings.zonePause && (chkAll ? !((Boolean)state.dupPendingSetup == true) : true) } - -static String documentationUrl() { return "https://tonesto7.github.io/echo-speaks-docs" } -static String videoUrl() { return "https://www.youtube.com/watch?v=wQPPlTFaGb4&ab_channel=SimplySmart123%E2%9C%85" } - -static String getAppImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/${imgName}.png" } - -static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } - -static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

    ${i ? " " : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}

    " } - -static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

    ${i ? " " : sBLANK} ${t?.replaceAll("\n", "
    ")}

    ${st ? "${st}" : sBLANK}" } - -static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
    " : sBLANK}${bold ? "" : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}${color ? "
    " : sBLANK}" } - -@SuppressWarnings('unused') -static String inTS1(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return spanSmBldUnd(str, clr, img) } -static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str.replaceAll("\n", sSPACE).replaceAll("
    ", sSPACE), und), clr, img) } - -// Root HTML Objects -static String sectHead(String str, String img = sNULL) { return str ? "

    " + spanImgStr(img) + span(str, "darkorange", sNULL, true) + "

    " + "
    " : sBLANK } -static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } -static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
    ${str}
    ${br ? sLINEBR : sBLANK}" : sBLANK } -static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } -static String divImgStr(String str, String img=sNULL) { return str ? div(img ? spanImg(img) + span(str) : str) : sBLANK } -static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } -static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } -static String htmlLine(String color=sCLR4D9) { return "
    " } -static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } -static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? ((noBr ? sBLANK : lineBr()) + divSmBld(str, clr)) : sBLANK } -static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } -static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } -static String optPrefix() { return spanSm(" (Optional)", "violet") } -// - -// Custom versions of the root objects above -static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } -static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } -static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } -static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } -static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } -static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } -static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } -static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } -static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } -static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } -static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } -static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } - - -static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } -static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } -static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } -static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } -static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } -static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } -static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } -static String bulletItem(String inStr, String strVal) { return "${inStr == sBLANK ? sBLANK : "\n"}"+sSPACE+sBULLET+sSPACE+strVal } -static String dashItem(String inStr, String strVal, Boolean newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : "\n"} - "+strVal } - -Integer stateSize() { - String j = new JsonOutput().toJson((Map)state) - return j.length() -} -Integer stateSizePerc() { return (Integer)(((stateSize() / 100000)*100).toDouble().round(0)) } - -private addToLogHistory(String logKey, String data, Integer max=10) { - Boolean ssOk = true //(stateSizePerc() > 70) - List eData = getMemStoreItem(logKey) - if(eData.find { it?.data == data }) { return } - eData.push([dt: getDtNow(), data: data]) - Integer lsiz = eData.size() - if(!ssOk || lsiz > max) { eData = eData?.drop( (lsiz-max) ) } - updMemStoreItem(logKey, eData) -} - -public Map getLogConfigs() { - return [ - info: (Boolean) settings.logInfo, - warn: (Boolean) settings.logWarn, - error: (Boolean) settings.logError, - debug: (Boolean) settings.logDebug, - trace: (Boolean) settings.logTrace, - ] -} - -private void logDebug(String msg) { if((Boolean)settings.logDebug) { log.debug logPrefix(msg, "purple") } } -private void logInfo(String msg) { if((Boolean)settings.logInfo != false) { log.info sSPACE + logPrefix(msg, "#0299b1") } } -private void logTrace(String msg) { if((Boolean)settings.logTrace) { log.trace logPrefix(msg, sCLRGRY) } } -private void logWarn(String msg, Boolean noHist=false) { if((Boolean)settings.logWarn != false) { log.warn sSPACE + logPrefix(msg, sCLRORG) }; if(!noHist) { addToLogHistory("warnHistory", msg, 15) } } - -void logError(String msg, Boolean noHist=false, ex=null) { - if((Boolean)settings.logError != false) { - log.error logPrefix(msg, sCLRRED) - String a - try { - if (ex) a = getExceptionMessageWithLine(ex) - } catch (ignored) { - } - if(a) log.error logPrefix(a, sCLRRED) - } - if(!noHist) { addToLogHistory("errorHistory", msg, 15) } -} - -static String logPrefix(String msg, String color = sNULL) { - return span("Zone (v" + appVersionFLD + ") | ", sCLRGRY) + span(msg, color) -} - -@SuppressWarnings('unused') -private Map getLogHistory() { - List warn = getMemStoreItem("warnHistory") - List errs = getMemStoreItem("errorHistory") - return [ warnings: []+warn, errors: []+errs ] -} - -@SuppressWarnings('unused') -private void clearHistory() { historyMapFLD = [:]; mb() } - -// FIELD VARIABLE FUNCTIONS -private void updMemStoreItem(String key, List val) { - String appId = app.getId() - Map memStore = historyMapFLD[appId] ?: [:] - memStore[key] = val - historyMapFLD[appId] = memStore - historyMapFLD = historyMapFLD - // log.debug("updMemStoreItem(${key}): ${memStore[key]}") -} - -private List getMemStoreItem(String key){ - String appId = app.getId() - Map memStore = historyMapFLD[appId] ?: [:] - return (List)memStore[key] ?: [] -} -// Memory Barrier -@Field static Semaphore theMBLockFLD=new Semaphore(0) - -static void mb(/*String meth=sNULL*/){ - if((Boolean)theMBLockFLD.tryAcquire()){ - theMBLockFLD.release() - } -} - -static 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 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 if(obj instanceof Date) {return "Date"} - else { return "unknown"} -} - -//******************************************************************* -// CLONE CHILD LOGIC -//******************************************************************* -public Map getSettingsAndStateMap() { - Map typeObj = parent?.getAppDuplTypes() - Map setObjs = [:] - if(typeObj) { - ((Map>)typeObj.stat).each { sk, sv-> - sv?.each { svi-> if(settings.containsKey(svi)) { setObjs[svi] = [type: sk, value: settings[svi] ] } } - } - ((Map>)typeObj.ends).each { ek, ev-> - ev?.each { evi-> - settings.findAll { it?.key?.endsWith(evi) }?.each { String fk, fv-> - def vv = settings[fk] // fv - if(ek==sTIME) vv = dateTimeFmt((Date)toDateTime(vv), "HH:mm") - setObjs[fk] = [type: ek, value: vv] - } - } - } - ((Map)typeObj.caps).each { ck, cv-> - settings.findAll { it.key.endsWith(ck) }?.each { String fk, fv-> - setObjs[fk] = [type: "capability", value: (fv instanceof List ? fv?.collect { it?.id?.toString() } : it?.id?.toString ) ] } //.toString().toList() - } - ((Map)typeObj.dev).each { dk, dv-> - settings.findAll { it.key.endsWith(dk) }?.each { String fk, fv-> - setObjs[fk] = [type: "device", value: (fv instanceof List ? fv.collect { it?.id?.toString() } : it?.id?.toString() ) ] } //.toString().toList() - } - } - Map data = [:] - String newlbl = ((String)app?.getLabel())?.replace(" (Z ${sPAUSESymFLD})", sBLANK) - data.label = newlbl?.replace(" (Z)", sBLANK) - data.settings = setObjs - - List stskip = [ - // "isInstalled", "isParent", - "lastNotifMsgDt", "lastNotificationMsg", "setupComplete", "valEvtHistory", "warnHistory", "errorHistory", - "appData", "actionHistory", "authValidHistory", "deviceRefreshInProgress", "noticeData", "installData", "herokuName", "zoneHistory", - // actions - "tierSchedActive", - // zones - "zoneConditionsOk", "configStatusMap", "tsDtMap", "dateInstalled", "handleGuardEvents", "startTime", "stopTime", "alexaGuardState", "appFlagsMap", - "dupPendingSetup", "dupOpenedByUser" - ] - data.state = state?.findAll { !((String)it?.key in stskip) } - return data -} +/** + * Echo Speaks - Zones (Hubitat) + * + * Copyright 2018, 2019, 2020, 2021, 2022 Anthony Santilli + * Code Contributions by @nh.schottfam + * + * 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. + * + */ +//file:noinspection GroovyUnusedAssignment +//file:noinspection unused +//file:noinspection GroovySillyAssignment +//file:noinspection GrMethodMayBeStatic + +import groovy.json.JsonOutput +import groovy.transform.Field + +import java.text.SimpleDateFormat +import java.util.concurrent.Semaphore + +//************************************************ +//* STATIC VARIABLES * +//************************************************ +@Field static final String appVersionFLD = '4.2.0.2' +@Field static final String appModifiedFLD = '2022-04-18' +@Field static final String sNULL = (String)null +@Field static final String sBLANK = '' +@Field static final String sSPACE = ' ' +@Field static final String sBULLET = '\u2022' +//@Field static final String sBULLETINV = '\u25E6' +//@Field static final String sSQUARE = '\u29C8' +//@Field static final String sPLUS = '\u002B' +//@Field static final String sRIGHTARR = '\u02C3' +@Field static final String okSymFLD = "\u2713" +@Field static final String notOkSymFLD = "\u2715" +@Field static final String sPAUSESymFLD = "\u275A\u275A" +@Field static final String sLINEBR = '
    ' +@Field static final String sFALSE = 'false' +@Field static final String sTRUE = 'true' +@Field static final String sBOOL = 'bool' +@Field static final String sENUM = 'enum' +@Field static final String sNUMBER = 'number' +@Field static final String sTEXT = 'text' +@Field static final String sTIME = 'time' +@Field static final String sMODE = 'mode' +//@Field static final String sCOMPLT = 'complete' +@Field static final String sMEDIUM = 'medium' +@Field static final String sSMALL = 'small' +@Field static final String sCLR4D9 = '#2784D9' +@Field static final String sCLRRED = 'red' +@Field static final String sCLRRED2 = '#cc2d3b' +@Field static final String sCLRGRY = 'gray' +//@Field static final String sCLRGRN = 'green' +@Field static final String sCLRGRN2 = '#43d843' +@Field static final String sCLRORG = 'orange' +@Field static final String sTTM = 'Tap to modify...' +@Field static final String sTTC = 'Tap to configure...' +//@Field static final String sTTCR = 'Tap to configure (Required)' +//@Field static final String sTTP = 'Tap to proceed...' +//@Field static final String sTTS = 'Tap to select...' +@Field static final String sSETTINGS = 'settings' +@Field static final String sRESET = 'reset' +@Field static final String sEXTNRL = 'external' +@Field static final String sDEBUG = 'debug' +@Field static final String sSWITCH = 'switch' +@Field static final String sCHKBOX = 'checkbox' +@Field static final String sCOMMAND = 'command' +@Field static final String zoneHistFLD = 'zoneHistory' +@Field static final List lSUNRISESET = ["sunrise", "sunset"] + +//************************************************ +//* IN-MEMORY ONLY VARIABLES * +//* (Cleared only on HUB REBOOT or CODE UPDATES) * +//************************************************ +@Field volatile static Map historyMapFLD = [:] +// @Field volatile static String gitBranchFLD = null + +static String appVersion() { return appVersionFLD } +static String appVersionDt() { return appModifiedFLD } + +definition( + name : "Echo Speaks - Zones", + namespace : "tonesto7", + author : "Anthony Santilli", + description : "DO NOT INSTALL FROM MARKETPLACE\n\nAllows you to create virtual broadcast zones based on your echo devices using device/location events to active the zone.", + category : "My Apps", + parent : "tonesto7:Echo Speaks", + iconUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", + iconX2Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", + iconX3Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/es_groups.png", + importUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/apps/echo-speaks-zones.groovy", + documentationLink : documentationUrl(), + videoLink : videoUrl() +) + +preferences { + page(name: "startPage") + page(name: "uhOhPage") + page(name: "codeUpdatePage") + page(name: "mainPage") + page(name: "prefsPage") + page(name: "conditionsPage") + page(name: "condTimePage") + page(name: "zoneNotifPage") + page(name: "zoneNotifTimePage") + page(name: "zoneHistoryPage") + page(name: "uninstallPage") + page(name: "namePage") +} + +def startPage() { + if(parent != null) { + if(!(Boolean)state.isInstalled && !(Boolean)parent?.childInstallOk()) { return uhOhPage() } + else { + updDeviceInputs() + return (minVersionFailed()) ? codeUpdatePage() : mainPage() + } + } else { return uhOhPage() } +} + +def codeUpdatePage () { + return dynamicPage(name: "codeUpdatePage", title: "Update is Required", install: false, uninstall: false) { + section() { paragraph spanSmBld("Looks like your Zone App needs an update

    Please make sure all app and device code is updated to the most current version

    Once updated your zones will resume normal operation.", sCLRRED) } + } +} + +def uhOhPage () { + return dynamicPage(name: "uhOhPage", title: "This install Method is Not Allowed", install: false, uninstall: true) { + section() { + paragraph spanBld("HOUSTON WE HAVE A PROBLEM!

    Echo Speaks - Zones can't be directly installed from the Marketplace.

    Please use the Echo Speaks SmartApp to configure them.", sCLRRED) + } + } +} + +def appInfoSect() { + String instDt = state.dateInstalled ? fmtTime((String)state.dateInstalled, "MMM dd '@' h:mm a", true) : sNULL + String str = spanBld((String)app.name, "black", "es_groups") + str += (app.label && (String)app.label != (String)app.name) ? spanBldBr(" (${app.label.replace(" (Z)", sBLANK)})") : sLINEBR + str += spanSmBld("Version: ") + spanSmBr(appVersionFLD) + str += instDt ? spanSmBld("Installed: ") + spanSmBr(instDt) : sBLANK + section() { paragraph divSm(str, sCLRGRY) } +} + +def mainPage() { + Boolean newInstall = !(Boolean)state.isInstalled + return dynamicPage(name: "mainPage", nextPage: (!newInstall ? sBLANK : "namePage"), uninstall: newInstall, install: !newInstall) { + Boolean dup = ((Boolean)settings.duplicateFlag == true && (Boolean)state.dupPendingSetup) + if(dup) { + state.dupOpenedByUser = true + section() { paragraph spanBld("This Zone was created from an existing zone.

    Please review the settings and save to activate...
    ${state.badMode ?: sBLANK}", sCLRORG, "pause_orange") } + } + appInfoSect() + + Boolean paused = isPaused() + if(paused) { + section() { + paragraph spanSmBlr("This Zone is currently disabled...
    To edit the please re-enable it.", sCLRRED, "pause_orange") + } + } else { + section() { paragraph divSm(getOverallDesc(), sCLRGRY) } + + section(sectHead("Zone Devices:")) { + echoDevicesInputByPerm('announce') + } + if(settings.zone_EchoDevices) { + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } + Boolean condConf = conditionsConfigured() + section(sectHead("Zone Configuration:")) { + href "conditionsPage", title: inTS1("Zone Activation Conditions", "conditions") + optPrefix(), description: divSm(getConditionsDesc(true), sCLR4D9) + } + + if(condConf && settings.zone_EchoDevices) { + section(sectHead("Condition Delays:")) { + input "zone_active_delay", sNUMBER, title: inTS1("Delay Activation (In Seconds)", "delay_time") + optPrefix(), defaultValue: null, required: false, submitOnChange: true + input "zone_inactive_delay", sNUMBER, title: inTS1("Delay Deactivation (In Seconds)", "delay_time") + optPrefix(), defaultValue: null, required: false, submitOnChange: true + } + section(sectHead("Control Switches on Zone Active (Optional):")) { + input "zone_active_switches_on", "capability.switch", title: inTS1("Turn on Switches when Zone Active", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true + input "zone_active_switches_off", "capability.switch", title: inTS1("Turn off Switches when Zone Active", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true + } + section(sectHead("Control Switches on Zone Inactive (Optional):")) { + input "zone_inactive_switches_on", "capability.switch", title: inTS1("Turn on Switches when Zone Inactive", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true + input "zone_inactive_switches_off", "capability.switch", title: inTS1("Turn off Switches when Zone Inactive", sSWITCH) + optPrefix(), description: inputFooter(sTTC, sCLRGRY, true), multiple: true, required: false, submitOnChange: true + } + section(sectHead("Notifications:")) { + def t0 = getAppNotifDesc() + href "zoneNotifPage", title: inTS1("Send Notifications", "notification2"), description: t0 ? divSm(t0 + inputFooter(sTTM), sCLR4D9) : inputFooter(sTTC, sNULL, true) + } + } + } + updConfigStatusMap() + } + + section(sectHead("Zone History")) { + href "zoneHistoryPage", title: inTS1("View Zone History", "tasks"), description: sBLANK + } + + section(sectHead("Preferences")) { + href "prefsPage", title: inTS1("Logging Preferences", sSETTINGS), description: sBLANK + if((Boolean)state.isInstalled) { + input "zonePause", sBOOL, title: inTS1("Disable Zone?", "pause_orange"), defaultValue: false, submitOnChange: true + input "createZoneDevice", sBOOL, title: inTS1("Create a Virtual Device for this Zone?", "question"), defaultValue: false, submitOnChange: true + input "forceAnnounce", sBOOL, title: inTS1("Convert Zone speak commands to announcements?", "question"), defaultValue: true, submitOnChange: true + if((Boolean)settings.zonePause) { unsubscribe() } + } + } + //log.debug "myZoneStatus: ${myZoneStatus()}" + if((Boolean) state.isInstalled) { + section(sectHead("Name this Zone:")) { + input "appLbl", sTEXT, title: inTS1("Zone Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true + } + section(sectHead("Remove Zone:")) { + href "uninstallPage", title: inTS1("Remove this Zone", "uninstall"), description: inputFooter("Tap to Remove...", sCLRGRY, true) + } + section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { + String issueUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28ZONES+BUG%29+&projects=echo-speaks%2F6" + String featUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BZones+Feature+Request%5D&projects=echo-speaks%2F6" + href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "www"), description: spanSm("Tap to open browser", sCLRGRY) + href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "www"), description: spanSm("Tap to open browser", sCLRGRY) + } + } + } +} + +private echoDevicesInputByPerm(String type) { + List echoDevs = parent?.getChildDevicesByCap(type) + if(echoDevs?.size()) { + Map eDevsMap = echoDevs?.collectEntries { [(it.getId()): [label: (String)it.getLabel(), lsd: (it.currentWasLastSpokenToDevice?.toString() == sTRUE)]] }?.sort { a,b -> b?.value?.lsd <=> a?.value?.lsd ?: a?.value?.label <=> b?.value?.label } + Map moptions = eDevsMap?.collectEntries { [(it.key.toString()): "${it?.value?.label}${(it?.value?.lsd == true) ? " (Last Spoken To)" : sBLANK}".toString()] } + input "zone_EchoDevices", sENUM, title: inTS1("Echo Devices in Zone", "echo_gen1"), description: spanSm("Select the devices", sCLRGRY), options: moptions, multiple: true, required: true, submitOnChange: true + + // updDeviceInputs() + } else { paragraph spanSmBld("No devices were found with support for ($type)", sCLRRED) } +} + +def zoneHistoryPage() { + return dynamicPage(name: "zoneHistoryPage", title: "Zone History", install: false, uninstall: false) { + section() { + getZoneHistory() + } + List eData = (List)getMemStoreItem(zoneHistFLD) ?: [] + if(eData.size()) { + section(sBLANK) { + input "clearZoneHistory", sBOOL, title: inTS1("Clear Zone History?", sRESET), description: spanSm("Clears Stored Zone History.", sCLRGRY), defaultValue: false, submitOnChange: true + if(settings.clearZoneHistory) { settingUpdate("clearZoneHistory", sFALSE, sBOOL); updMemStoreItem(zoneHistFLD, []) } + } + } + } +} + +def prefsPage() { + return dynamicPage(name: "prefsPage", install: false, uninstall: false) { + section(sectHead("Logging:")) { + input "logInfo", sBOOL, title: inTS1("Show Info Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logWarn", sBOOL, title: inTS1("Show Warning Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logError", sBOOL, title: inTS1("Show Error Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true + input "logDebug", sBOOL, title: inTS1("Show Debug Logs?", sDEBUG), description: spanSm("Auto disables after 6 hours", sCLRGRY), required: false, defaultValue: false, submitOnChange: true + input "logTrace", sBOOL, title: inTS1("Show Detailed Logs?", sDEBUG), description: spanSm("Only enable when asked to (Auto disables after 6 hours)", sCLRGRY), required: false, defaultValue: false, submitOnChange: true + } + if((Boolean)state.isInstalled) { + if(advLogsActive()) { logsEnabled() } + } + } +} + +def namePage() { + return dynamicPage(name: "namePage", install: true, uninstall: false) { + section(sectHead("Name the Zone:")) { + input "appLbl", sTEXT, title: inTS1("Zone Name", "name_tag"), description: sBLANK, required:true, submitOnChange: true + } + } +} + +def uninstallPage() { + return dynamicPage(name: "uninstallPage", title: "Uninstall", install: false , uninstall: true) { + section(sBLANK) { paragraph spanSmBld("This will delete this Echo Speaks Zone.", sCLRORG) } + } +} + +private void updDeviceInputs() { + // log.trace "updDeviceInputs..." + List aa = (List)settings.zone_EchoDevices + List devIds + Boolean updList = false + try { + updList = (aa.size() && aa[0].id != null) + devIds = aa.collect { it?.id?.toString() } + // log.debug "updList(try): ${devIds.unique()}" + } catch (ignored) { + // log.debug "ex: $ex" + devIds = aa.collect { it?.toString() } + } + if(updList && devIds) { + logDebug("updList: $devIds") + app.updateSetting( "zone_EchoDevices", [type: "enum", value: devIds.unique()]) + } + if(devIds) { app.updateSetting( "zone_EchoDeviceList", [type: "capability", value: devIds.unique()]) } // this won't take effect until next execution +} + +/****************************************************************************** + CONDITIONS SELECTION PAGE +******************************************************************************/ + +def conditionsPage() { + return dynamicPage(name: "conditionsPage", title: "Zone Activation Conditions", nextPage: "mainPage", install: false, uninstall: false) { + String desc = getConditionsDesc(false) + if(!desc) { desc= "No Conditions/Restrictions Set" } + section() { paragraph divSm(desc, sCLR4D9) } + + Boolean multiConds = multipleConditions() + section() { + if(multiConds) { + input "cond_require_all", sBOOL, title: inTS1("Require All Selected Conditions to Pass Before Activating Zone?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm(reqAllCond() ? "All selected conditions must pass before this zone is active." : "Any condition will allow this zone to become active.", sCLR4D9) + } + section(sectHead("Time/Date Restrictions")) { + href "condTimePage", title: inTS1("Time Schedule", "clock"), description: spanSm(getTimeCondDesc(false), sCLR4D9) + input "cond_days", sENUM, title: inTS1("Days of the week", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: weekDaysEnum() + input "cond_months", sENUM, title: inTS1("Months of the year", "day_calendar"), multiple: true, required: false, submitOnChange: true, options: monthEnum() + } + + section (sectHead("Mode Conditions")) { + input "cond_mode", sMODE, title: inTS1("Location Modes...", sMODE), multiple: true, required: false, submitOnChange: true + if((List)settings.cond_mode) { + input "cond_mode_cmd", sENUM, title: inTS1("are...", sCOMMAND), options: ["not":"not in these modes", "are":"In these Modes"], required: true, multiple: false, submitOnChange: true + if((List)settings.cond_mode && (String)settings.cond_mode_cmd) { + input "cond_mode_db", sBOOL, title: inTS1("Deactivate Zone immediately when Mode condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + } + + section (sectHead("Alarm Conditions")) { + input "cond_alarm", sENUM, title: inTS1("${getAlarmSystemName()} is...", "alarm_home"), options: getAlarmTrigOpts(), multiple: true, required: false, submitOnChange: true + if((List)settings.cond_alarm) { + input "cond_alarm_db", sBOOL, title: inTS1("Deactivate Zone immediately when Alarm condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + + condNonNumSect(sSWITCH, sSWITCH, "Switches/Outlets Conditions", "Switches/Outlets", ["on","off"], "are", sSWITCH) + + condNonNumSect("contact", "contactSensor", "Door, Window, Contact Sensors Conditions", "Contact Sensors", ["open","closed"], "are", "contact") + + condNonNumSect("motion", "motionSensor", "Motion Conditions", "Motion Sensors", ["active", "inactive"], "are", "motion") + + condNonNumSect("presence", "presenceSensor", "Presence Conditions", "Presence Sensors", ["present", "not present"], "are", "presence") + + condNonNumSect("acceleration", "accelerationSensor", "Accelerometer Conditions", "Accelerometer Sensors", ["active","inactive"], "are", "acceleration") + + condNumValSect("battery", "battery", "Battery Level Conditions", "Batteries", "Level (%)", "battery") + + condNumValSect("level", "switchLevel", "Dimmers/Levels", "Dimmers/Levels", "Level (%)", "speed_knob") + + condNonNumSect("door", "garageDoorControl", "Garage Door Conditions", "Garage Doors", ["open", "closed"], "are", "garage_door") + + condNumValSect("illuminance", "illuminanceMeasurement", "Illuminance Conditions", "Illuminance Sensors", "Lux Level (%)", "illuminance") + + condNumValSect("power", "powerMeter", "Power Events", "Power Meters", "Power Level (W)", "power") + + condNumValSect("humidity", "relativeHumidityMeasurement", "Humidity Conditions", "Relative Humidity Sensors", "Relative Humidity (%)", "humidity") + + condNonNumSect("securityKeypad", "securityKeypad", "Security Keypad Conditions", "Security Kepads", ["disarmed", "armed home", "armed away"], "are", "keypad") + + condNonNumSect("lock", "lock", "Lock Conditions", "Smart Locks", ["locked", "unlocked"], "are", "lock") + + condNumValSect("temperature", "temperatureMeasurement", "Temperature Conditions", "Temperature Sensors", "Temperature", "temperature") + + condNonNumSect("water", "waterSensor", "Water Sensors", "Water Sensors", ["wet", "dry"], "are", "water") + + condNonNumSect("windowShade", "windowShade", "Window Shades", "Window Shades", ["open", "closed"], "are", "shade") + + condNonNumSect("valve", "valve", "Valves", "Valves", ["open", "closed"], "are", "valve") + } +} + +def condNonNumSect(String inType, String capType, String sectStr, String devTitle, cmdOpts, String cmdTitle, String image) { + section(sectHead(sectStr) /*, hideWhenEmpty: true */) { + input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required:false //, hideWhenEmpty: true + if (settings."cond_${inType}") { + input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle}...", sCOMMAND), options: cmdOpts, multiple: false, required: true, submitOnChange: true + if ((String)settings."cond_${inType}_cmd" && ((List)settings."cond_${inType}")?.size() > 1) { + input "cond_${inType}_all", sBOOL, title: inTS1("ALL ${devTitle} must be (${(String)settings."cond_${inType}_cmd"})?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + input "cond_${inType}_db", sBOOL, title: inTS1("Deactivate Zone immediately when ${cmdTitle} condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } +} + +@SuppressWarnings('unused') +def condNumValSect(String inType, String capType, String sectStr, String devTitle, String cmdTitle, String image, hideable= false) { + section(sectHead(sectStr) /*, hideWhenEmpty: true */) { + input "cond_${inType}", "capability.${capType}", title: inTS1(devTitle, image), multiple: true, submitOnChange: true, required: false //, hideWhenEmpty: true + if(settings."cond_${inType}") { + input "cond_${inType}_cmd", sENUM, title: inTS1("${cmdTitle} is...", sCOMMAND), options: [sBETWEEN, sBELOW, sABOVE, sEQUALS], required: true, multiple: false, submitOnChange: true + if ((String)settings."cond_${inType}_cmd") { + if ((String)settings."cond_${inType}_cmd" in [sBETWEEN, sBELOW]) { + input "cond_${inType}_low", sNUMBER, title: inTS1("a ${(String)settings."cond_${inType}_cmd" == sBETWEEN ? "Low " : sBLANK}${cmdTitle} of...", "low"), required: true, submitOnChange: true + } + if ((String)settings."cond_${inType}_cmd" in [sBETWEEN, sABOVE]) { + input "cond_${inType}_high", sNUMBER, title: inTS1("${(String)settings."cond_${inType}_cmd" == sBETWEEN ? "and a high " : "a "}${cmdTitle} of...", "high"), required: true, submitOnChange: true + } + if ((String)settings."cond_${inType}_cmd" == sEQUALS) { + input "cond_${inType}_equal", sNUMBER, title: inTS1("a ${cmdTitle} of...", "equal"), required: true, submitOnChange: true + } + if (settings."cond_${inType}"?.size() > 1) { + input "cond_${inType}_all", sBOOL, title: inTS1("Require ALL devices to be (${(String)settings."cond_${inType}_cmd"}) values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + if(!(Boolean)settings."cond_${inType}_all") { + input "cond_${inType}_avg", sBOOL, title: inTS1("Use the average of all selected device values?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + input "cond_${inType}_db", sBOOL, title: inTS1("Deactivate Zone immediately when ${cmdTitle} condition no longer passes?", sCHKBOX), required: false, defaultValue: false, submitOnChange: true + } + } + } +} + +def condTimePage() { + return dynamicPage(name:"condTimePage", title: sBLANK, install: false, uninstall: false) { + Boolean timeReq = (settings["cond_time_start"] || settings["cond_time_stop"]) + section(sectHead("Start Time:")) { + input "cond_time_start_type", sENUM, title: inTS1("Starting at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true + if(cond_time_start_type == sTIME) { + input "cond_time_start", sTIME, title: inTS1("Start time", "start_time"), required: timeReq, submitOnChange: true + } else if(cond_time_start_type in lSUNRISESET) { + input "cond_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true + } + } + section(sectHead("Stop Time:")) { + input "cond_time_stop_type", sENUM, title: inTS1("Stopping at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true + if(cond_time_stop_type == sTIME) { + input "cond_time_stop", sTIME, title: inTS1("Stop time", "start_time"), required: timeReq, submitOnChange: true + } else if(cond_time_stop_type in lSUNRISESET) { + input "cond_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true + } + } + } +} + +def zoneNotifPage() { + return dynamicPage(name: "zoneNotifPage", title: "Zone Notifications", install: false, uninstall: false) { + String a = getAppNotifDesc() + if(!a) a = "Notifications not enabled" + section() { paragraph divSm(a, sCLR4D9) } + + section (sectHead("Notification Devices:")) { + input "notif_devs", "capability.notification", title: inTS1("Send to Notification devices?", "notification"), description: ((!settings?.notif_devs) ? inactFoot(sTTC) : sBLANK), required: false, multiple: true, submitOnChange: true + } + section (sectHead("Alexa Mobile Notification:")) { + paragraph spanSmBldBr("Description:", sCLRGRY) + spanSmBld("This will send a push notification the Alexa Mobile app.", sCLRGRY) + input "notif_alexa_mobile", sBOOL, title: inTS1("Send message to Alexa App?", "notification"), required: false, defaultValue: false, submitOnChange: true + } + + Boolean active = ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) + if(active) { + section (sectHead("Message Customization:")) { + paragraph spanSm("Configure either an Active or Inactive message to complete notification configuration.", sCLR4D9) + Boolean req = !((String)settings.notif_active_message || (String)settings.notif_inactive_message) + input "notif_active_message", sTEXT, title: inTS1("Active Message?", sTEXT), required: req, submitOnChange: true + input "notif_inactive_message", sTEXT, title: inTS1("Inactive Message?", sTEXT), required: req, submitOnChange: true + } + } else { + List sets = settings.findAll { it.key.startsWith("notif_") }?.collect { (String)it.key } + sets?.each { String sI-> settingRemove(sI) } + } + + if(isZoneNotifConfigured()) { + section(sectHead("Notification Restrictions:")) { + String nsd = getNotifSchedDesc() + href "zoneNotifTimePage", title: inTS1("Quiet Restrictions", "restriction"), description: (nsd ? divSm(spanSmBr(nsd) + inputFooter(sTTM), sCLR4D9) : inactFoot(sTTC)) + } + if(!(Boolean)state.notif_message_tested) { + List actDevices = (Boolean)settings.notif_alexa_mobile ? parent?.getDevicesFromList(settings.zone_EchoDevices) : [] + def aMsgDev = actDevices?.size() && (Boolean)settings.notif_alexa_mobile ? actDevices[0] : null + if(sendNotifMsg("Info", "Zone Notification Test Successful. Notifications Enabled for ${app?.getLabel()}", aMsgDev, true)) { state.notif_message_tested = true } + } + } else { state.remove("notif_message_tested") } + } +} + +def zoneNotifTimePage() { + return dynamicPage(name:"zoneNotifTimePage", title: sBLANK, install: false, uninstall: false) { + // String a = getNotifSchedDesc() + // if(a) { + // section() { + // paragraph spanSmBldBr("Restrictions Status:", sCLR4D9) + spanSm(a, sCLR4D9) + // paragraph spanSmBldBr("NOTICE: All selected restrictions must be ${strUnder("INACTIVE")} for notifications to be sent.", sCLRORG) + // paragraph htmlLine() + // } + // } + String pre = "notif" + Boolean timeReq = (settings["${pre}_time_start"] || settings["${pre}_time_stop"]) + section(sectHead("Quiet Start Time:")) { + input "${pre}_time_start_type", sENUM, title: inTS1("Starting at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true + if(settings."${pre}_time_start_type" == sTIME) { + input "${pre}_time_start", sTIME, title: inTS1("Start time", "start_time"), required: timeReq, submitOnChange: true + } else if(settings."${pre}_time_start_type" in lSUNRISESET) { + input "${pre}_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true + } + } + section(sectHead("Quiet Stop Time:")) { + input "${pre}_time_stop_type", sENUM, title: inTS1("Stopping at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true + if(settings."${pre}_time_stop_type" == sTIME) { + input "${pre}_time_stop", sTIME, title: inTS1("Stop time", "start_time"), required: timeReq, submitOnChange: true + } else if(settings."${pre}_time_stop_type" in lSUNRISESET) { + input "${pre}_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true + } + } + section(sectHead("Allowed Days:")) { + input "${pre}_days", sENUM, title: inTS1("Only on these week days", "day_calendar"), multiple: true, required: false, options: weekDaysEnum() + } + section(sectHead("Allowed Modes:")) { + input "${pre}_modes", sMODE, title: inTS1("Only in these Modes", sMODE), multiple: true, submitOnChange: true, required: false + } + } +} + + +//******************************************** +// CORE APP METHODS +//******************************************** + +def installed() { + logInfo("Installed Event Received...") + state.dateInstalled = getDtNow() + if((Boolean)settings.duplicateFlag && !(Boolean)state.dupPendingSetup) { + Boolean maybeDup = ((String)app?.getLabel())?.contains(" (Dup)") + state.dupPendingSetup = true + runIn(2, "processDuplication") + if(maybeDup) logInfo("installed found maybe a dup... ${(Boolean)settings.duplicateFlag}") + } else { + if(!(Boolean)state.dupPendingSetup) initialize() + } +} + +@Field static final String dupMSGFLD = "This zone is duplicated and has not had configuration completed... Please open zone and configure to complete setup..." + +def updated() { + logInfo("Updated Event Received...") + Boolean maybeDup = app?.getLabel()?.toString()?.contains(" (Dup)") + if(maybeDup) logInfo("updated found maybe a dup... ${(Boolean)settings.duplicateFlag}") + if((Boolean)settings.duplicateFlag) { + if((Boolean)state.dupOpenedByUser) { state.dupPendingSetup = false } + if((Boolean)state.dupPendingSetup){ + logInfo(dupMSGFLD) + return + } + logInfo("removing duplicate status") + settingRemove('duplicateFlag'); settingRemove('duplicateSrcId') + state.remove('dupOpenedByUser'); state.remove('dupPendingSetup'); state.remove('badMode') + } + initialize() +} + +def initialize() { + logInfo("Initialize Event Received...") + if((Boolean)settings.duplicateFlag && (Boolean)state.dupPendingSetup){ + logInfo(dupMSGFLD) + return + } + updDeviceInputs() + unsubscribe() + unschedule() + state.isInstalled = true + updAppLabel() + if(advLogsActive()) { logsEnabled() } + runIn(3, "zoneCleanup") + if(!isPaused(true)) { + runIn(7, "subscribeToEvts") + runEvery1Hour("healthCheck") + updConfigStatusMap() + } + + handleZoneDevice() + checkZoneStatus([name: "initialize", displayName: "initialize"]) +} + +void handleZoneDevice() { + String dni = [app?.id, "echoSpeaks-Zone-Dev"].join("|") + def childDevice = getChildDevice(dni) + String devLabel = "EchoZone - ${app.getLabel().replace(" (Z)", sBLANK)}" + String childHandlerName = "Echo Speaks Zone Device" + if (!childDevice && (Boolean)settings.createZoneDevice) { + // log.debug "childDevice not found | autoCreateDevices: ${settings.autoCreateDevices}" + try{ + logInfo("Creating NEW Echo Speaks Zone Device!!! | Device Label: ($devLabel)") + childDevice = addChildDevice("tonesto7", childHandlerName, dni, null, [name: childHandlerName, label: devLabel, settings:[isZone: true], completedSetup: true]) + } catch(ex) { + logError("AddDevice Error! | ${ex}", false, ex) + } + } + if (childDevice && !(Boolean)settings.createZoneDevice) { + try { + deleteChildDevice(childDevice.deviceNetworkId) + } catch(e) { + logError("RemoveDevice Error! } ${e}", false, e) + } + } +} + +List getEzDevice() { + return ((List)getChildDevices())?.findAll { (Boolean)it?.isZone() } +} + +void updateChildZoneState(Boolean zoneActive, Boolean active) { + if(zoneActive != null) getEzDevice()?.each { it?.updStatus(zoneActive) } + if(active != null) getEzDevice()?.each { it?.updSocketStatus(active) } +} + +/******************************************************************* + To Device from parent +*******************************************************************/ +@SuppressWarnings('unused') +String relayDevVersion() { + String a + a = sNULL + getEzDevice().each { a = it.devVersion() } + return a +} + +@SuppressWarnings('unused') +void relayUpdChildSocketStatus(Boolean active) { + updateChildZoneState(null, active) +} + +@SuppressWarnings('unused') +void relayUpdateCookies(Map cookies, Boolean doInit){ + getEzDevice().each { it.updateCookies(cookies, doInit) } +} + +@SuppressWarnings('unused') +void relayRemoveCookies(Boolean isParent) { + getEzDevice().each { it.removeCookies(isParent) } +} + +@SuppressWarnings('unused') +void relayEnableDebugLog() { getEzDevice().each { it.enableDebugLog() } } +@SuppressWarnings('unused') +void relayDisableDebugLog() { getEzDevice().each { it.disableDebugLog() } } +@SuppressWarnings('unused') +void relayEnableTraceLog() { getEzDevice().each { it.enableTraceLog() } } +@SuppressWarnings('unused') +void relayDisableTraceLog() { getEzDevice().each { it.disableTraceLog() } } +@SuppressWarnings('unused') +void relayLogsOff() { getEzDevice().each { it.logsOff() } } +@SuppressWarnings('unused') +Map relayGetLogHistory() { Map a; a=null; getEzDevice().each { a = it.getLogHistory() }; return a ?: [:] } +@SuppressWarnings('unused') +void relayClearLogHistory() { getEzDevice().each { it.clearLogHistory() } } +@SuppressWarnings('unused') +void relayFinishAnnouncement(String msg, LinkedHashMap vmap) { getEzDevice().each { it.finishAnnounce(msg, vmap.vol, vmap.restvol) } } +@SuppressWarnings('unused') +void relayFinishSpeak(Map resp, Integer statucode, Map data) { getEzDevice().each { it.finishSendSpeakZ(resp, statuscode, data) } } +@SuppressWarnings('unused') +void relaySetOnline(Boolean onl) { getEzDevice().each { it.setOnlineStatus(onl) } } + + +/******************************************************************* + To Parent from Device Command FUNCTIONS +*******************************************************************/ +Boolean zoneOnline() { + if(isPaused(true) || !isActive()) { + logTrace("zone offline or inactive; relay failed") + return false + } + return true +} + +Map relayMinVersions() { + parent.minVersions() +} + +@SuppressWarnings('unused') +void relayMultiSeqCommand(List commands, String srcDesc, Boolean parallel=false, Map cmdMap=[:], String device=sNULL, String callback=sNULL) { + if(zoneOnline()) parent.queueMultiSequenceCommand(commands, srcDesc, parallel, cmdMap, device, callback) +} + +@SuppressWarnings('unused') +void relaySeqCommand(String type, String command, value=null, Map deviceData=[:], String device=sNULL, String callback=sNULL) { + if(zoneOnline()) parent.queueSequenceCommand(type, command, value, deviceData, device, callback) +} + +@SuppressWarnings('unused') +void relayAnnounceZone(String zoneId, String msg, Boolean parallel, Boolean ignoreDoNotDisturb) { + if(zoneOnline()) parent.sendZoneAnnounce(zoneId, msg, parallel, ignoreDoNotDisturb) +} + +@SuppressWarnings('unused') +void relaySpeakZone(String zoneId, String msg, Boolean parallel, Boolean ignoreDoNotDisturb) { + if(zoneOnline()) parent.sendZoneSpeak(zoneId, msg, parallel, ignoreDoNotDisturb) +} + +@SuppressWarnings('unused') +void relayNopCommand() { + if(zoneOnline()) parent.queueNopCommand() +} + +@SuppressWarnings('unused') +private Boolean relayGetWWebSocketStatus() { + parent.getWWebSocketStatus() +} + +@SuppressWarnings('unused') +void relayChildInitialtedRefresh() { + parent.childInitiatedRefresh() +} + +String relayGetAlexaGuardStatus() { + parent.getAlexaGuardStatus() +} +/* +Boolean RelayGetDndEnabled(String serial) { + parent.getDndEnabled(serial) +} */ + +@SuppressWarnings('unused') +private void processDuplication() { + String al = (String)app?.getLabel() + String newLbl = "${al}${al?.contains(" (Dup)") ? sBLANK : " (Dup)"}" + app?.updateLabel(newLbl) + state.dupPendingSetup = true + + String dupSrcId = settings.duplicateSrcId ? (String)settings.duplicateSrcId : sNULL + Map dupData = parent?.getChildDupeData("zones", dupSrcId) + log.debug "dupData: ${dupData}" + if(dupData && dupData.state?.size()) { + dupData.state.each { String k,v-> state[k] = v } + } + + if(dupData && dupData.settings?.size()) { + dupData.settings.each { String k, Map v-> + if((String)v.type in [sENUM, sMODE]) { + settingRemove(k) + settingUpdate(k, (v.value != null ? v.value : null), (String)v.type) + } + } + } + + parent.childAppDuplicationFinished("zones", dupSrcId) + sendZoneStatus() + subscribe(location, "es3ZoneRefresh", zoneRefreshHandler) + logInfo("Duplicated Zone has been created... Please open zone and configure to complete setup...") +} + +def uninstalled() { + sendZoneRemoved() +} + +String getZoneName() { return (String)settings.appLbl } + +private void updAppLabel() { + String newLbl = "${(String)settings.appLbl} (Z${isPaused(true) ? " ${sPAUSESymFLD}" : sBLANK})".replaceAll(/ (Dup)/, sBLANK).replaceAll("\\s"," ") + if((String)settings.appLbl && (String)app?.getLabel() != newLbl) { app?.updateLabel(newLbl); sendZoneStatus() } +} + +private void updConfigStatusMap() { + Map sMap = (Map)state.configStatusMap + sMap = sMap ?: [:] + sMap.conditions = conditionsConfigured() + sMap.devices = devicesConfigured() + state.configStatusMap = sMap +} + +Boolean devicesConfigured() { return (settings.zone_EchoDevices?.size() > 0) } + +//private Boolean getConfStatusItem(String item) { Map sMap = state.configStatusMap; return (sMap?.containsKey(item) && sMap[item] == true) } + +@SuppressWarnings('unused') +private void zoneCleanup() { + // State Cleanup + List items = [] + items.each { String si-> if(state.containsKey(si)) { state.remove(si)} } + //Cleans up unused Zone setting items + List setItems = ["zone_delay"] + setItems.each { String sI-> if(settings.containsKey(sI)) { settingRemove(sI) } } +} + +public void triggerInitialize() { runIn(3, "initialize") } + +public void updatePauseState(Boolean pause) { + if((Boolean)settings.zonePause != pause) { + logDebug("Received Request to Update Pause State to (${pause})") + settingUpdate("zonePause", "${pause}", sBOOL) + runIn(4, "updated") + } +} + +@SuppressWarnings('unused') +private healthCheck() { + // logTrace("healthCheck", true) + checkZoneStatus([name: "healthCheck", displayName: "healthCheck"]) + if(advLogsActive()) { logsDisable() } +} + +//private condItemSet(String key) { return (settings.containsKey("cond_${key}") && settings["cond_${key}"]) } + +@SuppressWarnings('GroovyVariableNotAssigned') +void scheduleCondition() { + if(isPaused(true)) { logWarn("Zone is PAUSED... No Events will be subscribed to or scheduled....", true); return } + Boolean tC = timeCondConfigured() + Boolean dC = dateCondConfigured() + if (tC || dC) { + String msg = 'scheduleCondition: ' + Date startTime + Date stopTime + if (tC) { + Boolean timeOk = timeCondOk() // this updates state variables + startTime = (String)state.startTime ? parseDate((String)state.startTime) : null + stopTime = (String)state.stopTime ? parseDate((String)state.stopTime) : null + } + if(!startTime && !stopTime && dC){ + startTime = timeToday('00:00', location.timeZone) + stopTime = timeTodayAfter('23:59', '00:00', location.timeZone) + } + if(startTime && stopTime){ + Long t = now() + Long lstart = startTime.getTime() + Long lstop = stopTime.getTime() + + Boolean not = lstart > lstop + Boolean isStart = true + Long nextEvtT = 0L + if(!not) { + if(t 0L) { + Long tt = Math.round((nextEvtT)/1000.0D) + 1L + tt=(tt<1L ? 1L:tt) + if(isStart) runIn(tt, "zoneTimeStartCondHandler") + else runIn(tt, "zoneTimeStopCondHandler") + Date ttt = isStart ? startTime: stopTime + msg += "Setting Schedule for ${isStart ? "Start Condition" : "Stop Condition"} in $tt's at ${epochToTime(ttt)}" + msg += msg1 + } else msg += "Nothing to"+msg1 + } else log.warn "Strange - no start ($startTime) or stop ($stopTime) time: $tC date: $dC" + logTrace(msg) + } +} + +void subscribeToEvts() { +// state.handleGuardEvents = false + if(minVersionFailed()) { logError("CODE UPDATE required to RESUME operation. No events will be monitored.", true); return } + if(isPaused(true)) { logWarn("Zone is PAUSED... No Events will be subscribed to or scheduled....", true); return } + List attItems = [sMODE, "alarm"] + lDATTSTR + lDATTNUM + + //SCHEDULING + if(timeCondConfigured() || dateCondConfigured()) { + scheduleCondition() + schedule('33 0 0 1/1 * ? * ', scheduleCondition) // run at 00:00:33 every day +// in case we have been stopped a while... + Map condStat = conditionStatus() + if((Boolean)condStat.ok) zoneTimeStartCondHandler() + else zoneTimeStopCondHandler() + } + + attItems.each { String si-> + if((List)settings."cond_${si}") { + switch(si) { + case "alarm": + subscribe(location, "hsmStatus", zoneEvtHandler) + break + case sMODE: + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } + subscribe(location, si, zoneEvtHandler) + break + default: + subscribe((List)settings."cond_${si}", si, zoneEvtHandler) + break + } + } + } + // Subscribes to Zone Location Command from Other Echo Speaks apps. + subscribe(location, "es3ZoneCmd", zoneCmdHandler) + subscribe(location, "es3ZoneRefresh", zoneRefreshHandler) + subscribe(location, "systemStart", zoneStartHandler) +} + +/*********************************************************************************************************** + CONDITIONS HANDLER +************************************************************************************************************/ +// Boolean reqAllCond() { Boolean a = multipleConditions(); return (!a || (a && (Boolean)settings.cond_require_all) ); } +Boolean reqAllCond() { + Boolean mult = multipleConditions() + return ( mult && (Boolean)settings.cond_require_all ) +} + +Boolean timeCondOk() { + Date startTime + Date stopTime + Date now = new Date() + String startType = (String)settings.cond_time_start_type + String stopType = (String)settings.cond_time_stop_type + if(startType && stopType) { + startTime = startType == 'time' && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null + stopTime = stopType == 'time' && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null + + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + def sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L + Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + + if(startTime && stopTime) { + Boolean not = startTime.getTime() > stopTime.getTime() + Boolean isBtwn = timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, location?.timeZone) + isBtwn = not ? !isBtwn : isBtwn + state.startTime = formatDt(startTime) + state.stopTime = formatDt(stopTime) + logTrace("timeCondOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not? startTime:stopTime})") + return isBtwn + } + } + logTrace("timeCondOk | (null)") + state.startTime = sNULL + state.stopTime = sNULL + return null +} + +@SuppressWarnings('GroovyVariableNotAssigned') +Boolean dateCondOk() { + Boolean result = null + Boolean dOk + Boolean mOk + if(((List)settings.cond_days || (List)settings.cond_months) ) { + Boolean reqAll = reqAllCond() + dOk = (List)settings.cond_days ? (isDayOfWeek((List)settings.cond_days)) : reqAll // true + mOk = (List)settings.cond_months ? (isMonthOfYear((List)settings.cond_months)) : reqAll //true + result = reqAll ? (mOk && dOk) : (mOk || dOk) + } + logTrace("dateCondOk | $result | monthOk: $mOk | daysOk: $dOk") + return result +} + +@SuppressWarnings('GroovyVariableNotAssigned') +Boolean locationCondOk() { + Boolean result = null + Boolean mOk + Boolean aOk + if((List)settings.cond_mode || (String)settings.cond_mode_cmd || (List)settings.cond_alarm) { + Boolean reqAll = reqAllCond() + mOk = ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : reqAll //true + aOk = (List)settings.cond_alarm ? isInAlarmMode((List)settings.cond_alarm) : reqAll //true + result = reqAll ? (mOk && aOk) : (mOk || aOk) + } + logTrace("locationCondOk | $result | modeOk: $mOk | alarmOk: $aOk") + return result +} + +Boolean checkDeviceCondOk(String att) { + List devs = (List)settings."cond_${att}" ?: null + def cmdVal = settings."cond_${att}_cmd" ?: null // list or string + Boolean all = ((Boolean)settings."cond_${att}_all" == true) + if( !(att && devs && cmdVal) ) { return true } + return all ? allDevAttValsEqual(devs, att, cmdVal) : anyDevAttValsEqual(devs, att, cmdVal) +} + +Boolean checkDeviceNumCondOk(String att) { + List devs = (List)settings."cond_${att}" ?: null + String cmd = (String)settings."cond_${att}_cmd" ?: sNULL + Double dcl = settings."cond_${att}_low" ?: null + Double dch = settings."cond_${att}_high" ?: null + Double dce = settings."cond_${att}_equal" ?: null + Boolean dca = ((Boolean)settings."cond_${att}_all" == true) ?: false + if( !(att && devs && cmd) ) { return true } + + switch(cmd) { + case "equals": + if(dce) { + if(dca) { return allDevAttNumValsEqual(devs, att, dce) } + else { return anyDevAttNumValEqual(devs, att, dce) } + } + return true + break + case "between": + if(dcl && dch) { + if(dca) { return allDevAttNumValsBetween(devs, att, dcl, dch) } + else { return anyDevAttNumValBetween(devs, att, dcl, dch) } + } + return true + break + case "above": + if(dch) { + if(dca) { return allDevAttNumValsAbove(devs, att, dch) } + else { return anyDevAttNumValAbove(devs, att, dch) } + } + return true + break + case "below": + if(dcl) { + if(dca) { return allDevAttNumValsBelow(devs, att, dcl) } + else { return anyDevAttNumValBelow(devs, att, dcl) } + } + return true + break + } + return false +} + +@Field static List lDATTSTR = ["switch", "motion", "presence", "contact", "acceleration", "lock", "securityKeypad", "windowShade", "door", "valve", "water" ] +@Field static List lDATTNUM = ["battery", "temperature", "illuminance", "level", "power", "humidity"] + +private Boolean isConditionOk(String att) { + if(lDATTSTR.contains(att)) { + if(!settings."cond_${att}") { return true } + return checkDeviceCondOk(att) + } else if(lDATTNUM.contains(att)) { + if(!settings."cond_${att}") { return true } + return checkDeviceNumCondOk(att) + } else if (att == sMODE) { + return ((List)settings.cond_mode /*&& (String)settings.cond_mode_cmd*/) ? (isInMode((List)settings.cond_mode, ((String)settings.cond_mode_cmd == "not"))) : true + } else if (["hsmStatus", "alarmSystemStatus"].contains(att)) { + return (List)settings.cond_alarm ? isInAlarmMode((List)settings.cond_alarm) : true + } else { + return true + } +} + +Boolean deviceCondOk() { + List skipped = [] + List passed = [] + List failed = [] + lDATTSTR.each { String i-> + if(!settings."cond_${i}") { skipped.push(i); return } + checkDeviceCondOk(i) ? passed.push(i) : failed.push(i) + } + lDATTNUM.each { String i-> + if(!settings."cond_${i}") { skipped.push(i); return } + checkDeviceNumCondOk(i) ? passed.push(i) : failed.push(i) + } + Integer cndSize = (passed.size() + failed.size()) + Boolean result = null + if(cndSize != 0) result = reqAllCond() ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() >= 1) + logTrace("DeviceCondOk | ${result} | Found: (${(passed?.size() + failed?.size())}) | Skipped: $skipped | Passed: $passed | Failed: $failed") + return result +} + +Map conditionStatus() { + Boolean reqAll = reqAllCond() + List failed = [] + List passed = [] + List skipped = [] + Boolean ok = true + if((Boolean)state.dupPendingSetup) ok = false + if(!settings.zone_EchoDevices) ok = false + Integer cndSize=(Integer)null + if(ok) { + [sTIME, "date", "location", "device"]?.each { i-> + Boolean s = "${i}CondOk"() + if(s == null) { skipped.push(i); return } + s ? passed.push(i) : failed.push(i) + } + cndSize = passed.size() + failed.size() + ok = reqAll ? (cndSize == passed.size()) : (cndSize > 0 && passed.size() >= 1) + if(cndSize == 0) ok = true + } + logTrace("conditionStatus | ok: $ok | RequireAll: ${reqAll} | Found: (${cndSize}) | Skipped: $skipped | Passed: $passed | Failed: $failed") + return [ok: ok, passed: passed, blocks: failed] +} + +Boolean devCondConfigured(String att) { + return ((List)settings."cond_${att}" && (String)settings."cond_${att}_cmd") +} + +Boolean devNumCondConfigured(String att) { + return ((String)settings."cond_${att}_cmd" && (settings."cond_${att}_low" || settings."cond_${att}_high" || settings."trig_${att}_equal")) +} + +Boolean timeCondConfigured() { + Boolean startTime = ((String)settings.cond_time_start_type in lSUNRISESET || ((String)settings.cond_time_start_type == sTIME && settings.cond_time_start)) + Boolean stopTime = ((String)settings.cond_time_stop_type in lSUNRISESET || ((String)settings.cond_time_stop_type == sTIME && settings.cond_time_stop)) + return (startTime && stopTime) +} + +Boolean dateCondConfigured() { + Boolean days = ((List)settings.cond_days) + Boolean months = ((List)settings.cond_months) + return (days || months) +} + +Boolean locationCondConfigured() { + if((List)settings.cond_mode && !(String)settings.cond_mode_cmd) { settingUpdate("cond_mode_cmd", "are", sENUM) } + Boolean mode = ((List)settings.cond_mode && (String)settings.cond_mode_cmd) + Boolean alarm = ((List)settings.cond_alarm) + return (mode || alarm) +} + +Boolean deviceCondConfigured() { + return (deviceCondCount() > 0) +} + +Integer deviceCondCount() { + List devConds = lDATTSTR + lDATTNUM + List items = [] + devConds.each { String dc-> if(devCondConfigured(dc)) { items.push(dc) } } + return items.size() +} + +Boolean conditionsConfigured() { + return (timeCondConfigured() || dateCondConfigured() || locationCondConfigured() || deviceCondConfigured()) +} + +Boolean multipleConditions() { + Integer cnt = 0 + if(timeCondConfigured()) cnt++ + if(dateCondConfigured()) cnt++ + if(locationCondConfigured()) cnt++ + cnt = cnt + deviceCondCount() + return (cnt>1) +} + +/*********************************************************************************************************** + EVENT HANDLER FUNCTIONS +************************************************************************************************************/ + +def zoneEvtHandler(evt) { + logTrace( "${(String)evt?.name} Event | Device: ${(String)evt?.displayName} | Value: (${strCapitalize(evt?.value?.toString())}) with a delay of ${now() - ((Date)evt?.date)?.getTime()}ms") + checkZoneStatus(evt) + scheduleCondition() +} + +void zoneTimeStartCondHandler() { + Map evt = [date: new Date(), name: "Time", displayName: "Condition Start Time", value: now()] + zoneEvtHandler(evt) +} + +void zoneTimeStopCondHandler() { + Map evt = [date: new Date(), name: "Time", displayName: "Condition Stop Time", value: now()] + zoneEvtHandler(evt) +} + +def zoneStartHandler(evt) { + zoneEvtHandler(evt) +// match states incase we were down + sendZoneStatus() +} + +private void addToZoneHistory(Map evt, Map condStatus, Integer max=10) { + Boolean ssOk = true //(stateSizePerc() <= 70) + List eData = getMemStoreItem(zoneHistFLD) ?: [] + eData.push([dt: getDtNow(), active: (condStatus.ok == true), evtName: evt.name, evtDevice: evt.displayName, blocks: condStatus.blocks, passed: condStatus.passed]) + Integer lsiz = eData.size() + if(!ssOk || lsiz > max) { eData = eData.drop( (lsiz-max)+1 ) } + updMemStoreItem(zoneHistFLD, eData) +} + +void checkZoneStatus(evt) { + Map condStatus = conditionStatus() + Boolean active = (Boolean)condStatus.ok + String delayType = active ? "active" : "inactive" + String nm = (String)evt?.name + String msg1 = " | Call from ${nm} / ${(String)evt?.displayName}".toString() + if((Boolean)state.zoneConditionsOk == active) { logTrace("checkZoneStatus: Zone: ${delayType} | No changes${msg1}"); return } + Boolean bypassDelay = false + Map data = [active: active, recheck: false, evtData: [name: nm, displayName: (String)evt?.displayName], condStatus: condStatus] + Integer delay = settings."zone_${delayType}_delay" ?: null + if(!active && settings."cond_${nm}_db" == true) { bypassDelay = !isConditionOk(nm) } + String msg = !bypassDelay && delay ? "in (${delay} sec)" : (bypassDelay ? "Bypassing Inactive Delay for (${nm}) Event..." : sBLANK) + logTrace("calling updateZoneStatus [${delayType}] "+msg+msg1) + if(!bypassDelay && delay) { + runIn(delay, "updateZoneStatus", [data: data]) + } else { + updateZoneStatus(data) + } +} + +public Boolean isZoneDeviceEnabled() { + return (Boolean)settings.createZoneDevice +} + +Map myZoneStatus() { + Map zoneDevs = getZoneDevices() + // log.debug "zoneDevs: $zoneDevs" + return [name: (String)app?.getLabel(), active: isActive(), paused: isPaused(true), id: app?.getId(), createZoneDevice: isZoneDeviceEnabled(), zoneDevices: (List)zoneDevs.devIds ?: []] +} + +void sendZoneStatus() { + Map zs = myZoneStatus() + sendLocationEvent(name: "es3ZoneState", value: app?.getId(), data: zs, isStateChange: true, display: false, displayed: false) + Boolean onl = (Boolean)zs.active && !(Boolean)zs.paused + relaySetOnline(onl) +} + +void sendZoneRemoved() { + sendLocationEvent(name: "es3ZoneRemoved", value: app?.getId(), data:[name: (String)app?.getLabel()], isStateChange: true, display: false, displayed: false) +} + +void updateZoneStatus(Map data) { + Boolean active = (Boolean)data.active + Map condStatus = (Map)data.condStatus + if((Boolean)data.recheck) { + condStatus = conditionStatus() + active = (Boolean)condStatus.ok + } + if(state.zoneConditionsOk != active) { + logInfo("Setting Zone (${getZoneName()}) Status to (${active ? "Active" : "Inactive"})") + state.zoneConditionsOk = active + addToZoneHistory((Map)data.evtData, condStatus) + sendZoneStatus() + if(isZoneNotifConfigured()) { + Boolean ok2Send = true + String msgTxt = active ? ((String)settings.notif_active_message ?: sNULL) : ((String)settings.notif_inactive_message ?: sNULL) + if(ok2Send && msgTxt) { + Map zoneDevices = getZoneDevices() + def alexaMsgDev = ((List)zoneDevices.devices)?.size() && (Boolean)settings.notif_alexa_mobile ? ((List)zoneDevices.devices)[0] : null + if(sendNotifMsg(getZoneName(), msgTxt, alexaMsgDev, false)) { logTrace("Sent Zone Notification...") } + } + } + if(active) { + if(settings.zone_active_switches_off) settings.zone_active_switches_off*.off() + if(settings.zone_active_switches_on) settings.zone_active_switches_on*.on() + } else { + if(settings.zone_inactive_switches_off) settings.zone_inactive_switches_off*.off() + if(settings.zone_inactive_switches_on) settings.zone_inactive_switches_on*.on() + } + } else logDebug("no change to Zone (${getZoneName()}) Status (${active ? "Active" : "Inactive"})") +} + +public getZoneHistory(Boolean asObj=false) { + List zHist = (List)getMemStoreItem(zoneHistFLD) ?: [] + List output = [] + if(zHist?.size()) { + zHist.sort { a,b-> b.dt <=> a.dt }.each { h-> + List hList = [] + hList.push([name: "Trigger:", val: h?.evtName]) + hList.push([name: "Device:", val: h?.evtDevice]) + // hList.push([name: "Value:", val: h?.evtValue]) + hList.push([name: "Zone Status:", val: (h?.active ? "Activate" : "Deactivate")]) + hList.push([name: "Conditions Passed:", val: h?.passed]) + hList.push([name: "Conditions Blocks:", val: h?.blocks]) + hList.push([name: "DateTime:", val: h?.dt]) + if(hList.size()) { + output.push(spanSm(kvListToHtmlTable(hList, sCLR4D9), sCLRGRY)) + } + } + } else { output.push("No History Items Found...") } + if(!asObj) { + output.each { i-> paragraph spanSm(i) } + } else { return output } +} + +private static String kvListToHtmlTable(List tabList, String color=sCLRGRY) { + String str = sBLANK + if(tabList?.size()) { + str += "" + tabList.each { it-> + str += "" + } + str += "
    ${spanSmBld((String)it.name)}${spanSmBr("${it.val}")}
    " + } + return str +} + +Map getZoneDevices(String cmd=sNULL, Boolean ignoreDoNotDisturb=false) { + // updDeviceInputs() + List devObj = [] + List devIds = [] + if(cmd==sNULL) cmd='announce' + List devices = parent?.getDevicesFromList(settings.zone_EchoDevices) + //devices?.each { devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]); devIds.push(it?.getId()); } + devices?.each { + Map devInfo = it?.getEchoDevInfo(cmd, ignoreDoNotDisturb) + if(devInfo) { + devObj?.push(devInfo) + devIds.push(it?.getId()) + } else { + logDebug("Uh Oh. did not get devinfo") + } + // devObj?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]) + // devIds.push(it?.getId()) + } + return [devices: devices, devObj: devObj, devIds: devIds]//, jsonStr: new groovy.json.JsonOutput().toJson(devObj)] +} + +public zoneRefreshHandler(evt) { + String cmd = evt?.value + // Map data = evt?.jsonData + switch(cmd) { + case "checkStatus": + checkZoneStatus([name: "zoneRefresh", displayName: "zoneRefresh"]) + scheduleCondition() + break + case "sendStatus": + sendZoneStatus() + break + } +} + +/* + * handle commands sent to a zone + * caller could be actions (a broadcast location event) (actions calls parent for speak or announce to combine across zones) + * or a zone device handler (calling its parent) + */ + +@SuppressWarnings('GroovyFallthrough') +public zoneCmdHandler(evt, Boolean chldDev=false, Boolean ignoreDoNotDisturb=false) { + // log.warn "zoneCmdHandler $evt" + String cmd = evt?.value + Map data = evt?.jsonData + String appId = app?.getId() as String + + // log.warn "zoneCmdHandler cmd: $cmd data: $data appId: $appId" + if(cmd && data && appId && data.zones && data.zones.contains(appId) && data.cmd) { // is the broadcast for me? + if(isPaused(true) || !isActive()) { + logInfo( "zoneCmdHandler: zone is paused or inactive Skipping $evt") + return + } + + // log.trace "zoneCmdHandler | Cmd: $cmd | Data: $data" + if(cmd == 'speak'/* && zoneDevs?.size() >= 2 */ ) { + Boolean fA = true + if(settings.forceAnnounce != null) fA = (Boolean)settings.forceAnnounce + if(fA) cmd = "announcement" + } + + Map zoneDevMap = getZoneDevices(cmd == 'speak' ? 'TTS' : 'announce', ignoreDoNotDisturb) + List zoneDevs = (List)zoneDevMap.devices + + if(data.zoneVolumes && data.zoneVolumes?.size() && data.zoneVolumes[appId]) { + Map zVol = (Map)data.zoneVolumes[appId] + // log.debug "zoneVolume: ${zVol}" + data.changeVol = zVol.change ?: data.changeVol + data.restoreVol = zVol.restore ?: data.restoreVol + } + Integer delay = (Integer)data.delay // ?: null + + // log.warn "zoneCmdHandler cmd: $cmd data: $data zoneDevMap: $zoneDevMap zoneDevs: $zoneDevs" + if(cmd in [ 'speak', 'announcement', 'voicecmd', 'sequence'] && !data.message) { logWarning("Zone Command Message is missing", true); return } + + String tmsg = (String)data.message + String msgp1 = " to Zone (${getZoneName()})" + String msgv1 = "${data.changeVol!=null ? " | Volume: ${data.changeVol}" : sBLANK}${data.restoreVol!=null ? " | Restore Volume: ${data.restoreVol}" : sBLANK}" + String msgd1 = "${delay ? " | Delay: (${delay})" : sBLANK}" + Boolean doFin = false + switch(cmd) { + case "speak": + logDebug("Sending Speak Command: (${tmsg})${msgp1}${msgv1}${msgd1}") + if(data.changeVol!=null || data.restoreVol!=null) { + zoneDevs?.each { dev-> + dev?.setVolumeSpeakAndRestore(data.changeVol, tmsg, data.restoreVol) + } + /* call zone vdevice with finishSendSpeakZ */ + if(!chldDev) relayFinishSpeak([:], 200, [message: tmsg, oldVolume: data.restoreVol, newVolume: data.changeVol]) + } else { + zoneDevs?.each { dev-> + dev?.speak(tmsg) + } + } + break + case "announcement": + if(zoneDevs?.size() > 0 && (List)zoneDevMap.devObj) { + logDebug("Sending Announcement Command: (${tmsg})${msgp1}${msgv1}${msgd1}") + List valS = tmsg.contains("::") ? (List)tmsg.split("::") : [sNULL, tmsg] + String mymsg = valS[1] + String mtitle = data.title ?: (valS[0] ?: getZoneName()) + //NOTE: Only sends command to first device in the list | We send the list of devices to announce one and then Amazon does all the processing + zoneDevs[0]?.sendAnnouncementToDevices(mymsg, mtitle, (List)zoneDevMap.devObj, data.changeVol, data.restoreVol) + /* call zone vdevice with finishAnnounce */ + if(!chldDev) relayFinishAnnouncement(mtitle+'::'+mymsg, [vol: (Integer)data.changeVol, restvol: (Integer)data.restoreVol]) + } + break + + case "voicecmd": + logDebug("Sending VoiceCmdAsText Command: (${tmsg})${msgp1}${msgd1}") + zoneDevs?.each { dev-> + dev?.voiceCmdAsText(tmsg) + } + break + case "sequence": + logDebug("Sending Sequence Command: (${tmsg})${msgp1}${msgd1}") + zoneDevs?.each { dev-> + dev?.executeSequenceCommand(tmsg) + } + break + + case "alarmvolume": + case "volume": + logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgv1}${msgd1}") + if(data.changeVol != null) { + zoneDevs?.each { dev-> + dev?."${data.cmd}"(data.changeVol) + } + /* call zone vdevice with updateLevel */ + if(!chldDev) getEzDevice().each { it.updateLevel(null, data.changeVol) } + } + break + + case "playback": + case "dnd": + case "mute": + case "unmute": + logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgd1}") + zoneDevs?.each { dev-> + if(data.cmd) { dev?."${data.cmd}"() } + } + /* if(!chldDev) todo need to call zone vdevice with mute/unmute updates */ + // getEzDevice().each { it.updateCookies(cookies) } + break + + case "builtin": + case "calendar": + case "weather": + logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1}${msgv1}${msgd1}") + zoneDevs?.each { dev-> + if(data.cmd) { dev?."${data.cmd}"(data.changeVol, data.restoreVol) } + } + doFin=true + break + case "sounds": + logDebug("Sending ${data.cmd?.capitalize()} | Name: ${tmsg} Command${msgp1}${msgv1}${msgd1}") + zoneDevs?.each { dev-> + dev?."${data.cmd}"(tmsg, data.changeVol, data.restoreVol) + } + doFin=true + break + case "music": + logDebug("Sending ${data.cmd?.capitalize()} Command${msgp1} | Provider: ${data.provider} | Search: ${data.search}${msgd1}${msgv1}") + zoneDevs?.each { dev -> + dev?."${data.cmd}"(data.search, data.provider, data.changeVol, data.restoreVol) + } + doFin=true + break + } + /* call zone vdevice with updateLevel */ + if(doFin && !chldDev) getEzDevice().each { it.updateLevel(data.restoreVol, data.changeVol) } + } +} +/****************************************** +| Restriction validators +*******************************************/ + +@SuppressWarnings('GroovyFallthrough') +static String attUnit(String attr) { + switch(attr) { + case "humidity": + case "level": + case "battery": + return " percent" + case "temperature": + return " degrees" + case "illuminance": + return " lux" + case "power": + return " watts" + default: + return sBLANK + } +} +/* +Double getDevValueAvg(devs, attr) { + List vals = devs?.findAll { it?.currentValue(attr)?.isNumber() }?.collect { it?.currentValue(attr).toDouble() } + return vals?.size() ? (vals?.sum()/vals?.size())?.round(1) as Double : null +} */ + +String getCurrentMode() { + return location?.mode +} + +List getLocationModes(Boolean sorted=false) { + List modes = location?.modes*.name + // log.debug "modes: ${modes}" + return (sorted) ? modes?.sort() : modes +} + +Boolean isInMode(List modes, Boolean not=false) { + if(modes instanceof String){ + modes = modes.toList() + log.warn("Found bad mode settings $modes, this setting needs to be updated to work properly") + } + return (modes) ? (not ? (!(getCurrentMode() in modes)) : (getCurrentMode() in modes)) : false +} + +Boolean isInAlarmMode(List modes) { + String a = location?.hsmStatus ?: "disarmed" + //return (modes) ? (parent?.getAlarmSystemStatus() in modes) : false + return (modes) ? (a in modes) : false +} +/* +Boolean areAllDevsSame(List devs, String attr, val) { + if(devs && attr) { return (devs?.findAll { it?.currentValue(attr) == val as String }?.size() == devs?.size()) } + return false +} */ + +Boolean allDevAttValsEqual(List devs, String att, val) { + if(devs && att) { + if(val instanceof List) return (devs.findAll { it?.currentValue(att) in val }?.size() == devs.size()) + else return (devs.findAll { it?.currentValue(att) == val }?.size() == devs.size()) + } + return false +} + +Boolean anyDevAttValsEqual(List devs, String att, val) { + if(devs && att) { + if(val instanceof List) return (devs.findAll { it?.currentValue(att) in (List)val }?.size() >= 1) + else return (devs.findAll { it?.currentValue(att) == val }?.size() >= 1) + } + return false +} + +Boolean anyDevAttNumValAbove(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() >= 1) : false +} +Boolean anyDevAttNumValBelow(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() >= 1) : false +} +Boolean anyDevAttNumValBetween(List devs, String att, Double low, Double high) { + return (devs && att && (low < high)) ? (devs?.findAll { + Double t = it?.currentValue(att)?.toDouble() + ( (t >= low) && (t <= high) ) }?.size() >= 1) : false +} +Boolean anyDevAttNumValEqual(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() >= 1) : false +} + +Boolean allDevAttNumValsAbove(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() > val }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsBelow(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() < val }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsBetween(List devs, String att, Double low, Double high) { + return (devs && att && (low < high)) ? (devs?.findAll { + Double t = it?.currentValue(att)?.toDouble() + ( (t >= low) && (t <= high) ) }?.size() == devs?.size()) : false +} +Boolean allDevAttNumValsEqual(List devs, String att, Double val) { + return (devs && att) ? (devs?.findAll { it?.currentValue(att)?.toDouble() == val }?.size() == devs?.size()) : false +} +/* +Boolean devAttValEqual(List devs, String devId, String att, val) { + if(devs && att) { return (devs.find { it?.currentValue(att) == val }) } + return false +} */ + +static String getAlarmSystemName(Boolean abbr=false) { + return (abbr ? "HSM" : "Hubitat Safety Monitor") +} + +public Map getZoneMetrics() { + Map out = [:] + out.version = appVersionFLD + out.activeDelay = settings.zone_active_delay ?: 0 + out.inactiveDelay = settings.zone_inactive_delay ?: 0 + out.zoneDevices = settings.zone_EchoDevices ?: [] + out.activeSwitchesOnCnt = settings.zone_active_switches_on ?: [] + out.activeSwitchesOffCnt = settings.zone_active_switches_off ?: [] + out.inactiveSwitchesOnCnt = settings.zone_inactive_switches_on ?: [] + out.inactiveSwitchesOffCnt = settings.zone_inactive_switches_off ?: [] + return out +} + +/****************************************** +| Time and Date Conversion Functions +*******************************************/ +String formatDt(Date dt, Boolean tzChg=true) { + return dateTimeFmt(dt, "E MMM dd HH:mm:ss z yyyy", tzChg) +} + +String dateTimeFmt(Date dt, String fmt, Boolean tzChg=true) { +// if(!(dt instanceof Date)) { try { dt = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", dt?.toString()) } catch(e) { dt = Date.parse("E MMM dd HH:mm:ss z yyyy", dt?.toString()) } } + def tf = new SimpleDateFormat(fmt) + if(tzChg && location?.timeZone) { tf.setTimeZone((TimeZone)location?.timeZone) } + return (String)tf.format(dt) +} + +String convToTime(Date dt) { + String newDt = dateTimeFmt(dt, "h:mm a") + if(newDt?.contains(":00 ")) { newDt?.toString()?.replaceAll(":00 ", sSPACE) } + return newDt +} + +static Date parseDate(String dt) { return Date.parse("E MMM dd HH:mm:ss z yyyy", dt) } + +//static Boolean isDateToday(Date dt) { return (dt && dt.clearTime().compareTo(new Date().clearTime()) >= 0) } +static Boolean isDateToday(Date dt) { return (dt && dt.clearTime() >= new Date().clearTime()) } + +static String strCapitalize(String str) { return str ? str.toString().capitalize() : sNULL } + +static String pluralizeStr(List obj, Boolean para=true) { return (obj?.size() > 1) ? "${para ? "(s)": "s"}" : sBLANK } + +String getDtNow() { + Date now = new Date() + return formatDt(now) +} + +String epochToTime(Date dt) { + return dateTimeFmt(dt, "h:mm a") +} + +String fmtTime(t, String fmt="h:mm a", Boolean altFmt=false) { + if(!t) return sNULL + Date dt = new Date().parse(altFmt ? "E MMM dd HH:mm:ss z yyyy" : "yyyy-MM-dd'T'HH:mm:ss.SSSZ", t.toString()) + return dateTimeFmt(dt, fmt) +} + +Long GetTimeDiffSeconds(String lastDate, String sender=sNULL) { + try { + if(lastDate?.contains("dtNow")) { return 10000 } + Date lastDt = parseDate(lastDate) // Date.parse("E MMM dd HH:mm:ss z yyyy", lastDate) + Long start = lastDt.getTime() + Long stop = now() + Long diff = Math.round((stop - start) / 1000L) + return diff.abs() + } + catch (ex) { + logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : sBLANK}lastDate: $lastDate): ${ex?.message}", false, ex) + return 10000L + } +} + +String getWeekDay() { + def df = new SimpleDateFormat("EEEE") + df.setTimeZone((TimeZone)location?.timeZone) + return (String)df.format(new Date()) +} +/* +String getWeekMonth() { + def df = new java.text.SimpleDateFormat("W") + df.setTimeZone(location?.timeZone) + return (String)df.format(new Date()) +} + +String getDay() { + def df = new java.text.SimpleDateFormat("D") + df.setTimeZone(location?.timeZone) + return (String)df.format(new Date()) +} + +String getYear() { + def df = new java.text.SimpleDateFormat("yyyy") + df.setTimeZone(location?.timeZone) + return (String)df.format(new Date()) +} +*/ +String getMonth() { + def df = new SimpleDateFormat("MMMMM") + df.setTimeZone((TimeZone)location?.timeZone) + return (String)df.format(new Date()) +} +/* +String getWeekYear() { + def df = new java.text.SimpleDateFormat("w") + df.setTimeZone(location?.timeZone) + return (String)df.format(new Date()) +} */ +/* +Map getDateMap() { + return [d: getWeekDay(), dm: getDay(), wm: getWeekMonth(), wy: getWeekYear(), m: getMonth(), y: getYear() ] +} */ + +Boolean isDayOfWeek(List opts) { + String day = getWeekDay() + return ( opts?.contains(day) ) +} + +Boolean isMonthOfYear(List opts) { + String mon = getMonth() + return ( opts?.contains(mon) ) +} + +Boolean isTimeOfDay(String startTime, String stopTime) { + if(!startTime && !stopTime) { return true } + Date st = toDateTime(startTime) + Date et = toDateTime(stopTime) + return timeOfDayIsBetween(st, et, new Date(), location.timeZone) +} + +Boolean advLogsActive() { return ((Boolean)settings.logDebug || (Boolean)settings.logTrace) } +public void logsEnabled() { if(advLogsActive() && !getTsVal("logsEnabled")) { logTrace("enabling logging timer"); updTsVal("logsEnabled") } } +public void logsDisable() { + if(advLogsActive()) { + Integer dtSec = getLastTsValSecs("logsEnabled", null) + if(dtSec && (dtSec > 3600*6)) { + settingUpdate("logDebug", sFALSE, sBOOL) + settingUpdate("logTrace", sFALSE, sBOOL) + remTsVal("logsEnabled") + log.debug "Disabling debug logs" + } + } +} + +public void enableDebugLog() { settingUpdate("logDebug", sTRUE, sBOOL); logInfo("Debug Logs Enabled From Main App...") } +public void disableDebugLog() { settingUpdate("logDebug", sFALSE, sBOOL); logInfo("Debug Logs Disabled From Main App...") } +public void enableTraceLog() { settingUpdate("logTrace", sTRUE, sBOOL); logInfo("Trace Logs Enabled From Main App...") } +public void disableTraceLog() { settingUpdate("logTrace", sFALSE, sBOOL); logInfo("Trace Logs Disabled From Main App...") } + +@Field volatile static Map tsDtMapFLD=[:] + +private void updTsVal(String key, String dt=sNULL) { + String appId=app.getId() + Map data=tsDtMapFLD[appId] ?: [:] + if(!data) data = (Map)state.tsDtMap ?: [:] + if(key) { data[key] = dt ?: getDtNow() } + tsDtMapFLD[appId]=data + tsDtMapFLD=tsDtMapFLD + + state.tsDtMap = data +} + +private void remTsVal(key) { + String appId=app.getId() + Map data=tsDtMapFLD[appId] ?: [:] + if(!data) data = (Map)state.tsDtMap ?: [:] + if(key) { + if(key instanceof List) { + List aa = (List)key + aa.each { String k-> + if(data.containsKey(k)) { data.remove(k) } + } + } else if(data.containsKey((String)key)) { data.remove((String)key) } + } + tsDtMapFLD[appId]=data + tsDtMapFLD=tsDtMapFLD + + state.tsDtMap = data +} + +String getTsVal(String key) { + String appId=app.getId() + Map tsMap=tsDtMapFLD[appId] + if(!tsMap) tsMap = (Map)state.tsDtMap ?: [:] + if(key && tsMap && tsMap[key]) { return (String)tsMap[key] } + return sNULL +} + +Integer getLastTsValSecs(String val, Integer nullVal=1000000) { + return (val && getTsVal(val)) ? GetTimeDiffSeconds(getTsVal(val)).toInteger() : nullVal +} + +private void updAppFlag(String key, Boolean val) { + def data = atomicState?.appFlagsMap ?: [:] + if(key) { data[key] = val } + atomicState?.appFlagsMap = data +} + +@SuppressWarnings('unused') +private void remAppFlag(key) { + Map data = atomicState?.appFlagsMap ?: [:] + if(key) { + if(key instanceof List) { + List aa = (List)key + aa.each { String k-> if(data.containsKey(k)) { data.remove(k) } } + } else { if(data.containsKey(key)) { data.remove(key) } } + atomicState.appFlagsMap = data + } +} + +Boolean getAppFlag(String val) { + Map aMap = atomicState?.appFlagsMap + if(val && aMap && aMap[val]) { return (Boolean)aMap[val] } + return false +} + +@SuppressWarnings('unused') +private stateMapMigration() { + //Timestamp State Migrations + Map tsItems = [:] + tsItems?.each { k, v-> if(state.containsKey(k)) { updTsVal(v, (String)state[k]); state.remove(k) } } + + //App Flag Migrations + Map flagItems = [:] + flagItems?.each { k, v-> if(state.containsKey(k)) { updAppFlag(v, (Boolean)state[k]); state.remove(k) } } + updAppFlag("stateMapConverted", true) +} + +/****************************************** +| App Input Description Functions +*******************************************/ +/* +String unitStr(String att) { + switch(att) { + case "temp": + return "\u00b0${getTemperatureScale() ?: "F"}" + case "humidity": + case "battery": + return "%" + default: + return sBLANK + } +} */ + +String getAppNotifDesc(Boolean hide=false) { + String str = sBLANK + if(isZoneNotifConfigured()) { + Boolean ok = getOk2Notify() + str += hide ? sBLANK : spanSmBr(strUnder("Send allowed: ") + getOkOrNotSymHTML(ok)) + str += ((List)settings.notif_devs) ? spanSmBr(" ${sBULLET} Notification Device${pluralizeStr((List)settings.notif_devs)} (${((List)settings.notif_devs).size()})") : sBLANK + str += (Boolean)settings.notif_alexa_mobile ? spanSmBr(" ${sBULLET} Alexa Mobile App") : sBLANK + String res = getNotifSchedDesc(true) + str += res ? spanSmBr(res) : sBLANK + } + return str != sBLANK ? str : sNULL +} + +List getQuietDays() { + List allDays = weekDaysEnum() + List curDays = (List)settings.notif_days ?: [] + return allDays?.findAll { (!curDays?.contains(it)) } +} + +@SuppressWarnings('GroovyVariableNotAssigned') +String getNotifSchedDesc(Boolean min=false) { + String startType = settings.notif_time_start_type + Date startTime + String stopType = settings.notif_time_stop_type + Date stopTime + List dayInput = (List)settings.notif_days + List modeInput = (List)settings.notif_modes + String str = sBLANK + + if(startType && stopType) { + startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null + stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null + } + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L + Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + Boolean timeOk = notifTimeOk() + Boolean daysOk = dayInput ? (isDayOfWeek(dayInput)) : true + Boolean modesOk = modeInput ? (isInMode(modeInput)) : true + Boolean rest = !(daysOk && modesOk && timeOk) + String startLbl = startTime ? epochToTime(startTime) : sBLANK + String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK + str += (startLbl && stopLbl) ? spanSmBr(" ${sBULLET} Restricted Times: ${startLbl} - ${stopLbl} " + getOkOrNotSymHTML(!timeOk)) : sBLANK + List qDays = getQuietDays() + str += dayInput && qDays ? spanSmBr(" ${sBULLET} Restricted Day${pluralizeStr(qDays, false)}: (${qDays?.join(", ")}) " + getOkOrNotSymHTML(!daysOk)) : sBLANK + str += modeInput ? spanSm(" ${sBULLET} Allowed Mode${pluralizeStr(modeInput, false)}: (${modeInput?.join(", ")}) " + getOkOrNotSymHTML(!modesOk)) : sBLANK + str = str ? spanSmBr(" ${sBULLET} Restrictions Active: " + getOkOrNotSymHTML(rest)) + spanSm(str) : sBLANK + return (str != sBLANK) ? divSm(str, sCLR4D9) : sNULL +} + +String getOverallDesc() { + Map condStatus = conditionStatus() + String str = spanSmBld("Zone is ") + spanSmBr( ((Boolean)condStatus.ok ? "Active " : "Inactive ") + getOkOrNotSymHTML((Boolean)condStatus.ok)) + return str +} + +String getConditionsDesc(Boolean addFoot=true) { + Boolean confd = conditionsConfigured() + String sPre = "cond_" + String str = sBLANK + if(confd) { + str = getOverallDesc() + str += spanSmBr(" ${sBULLET} " + spanSmBld("${reqAllCond() ? "All Conditions Required" : "Any Condition Allowed"}")) + if((Boolean)timeCondConfigured()) { + str += spanSmBr(" ${sBULLET} Time Between Allowed: " + getOkOrNotSymHTML(timeCondOk())) + str += spanSmBr(" - ${getTimeCondDesc(false)}") + } + if((Boolean)dateCondConfigured()) { + str += spanSmBr(" ${sBULLET} Date:") + str += (List)settings.cond_days ? spanSmBr(" - Days Allowed: " + getOkOrNotSymHTML(isDayOfWeek((List)settings.cond_days))) : sBLANK + str += (List)settings.cond_months ? spanSmBr(" - Months Allowed: " + getOkOrNotSymHTML(isMonthOfYear((List)settings.cond_months))) : sBLANK + } + if((List)settings.cond_alarm || (List)settings.cond_mode) { + str += spanSmBr(" ${sBULLET} Location: " + getOkOrNotSymHTML(locationCondOk())) + String a = location?.hsmStatus ?: "disarmed" + str += (List)settings.cond_alarm ? spanSmBr(" - Alarm Mode ${a} in: ${(List)settings.cond_alarm} " + getOkOrNotSymHTML(isInAlarmMode((List)settings.cond_alarm))) : sBLANK + Boolean not = ((String)settings.cond_mode_cmd == "not") + str += settings.cond_mode ? spanSmBr(" - Mode ${getCurrentMode()} (${not ? "not in" : "in"}): ${(List)settings.cond_mode} " + getOkOrNotSymHTML(isInMode((List)settings.cond_mode, not))) : sBLANK + } + if(deviceCondConfigured()) { + List devConds = lDATTSTR + lDATTNUM + devConds.each { String evt-> + if(devCondConfigured(evt)) { + Boolean condOk = false + if(evt in lDATTSTR) { condOk = checkDeviceCondOk(evt) } + else if(evt in lDATTNUM) { condOk = checkDeviceNumCondOk(evt) } + + List devs = settings."${sPre}${evt}" ?: null + if(devs){ + List myV = [] + if(!addFoot) devs.each { dev -> myV.push(dev?.currentValue(evt)) } + str += spanSmBr(" ${sBULLET} ${evt?.capitalize()} (${settings."${sPre}${evt}"?.size()}) ${!addFoot ? myV : sBLANK} " + getOkOrNotSymHTML(condOk)) + } + + String a = " - Desired Value: " + String cmd = (String)settings."${sPre}${evt}_cmd" ?: sNULL + if(cmd in [sBETWEEN, sBELOW, sABOVE, sEQUALS]) { + def cmdLow = settings."${sPre}${evt}_low" ?: null + def cmdHigh = settings."${sPre}${evt}_high" ?: null + def cmdEq = settings."${sPre}${evt}_equal" ?: null + str += (cmd == sEQUALS && cmdEq) ? spanSmBr(a+"( =${cmdEq}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK + str += (cmd == sBETWEEN && cmdLow && cmdHigh) ? spanSmBr(a+"(${cmdLow-cmdHigh}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK + str += (cmd == sABOVE && cmdHigh) ? spanSmBr(a+"( >${cmdHigh}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK + str += (cmd == sBELOW && cmdLow) ? spanSmBr(a+"( <${cmdLow}${attUnit(evt)})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK + } else { + str += cmd ? spanSmBr(a+"(${cmd})" + (settings."cond_${inType}_avg" ? "(Avg)" : sBLANK)) : sBLANK + } + str += ((Boolean)settings."${sPre}${evt}_all" == true) ? spanSmBr(" - Require All: (${settings."${sPre}${evt}_all"})") : sBLANK + } + } + } + str += addFoot ? inputFooter(sTTM) : sBLANK + } else { + str += addFoot ? inputFooter(sTTC, sCLRGRY, true) : sBLANK + } + return str +} + +String getZoneDesc() { + if(devicesConfigured() && conditionsConfigured()) { + List eDevs = parent?.getDevicesFromList(settings.zone_EchoDevices)?.collect { it?.displayName as String } + String str = eDevs?.size() ? "Echo Devices in Zone:\n${eDevs?.join("\n")}\n" : sBLANK + str += settings.zone_active_delay ? bulletItem(sBLANK, "Activate Delay: (${settings.zone_active_delay})\n)") : sBLANK + str += settings.zone_inactive_delay ? bulletItem(sBLANK, "Deactivate Delay: (${settings.zone_inactive_delay})\n") : sBLANK + str += "\n"+sTTM + return str + } else { + return sTTC + } +} + +@SuppressWarnings('GroovyVariableNotAssigned') +String getTimeCondDesc(Boolean addPre=true) { + Date startTime + Date stopTime + String startType = settings.cond_time_start_type + String stopType = settings.cond_time_stop_type + if(startType && stopType) { + startTime = startType == 'time' && settings.cond_time_start ? toDateTime(settings.cond_time_start) : null + stopTime = stopType == 'time' && settings.cond_time_stop ? toDateTime(settings.cond_time_stop) : null + } + + String startLbl1 = sBLANK + String stopLbl1 = sBLANK + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + def sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.cond_time_start_offset ? settings.cond_time_start_offset*1000L : 0L + Long stopoffset = settings.cond_time_stop_offset ? settings.cond_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + startLbl1 = startType.capitalize() + sSPACE + "${startoffset ? "with offset " : sBLANK}" + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + stopLbl1 = stopType.capitalize() + sSPACE + "${stopoffset ? "with offset " : sBLANK}" + } + } + String startLbl = startTime ? epochToTime(startTime) : sBLANK + String stopLbl = stopTime ? epochToTime(stopTime) : sBLANK + + return startLbl && stopLbl ? "${addPre ? "Time Condition:\n" : sBLANK}(${startLbl1}${startLbl} - ${stopLbl1}${stopLbl})" : sTTC +} + +static String getInputToStringDesc(inpt, addSpace = null) { + Integer cnt = 0 + String str = sBLANK + if(inpt) { + inpt.sort().each { item -> + cnt = cnt+1 + str += item ? (((cnt < 1) || (inpt.size() > 1)) ? "\n ${item}" : "${addSpace ? " " : sBLANK}${item}") : sBLANK + } + } + //log.debug "str: $str" + return str != sBLANK ? str : sNULL +} + +static String randomString(Integer len) { + def pool = ["a".."z",0..9].flatten() + Random rand = new Random(new Date().getTime()) + def randChars = (0..len).collect { pool[rand.nextInt(pool.size())] } + // logDebug("randomString: ${randChars?.join()}") + return randChars.join() +} + +static def getRandomItem(items) { + List list = new ArrayList() + items?.each { list.add(it) } + return list?.get(new Random().nextInt(list?.size())) +} + +/*********************************************************************************************************** + HELPER FUNCTIONS +************************************************************************************************************/ +void settingUpdate(String name, value, String type=sNULL) { + if(name && type) { app?.updateSetting(name, [type: type, value: value]) } + else if (name) { app?.updateSetting(name, value) } +} + +void settingRemove(String name) { + logTrace("settingRemove($name)...") + if(name && settings.containsKey(name)) { app?.removeSetting(name) } +} + +static List weekDaysEnum() { return ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] } + +static Map daysOfWeekMap() { return ["MON":"Monday", "TUE":"Tuesday", "WED":"Wednesday", "THU":"Thursday", "FRI":"Friday", "SAT":"Saturday", "SUN":"Sunday"] } + +static List monthEnum() { return ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] } + +static Map monthMap() { return ["1":"January", "2":"February", "3":"March", "4":"April", "5":"May", "6":"June", "7":"July", "8":"August", "9":"September", "10":"October", "11":"November", "12":"December"] } + +static Map getAlarmTrigOpts() { + return ["armedAway":"Armed Away", "armingAway":"Arming Away Pending exit delay","armedHome":"Armed Home","armingHome":"Arming Home pending exit delay", "armedNight":"Armed Night", "armingNight":"Arming Night pending exit delay","disarmed":"Disarmed", "allDisarmed":"All Disarmed","alerts":"Alerts"] +} + +Integer getLastNotifMsgSec() { return !state.lastNotifMsgDt ? 100000 : GetTimeDiffSeconds((String)state.lastNotifMsgDt, "getLastMsgSec").toInteger() } +Integer getLastChildInitRefreshSec() { return !state.lastChildInitRefreshDt ? 3600 : GetTimeDiffSeconds((String)state.lastChildInitRefreshDt, "getLastChildInitRefreshSec").toInteger() } + +Boolean getOk2Notify() { + Boolean smsOk = false // = (settings.notif_sms_numbers?.toString()?.length()>=10) + Boolean pushOk = false // = settings.notif_send_push + Boolean pushOver = false // = (settings.notif_pushover && settings.notif_pushover_devices) + Boolean alexaMsg = (Boolean)settings.notif_alexa_mobile + Boolean notifDevsOk = (((List)settings.notif_devs)?.size()) + Boolean daysOk = (List)settings.notif_days ? (isDayOfWeek((List)settings.notif_days)) : true + Boolean timeOk = notifTimeOk() + Boolean modesOk = (List)settings.notif_modes ? (isInMode((List)settings.notif_modes)) : true + Boolean result + result = (smsOk || pushOk || alexaMsg || notifDevsOk || pushOver) + if(!(daysOk && modesOk && timeOk)) { result = false } + logDebug("getOk2Notify() RESULT: $result | notifDevsOk: $notifDevsOk | smsOk: $smsOk | pushOk: $pushOk | pushOver: $pushOver | alexaMsg: $alexaMsg || daysOk: $daysOk | timeOk: $timeOk | modesOk: $modesOk") + return result +} + +Boolean notifTimeOk() { + Date startTime + Date stopTime + // these are quiet time start/stop + String startType = settings.notif_time_start_type + String stopType = settings.notif_time_stop_type + if(startType && stopType) { + startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null + stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null + } else { return true } + + Date now = new Date() + if(startType in lSUNRISESET || stopType in lSUNRISESET) { + Map sun = getSunriseAndSunset() + Long lsunset = sun.sunset.time + Long lsunrise = sun.sunrise.time + Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L + Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L + if(startType in lSUNRISESET) { + Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset + startTime = new Date(startl) + } + if(stopType in lSUNRISESET) { + Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset + stopTime = new Date(stopl) + } + } + + if(startTime && stopTime) { + Boolean not = startTime.getTime() > stopTime.getTime() + Boolean isBtwn = !timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) + isBtwn = not ? !isBtwn : isBtwn + logTrace("NotifTimeOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not ? startTime:stopTime})") + return isBtwn + } else { return true } +} + +// Sends the notifications based on app settings +public Boolean sendNotifMsg(String msgTitle, String msg, alexaDev=null, Boolean showEvt=true) { + logTrace("sendNotifMsg() | msgTitle: ${msgTitle}, msg: ${msg}, $alexaDev, showEvt: ${showEvt}") + List sentSrc = [] // ["Push"] + Boolean sent = false + try { + String newMsg = "${msgTitle}: ${msg}" + String flatMsg = newMsg.toString().replaceAll("\n", sSPACE) + if(!getOk2Notify()) { + logInfo( "sendNotifMsg: Notification not configured or Message Skipped During Quiet Time ($flatMsg)") + //if(showEvt) { sendNotificationEvent(newMsg) } + } else { + if((List)settings.notif_devs) { + sentSrc.push("Notification Devices") + ((List)settings.notif_devs)?.each { it?.deviceNotification(newMsg) } + sent = true + } + if((Boolean)settings.notif_alexa_mobile && alexaDev) { + alexaDev?.sendAlexaAppNotification(newMsg) + sentSrc.push("Alexa Mobile App") + sent = true + } + if(sent) { + state.lastNotificationMsg = flatMsg + updTsVal("lastNotifMsgDt") + logDebug("sendNotifMsg: Sent ${sentSrc} (${flatMsg})") + } + } + } catch (ex) { + logError("sendNotifMsg $sentstr Exception: ${ex}", false, ex) + } + return sent +} + +Boolean isZoneNotifConfigured() { + return ((String)settings.notif_active_message || (String)settings.notif_inactive_message) && + ((List)settings.notif_devs || (Boolean)settings.notif_alexa_mobile) + +} + +static Integer versionStr2Int(String str) { return str ? str.replaceAll("\\.", sBLANK)?.toInteger() : null } + +Boolean minVersionFailed() { + try { + Integer minDevVer = (Integer)parent?.minVersions()["zoneApp"] + return minDevVer != null && versionStr2Int(appVersionFLD) < minDevVer + } catch (ignored) { + return false + } +} + +// public String gitBranch() { +// if(gitBranchFLD == sNULL) { gitBranchFLD = (String) parent?.gitBranch() } +// return (String)gitBranchFLD +// } + +Boolean isActive() { + Boolean st = (Boolean)state.zoneConditionsOk + return st != null ? st : (Boolean)conditionStatus().ok +} + +Boolean isPaused(Boolean chkAll = false) { return (Boolean)settings.zonePause && (chkAll ? !((Boolean)state.dupPendingSetup == true) : true) } + +static String documentationUrl() { return "https://tonesto7.github.io/echo-speaks-docs" } +static String videoUrl() { return "https://www.youtube.com/watch?v=wQPPlTFaGb4&ab_channel=SimplySmart123%E2%9C%85" } + +static String getAppImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/${imgName}.png" } + +static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } + +static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

    ${i ? " " : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}

    " } + +static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

    ${i ? " " : sBLANK} ${t?.replaceAll("\n", "
    ")}

    ${st ? "${st}" : sBLANK}" } + +static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
    " : sBLANK}${bold ? "" : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}${color ? "
    " : sBLANK}" } + +@SuppressWarnings('unused') +static String inTS1(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return spanSmBldUnd(str, clr, img) } +static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str.replaceAll("\n", sSPACE).replaceAll("
    ", sSPACE), und), clr, img) } + +// Root HTML Objects +static String sectHead(String str, String img = sNULL) { return str ? "

    " + spanImgStr(img) + span(str, "darkorange", sNULL, true) + "

    " + "
    " : sBLANK } +static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } +static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
    ${str}
    ${br ? sLINEBR : sBLANK}" : sBLANK } +static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } +static String divImgStr(String str, String img=sNULL) { return str ? div(img ? spanImg(img) + span(str) : str) : sBLANK } +static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } +static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } +static String htmlLine(String color=sCLR4D9) { return "
    " } +static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } +static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? ((noBr ? sBLANK : lineBr()) + divSmBld(str, clr)) : sBLANK } +static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } +static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } +static String optPrefix() { return spanSm(" (Optional)", "violet") } +// + +// Custom versions of the root objects above +static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } +static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } +static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } +static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } +static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } +static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } +static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } +static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } +static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } +static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } +static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } +static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } + + +static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } +static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } +static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } +static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } +static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } +static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } +static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } +static String bulletItem(String inStr, String strVal) { return "${inStr == sBLANK ? sBLANK : "\n"}"+sSPACE+sBULLET+sSPACE+strVal } +static String dashItem(String inStr, String strVal, Boolean newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : "\n"} - "+strVal } + +Integer stateSize() { + String j = new JsonOutput().toJson((Map)state) + return j.length() +} +Integer stateSizePerc() { return (Integer)(((stateSize() / 100000)*100).toDouble().round(0)) } + +private addToLogHistory(String logKey, String data, Integer max=10) { + Boolean ssOk = true //(stateSizePerc() > 70) + List eData = getMemStoreItem(logKey) + if(eData.find { it?.data == data }) { return } + eData.push([dt: getDtNow(), data: data]) + Integer lsiz = eData.size() + if(!ssOk || lsiz > max) { eData = eData?.drop( (lsiz-max) ) } + updMemStoreItem(logKey, eData) +} + +public Map getLogConfigs() { + return [ + info: (Boolean) settings.logInfo, + warn: (Boolean) settings.logWarn, + error: (Boolean) settings.logError, + debug: (Boolean) settings.logDebug, + trace: (Boolean) settings.logTrace, + ] +} + +private void logDebug(String msg) { if((Boolean)settings.logDebug) { log.debug logPrefix(msg, "purple") } } +private void logInfo(String msg) { if((Boolean)settings.logInfo != false) { log.info sSPACE + logPrefix(msg, "#0299b1") } } +private void logTrace(String msg) { if((Boolean)settings.logTrace) { log.trace logPrefix(msg, sCLRGRY) } } +private void logWarn(String msg, Boolean noHist=false) { if((Boolean)settings.logWarn != false) { log.warn sSPACE + logPrefix(msg, sCLRORG) }; if(!noHist) { addToLogHistory("warnHistory", msg, 15) } } + +void logError(String msg, Boolean noHist=false, ex=null) { + if((Boolean)settings.logError != false) { + log.error logPrefix(msg, sCLRRED) + String a + try { + if (ex) a = getExceptionMessageWithLine(ex) + } catch (ignored) { + } + if(a) log.error logPrefix(a, sCLRRED) + } + if(!noHist) { addToLogHistory("errorHistory", msg, 15) } +} + +static String logPrefix(String msg, String color = sNULL) { + return span("Zone (v" + appVersionFLD + ") | ", sCLRGRY) + span(msg, color) +} + +@SuppressWarnings('unused') +private Map getLogHistory() { + List warn = getMemStoreItem("warnHistory") + List errs = getMemStoreItem("errorHistory") + return [ warnings: []+warn, errors: []+errs ] +} + +@SuppressWarnings('unused') +private void clearHistory() { historyMapFLD = [:]; mb() } + +// FIELD VARIABLE FUNCTIONS +private void updMemStoreItem(String key, List val) { + String appId = app.getId() + Map memStore = historyMapFLD[appId] ?: [:] + memStore[key] = val + historyMapFLD[appId] = memStore + historyMapFLD = historyMapFLD + // log.debug("updMemStoreItem(${key}): ${memStore[key]}") +} + +private List getMemStoreItem(String key){ + String appId = app.getId() + Map memStore = historyMapFLD[appId] ?: [:] + return (List)memStore[key] ?: [] +} +// Memory Barrier +@Field static Semaphore theMBLockFLD=new Semaphore(0) + +static void mb(/*String meth=sNULL*/){ + if((Boolean)theMBLockFLD.tryAcquire()){ + theMBLockFLD.release() + } +} + +static 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 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 if(obj instanceof Date) {return "Date"} + else { return "unknown"} +} + +//******************************************************************* +// CLONE CHILD LOGIC +//******************************************************************* +public Map getSettingsAndStateMap() { + Map typeObj = parent?.getAppDuplTypes() + Map setObjs = [:] + if(typeObj) { + ((Map>)typeObj.stat).each { sk, sv-> + sv?.each { svi-> if(settings.containsKey(svi)) { setObjs[svi] = [type: sk, value: settings[svi] ] } } + } + ((Map>)typeObj.ends).each { ek, ev-> + ev?.each { evi-> + settings.findAll { it?.key?.endsWith(evi) }?.each { String fk, fv-> + def vv = settings[fk] // fv + if(ek==sTIME) vv = dateTimeFmt((Date)toDateTime(vv), "HH:mm") + setObjs[fk] = [type: ek, value: vv] + } + } + } + ((Map)typeObj.caps).each { ck, cv-> + settings.findAll { it.key.endsWith(ck) }?.each { String fk, fv-> + setObjs[fk] = [type: "capability", value: (fv instanceof List ? fv?.collect { it?.id?.toString() } : it?.id?.toString ) ] } //.toString().toList() + } + ((Map)typeObj.dev).each { dk, dv-> + settings.findAll { it.key.endsWith(dk) }?.each { String fk, fv-> + setObjs[fk] = [type: "device", value: (fv instanceof List ? fv.collect { it?.id?.toString() } : it?.id?.toString() ) ] } //.toString().toList() + } + } + Map data = [:] + String newlbl = ((String)app?.getLabel())?.replace(" (Z ${sPAUSESymFLD})", sBLANK) + data.label = newlbl?.replace(" (Z)", sBLANK) + data.settings = setObjs + + List stskip = [ + // "isInstalled", "isParent", + "lastNotifMsgDt", "lastNotificationMsg", "setupComplete", "valEvtHistory", "warnHistory", "errorHistory", + "appData", "actionHistory", "authValidHistory", "deviceRefreshInProgress", "noticeData", "installData", "herokuName", "zoneHistory", + // actions + "tierSchedActive", + // zones + "zoneConditionsOk", "configStatusMap", "tsDtMap", "dateInstalled", "handleGuardEvents", "startTime", "stopTime", "alexaGuardState", "appFlagsMap", + "dupPendingSetup", "dupOpenedByUser" + ] + data.state = state?.findAll { !((String)it?.key in stskip) } + return data +} diff --git a/apps/echo-speaks.groovy b/apps/echo-speaks.groovy index 22cc93601..0eca111cd 100644 --- a/apps/echo-speaks.groovy +++ b/apps/echo-speaks.groovy @@ -1,7609 +1,7609 @@ -/** - * Echo Speaks App (Hubitat) - * - * Copyright 2018, 2019, 2020, 2021, 2022 Anthony Santilli - * Code Contributions by @nh.schottfam - * - * 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. - * - */ -//file:noinspection GroovyUnusedAssignment -//file:noinspection unused -//file:noinspection GroovySillyAssignment -//file:noinspection GrMethodMayBeStatic - - -import groovy.json.JsonOutput -import groovy.json.JsonSlurper -import groovy.transform.Field -import java.text.SimpleDateFormat -import java.util.concurrent.Semaphore - -//************************************************ -//* STATIC VARIABLES * -//************************************************ -@Field static final String appVersionFLD = '4.2.0.2' -@Field static final String appModifiedFLD = '2022-04-18' -@Field static final String gitBranchFLD = 'master' -@Field static final String platformFLD = 'Hubitat' -@Field static final Boolean devModeFLD = false -@Field static final Map minVersionsFLD = [echoDevice: 4202, actionApp: 4202, zoneApp: 4202, zoneEchoDevice: 4202, server: 270] //These values define the minimum versions of code this app will work with. - -@Field static final String sNULL = (String)null -@Field static final String sBLANK = '' -@Field static final String sSPACE = ' ' -@Field static final String sBULLET = '\u2022' -@Field static final String sFRNFACE = '\u2639' -@Field static final String okSymFLD = "\u2713" -@Field static final String notOkSymFLD = "\u2715" -@Field static final String sPAUSESymFLD = "\u275A\u275A" -@Field static final String sLINEBR = '
    ' -@Field static final String sFALSE = 'false' -@Field static final String sTRUE = 'true' -@Field static final String sBOOL = 'bool' -@Field static final String sENUM = 'enum' -@Field static final String sNUMBER = 'number' -@Field static final String sTIME = 'time' -@Field static final String sMODE = 'mode' -@Field static final String sAPPJSON = 'application/json' -@Field static final String sIN_IGNORE = 'In Ignore Device Input' -@Field static final String sARM_AWAY = 'ARMED_AWAY' -@Field static final String sARM_STAY = 'ARMED_STAY' -@Field static final String sCOMPLT = 'complete' -@Field static final String sMEDIUM = 'medium' -@Field static final String sSMALL = 'small' -@Field static final String sCLR4D9 = '#2784D9' -@Field static final String sCLRRED = 'red' -@Field static final String sCLRRED2 = '#cc2d3b' -@Field static final String sCLRGRY = 'gray' -@Field static final String sCLRGRN = 'green' -@Field static final String sCLRGRN2 = '#43d843' -@Field static final String sCLRORG = 'orange' -@Field static final String sTTM = 'Tap to modify...' -@Field static final String sTTC = 'Tap to configure...' -@Field static final String sTTP = 'Tap to proceed...' -@Field static final String sTTVD = 'Tap to view details...' -@Field static final String sTTS = 'Tap to select...' -@Field static final String sSETTINGS = 'settings' -@Field static final String sUnknown = 'Unknown' -@Field static final String sUNKNOWN = 'unknown' -@Field static final String sRESET = 'reset' -@Field static final String sHEROKU = 'heroku' -@Field static final String sEXTNRL = 'external' -@Field static final String sDEBUG = 'debug' -@Field static final String sAMAZONORNG = 'amazon_orange' -@Field static final String sDEVICES = 'devices' -@Field static final String sSWITCH = 'switch' -@Field static final String sASTR = 'a' -@Field static final String sTSTR = 't' -@Field static final List lSUNRISESET = ['sunrise', 'sunset'] - -//************************************************ -//* IN-MEMORY ONLY VARIABLES * -//* (Cleared only on HUB REBOOT or CODE UPDATES) * -//************************************************ -@Field volatile static Map historyMapFLD = [:] -@Field volatile static Map cookieDataFLD = [:] -@Field volatile static Map echoDeviceMapFLD = [:] -@Field volatile static Map childDupMapFLD = [:] -@Field volatile static Map workQMapFLD = [:] -//@Field static Map guardDataFLD = [:] -@Field volatile static Map zoneStatusMapFLD = [:] -@Field volatile static Map bluetoothDataFLD = [:] -@Field volatile static Map alexaRoutinesDataFLD = [:] -@Field volatile static Map dndDataFLD = [:] -@Field volatile static Boolean guardArmPendingFLD = false - -definition( - name : "Echo Speaks", - namespace : "tonesto7", - author : "Anthony Santilli", - description : "Integrate your Amazon Echo devices into your Hubitat environment to create virtual Echo Devices. This allows you to speak text, make announcements, control media playback including volume, and many other Alexa features.", - category : "My Apps", - iconUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_speaks_3.1x${(Boolean)state.updateAvailable ? "_update" : sBLANK}.png", - iconX2Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_speaks_3.2x${(Boolean)state.updateAvailable ? "_update" : sBLANK}.png", - iconX3Url : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/resources/icons/echo_speaks_3.3x${(Boolean)state.updateAvailable ? "_update" : sBLANK}.png", - importUrl : "https://raw.githubusercontent.com/tonesto7/echo-speaks/master/apps/echo-speaks.groovy", - oauth : true, - singleInstance : false, - documentationLink : documentationUrl(), - videoLink : videoUrl() -) - -preferences { - page(name: "startPage") - page(name: "mainPage") - page(name: "settingsPage") - page(name: "devicePrefsPage") - page(name: "deviceManagePage") - page(name: "devCleanupPage") - page(name: "newSetupPage") - page(name: "authStatusPage") - page(name: "actionsPage") - page(name: "zonesPage") - page(name: "devicePage") - page(name: "deviceListPage") - page(name: "unrecogDevicesPage") - page(name: "changeLogPage") - page(name: "notifPrefPage") - page(name: "alexaGuardPage") - page(name: "alexaGuardAutoPage") - page(name: "servPrefPage") - page(name: "musicSearchTestPage") - page(name: "alexaRoutinesTestPage") - page(name: "searchTuneInResultsPage") - page(name: "deviceTestPage") - page(name: "donationPage") - page(name: "speechPage") - page(name: "announcePage") - page(name: "sequencePage") - page(name: "viewZoneHistory") - page(name: "viewActionHistory") - page(name: "setNotificationTimePage") - page(name: "actionDuplicationPage") - page(name: "zoneDuplicationPage") - page(name: "uninstallPage") -} - -def startPage() { - state.isParent = true - checkVersionData(true) - state.childInstallOkFlag = false - if(!(Boolean)state.resumeConfig && (Boolean)state.isInstalled) { stateMigrationChk(); checkGuardSupport() } - if((Boolean)state.resumeConfig || ((Boolean)state.isInstalled && !(Boolean)state.serviceConfigured)) { return servPrefPage() } - else if(showChgLogOk()) { return changeLogPage() } - else if(showDonationOk()) { return donationPage() } - else { return mainPage() } -} - -def mainPage() { - Boolean tokenOk = getAccessToken() - Boolean newInstall = !(Boolean)state.isInstalled - -// force defaults - if(settings.autoCreateDevices == null) settingUpdate("autoCreateDevices", sTRUE, sBOOL) - if(settings.autoRenameDevices == null) settingUpdate("autoRenameDevices", sTRUE, sBOOL) - if(settings.addEchoNamePrefix == null) settingUpdate("addEchoNamePrefix", sTRUE, sBOOL) - if(settings.refreshCookieDays == null) settingUpdate("refreshCookieDays", 5, "number") - if(settings.logInfo == null) settingUpdate("logInfo", sTRUE, sBOOL) - if(settings.logWarn == null) settingUpdate("logWarn", sTRUE, sBOOL) - if(settings.logError == null) settingUpdate("logError", sTRUE, sBOOL) - if(settings.sendMissedPollMsg == null) { - settingUpdate('sendMissedPollMsg', sTRUE, sBOOL) - settingUpdate('misPollNotifyWaitVal', 2700) - settingUpdate('misPollNotifyMsgWaitVal', 3600) - } - if(settings.sendCookieInvalidMsg == null) settingUpdate("sendCookieInvalidMsg", sTRUE, sBOOL) - if(settings.sendAppUpdateMsg == null) settingUpdate("sendAppUpdateMsg", sTRUE, sBOOL) - - getEchoDevices(true) - - return dynamicPage(name: "mainPage", uninstall: false, install: true) { - appInfoSect() - if(!tokenOk) { - section() { paragraph title: "Uh OH!!!", "Oauth Has NOT BEEN ENABLED. Please Remove this app and try again after it after enabling OAUTH" }; return - } - if(newInstall) { - deviceDetectOpts() - } else { - section(sectHead("Alexa Guard:")) { - if((Boolean) state.alexaGuardSupported) { - String gState = (String) state.alexaGuardState ? ((String) state.alexaGuardState == sARM_AWAY ? "Away" : "Home") : sUnknown - String gStateIcon = gState == sUnknown ? "alarm_disarm" : (gState == "Away" ? "alarm_away" : "alarm_home") - String ad = spanSmBld("Alarm System Mode:", sCLR4D9) + spanSm(" (${gState})", (gState == sUnknown ? sCLRGRY : (gState == "Away" ? sCLRORG : sCLRGRN))) - ad += guardAutoConfigured() ? lineBr() + lineBr() + guardAutoDesc() : sBLANK - ad += inputFooter(sTTM) - href "alexaGuardPage", title: inTS1("Alexa Guard Control", gStateIcon), description: ad - } else { paragraph divSm("Alexa Guard is not enabled or supported by any of your Echo Devices", sCLRGRY) } - } - - section(sectHead("Alexa Devices:")) { - if(!newInstall) { - List remDevs = getRemovableDevs() - if(remDevs?.size()) { - String rd = remDevs.sort().collect { spanSm(" ${sBULLET} ${it}") }.join("
    ") - href "devCleanupPage", title: inTS1("Removable Devices:"), description: divSm(rd, sCLRRED) - } - String devDesc = getDeviceList().collect { "${spanSm((String)it.value?.name)}${it.value?.online ? spanSm(" (Online)", sCLRGRN2) : sBLANK}${it.value?.supported == false ? spanSm(" ${sFRNFACE}", sCLRRED2) : sBLANK}" }.sort().join("
    ").toString() - String dd = devDesc ? divSm(devDesc, sCLR4D9) + inputFooter(sTTM) : inputFooter(sTTC, sCLRGRY) - href "deviceManagePage", title: inTS1("Manage Devices:", sDEVICES), description: dd - } else { paragraph spanSmBld("Device Management will be displayed after install is complete", sCLRORG) } - } - - section(sectHead("Companion Apps:")) { - List zones = getZoneApps() - List acts = getActionApps() - href "zonesPage", title: inTS1("Manage Zones${zones?.size() ? " (${zones?.size()} ${zones?.size() > 1 ? "Zones" : "Zone"})" : sBLANK}", "es_groups"), description: getZoneDesc() - href "actionsPage", title: inTS1("Manage Actions${acts?.size() ? " (${acts?.size()} ${acts?.size() > 1 ? "Actions" : "Action"})" : sBLANK}", "es_actions"), description: getActionsDesc() - } - - section(sectHead("Alexa Login Service:")) { - String ls = getLoginStatusDesc() - href "authStatusPage", title: inTS1("Login Status | Cookie Service Management", sSETTINGS), description: (ls ? "${ls}${inputFooter(sTTM)}" : inputFooter(sTTC, sNULL, true)) - } - if(!(Boolean)state.shownDevSharePage) { showDevSharePrefs() } - } - section(sectHead("Notifications:")) { - String t0 = getAppNotifConfDesc() - href "notifPrefPage", title: inTS1("Manage Notifications", "notification2"), description: (t0 ? "${t0}${inputFooter(sTTM)}" : inputFooter(sTTC, sNULL, true)) - } - section(sectHead("Documentation & Settings:")) { - href url: documentationUrl(), style: sEXTNRL, required: false, title: inTS1("View Documentation", "documentation"), description: inputFooter(sTTP, sCLRGRY, true) - href "settingsPage", title: inTS1("Manage Logging, and Metrics", sSETTINGS), description: inputFooter(sTTM, sCLRGRY, true) - href "changeLogPage", title: inTS1("View Change Logs", "change_log"), description: inputFooter(sTTVD, sCLRGRY, true) - } - - if(!newInstall) { - section(sectHead("Experimental Functions")) { - href "deviceTestPage", title: inTS1("Device Testing", "testing"), description: spanSm("Test Speech, Announcements, and Sequences Builder", sCLRGRY) + inputFooter(sTTP, sCLRGRY) - href "alexaRoutinesTestPage", title: inTS1("Alexa Routine Testing", "routine"), description: spanSm("View Routine Info and Test", sCLRGRY) + inputFooter(sTTP, sCLRGRY) - href "musicSearchTestPage", title: inTS1("Music Search Tests", "music"), description: spanSm("Test music queries", sCLRGRY) + inputFooter(sTTP, sCLRGRY) - } - section(sectHead("Donations:")) { - href url: textDonateUrl(), style: sEXTNRL, required: false, title: inTS1("Donations", "donate"), description: inputFooter("Tap to open browser", sCLRGRY, true) - } - section(sectHead("Remove Everything:")) { - href "uninstallPage", title: inTS1("Uninstall this App", "uninstall"), description: inputFooter("Tap to Remove...", sCLRGRY, true) - } - section(sectHead("Feature Requests/Issue Reporting"), hideable: true, hidden: true) { - String issueUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=bug&template=bug_report.md&title=%28BUG%29+&projects=echo-speaks%2F6" - String featUrl = "https://github.com/tonesto7/echo-speaks/issues/new?assignees=tonesto7&labels=enhancement&template=feature_request.md&title=%5BFeature+Request%5D&projects=echo-speaks%2F6" - href url: featUrl, style: sEXTNRL, required: false, title: inTS1("New Feature Request", "www"), description: inputFooter("Tap to open browser", sCLRGRY, true) - href url: issueUrl, style: sEXTNRL, required: false, title: inTS1("Report an Issue", "www"), description: inputFooter("Tap to open browser", sCLRGRY, true) - } - } else { - showDevSharePrefs() - section(sectHead("Important Step:")) { - paragraph spanSmBldBr("Notice", sCLRRED) + spanSmBr("Please complete the install (hit done below) and then return to the Echo Speaks App to resume deployment and configuration of the server.", sCLRRED) - state.resumeConfig = true - } - } - appFooter() - state.ok2InstallActionFlag = false - clearDuplicationItems() - } -} - -def authStatusPage() { - return dynamicPage(name: "authStatusPage", install: false, nextPage: "mainPage", uninstall: false) { - if((Boolean)state.authValid) { - Integer lastChkSec = getLastTsValSecs("lastCookieRrshDt") - Boolean pastDayChkOk = (lastChkSec > 86400) - section(sectHead("Cookie Status:")) { - Boolean cookieValid = validateCookie(true) - Boolean chk1 = (state.cookieData && state.cookieData.localCookie) - Boolean chk2 = (state.cookieData && state.cookieData.csrf ) - Boolean chk3 = (lastChkSec < 432000) - // Boolean chk4 = (cookieValid == true) - // log.debug "cookieValid: ${cookieValid} | chk1: $chk1 | chk2: $chl2 | chk3: $chk3 | chk4: $chk4" - String stat = spanSmBld("Auth Status:") + spanSmBr(getOkOrNotSymHTML(chk1 && chk2 && cookieValid)) - stat += spanSm(" ${sBULLET} Cookie:") + spanSmBr(getOkOrNotSymHTML(chk1)) - stat += spanSm(" ${sBULLET} CSRF Value:") + spanSmBr(getOkOrNotSymHTML(chk2)) - stat += lineBr() - stat += spanSmBld("Cookie Refresh:") + spanSmBr(getOkOrNotSymHTML(chk3)) - stat += spanSm(" ${sBULLET} Last Refresh:") + spanSmBr(" (${seconds2Duration(getLastTsValSecs("lastCookieRrshDt"))})", (!chk3 ? sCLRRED : sNULL)) - stat += spanSm(" ${sBULLET} Next Refresh:") + spanSmBr(" (${nextCookieRefreshDur()})") - paragraph divSm(stat, sCLR4D9) - } - - section(sectHead("Cookie Tools: (Tap to show)", getAppImg("cookie")), hideable: true, hidden: true) { - String ckDesc = pastDayChkOk ? "This will Refresh your Amazon Cookie." : "It's too soon to refresh your cookie.\nMinimum wait is 24 hours!!" - input "refreshCookieDays", "number", title: inTS1("Auto refresh cookie every?\n(in days)", "day_calendar"), description: "in Days (1-5 max)", required: true, range: '1..5', defaultValue: 5, submitOnChange: true - if(refreshCookieDays != null && refreshCookieDays < 1) { settingUpdate("refreshCookieDays", 1, "number") } - if(refreshCookieDays != null && refreshCookieDays > 5) { settingUpdate("refreshCookieDays", 5, "number") } - - // Refreshes the cookie - input "refreshCookie", sBOOL, title: inTS1("Manually refresh cookie?", sRESET) + lineBr() + spanSm(ckDesc, pastDayChkOk ? sCLRGRY : sCLRRED), - required: false, defaultValue: false, submitOnChange: true - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm("After manually refreshing the cookie leave this page and come back before the date will change.", sCLR4D9) - - // Clears cookies for app and devices - input "resetCookies", sBOOL, title: inTS1("Remove All Cookie Data?", sRESET) + lineBr() + spanSm("Clear all stored cookie data from the app and devices.", sCLRGRY), - required: false, defaultValue: false, submitOnChange: true - - input "refreshDevCookies", sBOOL, title: inTS1("Resend Cookies to Devices?", sRESET) + lineBr() + spanSm("Force devices to synchronize their stored cookies.", sCLRGRY), - required: false, defaultValue: false, submitOnChange: true - - if((Boolean)settings.refreshCookie) { settingUpdate("refreshCookie", sFALSE, sBOOL); runIn(2, "runCookieRefresh") } - if(settings.resetCookies) { clearCookieData("resetCookieToggle", false) } - if((Boolean)settings.refreshDevCookies) { Boolean a = refreshDevCookies() } - } - } - - section(sectHead("Cookie Service Management")) { - String t0 = getServiceConfDesc() - href "servPrefPage", title: inTS1("Manage Cookie Login Service", sSETTINGS), description: (t0 ? divSm(t0, sCLR4D9) + inputFooter(sTTM, sCLR4D9) : inputFooter(sTTC, sCLRGRY, true)) - } - } -} - -def servPrefPage() { - Boolean newInstall = !(Boolean)state.isInstalled - Boolean resumeConf = (Boolean)state.resumeConfig - return dynamicPage(name: "servPrefPage", install: (newInstall || resumeConf), nextPage: (!(newInstall || resumeConf) ? "mainPage" : sBLANK), uninstall: !(Boolean)state.serviceConfigured) { - Boolean authValid = (Boolean)state.authValid - - if(settings.useHeroku == null) settingUpdate("useHeroku", sTRUE, sBOOL) - if(settings.amazonDomain == null) settingUpdate("amazonDomain", "amazon.com", sENUM) - if(settings.regionLocale == null) settingUpdate("regionLocale", "en-US", sENUM) - - if(!(Boolean)state.serviceConfigured) { - section(sectHead("Cookie Server Deployment Option:")) { - input "useHeroku", sBOOL, title: inTS1("Deploy server to Heroku?", sHEROKU), description: spanSm("Turn Off to allow local server deployment"), required: false, defaultValue: true, submitOnChange: true - if((Boolean)settings.useHeroku == false) { paragraph spanSmBldBr("NOTICE:", sCLRRED) + spanSmBld("I highly recommend Heroku deployments for most users. Local Server deployments are something that can be very difficult for me to support remotely.", sCLRRED) } - } - section() { paragraph spanSmBld("To proceed with the server setup.
    Tap on 'Begin Server Setup below", sCLR4D9) } - srvcPrefOpts(true) - section(sectHead("Deploy the Server:")) { - href url: getAppEndpointUrl("config"), style: sEXTNRL, title: inTS1("Begin Server Setup", "upload"), description: inactFoot(sTTP) - } - } else { - String myUrl = "${getServerHostURL()}/config" - String t0 = getServiceConfDesc() - if(!authValid) { - section(sectHead("Authentication:")) { - paragraph spanSmBld("You still need to Login to Amazon to complete the setup", sCLRRED) - href url: myUrl, style: sEXTNRL, title: inTS1("Amazon Login Page", sAMAZONORNG), description: t0 + inputFooter(sTTP, sCLR4D9) - } - } else { - Boolean oH = (Boolean)getServerItem("onHeroku") - section(sectHead("Server Management:")) { - if(oH && (String)state.herokuName) { paragraph spanSmBldBr("Heroku Name:", sCLR4D9) + spanSm(" ${sBULLET} ${(String)state.herokuName}", sCLR4D9) } - href url: myUrl, style: sEXTNRL, title: inTS1("Amazon Login Page", sAMAZONORNG), description: t0 + inputFooter(sTTP, sCLR4D9) - if(oH) href url: "https://dashboard.heroku.com/apps/${getRandAppName()}/settings", style: sEXTNRL, title: inTS1("Heroku App Settings", sHEROKU), description: inactFoot(sTTP) - if(oH) href url: "https://dashboard.heroku.com/apps/${getRandAppName()}/logs", style: sEXTNRL, title: inTS1("Heroku App Logs", sHEROKU), description: inactFoot(sTTP) - } - } - srvcPrefOpts() - } - section(sectHead("Reset Options (Tap to show):"), hideable: true, hidden: true) { - input "resetService", sBOOL, title: inTS1("Reset Service Data?", sRESET), description: "This will clear all references to the current server and allow you to redeploy a new instance.\nLeave the page and come back after toggling.", - required: false, defaultValue: false, submitOnChange: true - paragraph spanSm("This will clear all references to the current server and allow you to redeploy a new instance.\nLeave the page and come back after toggling.", sCLRGRY) - if(settings.resetService) { clearCloudConfig() } - } - state.resumeConfig = false - } -} - -def srvcPrefOpts(Boolean req=false) { - section(sectHead("${req ? "Required " : sBLANK}Amazon Locale Settings"), hideable: false, hidden: false) { - if(req) { - input "amazonDomain", sENUM, title: inTS1("Select your Amazon Domain?", sAMAZONORNG), description: sBLANK, required: true, defaultValue: "amazon.com", options: amazonDomainOpts(), submitOnChange: true - input "regionLocale", sENUM, title: inTS1("Select your Locale?", "www"), description: sBLANK, required: true, defaultValue: "en-US", options: localeOpts(), submitOnChange: true - } else { - String s = sBLANK - s += settings.amazonDomain ? spanSmBld("Amazon Domain: ") + spanSmBr("(${settings.amazonDomain})") : sBLANK - s += settings.regionLocale ? spanSmBld("Locale Region: ") + spanSm("(${settings.regionLocale})") : sBLANK - paragraph spanSm(lineBr() + s, sCLR4D9, getAppImg(sAMAZONORNG)) - } - } -} - -def deviceManagePage() { - return dynamicPage(name: "deviceManagePage", uninstall: false, install: false) { - Boolean newInstall = !(Boolean)state.isInstalled - section(sectHead("Alexa Devices:")) { - if(!newInstall) { - Map devs = getDeviceList() - Map skDevs = ((Map)state.skippedDevices)?.findAll { (it?.value?.reason != sIN_IGNORE) } - Map ignDevs = ((Map)state.skippedDevices)?.findAll { (it?.value?.reason == sIN_IGNORE) } - if(devs.size()) { - String devDesc = devs.collect { "${it.value?.name}${it.value?.online ? " (Online)" : sBLANK}${it.value?.supported == false ? " ${sFRNFACE}" : sBLANK}" }?.sort()?.join("
    ")?.toString() - String dd = spanSmBr(devDesc) + inputFooter(sTTVD) - href "deviceListPage", title: inTS1("Installed Devices:"), description: divSm(dd, sCLR4D9) - } else { paragraph spanSm("Discovered Devices:
    No Devices Available", sCLRRED) } - List remDevs = getRemovableDevs() - if(remDevs?.size()) { - String rd = spanSm(remDevs.sort().collect { " ${sBULLET} ${it}" }.join("
    ")) - href "devCleanupPage", title: inTS1("Removable Devices:"), description: divSm(rd, sCLRGRY) - } - if(skDevs?.size()) { - String uDesc = "Unsupported: (${skDevs?.size()})" - uDesc += ignDevs?.size() ? "\nUser Ignored: (${ignDevs?.size()})" : sBLANK - uDesc += (Boolean)settings.bypassDeviceBlocks ? "\nBlock Bypass: (Active)" : sBLANK - href "unrecogDevicesPage", title: inTS1("Unused Devices:"), description: spanSmBr("${uDesc}", sCLRORG) + inputFooter(sTTVD) - } - } - String devPrefDesc = devicePrefsDesc() - href "devicePrefsPage", title: inTS1("Device Detection Preferences", sDEVICES), description: (devPrefDesc ? devPrefDesc + inputFooter(sTTM) : inputFooter(sTTC, sCLRGRY)) - } - } -} - -def alexaGuardPage() { - return dynamicPage(name: "alexaGuardPage", uninstall: false, install: false) { - String gState = (String)state.alexaGuardState ? ((String)state.alexaGuardState == sARM_AWAY ? "Away" : "Home") : sUnknown - String gStateIcon = gState == sUnknown ? "alarm_disarm" : (gState == "Away" ? "alarm_away" : "alarm_home") - String gStateTitle = (gState == sUnknown || gState == "Home") ? "Set Guard to Armed?" : "Set Guard to Home?" - section(sectHead("Alexa Guard Control")) { - paragraph spanSm("Current Status:", sCLR4D9) + spanSm(" (${gState})", (gState == sUnknown ? sCLRGRY : (gState == "Away" ? sCLRORG : sCLRGRN))) - input "alexaGuardAwayToggle", sBOOL, title: inTS1(gStateTitle, gStateIcon), defaultValue: false, submitOnChange: true - } - if(settings.alexaGuardAwayToggle != state.alexaGuardAwayToggle) { - setGuardState(settings.alexaGuardAwayToggle == true ? sARM_AWAY : sARM_STAY) - } - state.alexaGuardAwayToggle = settings.alexaGuardAwayToggle - section(sectHead("Automate Guard Control")) { - String t0 = guardAutoDesc() - href "alexaGuardAutoPage", title: inTS1("Automate Guard Changes", "alarm_disarm"), description: (t0 ? t0 + inputFooter(sTTM) : spanSm("Automate the control of Alexa using modes, HSM, and more.", sCLRGRY) + inputFooter(sTTC, sCLRGRY, true)) - } - } -} - -def alexaGuardAutoPage() { - return dynamicPage(name: "alexaGuardAutoPage", uninstall: false, install: false) { - String asn = getAlarmSystemName(true) - List amo = getAlarmModes() - Boolean alarmReq = (settings.guardAwayAlarm || settings.guardHomeAlarm) - Boolean modeReq = (settings.guardAwayModes || settings.guardHomeModes) - // Boolean swReq = (settings.guardAwaySw || settings.guardHomeSw) - section(sectHead("Set Guard Using ${asn}")) { - input "guardHomeAlarm", sENUM, title: inTS1("Home in ${asn} modes.", "alarm_home"), description: inputFooter(sTTS, sCLRGRY, true), options: amo, required: alarmReq, multiple: true, submitOnChange: true - input "guardAwayAlarm", sENUM, title: inTS1("Away in ${asn} modes.", "alarm_away"), description: inputFooter(sTTS, sCLRGRY, true), options: amo, required: alarmReq, multiple: true, submitOnChange: true - } - - section(sectHead("Set Guard Using Modes")) { - input "guardHomeModes", "mode", title: inTS1("Home in these Modes?", "mode"), description: inputFooter(sTTS, sCLRGRY, true), required: modeReq, multiple: true, submitOnChange: true - input "guardAwayModes", "mode", title: inTS1("Away in these Modes?", "mode"), description: inputFooter(sTTS, sCLRGRY, true), required: modeReq, multiple: true, submitOnChange: true - } - - section(sectHead("Set Guard Using Switches:")) { - input "guardHomeSwitch", "capability.switch", title: inTS1("Home when any of these are On?", sSWITCH), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - input "guardAwaySwitch", "capability.switch", title: inTS1("Away when any of these are On?", sSWITCH), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - input "guardFollowSwitch", "capability.switch", title: inTS1("Follow Switch State (ON = Away | OFF = HOME)?", sSWITCH), description: inputFooter(sTTS, sCLRGRY, true), multiple: false, required: false, submitOnChange: true - } - - section(sectHead("Set Guard using Presence")) { - input "guardAwayPresence", "capability.presenceSensor", title: inTS1("Away when these devices are All away?", "presence"), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - } - if(guardAutoConfigured()) { - section(sectHead("Delay:")) { - input "guardAwayDelay", "number", title: inTS1("Delay before arming Away?\n(in seconds)", "delay_time"), description: "Enter number in seconds", required: false, defaultValue: 30, submitOnChange: true - } - } - section(sectHead("Restrict Guard Changes (Optional):")) { - input "guardRestrictOnSwitch", "capability.switch", title: inTS1("Only when these are On?", sSWITCH), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - input "guardRestrictOffSwitch", "capability.switch", title: inTS1("Only when these are Off?", sSWITCH), description: inputFooter(sTTS, sCLRGRY, true), multiple: true, required: false, submitOnChange: true - } - } -} - -Boolean guardAutoConfigured() { - return ((settings.guardAwayAlarm && settings.guardHomeAlarm) || (settings.guardAwayModes && settings.guardHomeModes) || settings.guardFollowSwitch || (settings.guardAwaySwitch && settings.guardHomeSwitch) || settings.guardAwayPresence) -} - -String guardAutoDesc() { - String str = sBLANK - if(guardAutoConfigured()) { - str += spanSmBldBr("Guard Triggers:") - str += (settings.guardAwayAlarm && settings.guardHomeAlarm) ? spanSmBr(" ${sBULLET} Using ${getAlarmSystemName()}") : sBLANK - str += settings.guardHomeModes ? spanSmBr(" ${sBULLET} Home Modes: (${settings.guardHomeModes?.size()})") : sBLANK - str += settings.guardAwayModes ? spanSmBr(" ${sBULLET} Away Modes: (${settings.guardAwayModes?.size()})") : sBLANK - str += settings.guardHomeSwitch ? spanSmBr(" ${sBULLET} Home Switches: (${settings.guardHomeSwitch?.size()})") : sBLANK - str += settings.guardAwaySwitch ? spanSmBr(" ${sBULLET} Away Switches: (${settings.guardAwaySwitch?.size()})") : sBLANK - str += settings.guardFollowSwitch ? spanSmBr(" ${sBULLET} Follow Switch: (${isSwitchOn(settings.guardFollowSwitch) ? "Armed Away" : "Disarmed"})") : sBLANK - str += settings.guardAwayPresence ? spanSmBr(" ${sBULLET} Presence Home: (${settings.guardAwayPresence?.size()})") : sBLANK - } - return str != sBLANK ? divSm(str, sCLR4D9) : sBLANK -} - -@SuppressWarnings('GroovyFallthrough') -def guardTriggerEvtHandler(evt) { - Long evtDelay = now() - (Long)evt.date.getTime() - logDebug("${evt.name.toUpperCase()} Event | Device: ${evt?.displayName} | Value: (${strCapitalize((String)evt?.value)}) with a delay of ${evtDelay}ms") - if(!guardRestrictOk()) { - logDebug("guardTriggerEvtHandler | Skipping Guard Changes because Restriction are Active.") - return - } - String newState = sNULL - String curState = (String)state.alexaGuardState ?: sNULL - switch((String)evt?.name) { - case "mode": - Boolean inAwayMode = isInMode((List)settings.guardAwayModes) - Boolean inHomeMode = isInMode((List)settings.guardHomeModes) - if(inAwayMode && inHomeMode) { logError("Guard Control Trigger can't act because same mode is in both Home and Away input"); return } - if(inAwayMode && !inHomeMode) { newState = sARM_AWAY } - if(!inAwayMode && inHomeMode) { newState = sARM_STAY } - break - case "switch": - Boolean isFollowSwitch = (settings.guardFollowSwitch && settings.guardFollowSwitch.getId().toInteger() == evt.getDeviceId().toInteger()) - if(isFollowSwitch) { - newState = isSwitchOn(settings.guardFollowSwitch) ? sARM_AWAY : sARM_STAY - } else { - Boolean inAwaySw = isSwitchOn((List)settings.guardAwaySwitch) - Boolean inHomeSw = isSwitchOn((List)settings.guardHomeSwitch) - if(inAwaySw && inHomeSw) { logError("Guard Control Trigger can't act because both switch groups are in both Home and Away input"); return } - if(inAwaySw && !inHomeSw) { newState = sARM_AWAY } - if(!inAwaySw && inHomeSw) { newState = sARM_STAY } - } - break - case "presence": - newState = isSomebodyHome((List)settings.guardAwayPresence) ? sARM_STAY : sARM_AWAY - break - case "alarmSystemStatus": - case "hsmStatus": - Boolean inAlarmHome = isInAlarmMode((List)settings.guardHomeAlarm) - Boolean inAlarmAway = isInAlarmMode((List)settings.guardAwayAlarm) - if(inAlarmAway && !inAlarmHome) { newState = sARM_AWAY } - if(!inAlarmAway && inAlarmHome) { newState = sARM_STAY } - break - } - if(guardArmPendingFLD && curState == sARM_STAY) { - unschedule("setGuardAway") - logInfo("New Guard State is Now STAY... Scheduled Arming Has Been Cancelled...") - guardArmPendingFLD = false - return - } - if(curState == newState) { logInfo("Skipping Guard Change... New Guard State is the same as current state: ($curState)") } - if(newState && curState != newState) { - if (newState == sARM_STAY) { - unschedule("setGuardAway") - logInfo("Setting Alexa Guard Mode to Home...") - setGuardHome() - } - if(newState == sARM_AWAY) { - if(settings.guardAwayDelay) { - guardArmPendingFLD = true - logWarn("Setting Alexa Guard Mode to Away in (${settings.guardAwayDelay} seconds)", true) - runIn(settings.guardAwayDelay, "setGuardAway") - - } - else { setGuardAway(); logWarn("Setting Alexa Guard Mode to Away...", true) } - } - } -} - -Boolean guardRestrictOk() { - Boolean onSwOk = settings.guardRestrictOnSwitch ? isSwitchOn((List)settings.guardRestrictOnSwitch) : true - Boolean offSwOk = settings.guardRestrictOffSwitch ? !isSwitchOn((List)settings.guardRestrictOffSwitch) : true - return (onSwOk && offSwOk) -} - -def actionsPage() { - return dynamicPage(name: "actionsPage", nextPage: "mainPage", uninstall: false, install: false) { - List actApps = getActionApps() - List activeActions = actApps?.findAll { it?.isPaused() != true } - List pausedActions = actApps?.findAll { it?.isPaused() == true } - if(actApps) { /*Nothing to add here yet*/ } - else { section(sBLANK) { paragraph pTS("You haven't created any Actions yet!\nTap Create New Action to get Started") } } - section() { - app(name: "actionApp", appName: actChildName(), namespace: "tonesto7", multiple: true, title: inTS1("Create New Action", "es_actions")) - if(actApps?.size()) { - input "actionDuplicateSelect", sENUM, title: inTS1("Duplicate Existing Action", "es_actions"), description: sTTS, options: actApps?.collectEntries { [(it?.id):it?.getLabel()] }, required: false, multiple: false, submitOnChange: true - if(settings.actionDuplicateSelect) { - href "actionDuplicationPage", title: inTS1(spanSm("Create Duplicate Action?", sCLRGRY), "question"), description: inactFoot(sTTP) - } - } - } - if(actApps?.size()) { - section (sectHead("Action History:")) { - href "viewActionHistory", title: inTS1("View Action History", "tasks"), description: spanSm("(Grouped by Action)", sCLR4D9) - } - - section (sectHead("Global Actions Management:"), hideable: true, hidden: true) { - if(activeActions?.size()) { - input "pauseChildActions", sBOOL, title: inTS1("Pause all actions?", "pause_orange"), description: sBLANK, defaultValue: false, submitOnChange: true - if((Boolean)settings.pauseChildActions) { settingUpdate("pauseChildActions", sFALSE, sBOOL); runIn(3, "executeActionPause") } - paragraph spanSm("When pausing all Actions you can either restore all or open each action and manually unpause it.", sCLRGRY) - } - if(pausedActions?.size()) { - input "unpauseChildActions", sBOOL, title: inTS1("Restore all actions?", "pause_orange"), defaultValue: false, submitOnChange: true - if(settings.unpauseChildActions) { settingUpdate("unpauseChildActions", sFALSE, sBOOL); runIn(3, "executeActionUnpause") } - } - input "reinitChildActions", sBOOL, title: inTS1("Force Refresh all actions?", sRESET), defaultValue: false, submitOnChange: true - if(settings.reinitChildActions) { settingUpdate("reinitChildActions", sFALSE, sBOOL); runIn(3, "executeActionUpdate") } - } - } - state.childInstallOkFlag = true - state.actionDuplicated = false - } -} - -def actionDuplicationPage() { - return dynamicPage(name: "actionDuplicationPage", nextPage: "actionsPage", uninstall: false, install: false) { - section() { - if((Boolean)state.actionDuplicated) { - paragraph spanSmBldBr("Action already duplicated...", sCLRRED) + spanSmBld("Return to action page and select it", sCLRRED) - } else { - def act = getActionApps()?.find { it?.id?.toString() == settings.actionDuplicateSelect?.toString() } - if(act) { - Map actData = act.getSettingsAndStateMap() ?: [:] - String actId = (String)act.getId().toString() - if(actData.settings && actData.state) { - String myId=app.getId() - if(!childDupMapFLD[myId]) childDupMapFLD[myId] = [:] - if(!childDupMapFLD[myId].actions) childDupMapFLD[myId].actions = [:] - childDupMapFLD[myId].actions[actId] = actData - log.debug "Dup Data: ${childDupMapFLD[myId].actions[actId]}" - } - actData.settings["duplicateFlag"] = [type: sBOOL, value: true] - // actData?.settings["actionPause"] = [type: sBOOL, value: true] - actData.settings["duplicateSrcId"] = [type: "text", value: actId] - def a=addChildApp("tonesto7", actChildName(), "${actData.label} (Dup)", [settings: actData.settings]) - paragraph spanSmBldBr("Action Duplicated...", sCLR4D9) + spanSmBld("
    Return to Action Page and look for the App with '(Dup)' in the name...", sCLR4D9) - state.actionDuplicated = true - } else { paragraph spanSmBld("Action not Found", sCLRRED) } - } - } - } -} - -def zoneDuplicationPage() { - return dynamicPage(name: "zoneDuplicationPage", nextPage: "zonesPage", uninstall: false, install: false) { - section() { - if((Boolean)state.zoneDuplicated) { - paragraph spanSmBldBr("Zone already duplicated...", sCLRRED) + spanSmBld("
    Return to zone page and select it", sCLRRED) - } else { - def zn = getZoneApps()?.find { it.id.toString() == settings.zoneDuplicateSelect?.toString() } - if(zn) { - Map znData = zn?.getSettingsAndStateMap() ?: null - String znId = (String)zn.getId().toString() - if(znData.settings && znData.state) { - String myId=app.getId() - if(!childDupMapFLD[myId]) childDupMapFLD[myId] = [:] - if(!childDupMapFLD[myId].zones) childDupMapFLD[myId].zones = [:] - childDupMapFLD[myId].zones[znId] = znData - log.debug "Dup Data: ${childDupMapFLD[myId].zones[znId]}" - } - // log.debug "Dup Data: ${actData}" - znData.settings["duplicateFlag"] = [type: sBOOL, value: true] - // znData?.settings["zonePause"] = [type: sBOOL, value: true] - znData?.settings["duplicateSrcId"] = [type: "text", value: znId] - def a=addChildApp("tonesto7", zoneChildName(), "${znData?.label} (Dup)", [settings: znData.settings]) - paragraph spanSmBldBr("Zone Duplicated...", sCLR4D9) + spanSmBld("
    Return to Zone Page and look for the App with '(Dup)' in the name...", sCLR4D9) - state.zoneDuplicated = true - } else { paragraph spanSmBld("Zone not Found", sCLRRED) } - } - } - } -} - -public Map getChildDupeData(String type, String childId) { - String myId=app.getId() - return (childDupMapFLD[myId] && childDupMapFLD[myId][type] && childDupMapFLD[myId][type][childId]) ? (Map)childDupMapFLD[myId][type][childId] : [:] -} - -public void clearDuplicationItems() { - state.actionDuplicated = false - state.zoneDuplicated = false - if(settings.actionDuplicateSelect) settingRemove("actionDuplicateSelect") - if(settings.zoneDuplicateSelect) settingRemove("zoneDuplicateSelect") - state.remove('actionDuplicated') - state.remove('zoneDuplicated') -} - -public void childAppDuplicationFinished(String type, String childId) { - log.trace "childAppDuplicationFinished($type, $childId)" -// Map data = [:] - String myId=app.getId() - if(childDupMapFLD[myId] && childDupMapFLD[myId][type] && childDupMapFLD[myId][type][childId]) { - childDupMapFLD[myId][type].remove(childId) - } - clearDuplicationItems() -} - -def zonesPage() { - return dynamicPage(name: "zonesPage", nextPage: "mainPage", uninstall: false, install: false) { - List zApps = getZoneApps() - List activeZones = zApps?.findAll { it?.isPaused() != true } - List pausedZones = zApps?.findAll { it?.isPaused() == true } - if(zApps) { /*Nothing to add here yet*/ } - else { - section(sBLANK) { paragraph spanSmBld("You haven't created any Zones yet!
    Tap Create New Zone to get Started", sCLRGRY) } - } - section() { - app(name: "zoneApp", appName: zoneChildName(), namespace: "tonesto7", multiple: true, title: inTS1("Create New Zone", "es_groups")) - if(zApps?.size()) { - input "zoneDuplicateSelect", sENUM, title: inTS1("Duplicate Existing Zone", "es_groups"), description: sTTS, options: zApps?.collectEntries { [(it?.id):it?.getLabel()] }, required: false, multiple: false, submitOnChange: true - if(settings.zoneDuplicateSelect) { - href "zoneDuplicationPage", title: inTS1(spanSm("Create Duplicate Zone?", sCLRGRY), "question"), description: inactFoot(sTTP) - } - } - } - if(zApps?.size()) { - section (sectHead("Zone History:")) { - href "viewZoneHistory", title: inTS1("View Zone History", "tasks"), description: spanSm("(Grouped by Zone)", sCLR4D9) - } - } - section (sectHead("Zone Management:"), hideable: true, hidden: true) { - if(activeZones?.size()) { - input "pauseChildZones", sBOOL, title: inTS1("Pause all Zones?", "pause_orange"), description: "When pausing all Zones you can either restore all or open each zones and manually unpause it.", defaultValue: false, submitOnChange: true - if(settings.pauseChildZones) { settingUpdate("pauseChildZones", sFALSE, sBOOL); runIn(3, "executeZonePause") } - paragraph spanSm("When pausing all zones you can either restore all or open each zone and manually unpause it.", sCLRGRY) - } - if(pausedZones?.size()) { - input "unpauseChildZone", sBOOL, title: inTS1("Restore all actions?", "pause_orange"), defaultValue: false, submitOnChange: true - if(settings.unpauseChildZone) { settingUpdate("unpauseChildZone", sFALSE, sBOOL); runIn(3, "executeZoneUnpause") } - } - input "reinitChildZones", sBOOL, title: inTS1("Clear Zones Status and force a full status refresh for all zones?", sRESET), defaultValue: false, submitOnChange: true - if(settings.reinitChildZones) { settingUpdate("reinitChildZones", sFALSE, sBOOL); runIn(3, "executeZoneUpdate") } - } - state.childInstallOkFlag = true - state.zoneDuplicated = false - updateZoneSubscriptions() - } -} - -def viewZoneHistory() { - return dynamicPage(name: "viewZoneHistory", title: div("

    Zone Event History

    ", sNULL, sNULL, true), uninstall: false, install: false) { - List zApps = getZoneApps() - zApps?.each { z-> - section(sectHead((String)z.getLabel())) { - List items = z.getZoneHistory(true) - items = items ?: [] - items.each { String v-> - paragraph spanSm(v) - } - } - } - } -} - -def viewActionHistory() { - return dynamicPage(name: "viewActionHistory", title: div("

    Action Event History

    ", sNULL, sNULL, true), uninstall: false, install: false) { - List actApps = getActionApps() - actApps?.each { a-> - section(sectHead((String)a.getLabel())) { - List items = (List)a.getActionHistory(true) - items.each { String v-> - paragraph spanSm(v) - } - } - } - } -} - -void executeActionPause() { - getActionApps()?.findAll { it?.isPaused() != true }?.each { it?.updatePauseState(true) } -} -void executeActionUnpause() { - getActionApps()?.findAll { it?.isPaused() == true }?.each { it?.updatePauseState(false) } -} -void executeActionUpdate() { - getActionApps()?.each { it?.updated() } -} -void executeZonePause() { - getZoneApps()?.findAll { it?.isPaused() != true }?.each { it?.updatePauseState(true) } -} -void executeZoneUnpause() { - getZoneApps()?.findAll { it?.isPaused() == true }?.each { it?.updatePauseState(false) } -} -void executeZoneUpdate() { - getZoneApps()?.each { it?.updated() } -} - -def devicePrefsPage() { -// Boolean newInstall = !(Boolean)state.isInstalled -// Boolean resumeConf = (Boolean)state.resumeConfig - return dynamicPage(name: "devicePrefsPage", uninstall: false, install: false) { - deviceDetectOpts() - section(sectHead("Detection Override:")) { - paragraph spanSmBldBr("Device not detected?", sCLRORG) + spanSm("Enabling this will allow you to override the developer block for unrecognized or uncontrollable devices.
    This is useful for testing if a device supports certain features.", sCLRORG) - input "bypassDeviceBlocks", sBOOL, title: inTS1("Override Blocks and Create Ignored Devices?"), required: false, defaultValue: false, submitOnChange: true - paragraph spanSmBld("WARNING:", sCLRRED) + spanSm(" This will create devices for all remaining ignored devices", sCLRRED) - } - devCleanupSect() -// if(!newInstall && !resumeConf) { state.refreshDeviceData = true } - } -} - -private deviceDetectOpts() { -// Boolean newInstall = !(Boolean) state.isInstalled -// Boolean resumeConf = (Boolean) state.resumeConfig - section(sectHead("Device Detection Preferences")) { - input "autoCreateDevices", sBOOL, title: inTS1("Auto Create New Devices?", sDEVICES), description: sBLANK, required: false, defaultValue: true, submitOnChange: true - input "createTablets", sBOOL, title: inTS1("Create Devices for Tablets?", "amazon_tablet"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - input "createWHA", sBOOL, title: inTS1("Create Multiroom Devices?", "echo_wha"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - input "createOtherDevices", sBOOL, title: inTS1("Create Other Alexa Enabled Devices?", sDEVICES), description: "FireTV (Cube, Stick), Sonos, etc.", required: false, defaultValue: false, submitOnChange: true - input "autoRenameDevices", sBOOL, title: inTS1("Rename Devices to Match Amazon Echo Name?", "name_tag"), description: sBLANK, required: false, defaultValue: true, submitOnChange: true - input "addEchoNamePrefix", sBOOL, title: inTS1("Add 'Echo - ' Prefix to label?", "name_tag"), description: sBLANK, required: false, defaultValue: true, submitOnChange: true - Map devs = getAllDevices(true) - if(devs?.size()) { - input "echoDeviceFilter", sENUM, title: inTS1("Don't Use these Devices", "exclude"), description: sTTS, options: (devs ? devs?.sort{it?.value} : []), multiple: true, required: false, submitOnChange: true - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm("To prevent unwanted devices from reappearing after removal make sure to add the device to the Don't Use these Devices input above before removing.", sCLR4D9) - } - } -} - -private devCleanupPage() { - return dynamicPage(name: "devCleanupPage", uninstall: false, install: false) { - devCleanupSect() - } -} - -private devCleanupSect() { - if(state.isInstalled && !(Boolean)state.resumeConfig) { - section(sectHead("Device Cleanup Options:")) { - Map devs = getAllDevices(true) - if(devs?.size()) { - input "echoDeviceFilter", sENUM, title: inTS1("Don't Use these Devices", "exclude"), description: sTTS, options: (devs ? devs?.sort{it?.value} : []), multiple: true, required: false, submitOnChange: true - } - List remDevs = getRemovableDevs() - if(remDevs.size()) { paragraph spanSmBldBr("Removable Devices:", sCLRRED) + spanSm(remDevs.sort().collect { " ${sBULLET} ${it}" } ?.join("
    "), sCLRGRY), required: true } - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSm("Remember to add device to filter above to prevent recreation.
    Also the cleanup process will fail if the devices are used in external apps/automations", sCLR4D9) - input "cleanUpDevices", sBOOL, title: inTS1("Cleanup Unused Devices?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.cleanUpDevices) { removeDevices() } - } - } -} - -private List getRemovableDevs() { - Map eDevs = getEchoDeviceMap() - eDevs = eDevs ?: [:] - List cDevs = app?.getChildDevices() - List remDevs = [] - String myId=app.getId() - String nmS = 'echoSpeaks_websocket' - nmS = myId+'|'+nmS - - cDevs?.each { cDev-> - if(cDev?.deviceNetworkId?.toString() == nmS) { return } - List dni = cDev?.deviceNetworkId?.tokenize("|") - if(eDevs.size() && dni[2] && !eDevs.containsKey(dni[2])) { remDevs.push(cDev?.getLabel() as String) } - } - return remDevs ?: [] -} - -private String devicePrefsDesc() { - String str = sBLANK - str += "Auto Create (${!(Boolean)settings.autoCreateDevices ? "Disabled" : "Enabled"})" - if((Boolean)settings.autoCreateDevices) { - str += (Boolean) settings.createTablets ? "
    ${sBULLET} Tablets" : sBLANK - str += (Boolean) settings.createWHA ? "
    ${sBULLET} WHA" : sBLANK - str += (Boolean) settings.createOtherDevices ? "
    ${sBULLET} Other Devices" : sBLANK - } - str += (Boolean) settings.autoRenameDevices ? "
    ${sBULLET} Auto Rename" : sBLANK - str += (Boolean) settings.bypassDeviceBlocks ? "
    ${sBULLET} Block Bypass: (Active)" : sBLANK - str = paraTS(null, str, null, [:], [s: 'small', c: sCLR4D9]) - return str != sBLANK ? str : sNULL -} - -def settingsPage() { - return dynamicPage(name: "settingsPage", uninstall: false, install: false) { - section(sectHead("Logging:")) { - input "logInfo", sBOOL, title: inTS1("Show Info Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logWarn", sBOOL, title: inTS1("Show Warning Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logError", sBOOL, title: inTS1("Show Error Logs?", sDEBUG), required: false, defaultValue: true, submitOnChange: true - input "logDebug", sBOOL, title: inTS1("Show Debug Logs?", sDEBUG), description: "Auto disables after 6 hours", required: false, defaultValue: false, submitOnChange: true - input "logTrace", sBOOL, title: inTS1("Show Detailed Logs?", sDEBUG), description: "Only enabled when asked to.\n(Auto disables after 6 hours)", required: false, defaultValue: false, submitOnChange: true - } - - // if(devModeFLD) { - section(sectHead("Child Logging:")) { - input "childAppLogDebug", sBOOL, title: inTS1("Enable Debug Logs for All Child Apps?", sDEBUG), description: "Auto disables after 6 hours", required: false, defaultValue: false, submitOnChange: true - input "childAppLogTrace", sBOOL, title: inTS1("Enable Trace Logs for All Child Apps?", sDEBUG), description: "Only enabled when asked to.\n(Auto disables after 6 hours)", required: false, defaultValue: false, submitOnChange: true - input "childDeviceLogDebug", sBOOL, title: inTS1("Enable Debug Logs for All Child Devices?", sDEBUG), description: "Auto disables after 6 hours", required: false, defaultValue: false, submitOnChange: true - input "childDeviceLogTrace", sBOOL, title: inTS1("Enable Trace Logs for All Child Devices?", sDEBUG), description: "Only enabled when asked to.\n(Auto disables after 6 hours)", required: false, defaultValue: false, submitOnChange: true - input "disableAllChildAdvLogs", sBOOL, title: inTS1("Disable All Advanced Logging on Child Apps/Devices?", sDEBUG), description: "Only enabled when asked to.\n(Auto disables after 6 hours)", required: false, defaultValue: false, submitOnChange: true - /*if((Boolean)settings.childAppLogDebug || (Boolean)settings.childAppLogTrace || - (Boolean)settings.childDeviceLogDebug || (Boolean)settings.childDeviceLogTrace || - (Boolean)settings.disableAllChildAdvLogs) { activateChildAdvLogs() } */ - activateChildAdvLogs() - } - // } - - if(advLogsActive()) { logsEnabled() } - section(sectHead("Text Transforms:")) { - input "disableTextTransform", sBOOL, title: inTS1("Disable Text Transform?", "question"), description: "This will disable attempts to convert items in speech text like temp units and directions like `WSW` to west southwest", required: false, defaultValue: false, submitOnChange: true - } - showDevSharePrefs() - section(sectHead("Diagnostic Data:")) { - paragraph pTS("If you are having trouble send a private message to the developer with a link to this page that is shown below.", sNULL, false, sCLRGRY) - input "diagShareSensitveData", sBOOL, title: inTS1("Share Cookie Data?", "question"), required: false, defaultValue: false, submitOnChange: true - href url: getAppEndpointUrl("diagData"), style: sEXTNRL, title: inTS1("Diagnostic Data"), description: spanSm("Tap to view", sCLRGRY) - } - } -} - -def deviceListPage() { - return dynamicPage(name: "deviceListPage", install: false) { - section(sectHead("Discovered Devices:")) { - paragraph spanSmBldBr("NOTICE: The test buttons on this page sometimes do not work. Please use the testing section on the main page if you have issues!", sCLRRED2) - getEchoDeviceMap()?.sort { it?.value?.name }?.each { String k, Map v-> - String str = "Status: (${v.online ? "Online" : "Offline"})" - str += "
    Style: ${v.style?.n}" - str += "
    Family: ${v.family}" - str += "
    Type: ${v.type}" - str += "
    Volume Control: (${v.volumeSupport?.toString()?.capitalize()})" - str += "
    Announcements: (${v.announceSupport?.toString()?.capitalize()})" - str += "
    Text-to-Speech: (${v.ttsSupport?.toString()?.capitalize()})" - str += "
    Music Player: (${v.mediaPlayer?.toString()?.capitalize()})" - str += v.supported != true ? "
    Unsupported Device: (True)" : sBLANK - str += (v.mediaPlayer == true && v.musicProviders) ? "
    Music Providers: [${v.musicProviders}]" : sBLANK - paragraph paraTS((String)v.name, str, (String)v.style?.i, [c: 'black', b: true, u: true], [s: 'small', c: (v.online ? sCLR4D9 : sCLRGRY)]) - input "deviceSpeechTest::${k}", "button", title: spanSmBld("Test Speech", sCLRGRY), width: 4 - input "deviceAnnouncementTest::${k}", "button", title: spanSmBld("Test Announcement", sCLRGRY), width: 4 - } - } - } -} - -def unrecogDevicesPage() { - return dynamicPage(name: "unrecogDevicesPage", install: false) { - Map skDevMap = (Map)state.skippedDevices ?: [:] - Map ignDevs = skDevMap?.findAll { (it?.value?.reason == sIN_IGNORE) } - Map unDevs = skDevMap?.findAll { (it?.value?.reason != sIN_IGNORE) } - section(sectHead("Unrecognized/Unsupported Devices:")) { - if(unDevs?.size()) { - unDevs.sort { it?.value?.name }?.each { String k, Map v-> - // log.debug "v: $v" - String str = "Status: (${(Boolean)v.online ? "Online" : "Offline"})" - str += "
    Style: ${(String) v.desc}" - str += "
    Family: ${(String)v.family}" - str += "
    Type: ${(String)v.type}" - str += "
    Volume Control: (${v?.volume?.toString()?.capitalize()})" - str += "
    Text-to-Speech: (${v?.tts?.toString()?.capitalize()})" - str += "
    Music Player: (${v?.mediaPlayer?.toString()?.capitalize()})" - str += "
    Reason Ignored: (${v?.reason})" - paragraph paraTS((String)v.name, str, (String)v.image, [c: 'black', b: true, u: true], [s: 'small', c: (v.online ? sCLR4D9 : sCLRGRY)]) - } - input "bypassDeviceBlocks", sBOOL, title: inTS1("Override Blocks and Create Ignored Devices?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - } else { - paragraph pTS("No Uncognized Devices", sNULL, true) - } - } - if(ignDevs?.size()) { - section(sectHead("User Ignored Devices:")) { - ignDevs.sort { it?.value?.name }?.each { k,v-> - String str = spanSmBr("Status: (${v.online ? "Online" : "Offline"})") - str += spanSmBr("Style: ${(String)v.desc}") - str += spanSmBr("Family: ${(String)v.family}") - str += spanSmBr("Type: ${(String)v.type}") - str += spanSmBr("Volume Control: (${v?.volume?.toString()?.capitalize()})") - str += spanSmBr("Text-to-Speech: (${v?.tts?.toString()?.capitalize()})") - str += spanSmBr("Music Player: (${v?.mediaPlayer?.toString()?.capitalize()})") - str += spanSmBr("Reason Ignored: (${v?.reason})") - href "unrecogDevicesPage", title: inTS1((String)v.name, (String)v.image), description: divSm(str, sCLRGRY) - } - } - } - } -} - -def showDevSharePrefs() { - section(sectHead("Share Data with Developer:")) { - paragraph spanSmBldBr("What is this used for?", sCLRGRY) + spanSmBldBr("These options send non-user identifiable information and error data to diagnose catch trending issues.", sCLRGRY) - input ("optOutMetrics", sBOOL, title: inTS1("Do Not Share Data?", "analytics"), required: false, defaultValue: false, submitOnChange: true) - href url: getAppEndpointUrl("renderMetricData"), style: sEXTNRL, title: inTS1("View the Data shared with Developer", "view"), description: inputFooter("Tap to view Data", sCLRGRY, true) - } - if(!(Boolean)settings.optOutMetrics && (Boolean)state.isInstalled && (Boolean)state.serviceConfigured && !(Boolean)state.resumeConfig) { - section() { input "sendMetricsNow", sBOOL, title: inTS1("Send Metrics Now?", sRESET), description: sBLANK, required: false, defaultValue: false, submitOnChange: true } - if(sendMetricsNow) { sendInstallData() } - } - state.shownDevSharePage = true -} - -Map getDeviceList(Boolean isInputEnum=false, List filters=[]) { - Map devMap = [:] - Map availDevs = getEchoDeviceMap() //state.echoDeviceMap - availDevs = availDevs ?: [:] - availDevs.each { String key, Map val-> - if(filters.size()) { - if(filters.contains('tts') && val.ttsSupport != true) { return } - if(filters.contains('announce') && val.ttsSupport != true && val.announceSupport != true) { return } - } - devMap[key] = val - } - return isInputEnum ? (devMap.size() ? devMap?.collectEntries { [(it?.key):it?.value?.name] } : devMap) : devMap -} - -Map getAllDevices(Boolean isInputEnum=false) { - Map devMap = [:] - Map availDevs = (Map)state.allEchoDevices ?: [:] - availDevs?.each { String key, Map val-> devMap[key] = val } - return isInputEnum ? (devMap.size() ? devMap?.collectEntries { [(it?.key):it?.value?.name] } : devMap) : devMap -} - -def notifPrefPage() { - dynamicPage(name: "notifPrefPage", install: false) { - section(sBLANK) { - paragraph spanSmBldBr("Notice:", sCLR4D9) + spanSmBld("The settings configured here are used by both the App and the Devices.", sCLR4D9, getAppImg("info")) - } - section (sectHead("Notification Devices:")) { - input "notif_devs", "capability.notification", title: inTS1("Send to Notification devices?", "notification"), required: false, multiple: true, submitOnChange: true - } - -//TODO REMOVE - if(settings.smsNumbers) settingRemove('smsNumbers') - if(settings.usePush) settingRemove('usePush') - state.remove('pushoverManager') - settingRemove('pushoverEnabled') - settingRemove('pushoverDevices') - settingRemove('pushoverPriority') - settingRemove('pushoverSound') - - if((List)settings.notif_devs) { - if(!state.pushTested) { - if(sendMsg("Info", "Notification Test Successful. Notifications Enabled for ${app?.label}", true)) { - state.pushTested = true - } - } - section(sectHead("Notification Restrictions:")) { - String t1 = getNotifSchedDesc() - href "setNotificationTimePage", title: inTS1("Quiet Restrictions", "restriction"), description: (t1 ? t1 + inputFooter(sTTM) : inputFooter(sTTC, sCLRGRY)) - } - section(sectHead("Missed Poll Alerts:")) { - input (name: "sendMissedPollMsg", type: sBOOL, title: inTS1("Send Missed Checkin Alerts?", "late"), defaultValue: true, submitOnChange: true) - if((Boolean)settings.sendMissedPollMsg) { - input (name: "misPollNotifyWaitVal", type: sENUM, title: inTS1("Time Past the Missed Checkin?", "delay_time"), description: spanSm("Default: 45 Minutes"), required: false, defaultValue: 2700, options: notifValEnum(), submitOnChange: true) - input (name: "misPollNotifyMsgWaitVal", type: sENUM, title: inTS1("Send Reminder After?", "reminder"), description: spanSm("Default: 1 Hour"), required: false, defaultValue: 3600, options: notifValEnum(), submitOnChange: true) - } - } - section(sectHead("Cookie Alerts:")) { - input (name: "sendCookieRefreshMsg", type: sBOOL, title: inTS1("Send on Refreshed Cookie?", "cookie"), defaultValue: false, submitOnChange: true) - input (name: "sendCookieInvalidMsg", type: sBOOL, title: inTS1("Send on Invalid Cookie?", "cookie"), defaultValue: true, submitOnChange: true) - } - section(sectHead("Code Update Alerts:")) { - input "sendAppUpdateMsg", sBOOL, title: inTS1("Send for Updates...", "update"), defaultValue: true, submitOnChange: true - if((Boolean)settings.sendAppUpdateMsg) { - input (name: "updNotifyWaitVal", type: sENUM, title: inTS1("Send Reminders After?", "reminder"), description: spanSm("Default: 12 Hours"), required: false, defaultValue: 43200, options: notifValEnum(), submitOnChange: true) - } - } - } else { state.pushTested = false } - } -} - -def setNotificationTimePage() { - settingRemove("qStartInput") - settingRemove("qStopInput") - settingRemove("quietDays") - settingRemove("quietModes") - - dynamicPage(name: "setNotificationTimePage", title: "Restrict Notifications\nDuring these Times or to these Days or Modes", uninstall: false) { - String a = getNotifSchedDesc() - if(a) { - section() { - paragraph spanSmBldBr("Restrictions Status:", sCLR4D9) + spanSm(a, sCLR4D9) - paragraph spanSmBldBr("NOTICE: All selected restrictions must be ${strUnder("INACTIVE")} for notifications to be sent.", sCLRORG) - paragraph htmlLine() - } - } - String pre = "notif" - Boolean timeReq = (settings["${pre}_time_start"] || settings["${pre}_time_stop"]) - section(sectHead("Quiet Start Time:")) { - input "${pre}_time_start_type", sENUM, title: inTS1("Starting at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(settings."${pre}_time_start_type" == sTIME) { - input "${pre}_time_start", sTIME, title: inTS1("Start time", "start_time"), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_start_type" in lSUNRISESET) { - input "${pre}_time_start_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - section(sectHead("Quiet Stop Time:")) { - input "${pre}_time_stop_type", sENUM, title: inTS1("Stopping at...", "start_time"), options: [(sTIME):"Time of Day", "sunrise":"Sunrise", "sunset":"Sunset"], required: false , submitOnChange: true - if(settings."${pre}_time_stop_type" == sTIME) { - input "${pre}_time_stop", sTIME, title: inTS1("Stop time", "start_time"), required: timeReq, submitOnChange: true - } else if(settings."${pre}_time_stop_type" in lSUNRISESET) { - input "${pre}_time_stop_offset", sNUMBER, range: "*..*", title: inTS1("Offset in minutes (+/-)", "start_time"), required: false, submitOnChange: true - } - } - section(sectHead("Allowed Days:")) { - input "${pre}_days", sENUM, title: inTS1("Only on these week days", "day_calendar"), multiple: true, required: false, options: weekDaysEnum() - } - section(sectHead("Allowed Modes:")) { - input "${pre}_modes", sMODE, title: inTS1("Only in these Modes", sMODE), multiple: true, submitOnChange: true, required: false - } - } - -} - -def uninstallPage() { - dynamicPage(name: "uninstallPage", title: "Uninstall", uninstall: true) { - section(sBLANK) { paragraph spanSmBldBr("This will remove the app, all devices, all actions, all zones.
    ", sCLRRED) + spanSmBld("Please make sure that any devices created by this app are removed from any routines/rules/smartapps before tapping Remove.", sCLRRED) } - } -} - -static String bulletItem(String inStr, String strVal) { return "${inStr == sBLANK ? sBLANK : "\n"} "+ sBULLET + sSPACE + strVal } -static String dashItem(String inStr, String strVal, Boolean newLine=false) { return "${(inStr == sBLANK && !newLine) ? sBLANK : "\n"} - " + strVal } - -def deviceTestPage() { - return dynamicPage(name: "deviceTestPage", uninstall: false, install: false) { - section(sBLANK) { - href "speechPage", title: inTS1("Speech Test", "broadcast"), description: inputFooter(sTTC, sCLRGRY, true) - href "announcePage", title: inTS1("Announcement Test","announcement"), description: inputFooter(sTTC, sCLRGRY, true) - href "sequencePage", title: inTS1("Sequence Creator Test", "sequence"), description: inputFooter(sTTC, sCLRGRY, true) - } - } -} - -def speechPage() { - return dynamicPage(name: "speechPage", uninstall: false, install: false) { - // if(state.mainMenu) return mainPage() - section(sBLANK) { - paragraph pTS("This feature has been known to have issues and may not work because it's not supported by all Alexa devices. To test each device individually I suggest using the device interface and press Test Speech or Test Announcement") - Map devs = getDeviceList(true, ['tts']) - input "test_speechDevices", sENUM, title: inTS1("Select Devices to Test the Speech"), description: inputFooter(sTTS, sCLRGRY, true), options: (devs ? devs?.sort{it?.value} : []), multiple: true, required: false, submitOnChange: true - if(((List) settings.test_speechDevices)?.size() >= 3) { - paragraph spanSmBldBr("NOTICE:", sCLRRED) + spanSm("Amazon often rate limits when 3 or more device commands are sent at a time.
    There may be a delay in the other devices but they should play the test after a few seconds", sCLRRED) - } - input "test_speechVolume", "number", title: inTS1("Speak at this volume (0% - 100%)"), description: "Enter number", range: "0..100", defaultValue: null, required: false, submitOnChange: true - input "test_speechRestVolume", "number", title: inTS1("Restore to this volume after (0% - 100%)"), description: "Enter number", range: "0..100", defaultValue: null, required: false, submitOnChange: true - input "test_speechMessage", "text", title: inTS1("Message to Speak"), defaultValue: "This is a speech test for your Echo speaks device!!!", required: true, submitOnChange: true - } - if((List)settings.test_speechDevices) { - section() { - input "test_speechRun", sBOOL, title: inTS1("Perform the Speech Test?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.test_speechRun) { executeSpeechTest() } - } - } - // returnHomeBtn() - } -} - -def alexaRoutinesTestPage() { - return dynamicPage(name: "alexaRoutinesTestPage", uninstall: false, install: false) { - Map rts = (Map)getAlexaRoutines() - section("Available Routines") { - if(rts.size()) { - rts.each { String rk, String rv-> - String str = sBLANK - str += spanBldBr(rv) - str += spanSmBld("Routine ID: ") + spanSmBr(rk) - paragraph divSm(str, sCLR4D9) - input "executeRoutine::${rk}", "button", title: spanSmBld("Run Routine: ", sCLRGRY) + spanSm("(${rv})", sCLRGRY), width: 4 - paragraph htmlLine() - } - } else { - paragraph divSm("No Routine Data Found...", sCLRGRY) - } - } - } -} - -def returnHomeBtn() { - section { - paragraph htmlLine() - input "btnMainMenu", "button", title: "Home Page", width: 3 - } -} - -void appButtonHandler(String btn) { - logDebug("appButton Event Received: $btn") - switch (btn) { - case "btnMainMenu": - state.mainMenu = true - break - - default: - if(btn.contains("::")) { - List items = btn.tokenize("::") - if(items && items.size() > 1 && items[1]) { - String k = (String)items[0] - String v = (String)items[1] - switch(k) { - case "executeRoutine": - // log.debug "routine: ${rt[1]}" - executeRoutineTest(v) - break - case "deviceSpeechTest": - def childDev = getChildDeviceBySerial(v) - if(childDev && childDev?.hasCommand('speechTest')) { - logInfo("Sending SpeechTest Command to (${childDev.displayName})") - childDev?.speechTest() - } else { - logError("Speech Test device with Serial# (${v} was not located!!! or does not support speechTest()") - } - break - case "deviceAnnouncementTest": - def childDev = getChildDeviceBySerial(v) - if(childDev && childDev?.hasCommand('sendTestAnnouncement')) { - logInfo("Sending AnnouncementTest Command to (${childDev.displayName})") - childDev?.sendTestAnnouncement() - } else { - logError("Announcement Test device with Serial# (${v} was not located!!! or does not support sendTestAnnouncement()") - } - break - } - } - } - break - } -} - -void executeRoutineTest(String rtId) { - if(!(rtId && executeRoutineById(rtId)) ) { - logError("Valid Routine ID not received for Routine Test!!!") - } -} - -def announcePage() { - return dynamicPage(name: "announcePage", uninstall: false, install: false) { - section(sBLANK) { - paragraph pTS("This feature has known to have issues and may not work because it's not supported by all Alexa devices. To test each device individually I suggest using the device interface and press Test Speech or Test Announcement") - if(!settings.test_announceDevices) { - input "test_announceAllDevices", sBOOL, title: inTS1("Test Announcement using All Supported Devices"), defaultValue: false, required: false, submitOnChange: true - } - if(!(Boolean)settings.test_announceAllDevices) { - List devs = getChildDevicesByCap("announce") ?: [] - input "test_announceDevices", sENUM, title: inTS1("Select Devices to Test the Announcement"), description: sTTS, options: (devs?.collectEntries { [(it?.getId()): it?.getLabel() as String] }), multiple: true, required: false, submitOnChange: true - } - if((Boolean)settings.test_announceAllDevices || settings.test_announceDevices) { - input "test_announceVolume", "number", title: inTS1("Announce at this volume"), description: "Enter number", range: "0..100", defaultValue: null, required: false, submitOnChange: true - input "test_announceRestVolume", "number", title: inTS1("Restore to this volume after"), description: "Enter number", range: "0..100", defaultValue: null, required: false, submitOnChange: true - input "test_announceMessage", "text", title: inTS1("Message to announce"), defaultValue: "This is a test of the Echo speaks announcement system!!!", required: true, submitOnChange: true - } - } - if(settings.test_announceDevices || (Boolean)settings.test_announceAllDevices) { - section() { - input "test_announceRun", sBOOL, title: inTS1("Perform the Announcement?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.test_announceRun) { executeAnnouncement() } - } - } - } -} - -@Field final Map seqItemsAvailFLD = [ - other: [ - "weather":sNULL, "traffic":sNULL, "flashbriefing":sNULL, "goodnews":sNULL, "goodmorning":sNULL, "goodnight":sNULL, "cleanup":sNULL, - "singasong":sNULL, "tellstory":sNULL, "funfact":sNULL, "joke":sNULL, "playsearch":sNULL, "calendartoday":sNULL, - "calendartomorrow":sNULL, "calendarnext":sNULL, "stop":sNULL, "stopalldevices":sNULL, - "wait": "value (seconds)", "volume": "value (0-100)", "speak": "message", "announcement": "message", - "announcementall": "message", "pushnotification": "message", "email": sNULL - ], - // dnd: [ - // "dnd_duration": "2H30M", "dnd_time": "00:30", "dnd_all_duration": "2H30M", "dnd_all_time": "00:30", - // "dnd_duration":"2H30M", "dnd_time":"00:30" - // ], - speech: [ - "cannedtts_random": ["goodbye", "confirmations", "goodmorning", "compliments", "birthday", "goodnight", "iamhome"] - ], - music: [ - "amazonmusic": "search term", "applemusic": "search term", "iheartradio": "search term", "pandora": "search term", - "spotify": "search term", "tunein": "search term", "cloudplayer": "search term" - ] -] - -public Map seqItemsAvail() { - return seqItemsAvailFLD -} - -def sequencePage() { - return dynamicPage(name: "sequencePage", uninstall: false, install: false) { - section(sectHead("Command Legend:"), hideable: true, hidden: true) { - String str1 = "Sequence Options:" - ((Map)seqItemsAvailFLD.other).sort()?.each { String k, String v-> - str1 += "${bulletItem(str1, "${k}${v != sNULL ? "::${v}" : sBLANK}")}" - } - // String str4 = "DoNotDisturb Options:" - // seqItemsAvailFLD.dnd?.sort()?.each { String k, String v-> - // str4 += "${bulletItem(str4, "${k}${v != sNULL ? "::${v}" : sBLANK}")}" - // } - String str2 = "Music Options:" - ((Map)seqItemsAvailFLD.music).sort()?.each { String k, String v-> - str2 += "${bulletItem(str2, "${k}${v != sNULL ? "::${v}" : sBLANK}")}" - } - String str3 = "Canned TTS Options:" - ((Map)seqItemsAvailFLD.speech).sort()?.each { String k, v-> - String newV - if(v instanceof List) { newV = sBLANK; v?.sort()?.each { newV += " ${dashItem(newV, "${it}", true)}" } } - else newV=v - str3 += "${bulletItem(str3, "${k}${newV != sNULL ? "::${newV}" : sBLANK}")}" - } - paragraph spanSm(str1, sCLR4D9) - // paragraph spanSm(str4, sCLR4D9) - paragraph spanSm(str2, sCLR4D9) - paragraph spanSm(str3, sCLR4D9) - paragraph spanSmBldBr("Enter the command in a format exactly like this:") + spanSmBr("volume::40,, speak::this is so silly,, wait::60,, weather,, cannedtts_random::goodbye,, traffic,, amazonmusic::green day,, volume::30") + spanSm("
    Each command needs to be separated by a double comma `,,` and the separator between the command and value must be command::value.") - } - section(sectHead("Sequence Test Config:")) { - input "test_sequenceDevice", "device.EchoSpeaksDevice", title: inTS1("Select Devices to Test Sequence Command"), description: inputFooter(sTTS, sCLRGRY, true), multiple: false, required: ((String)settings.test_sequenceString != sNULL), submitOnChange: true - input "test_sequenceString", "text", title: inTS1("Sequence String to Use"), required: ((String)settings?.test_sequenceDevice != sNULL), submitOnChange: true - } - if(settings?.test_sequenceDevice && settings?.test_sequenceString) { - section() { - input "test_sequenceRun", sBOOL, title: inTS1("Perform the Sequence?"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.test_sequenceRun) { executeSequence() } - } - } - } -} - -void executeSpeechTest() { - settingUpdate("test_speechRun", sFALSE, sBOOL) - String testMsg = (String)settings.test_speechMessage - List selectedDevs = (List)settings.test_speechDevices - selectedDevs?.each { String devSerial-> - def childDev = getChildDeviceBySerial(devSerial) - if(childDev && childDev?.hasCommand('setVolumeSpeakAndRestore')) { - childDev?.setVolumeSpeakAndRestore((Integer)settings.test_speechVolume, testMsg, (Integer)settings.test_speechRestVolume ) - } else { - logError("Speech Test device with serial# (${devSerial} was not located!!! or does not support speakAndRestore") - } - } -} - -void executeAnnouncement() { - settingUpdate("test_announceRun", sFALSE, sBOOL) - String testMsg = (String)settings.test_announceMessage - List sDevs = (Boolean)settings.test_announceAllDevices ? getChildDevicesByCap("announce") : getDevicesFromList((List)settings.test_announceDevices) - if(sDevs?.size()) { - if(sDevs.size() > 1) { - List devObj = [] - sDevs.each { devObj.push([deviceTypeId: (String)it?.getEchoDeviceType(), deviceSerialNumber: (String)it?.getEchoSerial()]) } -// String devJson = new groovy.json.JsonOutput().toJson(devObj) -// send to first one which will have Amazon fan it out - sDevs[0]?.sendAnnouncementToDevices(testMsg, "Echo Speaks Test", devObj, settings.test_announceVolume ?: null, settings.test_announceRestVolume ?: null) - } else { - sDevs[0]?.playAnnouncement(testMsg, "Echo Speaks Test", settings.test_announceVolume ?: null, settings.test_announceRestVolume ?: null) - } - } -} - -void executeSequence() { - settingUpdate("test_sequenceRun", sFALSE, sBOOL) - String seqStr = settings.test_sequenceString - if(settings.test_sequenceDevice?.hasCommand("executeSequenceCommand")) { - settings.test_sequenceDevice?.executeSequenceCommand(seqStr) - } else { - logWarn("sequence test device doesn't support the executeSequenceCommand command...", true) - } -} - -Map executeTuneInSearch(String query) { - if(!isAuthValid("executeTuneInSearch")) { return null } - Map params = [ - uri: getAmazonUrl(), - path: "/api/tunein/search", - query: [ query: query, mediaOwnerCustomerId: state.deviceOwnerCustomerId ], - headers: getReqHeaderMap(true), - requestContentType: sAPPJSON, - contentType: sAPPJSON, - timeout: 20 - ] - Map results = [:] - try { - httpGet(params) { resp -> - if(resp?.status != 200) logWarn("${resp?.status} $params") - if(resp?.status == 200) { - results = resp?.data ?: [:] - updTsVal("lastSpokeToAmazon") - } - } - } catch (ex) { - respExceptionHandler(ex, "executeTuneInSearch") - } - return results -} - -void executeMusicSearchTest() { - settingUpdate("test_musicSearchRun", sFALSE, sBOOL) - if(settings.test_musicDevice && (String)settings.test_musicProvider && (String)settings.test_musicQuery) { - if(settings.test_musicDevice?.hasCommand("searchMusic")) { - logDebug("Performing ${(String)settings.test_musicProvider} Search Test with Query: (${(String)settings.test_musicQuery}) on Device: (${settings.test_musicDevice})") - settings.test_musicDevice.searchMusic((String)settings.test_musicQuery, (String)settings.test_musicProvider) - } else { logError("The Device ${settings.test_musicDevice} does NOT support the searchMusic() command...") } - } -} - -def musicSearchTestPage() { - return dynamicPage(name: "musicSearchTestPage", uninstall: false, install: false) { - section(sectHead("Test a Music Search on Device:")) { - paragraph spanSm("Use this to test the search you discovered above directly on a device.", sCLR4D9) - Map testEnum = ["CLOUDPLAYER": "My Library", "AMAZON_MUSIC": "Amazon Music", "I_HEART_RADIO": "iHeartRadio", "PANDORA": "Pandora", "APPLE_MUSIC": "Apple Music", "TUNEIN": "TuneIn", "SIRIUSXM": "siriusXm", "SPOTIFY": "Spotify"] - input "test_musicProvider", sENUM, title: inTS1("Select Music Provider to perform test", "music"), defaultValue: null, required: false, options: testEnum, multiple: false, submitOnChange: true - if((String)settings.test_musicProvider) { - input "test_musicQuery", "text", title: inTS1("Music Search term to test on Device", "search2"), defaultValue: null, required: false, submitOnChange: true - if((String)settings.test_musicQuery) { - input "test_musicDevice", "device.EchoSpeaksDevice", title: inTS1("Select a Device to Test Music Search", "echo_speaks_3.1x"), description: inputFooter(sTTS, sCLRGRY, true), multiple: false, required: false, submitOnChange: true - if(settings.test_musicDevice) { - input "test_musicSearchRun", sBOOL, title: inTS1("Perform the Music Search Test?", "music"), description: sBLANK, required: false, defaultValue: false, submitOnChange: true - if((Boolean)settings.test_musicSearchRun) { executeMusicSearchTest() } - } - } - } - } - section(sectHead("TuneIn Search Results:")) { - paragraph spanSm("Enter a search phrase to query TuneIn to help you find the right search term to use in searchTuneIn() command.", sCLR4D9) - input "test_tuneinSearchQuery", "text", title: inTS1("Enter search phrase for TuneIn", "tunein"), defaultValue: sNULL, required: false, submitOnChange: true - if((String)settings.test_tuneinSearchQuery) { - href "searchTuneInResultsPage", title: inTS1("View search results!", "search2"), description: inactFoot(sTTP) - } - } - } -} - -def searchTuneInResultsPage() { - return dynamicPage(name: "searchTuneInResultsPage", uninstall: false, install: false) { - Map results = executeTuneInSearch((String)settings.test_tuneinSearchQuery) - section(sectHead("Search Results: (Query: ${(String)settings.test_tuneinSearchQuery})")) { - if(results?.browseList && results?.browseList?.size()) { - results?.browseList?.eachWithIndex { Map item, Integer i-> - if(i < 25) { - if(item?.browseList != null && item?.browseList?.size()) { - item?.browseList?.eachWithIndex { Map item2, i2-> - dumpBrowseItem(item2) - } - } else { - dumpBrowseItem(item) - } - } - } - } else { paragraph "No Results found..." } - } - } -} - -def dumpBrowseItem(Map item) { - String str = sBLANK - str += "ContentType: (${item.contentType})" - str += "\nId: (${item.id})" - str += "\nDescription: ${item.description}" - String a = (String)item.image ?: sNULL - String b = ((String)item.name).take(75) - Boolean c = !item.name?.contains("Not Supported") - href "searchTuneInResultsPage", title: pTS(b, a, false), description: spanSm(str, c ? sCLR4D9 : sCLRRED) -} - -private getChildDeviceBySerial(String serial) { - List childDevs = app?.getChildDevices() - def a = childDevs?.find { it?.deviceNetworkId?.tokenize("|")?.contains(serial) } - return a ?: null -} -/* -public getChildDeviceByCap(String cap) { - List childDevs = app?.getChildDevices() - def a= childDevs?.find { it?.currentValue("permissions") && it?.currentValue("permissions")?.toString()?.contains(cap) } - return a ?: null -}*/ - -public List getDevicesFromList(List ids) { - List cDevs = app?.getChildDevices() - List a = cDevs?.findAll { it?.id in ids } - return a ?: null -} - -public getDeviceFromId(String id) { - List cDevs = app?.getChildDevices() - def a = cDevs?.find { it?.id == id } - return a ?: null -} - -public List getChildDevicesByCap(String cap) { - List childDevs = app?.getChildDevices() - List a = childDevs?.findAll { it?.currentValue("permissions") && it?.currentValue("permissions")?.toString()?.contains(cap) } - return a ?: null -} - -def donationPage() { - return dynamicPage(name: "donationPage", title: sBLANK, nextPage: "mainPage", install: false, uninstall: false) { - section(sBLANK) { - String str = sBLANK - str += spanSmBldBr("Hello User,") + spanSmBr("Please forgive the interuption but it's been 30 days since you installed/updated this App and I wanted to present you with this one time reminder that donations are accepted (We do not require them).") - str += spanSmBr("
    If you have been enjoying the software and devices please remember that we have spent thousand's of hours of our spare time working on features and stability for those applications and devices.") - str += spanSmBr("
    If you have already donated, thank you very much for your support!") - str += spanSmBr("
    If you are just not interested in donating please ignore this message") - str += spanSm("
    Thanks again for using Echo Speaks") - paragraph divSm(str, sCLRRED) - href url: textDonateUrl(), style: sEXTNRL, required: false, title: inTS1("Donations", "donate"), description: inputFooter("Tap to open in browser", sCLRGRY, true) - } - updInstData("shownDonation", true) - } -} - -def installed() { - logInfo("Installed Event Received...") - state.installData = [initVer: appVersionFLD, dt: getDtNow(), updatedDt: "Not Set", showDonation: false, sentMetrics: false, shownChgLog: true] - state.isInstalled = true - sendInstallData() - initialize() -} - -def updated() { - logInfo("Updated Event Received...") - if(!(Boolean)state.isInstalled) { state.isInstalled = true } - if(!state.installData) { state.installData = [initVer: appVersionFLD, dt: getDtNow(), updatedDt: getDtNow(), shownDonation: false, sentMetrics: false] } - unsubscribe() - state.clearCnt = 0 - state.zoneEvtsActive = false - unschedule() - stateMigrationChk() - initialize() -} - -def initialize() { - logInfo("running initialize...") - appCleanup() - //if(app?.getLabel() != "Echo Speaks") { app?.updateLabel("Echo Speaks") } - if((Boolean)settings.optOutMetrics && (String)state.appGuid) { if(removeInstallData()) { state.appGuid = sNULL; state.remove('appGuid') } } - subscribe(location, "systemStart", startHandler) - if(guardAutoConfigured()) { - if(settings.guardAwayAlarm && settings.guardHomeAlarm) { - subscribe(location, "hsmStatus", guardTriggerEvtHandler) - } - if(settings.guardAwayModes && settings.guardHomeModes) { - subscribe(location, "mode", guardTriggerEvtHandler) - } - if(settings.guardFollowSwitch || (settings.guardAwaySwitch && settings.guardHomeSwitch)) { - if(settings.guardFollowSwitch) subscribe(settings.guardFollowSwitch, sSWITCH, guardTriggerEvtHandler) - if(settings.guardHomeSwitch) subscribe(settings.guardHomeSwitch, sSWITCH, guardTriggerEvtHandler) - if(settings.guardAwaySwitch) subscribe(settings.guardAwaySwitch, sSWITCH, guardTriggerEvtHandler) - } - if(settings.guardAwayPresence) { - subscribe(settings.guardAwayPresence, "presence", guardTriggerEvtHandler) - } - } - if(!(Boolean)state.resumeConfig) { - updChildVers() - updateZoneSubscriptions() - Boolean a=validateCookie(true) - if(!(Boolean)state.noAuthActive) { - //runEvery15Minutes("getOtherData") now part of healthCheck - runEvery3Hours("getEchoDevices") //This will reload the device list from Amazon - runIn(11, "postInitialize") - remTsVal("donotdisturbDt") - remTsVal("musicProviderUpdDt") - - getOtherData() - - Long newD = now() - 999000 - Date d = new Date(newD) - updTsVal("lastDevDataUpdDt", formatDt(d)) // make sure not to cause a warning - // remTsVal("lastDevDataUpdDt") // will force next one to gather EchoDevices - getEchoDevices() - - if(advLogsActive()) { logsEnabled() } - runIn(60, "workQB") - - } else { unschedule("getEchoDevices") /*; unschedule("getOtherData") */ } - } -} - -void startHandler(evt){ - logDebug('startHandler called') - runIn(6, "chkRestartSocket") -} - -void chkRestartSocket(){ - def dev= getSocketDevice() - if(!(Boolean)dev?.isSocketActive()) { dev?.triggerInitialize() } -} - -void stateMigrationChk() { - if(!getAppFlag("stateMapConverted")) { stateMapMigration() } -} - -void updateZoneSubscriptions() { - if(state.zoneEvtsActive != true) { - subscribe(location, "es3ZoneState", zoneStateHandler) - subscribe(location, "es3ZoneRemoved", zoneRemovedHandler) - state.zoneEvtsActive = true - zoneStatusMapFLD = [:] - checkZoneData() - runIn(6, "requestZoneRefresh") - } -} - -void postInitialize() { - logTrace("postInitialize") - runEvery15Minutes("healthCheck") // This task checks for missed polls, app updates, code version changes, and cloud service health - Boolean a = refreshDevCookies(false) // don't have children re-init as it is coming next - reInitChildren() -} - -def uninstalled() { - log.warn "uninstalling app and devices" - unschedule() - if(!(Boolean)settings.optOutMetrics) { if(removeInstallData()) { state.appGuid = sNULL } } - clearCloudConfig() - clearCookieData("App Uninstalled", false) - removeDevices(true) -} - -void appCleanup() { - logTrace("appCleanup") - List items = [ - "availableDevices", "consecutiveCmdCnt", "isRateLimiting", "versionData", "heartbeatScheduled", "serviceAuthenticated", "cookie", "misPollNotifyWaitVal", "misPollNotifyMsgWaitVal", - "updNotifyWaitVal", "lastDevActivity", "devSupMap", "tempDevSupData", "devTypeIgnoreData", - "warnHistory", "errorHistory", "bluetoothData", "dndData", "zoneStatusMap", "guardData", "guardDataSrc", "guardDataOverMaxSize", "lastMsg" - ] - items.each { String si-> if(state.containsKey(si)) { state.remove(si)} } - - state.pollBlocked = false - state.resumeConfig = false - state.missPollRepair = false - state.deviceRefreshInProgress = false - - // Settings Cleanup - List setItems = ["performBroadcast", "stHub", "cookieRefreshDays"] - settings?.each { si-> ["music", "tunein", "announce", "perform", "broadcast", "sequence", "speech", "test_"].each { String swi-> if(si.key?.startsWith(swi)) { setItems.push(si?.key as String) } } } - setItems.unique().sort().each { String sI-> if(settings?.containsKey(sI)) { settingRemove(sI) } } - cleanUpdVerMap() -} - -void wsEvtHandler(Map evt) { - if(devModeFLD) logTrace("wsEvtHandler evt: ${evt}") - if(evt && /* evt.id && */ (evt.attributes?.size() || evt.triggers?.size())) { - List trigs = (List)evt.triggers - Map atts = (Map)evt.attributes - if("bluetooth" in trigs) { runIn(2, "getBluetoothRunIn") } // getBluetoothDevices(true) - if("activity" in trigs) { runIn(1, "getDeviceActivityRunIn") } // Map a=getDeviceActivity(sNULL, true) - if("notification" in trigs) { runIn(2, "getNotificationsRunIn") } - if((Boolean)evt.all == true) { - getEsDevices()?.each { eDev-> - atts.each { String k,v-> eDev.sendEvent(name: k, value: v, descriptionText: "ES wsEvt") } - if(trigs.size()) { eDev.websocketUpdEvt(trigs) } - } - } else { - def eDev = findEchoDevice((String)evt.id) - if(eDev) { - atts.each { String k,v-> eDev.sendEvent(name: k, value: v, descriptionText: "ES wsEvt") } - if(trigs.size()) { eDev.websocketUpdEvt(trigs) } - } - } - } -} - -private findEchoDevice(String serial) { - def a = getEsDevices()?.find { (String)it?.getEchoSerial() == serial } - return a ?: null -} - -Boolean getWWebSocketStatus(){ - return (Boolean)state.websocketActive -} - -void webSocketStatus(Boolean active) { - logTrace "webSocketStatus... | Active: ${active}" - state.websocketActive = active - if(active) { - remTsVal('bluetoothUpdDt') - remTsVal("donotdisturbDt") - } // healthcheck will re-read - runIn(6, "updChildSocketStatus") -} - -void updChildSocketStatus() { - Boolean active = (Boolean)state.websocketActive - logTrace "updChildSocketStatus... | Active: ${active}" - getEsDevices()?.each { it?.updSocketStatus(active) } - getZoneApps()?.each { it?.relayUpdChildSocketStatus(active) } - updTsVal("lastWebsocketUpdDt") -} - -def zoneStateHandler(evt) { - String id = evt?.value?.toString() - Map data = evt?.jsonData - logTrace("zoneStateHandler: ${id} | Data: $data") - String myId=app.getId() - checkZoneData() - if(data && id) { - Boolean aa = getTheLock(sHMLF, "zoneStateHandler") - - Map t0 = zoneStatusMapFLD[myId] - Map zoneMap = t0 ?: [:] - zoneMap[id] = [name: data?.name, active: data?.active, paused: data?.paused, zoneDevices: data.zoneDevices, t: now()] - zoneStatusMapFLD[myId] = zoneMap - zoneStatusMapFLD = zoneStatusMapFLD - - releaseTheLock(sHMLF) - - getActionApps()?.each { it?.updZones(zoneMap) } - } -} - -def zoneRemovedHandler(evt) { - String id = evt?.value?.toString() - Map data = evt?.jsonData - logTrace("zone removed: ${id} | Data: $data") - String myId=app.getId() - if(data && id) { - Boolean aa = getTheLock(sHMLF, "zoneRemoveHandler") - Map t0 = zoneStatusMapFLD[myId] - Map zoneMap = t0 ?: [:] - zoneMap = zoneMap ?: [:] - Boolean fnd=false - if(zoneMap.containsKey(id)) { - fnd = true - zoneMap.remove(id) - zoneStatusMapFLD[myId] = zoneMap - zoneStatusMapFLD = zoneStatusMapFLD - } - releaseTheLock(sHMLF) - if(fnd){ - getActionApps()?.each { it?.updZones(zoneMap) } - } - } -} - -void requestZoneRefresh() { - sendLocationEvent(name: "es3ZoneRefresh", value: "sendStatus", data: [sendStatus: true], isStateChange: true, display: false, displayed: false) -} - -void checkZoneData() { - String myId=app.getId() - if(!zoneStatusMapFLD[myId]) { - Boolean aa = getTheLock(sHMLF, "getZones") - - Map newField = [:] - List zones = getZoneApps() - zones?.each { - Map zoneMap = it?.myZoneStatus() - String id = zoneMap.id - newField[id] = [name: (String)zoneMap.name, active: (Boolean)zoneMap.active, paused: (Boolean)zoneMap.paused, zoneDevices: zoneMap.zoneDevices ?: null, t: now()] - } - newField.initialized = [a:true] - zoneStatusMapFLD[myId] = newField - zoneStatusMapFLD = zoneStatusMapFLD - - releaseTheLock(sHMLF) - - getActionApps()?.each { it?.updZones(zoneMap) } - } -} - -public Map getZones(Boolean chld=false) { - String myId=app.getId() - checkZoneData() - Map a = zoneStatusMapFLD[myId] - - if(!chld) { - String i = 'initialized' - if(a.containsKey(i)) a.remove(i) - } - return a -} - -Map getActiveZones() { - Map zones = getZones() - return zones.size() ? zones.findAll { (Boolean)it?.value?.active && !(Boolean)it?.value?.paused } : [:] -} - -Map getInActiveZones() { - Map zones = getZones() - return zones.size() ? zones.findAll { !(Boolean)it?.value?.active || (Boolean)it?.value?.paused } : [:] -} - -List getActiveZoneNames() { - Map t0 = getActiveZones() - return t0.size() ? t0.collect { (String)it?.value?.name } : [] -} - -List getInActiveZoneNames() { - Map t0 = getInActiveZones() - return t0.size() ? t0.collect { (String)it?.value?.name } : [] -} - - -List getZoneApps() { - return ((List)getAllChildApps())?.findAll { (String)it?.name == zoneChildName() } -} - -def getZoneById(String id) { - return getZoneApps()?.find { it?.id?.toString() == id } -} - -List getActiveApps() { - List acts = getActionApps() - return acts.size() ? acts.findAll { !(Boolean)it?.isPaused() } : [] -} - -List getInActiveApps() { - List acts = getActionApps() - return acts.size() ? acts.findAll { (Boolean)it?.isPaused() } : [] -} - -List getMyANames(List acts) { - acts = acts ?: [] - return acts.size() ? acts.findAll { it }.collect { (String)it?.getLabel() } : [] -} - -public List getActiveActionNames() { - return getMyANames(getActiveApps()) -} - -public List getInActiveActionNames() { - return getMyANames(getInActiveApps()) -} - -List getActionApps() { - return ((List)getAllChildApps())?.findAll { it?.name == actChildName() } -} - -List getEsDevices() { - return ((List)getChildDevices())?.findAll { !(Boolean)it?.isWS() && !(Boolean)it?.isZone() } -} - -def getSocketDevice() { - String myId=app.getId() - String nmS = 'echoSpeaks_websocket' - return getChildDevice(myId+'|'+nmS) -} - -mappings { - path("/renderMetricData") { action: [GET: "renderMetricData"] } - path("/receiveData") { action: [POST: "processData"] } - path("/config") { action: [GET: "renderConfig"] } - path("/textEditor/:cId/:inName") { action: [GET: "renderTextEditPage", POST: "textEditProcessing"] } - path("/cookie") { action: [GET: "getCookieData", POST: "storeCookieData", DELETE: "clearCookieD"] } - path("/diagData") { action: [GET: "getDiagData"] } - path("/diagCmds/:cmd") { action: [GET: "execDiagCmds"] } - path("/diagDataJson") { action: [GET: "getDiagDataJson"] } - path("/diagDataText") { action: [GET: "getDiagDataText"] } -} - -void clearCloudConfig() { - logTrace("clearCloudConfig called...") - settingUpdate("resetService", sFALSE, sBOOL) - unschedule("cloudServiceHeartbeat") - remServerItem(["onHeroku", "serverHost", "isLocal"]) - settingRemove("useHeroku") - state.remove("herokuName") - state.serviceConfigured = false - state.resumeConfig = true - clearCookieData("clearCloudConfig", false) -} - -String getEnvParamsStr() { - Map envParams = [:] - envParams["smartThingsUrl"] = getAppEndpointUrl("receiveData") - envParams["appCallbackUrl"] = getAppEndpointUrl("receiveData") - envParams["hubPlatform"] = platformFLD - envParams["useHeroku"] = ((Boolean)settings.useHeroku).toString() - envParams["serviceDebug"] = sFALSE - envParams["serviceTrace"] = sFALSE - envParams["amazonDomain"] = (String)settings.amazonDomain ?: "amazon.com" - envParams["regionLocale"] = (String)settings.regionLocale ?: "en-US" - envParams["hostUrl"] = "${getRandAppName()}.herokuapp.com".toString() - String envs = sBLANK - envParams.each { String k, String v-> envs += "&env[${k}]=${v}".toString() } - return envs -} - -Boolean checkIfCodeUpdated() { - Boolean codeUpdated = false - List chgs = [] - Map codeVerMap = (Map)state.codeVersions ?: [:] - //if(devModeFLD) logTrace("Code versions: ${codeVerMap}") - if(codeVerMap.mainApp != appVersionFLD) { - checkVersionData(true) - chgs.push("mainApp") - state.pollBlocked = true - updCodeVerMap("mainApp", appVersionFLD) - Map iData = state.installData - iData = iData ?: [:] - iData["updatedDt"] = getDtNow() - iData["shownChgLog"] = false - if(iData?.shownDonation == null) { - iData["shownDonation"] = false - } - state.installData = iData - codeUpdated = true - } - List cDevs = getEsDevices() - if(cDevs?.size()) { - String ver = (String)cDevs[0]?.devVersion() - if((String)codeVerMap.echoDevice != ver) { - chgs.push("echoDevice") - state.pollBlocked = true - updCodeVerMap("echoDevice", ver) - codeUpdated = true - } - } - // def wsDev = getSocketDevice() - // if(wsDev) { - // String ver = (String)wsDev?.devVersion() - // if((String)codeVerMap.wsDevice != ver) { - // chgs.push("wsDevice") - // updCodeVerMap("wsDevice", ver) - // codeUpdated = true - // } - // } - List cApps = getActionApps() - if(cApps?.size()) { - String ver = (String)cApps[0]?.appVersion() - if((String)codeVerMap.actionApp != ver) { - chgs.push("actionApp") - state.pollBlocked = true - updCodeVerMap("actionApp", ver) - codeUpdated = true - } - } - List zApps = getZoneApps() - if(zApps?.size()) { - String ver = (String)zApps[0]?.appVersion() - if((String)codeVerMap.zoneApp != ver) { - chgs.push("zoneApp") - state.pollBlocked = true - // log.debug "zoneVer: ver" - updCodeVerMap("zoneApp", ver) - codeUpdated = true - } - ver = sNULL - zApps.each { if(!ver) ver = it?.relayDevVersion() } - if(ver && (String)codeVerMap.zoneEchoDevice != ver) { - chgs.push("zoneEchoDevice") - state.pollBlocked = true - updCodeVerMap("zoneEchoDevice", ver) - codeUpdated = true - } - } - if(codeUpdated) { - logInfo("Code Version Change Detected... | Re-Initializing App in 5 seconds | Changes: ${chgs}") - runIn(5, "postCodeUpdated", [overwrite: false]) - return true - } else { - state.pollBlocked = false - return false - } -} - -void postCodeUpdated() { - updated() - runIn(10, "sendInstallData", [overwrite: false]) -} - -void resetQueues() { - getEsDevices()?.each { it?.resetQueue() } -} - -void reInitChildren() { - getEsDevices()?.each { it?.triggerInitialize() } - getSocketDevice()?.triggerInitialize() - runIn(11, "reInitChildActions") -} - -void reInitChildActions() { - getActionApps()?.each { it?.triggerInitialize() } - runIn(3, "reInitChildZones") -} - -void reInitChildZones() { - getZoneApps()?.each { it?.triggerInitialize() } -} - -def processData() { - logTrace("processData() | Data: ${request.JSON}") - Map data = request?.JSON as Map - if(data) { - if(data?.version) { - updServerItem("onHeroku", (data?.onHeroku != false || (!data?.isLocal && (Boolean)settings.useHeroku))) - updServerItem("isLocal", (data?.isLocal == true)) - updServerItem("serverHost", ((String)data?.serverUrl ?: sNULL)) - logTrace("processData Received | Version: ${data?.version} | onHeroku: ${data?.onHeroku} | serverUrl: ${data?.serverUrl}") - updCodeVerMap("server", (String)data?.version) - state.serviceConfigured = true - } else { log.debug "data: $data" } - } - String json = new JsonOutput().toJson([message: "success", version: appVersionFLD]) - render contentType: sAPPJSON, data: json, status: 200 -} - -Boolean serverConfigured() { - return ((Boolean)getServerItem("onHeroku") || (Boolean)getServerItem("isLocal")) -} - -def getCookieData() { - logTrace("getCookieData Request Received...") - Map resp = state.cookieData ?: [:] - String aa = getTsVal("lastCookieRrshDt") - resp["refreshDt"] = aa ?: null - String json = new JsonOutput().toJson(resp) - incrementCntByKey("getCookieCnt") - render contentType: sAPPJSON, data: json, status: 200 -} - -Map getCookieMap() { - return [cookie: getCookieVal(), csrf: getCsrfVal()] -} - -Map getReqHeaderMap(Boolean extra=false) { - Map head = [ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36", - Referer: "https://alexa.${state.cookieData?.amazonPage}/spa/index.html", - Origin: "https://alexa.${state.cookieData?.amazonPage}", - cookie: getCookieVal(), - csrf: getCsrfVal(), - ] - if(extra) return head + [Connection: "keep-alive", DNT: "1"] - return head -} - -String getCookieVal() { - String myId=app.getId() - if(! (cookieDataFLD[myId]!=null && cookieDataFLD[myId].localCookie != null)) { - Map cookieData = state.cookieData - //if (cookieData && cookieData.localCookie && cookieData.csrf) cookieDataFLD[myId] = cookieData - cookieDataFLD[myId] = cookieData - cookieDataFLD = cookieDataFLD - } - return cookieDataFLD[myId]?.localCookie ? (String)cookieDataFLD[myId].localCookie : sNULL -} - -String getCsrfVal() { - String myId=app.getId() - if(! (cookieDataFLD[myId]!=null && cookieDataFLD[myId].csrf != null)) { - Map cookieData = state.cookieData - //if (cookieData && cookieData?.localCookie && cookieData?.csrf) { cookieDataFLD[myId] = cookieData; } - cookieDataFLD[myId] = cookieData - cookieDataFLD = cookieDataFLD - } - return cookieDataFLD[myId]?.csrf ? (String)cookieDataFLD[myId].csrf : sNULL -} - -def storeCookieData() { - logTrace("storeCookieData Request Received...") - Map data = request?.JSON as Map - Map cookieItems = [:] - - if(data) { - if(data.cookieData) { - logTrace("cookieData Received: ${request?.JSON?.cookieData?.keySet()}") - - data.cookieData.each { String k,v-> cookieItems[k] = v as String } - state.cookieData = cookieItems - String myId=app.getId() - cookieDataFLD[myId] = cookieItems - cookieDataFLD = cookieDataFLD - state.clearCnt = 0 - } - - updServerItem("isLocal", (data.isLocal == true)) - updServerItem("onHeroku", (data.onHeroku != false || (!data?.isLocal && (Boolean)settings.useHeroku))) - updServerItem("serverHost", ((String)data.serverUrl ?: sNULL)) - updCodeVerMap("server", (String)data.version) - remTsVal(["lastCookieChkDt", "lastSpokeToAmazon"]) - } - - // log.debug "csrf: ${state.cookieData?.csrf}" - Boolean a=validateCookie(true) - if((Boolean)state.authValid && cookieItems.localCookie && cookieItems.csrf ) { - logInfo("Cookie data was updated | Reinitializing App... | in 10 seconds...") - state.serviceConfigured = true - updTsVal("lastCookieRrshDt") - checkGuardSupport() - runIn(10, "initialize", [overwrite: true]) - } else { - logWarn("Cookie data was updated but found invalid...") - } - -// should be rendering a response? - String json = new JsonOutput().toJson([message: "success", version: appVersionFLD]) - render contentType: sAPPJSON, data: json, status: 200 -} - -def clearCookieD() { -// deal with temporary problems - Integer a = state.clearCnt - a = a!= null ? a : 0 - a = a+1 - state.clearCnt = a - if(a > 5) clearCookieData('webCall', false) - else logTrace("skipping server call to clearCookieData()") - String json = new JsonOutput().toJson([message: "success", version: appVersionFLD]) - render contentType: sAPPJSON, data: json, status: 200 -} - -def clearCookieData(String src=sNULL, Boolean callSelf=false) { - logTrace("clearCookieData(${src ?: sBLANK}, $callSelf)") - settingUpdate("resetCookies", sFALSE, sBOOL) - if(!callSelf) authEvtHandler(false, "clearCookieData") - state.remove("cookie") - state.remove("cookieData") - String myId=app.getId() - cookieDataFLD[myId] = [:] - cookieDataFLD = cookieDataFLD - state.clearCnt = 0 - remTsVal(["lastCookieChkDt", "lastCookieRrshDt"]) - unschedule("getEchoDevices") -// unschedule("getOtherData") - logWarn("Cookie Data has been cleared and Device Data Refreshes have been suspended...") - updateChildAuth(false) -} - -Boolean refreshDevCookies(Boolean doInit=true) { - logTrace("refreshDevCookies()") - settingUpdate("refreshDevCookies", sFALSE, sBOOL) - logDebug("Re-Syncing Cookie Data with Devices") - Boolean isValid = ((Boolean)state.authValid && getCookieVal() && getCsrfVal()) - updateChildAuth(isValid, doInit) - return isValid -} - -void updateChildAuth(Boolean isValid, Boolean doInit=true) { - Map cook = getCookieMap() - ((List)getChildDevices())?.each { (isValid) ? it?.updateCookies(cook, doInit) : it?.removeCookies(true) } - getZoneApps()?.each { (isValid) ? it?.relayUpdateCookies(cook, doInit) : it?.relayRemoveCookies(true) } -} - -@Field volatile static Map authValidMapFLD = [:] - -void authValidationEvent(Boolean valid, String src=sNULL) { - Boolean isValid = valid && getCookieVal() && getCsrfVal() - Integer listSize = 3 - String myId=app.getId() - List eList = authValidMapFLD[myId] - eList = eList ?: [true, true, true] - eList.push(isValid) - if(eList.size() > listSize) { eList = eList.drop( eList.size()-listSize ) } - authValidMapFLD[myId]= eList - authValidMapFLD[myId]= authValidMapFLD[myId] - state.authValidHistory = eList - if(eList.every { it == false }) { - if(!(Boolean)state.noAuthActive) { - logError("The last 3 Authentication Validations have failed | Clearing Stored Auth Data | Please login again using the Echo Speaks service...") - } - authEvtHandler(false, 'a_v_'+src) - } else { authEvtHandler(true, 'a_v_'+src) } -} - -void authEvtHandler(Boolean isAuth, String src=sNULL) { - Boolean stC = ((Boolean)state.authValid != isAuth) - logDebug "authEvtHandler(${isAuth},$src) stateChange: ${stC}" - state.authValid = isAuth - state.remove('authEvtClearReason') - if(!isAuth && !(Boolean)state.noAuthActive) { - state.noAuthActive = true - clearCookieData('authHandler', true) - noAuthReminder() - if((Boolean)settings.sendCookieInvalidMsg && getLastTsValSecs("lastCookieInvalidMsgDt") > 28800) { - String loc='Local Server' - if((Boolean)getServerItem("onHeroku")) loc='Heroku' - sendMsg("${app.name} Amazon Login Issue", "Amazon Cookie Has Expired or is Missing!!! Please login again using the ${loc} Web Config page...") - updTsVal("lastCookieInvalidMsgDt") - } - logDebug("Scheduling noAuthReminder ${stC}") - runEvery1Hour("noAuthReminder") - state.authEvtClearReason = [dt: getDtNow(), src: src] - updateChildAuth(isAuth) - } else if(isAuth && (Boolean)state.noAuthActive) { - unschedule("noAuthReminder") - state.noAuthActive = false - logDebug("Scheduling initialize for auth change ${stC}") - runIn(10, "initialize", [overwrite: true]) - } -} - -Boolean isAuthValid(String methodName) { - if(!(Boolean)state.authValid) { - logWarn("Echo Speaks Authentication is no longer valid... Please login again and commands will be allowed again!!! | Method: (${methodName})", true) - return false - } - return true -} - -void noAuthReminder() { - String loc='Local Server' - if((Boolean)getServerItem("onHeroku")) loc='Heroku' - logWarn("Amazon Cookie Has Expired or is Missing!!! Please login again using the ${loc} Web Config page...") -} - -static String toQueryString(Map m) { - return m.collect { k, v -> "${k}=${URLEncoder.encode(v?.toString(), "utf-8").replaceAll("\\+", "%20")}" }?.sort()?.join("&") -} - -String getServerHostURL() { - String srvHost = (String)getServerItem("serverHost") - return ((Boolean)getServerItem("isLocal") && srvHost) ? (srvHost ?: sNULL) : "https://${getRandAppName()}.herokuapp.com".toString() -} - -Integer cookieRefreshSeconds() { return ((Integer)settings.refreshCookieDays ?: 5)*86400 as Integer } - -void clearServerAuth() { - logDebug("clearServerAuth: serverUrl: ${getServerHostURL()}") - Map params = [ uri: getServerHostURL(), path: "/clearAuth", timeout: 20 ] - Long execDt = now() - httpGet(params) { resp-> - // log.debug "resp: ${resp.status} | data: ${resp?.data}" - if(resp?.status != 200) logWarn("clearServerAuth: ${resp?.status} $params") - if (resp?.status == 200) { - logInfo("Clear Server Auth Completed... | Process Time: (${execDt ? (now()-execDt) : 0}ms)") - } - } -} - -void wakeupServer(Boolean c=false, Boolean g=false, String src) { - Map params = [ - uri: getServerHostURL(), - path: "/wakeup", - headers: [wakesrc: src], - contentType: "text/plain", - requestContentType: "text/plain", - timeout: 20 - ] -// if(!getCookieVal() || !getCsrfVal()) { logWarn("wakeupServer | Cookie or CSRF Missing... Skipping Wakeup"); return; } - logTrace("wakeupServer $c $g $src") - execAsyncCmd("post", "wakeupServerResp", params, [execDt: now(), refreshCookie: c, updateGuard: g, wakesrc: src]) -} - -void runCookieRefresh() { - logTrace("runCookieRefresh") - settingUpdate("refreshCookie", sFALSE, sBOOL) - if(getLastTsValSecs("lastCookieRrshDt", 500000) < 86400) { logError("Cookie Refresh is blocked... | Last refresh was less than 24 hours ago.", true); return } - wakeupServer(true, false, "runCookieRefresh") -} - -def wakeupServerResp(response, data) { - try { - if(response?.status != 200) { logWarn("wakeupServerResp: ${response?.status}") } - def rData = response?.data ?: null - if(response?.status == 200) { - updTsVal("lastServerWakeDt") - if (rData && rData == "OK") { - logDebug("$rData wakeupServer Completed... | Process Time: (${data?.execDt ? (now()-data?.execDt) : 0}ms) | Source: (${data?.wakesrc}) ${data}") - if(data?.refreshCookie == true) { runIn(2, "cookieRefresh") } - if(data?.updateGuard == true) { runIn(2, "checkGuardSupportFromServer") } - } else { - logWarn("wakeupServerResp: noData ${rData} ${data}") - } - } - } catch(ex) { - logError("wakeupServerResp Server may be down / unreachable") - respExceptionHandler(ex, "wakeupServerResp", false, false) - } -} - -void cookieRefresh() { - Map cookieData = state.cookieData ?: [:] - if (!cookieData || !cookieData?.loginCookie || !cookieData?.refreshToken) { - logError("cookieRefresh: Required Registration data is missing for Cookie Refresh ${cookieData}") - return - } - Map params = [ - uri: getServerHostURL(), - path: "/refreshCookie", - contentType: sAPPJSON, - timeout: 20 - ] - logTrace("cookieRefresh") - execAsyncCmd("get", "cookieRefreshResp", params, [execDt: now()]) -} - -def cookieRefreshResp(response, data) { - String cMsg - try { - if(response?.status != 200) { - cMsg = "Amazon Cookie Refresh FAILED ${response?.status}" - logWarn(cMsg) - } else { - Map rData = response?.data ? parseJson(response?.data?.toString()) : null - // log.debug "rData: $rData" - if (rData && rData?.result && rData?.result?.size()) { - logInfo("Amazon Cookie Refresh Completed | Process Time: (${data?.execDt ? (now()-data?.execDt) : 0}ms)") - cMsg = "Amazon Cookie was Refreshed Successfully!!!" - // log.debug "refreshAlexaCookie Response: ${rData?.result}" - } else { - logWarn("Amazon Cookie Refresh Completed with NO DATA ${rData} | Process Time: (${data?.execDt ? (now()-data?.execDt) : 0}ms)") - cMsg = "Amazon Cookie was Completed with NO DATA" - } - } - } catch(ex) { - cMsg = "Amazon Cookie FAILED!!! ${ex?.getMessage()}" - respExceptionHandler(ex, "cookieRefreshResp", false, false) - } - if((Boolean)settings.sendCookieRefreshMsg && getLastTsValSecs("lastCookieRfshMsgDt") > 15) { - sendMsg("${app.name} Cookie Refresh", cMsg) - updTsVal("lastCookieRfshMsgDt") - } -} - -Boolean validateCookie(Boolean frc=false) { - Boolean valid = (Boolean)state.authValid - Integer lastChk = getLastTsValSecs("lastCookieChkDt", 3600) - Integer lastSpoke = getLastTsValSecs("lastSpokeToAmazon", 3600) - Boolean cookieOk = getCookieVal() && getCsrfVal() - if(!frc && valid && lastChk <= 1800 && cookieOk) { return valid } - if(!frc && valid && lastSpoke <= 1800 && lastChk < 3600 && cookieOk) { return valid } - if(frc && valid && lastChk <= 60 && cookieOk) { return valid } - - valid = false - String meth='validateCookie' - if(!cookieOk) { - authValidationEvent(valid, meth) - return valid - } - try { - Long execDt = now() - Map params = [ - uri: getAmazonUrl(), - path: "/api/bootstrap", - query: ["version": 0], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - ] - logTrace(meth) - if(!frc) { execAsyncCmd("get", "validateCookieResp", params, [dt:execDt]) } - else { - httpGet(params) { resp-> - valid = validateCookieResp(resp, [dt:execDt]) - } - } - } catch(ex) { - respExceptionHandler(ex, "validateCookie", true) - incrementCntByKey("err_app_cookieValidCnt") - } - return valid -} - -Boolean validateCookieResp(resp, data){ - try { - String meth = 'validCookieResp' - if(resp?.status != 200) { logWarn("${resp?.status} $meth"); return false } - if(resp?.status == 200) updTsVal("lastSpokeToAmazon") - def t0 = resp?.data - Map aData - if( t0 instanceof String) aData = parseJson(resp?.data) - else aData = resp?.data - aData = aData ?: null - aData = (Map)aData?.authentication ?: null - if (aData) { - // log.debug "aData: $aData" - if(aData.customerId) { state.deviceOwnerCustomerId = aData.customerId } - if(aData.customerName) { state.customerName = aData.customerName } - Boolean valid = (aData.authenticated != false) - authValidationEvent(valid, meth) - updTsVal("lastCookieChkDt") - logDebug("Cookie Validation: (${valid}) | Process Time: (${(now()-(Long)data.dt)}ms)") - return true - } - } catch(ex) { - respExceptionHandler(ex, "validCookieResp", true) - incrementCntByKey("err_app_cookieValidCnt") - } - return false -} -/* -private getCustomerData(Boolean frc=false) { - try { - if(!frc && state.amazonCustomerData && getLastTsValSecs("lastCustDataUpdDt") < 3600) { return state.amazonCustomerData } - if(!isAuthValid("getCustomerData")) { return null } - Long execDt = now() - Map params = [ - uri: getAmazonUrl(), - path: "/api/get-customer-pfm", - query: ["_": execDt], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - ] - logTrace("getCustomerData") - httpGet(params) { resp-> - if(resp?.status != 200) logWarn("${resp?.status} $params") - if(resp?.status == 200) { - updTsVal("lastSpokeToAmazon") - Map pData = resp?.data ?: null - if (pData) { - Map d = [:] - if(pData?.marketPlaceLocale) { d["marketPlaceLocale"] = pData?.marketPlaceLocale } - if(pData?.marketPlaceId) { d["marketPlaceId"] = pData?.marketPlaceId } - state.amazonCustomerData = d - updTsVal("lastCustDataUpdDt") - return state.amazonCustomerData - } - } - } - } catch(ex) { - respExceptionHandler(ex, "getCustomerData", true) - updTsVal("lastCustDataUpdDt") - } -} -*/ -/* -private List getAllDeviceVolumes(Boolean frc=false) { - if(!isAuthValid("getAllDeviceVolumes")) { return [] } - if(!frc && (List)state.deviceVolumes && getLastTsValSecs("deviceVolumeUpdDt") < 3600) { return (List)state.deviceVolumes } - Map params = [ - uri: getAmazonUrl(), - path: "/api/devices/deviceType/dsn/audio/v1/allDeviceVolumes", - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] - List volumes = [] - try { - logTrace("getAllDeviceVolumes") - httpGet(params) { response -> - if(response?.status != 200) logWarn("${response?.status} $params") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - Map rData = response?.data ?: [:] - // log.debug "Device Volumes: ${rData.volumes}" - state.deviceVolumes = rData && rData.volumes ? rData.volumes : [] - volumes = rData && (List)rData.volumes ? (List)rData.volumes : [] - updTsVal("deviceVolumeUpdDt") - } - } - } catch (ex) { - respExceptionHandler(ex, "getAllDeviceVolumes", true) - } - return volumes -} - */ - -// private getCustomerHistoryRecords(Integer maxRecordSize = 1, Boolean frc) { -// if(!isAuthValid("getCustomerHistoryRecords")) { return [:] } -// if(!frc && (Map)state.deviceVolumes && getLastTsValSecs("customerHistoryRecUpdDt") < 3600) { return (Map)state.deviceVolumes } -// Map params = [ -// uri: getAmazonUrl(), -// path: "/alexa-privacy/apd/rvh/customer-history-records", -// query: [ -// startTime: (now() - 24 * 60 * 60 * 1000), -// endTime: now(), -// recordType: 'VOICE_HISTORY', -// maxRecordSize: maxRecordSize -// ], -// headers: getReqHeaderMap(), //getCookieMap(), -// contentType: sAPPJSON, -// timeout: 20 -// ] -// Map items = [:] -// try { -// logTrace("getCustomerHistoryRecords") -// httpGet(params) { response -> -// if(response?.status != 200) logWarn("${response?.status} $params") -// if(response?.status == 200) { -// updTsVal("lastSpokeToAmazon") -// def result = response?.data -// log.debug "result: $result" - -// List ret = []; -// if (result.customerHistoryRecords) { -// for (List r = 0; r < result.customerHistoryRecords.size(); r++) { -// def res = result.customerHistoryRecords[r]; -// def o = [ -// data: res -// ]; -// Map convParts = [:]; -// if (res.voiceHistoryRecordItems && Array.isArray(res.voiceHistoryRecordItems)) { -// res.voiceHistoryRecordItems.each { item -> -// convParts[item.recordItemType] = convParts[item.recordItemType] || []; -// convParts[item.recordItemType].push(item); -// }; -// } - -// def recordKey = res.recordKey.split('#'); // A3NSX4MMJVG96V#1612297041815#A1RABVCI4QCIKC#G0911W0793360TLG - -// o.deviceType = recordKey[2] || null; -// //o.deviceAccountId = res.sourceDeviceIds[i].deviceAccountId || null; -// o.creationTimestamp = res.timestamp || null; -// //o.activityStatus = res.activityStatus || null; // DISCARDED_NON_DEVICE_DIRECTED_INTENT, SUCCESS, FAIL, SYSTEM_ABANDONED - -// o.deviceSerialNumber = recordKey[3]; -// // if (!this.serialNumbers[o.deviceSerialNumber]) continue; -// // o.name = this.serialNumbers[o.deviceSerialNumber].accountName; -// // const dev = this.find(o.deviceSerialNumber); -// // let wakeWord = (dev && dev.wakeWord) ? dev.wakeWord : null; - -// // if (convParts.CUSTOMER_TRANSCRIPT) { -// // o.description = {'summary': ''}; -// // convParts.CUSTOMER_TRANSCRIPT.forEach(trans => { -// // let text = trans.transcriptText; -// // if (wakeWord && text.startsWith(wakeWord)) { -// // text = text.substr(wakeWord.length).trim(); -// // } -// // o.description.summary += text + ', '; -// // }); -// // o.description.summary = o.description.summary.substring(0, -2).trim(); -// // } -// // if (convParts.ALEXA_RESPONSE) { -// // o.alexaResponse = ''; -// // convParts.ALEXA_RESPONSE.forEach(trans => o.alexaResponse += trans.transcriptText + ', '); -// // o.alexaResponse = o.alexaResponse.substring(0, -2).trim(); -// // } -// // if (!o.description || !o.description.summary.length) continue; -// // if (options.filter) { -// // if (res.utteranceType === 'WAKE_WORD_ONLY') { -// // continue; -// // } - -// // switch (o.description.summary) { -// // case 'stopp': -// // case 'alexa': -// // case 'echo': -// // case 'computer': -// // case 'amazon': -// // case ',': -// // case '': -// // continue; -// // } -// // } - -// // if (o.description.summary || !options.filter) ret.push(o); -// } -// } -// } -// } -// } catch (ex) { -// respExceptionHandler(ex, "getCustomerHistoryRecords", true) -// } -// return items -// } - -/* -private userCommIds() { - if(!isAuthValid("userCommIds")) { return } - try { - Map params = [ - uri: "https://alexa-comms-mobile-service.${getAmazonDomain()}", - path: "/accounts", - headers: getCookieMap(), - contentType: sAPPJSON, - timeout: 20 - ] - logTrace("userCommIds") - httpGet(params) { response-> - if(response?.status != 200) logWarn("${response?.status} $params") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - List resp = response?.data ?: [] - Map accItems = (resp?.size()) ? resp?.findAll { it?.signedInUser?.toString() == sTRUE }?.collectEntries { [(it?.commsId as String): [firstName: it?.firstName as String, signedInUser: it?.signedInUser, isChild: it?.isChild]]} : [:] - state.accountCommIds = accItems - logDebug("Amazon User CommId's: (${accItems})") - } - } - } catch(ex) { - respExceptionHandler(ex, "userCommIds") - } -} -*/ -public void childInitiatedRefresh() { - Integer lastRfsh = getLastTsValSecs("lastChildInitRefreshDt", 3600)?.abs() - if(!(Boolean)state.deviceRefreshInProgress && lastRfsh > 40) { - updTsVal("lastChildInitRefreshDt") - logDebug("A Child Device is requesting a Device List Refresh...") - getOtherData() - runIn(3, "getEchoDevices1") - } else { - if(devModeFLD) logWarn("childInitiatedRefresh request ignored... Refresh already in progress or it's too soon to refresh again | Last Refresh: (${lastRfsh} seconds)") - } -} - -public updChildVers() { - List cApps = getActionApps() - List zApps = getZoneApps() - List eDevs = getEsDevices() - updCodeVerMap("actionApp", cApps?.size() ? cApps[0]?.appVersion() : null) - updCodeVerMap("zoneApp", zApps?.size() ? zApps[0]?.appVersion() : null) - updCodeVerMap("echoDevice", eDevs?.size() ? eDevs[0]?.devVersion() : null) - // def wDevs = getSocketDevice() - // updCodeVerMap("wsDevice", wDevs ? wDevs?.devVersion() : null) -} - -Map getMusicProviders(Boolean frc=false) { - if(!isAuthValid("getMusicProviders")) { return [:] } - if(!frc && (Map)state.musicProviders && getLastTsValSecs("musicProviderUpdDt") < 7200) { return (Map)state.musicProviders } - Map params = [ - uri: getAmazonUrl(), - path: "/api/behaviors/entities", - query: [ skillId: "amzn1.ask.1p.music" ], - headers: ["Routines-Version": "1.1.210292" ] + getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] - Map items = [:] - try { - logTrace("getMusicProviders") - httpGet(params) { response -> - if(response?.status != 200) logWarn("${response?.status} $params") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - List rData = (List)response?.data ?: [] - if(rData.size()) { - rData.findAll { it?.availability == "AVAILABLE" && it?.id != "DEFAULT" }?.each { Map item-> - items[item.id] = (String)item.displayName - } - } - // log.debug "Music Providers: ${items}" - state.musicProviders = items - updTsVal("musicProviderUpdDt") - } - } - } catch (ex) { - respExceptionHandler(ex, "getMusicProviders", true) - } - return items -} - -private getOtherData() { - stateMigrationChk() - getDoNotDisturb(true) - getBluetoothDevices() - Map aa = getMusicProviders() - // def bb=getAllDeviceVolumes() - // getCustomerData() - // getAlexaSkills() -} - -void getNotificationsRunIn(){ - getNotifications(true) -} - -// Called by child devices -private List getNotificationList(Boolean frc) { - getNotifications(frc) - return (List)state.notifications -} - -private void getNotifications(Boolean frc = false) { - Integer lastU = getLastTsValSecs("notificationsUpdDt") - if( (frc && lastU < 9)) { return } - if( (!frc && (Boolean)state.websocketActive && state.notifications && lastU < 10800) ) { return } - if(!isAuthValid("getNotifications")) { return } - updTsVal("notificationsUpdDt") - Map params = [ - uri: getAmazonUrl(), - path: "/api/notifications", - query: [cached: true], - headers: getCookieMap(), - contentType: sAPPJSON, - timeout: 20, - ] - try { - logTrace('getNotifications') - Map sData = null - List newList = [] - httpGet(params) { response-> - sData = response?.data ?: null - // log.trace "notifications: $sData" - } - if(sData?.size()) { - Boolean all = true - List s = ["ON"] - if(all) s.push("OFF") - List kI = ['id', 'reminderLabel', 'createdDate', 'originalDate', 'originalTime', 'deviceSerialNumber', 'type', 'remainingDuration', 'remainingTime', 'status'] - List> items = sData.notifications ? sData.notifications.findAll { it.status in s /* && (it.type == type) && it?.deviceSerialNumber == (String)state.serialNumber */ } : [] - items?.each { item-> - Map li = [:] - item.keySet()?.each { String key-> if(key in kI) { li[key] = item[key] } } - newList.push(li) - } - } - // log.trace "notifications: $newList" - state.notifications=newList - } catch (ex) { - respExceptionHandler(ex, "getNotifications") - } -} - -void getBluetoothRunIn(){ - getBluetoothDevices(true) -} - -void getBluetoothDevices(Boolean frc=false) { - String myId=app.getId() - Integer lastU = getLastTsValSecs("bluetoothUpdDt") - if( (frc && lastU < 30)) { return } - if( (!frc && (Boolean)state.websocketActive && bluetoothDataFLD[myId] && lastU < 10800) ) { return } - if(!isAuthValid("getBluetoothDevices")) { return } - updTsVal("bluetoothUpdDt") - Map params = [ - uri: getAmazonUrl(), - path: "/api/bluetooth", - query: [cached: true, _: new Date()?.getTime()], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] - - try { - logTrace("getBluetoothDevices") - if(!frc) { - execAsyncCmd("get", "getBluetoothResp", params, [:]) - } else { - httpGet(params) { response -> - getBluetoothResp(response, [:]) - } - } - } catch (ex) { - respExceptionHandler(ex, "getBluetoothDevices", true) - if(!bluetoothDataFLD[myId]) { bluetoothDataFLD[myId] = [:] } - } -} - -void getBluetoothResp(resp, data) { - try { - String meth = 'getBluetoothResp' - if(resp?.status != 200) logWarn("${resp?.status} $meth") - if(resp?.status == 200) { - updTsVal("lastSpokeToAmazon") - def t0 = resp?.data - Map btResp - if (t0 instanceof String) { btResp = parseJson(resp?.data) } - else { btResp = resp?.data } - // log.debug "Bluetooth Items: ${btResp}" - String myId=app.getId() - bluetoothDataFLD[myId] = btResp - bluetoothDataFLD=bluetoothDataFLD - updTsVal("bluetoothUpdDt") - } - } catch(ex) { - respExceptionHandler(ex, "getBluetoothResp", true) - String myId=app.getId() - if(!bluetoothDataFLD[myId]) { bluetoothDataFLD[myId] = [:] } - } -} - -Map getBluetoothData(String serialNumber) { - if(!isAuthValid("getBluetoothData")) { return [btObjs: [:], pairedNames: [], curConnName: sBLANK] } - String myId=app.getId() - // logTrace("getBluetoothData: ${serialNumber}") - String curConnName = sNULL - Map btObjs = [:] - getBluetoothDevices(true) - Map btData = bluetoothDataFLD[myId] - if(btData == null) { - bluetoothDataFLD[myId] = [:] - bluetoothDataFLD=bluetoothDataFLD - btData = [:] - } - Map bluData = btData && btData.bluetoothStates?.size() ? ((List)btData.bluetoothStates)?.find { it?.deviceSerialNumber == serialNumber } : [:] - if(bluData && bluData.size() && bluData.pairedDeviceList && bluData.pairedDeviceList?.size()) { - def bData = bluData.pairedDeviceList.findAll { (it?.deviceClass != "GADGET") } - bData?.findAll { it?.address != null }?.each { - btObjs[it?.address as String] = it - if(it?.connected == true) { curConnName = it?.friendlyName as String } - } - } - List tob = btObjs?.findAll { it?.value?.friendlyName != null }?.collect { it?.value?.friendlyName?.toString()?.replaceAll("\ufffd", sBLANK) } - return [btObjs: btObjs, pairedNames: tob ?: [], curConnName: curConnName?.replaceAll("\ufffd", sBLANK)] -} - -@Field volatile static Map devActivityMapFLD = [:] - -void getDeviceActivityRunIn() { - Map a = getDeviceActivity(sNULL, true) -} - -Map getDeviceActivity(String serialNum, Boolean frc=false) { - if(!isAuthValid("getDeviceActivity")) { return null } - try { - Integer lastUpdSec = getLastTsValSecs("lastDevActChk") - // log.debug "lastUpdSec: $lastUpdSec" - if((frc && lastUpdSec > 3) || lastUpdSec >= 360) { - updTsVal("lastDevActChk") - Long execDt = now() - Map params = [ - uri: getAmazonUrl(), - path: "/api/activities", - query: [ size: 5, offset: 1 ], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] - logTrace("getDeviceActivity($serialNum, $frc)") - - if(!serialNum) execAsyncCmd("get", "getLastActResp", params, [dt:execDt]) - else { - httpGet(params) { response -> - getLastActResp(response, [dt: execDt]) - } - } - } else if (!serialNum) runIn(3, "getDeviceActivityRunIn") - if(serialNum) { - String appId=app.getId() - Map lastActData = devActivityMapFLD[appId] - lastActData = lastActData ?: null - // log.debug "activityData(IN): $lastActData" - if(lastActData && (String)lastActData.serialNumber == serialNum) { - // log.debug "activityData(OUT): $lastActData" - return lastActData - } - } - } catch (ex) { - respExceptionHandler(ex, "getDeviceActivity") - // log.error "getDeviceActivity error: ${ex.message}" - } - return null -} - -def getLastActResp(resp, data){ - try { - String meth = 'getLastActResp' - if(resp?.status != 200) logWarn("${resp?.status} $meth") - if(resp?.status == 200) { - updTsVal("lastSpokeToAmazon") - def t0 = resp?.data - Map myResp - if (t0 instanceof String) { myResp = parseJson(t0) } - else { myResp = (Map)resp?.data } - List act = (List)myResp?.activities - if (myResp && act != null) { - Map lastCommand = act.find { - (it?.domainAttributes == null || it?.domainAttributes?.startsWith("{")) && - it?.activityStatus?.equals("SUCCESS") && - (it?.utteranceId?.startsWith(it?.sourceDeviceIds?.deviceType) || it?.utteranceId?.startsWith("Vox:")) && - it?.utteranceId?.contains(it?.sourceDeviceIds?.serialNumber) - } - if (lastCommand) { - Map lastDescription = (Map) new JsonSlurper().parseText((String)lastCommand.description) - def lastDevice = ((List)lastCommand.sourceDeviceIds)?.get(0) - Map lastActData = [ serialNumber: lastDevice?.serialNumber, spokenText: lastDescription?.summary, lastSpokenDt: lastCommand.creationTimestamp ] - - String appId=app.getId() - devActivityMapFLD[appId] = lastActData - devActivityMapFLD = devActivityMapFLD - } - } - } - logDebug("getDeviceActivity: Process Time: (${(now()-(Long)data.dt)}ms)") - } catch(ex) { - respExceptionHandler(ex, "getLastActResp") - } -} - -void getDoNotDisturb(Boolean frc=true) { - if(!isAuthValid("getDoNotDisturb")) { return } - String myId=app.getId() - Integer lastU = getLastTsValSecs("donotdisturbDt") - if( (frc && lastU < 20)) { runIn(24, "getDoNotDisturb"); return } - if( (!frc && (Boolean)state.websocketActive && dndDataFLD[myId] && lastU < 10800) ) { return } - updTsVal("donotdisturbDt") - Map params = [ - uri: getAmazonUrl(), - path: "/api/dnd/device-status-list", - query: [_: now()], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] -// Map dndResp = [:] -// String myId=app.getId() - try { - logTrace("getDoNotDisturb") - execAsyncCmd("get", "DnDResp", params, [:]) - } catch (ex) { - respExceptionHandler(ex, "getDoNotDisturb", true) - if(!dndDataFLD[myId]) { dndDataFLD[myId] = [:] } - } -} - -void DnDResp(resp, data){ - try { - String meth = 'DnDResp' - if(resp?.status != 200) logWarn("${resp?.status} $meth") - if(resp?.status == 200) { - updTsVal("lastSpokeToAmazon") - def t0 = resp?.data - def dndResp - if( t0 instanceof String) dndResp = parseJson(resp?.data) - else dndResp = resp?.data -// log.debug "DoNotDisturb Data: ${dndResp}" - String myId=app.getId() - dndDataFLD[myId] = (Map)dndResp - dndDataFLD=dndDataFLD - } - } catch(ex) { - respExceptionHandler(ex, "DnDResp", true) - String myId=app.getId() - if(!dndDataFLD[myId]) { dndDataFLD[myId] = [:] } - } -} - -Boolean getDndEnabled(String serialNumber) { - if(!isAuthValid("getDndEnabled")) { return false } - String myId=app.getId() - Map sData = dndDataFLD[myId] - if(sData == null) { - getDoNotDisturb() - sData = dndDataFLD[myId] - if(sData == null) { - dndDataFLD[myId] = [:] - dndDataFLD=dndDataFLD - sData = [:] - } - } - Map dndData = sData && sData.doNotDisturbDeviceStatusList?.size() ? ((List)sData.doNotDisturbDeviceStatusList)?.find { it?.deviceSerialNumber == serialNumber } : [:] - return (dndData && dndData.enabled == true) -} - -public Map getAlexaRoutines(String autoId=sNULL) { - if(!isAuthValid("getAlexaRoutines")) { return [:] } - - String myId=app.getId() - Integer lastU = getLastTsValSecs("alexaRoutinesUpdDt") - List rtList = [] - Map rtResp = [:] - - if(alexaRoutinesDataFLD[myId] && ( (autoId && lastU < 90) || (!autoId && lastU < 180) )) { rtList = alexaRoutinesDataFLD[myId] } - else { - Map params = [ - uri: getAmazonUrl(), - path: "/api/behaviors/v2/automations", - query: [ limit: 100 ], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20 - ] - - try { - logTrace("getAlexaRoutines($autoId)") - httpGet(params) { response -> - if(response?.status != 200) logWarn("${response?.status} $params") - if(response?.status == 200) { - rtList = response?.data ?: [] - updTsVal("lastSpokeToAmazon") - updTsVal("alexaRoutinesUpdDt") - alexaRoutinesDataFLD[myId] = rtList - alexaRoutinesDataFLD=alexaRoutinesDataFLD - } - } - } catch (ex) { - respExceptionHandler(ex, "getAlexaRoutines", true) - } - } - - if(!rtList && alexaRoutinesDataFLD[myId]) { rtList = alexaRoutinesDataFLD[myId] } - - // log.debug "alexaRoutines: $rtList" - Map items = [:] - Integer cnt = 1 - if(rtList.size()) { - if(autoId) { - rtResp = ((List)rtList).find { it?.automationId?.toString() == autoId } ?: [:] - //log.debug "rtResp: ${rtResp}" - return rtResp - } else { - ((List)rtList).findAll { it?.status == "ENABLED" }?.each { Map item -> - String myK = item.automationId.toString() - if(item.name != null) { - items[myK] = item.name.toString() - } else { - if(item.triggers?.size()) { - item.triggers.each { trg-> - if(trg.payload?.containsKey("utterance") && trg.payload?.utterance != null) { - items[myK] = (String)trg.payload.utterance - } else if(trg.type != null) { - // log.debug "trg: $trg" - String pt = trg.type.toString() - if(pt.toLowerCase().contains('guard')) { - items[myK] = "Unlabeled Guard Routine ($cnt)" - cnt++ - } else { - items[myK] = "Unlabeled Routine ($cnt)" - cnt++ - } - } - else { - items[myK] = "Unlabeled Routine ($cnt)" - cnt++ - } - } - } - } - } - } - rtResp = items - } - - //log.debug "routines: $rtResp" - return rtResp -} - -/*public getAlexaRoutineByNameOrID(String nameOrId) { - // TODO: This doesn't work yet... - Map routines = getAlexaRoutines() - if(routines.size()) { - Map match = routines.find { it.name == nameOrId || it.automationId == nameOrId } - if(match) return match - } -} */ - -Boolean executeRoutineById(String routineId) { - Long execDt = now() - Map routineData = getAlexaRoutines(routineId) - //log.debug "routineData: ${routineData.sequence}" - if(routineData && routineData.sequence) { - //sendSequenceCommand("ExecuteRoutine", routineData, null) - List seqList = [] - seqList.push([command: routineData]) - queueMultiSequenceCommand(seqList, "ExecuteRoutine", false) - String rtName = routineData && routineData.name ? routineData.name : sBLANK - logDebug("Queued Alexa Routine | Process Time: (${(now()-execDt)}ms) | Label: ${rtName} | RoutineId: ${routineId}") - return true - } else { - logError("No Routine Data Returned for ID: (${routineId})") - return false - } -} - -void checkGuardSupport() { -// Long execDt = now() - Integer lastUpdSec = getLastTsValSecs("lastGuardSupChkDt") - if(lastUpdSec < 125 ) { - if (state.alexaGuardSupported) { getGuardState() } - return - } - if(!isAuthValid("checkGuardSupport")) { return } - Map params = [ - uri: getAmazonUrl(), - path: "/api/phoenix", - query: [ cached: true, _: new Date().getTime() ], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - ] - logTrace("checkGuardSupport") - execAsyncCmd("get", "checkGuardSupportResponse", params, [execDt: now()]) -} - -void checkGuardSupportResponse(response, data) { - // log.debug "checkGuardSupportResponse Resp Size(${response?.data?.toString()?.size()})" - Boolean guardSupported = false - try { - if(response?.status != 200) logWarn("${response?.status} checkGuardSupportResponse") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - Integer respLen = response?.data?.toString()?.length() ?: null - Map resp = response?.data ? parseJson(response?.data?.toString()) : null - if(resp && resp.networkDetail) { - Map details = parseJson(resp.networkDetail as String) - Map locDetails = (Map)details?.locationDetails?.locationDetails?.Default_Location?.amazonBridgeDetails?.amazonBridgeDetails["LambdaBridge_AAA/OnGuardSmartHomeBridgeService"] ?: null - if(locDetails && locDetails.applianceDetails && locDetails.applianceDetails.applianceDetails) { - def guardKey = locDetails.applianceDetails.applianceDetails.find { it?.key?.startsWith("AAA_OnGuardSmartHomeBridgeService_") } - // TODO could there be multiple Guards? - def guardKeys = locDetails.applianceDetails.applianceDetails.findAll { it?.key?.startsWith("AAA_OnGuardSmartHomeBridgeService_") } - if(devModeFLD) logTrace("Guardkeys: ${guardKeys.size()}") - def guardData = locDetails.applianceDetails.applianceDetails[(String)guardKey?.key] - if(devModeFLD) logTrace("Guard: ${guardData}") - if(guardData?.modelName == "REDROCK_GUARD_PANEL") { - //TODO: we really need to match guardData to devices (and really locations) ie guard can be on some devices/locations and not on others - state.alexaGuardData = [ - entityId: guardData?.entityId, - applianceId: guardData?.applianceId, - friendlyName: guardData?.friendlyName, - ] - guardSupported = true - } // else { logDebug("checkGuardSupportResponse | No Guard Data Received Must Not Be Enabled...") } - } - state.alexaGuardSupported = guardSupported - updTsVal("lastGuardSupChkDt") - state.alexaGuardDataSrc = "app" - if(guardSupported) getGuardState() - } else { logError("checkGuardSupportResponse Error | No data received...") } - } - } catch (ex) { - respExceptionHandler(ex, 'checkGuardSupportResponse', true) - } -} - -void checkGuardSupportFromServer() { - if(!isAuthValid("checkGuardSupportFromServer")) { return } - Map params = [ - uri: getServerHostURL(), - path: "/agsData", - requestContentType: sAPPJSON, - contentType: sAPPJSON, - timeout: 20, - ] - logTrace("checkGuardSupportFromServer") - execAsyncCmd("get", "checkGuardSupportServerResponse", params, [execDt: now()]) -} - -void checkGuardSupportServerResponse(response, data) { - Boolean guardSupported = false - try { - if(response?.status != 200) { - logWarn("checkGuardSupportServerResp: ${response?.status}") - } else { - Map resp = response?.data ? parseJson(response?.data?.toString()) : null - // log.debug "GuardSupport Server Response: ${resp}" - if(resp) { - if(resp.guardData) { - logDebug("AGS Server Resp: ${resp?.guardData}") - state.alexaGuardData = resp.guardData - guardSupported = true - } - } else { logError("checkGuardSupportServerResponse Error | No data received..."); return } - state.alexaGuardSupported = guardSupported - state.alexaGuardDataOverMaxSize = guardSupported - state.alexaGuardDataSrc = "server" - updTsVal("lastGuardSupChkDt") - if(guardSupported) getGuardState() - } - } catch (ex) { - respExceptionHandler(ex, "checkGuardSupportServerResponse", false, false) - } -} - -void getGuardState() { - String meth = "getGuardState" - if(!isAuthValid(meth)) { return } - if(!(Boolean)state.alexaGuardSupported) { logError("Alexa Guard is either not enabled. or not supported by any of your devices"); return } - Map params = [ - uri: getAmazonUrl(), - path: "/api/phoenix/state", - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - body: [ stateRequests: [ [entityId: state.alexaGuardData?.applianceId, entityType: "APPLIANCE" ] ] ] - ] - try { - logTrace(meth) - httpPostJson(params) { resp -> - if(resp?.status != 200) logWarn("${resp?.status} "+meth) - if(resp?.status == 200) { - updTsVal("lastSpokeToAmazon") - Map respData = resp?.data ?: null - - if(devModeFLD) log.debug "GuardState resp: ${respData}" - - if(respData && respData.deviceStates && ((List)respData.deviceStates)[0] && ((List)respData.deviceStates)[0].capabilityStates) { - def guardStateData = parseJson(((List)respData.deviceStates)[0].capabilityStates as String) - if(devModeFLD) logTrace("guardState: ${guardStateData}") - String curState = (String)state.alexaGuardState ?: sNULL - state.alexaGuardState = ((List)guardStateData?.value)[0] ? (String)((List)guardStateData?.value)[0] : (String)guardStateData?.value - settingUpdate("alexaGuardAwayToggle", (((String)state.alexaGuardState == sARM_AWAY) ? sTRUE : sFALSE), sBOOL) - logDebug("Alexa Guard State: (${(String)state.alexaGuardState})") - if(curState != (String)state.alexaGuardState) updGuardActionTrig() - updTsVal("lastGuardStateChkDt") - } - } - } - } catch (ex) { - respExceptionHandler(ex, meth, true) - } -} - -void setGuardState(String guardState) { - Long execTime = now() - String meth = "setGuardState" - if(!isAuthValid("setGuardState")) { return } - if(!(Boolean)state.alexaGuardSupported) { logError("Alexa Guard is either not enabled. or not supported by any of your devices"); return } - guardState = guardStateConv(guardState) - logDebug("setAlexaGuard($guardState)") - try { - String body = new JsonOutput()?.toJson([ controlRequests: [ [ entityId: state.alexaGuardData?.applianceId as String, entityType: "APPLIANCE", parameters: [action: "controlSecurityPanel", armState: guardState ] ] ] ]) - Map params = [ - uri: getAmazonUrl(), - path: "/api/phoenix/state", - headers: getReqHeaderMap(),//getCookieMap(), - contentType: sAPPJSON, - timeout: 20, - body: body - ] - logTrace(meth) - httpPutJson(params) { response -> - if(response?.status != 200) logWarn("${response?.status} $params $meth") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - def resp = response?.data ?: null - if(resp && !resp.errors?.size() && resp.controlResponses && ((List)resp.controlResponses)[0] && ((List)resp.controlResponses)[0].code && (String)((List)resp.controlResponses)[0].code == "SUCCESS") { - logInfo("Alexa Guard set to (${guardState}) Successfully | (${(now()-execTime)}ms)") - state.alexaGuardState = guardState - updTsVal("lastGuardStateUpdDt") - updGuardActionTrig() - } else { logError("Failed to set Alexa Guard to (${guardState}) | Reason: ${resp?.errors ?: null}") } - } - } - } catch (ex) { - respExceptionHandler(ex, meth, true) - } -} - -// private getAlexaSkills() { -// Long execDt = now() -// if(!isAuthValid("getAlexaSkills") || !getCustomerData()) { return } // state.amazonCustomerData -// if(state.skillDataMap && getLastTsValSecs("skillDataUpdDt") < 3600) { return } -// Map params = [ -// uri: "https://skills-store.${getAmazonDomain()}", -// path: "/app/secure/your-skills-page?deviceType=app&ref-suffix=evt_sv_ub&pfm=${state.amazonCustomerData?.marketPlaceId}&cor=US&lang=en-us&_=${now()}", -// headers: [ -// Accept: "application/vnd+amazon.uitoolkit+json;ns=1;fl=0", -// Origin: getAmazonUrl() -// ] + getReqHeaderMap(true), -// contentType: sAPPJSON, -// timeout: 20, -// ] -// try { -// logTrace("getAlexaSkills") -// httpGet(params) { response-> -// if(response?.status != 200) logWarn("${response?.status} $params") -// if(response?.status == 200) { -// updTsVal("lastSpokeToAmazon") -// def respData = response?.data ?: null -// log.debug "respData: $respData" -// // log.debug respData[3]?.contents[3]?.contents?.products - -// // updTsVal("skillDataUpdDt") -// } -// } -// } catch (ex) { -// log.error "getAlexaSkills Exception: ${ex}" -// // respExceptionHandler(ex, "getAlexaSkills", true) -// } -// } - -void respExceptionHandler(ex, String mName, Boolean ignOn401=false, Boolean toAmazon=true, Boolean ignNullMsg=false) { - String toMsg = "Amazon" - if(!toAmazon) { toMsg = "Echo Speaks Server" } - String stackTr - if(ex) { - try { - stackTr = getStackTrace(ex) - } catch (ignored) { - } - if(stackTr) logError("${mName} | Stack Trace: "+stackTr) - } - if(ex instanceof groovyx.net.http.HttpResponseException ) { - Integer sCode = ex?.getResponse()?.getStatus() - String errMsg = ex?.getMessage() - if(sCode == 401) { - if(ignOn401) authValidationEvent(false, "${mName}_${sCode}") - } else if (sCode in [400, 429]) { - String respMsgLow = errMsg ? errMsg.toLowerCase() : sNULL - if((sCode in [400, 429]) && respMsgLow) { // && (respMsgLow in ["rate exceeded", "too many requests"])) { - switch(respMsgLow) { - case "rate exceeded": - 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) - break - case "too many requests": - logError("${mName} | ${toMsg} is currently rate-limiting your requests | Msg: ${errMsg}") - break - case "bad request": - logError("${mName} | Improperly formatted request sent to ${toMsg} | Msg: ${errMsg}") - break - default: - logError("${mName} | 400 Error | Msg: ${errMsg}") - break - } - } - } else { - logError("${mName} | Response Exception | Status: (${sCode}) | Msg: ${errMsg}") - } - } else if(ex instanceof java.net.SocketTimeoutException) { - logError("${mName} | Response Socket Timeout (Possibly an ${toMsg} Issue) | Msg: ${ex?.getMessage()}") - } else if(ex instanceof org.apache.http.conn.ConnectTimeoutException) { - logError("${mName} | Request Timeout | Msg: ${ex?.getMessage()}") - } else if(ex instanceof java.net.UnknownHostException) { - logError("${mName} | HostName Not Found (Possibly an ${toMsg}/Internet Issue) | Msg: ${ex?.getMessage()}") - } else if(ex instanceof java.net.NoRouteToHostException) { - logError("${mName} | No Route to Connection (Possibly a Local Internet Issue) | Msg: ${ex?.getMessage()}") - } else { logError("${mName} Exception: ${ex}") } -} - -@SuppressWarnings('GroovyFallthrough') -static String guardStateConv(String gState) { - switch(gState) { - case "disarm": - case "off": - case "stay": - case "home": - case sARM_STAY: - return sARM_STAY - case "away": - case sARM_AWAY: - return sARM_AWAY - default: - return sARM_STAY - } -} - -String getAlexaGuardStatus() { - return (String)state.alexaGuardState ?: sNULL -} - -Boolean getAlexaGuardSupported() { - return (Boolean)state.alexaGuardSupported -} - -public void updGuardActionTrig() { - List acts = getActionApps() - if(acts?.size()) { acts?.each { aa-> aa?.guardEventHandler((String)state.alexaGuardState) } } -} - -public setGuardHome() { - setGuardState(sARM_STAY) -} - -public setGuardAway() { - setGuardState(sARM_AWAY) - guardArmPendingFLD = false -} - -Map isFamilyAllowed(String family) { - Map famMap = getDeviceFamilyMap() - if(family in famMap?.block) { return [ok: false, reason: "Family Blocked"] } - if(family in famMap?.echo) { return [ok: true, reason: "Amazon Echos Allowed"] } - if(family in famMap?.tablet) { - if((Boolean)settings.createTablets) { return [ok: true, reason: "Tablets Enabled"] } - return [ok: false, reason: "Tablets Not Enabled"] - } - if(family in famMap?.wha) { - if((Boolean)settings.createWHA) { return [ok: true, reason: "WHA Enabled"] } - return [ok: false, reason: "WHA Devices Not Enabled"] - } - if((Boolean)settings.createOtherDevices) { - return [ok: true, reason: "Other Devices Enabled"] - } else { return [ok: false, reason: "Other Devices Not Enabled"] } -// return [ok: false, reason: "Unknown Reason"] -} - -void getEchoDevices1() { - getEchoDevices() -} - -void getEchoDevices(Boolean lazy=false) { - stateMigrationChk() - if(!isAuthValid("getEchoDevices")) { return } - String myId=app.getId() - Integer lastUpd = getLastTsValSecs("lastDevDataUpdDt") - if(echoDeviceMapFLD[myId] && (!lazy && lastUpd < 20) || (lazy && lastUpd <= 600)) { return } - Map params = [ - uri: getAmazonUrl(), - path: "/api/devices-v2/device", - query: [ cached: true, _: new Date().getTime() ], - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - ] - state.deviceRefreshInProgress = true -// state.refreshDeviceData = false - logTrace("getEchoDevices") - execAsyncCmd("get", "echoDevicesResponse", params, [execDt: now()]) -} - -void echoDevicesResponse(response, data) { - List ignoreTypes = getDeviceIgnoreData() - List removeKeys = ["appDeviceList", "charging", "macAddress", "deviceTypeFriendlyName", "registrationId", "remainingBatteryLevel", "postalCode", "language"] - List removeCaps = [ - "SUPPORTS_CONNECTED_HOME", "SUPPORTS_CONNECTED_HOME_ALL", "SUPPORTS_CONNECTED_HOME_CLOUD_ONLY", "ALLOW_LOG_UPLOAD", "FACTORY_RESET_DEVICE", "DIALOG_INTERFACE_VERSION", - "SUPPORTS_SOFTWARE_VERSION", "REQUIRES_OOBE_FOR_SETUP", "DEREGISTER DEVICE", "PAIR_REMOTE", "SET_LOCALE", "DEREGISTER_FACTORY_RESET" - ] - try { - if(response?.status != 200) logWarn("${response?.status} $data") - if(response?.status == 200) { - updTsVal("lastSpokeToAmazon") - // log.debug "json response is: ${response.json}" - state.deviceRefreshInProgress=false - List eDevData = response?.json?.devices ?: [] - Map echoDevices = [:] - if(eDevData.size()) { - eDevData.each { eDevice-> - if (!((String)eDevice.deviceType in ignoreTypes) && !((String)eDevice.accountName)?.startsWith("This Device")) { - removeKeys.each { rk-> eDevice.remove(rk) } - eDevice.capabilities = eDevice.capabilities?.findAll { !(it in removeCaps) }?.collect { it as String } - //if (eDevice.deviceOwnerCustomerId != null) { state.deviceOwnerCustomerId = eDevice.deviceOwnerCustomerId } - echoDevices[(String)eDevice.serialNumber] = eDevice - } - } - } - // log.debug "echoDevices: ${echoDevices}" - Map musicProvs = (Map)state.musicProviders ?: getMusicProviders(true) - receiveEventData([echoDevices: echoDevices, musicProviders: musicProvs, execDt: data?.execDt], "Groovy") - } - - } catch (ex) { - respExceptionHandler(ex, "echoDevicesResponse") - } -} - -private static List getDeviceIgnoreData() { - Map dData = (Map)deviceSupportMapFLD.types - if(dData.size()) { - List o = dData.findAll { it.value.ig == true }?.collect { it.key } - // log.debug "devTypeIgnoreData: ${o}" - return o - } - return [] -} - -List getUnknownDevices() { - List items = [] - ((List)state.unknownDevices)?.each { Map it-> - it.description = "What kind of device/model?(PLEASE UPDATE THIS)" - if(items.size() < 5) items.push(it) - } - return items -} - -void receiveEventData(Map evtData, String src) { - try { - if(checkIfCodeUpdated()) { - logWarn("Possible Code Version Change Detected... Device Updates will occur on next cycle.") - return - } - // log.debug "musicProviders: ${evtData?.musicProviders}" - logTrace("evtData(Keys): ${evtData?.keySet()}") - if (evtData?.keySet()?.size()) { - //List ignoreTheseDevs = settings.echoDeviceFilter ?: [] - //Boolean onHeroku = ((Boolean)getServerItem("onHeroku") && !(Boolean)getServerItem("isLocal")) - - //Check for minimum versions before processing - Map updReqMap = getMinVerUpdsRequired() - Boolean updRequired = updReqMap?.updRequired - List updRequiredItems = updReqMap?.updItems - - String myId=app.getId() - String wsChildHandlerName = "Echo Speaks WS" - String nmS = 'echoSpeaks_websocket' - // def oldWsDev = getChildDevice(nmS) - // if(oldWsDev) { deleteChildDevice(nmS) } - nmS = myId+'|'+nmS - def wsDevice = getChildDevice(nmS) - if(wsDevice) { deleteChildDevice(nmS) } - // if(!wsDevice) { def a=addChildDevice("tonesto7", wsChildHandlerName, nmS, null, [name: wsChildHandlerName, label: "Echo Speaks - WebSocket", completedSetup: true]) } - // updCodeVerMap("echoDeviceWs", (String)wsDevice?.devVersion()) - - if (evtData?.echoDevices?.size()) { - Long execTime = evtData?.execDt ? (now()-(Long)evtData.execDt) : 0L - Map echoDeviceMap = [:] - Map allEchoDevices = [:] - Map skippedDevices = [:] - List unknownDevices = [] - List curDevFamily = [] -// Integer cnt = 0 - String devAcctId = sNULL - evtData?.echoDevices?.each { String echoKey, echoValue-> - devAcctId = echoValue?.deviceAccountId - logTrace("echoDevice | $echoKey | ${echoValue}") - // logDebug("echoDevice | ${echoValue?.accountName}", false) - allEchoDevices[echoKey] = [name: echoValue?.accountName] - // log.debug "name: ${echoValue?.accountName}" - Map familyAllowed = isFamilyAllowed(echoValue?.deviceFamily as String) - Map deviceStyleData = getDeviceStyle(echoValue?.deviceFamily as String, echoValue?.deviceType as String) - // log.debug "deviceStyle: ${deviceStyleData}" - Boolean isBlocked = ((Boolean)deviceStyleData?.b || (String)familyAllowed?.reason == "Family Blocked") - Boolean isInIgnoreInput = (echoValue?.serialNumber in settings.echoDeviceFilter) - Boolean allowTTS = (deviceStyleData?.c && ((List)deviceStyleData.c)?.contains(sTSTR)) - Boolean isMediaPlayer = (echoValue?.capabilities?.contains("AUDIO_PLAYER") || echoValue?.capabilities?.contains("AMAZON_MUSIC") || echoValue?.capabilities?.contains("TUNE_IN") || echoValue?.capabilities?.contains("PANDORA") || echoValue?.capabilities?.contains("I_HEART_RADIO") || echoValue?.capabilities?.contains("SPOTIFY")) - Boolean volumeSupport = (echoValue?.capabilities?.contains("VOLUME_SETTING")) - Boolean unsupportedDevice = ((!(Boolean)familyAllowed?.ok && (String)familyAllowed?.reason == "Unknown Reason") || isBlocked) - Boolean bypassBlock = ((Boolean)settings.bypassDeviceBlocks && !isInIgnoreInput) - - if(!bypassBlock && (!(Boolean)familyAllowed?.ok || isBlocked || (!allowTTS && !isMediaPlayer) || isInIgnoreInput)) { - logTrace("familyAllowed(${echoValue?.deviceFamily}): ${(Boolean)familyAllowed?.ok} | Reason: ${(String)familyAllowed?.reason} | isBlocked: ${isBlocked} | deviceType: ${echoValue?.deviceType} | tts: ${allowTTS} | volume: ${volumeSupport} | mediaPlayer: ${isMediaPlayer}") - if(!skippedDevices.containsKey(echoValue?.serialNumber as String)) { - List reasons = [] - if((Boolean)deviceStyleData?.b) { - reasons.push("Device Blocked by App Config") - } else if((String)familyAllowed?.reason == "Family Blocked") { - reasons.push("Family Blocked by App Config") - } else if (!(Boolean)familyAllowed?.ok) { - reasons.push((String)familyAllowed?.reason) - } else if(isInIgnoreInput) { - reasons.push(sIN_IGNORE) - logDebug("skipping ${echoValue?.accountName} because it is in the do not use list...") - } else { - if(!allowTTS) { reasons.push("No TTS") } - if(!isMediaPlayer) { reasons.push("No Media Controls") } - } - skippedDevices[echoValue?.serialNumber as String] = [ - name: echoValue?.accountName, desc: (String)deviceStyleData?.n, image: (String)deviceStyleData?.i, family: echoValue?.deviceFamily, - type: echoValue?.deviceType, tts: allowTTS, volume: volumeSupport, mediaPlayer: isMediaPlayer, reason: reasons?.join(", "), - online: echoValue?.online - ] - } - return - } - // if(isBypassBlock && familyAllowed?.reason == "Family Blocked" || isBlocked == true) { return } - - echoValue["unsupported"] = (unsupportedDevice) - echoValue["authValid"] = (Boolean)state.authValid - echoValue["amazonDomain"] = (settings.amazonDomain ?: "amazon.com") - echoValue["regionLocale"] = (settings.regionLocale ?: "en-US") - echoValue["cookie"] = getCookieMap() - echoValue["deviceAccountId"] = echoValue?.deviceAccountId as String ?: sNULL - echoValue["deviceStyle"] = deviceStyleData - // log.debug "deviceStyle: ${echoValue?.deviceStyle}" - - Map permissions = [:] - permissions["TTS"] = allowTTS - permissions["announce"] = (deviceStyleData?.c && ((List)deviceStyleData.c)?.contains(sASTR)) - permissions["volumeControl"] = volumeSupport - permissions["mediaPlayer"] = isMediaPlayer - permissions["amazonMusic"] = (echoValue.capabilities?.contains("AMAZON_MUSIC")) - permissions["tuneInRadio"] = (echoValue.capabilities?.contains("TUNE_IN")) - permissions["iHeartRadio"] = (echoValue.capabilities?.contains("I_HEART_RADIO")) - permissions["pandoraRadio"] = (echoValue.capabilities?.contains("PANDORA")) - permissions["appleMusic"] = (((Map)evtData.musicProviders).containsKey("APPLE_MUSIC")) - permissions["siriusXm"] = (((Map)evtData.musicProviders).containsKey("SIRIUSXM")) - // permissions["tidal"] = true - permissions["spotify"] = true //(echoValue?.capabilities.contains("SPOTIFY")) // Temporarily removed restriction check - permissions["isMultiroomDevice"] = (echoValue.clusterMembers && echoValue.clusterMembers?.size() > 0) ?: false - permissions["isMultiroomMember"] = (echoValue.parentClusters && echoValue.parentClusters?.size() > 0) ?: false - permissions["alarms"] = (echoValue.capabilities?.contains("TIMERS_AND_ALARMS")) - permissions["reminders"] = (echoValue.capabilities?.contains("REMINDERS")) - permissions["doNotDisturb"] = (echoValue.capabilities?.contains("SLEEP")) - permissions["wakeWord"] = (echoValue.capabilities?.contains("FAR_FIELD_WAKE_WORD")) - permissions["flashBriefing"] = (echoValue.capabilities?.contains("FLASH_BRIEFING")) - permissions["microphone"] = (echoValue.capabilities?.contains("MICROPHONE")) - permissions["followUpMode"] = (echoValue.capabilities?.contains("GOLDFISH")) - permissions["connectedHome"] = (echoValue.capabilities?.contains("SUPPORTS_CONNECTED_HOME")) - permissions["bluetoothControl"] = (echoValue.capabilities?.contains("PAIR_BT_SOURCE") || echoValue.capabilities?.contains("PAIR_BT_SINK")) - permissions["guardSupported"] = (echoValue.capabilities?.contains("TUPLE")) - permissions["isEchoDevice"] = (echoValue.deviceFamily in (List)deviceSupportMapFLD.families.echo) - echoValue["guardStatus"] = ((Boolean)state.alexaGuardSupported && (String)state.alexaGuardState) ? (String)state.alexaGuardState : ((Boolean)permissions.guardSupported ? sUnknown : "Not Supported") - echoValue["musicProviders"] = (Map)evtData.musicProviders - echoValue["permissionMap"] = permissions - echoValue["hasClusterMembers"] = (echoValue.clusterMembers && echoValue.clusterMembers?.size() > 0) ?: false - - if(deviceStyleData?.n?.toString()?.toLowerCase()?.contains(sUNKNOWN)) { - unknownDevices.push([ - name: echoValue.accountName, - family: echoValue.deviceFamily, - type: echoValue.deviceType, - permissions: permissions.findAll {it?.value == true}?.collect {it?.key as String}?.join(", ")?.toString() - ]) - } - // echoValue["mainAccountCommsId"] = state.accountCommIds?.find { it?.value?.signedInUser == true && it?.value?.isChild == false }?.key as String ?: null - // logWarn("Device Permisions | Name: ${echoValue?.accountName} | $permissions") - echoDeviceMap[echoKey] = [ - name: echoValue.accountName, - online: echoValue.online, - family: echoValue.deviceFamily, - serialNumber: echoKey, - style: echoValue.deviceStyle, - type: echoValue.deviceType, - mediaPlayer: isMediaPlayer, - announceSupport: (Boolean)permissions.announce, - ttsSupport: allowTTS, - volumeSupport: volumeSupport, - clusterMembers: echoValue.clusterMembers, - musicProviders: ((Map)evtData.musicProviders)?.collect{ it?.value }?.sort()?.join(", "), - supported: (!unsupportedDevice) - ] - - String dni = [app?.id, "echoSpeaks", echoKey].join("|") - def childDevice = getChildDevice(dni) - String devLabel = "${(Boolean)settings.addEchoNamePrefix ? "Echo - " : sBLANK}${echoValue?.accountName}${echoValue?.deviceFamily == "WHA" ? " (WHA)" : sBLANK}" - String childHandlerName = "Echo Speaks Device" - Boolean autoRename = ((Boolean)settings.autoRenameDevices) - if (!childDevice) { - // log.debug "childDevice not found | autoCreateDevices: ${settings.autoCreateDevices}" - if((Boolean)settings.autoCreateDevices) { - try{ - logInfo("Creating NEW Echo Speaks Device!!! | Device Label: ($devLabel)${((Boolean)settings.bypassDeviceBlocks && unsupportedDevice) ? " | (UNSUPPORTED DEVICE)" : sBLANK }") - childDevice = addChildDevice("tonesto7", childHandlerName, dni, null, [name: childHandlerName, label: devLabel, completedSetup: true]) - } catch(ex) { - logError("AddDevice Error! | ${ex}", false, ex) - } - runIn(10, "updChildSocketStatus") - } else { - logInfo("Found NEW Echo Speaks Device, but not creating HE device due to settings | Device Label: ($devLabel)${((Boolean)settings.bypassDeviceBlocks && unsupportedDevice) ? " | (UNSUPPORTED DEVICE)" : sBLANK}") - } - } - if(childDevice) { - //Check and see if name needs a refresh - String curLbl = (String)childDevice?.getLabel() - if(autoRename && (String)childDevice?.name != childHandlerName) { childDevice.name = childHandlerName } - // log.debug "curLbl: ${curLbl} | newLbl: ${devLabel} | autoRename: ${autoRename}" - if(autoRename && (curLbl != devLabel)) { - logDebug("Amazon Device Name Change Detected... Updating Device Name to (${devLabel}) | Old Name: (${curLbl})") - childDevice?.setLabel(devLabel as String) - } - // logInfo("Sending Device Data Update to ${devLabel} | Last Updated (${getLastTsValSecs("lastDevDataUpdDt")}sec ago)") - childDevice?.updateDeviceStatus(echoValue) - updCodeVerMap("echoDevice", (String)childDevice?.devVersion()) // Update device versions in codeVersions state Map - } - curDevFamily.push((String)echoValue.deviceStyle?.n) - } - logDebug("Device Data Received and Updated for (${echoDeviceMap?.size()}) Alexa Devices | Took: (${execTime}ms) | Last Refreshed: (${(getLastTsValSecs("lastDevDataUpdDt")/60).toFloat()?.round(1)} minutes)") - updTsVal("lastDevDataUpdDt") - state.echoDeviceMap = echoDeviceMap - echoDeviceMapFLD[myId] = echoDeviceMap - echoDeviceMapFLD = echoDeviceMapFLD - state.allEchoDevices = allEchoDevices - state.skippedDevices = skippedDevices - state.deviceStyleCnts = curDevFamily.countBy { it } - state.unknownDevices = unknownDevices - } else { - log.warn "No Echo Device Data Sent... This may be the first transmission from the service after it started up!" - } - state.remove("tempDevSupData") - if(updRequired) { - logWarn("CODE UPDATES REQUIRED: Echo Speaks Integration may not function until the following items are ALL Updated ${updRequiredItems}...") - appUpdateNotify() - } - if(!(Boolean)getInstData('sentMetrics')) { runIn(900, "sendInstallData", [overwrite: false]) } - } - } catch(ex) { - logError("receiveEventData Error: ${ex}", false, ex) - incrementCntByKey("appErrorCnt") - } -} - -Map getEchoDeviceMap(){ - String myId=app.getId() - if(!echoDeviceMapFLD[myId]) { - echoDeviceMapFLD[myId] = (Map)state.echoDeviceMap - echoDeviceMapFLD = echoDeviceMapFLD - } - return echoDeviceMapFLD[myId] -} - -public static Map minVersions() { - return minVersionsFLD -} - -private Map getMinVerUpdsRequired() { - Boolean updRequired = false - List updItems = [] - Map codeItems = [server: "Echo Speaks Server", echoDevice: "Echo Speaks Device", wsDevice: "Echo Speaks Websocket", actionApp: "Echo Speaks Actions", zoneApp: "Echo Speaks Zones", zoneEchoDevice: "Echo Speaks Zone Device"] - Map codeVers = (Map)state.codeVersions ?: [:] - codeVers.each { String k,String v-> - if(codeItems.containsKey(k) && v != sNULL && (versionStr2Int(v) < minVersionsFLD[k])) { updRequired = true; updItems.push(codeItems[k]) } - } - return [updRequired: updRequired, updItems: updItems] -} - -static Map getDeviceStyle(String family, String type) { - Map typeData = (Map)deviceSupportMapFLD.types[type] ?: [:] - if(typeData) { - return typeData - } else { return [n: "Echo Unknown "+type, i: sUNKNOWN ] } -} - -public Map getDeviceFamilyMap() { - if(!state.appData || !state.appData.deviceFamilies) { checkVersionData(true) } - return (Map)state.appData?.deviceFamilies ?: (Map)deviceSupportMapFLD.families -} - -List getDevicesFromSerialList(List serialList) { - //logTrace("getDevicesFromSerialList called with: ${serialList}") - if (!serialList) { - logDebug("Serial List is empty") - return null - } - List devs = [] - serialList.each { String ser -> - def d = findEchoDevice(ser) - if(d) devs.push(d) - } - //log.debug "Device list: ${devs}" - return devs -} - -// This is called by the (WHA) device handler to send playback data to cluster members -public void sendPlaybackStateToClusterMembers(String whaKey, data) { - //logTrace("sendPlaybackStateToClusterMembers: key: ${ whaKey}") - try { - Map echoDeviceMap = getEchoDeviceMap() //state.echoDeviceMap - Map whaMap = echoDeviceMap[whaKey] - List clusterMembers = (List)whaMap?.clusterMembers - - if (clusterMembers) { - List clusterMemberDevices = getDevicesFromSerialList(clusterMembers) - if(clusterMemberDevices) { - clusterMemberDevices?.each { it?.playbackStateHandler(data, true) } - } - } else { - // The lookup will fail during initial refresh because echoDeviceMap isn't available yet - //log.debug "sendPlaybackStateToClusterMembers: no data found for ${whaKey} (first refresh?)" - } - } catch (ex) { - log.error "sendPlaybackStateToClusterMembers Error: ${ex}" - } -} - -void removeDevices(Boolean all=false) { - try { - settingUpdate("cleanUpDevices", sFALSE, sBOOL) - List devList = getDeviceList(true)?.collect { (String)[app?.id, "echoSpeaks", it?.key].join("|") } - List items = ((List)app.getChildDevices())?.findResults { (all || (!all && !devList?.contains(it?.deviceNetworkId as String))) ? it?.deviceNetworkId as String : sNULL } - logWarn("removeDevices(${all ? "all" : sBLANK}) | In Use: (${all ? 0 : devList.size()}) | Removing: (${items.size()})", true) - if(items.size() > 0) { - items.each { String it -> deleteChildDevice(it) } - } - } catch (ex) { logError("Device Removal Failed: ${ex}", false, ex) } -} - -void execAsyncCmd(String method, String callbackHandler, Map params, Map otherData = null) { - if(method && callbackHandler && params) { - String m = method?.toString()?.toLowerCase() - "asynchttp${m?.capitalize()}"("${callbackHandler}", params, otherData) - } else { logError("execAsyncCmd Error | Missing a required parameter") } -} - -void sendAmazonCommand(String method, Map params, Map otherData=null) { - String meth = "sendAmazonCommand ${method} ${params} ${otherData?.cmdDesc}" - try { - def rData = null - def rStatus = null - logTrace(meth) - switch(method) { - case "POST": - httpPostJson(params) { response-> - rStatus = response?.status - if(rStatus == 200) { rData = response?.data ?: null } - } - break - case "PUT": - if(params?.body) { params?.body = new JsonOutput().toJson(params?.body) } - httpPutJson(params) { response-> - rStatus = response?.status - if(rStatus == 200) { rData = response?.data ?: null } - } - break - case "DELETE": - httpDelete(params) { response-> - rStatus = response?.status - if(rStatus == 200) { rData = response?.data ?: null } - } - break - } - if(rStatus == 200) { - updTsVal("lastSpokeToAmazon") - logDebug("${meth} | Status: (${rStatus})${rData != null ? " | Response: ${rData}" : sBLANK} | ${otherData?.cmdDesc} was Successfully Sent!!!") - } else logWarn("${meth} | Status: ${rStatus} FAILED") - } catch (ex) { - respExceptionHandler(ex, meth, true) - } -} - -/* - * send speak command to one zone - * caller is vdevice handler via relay from zone app; this will callback the actual device(s) with status - */ -void sendZoneSpeak(String zoneId, String msg, Boolean parallel=false, Boolean bypassDoNotDisturb=false) { - List devObj = getZoneDevices([zoneId], "TTS", bypassDoNotDisturb) - String myMsg = "sendZoneSpeak" - devObj.each { dev -> - Map cmdMap = [ - cmdDt: now(), - cmdDesc: "SpeakCommand", - message: msg, - //msgLen: newmsg.length(), - oldVolume: null, - newVolume: null - ] - Map deviceData = [ - serialNumber : dev.deviceSerialNumber, - deviceType: dev.deviceTypeId, - owner: dev.deviceOwnerCustomerId, - account: dev.deviceAccountId - ] - - queueMultiSequenceCommand( - [ [command: 'sendspeak', value:msg, deviceData: deviceData] ], - myMsg+" to device ${dev.dni}", parallel, cmdMap, (String)dev.dni, "finishSendSpeakZ") - } -} - -/* - * send announce command to one zone - * caller is vdevice handler via relay from zone app; this will callback the actual device(s) with status - */ -void sendZoneAnnounce(String zoneId, String msg, Boolean parallel=false, Boolean bypassDoNotDisturb=false) { - List devObj = getZoneDevices([zoneId], "announce", bypassDoNotDisturb) - String myMsg = "sendZoneAnnounce" - devObj.each { dev -> - Map deviceData = [ - serialNumber : dev.deviceSerialNumber, - deviceType: dev.deviceTypeId, - owner: dev.deviceOwnerCustomerId, - account: dev.deviceAccountId - ] - - queueMultiSequenceCommand( - [ [command: 'announcement', value:msg, deviceData: deviceData] ], - myMsg+" to device ${dev.dni}", parallel) - } - devObj.each { dev-> - def child = getChildDevice((String)dev.dni) - child.finishAnnounce(msg, null, null) - } -} - -/* - * send command to one or more zones (actually devices in one or more zones) - * caller is actions; operations are speak or announcement commands - */ -void sendZoneCmd(Map cmdData) { - logTrace(span("sendZoneCmd | cmdData: $cmdData", "purple")) - String myCmd = cmdData ? (String)cmdData.cmd : sNULL - List znList = (List)cmdData.zones - if(myCmd && znList && znList.size()) { - List devObj = getZoneDevices(znList, myCmd in ['speak', 'speak_parallel'] ? "TTS" : "announce") - - String newmsg = (String)cmdData.message - String title = (String)cmdData.title - Integer volume = (Integer)cmdData.changeVol - Integer restoreVolume = (Integer)cmdData.restoreVol - sendDevObjCmd(devObj, myCmd, title, newmsg, volume, restoreVolume) - - /* need to call zone vdevice with updates to finishAnnouncement or finishSpeak */ - znList.each { znId -> - def zn = getZoneApps()?.find { it.id.toString() == znId.toString() } - if(zn) { - if(myCmd == 'announcement') { - newmsg = "${title ?: "Echo Speaks"}::${newmsg}".toString() - zn.relayFinishAnnouncement(newmsg, [vol:volume, restvol:restoreVolume]) - } - if(myCmd in ['speak', 'speak_parallel']) { - zn.relayFinishSpeak([:], 200, cmdData) - } - } - } - } -} - -/* - * send a speak or announcement command to a list of devices - * caller is actions (here or via above) when there is a list of devices - * this will callback the actual device(s) with status for attribute updates - */ -void sendDevObjCmd(List odevObj, String myCmd, String title, String newmsg, Integer volume, Integer restoreVolume){ - List devObj = odevObj.unique() // remove any duplicate devices - String origMsg = newmsg - if(devObj.size() == 0) { - logWarn("sendDevObjCmd NO DEVICES | cmd: $myCmd | devObj: $devObj | msg: ${newmsg} title: $title | volume: $volume | restoreVolume: $restoreVolume") - return - } - //noinspection GroovyFallthrough - switch(myCmd) { - case "announcement": - String zoneDevJson = devObj.size() ? new JsonOutput().toJson(devObj) : sNULL - newmsg = "${title ?: "Echo Speaks"}::${newmsg}::${zoneDevJson}" - case "speak": - case "speak_parallel": - logDebug("sendDevObjCmd | cmd: $myCmd | devObj: $devObj | msg: ${newmsg} title: $title | volume: $volume | restoreVolume: $restoreVolume") - String myMsg = "sendDevObjCmd ${myCmd}" - if(volume != null) { - List mainSeqa = [] - devObj.each { dev-> - Map deviceData = [ - serialNumber : dev.deviceSerialNumber, - deviceType: dev.deviceTypeId, - owner: dev.deviceOwnerCustomerId, - account: dev.deviceAccountId - ] - mainSeqa.push([command: "volume", value: volume, deviceData: deviceData]) - } - queueMultiSequenceCommand(mainSeqa, myMsg+"-VolumeSet") - } - List mainSeq = [] - if(myCmd in ['speak', 'speak_parallel']) { - Boolean para = myCmd == 'speak_parallel' - devObj.each { dev-> - //mainSeq.push([command: 'sendspeak', value:cmdData.message, devType: dev.deviceTypeId, devSerial: dev.deviceSerialNumber]) - Map cmdMap = [ - cmdDt: now(), - cmdDesc: !para ? "SpeakCommand" : "SpeakParallel", - message: newmsg, - msgLen: newmsg.length(), - oldVolume: restoreVolume, - newVolume: volume - ] - Map deviceData = [ - serialNumber : dev.deviceSerialNumber, - deviceType: dev.deviceTypeId, - owner: dev.deviceOwnerCustomerId, - account: dev.deviceAccountId - ] - - queueMultiSequenceCommand( - [ [command: 'sendspeak', value:newmsg, deviceData: deviceData] ], - myMsg+" to device ${dev.dni}", para, cmdMap, (String)dev.dni, "finishSendSpeakZ") - } - if(para) queueNopCommand() - } else if (myCmd == 'announcement') { - Map myDev = devObj[0] - Map deviceData = [ - serialNumber : myDev.deviceSerialNumber, - deviceType: myDev.deviceTypeId, - owner: myDev.deviceOwnerCustomerId, - account: myDev.deviceAccountId - ] - mainSeq.push([command: "announcement_devices", value: newmsg, deviceData: deviceData, cmdType: 'playAnnouncement']) - queueMultiSequenceCommand(mainSeq, myMsg) - devObj.each { dev-> - def child = getChildDevice((String)dev.dni) - child.finishAnnounce(origMsg, volume, restoreVolume) - } - } - - if(restoreVolume!=null) { - List amainSeq = [] - devObj.each { dev-> - Map deviceData = [ - serialNumber : dev.deviceSerialNumber, - deviceType: dev.deviceTypeId, - owner: dev.deviceOwnerCustomerId, - account: dev.deviceAccountId - ] - amainSeq.push([command: "volume", value: restoreVolume, deviceData: deviceData]) - } - queueMultiSequenceCommand(amainSeq, myMsg+"-VolumeRestore") - } -// log.debug "mainSeq: $mainSeq" - - break - } -} - -private List getZoneDevices(List znList, String cmd, Boolean chkDnd=false) { - // logTrace("getZoneDevices | $znList") - List devObjs = [] - if(znList && znList.size()) { - znList.each { znId -> - // log.debug "znId: $znId" - Map znData = getZoneState(znId.toString()) - // log.trace "znData: $znData" - if(znData && znData.zoneDevices) { - List devices = getDevicesFromList((List)znData.zoneDevices) - //devices?.each { devObjs?.push([deviceTypeId: it?.getEchoDeviceType() as String, deviceSerialNumber: it?.getEchoSerial() as String]) } - devices?.each { - Map devInfo = it?.getEchoDevInfo(cmd, true) // ignores dnd setting in device - if(devInfo) { - Boolean dnd = chkDnd ? getDndEnabled((String)devInfo.deviceSerialNumber) : false - if(!dnd) devObjs?.push(devInfo) -// if(!dnd) devObjs?.push([deviceTypeId: devInfo.deviceTypeId, deviceSerialNumber: devInfo.deviceSerialNumber, deviceOwnerCustomerId: devInfo.deviceOwnerCustomerId, deviceAccountId: devInfo.deviceAccountId, dni: devInfo.dni]) - } - } - } - } - } - //return (String) (devObjs.size()) ? new groovy.json.JsonOutput().toJson(devObjs) : sNULL - return devObjs -} - -private Map getZoneState(String znId) { - Map zones = getZones() - if(zones) { - return (Map)zones[znId] - } - return null -} - -/* - * called by drivers to queue speak (and possible volume changes) - * will setup to call back the device handler when the command completes with status - */ - -void sendSpeak(Map cmdMap, Map deviceData, String device, String callback, Boolean parallel=false){ - String nm = cmdMap.toString().replaceAll('<', '<').replaceAll('>', '>') - logTrace("sendSpeak cmdMap: $nm callback: $callback, device: $device") - -/* Map deviceData = [ - serialNumber : cmdMap.serialNumber, - deviceType: cmdMap.deviceType, - owner: cmdMap.owner, - account: cmdMap.account - ] */ -// Map st = [serialNumber: cmdMap.serialNumber, deviceType: cmdMap.deviceType] - List seqCmds = [] - if(cmdMap.newVolume) { seqCmds.push([command: "volume", value: cmdMap.newVolume, deviceData: deviceData]) } - seqCmds.push([command: 'sendspeak', value:cmdMap.message, deviceData:deviceData]) - if(cmdMap.oldVolume) { seqCmds.push([command: "volume", value: cmdMap.oldVolume, deviceData: deviceData]) } - - queueMultiSequenceCommand(seqCmds, "sendSpeak from $device", parallel, cmdMap, device, callback) -} - -void queueNopCommand(){ - Map item= [ - t: 'nop', - time: now(), - device: null, - callback: null - ] - addToQ(item) - -} - -void queueSequenceCommand(String type, String command, value, Map deviceData=[:], String device=sNULL, String callback=sNULL){ - Map item= [ - t: 'sequence', - time: now(), - type: type, - command: command, - value: value, - deviceData: deviceData, - device: device, - callback: callback - ] - addToQ(item) -} - -void queueMultiSequenceCommand(List commands, String srcDesc, Boolean parallel=false, Map cmdMap=[:], String device=sNULL, String callback=sNULL) { -//log.warn "commands: $commands srcDesc: $srcDesc parallel: $parallel cmdMap: $cmdMap device: $device" -// expand speak commands and handle ssml - List newCmds = [] - List seqCmds = commands - seqCmds?.each { cmdItem-> - // log.debug "cmdItem: $cmdItem" - if(cmdItem.command instanceof String){ - if((String)cmdItem.command in ['sendspeak']){ - Map deviceData = (Map)cmdItem.deviceData -// Map st = cmdItem.devType ? [serialNumber: cmdItem.devSerial, deviceType: cmdItem.devType] : deviceData - newCmds = newCmds + msgSeqBuilder((String)cmdItem.value, deviceData, 'sendSpeak') - } else newCmds.push(cmdItem) - } else newCmds.push(cmdItem) - } - Map item = [ - t: 'multi', - time: now(), - commands: newCmds, - srcDesc: srcDesc, - parallel: parallel, -// deviceData: deviceData, - cmdMap: cmdMap, - device: device, - callback: callback - ] - addToQ(item) -} - -void addToQ(Map item) { - String appId=app.getId() - Boolean aa = getTheLock(sHMLF, "addToQ(${item})") - // log.trace "lock wait: ${aa}" - - Map memStore = historyMapFLD[appId] ?: [:] - String k = 'cmdQ' - List eData = (List)memStore[k] ?: [] - eData.push(item) - Integer qsiz = eData.size() - updMemStoreItem(k, eData) - - releaseTheLock(sHMLF) - - if(qsiz == 1) runInMillis(300L, "workQ") - else runIn(24, "workQB") - - List lmsg = [] - String t = item.t - Boolean fir=true - ['cmdMap', 'time', 'deviceData', 'device', 'callback', 'parallel', 'command', 'value', 'srcDesc', 'type'].each { String s -> - def ss = item."${s}" - if(ss) { - if(fir) { fir=false; lmsg.push(spanSm("addToQ NEW COMMAND (${qsiz})", sCLRGRN2)) } - String nm = ss.toString().replaceAll('<', '<').replaceAll('>', '>') - lmsg.push("addToQ (${t}) | ${s}: ${nm}".toString()) - } - } - if(item.commands?.size()) { - Integer cnt = 1 - item.commands.each { cmd -> - lmsg.push("addToQ (${item.t}) | Command(${cnt}): ${cmd}".toString()) - cnt++ - } - } - if((Boolean)settings.logDebug) lmsg.each { String msg -> log.debug(msg) } -} - -void workQF() { workQ() } -void workQB() { workQ() } - -void workQ() { - logTrace "running workQ" - String mmsg - - String appId=app.getId() - Boolean aa = getTheLock(sHMLF, "addToQ(${item})") - // log.trace "lock wait: ${aa}" - Boolean locked = true - - Map myMap = workQMapFLD[appId] ?: [:] - Boolean active = (Boolean)myMap.active - if(active==null) { active = false; myMap.active=active; workQMapFLD[appId]=myMap; workQMapFLD=workQMapFLD } - // log.debug "active: $active myMap: $myMap" - Long nextOk = (Long)myMap.nextOk ?: 0L - if(nextOk < now()) nextOk = 0L - - Map memStore = historyMapFLD[appId] ?: [:] - String k = 'cmdQ' - List eData = (List)memStore[k] ?: [] - - Boolean fnd = (eData.size() > 0) - - // if we are not doing anything grab next item off queue and start it; - if(!active && now() > nextOk && fnd) { - - List lmsg = [] - Double msSum = 0.0D - List seqList = [] - List activeD = [] - Map extData=[:] - List extList = [] - - Boolean oldParallel = null - Boolean parallel = false - - String srcDesc - Map seqObj=null - Integer mdelay = 0 - - // lets try to join commands in single request to Alexa - while(eData.size()>0){ - - Map item = (Map)eData[0] - - String t=(String)item.t - // Long tLong=(Long)item.time - Map cmdMap=[:] - String device = (String)item.device - String callback = (String)item.callback - srcDesc = sNULL - //log.debug "item: $item" - - if(t=='multi') { - srcDesc = (String)item.srcDesc - List seqCmds = (List)item.commands - - if(srcDesc == 'ExecuteRoutine'){ - if(seqList.size() > 0) break // execute runs by itself - Map seqMap = (Map)seqCmds[0].command // already have a sequence map - seqObj = sequenceBuilder(seqMap, null, null) - if(oldParallel == null) oldParallel = parallel - } else { - - Boolean nparallel = item.parallel - parallel = nparallel != null ? nparallel : parallel - if(oldParallel == null) oldParallel = parallel - if(parallel != oldParallel) { break } // if parallel changes we are done this set of commands - - cmdMap = (Map)item.cmdMap ?: [:] - - //log.debug "seqCmds: $seqCmds" - seqList = seqList + multiSequenceListBuilder(seqCmds) - if(!parallel) mdelay = 0 - seqCmds?.each { cmdItem-> - //log.debug "cmdItem: $cmdItem" - if(cmdItem.command instanceof String){ - String mcommand = cmdItem.command - String type=cmdItem?.cmdType ?: sBLANK - String tv = cmdItem?.value?.toString() - Integer del = getMsgDur(mcommand, type, tv) - if(del) { - if(!parallel) mdelay += del - else mdelay = del > mdelay ? del : mdelay - } - } - if(mdelay) cmdMap.msgDelay= mdelay - } - //log.debug "seqList: ${seqList}" - } - } - - if(t=='sequence') { - String type=item.type - Map deviceData = (Map)item.deviceData - srcDesc = type + "${device ? " from $device" : sBLANK}" - - Boolean nparallel = item.parallel - parallel = nparallel != null ? nparallel : false - if(oldParallel == null) oldParallel = parallel - if(parallel != oldParallel) { break } // if parallel changes we are done this set of commands - - String command=(String)item.command - - def value = item.value - - if(command in ['announcement_devices'] ) { - if(seqList.size() > 0) break // this command runs alone so finish off what we are planning to do - seqObj = sequenceBuilder(command, value, deviceData) - } else { - seqList = seqList + [createSequenceNode(command, value, deviceData)] - } - - String tv = value?.toString() - Integer del = getMsgDur(command, type, tv) - if(del) { - if(parallel) mdelay = del > mdelay ? del : mdelay - } - if(del && !cmdMap) cmdMap = [ msgDelay: del ] - } - - Map titem = (Map)eData.remove(0) // pop the command as we are going to do it - updMemStoreItem(k, eData) - - if(t=='nop') { - if(seqList.size() > 0) { - lmsg.push("workQ found nop, processing current list") - break // finish off what we are planning to do - } - lmsg.push("workQ found nop, skipping") - } else { - - activeD.push(titem) // save what we are doing to an active list - updMemStoreItem('active', activeD) - - lmsg.push("workQ adding ${srcDesc} | ${seqList ? "MultiSequence" : "Sequence"} ${seqList ? "${parallel ? ": Parallel" : ": Sequential"}" : sBLANK}".toString()) - - Map t_extData =[:] - if(device && callback) { - String nstr = cmdMap?.message?.toString() - nstr = nstr?.trim() - Boolean isSSML = (nstr?.toString()?.startsWith("") && nstr?.endsWith("")) - //if(isSSML) nstr = nstr[7..-9] - Integer msgLen = nstr?.length() - t_extData = [ - cmdDt:(cmdMap.cmdDt ?: null), - cmdDesc: (cmdMap.cmdDesc ?: null), - msgLen: msgLen, - isSSML: isSSML, - deviceId: device, - callback: callback, - msgDelay: (cmdMap.msgDelay ?: null), - message: (cmdMap.message ? cmdMap.message : null), - newVolume: (cmdMap.newVolume ?: null), - oldVolume: (cmdMap.oldVolume ?: null), - cmdId: (cmdMap.cmdId ?: null), - ] - } - extList.push(t_extData) - extData.extList = extList - - Double ms = ((cmdMap?.msgDelay ?: 0.5D) * 1000.0D) - ms = Math.min(240000, Math.max(ms, 0)) // at least 0, max 240 seconds - msSum = parallel ? ms : msSum + ms - lmsg.push("workQ ms delay is $msSum".toString()) - - if(seqObj) { break } // runs by itself - // if(parallel) { break } // only run 1 parallel at a time in case they are changing the same thing again - } - } - - if(seqList.size() > 0 || seqObj) { - - Integer mymin = 3000 // min ms between Alexa commands - - msSum = Math.min(240000, Math.max(msSum, mymin)) - nextOk = (Long)now() + msSum.toLong() - lmsg.push("workQ FINAL ms delay is $msSum".toString()) - active = true - myMap.active=active - myMap.nextOk = nextOk; workQMapFLD[appId]=myMap; workQMapFLD=workQMapFLD - - locked = false - releaseTheLock(sHMLF) - - lmsg.each { String msg -> logDebug(msg) } - - extData = extData + [nextOk: nextOk] - - if(!seqObj) { //'ExecuteRoutine' - Map seqMap = multiSequenceBuilder(seqList, oldParallel) - seqObj = sequenceBuilder(seqMap, null, null) - } - - Map params = [ - uri: getAmazonUrl(), - path: "/api/behaviors/preview", - headers: getReqHeaderMap(true), - contentType: sAPPJSON, - timeout: 20, - body: new JsonOutput().toJson(seqObj) - ] - - //String nm = params.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.trace spanSm("workQ params: $nm extData: $extData", sCLRGRN) - - try{ - execAsyncCmd("post", "finishWorkQ", params, extData) - } catch (ex) { - respExceptionHandler(ex, "workQ", true) - finishWorkQ([status: 500, data: [:]], extData) - } - } - } - - Long t0 = (Long)now() - mmsg = "workQ active: ${active} work items fnd: ${fnd} now: ${t0} nextOk: ${nextOk}" - if(!active && fnd) { // if we have more work to do - t0 = (Long)now() - Long ms = nextOk+200L - t0 - if(ms <= 0L) ms = 4000 - if(t0 < nextOk) { // if we are waiting between commands due to Alexa limits, schedule wakeup to resume - runInMillis(ms, "workQF") - mmsg = "workQ wakeup requested in $ms ms" + mmsg - } - } - if(locked) releaseTheLock(sHMLF) - if(mmsg) logDebug(mmsg) -} - -// this does not handle SSML break commands -// https://developer.amazon.com/en-US/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html -Integer getMsgDur(String command, String type, String tv){ - Integer del = 0 - if(command in ['announcement_devices', 'announcement', 'announcementall'] || type in ['sendSpeak']) { - String[] valObj = (tv?.contains("::")) ? tv.split("::") : ["Echo Speaks", tv] - String nstr = valObj[1].trim() - nstr = nstr.replaceAll(/\s\s+/, sSPACE) - //String nm = nstr.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "getMsgDur $nm" - Boolean isSSML = (nstr?.startsWith("") && nstr?.endsWith("")) - if(isSSML) nstr = nstr[7..-9] - isSSML = (isSSML || command == 'ssml') - String actMsg = isSSML ? nstr?.replaceAll(/<[^>]+>/, sBLANK) : cleanString(nstr) - //nm = actMsg.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "getMsgDur1 $nm" - Integer msgLen = actMsg?.length() ?: 0 - del = calcDelay(msgLen) - if(devModeFLD) logTrace("getMsgDur res: $del | actMsg: ${actMsg} msgLen: $msgLen origLen: ${tv.length()} isSSML: ${isSSML} ($command, $type, $tv)") - } - else if(type.startsWith('play')) del = 18 - else if(type.startsWith('say')) del = 3 - //logTrace("getMsgDur res: $del ($command, $type, $tv)") - return del -} - -static Integer calcDelay(Integer msgLen=null, Boolean addRandom=false) { - if(!msgLen) { return 30 } - Integer twd = 2 - Integer v = (Integer)((msgLen <= 14 ? 1 : (msgLen / 14)) * twd) - Integer res=v - Integer randomInt - if(addRandom){ - Random random = new Random() - randomInt = random?.nextInt(5) //Was using 7 - res=v + randomInt - } -// logTrace("calcDelay($msgLen) | res:$res | twd: $twd | delay: $v ${addRandom ? '+ '+randomInt.toString() : sBLANK}") - return res //+2 -} - -void finishWorkQ(response, extData){ - String meth = 'finishWorkQ' - logTrace "running "+meth - Integer statusCode=null - def sData - String respMsg=sNULL - try { - statusCode = response?.status?.toInteger() - if(response.hasError()){ - respMsg = response.getErrorMessage() - } else sData = response?.data - } catch(ex) { - respExceptionHandler(ex, "finishWorkQ", true) - } - - Boolean retry=false - if(statusCode == 200) updTsVal("lastSpokeToAmazon") - else { - logWarn("$meth | ${statusCode} | $respMsg | $extData") - String respMsgLow = respMsg ? respMsg.toLowerCase() : sNULL - if((statusCode in [400, 429]) && respMsgLow && (respMsgLow in ["rate exceeded", "too many requests"])) { - switch(respMsgLow) { - case "rate exceeded": - 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) - retry=true - break - case "too many requests": - logWarn("You've sent too many consecutive commands to your devices... | Device will retry again in ${rDelay} seconds", true) - retry=true - break - } - } - } - - String appId=app.getId() - Boolean aa = getTheLock(sHMLF, "addToQ(${item})") - - Map myMap = workQMapFLD[appId] - Boolean active = false; myMap.active=active; workQMapFLD[appId]=myMap; workQMapFLD=workQMapFLD - - Map memStore = historyMapFLD[appId] ?: [:] - String k = 'active' - List activeD = (List)memStore[k] ?: [] - if(retry) { - log.warn "wanted to retry but did not" -// String kk = 'cmdQ' -// List eData = (List)memStore[kk] ?: [] -// List newL = activeD + eData -// activeD = newL -// TODO delays - activeD = [] - } else { - activeD = [] - } - updMemStoreItem(k, activeD) - - releaseTheLock(sHMLF) - -// if(!retry) { - extData?.extList?.each { extItem -> - if(extItem && (String)extItem.deviceId && (String)extItem.callback) { - if(extItem != null && statusCode==200) extItem["amznReqId"] = response?.headers["x-amz-rid"] ?: null - def child = getChildDevice((String)extItem.deviceId) - child."${(String)extItem.callback}"(sData, statusCode, extItem) - } - } -// } - workQ() -} - -Map sequenceBuilder(cmd, val, Map deviceData=[:]) { -//log.debug "sequenceBuilder: $cmd val: $val" - Map seqJson - if (cmd instanceof Map) { - seqJson = (Map)cmd?.sequence ?: (Map)cmd - } else { - seqJson = [ - "@type": "com.amazon.alexa.behaviors.model.Sequence", - "startNode": createSequenceNode((String)cmd, val, deviceData) - ] - } - Map seqObj = [ - behaviorId: (seqJson?.sequenceId ? cmd?.automationId : "PREVIEW"), - sequenceJson: new JsonOutput().toJson(seqJson), - status: "ENABLED" - ] - return seqObj -} - -List multiSequenceListBuilder(Listcommands) { - //log.debug "multiSequenceListBuilder commands: $commands" - List nodeList = [] - commands?.each { cmdItem-> - //log.debug "multiSequenceListBuilder cmdItem: $cmdItem" - if(cmdItem.command instanceof String){ - Map deviceData = (Map)cmdItem.deviceData - nodeList.push(createSequenceNode((String)cmdItem.command, cmdItem.value, deviceData) ) - } else { - nodeList.push(cmdItem.command) - } - } - return nodeList -} - -static Map multiSequenceBuilder(List nodeList, Boolean parallel=false) { -//log.debug "multiSequenceBuilder: $nodeList" - String seqType = parallel ? "ParallelNode" : "SerialNode" - Map seqMap = [ - "sequence": [ - "@type": "com.amazon.alexa.behaviors.model.Sequence", - "startNode": [ - "@type": "com.amazon.alexa.behaviors.model.${seqType}", - "name": null, - "nodesToExecute": nodeList - ] - ] - ] - return seqMap -} - -static Integer getStringLen(String str) { return str?.length() ?: 0 } - -private static List msgSeqBuilder(String str, Map deviceData, String cmdType) { - //String nm = str.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "msgSeqBuilder: $nm" - List seqCmds = [] - List strArr = [] - String nstr = str.trim() - Boolean isSSML = (nstr.startsWith("") && nstr.endsWith("")) - //if(isSSML) nstr = nstr[7..-9] - str = nstr - if(str.length() < 450) { - seqCmds.push([command: (isSSML ? "ssml": "speak"), value: str, deviceData: deviceData, cmdType: cmdType]) - } else { - List msgItems = str.split() - msgItems.each { String wd-> - // log.debug "CurArrLen: ${(getStringLen(strArr.join(" ")))} | CurStrLen: (${wd.length()})" - if((getStringLen(strArr.join(sSPACE)) + wd.length()) > 430) { - seqCmds.push([command: (isSSML ? "ssml": "speak"), value: strArr.join(sSPACE), deviceData: deviceData, cmdType: cmdType]) - strArr = [] - } - strArr.push(wd) - if(wd == msgItems.last()) { seqCmds.push([command: (isSSML ? "ssml": "speak"), value: strArr.join(sSPACE), deviceData: deviceData, cmdType: cmdType]) } - } - } - // log.debug "seqCmds: $seqCmds" - return seqCmds -} - -String cleanString(String str, Boolean frcTrans=false) { - if(!str) { return sNULL } - //String nm = str.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "cleanString1: $nm" - - //Cleans up characters from message - -// some folks try to use ssml without markers. It sometimes works and sometimes does not - below makes it always fail as it removes some ssml markup ( / for example) - //str = str.replaceAll(~/[^a-zA-Z0-9-?%°.,:&#;<>!\/ ]+/, sSPACE)?.replaceAll(/\s\s+/, sSPACE) - str = str.replaceAll(/\s\s+/, sSPACE) - - str = textTransform(str, frcTrans) - //nm = str.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "cleanString: $nm" - return str -} - -private String textTransform(String str, Boolean force=false) { - if(!force && (Boolean)settings.disableTextTransform) { 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("°"," degrees ") - return str -} -/* -private String timeTransform(String str, Boolean force=false) { - str = str.replaceAll(/^(?:(?:(?:0?[1-9]|1[0-2])(?::|\.)[0-5]\d(?:(?::|\.)[0-5]\d)?\s?[aApP][mM])|(?:(?:0?\d|1\d|2[0-3])(?::|\.)[0-5]\d(?:(?::|\.)[0-5]\d)?))$/) { - log.debug "timeTransform: ${it[0]}" - // return "${it[0]?.toString()?.replaceAll("[-]", "minus ")?.replaceAll("[FfCc]", " degrees")}" - } - return str -}*/ - -@SuppressWarnings('GroovyFallthrough') -Map createSequenceNode(String command, value, Map deviceData = [:]) { - //log.debug "createSequenceNode: command: $command " - //String nm = value.toString().replaceAll('<', '<').replaceAll('>', '>') - //log.debug "createSequenceNode: value: $nm" - //log.debug "createSequenceNode: deviceData: $deviceData " - try { - Boolean remDevSpecifics = false - String deviceType = deviceData?.deviceType ?: sNULL - String serialNumber = deviceData?.serialNumber ?: sNULL - String accountId = deviceData?.account ?: sNULL - String owner = deviceData?.owner ?: sNULL - - Map seqNode = [ - "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", - operationPayload: [ - deviceType: deviceType, - deviceSerialNumber: serialNumber, - locale: ((String)settings.regionLocale ?: "en-US"), - customerId: owner //state.deviceOwnerCustomerId - ] - ] - - String sval = value.toString() - String lcmd = command.toLowerCase() - switch (lcmd) { - case "weather": - seqNode.type = "Alexa.Weather.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "traffic": - seqNode.type = "Alexa.Traffic.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "flashbriefing": - seqNode.type = "Alexa.FlashBriefing.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "goodmorning": - seqNode.type = "Alexa.GoodMorning.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "goodnight": - seqNode.type = "Alexa.GoodNight.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "cleanup": - seqNode.type = "Alexa.CleanUp.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "singasong": - seqNode.type = "Alexa.SingASong.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "tellstory": - seqNode.type = "Alexa.TellStory.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "funfact": - seqNode.type = "Alexa.FunFact.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "joke": - seqNode.type = "Alexa.Joke.Play" - seqNode.skillId = "amzn1.ask.1p.saysomething" - break - case "calendartomorrow": - seqNode.type = "Alexa.Calendar.PlayTomorrow" - seqNode.skillId = "amzn1.ask.1p.calendar" - break - case "calendartoday": - seqNode.type = "Alexa.Calendar.PlayToday" - seqNode.skillId = "amzn1.ask.1p.calendar" - break - case "calendarnext": - seqNode.type = "Alexa.Calendar.PlayNext" - seqNode.skillId = "amzn1.ask.1p.calendar" - break - case "date": - seqNode.type = "Alexa.Date.Play" - seqNode.skillId = "amzn1.ask.1p.dateandtime" - break - case "time": - seqNode.type = "Alexa.Time.Play" - seqNode.skillId = "amzn1.ask.1p.dateandtime" - break - case "stop": - remDevSpecifics = true - seqNode.type = "Alexa.DeviceControls.Stop" - seqNode.skillId = "amzn1.ask.1p.alexadevicecontrols" - seqNode.operationPayload.devices = [ [deviceType: deviceType, deviceSerialNumber: serialNumber] ] - seqNode.operationPayload.isAssociatedDevice = false - 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": - case "cannedtts": - List okVals = (List)seqItemsAvail().speech.cannedtts_random //["goodbye", "confirmations", "goodmorning", "compliments", "birthday", "goodnight", "iamhome"] - if(!(sval in okVals)) { return null } - seqNode.type = "Alexa.CannedTts.Speak" - String[] valObj = lcmd == 'cannedtts_random' ? [sval, 'random'] : (sval?.contains("::") ? sval.split("::") : [sval, sval]) - seqNode.operationPayload.cannedTtsStringId = "alexa.cannedtts.speak.curatedtts-category-${valObj[0]}/alexa.cannedtts.speak.curatedtts-${valObj[1]}" - break - case "sound": - String sndName - if(sval?.startsWith("amzn_sfx_")) { - sndName = sval - } else { - Map sounds = getAvailableSounds() - if(sounds[sval]) { sndName = sounds[sval] } - else { sndName = sval } - //if(!(sounds[value])) { return null } - //sndName = sounds[value] - } - seqNode.type = "Alexa.Sound" - seqNode.operationPayload.soundStringId = sndName - break - case "wait": - remDevSpecifics = true - seqNode.operationPayload?.remove('customerId') - seqNode.type = "Alexa.System.Wait" - seqNode.operationPayload.waitTimeInSeconds = sval?.toInteger() ?: 5 - 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 = (String)state.deviceOwnerCustomerId - if(lcmd in ["dnd_all_time", "dnd_all_duration"]) { - seqNode.operationPayload.devices = [ [deviceType: "ALEXA_ALL_DEVICE_TYPE", deviceSerialNumber: "ALEXA_ALL_DSN"] ] - } - if(lcmd in ["dnd_time", "dnd_duration"]) { - seqNode.operationPayload.devices = [ [deviceAccountId: accountId /*(String)state.deviceAccountId*/, deviceType: deviceType, deviceSerialNumber: serialNumber] ] - } - seqNode.operationPayload.action = "Enable" - if(lcmd in ["dnd_time","dnd_all_time"]) { - seqNode.operationPayload.until = "TIME#T${value}" - } else if (lcmd in ["dnd_duration","dnd_all_duration"]) { seqNode?.operationPayload?.duration = "DURATION#PT${value}" } -//ERS - seqNode.operationPayload.timeZoneId = "America/Detroit" //location?.timeZone?.ID ?: null - break - case "speak": - seqNode.type = "Alexa.Speak" - sval = cleanString(sval) - seqNode.operationPayload.textToSpeak = sval - break - case "volume": - seqNode.type = "Alexa.DeviceControls.Volume" - seqNode.skillId = "amzn1.ask.1p.alexadevicecontrols" - seqNode.operationPayload.value = value - break - case "ssml": - case "announcement": - case "announcementall": - case "announcement_devices": - remDevSpecifics = true - seqNode.type = "AlexaAnnouncement" - seqNode.skillId = "amzn1.ask.1p.routines.messaging" - seqNode.operationPayload.expireAfter = "PT5S" - String[] valObj = (sval?.contains("::")) ? sval.split("::") : ["Echo Speaks", sval] - // log.debug "valObj(size: ${valObj?.size()}): $valObj" - // valObj[1] = valObj[1]?.toString()?.replace(/([^0-9]?[0-9]+)\.([0-9]+[^0-9])?/, "\$1,\$2") - // log.debug "valObj[1]: ${valObj[1]}" - String nstr = valObj[1].trim() - Boolean isSSML = (nstr?.startsWith("") && nstr?.endsWith("")) - //if(isSSML) nstr = nstr[7..-9] - String str = nstr - String mtype = lcmd == "ssml" || isSSML ? "ssml" : "text" - String mval = lcmd == "ssml" || isSSML ? str : cleanString(str) - String mtitle = cleanString(lcmd == "ssml" || isSSML ? str.replaceAll(/<[^>]+>/, sBLANK) : str) - seqNode.operationPayload.content = [[ - locale: ((String)settings.regionLocale ?: "en-US"), - display: [ title: cleanString(valObj[0]), body: mtitle ], //valObj[1].replaceAll(/<[^>]+>/, '') ], - speak: [ type: mtype, value: mval ] //(lcmd == "ssml" || isSSML ? "ssml" : "text"), value: valObj[1] ] - ]] - seqNode.operationPayload.target = [ customerId: owner /* (String)state.deviceOwnerCustomerId */] - if(!(lcmd in ["announcementall", "announcement_devices"])) { - seqNode.operationPayload.target.devices = [ [ deviceTypeId: deviceType, deviceSerialNumber: serialNumber ] ] - } else if(lcmd == "announcement_devices" && valObj?.size() && valObj[2] != null) { -// log.debug spanSm("valObj: ${valObj}", sCLRGRN2) - List devObjs = (List)new JsonSlurper().parseText((String)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 = sval - 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: deviceType, deviceSerialNumber: 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 - case "goodnews": - seqNode.type = "Alexa.GoodNews.Play" - seqNode.skillId = "amzn1.ask.1p.goodnews" - break - case "voicecmdtxt": - seqNode.type = "Alexa.TextCommand" - seqNode.skillId = "amzn1.ask.1p.tellalexa" - seqNode.operationPayload.text = sval - break - - default: - return null - } - if(remDevSpecifics) { - seqNode.operationPayload.remove('deviceType') - seqNode.operationPayload.remove('deviceSerialNumber') - seqNode.operationPayload.remove('locale') - } - // log.debug "seqNode: $seqNode" - return seqNode - } catch (ex) { - logError("createSequenceNode Exception: ${ex}", false, ex) - } - return [:] -} - -/****************************************** -| Notification Functions -*******************************************/ -String getAmazonDomain() { return (String)settings.amazonDomain } -String getAmazonUrl() {return "https://alexa.${(String)settings.amazonDomain}"} - -static Map notifValEnum(Boolean allowCust = true) { - Map items = [ - 300:"5 Minutes", 600:"10 Minutes", 900:"15 Minutes", 1200:"20 Minutes", 1500:"25 Minutes", - 1800:"30 Minutes", 2700:"45 Minutes", 3600:"1 Hour", 7200:"2 Hours", 14400:"4 Hours", 21600:"6 Hours", 43200:"12 Hours", 86400:"24 Hours" - ] - if(allowCust) { items[100000] = "Custom" } - return items -} - -@Field volatile static Map healthChkMapFLD = [:] - -void healthCheck() { - logTrace("healthCheck") - String appId=app.getId() - if(!healthChkMapFLD[appId]) { - healthChkMapFLD[appId] = true - healthChkMapFLD = healthChkMapFLD - } - - if(!devModeFLD && advLogsActive()) { logsEnabled() } - checkVersionData() - if(checkIfCodeUpdated()) { - logWarn("Code Version Change Detected... Health Check will occur on next cycle.") - return - } - Boolean a=validateCookie() - if(getLastTsValSecs("lastCookieRrshDt") > cookieRefreshSeconds()) { - runCookieRefresh() - } else if (getLastTsValSecs("lastGuardSupChkDt") > 43200) { - checkGuardSupport() - } else if(getLastTsValSecs("lastServerWakeDt") > 86400 && serverConfigured()) { wakeupServer(false, false, "healthCheck") } - - if(!(Boolean)state.noAuthActive) runIn(2, "getOtherData") - chkRestartSocket() - - if((Boolean)state.isInstalled && getLastTsValSecs("lastMetricUpdDt") > (3600*24)) { runIn(30, "sendInstallData", [overwrite: true]) } - logsDisable() - appUpdateNotify() - - //if(!getOk2Notify()) { return } - missPollNotify((Boolean)settings.sendMissedPollMsg, (settings.misPollNotifyMsgWaitVal as Integer ?: 3600)) -} - -Boolean advLogsActive() { - return ((Boolean)settings.logDebug || (Boolean)settings.logTrace || - (Boolean)settings.childAppLogDebug || (Boolean)settings.childAppLogTrace || - (Boolean)settings.childDeviceLogDebug || (Boolean)settings.childDeviceLogTrace) -} - -public void logsEnabled() { - if(advLogsActive() && !getTsVal("logsEnabled")) { - logTrace("enabling logging timer") - updTsVal("logsEnabled") - } -} - -public void logsDisable() { - if(advLogsActive()) { - Integer dtSec = getLastTsValSecs("logsEnabled", null) - if(dtSec && (dtSec > 3600*6)) { - settingUpdate("logDebug", sFALSE, sBOOL) - settingUpdate("logTrace", sFALSE, sBOOL) - if((Boolean)settings.childAppLogDebug || (Boolean)settings.childAppLogTrace || - (Boolean)settings.childDeviceLogDebug || (Boolean)settings.childDeviceLogTrace) { - runIn(4, 'disableAdvChldLogs') - } - remTsVal("logsEnabled") - log.debug "Disabling debug logs" - } - } -} - -@Field volatile static Map childLogMapFLD = [:] - -public void activateChildAdvLogs() { - String myId=app.getId() - Map myMap = childLogMapFLD[myId] - myMap = myMap ?: [:] - Boolean a - - if((Boolean)settings.disableAllChildAdvLogs) { - settingUpdate("disableAllChildAdvLogs", sFALSE, sBOOL) - settingUpdate("childAppLogDebug", sFALSE, sBOOL) - settingUpdate("childAppLogTrace", sFALSE, sBOOL) - settingUpdate("childDeviceLogDebug", sFALSE, sBOOL) - settingUpdate("childDeviceLogTrace", sFALSE, sBOOL) - } - - a = (Boolean)settings.childAppLogDebug - if(a != (Boolean)myMap.childAppLogDebug) { - if (a) { - manAllZonesDbgLogs(true) - manAllActsDbgLogs(true) - updTsVal("logsEnabled") - } else { - manAllZonesDbgLogs(false) - manAllActsDbgLogs(false) - } - myMap.childAppLogDebug = a - } - - a = (Boolean)settings.childAppLogTrace - if(a != (Boolean)myMap.childAppLogTrace) { - if(a) { - manAllZonesTrcLogs(true) - manAllActsTrcLogs(true) - updTsVal("logsEnabled") - } else { - manAllZonesTrcLogs(false) - manAllActsTrcLogs(false) - } - myMap.childAppLogTrace = a - } - - a = (Boolean)settings.childDeviceLogDebug - if(a != (Boolean)myMap.childDeviceLogDebug) { - if(a) { - manAllEchosDbgLogs(true) - updTsVal("logsEnabled") - } else { - manAllEchosDbgLogs(false) - } - myMap.childDeviceLogDebug = a - } - - a = (Boolean)settings.childDeviceLogTrace - if(a != (Boolean)myMap.childDeviceLogTrace) { - if(a) { - manAllEchosTrcLogs(true) - updTsVal("logsEnabled") - } else { - manAllEchosTrcLogs(false) - } - myMap.childDeviceLogTrace = a - } - - childLogMapFLD[myId] = myMap - childLogMapFLD = childLogMapFLD -} - -private void manAllZonesDbgLogs(Boolean enable=true) { getZoneApps()?.each { ca-> enable ? ca?.enableDebugLog() : ca?.disableDebugLog() } } -private void manAllZonesTrcLogs(Boolean enable=true) { getZoneApps()?.each { ca-> enable ? ca?.enableTraceLog() : ca?.disableTraceLog() } } -private void manAllActsDbgLogs(Boolean enable=true) { getActionApps()?.each { ca-> enable ? ca?.enableDebugLog() : ca?.disableDebugLog() } } -private void manAllActsTrcLogs(Boolean enable=true) { getActionApps()?.each { ca-> enable ? ca?.enableTraceLog() : ca?.disableTraceLog() } } -private void manAllEchosDbgLogs(Boolean enable=true) { ((List)getChildDevices())?.each { cd-> enable ? cd?.enableDebugLog() : cd?.disableDebugLog() } - getZoneApps()?.each { ca-> enable ? ca?.relayEnableDebugLog() : ca?.relayDisableDebugLog() } } -private void manAllEchosTrcLogs(Boolean enable=true) { ((List)getChildDevices())?.each { cd-> enable ? cd?.enableTraceLog() : cd?.disableTraceLog() } - getZoneApps()?.each { ca-> enable ? ca?.relayEnableTraceLog() : ca?.relayDisableTraceLog() } } - - -private void disableAdvChldLogs() { - getActionApps()?.each { ca-> ca?.logsDisable() } - getZoneApps()?.each { ca-> ca?.logsDisable() } - getZoneApps()?.each { ca-> ca?.relayLogsOff() } - ((List)getChildDevices())?.each { cd-> cd?.logsOff() } - settingUpdate("disableAllChildAdvLogs", sFALSE, sBOOL) - settingUpdate("childAppLogDebug", sFALSE, sBOOL) - settingUpdate("childAppLogTrace", sFALSE, sBOOL) - settingUpdate("childDeviceLogDebug", sFALSE, sBOOL) - settingUpdate("childDeviceLogTrace", sFALSE, sBOOL) - - String myId=app.getId() - Map myMap = childLogMapFLD[myId] - myMap = myMap ?: [:] - Boolean a = false - myMap.childAppLogDebug = a - myMap.childAppLogTrace = a - myMap.childDeviceLogDebug = a - myMap.childDeviceLogTrace = a - childLogMapFLD[myId] = myMap - childLogMapFLD = childLogMapFLD -} - -void missPollNotify(Boolean on, Integer wait) { - Integer lastDataUpd = getLastTsValSecs("lastDevDataUpdDt", 1000000) - Integer lastMissPollM = getLastTsValSecs("lastMissedPollMsgDt") - //if(devModeFLD) logTrace("missPollNotify() | on: ($on) | wait: ($wait) | getLastDevicePollSec: (${lastDataUpd}) | misPollNotifyWaitVal: (${settings.misPollNotifyWaitVal}) | getLastMisPollMsgSec: (${lastMissPollM})") - if(lastDataUpd <= ((settings.misPollNotifyWaitVal as Integer ?: 2700)+10800)) { - state.missPollRepair = false - } else { - if(lastDataUpd != 1000000) { - String msg - if((Boolean)state.authValid) { - msg = "\nThe Echo Speaks app has NOT received any device data from Amazon in the last (${getLastTsValSecs("lastDevDataUpdDt")}) seconds.\nThere maybe an issue with network access." - } else { msg = "\nThe Amazon login info has expired!\nPlease open the heroku amazon authentication page and login again to restore normal operation." } - logWarn(msg.toString().replaceAll("\n", sSPACE)) - - if(lastMissPollM < wait?.toInteger()) { on = false } - if(on && sendMsg("${app.name} ${(Boolean)state.authValid ? "Data Refresh Issue" : "Amazon Login Issue"}", msg)) { - updTsVal("lastMissedPollMsgDt") - } - } - if(!(Boolean)state.missPollRepair) { - if((Boolean)state.authValid){ - if(lastDataUpd == 1000000) logTrace("code reload or system restart, calling initialize") - else logTrace("calling initialize to attempt recovery") - state.missPollRepair = true - initialize() - } - } - } -} - -@SuppressWarnings('GroovyVariableNotAssigned') -void appUpdateNotify() { - Boolean appUpd = appUpdAvail() - Boolean actUpd = actionUpdAvail() - Boolean zoneUpd = zoneUpdAvail() - Boolean zoneChildDevUpd = zoneChildDevUpdAvail() - Boolean echoDevUpd = echoDevUpdAvail() - // Boolean socketUpd = socketUpdAvail() - Boolean servUpd = serverUpdAvail() - Boolean res=false - if(appUpd || actUpd || zoneUpd || zoneChildDevUpd || echoDevUpd || socketUpd || servUpd) res=true - - Integer secs - Integer updW - Boolean on=false - if(res) { - on = ((Boolean)settings.sendAppUpdateMsg) - updW = settings.updNotifyWaitVal?.toInteger() - if(updW == null) { updW = 43200; settingUpdate("updNotifyWaitVal", "43200", sENUM) } - secs=getLastTsValSecs("lastUpdMsgDt") - if(secs > updW && on) { - String str = sBLANK - str += !appUpd ? sBLANK : "\nEcho Speaks App: v${state.appData?.versions?.mainApp?.ver?.toString()}" - str += !actUpd ? sBLANK : "\nEcho Speaks Actions: v${state.appData?.versions?.actionApp?.ver?.toString()}" - str += !zoneUpd ? sBLANK : "\nEcho Speaks Zones: v${state.appData?.versions?.zoneApp?.ver?.toString()}" - str += !zoneChildDevUpd ? sBLANK : "\nEcho Speaks Zone Device: v${state.appData?.versions?.zoneChildDevice?.ver?.toString()}" - str += !echoDevUpd ? sBLANK : "\nEcho Speaks Device: v${state.appData?.versions?.echoDevice?.ver?.toString()}" - // str += !socketUpd ? sBLANK : "\nEcho Speaks Socket: v${state.appData?.versions?.wsDevice?.ver?.toString()}" - str += !servUpd ? sBLANK : "\n${((Boolean)getServerItem("onHeroku") == true) ? "Heroku Service" : "Node Service"}: v${state.appData?.versions?.server?.ver?.toString()}" - sendMsg("Info", "Echo Speaks Update(s) are Available:${str}...\n\nPlease visit the IDE to Update your code...") - updTsVal("lastUpdMsgDt") - } - } - state.updateAvailable = res - String msg="appUpdateNotify() RESULT: ${res} | on: (${on}) | appUpd: (${appUpd}) | actUpd: (${actUpd}) | zoneUpd: (${zoneUpd}) | echoDevUpd: (${echoDevUpd}) | echoZoneDevUpd (${zoneChildDevUpd}) | servUpd: (${servUpd}) | getLastUpdMsgSec: ${secs} | updNotifyWaitVal: ${updW}" - if(res) logDebug(msg) - //else if(devModeFLD) logTrace(msg) -} - -private List codeUpdateItems(Boolean shrt=false) { - Boolean appUpd = appUpdAvail() - Boolean actUpd = actionUpdAvail() - Boolean zoneUpd = zoneUpdAvail() - Boolean zoneChildDevUpd = zoneChildDevUpdAvail() - Boolean devUpd = echoDevUpdAvail() - // Boolean socketUpd = socketUpdAvail() - Boolean servUpd = serverUpdAvail() - List updItems = [] - if(appUpd || actUpd || zoneUpd || zoneChildDevUpd || devUpd || socketUpd || servUpd) { - if(appUpd) updItems.push("${!shrt ? "\nEcho Speaks " : sBLANK}App: (v${state.appData?.versions?.mainApp?.ver?.toString()})") - if(actUpd) updItems.push("${!shrt ? "\nEcho Speaks " : sBLANK}Actions: (v${state.appData?.versions?.actionApp?.ver?.toString()})") - if(zoneUpd) updItems.push("${!shrt ? "\nEcho Speaks " : sBLANK}Zones: (v${state.appData?.versions?.zoneApp?.ver?.toString()})") - if(zoneChildDevUpd) updItems.push("${!shrt ? "\nEcho Speaks " : sBLANK}Zone Child Device: (v${state.appData?.versions?.zoneChildDevice?.ver?.toString()})") - if(devUpd) updItems.push("${!shrt ? "\nEcho Speaks " : "ES "}Device: (v${state.appData?.versions?.echoDevice?.ver?.toString()})") - // if(socketUpd) updItems.push("${!shrt ? "\nEcho Speaks " : sBLANK}Websocket: (v${state.appData?.versions?.wsDevice?.ver?.toString()})") - if(servUpd) updItems.push("${!shrt ? "\n" : sBLANK}Server: (v${state.appData?.versions?.server?.ver?.toString()})") - } - return updItems -} - -Boolean getOk2Notify() { - Boolean smsOk = false - Boolean pushOk = false - Boolean pushOver = false - Boolean notifDevsOk = (((List)settings.notif_devs)?.size() > 0) - Boolean alexaMsg = ((Boolean)settings.notif_alexa_mobile) - Boolean daysOk = (List)settings.notif_days ? (isDayOfWeek((List)settings.notif_days)) : true - Boolean timeOk = notifTimeOk() - Boolean modesOk = (List)settings.notif_modes ? (isInMode((List)settings.notif_modes)) : true - Boolean result - result = (smsOk || pushOk || alexaMsg || notifDevsOk || pushOver) - if(!(daysOk && modesOk && timeOk)) { result = false } - logDebug("getOk2Notify() RESULT: $result | notifDevsOk: $notifDevsOk | smsOk: $smsOk | pushOk: $pushOk | pushOver: $pushOver | alexaMsg: $alexaMsg || daysOk: $daysOk | timeOk: $timeOk | modesOk: $modesOk") - return result -} - -Boolean notifTimeOk() { - Date startTime - Date stopTime - // these are quiet time start/stop - String startType = settings.notif_time_start_type - String stopType = settings.notif_time_stop_type - if(startType && stopType) { - startTime = startType == sTIME && settings.notif_time_start ? toDateTime(settings.notif_time_start) : null - stopTime = stopType == sTIME && settings.notif_time_stop ? toDateTime(settings.notif_time_stop) : null - } else { return true } - - Date now = new Date() - if(startType in lSUNRISESET || stopType in lSUNRISESET) { - Map sun = getSunriseAndSunset() - Long lsunset = sun.sunset.time - Long lsunrise = sun.sunrise.time - Long startoffset = settings.notif_time_start_offset ? settings.notif_time_start_offset*1000L : 0L - Long stopoffset = settings.notif_time_stop_offset ? settings.notif_time_stop_offset*1000L : 0L - if(startType in lSUNRISESET) { - Long startl = (startType == 'sunrise' ? lsunrise : lsunset) + startoffset - startTime = new Date(startl) - } - if(stopType in lSUNRISESET) { - Long stopl = (stopType == 'sunrise' ? lsunrise : lsunset) + stopoffset - stopTime = new Date(stopl) - } - } - - if(startTime && stopTime) { - Boolean not = startTime.getTime() > stopTime.getTime() - Boolean isBtwn = !timeOfDayIsBetween((not ? stopTime : startTime), (not ? startTime : stopTime), now, (TimeZone)location?.timeZone) - isBtwn = not ? !isBtwn : isBtwn - logTrace("NotifTimeOk ${isBtwn} | CurTime: (${now}) is${!isBtwn ? " NOT": sBLANK} between (${not ? stopTime:startTime} and ${not ? startTime:stopTime})") - return isBtwn - } else { return true } -} - -// Sends the notifications based on app settings -public Boolean sendMsg(String msgTitle, String msg, Boolean showEvt=true, Map pushoverMap=null, sms=null, push=null) { - logTrace("sendMsg() | msgTitle: ${msgTitle}, msg: ${msg}, showEvt: ${showEvt}") - String sentstr = sBLANK - Boolean sent = false - try { - String newMsg = msgTitle+": "+msg - String flatMsg = newMsg.replaceAll("\n", sSPACE) - if(!getOk2Notify()) { - logInfo("sendMsg: Message Skipped Notification not configured or During Quiet Time ($flatMsg)") - } else { - if((List)settings.notif_devs) { - sentstr = "Notification Devices" - ((List)settings.notif_devs).each { it?.deviceNotification(newMsg) } - sent = true - } - if(sent) { - state.lastMsg = flatMsg - updTsVal("lastMsgDt") - logDebug("sendMsg: Sent ${sentstr} (${flatMsg})") - } - } - } catch (ex) { - incrementCntByKey("appErrorCnt") - logError("sendMsg $sentstr Exception: ${ex}", false, ex) - } - return sent -} - -Boolean childInstallOk() { return (Boolean)state.childInstallOkFlag } - -public static String gitBranch() { return gitBranchFLD } - -static String getAppImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/echo-speaks/${gitBranchFLD}/resources/icons/${imgName}.png" } - -static String getPublicImg(String imgName) { return "https://raw.githubusercontent.com/tonesto7/SmartThings-tonesto7-public/master/resources/icons/${imgName}.png" } - -//static String sectTS(String t, String i = sNULL, Boolean bold=false) { return """

    ${i ? """ """ : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\\n", "
    ")}${bold ? "
    " : sBLANK}

    """ } - -static String sectH3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return """

    ${i ? """ """ : sBLANK} ${t?.replaceAll("\\n", "
    ")}

    ${st ?: sBLANK}""" } - -public static String paraTS(String title = sNULL, String body = sNULL, String img = sNULL, Map tOpts=[s: 'normal', c: 'black', b: true, u:true], Map bOpts = [s:'normal', c: sNULL, b: false]) { - String s = sBLANK - s += title ? "
    ${img != sNULL ? """ """ : sBLANK}${title}
    " : sBLANK - s += body ? "
    ${body}
    " : sBLANK - return s -} - -static String sectHead(String str, String img = sNULL) { return str ? "

    " + spanImgStr(img) + span(str, "darkorange", sNULL, true) + "

    " + "
    " : sBLANK } -static String sTS(String t, String i = sNULL, Boolean bold=false) { return "

    ${i ? " " : sBLANK} ${bold ? "" : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}

    " } -static String s3TS(String t, String st, String i = sNULL, String c=sCLR4D9) { return "

    ${i ? " " : sBLANK} ${t?.replaceAll("\n", "
    ")}

    ${st ? "${st}" : sBLANK}" } -static String pTS(String t, String i = sNULL, Boolean bold=true, String color=sNULL) { return "${color ? "
    " : sBLANK}${bold ? "" : sBLANK}${i ? " " : sBLANK}${t?.replaceAll("\n", "
    ")}${bold ? "
    " : sBLANK}${color ? "
    " : sBLANK}" } - -static String inTS1(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return spanSmBldUnd(str, clr, img) } -static String inTS(String str, String img = sNULL, String clr=sNULL, Boolean und=true) { return divSm(strUnder(str?.replaceAll("\n", sSPACE)?.replaceAll("
    ", sSPACE), und), clr, img) } - -// Root HTML Objects -static String span(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "${str}${br ? sLINEBR : sBLANK}" : sBLANK } -static String div(String str, String clr=sNULL, String sz=sNULL, Boolean bld=false, Boolean br=false) { return str ? "
    ${str}
    ${br ? sLINEBR : sBLANK}" : sBLANK } -static String spanImgStr(String img=sNULL) { return img ? span(" ") : sBLANK } -static String divImgStr(String str, String img=sNULL) { return str ? div(img ? spanImg(img) + span(str) : str) : sBLANK } -static String strUnder(String str, Boolean showUnd=true) { return str ? (showUnd ? "${str}" : str) : sBLANK } -static String getOkOrNotSymHTML(Boolean ok) { return ok ? span("(${okSymFLD})", sCLRGRN2) : span("(${notOkSymFLD})", sCLRRED2) } -static String htmlLine(String color=sCLR4D9) { return "
    " } -static String lineBr(Boolean show=true) { return show ? sLINEBR : sBLANK } -static String inputFooter(String str, String clr=sCLR4D9, Boolean noBr=false) { return str ? lineBr(!noBr) + divSmBld(str, clr) : sBLANK } -static String inactFoot(String str) { return str ? inputFooter(str, sCLRGRY, true) : sBLANK } -static String actFoot(String str) { return str ? inputFooter(str, sCLR4D9, false) : sBLANK } -static String optPrefix() { return spanSm(" (Optional)", "violet") } -// - -// Custom versions of the root objects above -static String spanBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true) : sBLANK } -static String spanBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, true, true) : sBLANK } -static String spanBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sNULL, false, true) : sBLANK } -static String spanSm(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL) : sBLANK } -static String spanSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, false, true) : sBLANK } -static String spanSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true) : sBLANK } -static String spanSmBldUnd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(strUnder(str), clr, sSMALL, true) : sBLANK } -static String spanSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sSMALL, true, true) : sBLANK } -static String spanMd(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM) : sBLANK } -static String spanMdBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, false, true) : sBLANK } -static String spanMdBld(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true) : sBLANK } -static String spanMdBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? spanImgStr(img) + span(str, clr, sMEDIUM, true, true) : sBLANK } - - -static String divBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, false) : sBLANK } -static String divBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, true, true) : sBLANK } -static String divBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sNULL, false, true) : sBLANK } -static String divSm(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL) : sBLANK } -static String divSmBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, false, true) : sBLANK } -static String divSmBld(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true) : sBLANK } -static String divSmBldBr(String str, String clr=sNULL, String img=sNULL) { return str ? div(spanImgStr(img) + span(str), clr, sSMALL, true, true) : sBLANK } - - -def appFooter() { - section() { - paragraph htmlLine() - paragraph """
    Echo Speaks


    Please consider donating if you find this integration useful.
    """ - } -} - -static String actChildName(){ return "Echo Speaks - Actions" } -static String zoneChildName(){ return "Echo Speaks - Zones" } -//static String zoneChildDeviceName(){ return "Echo Speaks - Zones" } -static String documentationUrl() { return "https://tonesto7.github.io/echo-speaks-docs" } -static String videoUrl() { return "https://www.youtube.com/watch?v=wQPPlTFaGb4&ab_channel=SimplySmart123%E2%9C%85" } -static String textDonateUrl() { return "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HWBN4LB9NMHZ4" } -def updateDocsInput() { href url: documentationUrl(), style: sEXTNRL, required: false, title: inTS1("View Documentation", "documentation"), description: inactFoot(sTTP) } - -String getAppEndpointUrl(subPath) { return "${getApiServerUrl()}/${getHubUID()}/apps/${app?.id}${subPath ? "/${subPath}" : sBLANK}?access_token=${state.accessToken}".toString() } - -String getLocalEndpointUrl(subPath) { return "${getLocalApiServerUrl()}/apps/${app?.id}${subPath ? "/${subPath}" : sBLANK}?access_token=${state.accessToken}" } - -/****************************************** -| Changelog Logic -******************************************/ -Boolean showDonationOk() { return ((Boolean)state.isInstalled && !(Boolean)getInstData('shownDonation') && getDaysSinceUpdated() >= 30) } - -Integer getDaysSinceUpdated() { - String updDt = getInstData('updatedDt') - updDt = updDt ?: sNULL - if(updDt == sNULL || updDt == "Not Set") { - updInstData("updatedDt", getDtNow()) - return 0 - } else { - Date start = Date.parse("E MMM dd HH:mm:ss z yyyy", updDt) - Date stop = new Date() - if(start && stop) { return (stop - start) } - return 0 - } -} - -String changeLogData() { - String txt = (String) getWebData([uri: "https://raw.githubusercontent.com/tonesto7/echo-speaks/${gitBranchFLD}/CHANGELOG.html", contentType: "text/plain; charset=UTF-8", timeout: 20], "changelog", true) - return txt?.toString() -} - -Boolean showChgLogOk() { return ((Boolean) state.isInstalled && !((String) state.curAppVer == appVersionFLD && (Boolean) getInstData('shownChgLog')) ) } - -def changeLogPage() { - return dynamicPage(name: "changeLogPage", title: sBLANK, nextPage: "mainPage", install: false) { - section(sectHead("Release Notes:", getAppImg("change_log"))) { paragraph "${changeLogData()}" } - state.curAppVer = appVersionFLD - updInstData("shownChgLog", true) - } -} - -/****************************************** -| METRIC Logic -******************************************/ -String getFbMetricsUrl() { return state.appData?.settings?.database?.metricsUrl ?: "https://echo-speaks-metrics.firebaseio.com/" } -String getFbConfigUrl() { return state.appData?.settings?.database?.configUrl ?: "https://echospeaks-config.firebaseio.com/" } -Boolean metricsOk() { (!(Boolean)settings.optOutMetrics && state.appData?.settings?.sendMetrics) } -private generateGuid() { if(!(String)state.appGuid) { state.appGuid = UUID?.randomUUID()?.toString() } } -void sendInstallData() { settingUpdate("sendMetricsNow", sFALSE, sBOOL); if(metricsOk()) { Boolean aa=sendFirebaseData(getFbMetricsUrl(), "/clients/${(String)state.appGuid}.json", createMetricsDataJson(), "put", "heartbeat") } } -Boolean removeInstallData() { return removeFirebaseData("/clients/${(String)state.appGuid}.json") } -Boolean sendFirebaseData(String url, String path, String data, String cmdType=null, String type=null) { logTrace("sendFirebaseData(${path}, ${data}, $cmdType, $type"); return queueFirebaseData(url, path, data, cmdType, type) } - -Boolean queueFirebaseData(String url, String path, String data, String cmdType=sNULL, String type=sNULL) { - // logTrace("queueFirebaseData(${path}, ${data}, $cmdType, $type") - Boolean result = false - String json = new JsonOutput().prettyPrint(data) - Map params = [uri: url, path: path, requestContentType: sAPPJSON, contentType: sAPPJSON, timeout: 20, body: json] - String typeDesc = type ?: "Data" - try { - if(!cmdType || cmdType == "put") { - execAsyncCmd(cmdType, "processFirebaseResponse", params, [type: typeDesc]) - result = true - } else if (cmdType == "post") { - execAsyncCmd(cmdType, "processFirebaseResponse", params, [type: typeDesc]) - result = true - } else { logWarn("queueFirebaseData UNKNOWN cmdType: ${cmdType}") } - - } catch(ex) { logError("queueFirebaseData (type: $typeDesc) Exception: ${ex}", false, ex) } - return result -} - -Boolean removeFirebaseData(String pathVal) { - logTrace("removeFirebaseData(${pathVal})") - Boolean result = true - try { - httpDelete(uri: getFbMetricsUrl(), path: pathVal) { resp -> - logDebug("Remove Firebase | resp: ${resp?.status}") - } - } catch (ex) { - if(ex instanceof groovyx.net.http.HttpResponseException ) { - logError("removeFirebaseData Response Exception: ${ex}", false, ex) - } else { - logError("removeFirebaseData Exception: ${ex}", false, ex) - result = false - } - } - return result -} - -void processFirebaseResponse(resp, Map data) { - String mName="processFirebaseResponse" - String typeDesc = (String)data.type - logTrace(mName+"(${typeDesc})") - try { - if(resp?.status == 200) { - logDebug(mName+": ${typeDesc} Data Sent SUCCESSFULLY") - if(typeDesc == "heartbeat") { updTsVal("lastMetricUpdDt") } - updInstData("sentMetrics", true) - } else if(resp?.status == 400) { - logError(mName+": 'Bad Request': ${resp?.status}") - } else { logWarn(mName+": 'Unexpected' Response: ${resp?.status}") } - } catch(ex) { - logError(mName+" (type: $typeDesc) Exception: ${ex}", false, ex) - } -} - -def renderMetricData() { - try { - String jsonIn = createMetricsDataJson() - String json = "Not Data Found" - if(jsonIn) { - json = new JsonOutput().prettyPrint(jsonIn) - } - render contentType: sAPPJSON, data: json, status: 200 - } catch (ex) { logError("renderMetricData Exception: ${ex}", false, ex) } -} - -private Map getSkippedDevsAnon() { - Map res = [:] - Map sDevs = (Map)state.skippedDevices ?: [:] - sDevs.each { k, v-> if(!res.containsKey((String)v?.type)) { res[(String)v?.type] = v } } - return res -} - -private String createMetricsDataJson() { - try { - generateGuid() - Map swVer = (Map)state.codeVersions - Map deviceUsageMap = [:] - Map deviceErrorMap = [:] - ((List)getChildDevices())?.each { d-> - Map obj = d?.getDeviceMetrics() - if(obj?.usage?.size()) { obj?.usage?.each { String k, v-> deviceUsageMap[k] = (deviceUsageMap[k] ? deviceUsageMap[k] + v : v) } } - if(obj?.errors?.size()) { obj?.errors?.each { String k, v-> deviceErrorMap[k] = (deviceErrorMap[k] ? deviceErrorMap[k] + v : v) } } - } - Map actData = [:] - Integer actCnt = 0 - getActionApps()?.each { a-> actData[actCnt] = a?.getActionMetrics(); actCnt++ } - Map zoneData = [:] - Integer zoneCnt = 0 - getZoneApps()?.each { a-> zoneData[zoneCnt] = a?.getZoneMetrics(); zoneCnt++ } - Map dataObj = [ - guid: (String)state.appGuid, - datetime: getDtNow(), - installDt: (String)getInstData('dt'), - updatedDt: (String)getInstData('updatedDt'), - timeZone: location?.timeZone?.ID?.toString(), - hubPlatform: platformFLD, - authValid: (Boolean)state.authValid, - stateUsage: "${stateSizePerc()}%", - amazonDomain: settings?.amazonDomain, - serverPlatform: (Boolean)getServerItem("onHeroku") ? "Cloud" : "Local", - versions: [app: appVersionFLD, server: (String)swVer?.server ?: "N/A", actions: (String)swVer?.actionApp ?: "N/A", zones: (String)swVer?.zoneApp ?: "N/A", device: (String)swVer?.echoDevice ?: "N/A", /*socket: (String)swVer?.wsDevice ?: "N/A"*/], - detections: [skippedDevices: getSkippedDevsAnon()], - actions: actData, - zones: zoneData, - counts: [ - deviceStyleCnts: (Map)state.deviceStyleCnts ?: [:], - appHeartbeatCnt: state.appHeartbeatCnt ?: 0, - getCookieCnt: state.getCookieCnt ?: 0, - appErrorCnt: state.appErrorCnt ?: 0, - deviceErrors: deviceErrorMap ?: [:], - deviceUsage: deviceUsageMap ?: [:] - ] - ] - - String json = new JsonOutput().toJson(dataObj) - // log.debug "dataObj: $dataObj" - return json - } catch (ex) { - logError("createMetricsDataJson: Exception: ${ex}", false, ex) - } - return sNULL -} - -void incrementCntByKey(String key) { - Long evtCnt = (Long)state."${key}" - evtCnt = evtCnt != null ? evtCnt : 0L - evtCnt++ - // logTrace("${key?.toString()?.capitalize()}: $evtCnt", true) - state."${key}" = evtCnt -} - -// ****************************************** -// APP/DEVICE Version Functions -// ****************************************** -static Boolean codeUpdIsAvail(String newVer, String curVer, String type) { - Boolean result = false - def latestVer - if(newVer && curVer) { - List versions = [newVer, curVer] - if(newVer != curVer) { - latestVer = versions?.max { a, b -> - List verA = a?.tokenize('.'); List verB = b?.tokenize('.'); Integer commonIndices = Math.min(verA?.size(), verB?.size()) - for (Integer i = 0; i < commonIndices; ++i) { if(verA[i]?.toInteger() != verB[i]?.toInteger()) { return verA[i]?.toInteger() <=> verB[i]?.toInteger() } } - verA?.size() <=> verB?.size() - } - result = (latestVer == newVer) - } - } - return result -} - -Boolean appUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.mainApp && codeUpdIsAvail((String)state.appData.versions?.mainApp?.ver, (String)state.codeVersions.mainApp, "main_app")) } -Boolean actionUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.actionApp && codeUpdIsAvail((String)state.appData.versions?.actionApp?.ver, (String)state.codeVersions.actionApp, "action_app")) } -Boolean zoneUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.zoneApp && codeUpdIsAvail((String)state.appData.versions?.zoneApp?.ver, (String)state.codeVersions.zoneApp, "zone_app")) } -Boolean zoneChildDevUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.zoneEchoDevice && codeUpdIsAvail((String)state.appData.versions?.zoneChildDevice?.ver, (String)state.codeVersions.zoneEchoDevice, "zone_child_dev")) } -Boolean echoDevUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.echoDevice && codeUpdIsAvail((String)state.appData.versions?.echoDevice?.ver, (String)state.codeVersions.echoDevice, "dev")) } -// Boolean socketUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.wsDevice && codeUpdIsAvail((String)state.appData.versions?.wsDevice?.ver, (String)state.codeVersions.wsDevice, "socket")) } -Boolean serverUpdAvail() { return ((Map)state.appData?.versions && (String)state.codeVersions?.server && codeUpdIsAvail((String)state.appData.versions?.server?.ver, (String)state.codeVersions.server, "server")) } - -static Integer versionStr2Int(String str) { return str ? str.replaceAll("\\.", sBLANK)?.toInteger() : null } - -void checkVersionData(Boolean now = false) { //This reads a JSON file from GitHub with version numbers - Integer lastUpd = getLastTsValSecs("lastAppDataUpdDt") - if (now || !state.appData || (lastUpd > (3600*6))) { - if(now && (lastUpd < 300)) { return } - getConfigData() - getNoticeData() - } -} - -void getConfigData() { - Map params = [ - uri: "https://raw.githubusercontent.com/tonesto7/echo-speaks/${gitBranchFLD}/resources/appData.json", - contentType: sAPPJSON, - timeout: 20 - ] - Map data = (Map)getWebData(params, "appData", false) - if(data) { - state.appData = data - updTsVal("lastAppDataUpdDt") - logDebug("Successfully Retrieved (v${data?.appDataVer}) of AppData Content from GitHub Repo...") - } -} - -void getNoticeData() { - Map params = [ - uri: "https://raw.githubusercontent.com/tonesto7/echo-speaks/${gitBranchFLD}/notices.json", - contentType: sAPPJSON, - timeout: 20 - ] - Map data = (Map)getWebData(params, "noticeData", false) - if(data) { - state.noticeData = data - logDebug("Successfully Retrieved Developer Notices from GitHub Repo...") - } -} - -private getWebData(Map params, String desc, Boolean text=true) { - try { - // log.trace("getWebData: ${desc} data") - httpGet(params) { resp -> - if(resp?.status != 200) logWarn("${resp?.status} $params") - if(resp?.data) { - if(text) { return resp.data.text?.toString() } - return resp.data - } - } - } catch (ex) { - incrementCntByKey("appErrorCnt") - if(ex instanceof groovyx.net.http.HttpResponseException) { - logWarn("${desc} file not found") - } else { logError("getWebData(params: $params, desc: $desc, text: $text) Exception: ${ex}", false, ex) } - return "${desc} info not found" - } -} - - -static Map getAvailableSounds() { - return getAvailableSoundsFLD -} - -// https://developer.amazon.com/en-US/docs/alexa/custom-skills/ask-soundlibrary.html -// TODO: https://m.media-amazon.com/images/G/01/mobile-apps/dex/ask-tech-docs/ask-soundlibrary._TTH_.json -// send this to speak command: