Update speaker-control.groovy
[smartapps.git] / official / speaker-control.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  *  in compliance with the License. You may obtain a copy of the License at:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11  *  for the specific language governing permissions and limitations under the License.
12  *
13  *  Speaker Control
14  *
15  *  Author: SmartThings
16  *
17  *  Date: 2013-12-10
18  */
19 definition(
20     name: "Speaker Control",
21     namespace: "smartthings",
22     author: "SmartThings",
23     description: "Play or pause your Speaker when certain actions take place in your home.",
24     category: "SmartThings Labs",
25     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
26     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
27 )
28
29 preferences {
30         page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true)
31         page(name: "timeIntervalInput", title: "Only during a certain time") {
32                 section {
33                         input "starting", "time", title: "Starting", required: false
34                         input "ending", "time", title: "Ending", required: false
35                 }
36         }
37 }
38
39 // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
40 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
41 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
42 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
43 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
44 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
45 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
46 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
47 // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
48 // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
49 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
50 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
51 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
52
53 def mainPage() {
54         dynamicPage(name: "mainPage") {
55                 def anythingSet = anythingSet()
56                 if (anythingSet) {
57                         section("When..."){
58                                 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
59                                 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
60                                 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
61                                 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
62                                 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
63                                 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
64                                 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
65                                 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
66                                 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
67                                 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
68                                 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
69                                 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
70                                 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
71                         }
72                 }
73                 section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){
74                         ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
75                         ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
76                         ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
77                         ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
78                         ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
79                         ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
80                         ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
81                         ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
82                         ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
83                         ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
84                         ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
85                         ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
86                         ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
87                 }
88                 section("Perform this action"){
89                         input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [
90                                 "Play",
91                                 "Stop Playing",
92                                 "Toggle Play/Pause",
93                                 "Skip to Next Track",
94                                 "Play Previous Track"
95                         ]
96                 }
97                 section {
98                         input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true
99                 }
100                 section("More options", hideable: true, hidden: true) {
101                         input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false
102                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
103                         href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
104                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
105                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
106                         if (settings.modes) {
107                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
108             }
109                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
110                 }
111                 section([mobileOnly:true]) {
112                         label title: "Assign a name", required: false
113                         mode title: "Set for specific mode(s)"
114                 }
115         }
116 }
117
118 private anythingSet() {
119         for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) {
120                 if (settings[name]) {
121                         return true
122                 }
123         }
124         return false
125 }
126
127 private ifUnset(Map options, String name, String capability) {
128         if (!settings[name]) {
129                 input(options, name, capability)
130         }
131 }
132
133 private ifSet(Map options, String name, String capability) {
134         if (settings[name]) {
135                 input(options, name, capability)
136         }
137 }
138
139 def installed() {
140         log.debug "Installed with settings: ${settings}"
141         subscribeToEvents()
142 }
143
144 def updated() {
145         log.debug "Updated with settings: ${settings}"
146         unsubscribe()
147         unschedule()
148         subscribeToEvents()
149 }
150
151 def subscribeToEvents() {
152         log.trace "subscribeToEvents()"
153         subscribe(app, appTouchHandler)
154         subscribe(contact, "contact.open", eventHandler)
155         subscribe(contactClosed, "contact.closed", eventHandler)
156         subscribe(acceleration, "acceleration.active", eventHandler)
157         subscribe(motion, "motion.active", eventHandler)
158         subscribe(mySwitch, "switch.on", eventHandler)
159         subscribe(mySwitchOff, "switch.off", eventHandler)
160         subscribe(arrivalPresence, "presence.present", eventHandler)
161         subscribe(departurePresence, "presence.not present", eventHandler)
162         subscribe(smoke, "smoke.detected", eventHandler)
163         subscribe(smoke, "smoke.tested", eventHandler)
164         subscribe(smoke, "carbonMonoxide.detected", eventHandler)
165         subscribe(water, "water.wet", eventHandler)
166         subscribe(button1, "button.pushed", eventHandler)
167
168         if (triggerModes) {
169                 subscribe(location, modeChangeHandler)
170         }
171
172         if (timeOfDay) {
173                 schedule(timeOfDay, scheduledTimeHandler)
174         }
175 }
176
177 def eventHandler(evt) {
178         if (allOk) {
179                 def lastTime = state[frequencyKey(evt)]
180                 if (oncePerDayOk(lastTime)) {
181                         if (frequency) {
182                                 if (lastTime == null || now() - lastTime >= frequency * 60000) {
183                                         takeAction(evt)
184                                 }
185                                 else {
186                                         log.debug "Not taking action because $frequency minutes have not elapsed since last action"
187                                 }
188                         }
189                         else {
190                                 takeAction(evt)
191                         }
192                 }
193                 else {
194                         log.debug "Not taking action because it was already taken today"
195                 }
196         }
197 }
198
199 def modeChangeHandler(evt) {
200         log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
201         if (evt.value in triggerModes) {
202                 eventHandler(evt)
203         }
204 }
205
206 def scheduledTimeHandler() {
207         eventHandler(null)
208 }
209
210 def appTouchHandler(evt) {
211         takeAction(evt)
212 }
213
214 private takeAction(evt) {
215         log.debug "takeAction($actionType)"
216         def options = [:]
217         if (volume) {
218                 sonos.setLevel(volume as Integer)
219                 options.delay = 1000
220         }
221
222         switch (actionType) {
223                 case "Play":
224                         options ? sonos.on(options) : sonos.on()
225                         break
226                 case "Stop Playing":
227                         options ? sonos.off(options) : sonos.off()
228                         break
229                 case "Toggle Play/Pause":
230                         def currentStatus = sonos.currentValue("status")
231                         if (currentStatus == "playing") {
232                                 options ? sonos.pause(options) : sonos.pause()
233                         }
234                         else {
235                                 options ? sonos.play(options) : sonos.play()
236                         }
237                         break
238                 case "Skip to Next Track":
239                         options ? sonos.nextTrack(options) : sonos.nextTrack()
240                         break
241                 case "Play Previous Track":
242                         options ? sonos.previousTrack(options) : sonos.previousTrack()
243                         break
244                 default:
245                         log.error "Action type '$actionType' not defined"
246         }
247
248         if (frequency) {
249                 state.lastActionTimeStamp = now()
250         }
251 }
252
253 private frequencyKey(evt) {
254         //evt.deviceId ?: evt.value
255         "lastActionTimeStamp"
256 }
257
258 private dayString(Date date) {
259         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
260         if (location.timeZone) {
261                 df.setTimeZone(location.timeZone)
262         }
263         else {
264                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
265         }
266         df.format(date)
267 }
268
269 private oncePerDayOk(Long lastTime) {
270         def result = true
271         if (oncePerDay) {
272                 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
273                 log.trace "oncePerDayOk = $result"
274         }
275         result
276 }
277
278 // TODO - centralize somehow
279 private getAllOk() {
280         modeOk && daysOk && timeOk
281 }
282
283 private getModeOk() {
284         def result = !modes || modes.contains(location.mode)
285         log.trace "modeOk = $result"
286         result
287 }
288
289 private getDaysOk() {
290         def result = true
291         if (days) {
292                 def df = new java.text.SimpleDateFormat("EEEE")
293                 if (location.timeZone) {
294                         df.setTimeZone(location.timeZone)
295                 }
296                 else {
297                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
298                 }
299                 def day = df.format(new Date())
300                 result = days.contains(day)
301         }
302         log.trace "daysOk = $result"
303         result
304 }
305
306 private getTimeOk() {
307         def result = true
308         if (starting && ending) {
309                 def currTime = now()
310                 def start = timeToday(starting, location?.timeZone).time
311                 def stop = timeToday(ending, location?.timeZone).time
312                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
313         }
314         log.trace "timeOk = $result"
315         result
316 }
317
318 private hhmm(time, fmt = "h:mm a")
319 {
320         def t = timeToday(time, location.timeZone)
321         def f = new java.text.SimpleDateFormat(fmt)
322         f.setTimeZone(location.timeZone ?: timeZone(time))
323         f.format(t)
324 }
325
326 private timeIntervalLabel()
327 {
328         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
329 }
330 // TODO - End Centralize
331