GroveStreams SmartApp

Hi, www.grovestreams.com is a service that will graph your devices and let you build a dashboard.

Here’s my SmartApp, adapted from @florianz ThingSpeak app to let you connect your devices to GroveStreams. You’ll need a free account first, when your create one it will prompt you for an organisation name - you can enter something like “Home”. Once you have created your organisation you need to get the “Feed PUT API key”:

It’s long… you might want to email this to your phone so that you can cut and paste it into the ST app.

The SmartApp uses the device name as the “Component” in GroveStreams and the capability attribute (e.g. “temperature”) becomes a “Stream” in GroveStreams. These will be auto created so you don’t need to set them up in GroveStreams. However, once they have been created you may want to edit them (by right-clicking the Component) to set things like the type (e.g. “Boolean”) and the units (e.g. “Yes/No”).

Here’s the SmartApp, let me know if you have any problems

/**
 *  GroveStreams
 *
 *  Copyright 2014 Jason Steele
 *
 *  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.
 *
 */
definition(
    name: "GroveStreams",
    namespace: "JasonBSteele",
    author: "Jason Steele",
    description: "Log to GroveStreams",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
    iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")



preferences {
    section("Log devices...") {
        input "temperatures", "capability.temperatureMeasurement", title: "Temperatures", required:false, multiple: true
        input "contacts", "capability.contactSensor", title: "Doors open/close", required: false, multiple: true
        input "accelerations", "capability.accelerationSensor", title: "Accelerations", required: false, multiple: true
        input "motions", "capability.motionSensor", title: "Motions", required: false, multiple: true
        input "presence", "capability.presenceSensor", title: "Presence", required: false, multiple: true
        input "switches", "capability.switch", title: "Switches", required: false, multiple: true
    }

    section ("GroveStreams Feed PUT API key...") {
        input "channelKey", "text", title: "API key"
    }
}

def installed() {
    initialize()
}

def updated() {
    unsubscribe()
    initialize()
}

def initialize() {
    subscribe(temperatures, "temperature", handleTemperatureEvent)
    subscribe(contacts, "contact", handleContactEvent)
    subscribe(accelerations, "acceleration", handleAccelerationEvent)
    subscribe(motions, "motion", handleMotionEvent)
    subscribe(presence, "presence", handlePresenceEvent)
    subscribe(switches, "switch", handleSwitchEvent)

}

def handleTemperatureEvent(evt) {
    sendValue(evt) { it.toString() }
}

def handleContactEvent(evt) {
    sendValue(evt) { it == "open" ? "true" : "false" }
}

def handleAccelerationEvent(evt) {
    sendValue(evt) { it == "active" ? "true" : "false" }
}

def handleMotionEvent(evt) {
    sendValue(evt) { it == "active" ? "true" : "false" }
}

def handlePresenceEvent(evt) {
    sendValue(evt) { it == "present" ? "true" : "false" }
}

def handleSwitchEvent(evt) {
    sendValue(evt) { it == "on" ? "true" : "false" }
}


private sendValue(evt, Closure convert) {
    def compId = URLEncoder.encode(evt.displayName.trim())
    def streamId = evt.name
    def value = convert(evt.value)
    
    log.debug "Logging to GroveStreams ${compId}, ${streamId} = ${value}"

	def url = "https://grovestreams.com/api/feed?api_key=${channelKey}&compId=${compId}&${streamId}=${value}"
    
    def putParams = [
        uri: url,
        body: []
    ]
    
    httpPut(putParams) { 
        response -> 
        if (response.status != 200 ) {
            log.debug "GroveStreams logging failed, status = ${response.status}"
        }
    }
 
}
11 Likes

Is it free? I looked at the pricing, and I couldn’t tell. Is each device a stream or ST is one stream in itself?

Do you have any screenshots of what the graphs look like?

Super cool, thanks for doing this! I was looking for something to log my 2 thermostats. Based on how you’ve described this working, I assume that I can add other capabilities like Power Meter and Energy Meter so I can log those too?

It’s free for personal “normal usage” use.

Each devices “capability value” (not sure what the proper term is) is a stream. So the Open/Closed sensor has a “contact” stream and a “temperature” stream. The sensor itself is a “Component” and the streams are created under it. My dashboard:

Yes, although you will probably have to add a few lines of code to include those capabilities for selection and handle their events - hopefully the code is simple enough to work out how to add them.

Looking at the capabilities at https://graph.api.smartthings.com/ide/doc/capabilities you will need to add a “capability.energyMeter” to the preferences and subscribe to the “energy” event.

HTH

Subscription Pricing

1 Like

Yes indeed, thanks Jason. I created my account and set up the smartapp to stream temps for now while I mod your code for the meters. How long should it take for data to start streaming? I assume only during temp changes.

@JasonBSteele Works like a charm! Setting up the account was easy, and modding your app for the energy meter was easy as well. Here’s what I added:

    input "powers", "capability.powerMeter", title: "Power Meters", required:false, multiple: true
    input "energys", "capability.energyMeter", title: "Energy Meters", required:false, multiple: true 

subscribe(powers, "power", handlePowerEvent)
subscribe(energys, "energy", handleEnergyEvent)

def handlePowerEvent(evt) {
sendValue(evt) { it.toString() }
}

def handleEnergyEvent(evt) {
sendValue(evt) { it.toString() }
}

That’s it, and here’s the result (be it only 2 points, but cool none the less):

2 Likes

This is awesome! Thanks for your contribution to this community!

YAY! Monkey see, monkey do!

Thanks - I’ve been wanting to see KW on an automatic chart for a month now

Many thanks, signing up.

Yes - that’s right. You may have to wait some time for a new value to be sent from ST, and it is only then that the Stream will get created in GroveStreams.

Another thing to be aware of is that the graph will stop at the last value sent, so if your temperature is a steady 20 for 5 hours, the graph will not show a line for the last 5 hours. However as soon as the temperature changes a line will be drawn to the new temperature.

I’ve raised this with GroveStreams Line graphs with infrequent data points | GroveStreams | Page 1 so add your voice if it bothers you too.

Really great that the energy and power graphing worked out for you John. Now that you have them working you may want to create a dashboard. These can be shared as a web page straight to the dashboard (without having to be setup as a user and going through Observation Studio). You will be prompted for an API key for sharing it, which you can get from the same place as the Feed API key.

Very cool; but I’ve noticed that GroveStreams limits you to one event every 10 seconds, with one per hour exception…

Feed PUT call limit has been exceeded for address xxx.yyy.kkk.www. One call every 10 seconds is allowed. 

You’ll find those in your system messages.
I’ve changed the code to rely on a queue and scheduled “worker” which processes items in the queue every minute; to avoid error in the times handled by GroveStreams, I pass the time as part of the call:

def updated() {
    unsubscribe()
    unschedule()
    initialize()
}

def initialize() {
    subscribe(temperatures, "temperature", handleTemperatureEvent)
    subscribe(humidities, "humidity", handleHumidityEvent)
    subscribe(contacts, "contact", handleContactEvent)
    subscribe(accelerations, "acceleration", handleAccelerationEvent)
    subscribe(motions, "motion", handleMotionEvent)
    subscribe(presence, "presence", handlePresenceEvent)
    subscribe(switches, "switch", handleSwitchEvent)
    subscribe(batteries, "battery", handleBatteryEvent)
    state.queue = []
    schedule("0/1 * * * * ?", processQueue)
}

def handleTemperatureEvent(evt) {
    queueValue(evt) { it.toString() }
}

def handleHumidityEvent(evt) {
    queueValue(evt) { it.toString() }
}

def handleBatteryEvent(evt) {
    queueValue(evt) { it.toString() }
}

def handleContactEvent(evt) {
    queueValue(evt) { it == "open" ? "true" : "false" }
}

def handleAccelerationEvent(evt) {
    queueValue(evt) { it == "active" ? "true" : "false" }
}

def handleMotionEvent(evt) {
    queueValue(evt) { it == "active" ? "true" : "false" }
}

def handlePresenceEvent(evt) {
    queueValue(evt) { it == "present" ? "true" : "false" }
}

def handleSwitchEvent(evt) {
    queueValue(evt) { it == "on" ? "true" : "false" }
}

private queueValue(evt, Closure convert) {
    def compId = URLEncoder.encode(evt.displayName.trim())
    def streamId = evt.name
    def value = convert(evt.value)

    log.debug "Logging to queue ${compId}, ${streamId} = ${value}"

    def url = "https://grovestreams.com/api/feed?api_key=${channelKey}&compId=${compId}&${streamId}=${value}&time=${now()}"

    def putParams = [
        uri: url,
        body: []
    ]

    state.queue << putParams
}

def processQueue() {
    log.debug "processQueue"
    if (state.queue != []) {
        def putParams = state.queue.head()
        if (putParams) {
            state.queue = state.queue.drop(1)
            log.debug "Event: ${putParams}"
            try {
                httpPut(putParams) { 
                    response -> 
                    if (response.status != 200 ) {
                        log.debug "GroveStreams logging failed, status = ${response.status}"
                    }
                }
            } catch(e) {
                def errorInfo = "Error sending value: ${e}"
                log.error errorInfo
            }
        }
    }
}
1 Like

Brilliant! Thanks for sharing this

This is something which applies to several ST devices/events I would like to graph; probably something many of you are interested in as well: http://forum.grovestreams.com/topic/152/is-there-a-way-to-create-a-binary-graph/

So does that mean that the more devices you’re streaming data from, the more likely to run into this, or that just one device can only send data every 10 seconds? Based on your statement, it looks like it’s IP addressed limited, so the more devices the more likely.

I believe the more devices the more likely, yes.

https://www.grovestreams.com/developers/limits.html

I noticed they say:

GroveStreams allows for batching many device calls into a single call to work around this limit

…so, I’ll change the code later to make the “worker” event completely empty the queue every time.

Much better; this will process the whole queue in one shot batching multiple events. You can make the scheduled processing less frequent, like a few minutes. It will also reprocess the queue if an error occurs.

private queueValue(evt, Closure convert) {
    def jsonPayload = [compId: evt.displayName, streamId: evt.name, data: convert(evt.value), time: now()]
    log.debug "Appending to queue ${jsonPayload}"

    state.queue << jsonPayload
}

def processQueue() {
    def url = "https://grovestreams.com/api/feed?api_key=${channelKey}"
    log.debug "processQueue"
    if (state.queue != []) {
        log.debug "Events: ${state.queue}"
  
        try {
            httpPutJson([uri: url, body: state.queue ]) { 
                response -> 
                if (response.status != 200 ) {
                    log.debug "GroveStreams logging failed, status = ${response.status}"
                } else {
                    log.debug "GroveStreams accepted event(s)"
                    state.queue = []
                }
            }
        } catch(e) {
            def errorInfo = "Error sending value: ${e}"
            log.error errorInfo
        }
    }
}

This is great @minollo - it’s really helping me get a better understanding of Groovy as well!

Also the discussion on stepped graphs is hopeful. I wrestled with using GroveStreams derived Interval Streams to try to simulate this but gave up as it was getting messy and not giving me enough in return.