2 * Initial State Event Streamer
4 * Copyright 2016-2017 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 POST: "disableIntegration"
85 POST: "enableIntegration"
90 def disableIntegration() {
91 log.trace "disabling integration"
93 atomicState.isDisabled = true
95 [ isDisabled: atomicState.isDisabled ]
98 def enableIntegration() {
99 log.trace "enable integration"
101 atomicState.isDisabled = false
103 [ isDisabled: atomicState.isDisabled]
107 log.trace "get access key"
108 if (atomicState.accessKey == null) {
109 httpError(404, "Access Key Not Found")
112 accessKey: atomicState.accessKey
118 log.trace "get bucket key"
119 if (atomicState.bucketKey == null) {
120 httpError(404, "Bucket key Not Found")
123 bucketKey: atomicState.bucketKey,
124 bucketName: atomicState.bucketName
130 log.trace "set bucket key"
131 def newBucketKey = request.JSON?.bucketKey
132 def newBucketName = request.JSON?.bucketName
134 log.debug "bucket name: $newBucketName"
135 log.debug "bucket key: $newBucketKey"
137 if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
138 atomicState.bucketKey = "$newBucketKey"
139 atomicState.bucketName = "$newBucketName"
140 atomicState.isBucketCreated = false
147 log.trace "set access key"
148 def newAccessKey = request.JSON?.accessKey
149 def newGrokerSubdomain = request.JSON?.grokerSubdomain
151 if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
152 atomicState.grokerSubdomain = "$newGrokerSubdomain"
153 atomicState.isBucketCreated = false
156 if (newAccessKey && newAccessKey != atomicState.accessKey) {
157 atomicState.accessKey = "$newAccessKey"
158 atomicState.isBucketCreated = false
162 def subscribeToEvents() {
163 if (accelerometers != null) {
164 subscribe(accelerometers, "acceleration", genericHandler)
166 if (alarms != null) {
167 subscribe(alarms, "alarm", genericHandler)
169 if (batteries != null) {
170 subscribe(batteries, "battery", genericHandler)
172 if (beacons != null) {
173 subscribe(beacons, "presence", genericHandler)
177 subscribe(cos, "carbonMonoxide", genericHandler)
179 if (colors != null) {
180 subscribe(colors, "hue", genericHandler)
181 subscribe(colors, "saturation", genericHandler)
182 subscribe(colors, "color", genericHandler)
184 if (contacts != null) {
185 subscribe(contacts, "contact", genericHandler)
187 if (energyMeters != null) {
188 subscribe(energyMeters, "energy", genericHandler)
190 if (illuminances != null) {
191 subscribe(illuminances, "illuminance", genericHandler)
194 subscribe(locks, "lock", genericHandler)
196 if (motions != null) {
197 subscribe(motions, "motion", genericHandler)
199 if (musicPlayers != null) {
200 subscribe(musicPlayers, "status", genericHandler)
201 subscribe(musicPlayers, "level", genericHandler)
202 subscribe(musicPlayers, "trackDescription", genericHandler)
203 subscribe(musicPlayers, "trackData", genericHandler)
204 subscribe(musicPlayers, "mute", genericHandler)
206 if (powerMeters != null) {
207 subscribe(powerMeters, "power", genericHandler)
209 if (presences != null) {
210 subscribe(presences, "presence", genericHandler)
212 if (humidities != null) {
213 subscribe(humidities, "humidity", genericHandler)
215 if (relaySwitches != null) {
216 subscribe(relaySwitches, "switch", genericHandler)
218 if (sleepSensors != null) {
219 subscribe(sleepSensors, "sleeping", genericHandler)
221 if (smokeDetectors != null) {
222 subscribe(smokeDetectors, "smoke", genericHandler)
225 subscribe(peds, "steps", genericHandler)
226 subscribe(peds, "goal", genericHandler)
228 if (switches != null) {
229 subscribe(switches, "switch", genericHandler)
231 if (switchLevels != null) {
232 subscribe(switchLevels, "level", genericHandler)
234 if (temperatures != null) {
235 subscribe(temperatures, "temperature", genericHandler)
237 if (thermostats != null) {
238 subscribe(thermostats, "temperature", genericHandler)
239 subscribe(thermostats, "heatingSetpoint", genericHandler)
240 subscribe(thermostats, "coolingSetpoint", genericHandler)
241 subscribe(thermostats, "thermostatSetpoint", genericHandler)
242 subscribe(thermostats, "thermostatMode", genericHandler)
243 subscribe(thermostats, "thermostatFanMode", genericHandler)
244 subscribe(thermostats, "thermostatOperatingState", genericHandler)
246 if (valves != null) {
247 subscribe(valves, "contact", genericHandler)
249 if (waterSensors != null) {
250 subscribe(waterSensors, "water", genericHandler)
255 atomicState.version = "1.2.0"
257 atomicState.isBucketCreated = false
258 atomicState.grokerSubdomain = "groker"
259 atomicState.isDisabled = false
263 atomicState.isBucketCreated = false
264 atomicState.grokerSubdomain = "groker"
266 log.debug "installed (version $atomicState.version)"
270 atomicState.version = "1.2.0"
273 if (atomicState.bucketKey != null && atomicState.accessKey != null) {
274 atomicState.isBucketCreated = false
276 if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
277 atomicState.grokerSubdomain = "groker"
282 log.debug "updated (version $atomicState.version)"
286 log.debug "uninstalled (version $atomicState.version)"
289 def tryCreateBucket() {
290 // if the integration has been disabled, don't do anything
291 if (atomicState.isDisabled) {
292 log.debug "integration is disabled"
296 // can't ship events if there is no grokerSubdomain
297 if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
298 log.error "streaming url is currently null"
302 // if the bucket has already been created, no need to continue
303 if (atomicState.isBucketCreated) {
307 if (!atomicState.bucketName) {
308 atomicState.bucketName = atomicState.bucketKey
310 if (!atomicState.accessKey) {
313 def bucketName = "${atomicState.bucketName}"
314 def bucketKey = "${atomicState.bucketKey}"
315 def accessKey = "${atomicState.accessKey}"
317 def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
319 def bucketCreatePost = [
320 uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
322 "Content-Type": "application/json",
323 "X-IS-AccessKey": accessKey
325 body: bucketCreateBody
328 log.debug bucketCreatePost
331 // Create a bucket on Initial State so the data has a logical grouping
332 httpPostJson(bucketCreatePost) { resp ->
333 log.debug "bucket posted"
334 if (resp.status >= 400) {
335 log.error "bucket not created successfully"
337 atomicState.isBucketCreated = true
341 log.error "bucket creation error: $e"
346 def genericHandler(evt) {
347 log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value"
349 def key = "$evt.displayName($evt.name)"
350 if (evt.unit != null) {
351 key = "$evt.displayName(${evt.name}_$evt.unit)"
353 def value = "$evt.value"
357 eventHandler(key, value)
360 def eventHandler(name, value) {
361 def epoch = now() / 1000
363 def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
367 log.debug "Shipped Event: " + event
370 def tryShipEvents(event) {
372 // if the integration is not enabled, then don't attempt to send any data
373 if (atomicState.isDisabled) {
374 log.debug "not shipping, integration is disabled"
378 def grokerSubdomain = atomicState.grokerSubdomain
379 // can't ship events if there is no grokerSubdomain
380 if (grokerSubdomain == null || grokerSubdomain == "") {
381 log.error "streaming url is currently null"
384 def accessKey = atomicState.accessKey
385 def bucketKey = atomicState.bucketKey
386 // can't ship if access key and bucket key are null, so finish trying
387 if (accessKey == null || bucketKey == null) {
392 uri: "https://${grokerSubdomain}.initialstate.com/api/events",
394 "Content-Type": "application/json",
395 "X-IS-BucketKey": "${bucketKey}",
396 "X-IS-AccessKey": "${accessKey}",
397 "Accept-Version": "0.0.2"
403 // post the events to initial state
404 httpPostJson(eventPost) { resp ->
405 log.debug "shipped events and got ${resp.status}"
406 if (resp.status >= 400) {
407 log.error "shipping failed... ${resp.data}"
411 log.error "shipping events failed: $e"