Update sonos-music-modes.groovy
[smartapps.git] / official / sonos-music-modes.groovy
1 /**
2  *  Copyright 2015 SmartThings
3  *
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:
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
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.
12  *
13  *  Sonos Mood Music
14  *
15  *  Author: SmartThings
16  *  Date: 2014-02-12
17  */
18
19
20 definition(
21     name: "Sonos Music Modes",
22     namespace: "smartthings",
23     author: "SmartThings",
24     description: "Plays a different selected song or station for each mode.",
25     category: "SmartThings Internal",
26     iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png",
27     iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png"
28 )
29
30 preferences {
31         page(name: "mainPage", title: "Play a message on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true)
32         page(name: "chooseTrack", title: "Select a song", install: true)
33         page(name: "timeIntervalInput", title: "Only during a certain time") {
34                 section {
35                         input "starting", "time", title: "Starting", required: false
36                         input "ending", "time", title: "Ending", required: false
37                 }
38         }
39 }
40
41
42 private songOptions() {
43
44         // Make sure current selection is in the set
45
46         def options = new LinkedHashSet()
47         options << "STOP PLAYING"
48         if (state.selectedSong?.station) {
49                 options << state.selectedSong.station
50         }
51         else if (state.selectedSong?.description) {
52                 // TODO - Remove eventually? 'description' for backward compatibility
53                 options << state.selectedSong.description
54         }
55
56         // Query for recent tracks
57         def states = sonos.statesSince("trackData", new Date(0), [max:30])
58         def dataMaps = states.collect{it.jsonValue}
59         options.addAll(dataMaps.collect{it.station})
60
61         log.trace "${options.size()} songs in list"
62         options.take(20) as List
63 }
64
65 private saveSelectedSongs() {
66         try {
67                 def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue}
68                 log.info "Searching ${songs.size()} records"
69
70                 if (!state.selectedSongs) {
71                         state.selectedSongs = [:]
72                 }
73
74                 settings.each {name, thisSong ->
75                         if (thisSong == "STOP PLAYING") {
76                                 state.selectedSongs."$name" = "PAUSE"
77                         }
78                         if (name.startsWith("mode_")) {
79                                 log.info "Looking for $thisSong"
80
81                                 def data = songs.find {s -> s.station == thisSong}
82                                 log.info "Found ${data?.station}"
83                                 if (data) {
84                                         state.selectedSongs."$name" = data
85                                         log.debug "Selected song = $data.station"
86                                 }
87                                 else if (song == state.selectedSongs."$name"?.station) {
88                                         log.debug "Selected existing entry '$thisSong', which is no longer in the last 20 list"
89                                 }
90                                 else {
91                                         log.warn "Selected song '$thisSong' not found"
92                                 }
93                         }
94                 }
95         }
96         catch (Throwable t) {
97                 log.error t
98         }
99 }
100
101
102 def mainPage() {
103         dynamicPage(name: "mainPage") {
104
105                 section {
106                         input "sonos", "capability.musicPlayer", title: "Sonos player", required: true
107                 }
108                 section("More options", hideable: true, hidden: true) {
109                         input "volume", "number", title: "Set the volume", description: "0-100%", required: false
110                         input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false
111                         href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete"
112                         input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false,
113                                 options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
114                         if (settings.modes) {
115                 input "modes", "mode", title: "Only when mode is", multiple: true, required: false
116             }
117                         input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false
118                 }
119         }
120 }
121
122 def chooseTrack() {
123         dynamicPage(name: "chooseTrack") {
124                 section("Play a different song for each mode in which you want music") {
125                         def options = songOptions()
126                         location.modes.each {mode ->
127                                 input "mode_$mode.name", "enum", title: mode.name, options: options, required: false
128                         }
129                 }
130                 section([mobileOnly:true]) {
131                         label title: "Assign a name", required: false
132                         mode title: "Set for specific mode(s)", required: false
133                 }
134         }
135 }
136
137 def installed() {
138         log.debug "Installed with settings: ${settings}"
139         subscribeToEvents()
140 }
141
142 def updated() {
143         log.debug "Updated with settings: ${settings}"
144         unsubscribe()
145         subscribeToEvents()
146 }
147
148 def subscribeToEvents() {
149         log.trace "subscribeToEvents()"
150         saveSelectedSongs()
151
152         subscribe(location, modeChangeHandler)
153 }
154
155 def modeChangeHandler(evt) {
156         log.trace "modeChangeHandler($evt.name: $evt.value)"
157         if (allOk) {
158                 if (frequency) {
159                         def lastTime = state[frequencyKey(evt)]
160                         if (lastTime == null || now() - lastTime >= frequency * 60000) {
161                                 takeAction(evt)
162                         }
163                 }
164                 else {
165                         takeAction(evt)
166                 }
167         }
168 }
169
170 private takeAction(evt) {
171
172         def name = "mode_$evt.value".toString()
173         def selectedSong = state.selectedSongs."$name"
174
175         if (selectedSong == "PAUSE") {
176                 sonos.stop()
177         }
178         else {
179                 log.info "Playing '$selectedSong"
180
181                 if (volume != null) {
182                         sonos.stop()
183                         pause(500)
184                         sonos.setLevel(volume)
185                         pause(500)
186                 }
187
188                 sonos.playTrack(selectedSong)
189         }
190
191         if (frequency || oncePerDay) {
192                 state[frequencyKey(evt)] = now()
193         }
194         log.trace "Exiting takeAction()"
195 }
196
197 private frequencyKey(evt) {
198         "lastActionTimeStamp"
199 }
200
201 private dayString(Date date) {
202         def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
203         if (location.timeZone) {
204                 df.setTimeZone(location.timeZone)
205         }
206         else {
207                 df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
208         }
209         df.format(date)
210 }
211
212 private oncePerDayOk(Long lastTime) {
213         def result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true
214         log.trace "oncePerDayOk = $result"
215         result
216 }
217
218 // TODO - centralize somehow
219 private getAllOk() {
220         modeOk && daysOk && timeOk
221 }
222
223 private getModeOk() {
224         def result = !modes || modes.contains(location.mode)
225         log.trace "modeOk = $result"
226         result
227 }
228
229 private getDaysOk() {
230         def result = true
231         if (days) {
232                 def df = new java.text.SimpleDateFormat("EEEE")
233                 if (location.timeZone) {
234                         df.setTimeZone(location.timeZone)
235                 }
236                 else {
237                         df.setTimeZone(TimeZone.getTimeZone("America/New_York"))
238                 }
239                 def day = df.format(new Date())
240                 result = days.contains(day)
241         }
242         log.trace "daysOk = $result"
243         result
244 }
245
246 private getTimeOk() {
247         def result = true
248         if (starting && ending) {
249                 def currTime = now()
250                 def start = timeToday(starting, location?.timeZone).time
251                 def stop = timeToday(ending, location?.timeZone).time
252                 result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start
253         }
254         log.trace "timeOk = $result"
255         result
256 }
257
258 private hhmm(time, fmt = "h:mm a")
259 {
260         def t = timeToday(time, location.timeZone)
261         def f = new java.text.SimpleDateFormat(fmt)
262         f.setTimeZone(location.timeZone ?: timeZone(time))
263         f.format(t)
264 }
265
266 private timeIntervalLabel()
267 {
268         (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : ""
269 }
270 // TODO - End Centralize
271