2 * Initial State Event Streamer
4 * Copyright 2015 David Sulpy
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:
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
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
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"])
32 import groovy.json.JsonSlurper
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
80 def subscribeToEvents() {
81 if (accelerometers != null) {
82 subscribe(accelerometers, "acceleration", genericHandler)
85 subscribe(alarms, "alarm", genericHandler)
87 if (batteries != null) {
88 subscribe(batteries, "battery", genericHandler)
90 if (beacons != null) {
91 subscribe(beacons, "presence", genericHandler)
95 subscribe(cos, "carbonMonoxide", genericHandler)
98 subscribe(colors, "hue", genericHandler)
99 subscribe(colors, "saturation", genericHandler)
100 subscribe(colors, "color", genericHandler)
102 if (contacts != null) {
103 subscribe(contacts, "contact", genericHandler)
105 if (energyMeters != null) {
106 subscribe(energyMeters, "energy", genericHandler)
108 if (illuminances != null) {
109 subscribe(illuminances, "illuminance", genericHandler)
112 subscribe(locks, "lock", genericHandler)
114 if (motions != null) {
115 subscribe(motions, "motion", genericHandler)
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)
124 if (powerMeters != null) {
125 subscribe(powerMeters, "power", genericHandler)
127 if (presences != null) {
128 subscribe(presences, "presence", genericHandler)
130 if (humidities != null) {
131 subscribe(humidities, "humidity", genericHandler)
133 if (relaySwitches != null) {
134 subscribe(relaySwitches, "switch", genericHandler)
136 if (sleepSensors != null) {
137 subscribe(sleepSensors, "sleeping", genericHandler)
139 if (smokeDetectors != null) {
140 subscribe(smokeDetectors, "smoke", genericHandler)
143 subscribe(peds, "steps", genericHandler)
144 subscribe(peds, "goal", genericHandler)
146 if (switches != null) {
147 subscribe(switches, "switch", genericHandler)
149 if (switchLevels != null) {
150 subscribe(switchLevels, "level", genericHandler)
152 if (temperatures != null) {
153 subscribe(temperatures, "temperature", genericHandler)
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)
164 if (valves != null) {
165 subscribe(valves, "contact", genericHandler)
167 if (waterSensors != null) {
168 subscribe(waterSensors, "water", genericHandler)
173 log.trace "get access key"
174 if (atomicState.accessKey == null) {
175 httpError(404, "Access Key Not Found")
178 accessKey: atomicState.accessKey
184 log.trace "get bucket key"
185 if (atomicState.bucketKey == null) {
186 httpError(404, "Bucket key Not Found")
189 bucketKey: atomicState.bucketKey,
190 bucketName: atomicState.bucketName
196 log.trace "set bucket key"
197 def newBucketKey = request.JSON?.bucketKey
198 def newBucketName = request.JSON?.bucketName
200 log.debug "bucket name: $newBucketName"
201 log.debug "bucket key: $newBucketKey"
203 if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
204 atomicState.bucketKey = "$newBucketKey"
205 atomicState.bucketName = "$newBucketName"
206 atomicState.isBucketCreated = false
213 log.trace "set access key"
214 def newAccessKey = request.JSON?.accessKey
215 def newGrokerSubdomain = request.JSON?.grokerSubdomain
217 if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
218 atomicState.grokerSubdomain = "$newGrokerSubdomain"
219 atomicState.isBucketCreated = false
222 if (newAccessKey && newAccessKey != atomicState.accessKey) {
223 atomicState.accessKey = "$newAccessKey"
224 atomicState.isBucketCreated = false
229 atomicState.version = "1.0.18"
232 atomicState.isBucketCreated = false
233 atomicState.grokerSubdomain = "groker"
234 atomicState.eventBuffer = []
236 runEvery15Minutes(flushBuffer)
238 log.debug "installed (version $atomicState.version)"
242 atomicState.version = "1.0.18"
245 if (atomicState.bucketKey != null && atomicState.accessKey != null) {
246 atomicState.isBucketCreated = false
248 if (atomicState.eventBuffer == null) {
249 atomicState.eventBuffer = []
251 if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
252 atomicState.grokerSubdomain = "groker"
257 log.debug "updated (version $atomicState.version)"
261 log.debug "uninstalled (version $atomicState.version)"
264 def tryCreateBucket() {
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"
272 // if the bucket has already been created, no need to continue
273 if (atomicState.isBucketCreated) {
277 if (!atomicState.bucketName) {
278 atomicState.bucketName = atomicState.bucketKey
280 if (!atomicState.accessKey) {
283 def bucketName = "${atomicState.bucketName}"
284 def bucketKey = "${atomicState.bucketKey}"
285 def accessKey = "${atomicState.accessKey}"
287 def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
289 def bucketCreatePost = [
290 uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
292 "Content-Type": "application/json",
293 "X-IS-AccessKey": accessKey
295 body: bucketCreateBody
298 log.debug bucketCreatePost
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"
307 atomicState.isBucketCreated = true
311 log.error "bucket creation error: $e"
316 def genericHandler(evt) {
317 log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value"
319 def key = "$evt.displayName($evt.name)"
320 if (evt.unit != null) {
321 key = "$evt.displayName(${evt.name}_$evt.unit)"
323 def value = "$evt.value"
327 eventHandler(key, value)
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
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)
341 def eventHandler(name, value) {
342 def epoch = now() / 1000
343 def eventBuffer = atomicState.eventBuffer ?: []
344 eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
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)
353 // Make sure we persist the updated eventBuffer with the new event added back to atomicState
354 atomicState.eventBuffer = eventBuffer
356 log.debug "Event added to buffer: " + eventBuffer
359 // a helper function for shipping the atomicState.eventBuffer to Initial State
360 def tryShipEvents(eventBuffer) {
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"
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) {
376 uri: "https://${grokerSubdomain}.initialstate.com/api/events",
378 "Content-Type": "application/json",
379 "X-IS-BucketKey": "${bucketKey}",
380 "X-IS-AccessKey": "${accessKey}",
381 "Accept-Version": "0.0.2"
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}"
395 log.error "shipping events failed: $e"