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