Merge pull request #118 from InitialState/master

New capabilities; buffering; scheduling
This commit is contained in:
Kristofer Schaller
2015-09-14 16:36:33 -07:00

View File

@@ -11,8 +11,13 @@
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * 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 * 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. * 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( definition(
name: "Initial State Event Streamer", name: "Initial State Event Streamer",
namespace: "initialstate.events", namespace: "initialstate.events",
@@ -28,32 +33,31 @@ import groovy.json.JsonSlurper
preferences { preferences {
section("Choose which devices to monitor...") { section("Choose which devices to monitor...") {
//input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false
input "alarms", "capability.alarm", title: "Alarms", 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 "batteries", "capability.battery", title: "Batteries", multiple: true, required: false
//input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false
//input "buttons", "capability.button", title: "Buttons", multiple: true, required: false input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", 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 "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false
input "contacts", "capability.contactSensor", title: "Contact Sensors", 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 "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false
//input "energyMeters", "capability.energyMeter", title: "Energy Meters", 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 "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false
input "locks", "capability.lock", title: "Locks", 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 "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false
//input "musicPlayers", "capability.musicPlayer", title: "Music Players", 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 "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false
input "presences", "capability.presenceSensor", title: "Presence Sensors", 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 "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false
//input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", 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 "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false
//input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", 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 "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false
input "switches", "capability.switch", title: "Switches", 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 "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false
input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", 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 "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false
//input "valves", "capability.valve", title: "Valves", 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 input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false
} }
} }
@@ -74,77 +78,71 @@ mappings {
} }
def subscribeToEvents() { def subscribeToEvents() {
/*if (accelerometers != null) { if (accelerometers != null) {
subscribe(accelerometers, "acceleration", genericHandler) subscribe(accelerometers, "acceleration", genericHandler)
}*/ }
if (alarms != null) { if (alarms != null) {
subscribe(alarms, "alarm", genericHandler) subscribe(alarms, "alarm", genericHandler)
} }
/*if (batteries != null) { if (batteries != null) {
subscribe(batteries, "battery", genericHandler) subscribe(batteries, "battery", genericHandler)
}*/ }
/*if (beacons != null) { if (beacons != null) {
subscribe(beacons, "presence", genericHandler) subscribe(beacons, "presence", genericHandler)
}*/ }
/*
if (buttons != null) { if (cos != null) {
subscribe(buttons, "button", genericHandler)
}*/
/*if (cos != null) {
subscribe(cos, "carbonMonoxide", genericHandler) subscribe(cos, "carbonMonoxide", genericHandler)
}*/ }
/*if (colors != null) { if (colors != null) {
subscribe(colors, "hue", genericHandler) subscribe(colors, "hue", genericHandler)
subscribe(colors, "saturation", genericHandler) subscribe(colors, "saturation", genericHandler)
subscribe(colors, "color", genericHandler) subscribe(colors, "color", genericHandler)
}*/ }
if (contacts != null) { if (contacts != null) {
subscribe(contacts, "contact", genericHandler) subscribe(contacts, "contact", genericHandler)
} }
/*if (doorsControllers != null) { if (energyMeters != null) {
subscribe(doorsControllers, "door", genericHandler)
}*/
/*if (energyMeters != null) {
subscribe(energyMeters, "energy", genericHandler) subscribe(energyMeters, "energy", genericHandler)
}*/ }
/*if (illuminances != null) { if (illuminances != null) {
subscribe(illuminances, "illuminance", genericHandler) subscribe(illuminances, "illuminance", genericHandler)
}*/ }
if (locks != null) { if (locks != null) {
subscribe(locks, "lock", genericHandler) subscribe(locks, "lock", genericHandler)
} }
if (motions != null) { if (motions != null) {
subscribe(motions, "motion", genericHandler) subscribe(motions, "motion", genericHandler)
} }
/*if (musicPlayers != null) { if (musicPlayers != null) {
subscribe(musicPlayers, "status", genericHandler) subscribe(musicPlayers, "status", genericHandler)
subscribe(musicPlayers, "level", genericHandler) subscribe(musicPlayers, "level", genericHandler)
subscribe(musicPlayers, "trackDescription", genericHandler) subscribe(musicPlayers, "trackDescription", genericHandler)
subscribe(musicPlayers, "trackData", genericHandler) subscribe(musicPlayers, "trackData", genericHandler)
subscribe(musicPlayers, "mute", genericHandler) subscribe(musicPlayers, "mute", genericHandler)
}*/ }
/*if (powerMeters != null) { if (powerMeters != null) {
subscribe(powerMeters, "power", genericHandler) subscribe(powerMeters, "power", genericHandler)
}*/ }
if (presences != null) { if (presences != null) {
subscribe(presences, "presence", genericHandler) subscribe(presences, "presence", genericHandler)
} }
if (humidities != null) { if (humidities != null) {
subscribe(humidities, "humidity", genericHandler) subscribe(humidities, "humidity", genericHandler)
} }
/*if (relaySwitches != null) { if (relaySwitches != null) {
subscribe(relaySwitches, "switch", genericHandler) subscribe(relaySwitches, "switch", genericHandler)
}*/ }
/*if (sleepSensors != null) { if (sleepSensors != null) {
subscribe(sleepSensors, "sleeping", genericHandler) subscribe(sleepSensors, "sleeping", genericHandler)
}*/ }
/*if (smokeDetectors != null) { if (smokeDetectors != null) {
subscribe(smokeDetectors, "smoke", genericHandler) subscribe(smokeDetectors, "smoke", genericHandler)
}*/ }
/*if (peds != null) { if (peds != null) {
subscribe(peds, "steps", genericHandler) subscribe(peds, "steps", genericHandler)
subscribe(peds, "goal", genericHandler) subscribe(peds, "goal", genericHandler)
}*/ }
if (switches != null) { if (switches != null) {
subscribe(switches, "switch", genericHandler) subscribe(switches, "switch", genericHandler)
} }
@@ -163,9 +161,9 @@ def subscribeToEvents() {
subscribe(thermostats, "thermostatFanMode", genericHandler) subscribe(thermostats, "thermostatFanMode", genericHandler)
subscribe(thermostats, "thermostatOperatingState", genericHandler) subscribe(thermostats, "thermostatOperatingState", genericHandler)
} }
/*if (valves != null) { if (valves != null) {
subscribe(valves, "contact", genericHandler) subscribe(valves, "contact", genericHandler)
}*/ }
if (waterSensors != null) { if (waterSensors != null) {
subscribe(waterSensors, "water", genericHandler) subscribe(waterSensors, "water", genericHandler)
} }
@@ -173,23 +171,23 @@ def subscribeToEvents() {
def getAccessKey() { def getAccessKey() {
log.trace "get access key" log.trace "get access key"
if (state.accessKey == null) { if (atomicState.accessKey == null) {
httpError(404, "Access Key Not Found") httpError(404, "Access Key Not Found")
} else { } else {
[ [
accessKey: state.accessKey accessKey: atomicState.accessKey
] ]
} }
} }
def getBucketKey() { def getBucketKey() {
log.trace "get bucket key" log.trace "get bucket key"
if (state.bucketKey == null) { if (atomicState.bucketKey == null) {
httpError(404, "Bucket key Not Found") httpError(404, "Bucket key Not Found")
} else { } else {
[ [
bucketKey: state.bucketKey, bucketKey: atomicState.bucketKey,
bucketName: state.bucketName bucketName: atomicState.bucketName
] ]
} }
} }
@@ -202,53 +200,94 @@ def setBucketKey() {
log.debug "bucket name: $newBucketName" log.debug "bucket name: $newBucketName"
log.debug "bucket key: $newBucketKey" log.debug "bucket key: $newBucketKey"
if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) { if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) {
state.bucketKey = "$newBucketKey" atomicState.bucketKey = "$newBucketKey"
state.bucketName = "$newBucketName" atomicState.bucketName = "$newBucketName"
state.isBucketCreated = false atomicState.isBucketCreated = false
} }
tryCreateBucket()
} }
def setAccessKey() { def setAccessKey() {
log.trace "set access key" log.trace "set access key"
def newAccessKey = request.JSON?.accessKey def newAccessKey = request.JSON?.accessKey
def newGrokerSubdomain = request.JSON?.grokerSubdomain
if (newAccessKey && newAccessKey != state.accessKey) { if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) {
state.accessKey = "$newAccessKey" atomicState.grokerSubdomain = "$newGrokerSubdomain"
state.isBucketCreated = false atomicState.isBucketCreated = false
}
if (newAccessKey && newAccessKey != atomicState.accessKey) {
atomicState.accessKey = "$newAccessKey"
atomicState.isBucketCreated = false
} }
} }
def installed() { def installed() {
atomicState.version = "1.0.18"
subscribeToEvents() subscribeToEvents()
state.isBucketCreated = false atomicState.isBucketCreated = false
atomicState.grokerSubdomain = "groker"
atomicState.eventBuffer = []
runEvery15Minutes(flushBuffer)
log.debug "installed (version $atomicState.version)"
} }
def updated() { def updated() {
atomicState.version = "1.0.18"
unsubscribe() unsubscribe()
if (state.bucketKey != null && state.accessKey != null) { if (atomicState.bucketKey != null && atomicState.accessKey != null) {
state.isBucketCreated = false atomicState.isBucketCreated = false
} }
if (atomicState.eventBuffer == null) {
atomicState.eventBuffer = []
}
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
atomicState.grokerSubdomain = "groker"
}
subscribeToEvents() subscribeToEvents()
log.debug "updated (version $atomicState.version)"
} }
def createBucket() { def uninstalled() {
log.debug "uninstalled (version $atomicState.version)"
}
if (!state.bucketName) { def tryCreateBucket() {
state.bucketName = state.bucketKey
// 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
} }
def bucketName = "${state.bucketName}" if (!atomicState.accessKey) {
def bucketKey = "${state.bucketKey}" return
def accessKey = "${state.accessKey}" }
def bucketName = "${atomicState.bucketName}"
def bucketKey = "${atomicState.bucketKey}"
def accessKey = "${atomicState.accessKey}"
def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}")
def bucketCreatePost = [ def bucketCreatePost = [
uri: 'https://groker.initialstate.com/api/buckets', uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets",
headers: [ headers: [
"Content-Type": "application/json", "Content-Type": "application/json",
"X-IS-AccessKey": accessKey "X-IS-AccessKey": accessKey
@@ -258,10 +297,20 @@ def createBucket() {
log.debug bucketCreatePost log.debug bucketCreatePost
httpPostJson(bucketCreatePost) { try {
log.debug "bucket posted" // Create a bucket on Initial State so the data has a logical grouping
state.isBucketCreated = true 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) { def genericHandler(evt) {
@@ -273,33 +322,73 @@ def genericHandler(evt) {
} }
def value = "$evt.value" def value = "$evt.value"
tryCreateBucket()
eventHandler(key, value) eventHandler(key, value)
} }
def eventHandler(name, value) { // This is a handler function for flushing the event buffer
// after a specified amount of time to reduce the load on ST servers
def flushBuffer() {
log.trace "About to flush the buffer on schedule"
if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) {
tryShipEvents()
}
}
if (state.accessKey == null || state.bucketKey == null) { def eventHandler(name, value) {
log.debug atomicState.eventBuffer
def eventBuffer = atomicState.eventBuffer
def epoch = now() / 1000
eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"]
log.debug eventBuffer
atomicState.eventBuffer = eventBuffer
if (eventBuffer.size() >= 10) {
tryShipEvents()
}
}
// a helper function for shipping the atomicState.eventBuffer to Initial State
def tryShipEvents() {
// can't ship events if there is no grokerSubdomain
if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") {
log.error "streaming url is currently null"
return
}
// can't ship if access key and bucket key are null, so finish trying
if (atomicState.accessKey == null || atomicState.bucketKey == null) {
return return
} }
if (!state.isBucketCreated) {
createBucket()
}
def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]")
def eventPost = [ def eventPost = [
uri: 'https://groker.initialstate.com/api/events', uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events",
headers: [ headers: [
"Content-Type": "application/json", "Content-Type": "application/json",
"X-IS-BucketKey": "${state.bucketKey}", "X-IS-BucketKey": "${atomicState.bucketKey}",
"X-IS-AccessKey": "${state.accessKey}" "X-IS-AccessKey": "${atomicState.accessKey}",
"Accept-Version": "0.0.2"
], ],
body: eventBody body: atomicState.eventBuffer
] ]
log.debug eventPost try {
// post the events to initial state
httpPostJson(eventPost) { httpPostJson(eventPost) { resp ->
log.debug "event data posted" log.debug "shipped events and got ${resp.status}"
if (resp.status >= 400) {
log.error "shipping failed... ${resp.data}"
} else {
// clear the buffer
atomicState.eventBuffer = []
}
}
} catch (e) {
log.error "shipping events failed: $e"
} }
} }