From 0fcff53eba9e9fed33fce1660fc6586817f560ac Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Fri, 14 Aug 2015 14:41:27 -0500 Subject: [PATCH 01/13] Removed door controllers from comments and further implementation to see if that fixes ST 403 error on authentication --- .../initial-state-event-streamer.groovy | 4 ---- 1 file changed, 4 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 566f8e1..27b9351 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -36,7 +36,6 @@ preferences { //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 @@ -101,9 +100,6 @@ def subscribeToEvents() { if (contacts != null) { subscribe(contacts, "contact", genericHandler) } - /*if (doorsControllers != null) { - subscribe(doorsControllers, "door", genericHandler) - }*/ /*if (energyMeters != null) { subscribe(energyMeters, "energy", genericHandler) }*/ From 8d07e81b80c38091f7f74f7f29687a4c6bb4c0f4 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Tue, 25 Aug 2015 12:23:05 -0500 Subject: [PATCH 02/13] Added all capabilities possible sans door controllers and buttons. --- .../initial-state-event-streamer.groovy | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 27b9351..5aaeeac 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -28,31 +28,31 @@ import groovy.json.JsonSlurper preferences { 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 "batteries", "capability.battery", title: "Batteries", multiple: true, required: false - //input "beacons", "capability.beacon", title: "Beacons", 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 "buttons", "capability.button", title: "Buttons", 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 "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 "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false - //input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance 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 "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 "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 "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 "valves", "capability.valve", title: "Valves", multiple: true, required: false input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false } } @@ -73,74 +73,74 @@ mappings { } def subscribeToEvents() { - /*if (accelerometers != null) { + if (accelerometers != null) { subscribe(accelerometers, "acceleration", genericHandler) - }*/ + } if (alarms != null) { subscribe(alarms, "alarm", genericHandler) } - /*if (batteries != null) { + if (batteries != null) { subscribe(batteries, "battery", genericHandler) - }*/ - /*if (beacons != null) { + } + if (beacons != null) { subscribe(beacons, "presence", genericHandler) - }*/ - /* - if (buttons != null) { + } + + /*if (buttons != null) { subscribe(buttons, "button", genericHandler) }*/ - /*if (cos != null) { + if (cos != null) { subscribe(cos, "carbonMonoxide", genericHandler) - }*/ - /*if (colors != null) { + } + 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) { + if (energyMeters != null) { subscribe(energyMeters, "energy", genericHandler) - }*/ - /*if (illuminances != null) { + } + if (illuminances != null) { subscribe(illuminances, "illuminance", genericHandler) - }*/ + } if (locks != null) { subscribe(locks, "lock", genericHandler) } if (motions != null) { subscribe(motions, "motion", genericHandler) } - /*if (musicPlayers != null) { + 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) { + } + if (powerMeters != null) { subscribe(powerMeters, "power", genericHandler) - }*/ + } if (presences != null) { subscribe(presences, "presence", genericHandler) } if (humidities != null) { subscribe(humidities, "humidity", genericHandler) } - /*if (relaySwitches != null) { + if (relaySwitches != null) { subscribe(relaySwitches, "switch", genericHandler) - }*/ - /*if (sleepSensors != null) { + } + if (sleepSensors != null) { subscribe(sleepSensors, "sleeping", genericHandler) - }*/ - /*if (smokeDetectors != null) { + } + if (smokeDetectors != null) { subscribe(smokeDetectors, "smoke", genericHandler) - }*/ - /*if (peds != null) { + } + if (peds != null) { subscribe(peds, "steps", genericHandler) subscribe(peds, "goal", genericHandler) - }*/ + } if (switches != null) { subscribe(switches, "switch", genericHandler) } @@ -159,9 +159,9 @@ def subscribeToEvents() { subscribe(thermostats, "thermostatFanMode", genericHandler) subscribe(thermostats, "thermostatOperatingState", genericHandler) } - /*if (valves != null) { + if (valves != null) { subscribe(valves, "contact", genericHandler) - }*/ + } if (waterSensors != null) { subscribe(waterSensors, "water", genericHandler) } @@ -208,7 +208,13 @@ def setBucketKey() { def setAccessKey() { log.trace "set access key" def newAccessKey = request.JSON?.accessKey + def newGrokerSubdomain = request.JSON?.grokerSubdomain + if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != state.grokerSubdomain) { + state.grokerSubdomain = "$newGrokerSubdomain" + state.isBucketCreated = false + } + if (newAccessKey && newAccessKey != state.accessKey) { state.accessKey = "$newAccessKey" state.isBucketCreated = false @@ -220,6 +226,7 @@ def installed() { subscribeToEvents() state.isBucketCreated = false + state.grokerSubdomain = "groker" } def updated() { @@ -244,7 +251,7 @@ def createBucket() { def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") def bucketCreatePost = [ - uri: 'https://groker.initialstate.com/api/buckets', + uri: "https://${state.grokerSubdomain}.initialstate.com/api/buckets", headers: [ "Content-Type": "application/json", "X-IS-AccessKey": accessKey @@ -284,7 +291,7 @@ def eventHandler(name, value) { def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]") def eventPost = [ - uri: 'https://groker.initialstate.com/api/events', + uri: "https://${state.grokerSubdomain}.initialstate.com/api/events", headers: [ "Content-Type": "application/json", "X-IS-BucketKey": "${state.bucketKey}", From 5719bbcaac024dc03226549e05527b48c707db44 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Thu, 10 Sep 2015 11:14:27 -0500 Subject: [PATCH 03/13] switched from using state to using atomicState in preparation of event buffering; using atomicState exclusively per important tip found here: http://docs.smartthings.com/en/latest/smartapp-developers-guide/state.html#atomic-state --- .../initial-state-event-streamer.groovy | 65 ++++++++++--------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 4f2b8c7..7c6ac38 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -13,6 +13,7 @@ * for the specific language governing permissions and limitations under the License. * */ + definition( name: "Initial State Event Streamer", namespace: "initialstate.events", @@ -173,24 +174,24 @@ def subscribeToEvents() { def getAccessKey() { log.trace "get access key" - if (state.accessKey == null) { + if (atomicState.accessKey == null) { httpError(404, "Access Key Not Found") } else { [ - grokerRootUrl: state.grokerRootUrl, - accessKey: state.accessKey + grokerRootUrl: atomicState.grokerRootUrl, + accessKey: atomicState.accessKey ] } } def getBucketKey() { log.trace "get bucket key" - if (state.bucketKey == null) { + if (atomicState.bucketKey == null) { httpError(404, "Bucket key Not Found") } else { [ - bucketKey: state.bucketKey, - bucketName: state.bucketName + bucketKey: atomicState.bucketKey, + bucketName: atomicState.bucketName ] } } @@ -203,10 +204,10 @@ def setBucketKey() { log.debug "bucket name: $newBucketName" log.debug "bucket key: $newBucketKey" - if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) { - state.bucketKey = "$newBucketKey" - state.bucketName = "$newBucketName" - state.isBucketCreated = false + if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) { + atomicState.bucketKey = "$newBucketKey" + atomicState.bucketName = "$newBucketName" + atomicState.isBucketCreated = false } } @@ -215,14 +216,14 @@ def setAccessKey() { def newAccessKey = request.JSON?.accessKey def newGrokerRootUrl = request.JSON?.grokerRootUrl - if (newGrokerRootUrl && newGrokerRootUrl != "" && newGrokerRootUrl != state.grokerRootUrl) { - state.grokerRootUrl = "$newGrokerRootUrl" - state.isBucketCreated = false + if (newGrokerRootUrl && newGrokerRootUrl != "" && newGrokerRootUrl != atomicState.grokerRootUrl) { + atomicState.grokerRootUrl = "$newGrokerRootUrl" + atomicState.isBucketCreated = false } - if (newAccessKey && newAccessKey != state.accessKey) { - state.accessKey = "$newAccessKey" - state.isBucketCreated = false + if (newAccessKey && newAccessKey != atomicState.accessKey) { + atomicState.accessKey = "$newAccessKey" + atomicState.isBucketCreated = false } } @@ -230,15 +231,15 @@ def installed() { subscribeToEvents() - state.isBucketCreated = false - state.grookerRootUrl = "https://groker.initialstate.com" + atomicState.isBucketCreated = false + atomicState.grookerRootUrl = "https://groker.initialstate.com" } def updated() { unsubscribe() - if (state.bucketKey != null && state.accessKey != null) { - state.isBucketCreated = false + if (atomicState.bucketKey != null && atomicState.accessKey != null) { + atomicState.isBucketCreated = false } subscribeToEvents() @@ -246,17 +247,17 @@ def updated() { def createBucket() { - if (!state.bucketName) { - state.bucketName = state.bucketKey + if (!atomicState.bucketName) { + atomicState.bucketName = atomicState.bucketKey } - def bucketName = "${state.bucketName}" - def bucketKey = "${state.bucketKey}" - 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 bucketCreatePost = [ - uri: '${state.grokerRootUrl}/api/buckets', + uri: '${atomicState.grokerRootUrl}/api/buckets', headers: [ "Content-Type": "application/json", "X-IS-AccessKey": accessKey, @@ -269,7 +270,7 @@ def createBucket() { httpPostJson(bucketCreatePost) { log.debug "bucket posted" - state.isBucketCreated = true + atomicState.isBucketCreated = true } } @@ -287,21 +288,21 @@ def genericHandler(evt) { def eventHandler(name, value) { - if (state.accessKey == null || state.bucketKey == null) { + if (atomicState.accessKey == null || atomicState.bucketKey == null) { return } - if (!state.isBucketCreated) { + if (!atomicState.isBucketCreated) { createBucket() } def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]") def eventPost = [ - uri: '${state.grokerRootUrl}/api/events', + uri: '${atomicState.grokerRootUrl}/api/events', headers: [ "Content-Type": "application/json", - "X-IS-BucketKey": "${state.bucketKey}", - "X-IS-AccessKey": "${state.accessKey}", + "X-IS-BucketKey": "${atomicState.bucketKey}", + "X-IS-AccessKey": "${atomicState.accessKey}", "Accept-Version": "0.0.2" ], body: eventBody From bde5abcdb51ad5a5d829ac0b1e3c054c2a66a4b2 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Fri, 11 Sep 2015 10:31:30 -0500 Subject: [PATCH 04/13] added buffering of events to reduce the calls from ST to IS; added buffer flushing after 10 events or 15 min with a scheduler; added a version to show in logs and help troubleshoot --- .../initial-state-event-streamer.groovy | 114 +++++++++++++----- 1 file changed, 86 insertions(+), 28 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 7c6ac38..245c06d 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -11,7 +11,11 @@ * 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( @@ -33,7 +37,6 @@ preferences { 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 "buttons", "capability.button", title: "Buttons", 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 @@ -88,9 +91,6 @@ def subscribeToEvents() { subscribe(beacons, "presence", genericHandler) } - if (buttons != null) { - subscribe(buttons, "button", genericHandler) - } if (cos != null) { subscribe(cos, "carbonMonoxide", genericHandler) } @@ -170,6 +170,10 @@ def subscribeToEvents() { if (waterSensors != null) { subscribe(waterSensors, "water", genericHandler) } + + if (canSchedule()) { + runEvery15Minutes(flushBuffer()) + } } def getAccessKey() { @@ -214,13 +218,13 @@ def setBucketKey() { def setAccessKey() { log.trace "set access key" def newAccessKey = request.JSON?.accessKey - def newGrokerRootUrl = request.JSON?.grokerRootUrl + def newGrokerSubdomain = request.JSON?.grokerSubdomain - if (newGrokerRootUrl && newGrokerRootUrl != "" && newGrokerRootUrl != atomicState.grokerRootUrl) { - atomicState.grokerRootUrl = "$newGrokerRootUrl" + if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) { + atomicState.grokerSubdomain = "$newGrokerSubdomain" atomicState.isBucketCreated = false } - + if (newAccessKey && newAccessKey != atomicState.accessKey) { atomicState.accessKey = "$newAccessKey" atomicState.isBucketCreated = false @@ -228,11 +232,14 @@ def setAccessKey() { } def installed() { - + atomicState.version = "1.0.17" subscribeToEvents() atomicState.isBucketCreated = false - atomicState.grookerRootUrl = "https://groker.initialstate.com" + atomicState.grokerSubdomain = "groker" + atomicState.eventBuffer = []; + + log.debug "installed (version $atomicState.version)" } def updated() { @@ -243,6 +250,14 @@ def updated() { } subscribeToEvents() + + log.debug "updated (version $atomicState.version)" +} + +def uninstalled() { + unsubscribe() + unschedule() + log.debug "uninstalled (version $atomicState.version)" } def createBucket() { @@ -257,7 +272,7 @@ def createBucket() { def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") def bucketCreatePost = [ - uri: '${atomicState.grokerRootUrl}/api/buckets', + uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", headers: [ "Content-Type": "application/json", "X-IS-AccessKey": accessKey, @@ -268,10 +283,20 @@ def createBucket() { log.debug bucketCreatePost - httpPostJson(bucketCreatePost) { - log.debug "bucket posted" - atomicState.isBucketCreated = true + 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) { @@ -283,11 +308,6 @@ def genericHandler(evt) { } def value = "$evt.value" - eventHandler(key, value) -} - -def eventHandler(name, value) { - if (atomicState.accessKey == null || atomicState.bucketKey == null) { return } @@ -296,21 +316,59 @@ def eventHandler(name, value) { createBucket() } - def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]") + eventHandler(key, 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() { + if (atomicState.eventBuffer.size() > 0) { + shipEvents(); + } +} + +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) { + shipEvents(); + } +} + +// a helper function for shipping the atomicState.eventBuffer to Initial State +def shipEvents() { def eventPost = [ - uri: '${atomicState.grokerRootUrl}/api/events', + uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events", headers: [ "Content-Type": "application/json", "X-IS-BucketKey": "${atomicState.bucketKey}", "X-IS-AccessKey": "${atomicState.accessKey}", "Accept-Version": "0.0.2" ], - body: eventBody - ] + body: atomicState.eventBuffer + ]; - log.debug eventPost - - httpPostJson(eventPost) { - log.debug "event data posted" + 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}" + } else { + // clear the buffer + atomicState.eventBuffer = []; + } + } + } catch (e) { + log.error "shipping events failed: $e" } + } \ No newline at end of file From 621bcfadd29fd76c0ba3e981eebe91eb549f1229 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Fri, 11 Sep 2015 10:39:58 -0500 Subject: [PATCH 05/13] fixed a goovy syntax issue with http posts callback closures --- .../initial-state-event-streamer.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 16a9390..d24e271 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -280,7 +280,7 @@ def createBucket() { try { // Create a bucket on Initial State so the data has a logical grouping - httpPostJson(bucketCreatePost) { resp => + httpPostJson(bucketCreatePost) { resp -> log.debug "bucket posted" if (resp.status >= 400) { log.error "bucket not created successfully" @@ -353,7 +353,7 @@ def shipEvents() { try { // post the events to initial state - httpPostJson(eventPost) { resp => + httpPostJson(eventPost) { resp -> log.debug "shipped events and got ${resp.status}" if (resp.status >= 400) { log.error "shipping failed... ${resp.data}" From 7baad1c35e1eec142e9d9d7a7287c2b834e55733 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Fri, 11 Sep 2015 11:09:03 -0500 Subject: [PATCH 06/13] fixed the scheduling from throwing exceptions because it was using parenthesis when passing the handler method in to the scheduling method; ensured that the scheduler doesn't send empty events --- .../initial-state-event-streamer.groovy | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index d24e271..d82ccb3 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -167,10 +167,6 @@ def subscribeToEvents() { if (waterSensors != null) { subscribe(waterSensors, "water", genericHandler) } - - if (canSchedule()) { - runEvery15Minutes(flushBuffer()) - } } def getAccessKey() { @@ -235,6 +231,8 @@ def installed() { atomicState.grokerSubdomain = "groker" atomicState.eventBuffer = []; + runEvery15Minutes(flushBuffer) + log.debug "installed (version $atomicState.version)" } @@ -317,7 +315,8 @@ def genericHandler(evt) { // 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() { - if (atomicState.eventBuffer.size() > 0) { + log.trace "About to flush the buffer on schedule" + if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) { shipEvents(); } } From 80b4d6a665971d2c2e67a8ee34717a3bad1c39bf Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 16:16:21 -0500 Subject: [PATCH 07/13] removed functions from uninstall that may have been causing errors during the uninstall process; added an initializer for the atomicState.eventBuffer on update if it's whiped away; added a check for access key in createBucket to make the function more idempotent --- .../initial-state-event-streamer.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index d82ccb3..502178a 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -242,22 +242,26 @@ def updated() { if (atomicState.bucketKey != null && atomicState.accessKey != null) { atomicState.isBucketCreated = false } - + if (atomicState.eventBuffer == null) { + atomicState.eventBuffer = []; + } + subscribeToEvents() log.debug "updated (version $atomicState.version)" } def uninstalled() { - unsubscribe() - unschedule() log.debug "uninstalled (version $atomicState.version)" } def createBucket() { if (!atomicState.bucketName) { - atomicState.bucketName = atomicState.bucketKey + atomicState.bucketName = atomicState.bucketKey; + } + if (!atomicState.accessKey) { + return; } def bucketName = "${atomicState.bucketName}" def bucketKey = "${atomicState.bucketKey}" From bad978afbd378e09666c9a982cc01b15011dd3cc Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 16:19:30 -0500 Subject: [PATCH 08/13] removed all simicolons as per the groovy-lang.org style guide and better consistency --- .../initial-state-event-streamer.groovy | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 502178a..ae178f2 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -229,7 +229,7 @@ def installed() { atomicState.isBucketCreated = false atomicState.grokerSubdomain = "groker" - atomicState.eventBuffer = []; + atomicState.eventBuffer = [] runEvery15Minutes(flushBuffer) @@ -243,7 +243,7 @@ def updated() { atomicState.isBucketCreated = false } if (atomicState.eventBuffer == null) { - atomicState.eventBuffer = []; + atomicState.eventBuffer = [] } subscribeToEvents() @@ -258,10 +258,10 @@ def uninstalled() { def createBucket() { if (!atomicState.bucketName) { - atomicState.bucketName = atomicState.bucketKey; + atomicState.bucketName = atomicState.bucketKey } if (!atomicState.accessKey) { - return; + return } def bucketName = "${atomicState.bucketName}" def bucketKey = "${atomicState.bucketKey}" @@ -321,23 +321,23 @@ def genericHandler(evt) { def flushBuffer() { log.trace "About to flush the buffer on schedule" if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) { - shipEvents(); + shipEvents() } } def eventHandler(name, value) { - log.debug atomicState.eventBuffer; + log.debug atomicState.eventBuffer - def eventBuffer = atomicState.eventBuffer; - def epoch = now() / 1000; + def eventBuffer = atomicState.eventBuffer + def epoch = now() / 1000 eventBuffer << [key: "$name", value: "$value", epoch: "$epoch"] - log.debug eventBuffer; + log.debug eventBuffer - atomicState.eventBuffer = eventBuffer; + atomicState.eventBuffer = eventBuffer if (eventBuffer.size() >= 10) { - shipEvents(); + shipEvents() } } @@ -352,7 +352,7 @@ def shipEvents() { "Accept-Version": "0.0.2" ], body: atomicState.eventBuffer - ]; + ] try { // post the events to initial state @@ -362,7 +362,7 @@ def shipEvents() { log.error "shipping failed... ${resp.data}" } else { // clear the buffer - atomicState.eventBuffer = []; + atomicState.eventBuffer = [] } } } catch (e) { From 1adb4000a64269e2335e4e1325574674af550dac Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 16:30:40 -0500 Subject: [PATCH 09/13] refactored the methods for creating buckets and sending events to be more idempotent friendly; called tryCreateBucket on bucketKey setting to create the bucket sooner in the workflow than an events shipment --- .../initial-state-event-streamer.groovy | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index ae178f2..d1ccf79 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -205,6 +205,8 @@ def setBucketKey() { atomicState.bucketName = "$newBucketName" atomicState.isBucketCreated = false } + + tryCreateBucket() } def setAccessKey() { @@ -255,7 +257,12 @@ def uninstalled() { log.debug "uninstalled (version $atomicState.version)" } -def createBucket() { +def tryCreateBucket() { + + // if the bucket has already been created, no need to continue + if (atomicState.isBucketCreated) { + return + } if (!atomicState.bucketName) { atomicState.bucketName = atomicState.bucketKey @@ -305,13 +312,7 @@ def genericHandler(evt) { } def value = "$evt.value" - if (atomicState.accessKey == null || atomicState.bucketKey == null) { - return - } - - if (!atomicState.isBucketCreated) { - createBucket() - } + tryCreateBucket() eventHandler(key, value) } @@ -321,7 +322,7 @@ def genericHandler(evt) { def flushBuffer() { log.trace "About to flush the buffer on schedule" if (atomicState.eventBuffer != null && atomicState.eventBuffer.size() > 0) { - shipEvents() + tryShipEvents() } } @@ -337,12 +338,18 @@ def eventHandler(name, value) { atomicState.eventBuffer = eventBuffer if (eventBuffer.size() >= 10) { - shipEvents() + tryShipEvents() } } // a helper function for shipping the atomicState.eventBuffer to Initial State -def shipEvents() { +def tryShipEvents() { + + // can't ship if access key and bucket key are null, so finish trying + if (atomicState.accessKey == null || atomicState.bucketKey == null) { + return + } + def eventPost = [ uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/events", headers: [ From 1c2189b63c5fcb7a1f99d7b1a428ca5715fb0420 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 16:33:48 -0500 Subject: [PATCH 10/13] bumped the version --- .../initial-state-event-streamer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index d1ccf79..b4cf20b 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -226,7 +226,7 @@ def setAccessKey() { } def installed() { - atomicState.version = "1.0.17" + atomicState.version = "1.0.18" subscribeToEvents() atomicState.isBucketCreated = false From 61d2aac45a11c82a6c5ad5c315ba90ed9f5ba7d7 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 16:48:27 -0500 Subject: [PATCH 11/13] added version set in the update function as well so that it will update the version on an update --- .../initial-state-event-streamer.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index b4cf20b..a0e7a9e 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -239,6 +239,7 @@ def installed() { } def updated() { + atomicState.version = "1.0.18" unsubscribe() if (atomicState.bucketKey != null && atomicState.accessKey != null) { From 6eac6affcfd1653ee0f8a0c4ef43df2dd15cb446 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 17:45:04 -0500 Subject: [PATCH 12/13] added some protection against potential error cases if grokerSubdomain isn't properly set --- .../initial-state-event-streamer.groovy | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index a0e7a9e..c480a11 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -248,6 +248,9 @@ def updated() { if (atomicState.eventBuffer == null) { atomicState.eventBuffer = [] } + if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { + atomicState.grokerSubdomain = "groker" + } subscribeToEvents() @@ -260,6 +263,11 @@ def uninstalled() { def tryCreateBucket() { + // can't ship events if there is no grokerSubdomain + if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { + return + } + // if the bucket has already been created, no need to continue if (atomicState.isBucketCreated) { return @@ -346,6 +354,10 @@ def eventHandler(name, value) { // 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 == "") { + return + } // can't ship if access key and bucket key are null, so finish trying if (atomicState.accessKey == null || atomicState.bucketKey == null) { return From 811a1af4bff848fe190f9efff6d3d5aa3488b579 Mon Sep 17 00:00:00 2001 From: David Sulpy Date: Mon, 14 Sep 2015 18:33:21 -0500 Subject: [PATCH 13/13] added an error log when the grokerSubdomain is ever null or empty and the app is trying to ship events or create a bucket --- .../initial-state-event-streamer.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index c480a11..e682f36 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -265,9 +265,10 @@ 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 @@ -356,6 +357,7 @@ 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