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