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.
19 name: "Speaker Mood Music",
20 namespace: "smartthings",
21 author: "SmartThings",
22 description: "Plays a selected song or station.",
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 a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
30 page(name: "chooseTrack", title: "Select a song", install: true)
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 music 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
74 def hideable = anythingSet //|| app.installationState == "COMPLETE"
75 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
77 section(sectionTitle, hideable: hideable, hidden: true){
78 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
79 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
80 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
81 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
82 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
83 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
84 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
85 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
86 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
87 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
88 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
89 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
90 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
93 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
95 section("More options", hideable: true, hidden: true) {
96 input "volume", "number", title: "Set the volume", description: "0-100%", required: false
97 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
98 //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
99 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
100 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
101 //if (settings.modes) {
102 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
104 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
110 dynamicPage(name: "chooseTrack") {
112 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
114 section([mobileOnly:true]) {
115 label title: "Assign a name", required: false
116 mode title: "Set for specific mode(s)", required: false
121 private songOptions() {
123 // Make sure current selection is in the set
125 def options = new LinkedHashSet()
126 if (state.selectedSong?.station) {
127 options << state.selectedSong.station
129 else if (state.selectedSong?.description) {
130 // TODO - Remove eventually? 'description' for backward compatibility
131 options << state.selectedSong.description
134 // Query for recent tracks
135 def states = sonos.statesSince("trackData", new Date(0), [max:30])
136 def dataMaps = states.collect{it.jsonValue}
137 options.addAll(dataMaps.collect{it.station})
139 log.trace "${options.size()} songs in list"
140 options.take(20) as List
143 private saveSelectedSong() {
146 log.info "Looking for $thisSong"
147 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
148 log.info "Searching ${songs.size()} records"
150 def data = songs.find {s -> s.station == thisSong}
151 log.info "Found ${data?.station}"
153 state.selectedSong = data
154 log.debug "Selected song = $state.selectedSong"
156 else if (song == state.selectedSong?.station) {
157 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
160 log.warn "Selected song '$song' not found"
163 catch (Throwable t) {
168 private anythingSet() {
169 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
170 if (settings[name]) {
177 private ifUnset(Map options, String name, String capability) {
178 if (!settings[name]) {
179 input(options, name, capability)
183 private ifSet(Map options, String name, String capability) {
184 if (settings[name]) {
185 input(options, name, capability)
190 log.debug "Installed with settings: ${settings}"
195 log.debug "Updated with settings: ${settings}"
201 def subscribeToEvents() {
202 log.trace "subscribeToEvents()"
205 subscribe(app, appTouchHandler)
206 subscribe(contact, "contact.open", eventHandler)
207 subscribe(contactClosed, "contact.closed", eventHandler)
208 subscribe(acceleration, "acceleration.active", eventHandler)
209 subscribe(motion, "motion.active", eventHandler)
210 subscribe(mySwitch, "switch.on", eventHandler)
211 subscribe(mySwitchOff, "switch.off", eventHandler)
212 subscribe(arrivalPresence, "presence.present", eventHandler)
213 subscribe(departurePresence, "presence.not present", eventHandler)
214 subscribe(smoke, "smoke.detected", eventHandler)
215 subscribe(smoke, "smoke.tested", eventHandler)
216 subscribe(smoke, "carbonMonoxide.detected", eventHandler)
217 subscribe(water, "water.wet", eventHandler)
218 subscribe(button1, "button.pushed", eventHandler)
221 subscribe(location, modeChangeHandler)
225 schedule(timeOfDay, scheduledTimeHandler)
229 def eventHandler(evt) {
232 def lastTime = state[frequencyKey(evt)]
233 if (lastTime == null || now() - lastTime >= frequency * 60000) {
243 def modeChangeHandler(evt) {
244 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
245 if (evt.value in triggerModes) {
250 def scheduledTimeHandler() {
254 def appTouchHandler(evt) {
258 private takeAction(evt) {
260 log.info "Playing '$state.selectedSong"
262 if (volume != null) {
265 sonos.setLevel(volume)
269 sonos.playTrack(state.selectedSong)
271 if (frequency || oncePerDay) {
272 state[frequencyKey(evt)] = now()
274 log.trace "Exiting takeAction()"
277 private frequencyKey(evt) {
278 "lastActionTimeStamp"
281 private dayString(Date date) {
282 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
283 if (location.timeZone) {
284 df.setTimeZone(location.timeZone)
287 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
292 private oncePerDayOk(Long lastTime) {
293 def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
294 log.trace "oncePerDayOk = $result"
298 // TODO - centralize somehow
300 modeOk//&& daysOk && timeOk
303 private getModeOk() {
304 def result = !modes || modes.contains(location.mode)
305 log.trace "modeOk = $result"
309 private getDaysOk() {
312 def df = new java.text.SimpleDateFormat("EEEE")
313 if (location.timeZone) {
314 df.setTimeZone(location.timeZone)
317 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
319 def day = df.format(new Date())
320 result = days.contains(day)
322 log.trace "daysOk = $result"
326 private getTimeOk() {
328 if (starting && ending) {
330 def start = timeToday(starting, location?.timeZone).time
331 def stop = timeToday(ending, location?.timeZone).time
332 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
334 log.trace "timeOk = $result"
338 private hhmm(time, fmt = "h:mm a")
340 def t = timeToday(time, location.timeZone)
341 def f = new java.text.SimpleDateFormat(fmt)
342 f.setTimeZone(location.timeZone ?: timeZone(time))
346 private timeIntervalLabel()
348 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
350 // TODO - End Centralize