Update thermostat.groovy
[smartapps.git] / official / speaker-weather-forecast.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 Weather Forecast
14  *
15  *  Author: SmartThings
16  *  Date: 2014-1-29
17  */
18 definition(
19     name: "Speaker Weather Forecast",
20     namespace: "smartthings",
21     author: "SmartThings",
22     description: "Play a weather report through your Speaker when the mode changes or other events occur",
23     category: "SmartThings Labs",
24     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
25     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
26 )
27
28 preferences {
29         page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true)
30         page(name: "chooseTrack", title: "Select a song or station")
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("Play weather report 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                 def hideable = anythingSet //|| app.installationState == "COMPLETE"
74                 def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..."
75
76                 section(sectionTitle, hideable: hideable, hidden: true){
77                         ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
78                         ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
79                         ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
80                         ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
81                         ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
82                         ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
83                         ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
84                         ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
85                         ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
86                         ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
87                         ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
88                         ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
89                         ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
90                 }
91                 section {
92                         input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true,
93                                 options: [
94                                         ["0": "Current Conditions"],
95                                         ["1": "Today's Forecast"],
96                                         ["2": "Tonight's Forecast"],
97                                         ["3": "Tomorrow's Forecast"],
98                                 ]
99                         )
100                 }
101                 section {
102                         input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
103                 }
104                 section("More options", hideable: true, hidden: true) {
105                         input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
106                         //href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete"
107
108                         //input "zipCode", "text", title: "Zip Code", required: false
109                         input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
110                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
111                         //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
112                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
113                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
114                         //if (settings.modes) {
115                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
116             //}
117                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
118                 }
119                 section([mobileOnly:true]) {
120                         label title: "Assign a name", required: false
121                         mode title: "Set for specific mode(s)"
122                 }
123         }
124 }
125
126 def chooseTrack() {
127         dynamicPage(name: "chooseTrack") {
128                 section{
129                         input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
130                 }
131         }
132 }
133
134 private anythingSet() {
135         for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) {
136                 if (settings[name]) {
137                         return true
138                 }
139         }
140         return false
141 }
142
143 private ifUnset(Map options, String name, String capability) {
144         if (!settings[name]) {
145                 input(options, name, capability)
146         }
147 }
148
149 private ifSet(Map options, String name, String capability) {
150         if (settings[name]) {
151                 input(options, name, capability)
152         }
153 }
154
155 def installed() {
156         log.debug "Installed with settings: ${settings}"
157         subscribeToEvents()
158 }
159
160 def updated() {
161         log.debug "Updated with settings: ${settings}"
162         unsubscribe()
163         unschedule()
164         subscribeToEvents()
165 }
166
167 def subscribeToEvents() {
168         subscribe(app, appTouchHandler)
169         subscribe(contact, "contact.open", eventHandler)
170         subscribe(contactClosed, "contact.closed", eventHandler)
171         subscribe(acceleration, "acceleration.active", eventHandler)
172         subscribe(motion, "motion.active", eventHandler)
173         subscribe(mySwitch, "switch.on", eventHandler)
174         subscribe(mySwitchOff, "switch.off", eventHandler)
175         subscribe(arrivalPresence, "presence.present", eventHandler)
176         subscribe(departurePresence, "presence.not present", eventHandler)
177         subscribe(smoke, "smoke.detected", eventHandler)
178         subscribe(smoke, "smoke.tested", eventHandler)
179         subscribe(smoke, "carbonMonoxide.detected", eventHandler)
180         subscribe(water, "water.wet", eventHandler)
181         subscribe(button1, "button.pushed", eventHandler)
182
183         if (triggerModes) {
184                 subscribe(location,modeChangeHandler)
185         }
186
187         if (timeOfDay) {
188                 schedule(timeOfDay, scheduledTimeHandler)
189         }
190
191         if (song) {
192                 saveSelectedSong()
193         }
194 }
195
196 def eventHandler(evt) {
197         log.trace "eventHandler($evt?.name: $evt?.value)"
198         if (allOk) {
199                 log.trace "allOk"
200                 def lastTime = state[frequencyKey(evt)]
201                 if (oncePerDayOk(lastTime)) {
202                         if (frequency) {
203                                 if (lastTime == null || now() - lastTime >= frequency * 60000) {
204                                         takeAction(evt)
205                                 }
206                                 else {
207                                         log.debug "Not taking action because $frequency minutes have not elapsed since last action"
208                                 }
209                         }
210                         else {
211                                 takeAction(evt)
212                         }
213                 }
214                 else {
215                         log.debug "Not taking action because it was already taken today"
216                 }
217         }
218 }
219
220 def modeChangeHandler(evt) {
221         log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
222         if (evt.value in triggerModes) {
223                 eventHandler(evt)
224         }
225 }
226
227 def scheduledTimeHandler() {
228         eventHandler(null)
229 }
230
231 def appTouchHandler(evt) {
232         takeAction(evt)
233 }
234
235 private takeAction(evt) {
236
237         //loadText()
238
239         if (song) {
240                 //sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
241         }
242         else if (resumePlaying){
243                 //sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
244         }
245         else if (volume) {
246                 //sonos.playTrackAtVolume(state.sound.uri, volume)
247         }
248         else {
249                 //sonos.playTrack(state.sound.uri)
250         }
251
252         if (frequency || oncePerDay) {
253                 state[frequencyKey(evt)] = now()
254         }
255 }
256
257 private songOptions() {
258
259         // Make sure current selection is in the set
260
261         def options = new LinkedHashSet()
262         if (state.selectedSong?.station) {
263                 options << state.selectedSong.station
264         }
265         else if (state.selectedSong?.description) {
266                 // TODO - Remove eventually? 'description' for backward compatibility
267                 options << state.selectedSong.description
268         }
269
270         // Query for recent tracks
271         def states = sonos.statesSince("trackData", new Date(0), [max:30])
272         def dataMaps = states.collect{it.jsonValue}
273         options.addAll(dataMaps.collect{it.station})
274
275         log.trace "${options.size()} songs in list"
276         options.take(20) as List
277 }
278
279 private saveSelectedSong() {
280         try {
281                 def thisSong = song
282                 log.info "Looking for $thisSong"
283                 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
284                 log.info "Searching ${songs.size()} records"
285
286                 def data = songs.find {s -> s.station == thisSong}
287                 log.info "Found ${data?.station}"
288                 if (data) {
289                         state.selectedSong = data
290                         log.debug "Selected song = $state.selectedSong"
291                 }
292                 else if (song == state.selectedSong?.station) {
293                         log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
294                 }
295                 else {
296                         log.warn "Selected song '$song' not found"
297                 }
298         }
299         catch (Throwable t) {
300                 log.error t
301         }
302 }
303
304 private frequencyKey(evt) {
305         "lastActionTimeStamp"
306 }
307
308 private dayString(Date date) {
309         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
310         if (location.timeZone) {
311                 df.setTimeZone(location.timeZone)
312         }
313         else {
314                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
315         }
316         df.format(date)
317 }
318
319 private oncePerDayOk(Long lastTime) {
320         def result = true
321         if (oncePerDay) {
322                 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
323                 log.trace "oncePerDayOk = $result"
324         }
325         result
326 }
327
328 // TODO - centralize somehow
329 private getAllOk() {
330         modeOk && daysOk && timeOk
331 }
332
333 private getModeOk() {
334         def result = !modes || modes.contains(location.mode)
335         log.trace "modeOk = $result"
336         result
337 }
338
339 private getDaysOk() {
340         def result = true
341         if (days) {
342                 def df = new java.text.SimpleDateFormat("EEEE")
343                 if (location.timeZone) {
344                         df.setTimeZone(location.timeZone)
345                 }
346                 else {
347                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
348                 }
349                 def day = df.format(new Date())
350                 result = days.contains(day)
351         }
352         log.trace "daysOk = $result"
353         result
354 }
355
356 private getTimeOk() {
357         def result = true
358         if (starting && ending) {
359                 def currTime = now()
360                 def start = timeToday(starting, location?.timeZone).time
361                 def stop = timeToday(ending, location?.timeZone).time
362                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
363         }
364         log.trace "timeOk = $result"
365         result
366 }
367
368 private hhmm(time, fmt = "h:mm a")
369 {
370         def t = timeToday(time, location.timeZone)
371         def f = new java.text.SimpleDateFormat(fmt)
372         f.setTimeZone(location.timeZone ?: timeZone(time))
373         f.format(t)
374 }
375
376 private getTimeLabel()
377 {
378         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
379 }
380 // TODO - End Centralize
381
382 private loadText() {
383         if (location.timeZone || zipCode) {
384                 def weather = getWeatherFeature("forecast", zipCode)
385                 def current = getWeatherFeature("conditions", zipCode)
386                 def isMetric = location.temperatureScale == "C"
387                 def delim = ""
388                 def sb = new StringBuilder()
389                 list(forecastOptions).sort().each {opt ->
390                         if (opt == "0") {
391                                 if (isMetric) {
392                         sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees."
393                 }
394                 else {
395                         sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees."
396                 }
397                                 delim = " "
398                         }
399                         else if (opt == "1") {
400                                 sb << delim
401                                 sb << "Today's forecast is "
402                                 if (isMetric) {
403                         sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric 
404                 }
405                 else {
406                         sb << weather.forecast.txt_forecast.forecastday[0].fcttext
407                 }
408                         }
409                         else if (opt == "2") {
410                                 sb << delim
411                                 sb << "Tonight will be "
412                                 if (isMetric) {
413                         sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric 
414                 }
415                 else {
416                         sb << weather.forecast.txt_forecast.forecastday[1].fcttext
417                 }
418                         }
419                         else if (opt == "3") {
420                                 sb << delim
421                                 sb << "Tomorrow will be "
422                                 if (isMetric) {
423                         sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric 
424                 }
425                 else {
426                         sb << weather.forecast.txt_forecast.forecastday[2].fcttext
427                 }
428                         }
429                 }
430
431                 def msg = sb.toString()
432         msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release
433                 log.debug "msg = ${msg}"
434                 state.sound = textToSpeech(msg, true)
435         }
436         else {
437                 state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.")
438         }
439 }
440
441 private list(String s) {
442         [s]
443 }