2 * Copyright 2015 SmartThings
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:
7 * http://www.apache.org/licenses/LICENSE-2.0
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.
13 * Speaker Weather Forecast
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"
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") {
33 input "starting", "time", title: "Starting", required: false
34 input "ending", "time", title: "Ending", required: false
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
54 dynamicPage(name: "mainPage") {
55 def anythingSet = 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
73 def hideable = anythingSet //|| app.installationState == "COMPLETE"
74 def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..."
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
92 input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true,
94 ["0": "Current Conditions"],
95 ["1": "Today's Forecast"],
96 ["2": "Tonight's Forecast"],
97 ["3": "Tomorrow's Forecast"],
102 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
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"
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
117 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
119 section([mobileOnly:true]) {
120 label title: "Assign a name", required: false
121 mode title: "Set for specific mode(s)"
127 dynamicPage(name: "chooseTrack") {
129 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
134 private anythingSet() {
135 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) {
136 if (settings[name]) {
143 private ifUnset(Map options, String name, String capability) {
144 if (!settings[name]) {
145 input(options, name, capability)
149 private ifSet(Map options, String name, String capability) {
150 if (settings[name]) {
151 input(options, name, capability)
156 log.debug "Installed with settings: ${settings}"
161 log.debug "Updated with settings: ${settings}"
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)
184 subscribe(location,modeChangeHandler)
188 schedule(timeOfDay, scheduledTimeHandler)
196 def eventHandler(evt) {
197 log.trace "eventHandler($evt?.name: $evt?.value)"
200 def lastTime = state[frequencyKey(evt)]
201 if (oncePerDayOk(lastTime)) {
203 if (lastTime == null || now() - lastTime >= frequency * 60000) {
207 log.debug "Not taking action because $frequency minutes have not elapsed since last action"
215 log.debug "Not taking action because it was already taken today"
220 def modeChangeHandler(evt) {
221 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
222 if (evt.value in triggerModes) {
227 def scheduledTimeHandler() {
231 def appTouchHandler(evt) {
235 private takeAction(evt) {
240 //sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
242 else if (resumePlaying){
243 //sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
246 //sonos.playTrackAtVolume(state.sound.uri, volume)
249 //sonos.playTrack(state.sound.uri)
252 if (frequency || oncePerDay) {
253 state[frequencyKey(evt)] = now()
257 private songOptions() {
259 // Make sure current selection is in the set
261 def options = new LinkedHashSet()
262 if (state.selectedSong?.station) {
263 options << state.selectedSong.station
265 else if (state.selectedSong?.description) {
266 // TODO - Remove eventually? 'description' for backward compatibility
267 options << state.selectedSong.description
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})
275 log.trace "${options.size()} songs in list"
276 options.take(20) as List
279 private saveSelectedSong() {
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"
286 def data = songs.find {s -> s.station == thisSong}
287 log.info "Found ${data?.station}"
289 state.selectedSong = data
290 log.debug "Selected song = $state.selectedSong"
292 else if (song == state.selectedSong?.station) {
293 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
296 log.warn "Selected song '$song' not found"
299 catch (Throwable t) {
304 private frequencyKey(evt) {
305 "lastActionTimeStamp"
308 private dayString(Date date) {
309 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
310 if (location.timeZone) {
311 df.setTimeZone(location.timeZone)
314 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
319 private oncePerDayOk(Long lastTime) {
322 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
323 log.trace "oncePerDayOk = $result"
328 // TODO - centralize somehow
330 modeOk && daysOk && timeOk
333 private getModeOk() {
334 def result = !modes || modes.contains(location.mode)
335 log.trace "modeOk = $result"
339 private getDaysOk() {
342 def df = new java.text.SimpleDateFormat("EEEE")
343 if (location.timeZone) {
344 df.setTimeZone(location.timeZone)
347 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
349 def day = df.format(new Date())
350 result = days.contains(day)
352 log.trace "daysOk = $result"
356 private getTimeOk() {
358 if (starting && ending) {
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
364 log.trace "timeOk = $result"
368 private hhmm(time, fmt = "h:mm a")
370 def t = timeToday(time, location.timeZone)
371 def f = new java.text.SimpleDateFormat(fmt)
372 f.setTimeZone(location.timeZone ?: timeZone(time))
376 private getTimeLabel()
378 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
380 // TODO - End Centralize
383 if (location.timeZone || zipCode) {
384 def weather = getWeatherFeature("forecast", zipCode)
385 def current = getWeatherFeature("conditions", zipCode)
386 def isMetric = location.temperatureScale == "C"
388 def sb = new StringBuilder()
389 list(forecastOptions).sort().each {opt ->
392 sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees."
395 sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees."
399 else if (opt == "1") {
401 sb << "Today's forecast is "
403 sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric
406 sb << weather.forecast.txt_forecast.forecastday[0].fcttext
409 else if (opt == "2") {
411 sb << "Tonight will be "
413 sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric
416 sb << weather.forecast.txt_forecast.forecastday[1].fcttext
419 else if (opt == "3") {
421 sb << "Tomorrow will be "
423 sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric
426 sb << weather.forecast.txt_forecast.forecastday[2].fcttext
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)
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.")
441 private list(String s) {