/** * Initial State Event Streamer * * Copyright 2016 David Sulpy * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * SmartThings data is sent from this SmartApp to Initial State. This is event data only for * devices for which the user has authorized. Likewise, Initial State's services call this * SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and * Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms */ definition( name: "Initial State Event Streamer", namespace: "initialstate.events", author: "David Sulpy", description: "A SmartThings SmartApp to allow SmartThings events to be viewable inside an Initial State Event Bucket in your https://www.initialstate.com account.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertica_small.png", iconX2Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png", iconX3Url: "https://s3.amazonaws.com/initialstate-web-cdn/IS-wordmark-vertical.png", oauth: [displayName: "Initial State", displayLink: "https://www.initialstate.com"]) import groovy.json.JsonSlurper preferences { section("Choose which devices to monitor...") { input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false input "locks", "capability.lock", title: "Locks", multiple: true, required: false input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false input "switches", "capability.switch", title: "Switches", multiple: true, required: false input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false input "valves", "capability.valve", title: "Valves", multiple: true, required: false input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false } } mappings { path("/access_key") { action: [ GET: "getAccessKey", PUT: "setAccessKey" ] } path("/bucket") { action: [ GET: "getBucketKey", PUT: "setBucketKey" ] } } def getAccessKey() { log.trace "get access key" if (atomicState.accessKey == null) { httpError(404, "Access Key Not Found") } else { [ accessKey: atomicState.accessKey ] } } def getBucketKey() { log.trace "get bucket key" if (atomicState.bucketKey == null) { httpError(404, "Bucket key Not Found") } else { [ bucketKey: atomicState.bucketKey, bucketName: atomicState.bucketName ] } } def setBucketKey() { log.trace "set bucket key" def newBucketKey = request.JSON?.bucketKey def newBucketName = request.JSON?.bucketName log.debug "bucket name: $newBucketName" log.debug "bucket key: $newBucketKey" if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) { atomicState.bucketKey = "$newBucketKey" atomicState.bucketName = "$newBucketName" atomicState.isBucketCreated = false } tryCreateBucket() } def setAccessKey() { log.trace "set access key" def newAccessKey = request.JSON?.accessKey def newGrokerSubdomain = request.JSON?.grokerSubdomain if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) { atomicState.grokerSubdomain = "$newGrokerSubdomain" atomicState.isBucketCreated = false } if (newAccessKey && newAccessKey != atomicState.accessKey) { atomicState.accessKey = "$newAccessKey" atomicState.isBucketCreated = false } } def subscribeToEvents() { if (accelerometers != null) { subscribe(accelerometers, "acceleration", genericHandler) } if (alarms != null) { subscribe(alarms, "alarm", genericHandler) } if (batteries != null) { subscribe(batteries, "battery", genericHandler) } if (beacons != null) { subscribe(beacons, "presence", genericHandler) } if (cos != null) { subscribe(cos, "carbonMonoxide", genericHandler) } if (colors != null) { subscribe(colors, "hue", genericHandler) subscribe(colors, "saturation", genericHandler) subscribe(colors, "color", genericHandler) } if (contacts != null) { subscribe(contacts, "contact", genericHandler) } if (energyMeters != null) { subscribe(energyMeters, "energy", genericHandler) } if (illuminances != null) { subscribe(illuminances, "illuminance", genericHandler) } if (locks != null) { subscribe(locks, "lock", genericHandler) } if (motions != null) { subscribe(motions, "motion", genericHandler) } if (musicPlayers != null) { subscribe(musicPlayers, "status", genericHandler) subscribe(musicPlayers, "level", genericHandler) subscribe(musicPlayers, "trackDescription", genericHandler) subscribe(musicPlayers, "trackData", genericHandler) subscribe(musicPlayers, "mute", genericHandler) } if (powerMeters != null) { subscribe(powerMeters, "power", genericHandler) } if (presences != null) { subscribe(presences, "presence", genericHandler) } if (humidities != null) { subscribe(humidities, "humidity", genericHandler) } if (relaySwitches != null) { subscribe(relaySwitches, "switch", genericHandler) } if (sleepSensors != null) { subscribe(sleepSensors, "sleeping", genericHandler) } if (smokeDetectors != null) { subscribe(smokeDetectors, "smoke", genericHandler) } if (peds != null) { subscribe(peds, "steps", genericHandler) subscribe(peds, "goal", genericHandler) } if (switches != null) { subscribe(switches, "switch", genericHandler) } if (switchLevels != null) { subscribe(switchLevels, "level", genericHandler) } if (temperatures != null) { subscribe(temperatures, "temperature", genericHandler) } if (thermostats != null) { subscribe(thermostats, "temperature", genericHandler) subscribe(thermostats, "heatingSetpoint", genericHandler) subscribe(thermostats, "coolingSetpoint", genericHandler) subscribe(thermostats, "thermostatSetpoint", genericHandler) subscribe(thermostats, "thermostatMode", genericHandler) subscribe(thermostats, "thermostatFanMode", genericHandler) subscribe(thermostats, "thermostatOperatingState", genericHandler) } if (valves != null) { subscribe(valves, "contact", genericHandler) } if (waterSensors != null) { subscribe(waterSensors, "water", genericHandler) } } def installed() { atomicState.version = "1.1.0" atomicState.isBucketCreated = false atomicState.grokerSubdomain = "groker" subscribeToEvents() atomicState.isBucketCreated = false atomicState.grokerSubdomain = "groker" log.debug "installed (version $atomicState.version)" } def updated() { atomicState.version = "1.1.0" unsubscribe() if (atomicState.bucketKey != null && atomicState.accessKey != null) { atomicState.isBucketCreated = false } if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { atomicState.grokerSubdomain = "groker" } subscribeToEvents() log.debug "updated (version $atomicState.version)" } def uninstalled() { log.debug "uninstalled (version $atomicState.version)" } def tryCreateBucket() { // can't ship events if there is no grokerSubdomain if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { log.error "streaming url is currently null" return } // if the bucket has already been created, no need to continue if (atomicState.isBucketCreated) { return } if (!atomicState.bucketName) { atomicState.bucketName = atomicState.bucketKey } if (!atomicState.accessKey) { return } def bucketName = "${atomicState.bucketName}" def bucketKey = "${atomicState.bucketKey}" def accessKey = "${atomicState.accessKey}" def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") def bucketCreatePost = [ uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", headers: [ "Content-Type": "application/json", "X-IS-AccessKey": accessKey ], body: bucketCreateBody ] log.debug bucketCreatePost try { // Create a bucket on Initial State so the data has a logical grouping httpPostJson(bucketCreatePost) { resp -> log.debug "bucket posted" if (resp.status >= 400) { log.error "bucket not created successfully" } else { atomicState.isBucketCreated = true } } } catch (e) { log.error "bucket creation error: $e" } } def genericHandler(evt) { log.trace "$evt.displayName($evt.name:$evt.unit) $evt.value" def key = "$evt.displayName($evt.name)" if (evt.unit != null) { key = "$evt.displayName(${evt.name}_$evt.unit)" } def value = "$evt.value" tryCreateBucket() eventHandler(key, value) } def eventHandler(name, value) { def epoch = now() / 1000 def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}") tryShipEvents(event) log.debug "Shipped Event: " + event } def tryShipEvents(event) { def grokerSubdomain = atomicState.grokerSubdomain // can't ship events if there is no grokerSubdomain if (grokerSubdomain == null || grokerSubdomain == "") { log.error "streaming url is currently null" return } def accessKey = atomicState.accessKey def bucketKey = atomicState.bucketKey // can't ship if access key and bucket key are null, so finish trying if (accessKey == null || bucketKey == null) { return } def eventPost = [ uri: "https://${grokerSubdomain}.initialstate.com/api/events", headers: [ "Content-Type": "application/json", "X-IS-BucketKey": "${bucketKey}", "X-IS-AccessKey": "${accessKey}", "Accept-Version": "0.0.2" ], body: event ] try { // post the events to initial state httpPostJson(eventPost) { resp -> log.debug "shipped events and got ${resp.status}" if (resp.status >= 400) { log.error "shipping failed... ${resp.data}" } } } catch (e) { log.error "shipping events failed: $e" } }