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.
20 private songOptions() {
22 // Make sure current selection is in the set
24 def options = new LinkedHashSet()
25 if (state.selectedSong?.station) {
26 options << state.selectedSong.station
28 else if (state.selectedSong?.description) {
29 // TODO - Remove eventually? 'description' for backward compatibility
30 options << state.selectedSong.description
33 // Query for recent tracks
34 def states = sonos.statesSince("trackData", new Date(0), [max:30])
35 def dataMaps = states.collect{it.jsonValue}
36 options.addAll(dataMaps.collect{it.station})
38 log.trace "${options.size()} songs in list"
39 options.take(20) as List
42 private saveSelectedSong() {
45 log.info "Looking for $thisSong"
46 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
47 log.info "Searching ${songs.size()} records"
49 def data = songs.find {s -> s.station == thisSong}
50 log.info "Found ${data?.station}"
52 state.selectedSong = data
53 log.debug "Selected song = $state.selectedSong"
55 else if (song == state.selectedSong?.station) {
56 log.debug "Selected existing entry '$song', which is no longer in the last 20 list"
59 log.warn "Selected song '$song' not found"
68 name: "Speaker Mood Music",
69 namespace: "smartthings",
70 author: "SmartThings",
71 description: "Plays a selected song or station.",
72 category: "SmartThings Labs",
73 iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
74 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
78 page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true)
79 page(name: "chooseTrack", title: "Select a song", install: true)
80 page(name: "timeIntervalInput", title: "Only during a certain time") {
82 input "starting", "time", title: "Starting", required: false
83 input "ending", "time", title: "Ending", required: false
89 dynamicPage(name: "mainPage") {
90 def anythingSet = anythingSet()
92 section("Play music when..."){
93 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
94 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
95 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
96 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
97 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
98 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
99 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
100 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
101 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
102 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
103 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
104 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
105 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
109 def hideable = anythingSet || app.installationState == "COMPLETE"
110 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
112 section(sectionTitle, hideable: hideable, hidden: true){
113 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
114 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
115 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
116 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
117 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
118 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
119 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
120 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
121 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
122 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
123 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
124 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
125 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
128 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
130 section("More options", hideable: true, hidden: true) {
131 input "volume", "number", title: "Set the volume", description: "0-100%", required: false
132 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
133 href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
134 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
135 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
136 if (settings.modes) {
137 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
139 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
145 dynamicPage(name: "chooseTrack") {
147 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
149 section([mobileOnly:true]) {
150 label title: "Assign a name", required: false
151 mode title: "Set for specific mode(s)", required: false
156 private anythingSet() {
157 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
158 if (settings[name]) {
165 private ifUnset(Map options, String name, String capability) {
166 if (!settings[name]) {
167 input(options, name, capability)
171 private ifSet(Map options, String name, String capability) {
172 if (settings[name]) {
173 input(options, name, capability)
178 log.debug "Installed with settings: ${settings}"
183 log.debug "Updated with settings: ${settings}"
189 def subscribeToEvents() {
190 log.trace "subscribeToEvents()"
193 subscribe(app, appTouchHandler)
194 subscribe(contact, "contact.open", eventHandler)
195 subscribe(contactClosed, "contact.closed", eventHandler)
196 subscribe(acceleration, "acceleration.active", eventHandler)
197 subscribe(motion, "motion.active", eventHandler)
198 subscribe(mySwitch, "switch.on", eventHandler)
199 subscribe(mySwitchOff, "switch.off", eventHandler)
200 subscribe(arrivalPresence, "presence.present", eventHandler)
201 subscribe(departurePresence, "presence.not present", eventHandler)
202 subscribe(smoke, "smoke.detected", eventHandler)
203 subscribe(smoke, "smoke.tested", eventHandler)
204 subscribe(smoke, "carbonMonoxide.detected", eventHandler)
205 subscribe(water, "water.wet", eventHandler)
206 subscribe(button1, "button.pushed", eventHandler)
209 subscribe(location, modeChangeHandler)
213 schedule(timeOfDay, scheduledTimeHandler)
217 def eventHandler(evt) {
220 def lastTime = state[frequencyKey(evt)]
221 if (lastTime == null || now() - lastTime >= frequency * 60000) {
231 def modeChangeHandler(evt) {
232 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
233 if (evt.value in triggerModes) {
238 def scheduledTimeHandler() {
242 def appTouchHandler(evt) {
246 private takeAction(evt) {
248 log.info "Playing '$state.selectedSong"
250 if (volume != null) {
253 sonos.setLevel(volume)
257 sonos.playTrack(state.selectedSong)
259 if (frequency || oncePerDay) {
260 state[frequencyKey(evt)] = now()
262 log.trace "Exiting takeAction()"
265 private frequencyKey(evt) {
266 "lastActionTimeStamp"
269 private dayString(Date date) {
270 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
271 if (location.timeZone) {
272 df.setTimeZone(location.timeZone)
275 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
280 private oncePerDayOk(Long lastTime) {
281 def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
282 log.trace "oncePerDayOk = $result"
286 // TODO - centralize somehow
288 modeOk && daysOk && timeOk
291 private getModeOk() {
292 def result = !modes || modes.contains(location.mode)
293 log.trace "modeOk = $result"
297 private getDaysOk() {
300 def df = new java.text.SimpleDateFormat("EEEE")
301 if (location.timeZone) {
302 df.setTimeZone(location.timeZone)
305 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
307 def day = df.format(new Date())
308 result = days.contains(day)
310 log.trace "daysOk = $result"
314 private getTimeOk() {
316 if (starting && ending) {
318 def start = timeToday(starting, location?.timeZone).time
319 def stop = timeToday(ending, location?.timeZone).time
320 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
322 log.trace "timeOk = $result"
326 private hhmm(time, fmt = "h:mm a")
328 def t = timeToday(time, location.timeZone)
329 def f = new java.text.SimpleDateFormat(fmt)
330 f.setTimeZone(location.timeZone ?: timeZone(time))
334 private timeIntervalLabel()
336 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
338 // TODO - End Centralize