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
88 // input "motion", "capability.motionSensor", title: "Motion Detected", required: false, multiple: true
89 // input "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true
90 // input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
91 // input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
92 // input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
93 // input "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
94 // input "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
95 // input "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
96 // input "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
97 // input "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
98 // input "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
99 // input "timeOfDay", "time", title: "At a Scheduled Time", required: false
102 dynamicPage(name: "mainPage") {
103 def anythingSet = anythingSet()
105 section("Play music when..."){
106 ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
107 ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
108 ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
109 ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
110 ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
111 ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
112 ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
113 ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
114 ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
115 ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
116 ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
117 ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
118 ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false
122 def hideable = anythingSet || app.installationState == "COMPLETE"
123 def sectionTitle = anythingSet ? "Select additional triggers" : "Play music when..."
125 section(sectionTitle, hideable: hideable, hidden: true){
126 ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true
127 ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true
128 ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true
129 ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true
130 ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true
131 ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true
132 ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true
133 ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true
134 ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true
135 ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true
136 ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production
137 ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true
138 ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false
141 input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true
143 section("More options", hideable: true, hidden: true) {
144 input "volume", "number", title: "Set the volume", description: "0-100%", required: false
145 input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
146 href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
147 input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
148 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
149 if (settings.modes) {
150 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
152 input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
158 dynamicPage(name: "chooseTrack") {
160 input "song","enum",title:"Play this track", required:true, multiple: false, options: songOptions()
162 section([mobileOnly:true]) {
163 label title: "Assign a name", required: false
164 mode title: "Set for specific mode(s)", required: false
169 private anythingSet() {
170 for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","timeOfDay","triggerModes","timeOfDay"]) {
171 if (settings[name]) {
178 private ifUnset(Map options, String name, String capability) {
179 if (!settings[name]) {
180 input(options, name, capability)
184 private ifSet(Map options, String name, String capability) {
185 if (settings[name]) {
186 input(options, name, capability)
191 log.debug "Installed with settings: ${settings}"
196 log.debug "Updated with settings: ${settings}"
202 def subscribeToEvents() {
203 log.trace "subscribeToEvents()"
206 subscribe(app, appTouchHandler)
207 subscribe(contact, "contact.open", eventHandler)
208 subscribe(contactClosed, "contact.closed", eventHandler)
209 subscribe(acceleration, "acceleration.active", eventHandler)
210 subscribe(motion, "motion.active", eventHandler)
211 subscribe(mySwitch, "switch.on", eventHandler)
212 subscribe(mySwitchOff, "switch.off", eventHandler)
213 subscribe(arrivalPresence, "presence.present", eventHandler)
214 subscribe(departurePresence, "presence.not present", eventHandler)
215 subscribe(smoke, "smoke.detected", eventHandler)
216 subscribe(smoke, "smoke.tested", eventHandler)
217 subscribe(smoke, "carbonMonoxide.detected", eventHandler)
218 subscribe(water, "water.wet", eventHandler)
219 subscribe(button1, "button.pushed", eventHandler)
222 subscribe(location, modeChangeHandler)
226 schedule(timeOfDay, scheduledTimeHandler)
230 def eventHandler(evt) {
233 def lastTime = state[frequencyKey(evt)]
234 if (lastTime == null || now() - lastTime >= frequency * 60000) {
244 def modeChangeHandler(evt) {
245 log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)"
246 if (evt.value in triggerModes) {
251 def scheduledTimeHandler() {
255 def appTouchHandler(evt) {
259 private takeAction(evt) {
261 log.info "Playing '$state.selectedSong"
263 if (volume != null) {
266 sonos.setLevel(volume)
270 sonos.playTrack(state.selectedSong)
272 if (frequency || oncePerDay) {
273 state[frequencyKey(evt)] = now()
275 log.trace "Exiting takeAction()"
278 private frequencyKey(evt) {
279 "lastActionTimeStamp"
282 private dayString(Date date) {
283 def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
284 if (location.timeZone) {
285 df.setTimeZone(location.timeZone)
288 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
293 private oncePerDayOk(Long lastTime) {
294 def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
295 log.trace "oncePerDayOk = $result"
299 // TODO - centralize somehow
301 modeOk && daysOk && timeOk
304 private getModeOk() {
305 def result = !modes || modes.contains(location.mode)
306 log.trace "modeOk = $result"
310 private getDaysOk() {
313 def df = new java.text.SimpleDateFormat("EEEE")
314 if (location.timeZone) {
315 df.setTimeZone(location.timeZone)
318 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
320 def day = df.format(new Date())
321 result = days.contains(day)
323 log.trace "daysOk = $result"
327 private getTimeOk() {
329 if (starting && ending) {
331 def start = timeToday(starting, location?.timeZone).time
332 def stop = timeToday(ending, location?.timeZone).time
333 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
335 log.trace "timeOk = $result"
339 private hhmm(time, fmt = "h:mm a")
341 def t = timeToday(time, location.timeZone)
342 def f = new java.text.SimpleDateFormat(fmt)
343 f.setTimeZone(location.timeZone ?: timeZone(time))
347 private timeIntervalLabel()
349 (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
351 // TODO - End Centralize