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
40 dynamicPage(name: "mainPage") {
41 def anythingSet = anythingSet()
43 section("Play weather report when"){
44 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
45 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
46 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
47 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
48 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
49 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
50 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
51 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
52 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
53 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
54 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
55 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
56 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
59 def hideable = anythingSet || app.installationState == "COMPLETE"
60 def sectionTitle = anythingSet ? "Select additional triggers" : "Play weather report when..."
62 section(sectionTitle, hideable: hideable, hidden: true){
63 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
64 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
65 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
66 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
67 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
68 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
69 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
70 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
71 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
72 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
73 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
74 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
75 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
78 input("forecastOptions", "enum", defaultValue: "0", title: "Weather report options", description: "Select one or more", multiple: true,
80 ["0": "Current Conditions"],
81 ["1": "Today's Forecast"],
82 ["2": "Tonight's Forecast"],
83 ["3": "Tomorrow's Forecast"],
88 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
90 section("More options", hideable: true, hidden: true) {
91 input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true
92 href "chooseTrack", title: "Or play this music or radio station", description: song ? state.selectedSong?.station : "Tap to set", state: song ? "complete" : "incomplete"
94 input "zipCode", "text", title: "Zip Code", required: false
95 input "volume", "number", title: "Temporarily change volume", description: "0-100%", required: false
96 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
97 href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
98 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
99 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
100 if (settings.modes) {
101 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
103 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
105 section([mobileOnly:true]) {
106 label title: "Assign a name", required: false
107 mode title: "Set for specific mode(s)"
113 dynamicPage(name: "chooseTrack") {
115 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
120 private anythingSet() {
121 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes"]) {
122 if (settings[name]) {
129 private ifUnset(Map options, String name, String capability) {
130 if (!settings[name]) {
131 input(options, name, capability)
135 private ifSet(Map options, String name, String capability) {
136 if (settings[name]) {
137 input(options, name, capability)
142 log.debug "Installed with settings: ${settings}"
147 log.debug "Updated with settings: ${settings}"
153 def subscribeToEvents() {
154 subscribe(app, appTouchHandler)
155 subscribe(contact, "contact.open", eventHandler)
156 subscribe(contactClosed, "contact.closed", eventHandler)
157 subscribe(acceleration, "acceleration.active", eventHandler)
158 subscribe(motion, "motion.active", eventHandler)
159 subscribe(mySwitch, "switch.on", eventHandler)
160 subscribe(mySwitchOff, "switch.off", eventHandler)
161 subscribe(arrivalPresence, "presence.present", eventHandler)
162 subscribe(departurePresence, "presence.not present", eventHandler)
163 subscribe(smoke, "smoke.detected", eventHandler)
164 subscribe(smoke, "smoke.tested", eventHandler)
165 subscribe(smoke, "carbonMonoxide.detected", eventHandler)
166 subscribe(water, "water.wet", eventHandler)
167 subscribe(button1, "button.pushed", eventHandler)
170 subscribe(location,modeChangeHandler)
174 schedule(timeOfDay, scheduledTimeHandler)
182 def eventHandler(evt) {
183 log.trace "eventHandler($evt?.name: $evt?.value)"
186 def lastTime = state[frequencyKey(evt)]
187 if (oncePerDayOk(lastTime)) {
189 if (lastTime == null || now() - lastTime >= frequency * 60000) {
193 log.debug "Not taking action because $frequency minutes have not elapsed since last action"
201 log.debug "Not taking action because it was already taken today"
206 def modeChangeHandler(evt) {
207 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
208 if (evt.value in triggerModes) {
213 def scheduledTimeHandler() {
217 def appTouchHandler(evt) {
221 private takeAction(evt) {
226 sonos.playSoundAndTrack(state.sound.uri, state.sound.duration, state.selectedSong, volume)
228 else if (resumePlaying){
229 sonos.playTrackAndResume(state.sound.uri, state.sound.duration, volume)
232 sonos.playTrackAtVolume(state.sound.uri, volume)
235 sonos.playTrack(state.sound.uri)
238 if (frequency || oncePerDay) {
239 state[frequencyKey(evt)] = now()
243 private songOptions() {
245 // Make sure current selection is in the set
247 def options = new LinkedHashSet()
248 if (state.selectedSong?.station) {
249 options << state.selectedSong.station
251 else if (state.selectedSong?.description) {
252 // TODO - Remove eventually? 'description' for backward compatibility
253 options << state.selectedSong.description
256 // Query for recent tracks
257 def states = sonos.statesSince("trackData", new Date(0), [max:30])
258 def dataMaps = states.collect{it.jsonValue}
259 options.addAll(dataMaps.collect{it.station})
261 log.trace "${options.size()} songs in list"
262 options.take(20) as List
265 private saveSelectedSong() {
268 log.info "Looking for $thisSong"
269 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
270 log.info "Searching ${songs.size()} records"
272 def data = songs.find {s -> s.station == thisSong}
273 log.info "Found ${data?.station}"
275 state.selectedSong = data
276 log.debug "Selected song = $state.selectedSong"
278 else if (song == state.selectedSong?.station) {
279 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
282 log.warn "Selected song '$song' not found"
285 catch (Throwable t) {
290 private frequencyKey(evt) {
291 "lastActionTimeStamp"
294 private dayString(Date date) {
295 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
296 if (location.timeZone) {
297 df.setTimeZone(location.timeZone)
300 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
305 private oncePerDayOk(Long lastTime) {
308 result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
309 log.trace "oncePerDayOk = $result"
314 // TODO - centralize somehow
316 modeOk && daysOk && timeOk
319 private getModeOk() {
320 def result = !modes || modes.contains(location.mode)
321 log.trace "modeOk = $result"
325 private getDaysOk() {
328 def df = new java.text.SimpleDateFormat("EEEE")
329 if (location.timeZone) {
330 df.setTimeZone(location.timeZone)
333 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
335 def day = df.format(new Date())
336 result = days.contains(day)
338 log.trace "daysOk = $result"
342 private getTimeOk() {
344 if (starting && ending) {
346 def start = timeToday(starting, location?.timeZone).time
347 def stop = timeToday(ending, location?.timeZone).time
348 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
350 log.trace "timeOk = $result"
354 private hhmm(time, fmt = "h:mm a")
356 def t = timeToday(time, location.timeZone)
357 def f = new java.text.SimpleDateFormat(fmt)
358 f.setTimeZone(location.timeZone ?: timeZone(time))
362 private getTimeLabel()
364 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
366 // TODO - End Centralize
369 if (location.timeZone || zipCode) {
370 def weather = getWeatherFeature("forecast", zipCode)
371 def current = getWeatherFeature("conditions", zipCode)
372 def isMetric = location.temperatureScale == "C"
374 def sb = new StringBuilder()
375 list(forecastOptions).sort().each {opt ->
378 sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees."
381 sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees."
385 else if (opt == "1") {
387 sb << "Today's forecast is "
389 sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric
392 sb << weather.forecast.txt_forecast.forecastday[0].fcttext
395 else if (opt == "2") {
397 sb << "Tonight will be "
399 sb << weather.forecast.txt_forecast.forecastday[1].fcttext_metric
402 sb << weather.forecast.txt_forecast.forecastday[1].fcttext
405 else if (opt == "3") {
407 sb << "Tomorrow will be "
409 sb << weather.forecast.txt_forecast.forecastday[2].fcttext_metric
412 sb << weather.forecast.txt_forecast.forecastday[2].fcttext
417 def msg = sb.toString()
418 msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') // TODO - remove after next release
419 log.debug "msg = ${msg}"
420 state.sound = textToSpeech(msg, true)
423 state.sound = textToSpeech("Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.")
427 private list(String s) {