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 private songOptions() {
41 // Make sure current selection is in the set
43 def options = new LinkedHashSet()
44 if (state.selectedSong?.station) {
45 options << state.selectedSong.station
47 else if (state.selectedSong?.description) {
48 // TODO - Remove eventually? 'description' for backward compatibility
49 options << state.selectedSong.description
52 // Query for recent tracks
53 def states = sonos.statesSince("trackData", new Date(0), [max:30])
54 def dataMaps = states.collect{it.jsonValue}
55 options.addAll(dataMaps.collect{it.station})
57 log.trace "${options.size()} songs in list"
58 options.take(20) as List
61 private saveSelectedSong() {
64 log.info "Looking for $thisSong"
65 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
66 log.info "Searching ${songs.size()} records"
68 def data = songs.find {s -> s.station == thisSong}
69 log.info "Found ${data?.station}"
71 state.selectedSong = data
72 log.debug "Selected song = $state.selectedSong"
74 else if (song == state.selectedSong?.station) {
75 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
78 log.warn "Selected song '$song' not found"
86 // input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
87 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
88 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
89 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
90 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
91 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
92 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
93 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
94 // input "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
95 // input "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
96 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
97 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
98 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
101 dynamicPage(name: "mainPage") {
102 def anythingSet = anythingSet()
104 section("Play music when..."){
105 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
106 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
107 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
108 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
109 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
110 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
111 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
112 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
113 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
114 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
115 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
116 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
117 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
121 def hideable = anythingSet //|| app.installationState == "COMPLETE"
122 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
124 section(sectionTitle, hideable: hideable, hidden: true){
125 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
126 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
127 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
128 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
129 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
130 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
131 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
132 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
133 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
134 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
135 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
136 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
137 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
140 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
142 section("More options", hideable: true, hidden: true) {
143 input "volume", "number", title: "Set the volume", description: "0-100%", required: false
144 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
145 //href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
146 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
147 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
148 //if (settings.modes) {
149 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
151 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
157 dynamicPage(name: "chooseTrack") {
159 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
161 section([mobileOnly:true]) {
162 label title: "Assign a name", required: false
163 mode title: "Set for specific mode(s)", required: false
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