2 * Initial State Event Streamer
4 * Copyright 2016 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
81 log.trace "get access key"
82 if (atomicState.accessKey == null) {
83 httpError(404, "Access Key Not Found")
86 accessKey: atomicState.accessKey
92 log.trace "get bucket key"
93 if (atomicState.bucketKey == null) {
94 httpError(404, "Bucket key Not Found")
97 bucketKey: atomicState.bucketKey,
98 bucketName: atomicState.bucketName
104 log.trace "set bucket key"
105 def newBucketKey = request.JSON?.bucketKey
106 def newBucketName = request.JSON?.bucketName
108 log.debug "bucket name: $newBucketName"
109 log.debug "bucket key: $newBucketKey"
111 if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
112 atomicState.bucketKey = "$newBucketKey"
113 atomicState.bucketName = "$newBucketName"
114 atomicState.isBucketCreated = false
121 log.trace "set access key"
122 def newAccessKey = request.JSON?.accessKey
123 def newGrokerSubdomain = request.JSON?.grokerSubdomain
125 if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
126 atomicState.grokerSubdomain = "$newGrokerSubdomain"
127 atomicState.isBucketCreated = false
130 if (newAccessKey && newAccessKey != atomicState.accessKey) {
131 atomicState.accessKey = "$newAccessKey"
132 atomicState.isBucketCreated = false
136 def subscribeToEvents() {
137 if (accelerometers != null) {
138 subscribe(accelerometers, "acceleration", genericHandler)
140 if (alarms != null) {
141 subscribe(alarms, "alarm", genericHandler)
143 if (batteries != null) {
144 subscribe(batteries, "battery", genericHandler)
146 if (beacons != null) {
147 subscribe(beacons, "presence", genericHandler)
151 subscribe(cos, "carbonMonoxide", genericHandler)
153 if (colors != null) {
154 subscribe(colors, "hue", genericHandler)
155 subscribe(colors, "saturation", genericHandler)
156 subscribe(colors, "color", genericHandler)
158 if (contacts != null) {
159 subscribe(contacts, "contact", genericHandler)
161 if (energyMeters != null) {
162 subscribe(energyMeters, "energy", genericHandler)
164 if (illuminances != null) {
165 subscribe(illuminances, "illuminance", genericHandler)
168 subscribe(locks, "lock", genericHandler)
170 if (motions != null) {
171 subscribe(motions, "motion", genericHandler)
173 if (musicPlayers != null) {
174 subscribe(musicPlayers, "status", genericHandler)
175 subscribe(musicPlayers, "level", genericHandler)
176 subscribe(musicPlayers, "trackDescription", genericHandler)
177 subscribe(musicPlayers, "trackData", genericHandler)
178 subscribe(musicPlayers, "mute", genericHandler)
180 if (powerMeters != null) {
181 subscribe(powerMeters, "power", genericHandler)
183 if (presences != null) {
184 subscribe(presences, "presence", genericHandler)
186 if (humidities != null) {
187 subscribe(humidities, "humidity", genericHandler)
189 if (relaySwitches != null) {
190 subscribe(relaySwitches, "switch", genericHandler)
192 if (sleepSensors != null) {
193 subscribe(sleepSensors, "sleeping", genericHandler)
195 if (smokeDetectors != null) {
196 subscribe(smokeDetectors, "smoke", genericHandler)
199 subscribe(peds, "steps", genericHandler)
200 subscribe(peds, "goal", genericHandler)
202 if (switches != null) {
203 subscribe(switches, "switch", genericHandler)
205 if (switchLevels != null) {
206 subscribe(switchLevels, "level", genericHandler)
208 if (temperatures != null) {
209 subscribe(temperatures, "temperature", genericHandler)
211 if (thermostats != null) {
212 subscribe(thermostats, "temperature", genericHandler)
213 subscribe(thermostats, "heatingSetpoint", genericHandler)
214 subscribe(thermostats, "coolingSetpoint", genericHandler)
215 subscribe(thermostats, "thermostatSetpoint", genericHandler)
216 subscribe(thermostats, "thermostatMode", genericHandler)
217 subscribe(thermostats, "thermostatFanMode", genericHandler)
218 subscribe(thermostats, "thermostatOperatingState", genericHandler)
220 if (valves != null) {
221 subscribe(valves, "contact", genericHandler)
223 if (waterSensors != null) {
224 subscribe(waterSensors, "water", genericHandler)
229 atomicState.version = "1.1.0"
231 atomicState.isBucketCreated = false
232 atomicState.grokerSubdomain = "groker"
236 atomicState.isBucketCreated = false
237 atomicState.grokerSubdomain = "groker"
239 log.debug "installed (version $atomicState.version)"
243 atomicState.version = "1.1.0"
246 if (atomicState.bucketKey != null && atomicState.accessKey != null) {
247 atomicState.isBucketCreated = false
249 if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
250 atomicState.grokerSubdomain = "groker"
255 log.debug "updated (version $atomicState.version)"
259 log.debug "uninstalled (version $atomicState.version)"
262 def tryCreateBucket() {
264 // can't ship events if there is no grokerSubdomain
265 if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
266 log.error "streaming url is currently null"
270 // if the bucket has already been created, no need to continue
271 if (atomicState.isBucketCreated) {
275 if (!atomicState.bucketName) {
276 atomicState.bucketName = atomicState.bucketKey
278 if (!atomicState.accessKey) {
281 def bucketName = "${atomicState.bucketName}"
282 def bucketKey = "${atomicState.bucketKey}"
283 def accessKey = "${atomicState.accessKey}"
285 def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
287 def bucketCreatePost = [
288 uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
290 "Content-Type": "application/json",
291 "X-IS-AccessKey": accessKey
293 body: bucketCreateBody
296 log.debug bucketCreatePost
299 // Create a bucket on Initial State so the data has a logical grouping
300 httpPostJson(bucketCreatePost) { resp ->
301 log.debug "bucket posted"
302 if (resp.status >= 400) {
303 log.error "bucket not created successfully"
305 atomicState.isBucketCreated = true
309 log.error "bucket creation error: $e"
314 def genericHandler(evt) {
315 log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value"
317 def key = "$evt.displayName($evt.name)"
318 if (evt.unit != null) {
319 key = "$evt.displayName(${evt.name}_$evt.unit)"
321 def value = "$evt.value"
325 eventHandler(key, value)
328 def eventHandler(name, value) {
329 def epoch = now() / 1000
331 def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}")
335 log.debug "Shipped Event: " + event
338 def tryShipEvents(event) {
340 def grokerSubdomain = atomicState.grokerSubdomain
341 // can't ship events if there is no grokerSubdomain
342 if (grokerSubdomain == null || grokerSubdomain == "") {
343 log.error "streaming url is currently null"
346 def accessKey = atomicState.accessKey
347 def bucketKey = atomicState.bucketKey
348 // can't ship if access key and bucket key are null, so finish trying
349 if (accessKey == null || bucketKey == null) {
354 uri: "https://${grokerSubdomain}.initialstate.com/api/events",
356 "Content-Type": "application/json",
357 "X-IS-BucketKey": "${bucketKey}",
358 "X-IS-AccessKey": "${accessKey}",
359 "Accept-Version": "0.0.2"
365 // post the events to initial state
366 httpPostJson(eventPost) { resp ->
367 log.debug "shipped events and got ${resp.status}"
368 if (resp.status >= 400) {
369 log.error "shipping failed... ${resp.data}"
373 log.error "shipping events failed: $e"