Update Hue-Party-Mode.groovy
[smartapps.git] / third-party / buffered-event-sender.groovy
1 /**
2  *  Initial State Event Streamer
3  *
4  *  Copyright 2015 David Sulpy
5  *
6  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
7  *  in compliance with the License. You may obtain a copy of the License at:
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
12  *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
13  *  for the specific language governing permissions and limitations under the License.
14  *
15  *  SmartThings data is sent from this SmartApp to Initial State. This is event data only for
16  *  devices for which the user has authorized. Likewise, Initial State's services call this
17  *  SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and
18  *  Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms
19  */
20
21 definition(
22     name: "Initial State Event Streamer",
23     namespace: "initialstate.events",
24     author: "David Sulpy",
25     description: "A SmartThings SmartApp to allow SmartThings events to be viewable inside an Initial State Event Bucket in your https://www.initialstate.com account.",
26     category: "SmartThings Labs",
27     iconUrl: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertica_small.png",
28     iconX2Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png",
29     iconX3Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png",
30     oauth: [displayName: "Initial State", displayLink: "https://www.initialstate.com"])
31
32 import groovy.json.JsonSlurper
33
34 preferences {
35         section("Choose which devices to monitor...") {
36         input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
37         input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false
38         input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
39         input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
40         input "cos", "capability.carbonMonoxideDetector", title: "Carbon  Monoxide Detectors", multiple: true, required: false
41         input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
42         input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false
43         input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
44         input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false
45         input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
46         input "locks", "capability.lock", title: "Locks", multiple: true, required: false
47         input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
48         input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false
49         input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
50         input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false
51         input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
52         input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false
53         input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
54         input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false
55         input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
56         input "switches", "capability.switch", title: "Switches", multiple: true, required: false
57         input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
58         input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false
59         input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
60         input "valves", "capability.valve", title: "Valves", multiple: true, required: false
61         input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
62     }
63 }
64
65 mappings {
66         path("/access_key") {
67                 action: [
68                         GET: "getAccessKey",
69                         PUT: "setAccessKey"
70                 ]
71         }
72         path("/bucket") {
73                 action: [
74                         GET: "getBucketKey",
75                         PUT: "setBucketKey"
76                 ]
77         }
78 }
79
80 def subscribeToEvents() {
81         if (accelerometers != null) {
82                 subscribe(accelerometers, "acceleration", genericHandler)
83         }
84         if (alarms != null) {
85                 subscribe(alarms, "alarm", genericHandler)
86         }
87         if (batteries != null) {
88                 subscribe(batteries, "battery", genericHandler)
89         }
90         if (beacons != null) {
91                 subscribe(beacons, "presence", genericHandler)
92         }
93
94         if (cos != null) {
95                 subscribe(cos, "carbonMonoxide", genericHandler)
96         }
97         if (colors != null) {
98                 subscribe(colors, "hue", genericHandler)
99                 subscribe(colors, "saturation", genericHandler)
100                 subscribe(colors, "color", genericHandler)
101         }
102         if (contacts != null) {
103                 subscribe(contacts, "contact", genericHandler)
104         }
105         if (energyMeters != null) {
106                 subscribe(energyMeters, "energy", genericHandler)
107         }
108         if (illuminances != null) {
109                 subscribe(illuminances, "illuminance", genericHandler)
110         }
111         if (locks != null) {
112                 subscribe(locks, "lock", genericHandler)
113         }
114         if (motions != null) {
115                 subscribe(motions, "motion", genericHandler)
116         }
117         if (musicPlayers != null) {
118                 subscribe(musicPlayers, "status", genericHandler)
119                 subscribe(musicPlayers, "level", genericHandler)
120                 subscribe(musicPlayers, "trackDescription", genericHandler)
121                 subscribe(musicPlayers, "trackData", genericHandler)
122                 subscribe(musicPlayers, "mute", genericHandler)
123         }
124         if (powerMeters != null) {
125                 subscribe(powerMeters, "power", genericHandler)
126         }
127         if (presences != null) {
128                 subscribe(presences, "presence", genericHandler)
129         }
130         if (humidities != null) {
131                 subscribe(humidities, "humidity", genericHandler)
132         }
133         if (relaySwitches != null) {
134                 subscribe(relaySwitches, "switch", genericHandler)
135         }
136         if (sleepSensors != null) {
137                 subscribe(sleepSensors, "sleeping", genericHandler)
138         }
139         if (smokeDetectors != null) {
140                 subscribe(smokeDetectors, "smoke", genericHandler)
141         }
142         if (peds != null) {
143                 subscribe(peds, "steps", genericHandler)
144                 subscribe(peds, "goal", genericHandler)
145         }
146         if (switches != null) {
147                 subscribe(switches, "switch", genericHandler)
148         }
149         if (switchLevels != null) {
150                 subscribe(switchLevels, "level", genericHandler)
151         }
152         if (temperatures != null) {
153                 subscribe(temperatures, "temperature", genericHandler)
154         }
155         if (thermostats != null) {
156                 subscribe(thermostats, "temperature", genericHandler)
157                 subscribe(thermostats, "heatingSetpoint", genericHandler)
158                 subscribe(thermostats, "coolingSetpoint", genericHandler)
159                 subscribe(thermostats, "thermostatSetpoint", genericHandler)
160                 subscribe(thermostats, "thermostatMode", genericHandler)
161                 subscribe(thermostats, "thermostatFanMode", genericHandler)
162                 subscribe(thermostats, "thermostatOperatingState", genericHandler)
163         }
164         if (valves != null) {
165                 subscribe(valves, "contact", genericHandler)
166         }
167         if (waterSensors != null) {
168                 subscribe(waterSensors, "water", genericHandler)
169         }
170 }
171
172 def getAccessKey() {
173         log.trace "get access key"
174         if (atomicState.accessKey == null) {
175                 httpError(404, "Access Key Not Found")
176         } else {
177                 [
178                         accessKey: atomicState.accessKey
179                 ]
180         }
181 }
182
183 def getBucketKey() {
184         log.trace "get bucket key"
185         if (atomicState.bucketKey == null) {
186                 httpError(404, "Bucket key Not Found")
187         } else {
188                 [
189                         bucketKey: atomicState.bucketKey,
190                         bucketName: atomicState.bucketName
191                 ]
192         }
193 }
194
195 def setBucketKey() {
196         log.trace "set bucket key"
197         def newBucketKey = request.JSON?.bucketKey
198         def newBucketName = request.JSON?.bucketName
199
200         log.debug "bucket name: $newBucketName"
201         log.debug "bucket key: $newBucketKey"
202
203         if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
204                 atomicState.bucketKey = "$newBucketKey"
205                 atomicState.bucketName = "$newBucketName"
206                 atomicState.isBucketCreated = false
207         }
208
209         tryCreateBucket()
210 }
211
212 def setAccessKey() {
213         log.trace "set access key"
214         def newAccessKey = request.JSON?.accessKey
215         def newGrokerSubdomain = request.JSON?.grokerSubdomain
216
217         if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
218                 atomicState.grokerSubdomain = "$newGrokerSubdomain"
219                 atomicState.isBucketCreated = false
220         }
221
222         if (newAccessKey && newAccessKey != atomicState.accessKey) {
223                 atomicState.accessKey = "$newAccessKey"
224                 atomicState.isBucketCreated = false
225         }
226 }
227
228 def installed() {
229         atomicState.version = "1.0.18"
230         subscribeToEvents()
231
232         atomicState.isBucketCreated = false
233         atomicState.grokerSubdomain = "groker"
234         atomicState.eventBuffer = []
235
236         runEvery15Minutes(flushBuffer)
237
238         log.debug "installed (version $atomicState.version)"
239 }
240
241 def updated() {
242         atomicState.version = "1.0.18"
243         unsubscribe()
244
245         if (atomicState.bucketKey != null && atomicState.accessKey != null) {
246                 atomicState.isBucketCreated = false
247         }
248         if (atomicState.eventBuffer == null) {
249                 atomicState.eventBuffer = []
250         }
251         if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
252                 atomicState.grokerSubdomain = "groker"
253         }
254
255         subscribeToEvents()
256
257         log.debug "updated (version $atomicState.version)"
258 }
259
260 def uninstalled() {
261         log.debug "uninstalled (version $atomicState.version)"
262 }
263
264 def tryCreateBucket() {
265
266         // can't ship events if there is no grokerSubdomain
267         if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
268                 log.error "streaming url is currently null"
269                 return
270         }
271
272         // if the bucket has already been created, no need to continue
273         if (atomicState.isBucketCreated) {
274                 return
275         }
276
277         if (!atomicState.bucketName) {
278         atomicState.bucketName = atomicState.bucketKey
279     }
280     if (!atomicState.accessKey) {
281         return
282     }
283         def bucketName = "${atomicState.bucketName}"
284         def bucketKey = "${atomicState.bucketKey}"
285         def accessKey = "${atomicState.accessKey}"
286
287         def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
288
289         def bucketCreatePost = [
290                 uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
291                 headers: [
292                         "Content-Type": "application/json",
293                         "X-IS-AccessKey": accessKey
294                 ],
295                 body: bucketCreateBody
296         ]
297
298         log.debug bucketCreatePost
299
300         try {
301                 // Create a bucket on Initial State so the data has a logical grouping
302                 httpPostJson(bucketCreatePost) { resp ->
303                         log.debug "bucket posted"
304                         if (resp.status >= 400) {
305                                 log.error "bucket not created successfully"
306                         } else {
307                                 atomicState.isBucketCreated = true
308                         }
309                 }
310         } catch (e) {
311                 log.error "bucket creation error: $e"
312         }
313
314 }
315
316 def genericHandler(evt) {
317         log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value"
318
319         def key = "$evt.displayName($evt.name)"
320         if (evt.unit != null) {
321                 key = "$evt.displayName(${evt.name}_$evt.unit)"
322         }
323         def value = "$evt.value"
324
325         tryCreateBucket()
326
327         eventHandler(key, value)
328 }
329
330 // This is a handler function for flushing the event buffer
331 // after a specified amount of time to reduce the load on ST servers
332 def flushBuffer() {
333         def eventBuffer = atomicState.eventBuffer
334         log.trace "About to flush the buffer on schedule"
335         if (eventBuffer != null && eventBuffer.size() > 0) {
336                 atomicState.eventBuffer = []
337                 tryShipEvents(eventBuffer)
338         }
339 }
340
341 def eventHandler(name, value) {
342         def epoch = now() / 1000
343         def eventBuffer = atomicState.eventBuffer ?: []
344         eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
345
346         if (eventBuffer.size() >= 10) {
347                 // Clear eventBuffer right away since we've already pulled it off of atomicState to reduce the risk of missing
348                 // events.  This assumes the grokerSubdomain, accessKey, and bucketKey are set correctly to avoid the eventBuffer
349                 // from growing unbounded.
350                 atomicState.eventBuffer = []
351                 tryShipEvents(eventBuffer)
352         } else {
353                 // Make sure we persist the updated eventBuffer with the new event added back to atomicState
354                 atomicState.eventBuffer = eventBuffer
355         }
356         log.debug "Event added to buffer: " + eventBuffer
357 }
358
359 // a helper function for shipping the atomicState.eventBuffer to Initial State
360 def tryShipEvents(eventBuffer) {
361
362         def grokerSubdomain = atomicState.grokerSubdomain
363         // can't ship events if there is no grokerSubdomain
364         if (grokerSubdomain == null || grokerSubdomain == "") {
365                 log.error "streaming url is currently null"
366                 return
367         }
368         def accessKey = atomicState.accessKey
369         def bucketKey = atomicState.bucketKey
370         // can't ship if access key and bucket key are null, so finish trying
371         if (accessKey == null || bucketKey == null) {
372                 return
373         }
374
375         def eventPost = [
376                 uri: "https://${grokerSubdomain}.initialstate.com/api/events",
377                 headers: [
378                         "Content-Type": "application/json",
379                         "X-IS-BucketKey": "${bucketKey}",
380                         "X-IS-AccessKey": "${accessKey}",
381                         "Accept-Version": "0.0.2"
382                 ],
383                 body: eventBuffer
384         ]
385
386         try {
387                 // post the events to initial state
388                 httpPostJson(eventPost) { resp ->
389                         log.debug "shipped events and got ${resp.status}"
390                         if (resp.status >= 400) {
391                                 log.error "shipping failed... ${resp.data}"
392                         }
393                 }
394         } catch (e) {
395                 log.error "shipping events failed: $e"
396         }
397
398 }