Update once-a-day.groovy
[smartapps.git] / third-party / DoubleTapModeChange.groovy
1 /**
2  *  Double Tap Switch for "Hello, Home" Action
3  *
4  *  Copyright 2014 George Sudarkoff
5  *
6  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7  *  in compliance with the License. You may obtain a copy of the License at:
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13  *  for the specific language governing permissions and limitations under the License.
14  *
15  */
16
17 definition(
18     name: "Double Tap Switch for 'Hello, Home' Action",
19     namespace: "com.sudarkoff",
20     author: "George Sudarkoff",
21     description: "Execute a 'Hello, Home' action when an existing switch is tapped twice in a row.",
22     category: "Mode Magic",
23     iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances17-icn.png",
24     iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Appliances/appliances17-icn@2x.png"
25 )
26
27 preferences {
28     page (name: "configApp")
29
30     page (name: "timeIntervalInput", title: "Only during a certain time") {
31         section {
32             input "starting", "time", title: "Starting", required: false
33             input "ending", "time", title: "Ending", required: false
34         }
35     }
36 }
37
38 def configApp() {
39     dynamicPage(name: "configApp", install: true, uninstall: true) {
40         section ("When this switch is double-tapped...") {
41             input "master", "capability.switch", required: true
42         }
43
44         def phrases = location.helloHome?.getPhrases()*.label
45         if (phrases) {
46             phrases.sort()
47             section("Perform this actions...") {
48                 input "onPhrase", "enum", title: "ON action", required: false, options: phrases
49                 input "offPhrase", "enum", title: "OFF action", required: false, options: phrases
50             }
51         }
52
53         section (title: "Notification method") {
54             input "sendPushMessage", "bool", title: "Send a push notification?"
55         }
56
57         section (title: "More Options", hidden: hideOptionsSection(), hideable: true) {
58             input "phone", "phone", title: "Additionally, also send a text message to:", required: false
59             input "flashLights", "capability.switch", title: "And flash these lights", multiple: true, required: false
60
61             def timeLabel = timeIntervalLabel()
62             href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : null
63
64             input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
65                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
66         }
67
68         section([mobileOnly:true]) {
69             label title: "Assign a name", required: false
70             mode title: "Set for specific mode(s)", required: false
71         }
72     }
73 }
74
75 def installed()
76 {
77     initialize()
78 }
79
80 def updated()
81 {
82     unsubscribe()
83     initialize()
84 }
85
86 def initialize()
87 {
88     if (customName) {
89         state.currentAppLabel = customName
90     }
91     subscribe(master, "switch", switchHandler, [filterEvents: false])
92 }
93
94 def switchHandler(evt) {
95     log.info evt.value
96
97     if (allOk) {
98         // use Event rather than DeviceState because we may be changing DeviceState to only store changed values
99         def recentStates = master.eventsSince(new Date(now() - 4000), [all:true, max: 10]).findAll{it.name == "switch"}
100         log.debug "${recentStates?.size()} STATES FOUND, LAST AT ${recentStates ? recentStates[0].dateCreated : ''}"
101
102         if (evt.isPhysical()) {
103             if (evt.value == "on" && lastTwoStatesWere("on", recentStates, evt)) {
104                 log.debug "detected two taps, execute ON phrase"
105                 location.helloHome.execute(settings.onPhrase)
106                 def message = "${location.name} executed ${settings.onPhrase} because ${evt.title} was tapped twice."
107                 send(message)
108                 flashLights()
109             } else if (evt.value == "off" && lastTwoStatesWere("off", recentStates, evt)) {
110                 log.debug "detected two taps, execute OFF phrase"
111                 location.helloHome.execute(settings.offPhrase)
112                 def message = "${location.name} executed ${settings.offPhrase} because ${evt.title} was tapped twice."
113                 send(message)
114                 flashLights()
115             }
116         }
117         else {
118             log.trace "Skipping digital on/off event"
119         }
120     }
121 }
122
123 private lastTwoStatesWere(value, states, evt) {
124     def result = false
125     if (states) {
126         log.trace "unfiltered: [${states.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
127         def onOff = states.findAll { it.isPhysical() || !it.type }
128         log.trace "filtered:   [${onOff.collect{it.dateCreated + ':' + it.value}.join(', ')}]"
129
130         // This test was needed before the change to use Event rather than DeviceState. It should never pass now.
131         if (onOff[0].date.before(evt.date)) {
132             log.warn "Last state does not reflect current event, evt.date: ${evt.dateCreated}, state.date: ${onOff[0].dateCreated}"
133             result = evt.value == value && onOff[0].value == value
134         }
135         else {
136             result = onOff.size() > 1 && onOff[0].value == value && onOff[1].value == value
137         }
138     }
139     result
140 }
141
142 private send(msg) {
143     if (sendPushMessage != "No") {
144         sendPush(msg)
145     }
146
147     if (phone) {
148         sendSms(phone, msg)
149     }
150
151     log.debug msg
152 }
153
154 private flashLights() {
155     def doFlash = true
156     def onFor = onFor ?: 200
157     def offFor = offFor ?: 200
158     def numFlashes = numFlashes ?: 2
159
160     if (state.lastActivated) {
161         def elapsed = now() - state.lastActivated
162         def sequenceTime = (numFlashes + 1) * (onFor + offFor)
163         doFlash = elapsed > sequenceTime
164     }
165
166     if (doFlash) {
167         state.lastActivated = now()
168         def initialActionOn = flashLights.collect{it.currentSwitch != "on"}
169         def delay = 1L
170         numFlashes.times {
171             flashLights.eachWithIndex {s, i ->
172                 if (initialActionOn[i]) {
173                     s.on(delay: delay)
174                 }
175                 else {
176                     s.off(delay:delay)
177                 }
178             }
179             delay += onFor
180             flashLights.eachWithIndex {s, i ->
181                 if (initialActionOn[i]) {
182                     s.off(delay: delay)
183                 }
184                 else {
185                     s.on(delay:delay)
186                 }
187             }
188             delay += offFor
189         }
190     }
191 }
192
193 // execution filter methods
194 private getAllOk() {
195     modeOk && daysOk && timeOk
196 }
197
198 private getModeOk() {
199     def result = !modes || modes.contains(location.mode)
200     result
201 }
202
203 private getDaysOk() {
204     def result = true
205     if (days) {
206         def df = new java.text.SimpleDateFormat("EEEE")
207         if (location.timeZone) {
208             df.setTimeZone(location.timeZone)
209         }
210         else {
211             df.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"))
212         }
213         def day = df.format(new Date())
214         result = days.contains(day)
215     }
216     result
217 }
218
219 private getTimeOk() {
220     def result = true
221     if (starting && ending) {
222         def currTime = now()
223         def start = timeToday(starting).time
224         def stop = timeToday(ending).time
225         result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
226     }
227     log.trace "timeOk = $result"
228     result
229 }
230
231 private hideOptionsSection() {
232     (phone || starting || ending || flashLights || customName || days || modes) ? false : true
233 }
234
235 private hhmm(time, fmt = "h:mm a")
236 {
237     def t = timeToday(time, location.timeZone)
238     def f = new java.text.SimpleDateFormat(fmt)
239     f.setTimeZone(location.timeZone ?: timeZone(time))
240     f.format(t)
241 }
242
243 private timeIntervalLabel() {
244     (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
245 }
246
247