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