├── .gitignore ├── LICENSE ├── README.md ├── buffered-event-sender.groovy ├── initial-state-event-sender.groovy ├── initialstate-smart-app-v1.2.0.groovy └── unbuffered-event-sender.groovy /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Sulpy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Initial State SmartThings SmartApp 2 | 3 | This repository is a host of the Initial State Event Sender - a SmartThings SmartApp intended to make easy the process of sending events that occur on a SmartThings network to Initial State for visualization. 4 | 5 | The actual SmartApp code is inside the official [SmartThings Public](https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy) github repo. However, because of the way SmartThings does app approval and deployment as well as the requirement for commit squashing, I wanted to keep the code in a separate repo as well to keep the git history. This way, any change in the official github repo can just be a copy and paste commit. 6 | 7 | Additionally, you'll find un-official or do-it-yourself SmartApps in this repo like the unbuffered Initial State Event Sender (which is based on the original design of this app and is by its very nature much less complex and therefore less fragile in the SmartThings ecosystem). 8 | 9 | ## Installation 10 | 11 | 12 | ### Official Version 13 | For the official version, you can install the SmartApp by following the SmartThings button in your [Initial State Account Settings](https://www.initialstate.com/app#/account) page. 14 | 15 | ### DIY Version 16 | 17 | 1. Copy the code from [`unbuffered-event-sender.groovy`](https://raw.githubusercontent.com/davidsulpy/initialstate-smartapp/master/unbuffered-event-sender.groovy) 18 | 2. Log in to ide.smartthings.com with your SmartThings account. 19 | 3. Navigate to My SmartApps 20 | 4. Select "New SmartApp" 21 | 5. Select "From Code" 22 | 6. Paste the code copied from the unbuffered-event-sender.groovy 23 | 7. Select "Create" 24 | 8. Edit line 162 and replace `YOUR_ACCESS_KEY` with an access key from your Initial State account. Optionally, you can edit the bucket information above this line should you wish. 25 | 9. Select "Save" 26 | 10. Select "Publish" -> "For Me" 27 | 28 | Now switch to the SmartThings Classic mobile app: 29 | 30 | 1. Go to the Marketplace 31 | 2. Select the SmartApps tab 32 | 3. Select My Apps 33 | 4. Select the DIY Initial State Event Streamer 34 | 5. Configure the sensors and capabilities you'd like to monitor 35 | 6. Select Done 36 | 37 | NOTE: This only works on the "SmartThings Classic" app. This functionality does not currently exist on the "SmartThings (Samsung Connect)" app. 38 | 39 | You should now be all set! Please note, however, that events will populate the bucket automatically as new events happen on your SmartThings network. Your history of events will build from when you setup the SmartApp forward, so, if you don't see any events in your Initial State bucket immediately, it's most likely because no events have occurred just yet! 40 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /initial-state-event-sender.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Initial State Event Streamer 3 | * 4 | * Copyright 2016 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 getAccessKey() { 81 | log.trace "get access key" 82 | if (atomicState.accessKey == null) { 83 | httpError(404, "Access Key Not Found") 84 | } else { 85 | [ 86 | accessKey: atomicState.accessKey 87 | ] 88 | } 89 | } 90 | 91 | def getBucketKey() { 92 | log.trace "get bucket key" 93 | if (atomicState.bucketKey == null) { 94 | httpError(404, "Bucket key Not Found") 95 | } else { 96 | [ 97 | bucketKey: atomicState.bucketKey, 98 | bucketName: atomicState.bucketName 99 | ] 100 | } 101 | } 102 | 103 | def setBucketKey() { 104 | log.trace "set bucket key" 105 | def newBucketKey = request.JSON?.bucketKey 106 | def newBucketName = request.JSON?.bucketName 107 | 108 | log.debug "bucket name: $newBucketName" 109 | log.debug "bucket key: $newBucketKey" 110 | 111 | if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) { 112 | atomicState.bucketKey = "$newBucketKey" 113 | atomicState.bucketName = "$newBucketName" 114 | atomicState.isBucketCreated = false 115 | } 116 | 117 | tryCreateBucket() 118 | } 119 | 120 | def setAccessKey() { 121 | log.trace "set access key" 122 | def newAccessKey = request.JSON?.accessKey 123 | def newGrokerSubdomain = request.JSON?.grokerSubdomain 124 | 125 | if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) { 126 | atomicState.grokerSubdomain = "$newGrokerSubdomain" 127 | atomicState.isBucketCreated = false 128 | } 129 | 130 | if (newAccessKey && newAccessKey != atomicState.accessKey) { 131 | atomicState.accessKey = "$newAccessKey" 132 | atomicState.isBucketCreated = false 133 | } 134 | } 135 | 136 | def subscribeToEvents() { 137 | if (accelerometers != null) { 138 | subscribe(accelerometers, "acceleration", genericHandler) 139 | } 140 | if (alarms != null) { 141 | subscribe(alarms, "alarm", genericHandler) 142 | } 143 | if (batteries != null) { 144 | subscribe(batteries, "battery", genericHandler) 145 | } 146 | if (beacons != null) { 147 | subscribe(beacons, "presence", genericHandler) 148 | } 149 | 150 | if (cos != null) { 151 | subscribe(cos, "carbonMonoxide", genericHandler) 152 | } 153 | if (colors != null) { 154 | subscribe(colors, "hue", genericHandler) 155 | subscribe(colors, "saturation", genericHandler) 156 | subscribe(colors, "color", genericHandler) 157 | } 158 | if (contacts != null) { 159 | subscribe(contacts, "contact", genericHandler) 160 | } 161 | if (energyMeters != null) { 162 | subscribe(energyMeters, "energy", genericHandler) 163 | } 164 | if (illuminances != null) { 165 | subscribe(illuminances, "illuminance", genericHandler) 166 | } 167 | if (locks != null) { 168 | subscribe(locks, "lock", genericHandler) 169 | } 170 | if (motions != null) { 171 | subscribe(motions, "motion", genericHandler) 172 | } 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) 179 | } 180 | if (powerMeters != null) { 181 | subscribe(powerMeters, "power", genericHandler) 182 | } 183 | if (presences != null) { 184 | subscribe(presences, "presence", genericHandler) 185 | } 186 | if (humidities != null) { 187 | subscribe(humidities, "humidity", genericHandler) 188 | } 189 | if (relaySwitches != null) { 190 | subscribe(relaySwitches, "switch", genericHandler) 191 | } 192 | if (sleepSensors != null) { 193 | subscribe(sleepSensors, "sleeping", genericHandler) 194 | } 195 | if (smokeDetectors != null) { 196 | subscribe(smokeDetectors, "smoke", genericHandler) 197 | } 198 | if (peds != null) { 199 | subscribe(peds, "steps", genericHandler) 200 | subscribe(peds, "goal", genericHandler) 201 | } 202 | if (switches != null) { 203 | subscribe(switches, "switch", genericHandler) 204 | } 205 | if (switchLevels != null) { 206 | subscribe(switchLevels, "level", genericHandler) 207 | } 208 | if (temperatures != null) { 209 | subscribe(temperatures, "temperature", genericHandler) 210 | } 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) 219 | } 220 | if (valves != null) { 221 | subscribe(valves, "contact", genericHandler) 222 | } 223 | if (waterSensors != null) { 224 | subscribe(waterSensors, "water", genericHandler) 225 | } 226 | } 227 | 228 | def installed() { 229 | atomicState.version = "1.1.0" 230 | 231 | atomicState.isBucketCreated = false 232 | atomicState.grokerSubdomain = "groker" 233 | 234 | subscribeToEvents() 235 | 236 | atomicState.isBucketCreated = false 237 | atomicState.grokerSubdomain = "groker" 238 | 239 | log.debug "installed (version $atomicState.version)" 240 | } 241 | 242 | def updated() { 243 | atomicState.version = "1.1.0" 244 | unsubscribe() 245 | 246 | if (atomicState.bucketKey != null && atomicState.accessKey != null) { 247 | atomicState.isBucketCreated = false 248 | } 249 | if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { 250 | atomicState.grokerSubdomain = "groker" 251 | } 252 | 253 | subscribeToEvents() 254 | 255 | log.debug "updated (version $atomicState.version)" 256 | } 257 | 258 | def uninstalled() { 259 | log.debug "uninstalled (version $atomicState.version)" 260 | } 261 | 262 | def tryCreateBucket() { 263 | 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" 267 | return 268 | } 269 | 270 | // if the bucket has already been created, no need to continue 271 | if (atomicState.isBucketCreated) { 272 | return 273 | } 274 | 275 | if (!atomicState.bucketName) { 276 | atomicState.bucketName = atomicState.bucketKey 277 | } 278 | if (!atomicState.accessKey) { 279 | return 280 | } 281 | def bucketName = "${atomicState.bucketName}" 282 | def bucketKey = "${atomicState.bucketKey}" 283 | def accessKey = "${atomicState.accessKey}" 284 | 285 | def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") 286 | 287 | def bucketCreatePost = [ 288 | uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", 289 | headers: [ 290 | "Content-Type": "application/json", 291 | "X-IS-AccessKey": accessKey 292 | ], 293 | body: bucketCreateBody 294 | ] 295 | 296 | log.debug bucketCreatePost 297 | 298 | try { 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" 304 | } else { 305 | atomicState.isBucketCreated = true 306 | } 307 | } 308 | } catch (e) { 309 | log.error "bucket creation error: $e" 310 | } 311 | 312 | } 313 | 314 | def genericHandler(evt) { 315 | log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value" 316 | 317 | def key = "$evt.displayName($evt.name)" 318 | if (evt.unit != null) { 319 | key = "$evt.displayName(${evt.name}_$evt.unit)" 320 | } 321 | def value = "$evt.value" 322 | 323 | tryCreateBucket() 324 | 325 | eventHandler(key, value) 326 | } 327 | 328 | def eventHandler(name, value) { 329 | def epoch = now() / 1000 330 | 331 | def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}") 332 | 333 | tryShipEvents(event) 334 | 335 | log.debug "Shipped Event: " + event 336 | } 337 | 338 | def tryShipEvents(event) { 339 | 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" 344 | return 345 | } 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) { 350 | return 351 | } 352 | 353 | def eventPost = [ 354 | uri: "https://${grokerSubdomain}.initialstate.com/api/events", 355 | headers: [ 356 | "Content-Type": "application/json", 357 | "X-IS-BucketKey": "${bucketKey}", 358 | "X-IS-AccessKey": "${accessKey}", 359 | "Accept-Version": "0.0.2" 360 | ], 361 | body: event 362 | ] 363 | 364 | try { 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}" 370 | } 371 | } 372 | } catch (e) { 373 | log.error "shipping events failed: $e" 374 | } 375 | 376 | } -------------------------------------------------------------------------------- /initialstate-smart-app-v1.2.0.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Initial State Event Streamer 3 | * 4 | * Copyright 2016-2017 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 | path("/disable") { 79 | action: [ 80 | POST: "disableIntegration" 81 | ] 82 | } 83 | path("/enable") { 84 | action: [ 85 | POST: "enableIntegration" 86 | ] 87 | } 88 | } 89 | 90 | def disableIntegration() { 91 | log.trace "disabling integration" 92 | 93 | atomicState.isDisabled = true 94 | 95 | [ isDisabled: atomicState.isDisabled ] 96 | } 97 | 98 | def enableIntegration() { 99 | log.trace "enable integration" 100 | 101 | atomicState.isDisabled = false 102 | 103 | [ isDisabled: atomicState.isDisabled] 104 | } 105 | 106 | def getAccessKey() { 107 | log.trace "get access key" 108 | if (atomicState.accessKey == null) { 109 | httpError(404, "Access Key Not Found") 110 | } else { 111 | [ 112 | accessKey: atomicState.accessKey 113 | ] 114 | } 115 | } 116 | 117 | def getBucketKey() { 118 | log.trace "get bucket key" 119 | if (atomicState.bucketKey == null) { 120 | httpError(404, "Bucket key Not Found") 121 | } else { 122 | [ 123 | bucketKey: atomicState.bucketKey, 124 | bucketName: atomicState.bucketName 125 | ] 126 | } 127 | } 128 | 129 | def setBucketKey() { 130 | log.trace "set bucket key" 131 | def newBucketKey = request.JSON?.bucketKey 132 | def newBucketName = request.JSON?.bucketName 133 | 134 | log.debug "bucket name: $newBucketName" 135 | log.debug "bucket key: $newBucketKey" 136 | 137 | if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) { 138 | atomicState.bucketKey = "$newBucketKey" 139 | atomicState.bucketName = "$newBucketName" 140 | atomicState.isBucketCreated = false 141 | } 142 | 143 | tryCreateBucket() 144 | } 145 | 146 | def setAccessKey() { 147 | log.trace "set access key" 148 | def newAccessKey = request.JSON?.accessKey 149 | def newGrokerSubdomain = request.JSON?.grokerSubdomain 150 | 151 | if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) { 152 | atomicState.grokerSubdomain = "$newGrokerSubdomain" 153 | atomicState.isBucketCreated = false 154 | } 155 | 156 | if (newAccessKey && newAccessKey != atomicState.accessKey) { 157 | atomicState.accessKey = "$newAccessKey" 158 | atomicState.isBucketCreated = false 159 | } 160 | } 161 | 162 | def subscribeToEvents() { 163 | if (accelerometers != null) { 164 | subscribe(accelerometers, "acceleration", genericHandler) 165 | } 166 | if (alarms != null) { 167 | subscribe(alarms, "alarm", genericHandler) 168 | } 169 | if (batteries != null) { 170 | subscribe(batteries, "battery", genericHandler) 171 | } 172 | if (beacons != null) { 173 | subscribe(beacons, "presence", genericHandler) 174 | } 175 | 176 | if (cos != null) { 177 | subscribe(cos, "carbonMonoxide", genericHandler) 178 | } 179 | if (colors != null) { 180 | subscribe(colors, "hue", genericHandler) 181 | subscribe(colors, "saturation", genericHandler) 182 | subscribe(colors, "color", genericHandler) 183 | } 184 | if (contacts != null) { 185 | subscribe(contacts, "contact", genericHandler) 186 | } 187 | if (energyMeters != null) { 188 | subscribe(energyMeters, "energy", genericHandler) 189 | } 190 | if (illuminances != null) { 191 | subscribe(illuminances, "illuminance", genericHandler) 192 | } 193 | if (locks != null) { 194 | subscribe(locks, "lock", genericHandler) 195 | } 196 | if (motions != null) { 197 | subscribe(motions, "motion", genericHandler) 198 | } 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) 205 | } 206 | if (powerMeters != null) { 207 | subscribe(powerMeters, "power", genericHandler) 208 | } 209 | if (presences != null) { 210 | subscribe(presences, "presence", genericHandler) 211 | } 212 | if (humidities != null) { 213 | subscribe(humidities, "humidity", genericHandler) 214 | } 215 | if (relaySwitches != null) { 216 | subscribe(relaySwitches, "switch", genericHandler) 217 | } 218 | if (sleepSensors != null) { 219 | subscribe(sleepSensors, "sleeping", genericHandler) 220 | } 221 | if (smokeDetectors != null) { 222 | subscribe(smokeDetectors, "smoke", genericHandler) 223 | } 224 | if (peds != null) { 225 | subscribe(peds, "steps", genericHandler) 226 | subscribe(peds, "goal", genericHandler) 227 | } 228 | if (switches != null) { 229 | subscribe(switches, "switch", genericHandler) 230 | } 231 | if (switchLevels != null) { 232 | subscribe(switchLevels, "level", genericHandler) 233 | } 234 | if (temperatures != null) { 235 | subscribe(temperatures, "temperature", genericHandler) 236 | } 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) 245 | } 246 | if (valves != null) { 247 | subscribe(valves, "contact", genericHandler) 248 | } 249 | if (waterSensors != null) { 250 | subscribe(waterSensors, "water", genericHandler) 251 | } 252 | } 253 | 254 | def installed() { 255 | atomicState.version = "1.2.0" 256 | 257 | atomicState.isBucketCreated = false 258 | atomicState.grokerSubdomain = "groker" 259 | atomicState.isDisabled = false 260 | 261 | subscribeToEvents() 262 | 263 | atomicState.isBucketCreated = false 264 | atomicState.grokerSubdomain = "groker" 265 | 266 | log.debug "installed (version $atomicState.version)" 267 | } 268 | 269 | def updated() { 270 | atomicState.version = "1.2.0" 271 | unsubscribe() 272 | 273 | if (atomicState.bucketKey != null && atomicState.accessKey != null) { 274 | atomicState.isBucketCreated = false 275 | } 276 | if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { 277 | atomicState.grokerSubdomain = "groker" 278 | } 279 | 280 | subscribeToEvents() 281 | 282 | log.debug "updated (version $atomicState.version)" 283 | } 284 | 285 | def uninstalled() { 286 | log.debug "uninstalled (version $atomicState.version)" 287 | } 288 | 289 | def tryCreateBucket() { 290 | // if the integration has been disabled, don't do anything 291 | if (atomicState.isDisabled) { 292 | log.debug "integration is disabled" 293 | return 294 | } 295 | 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" 299 | return 300 | } 301 | 302 | // if the bucket has already been created, no need to continue 303 | if (atomicState.isBucketCreated) { 304 | return 305 | } 306 | 307 | if (!atomicState.bucketName) { 308 | atomicState.bucketName = atomicState.bucketKey 309 | } 310 | if (!atomicState.accessKey) { 311 | return 312 | } 313 | def bucketName = "${atomicState.bucketName}" 314 | def bucketKey = "${atomicState.bucketKey}" 315 | def accessKey = "${atomicState.accessKey}" 316 | 317 | def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") 318 | 319 | def bucketCreatePost = [ 320 | uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", 321 | headers: [ 322 | "Content-Type": "application/json", 323 | "X-IS-AccessKey": accessKey 324 | ], 325 | body: bucketCreateBody 326 | ] 327 | 328 | log.debug bucketCreatePost 329 | 330 | try { 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" 336 | } else { 337 | atomicState.isBucketCreated = true 338 | } 339 | } 340 | } catch (e) { 341 | log.error "bucket creation error: $e" 342 | } 343 | 344 | } 345 | 346 | def genericHandler(evt) { 347 | log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value" 348 | 349 | def key = "$evt.displayName($evt.name)" 350 | if (evt.unit != null) { 351 | key = "$evt.displayName(${evt.name}_$evt.unit)" 352 | } 353 | def value = "$evt.value" 354 | 355 | tryCreateBucket() 356 | 357 | eventHandler(key, value) 358 | } 359 | 360 | def eventHandler(name, value) { 361 | def epoch = now() / 1000 362 | 363 | def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}") 364 | 365 | tryShipEvents(event) 366 | 367 | log.debug "Shipped Event: " + event 368 | } 369 | 370 | def tryShipEvents(event) { 371 | 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" 375 | return 376 | } 377 | 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" 382 | return 383 | } 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) { 388 | return 389 | } 390 | 391 | def eventPost = [ 392 | uri: "https://${grokerSubdomain}.initialstate.com/api/events", 393 | headers: [ 394 | "Content-Type": "application/json", 395 | "X-IS-BucketKey": "${bucketKey}", 396 | "X-IS-AccessKey": "${accessKey}", 397 | "Accept-Version": "0.0.2" 398 | ], 399 | body: event 400 | ] 401 | 402 | try { 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}" 408 | } 409 | } 410 | } catch (e) { 411 | log.error "shipping events failed: $e" 412 | } 413 | 414 | } -------------------------------------------------------------------------------- /unbuffered-event-sender.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Initial State Event Streamer (non-buffered) 3 | * 4 | * Copyright 2016 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: "DIY 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 | def subscribeToEvents() { 66 | if (accelerometers != null) { 67 | subscribe(accelerometers, "acceleration", genericHandler) 68 | } 69 | if (alarms != null) { 70 | subscribe(alarms, "alarm", genericHandler) 71 | } 72 | if (batteries != null) { 73 | subscribe(batteries, "battery", genericHandler) 74 | } 75 | if (beacons != null) { 76 | subscribe(beacons, "presence", genericHandler) 77 | } 78 | 79 | if (cos != null) { 80 | subscribe(cos, "carbonMonoxide", genericHandler) 81 | } 82 | if (colors != null) { 83 | subscribe(colors, "hue", genericHandler) 84 | subscribe(colors, "saturation", genericHandler) 85 | subscribe(colors, "color", genericHandler) 86 | } 87 | if (contacts != null) { 88 | subscribe(contacts, "contact", genericHandler) 89 | } 90 | if (energyMeters != null) { 91 | subscribe(energyMeters, "energy", genericHandler) 92 | } 93 | if (illuminances != null) { 94 | subscribe(illuminances, "illuminance", genericHandler) 95 | } 96 | if (locks != null) { 97 | subscribe(locks, "lock", genericHandler) 98 | } 99 | if (motions != null) { 100 | subscribe(motions, "motion", genericHandler) 101 | } 102 | if (musicPlayers != null) { 103 | subscribe(musicPlayers, "status", genericHandler) 104 | subscribe(musicPlayers, "level", genericHandler) 105 | subscribe(musicPlayers, "trackDescription", genericHandler) 106 | subscribe(musicPlayers, "trackData", genericHandler) 107 | subscribe(musicPlayers, "mute", genericHandler) 108 | } 109 | if (powerMeters != null) { 110 | subscribe(powerMeters, "power", genericHandler) 111 | } 112 | if (presences != null) { 113 | subscribe(presences, "presence", genericHandler) 114 | } 115 | if (humidities != null) { 116 | subscribe(humidities, "humidity", genericHandler) 117 | } 118 | if (relaySwitches != null) { 119 | subscribe(relaySwitches, "switch", genericHandler) 120 | } 121 | if (sleepSensors != null) { 122 | subscribe(sleepSensors, "sleeping", genericHandler) 123 | } 124 | if (smokeDetectors != null) { 125 | subscribe(smokeDetectors, "smoke", genericHandler) 126 | } 127 | if (peds != null) { 128 | subscribe(peds, "steps", genericHandler) 129 | subscribe(peds, "goal", genericHandler) 130 | } 131 | if (switches != null) { 132 | subscribe(switches, "switch", genericHandler) 133 | } 134 | if (switchLevels != null) { 135 | subscribe(switchLevels, "level", genericHandler) 136 | } 137 | if (temperatures != null) { 138 | subscribe(temperatures, "temperature", genericHandler) 139 | } 140 | if (thermostats != null) { 141 | subscribe(thermostats, "temperature", genericHandler) 142 | subscribe(thermostats, "heatingSetpoint", genericHandler) 143 | subscribe(thermostats, "coolingSetpoint", genericHandler) 144 | subscribe(thermostats, "thermostatSetpoint", genericHandler) 145 | subscribe(thermostats, "thermostatMode", genericHandler) 146 | subscribe(thermostats, "thermostatFanMode", genericHandler) 147 | subscribe(thermostats, "thermostatOperatingState", genericHandler) 148 | } 149 | if (valves != null) { 150 | subscribe(valves, "contact", genericHandler) 151 | } 152 | if (waterSensors != null) { 153 | subscribe(waterSensors, "water", genericHandler) 154 | } 155 | } 156 | 157 | def installed() { 158 | atomicState.version = "1.0.18 (unbuffered)" 159 | 160 | atomicState.bucketKey = "SmartThings" //change if needed 161 | atomicState.bucketName = "SmartThings" //change if wanted 162 | atomicState.accessKey = "YOUR_ACCESS_KEY" //MUST CHANGE 163 | 164 | subscribeToEvents() 165 | 166 | atomicState.isBucketCreated = false 167 | atomicState.grokerSubdomain = "groker" 168 | 169 | log.debug "installed (version $atomicState.version)" 170 | } 171 | 172 | def updated() { 173 | atomicState.version = "1.0.18 (unbuffered)" 174 | unsubscribe() 175 | 176 | if (atomicState.bucketKey != null && atomicState.accessKey != null) { 177 | atomicState.isBucketCreated = false 178 | } 179 | if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { 180 | atomicState.grokerSubdomain = "groker" 181 | } 182 | 183 | subscribeToEvents() 184 | 185 | log.debug "updated (version $atomicState.version)" 186 | } 187 | 188 | def uninstalled() { 189 | log.debug "uninstalled (version $atomicState.version)" 190 | } 191 | 192 | def tryCreateBucket() { 193 | 194 | // can't ship events if there is no grokerSubdomain 195 | if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { 196 | log.error "streaming url is currently null" 197 | return 198 | } 199 | 200 | // if the bucket has already been created, no need to continue 201 | if (atomicState.isBucketCreated) { 202 | return 203 | } 204 | 205 | if (!atomicState.bucketName) { 206 | atomicState.bucketName = atomicState.bucketKey 207 | } 208 | if (!atomicState.accessKey) { 209 | return 210 | } 211 | def bucketName = "${atomicState.bucketName}" 212 | def bucketKey = "${atomicState.bucketKey}" 213 | def accessKey = "${atomicState.accessKey}" 214 | 215 | def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") 216 | 217 | def bucketCreatePost = [ 218 | uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", 219 | headers: [ 220 | "Content-Type": "application/json", 221 | "X-IS-AccessKey": accessKey 222 | ], 223 | body: bucketCreateBody 224 | ] 225 | 226 | log.debug bucketCreatePost 227 | 228 | try { 229 | // Create a bucket on Initial State so the data has a logical grouping 230 | httpPostJson(bucketCreatePost) { resp -> 231 | log.debug "bucket posted" 232 | if (resp.status >= 400) { 233 | log.error "bucket not created successfully" 234 | } else { 235 | atomicState.isBucketCreated = true 236 | } 237 | } 238 | } catch (e) { 239 | log.error "bucket creation error: $e" 240 | } 241 | 242 | } 243 | 244 | def genericHandler(evt) { 245 | log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value" 246 | 247 | def key = "$evt.displayName($evt.name)" 248 | if (evt.unit != null) { 249 | key = "$evt.displayName(${evt.name}_$evt.unit)" 250 | } 251 | def value = "$evt.value" 252 | 253 | tryCreateBucket() 254 | 255 | eventHandler(key, value) 256 | } 257 | 258 | def eventHandler(name, value) { 259 | def epoch = now() / 1000 260 | 261 | def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}") 262 | 263 | tryShipEvents(event) 264 | 265 | log.debug "Shipped Event: " + event 266 | } 267 | 268 | def tryShipEvents(event) { 269 | 270 | def grokerSubdomain = atomicState.grokerSubdomain 271 | // can't ship events if there is no grokerSubdomain 272 | if (grokerSubdomain == null || grokerSubdomain == "") { 273 | log.error "streaming url is currently null" 274 | return 275 | } 276 | def accessKey = atomicState.accessKey 277 | def bucketKey = atomicState.bucketKey 278 | // can't ship if access key and bucket key are null, so finish trying 279 | if (accessKey == null || bucketKey == null) { 280 | return 281 | } 282 | 283 | def eventPost = [ 284 | uri: "https://${grokerSubdomain}.initialstate.com/api/events", 285 | headers: [ 286 | "Content-Type": "application/json", 287 | "X-IS-BucketKey": "${bucketKey}", 288 | "X-IS-AccessKey": "${accessKey}", 289 | "Accept-Version": "0.0.2" 290 | ], 291 | body: event 292 | ] 293 | 294 | try { 295 | // post the events to initial state 296 | httpPostJson(eventPost) { resp -> 297 | log.debug "shipped events and got ${resp.status}" 298 | if (resp.status >= 400) { 299 | log.error "shipping failed... ${resp.data}" 300 | } 301 | } 302 | } catch (e) { 303 | log.error "shipping events failed: $e" 304 | } 305 | 306 | } --------------------------------------------------------------------------------