How do I parse battery percentage from a HTTP GET response?

Hi all! New SmartThings convert here, looking for a little help on setting up a custom device:

I have a virtual switch setup to control my arduino blinds through a GET link, and it works great. But my question is how can I tweak my virtual driver code to show tiles with the battery percentage and voltage values the blinds return? This would be super helpful to keep an eye on my solar blinds batteries!

Driver code:

/*
* Http GET Switch
*
* Calls URIs with HTTP GET for switch on or off
*
*/
metadata {
definition(name: "Http GET Switch", namespace: "community", author: "Community", importUrl: "https://raw.githubusercontent.com/hubitat/HubitatPublic/master/examples/drivers/httpGetSwitch.groovy") {
capability "Actuator"
capability "WindowShade"
capability "Switch"
capability "Sensor"
}
}

preferences {
    section("URIs") {
        input "onURI", "text", title: "On URI", required: false
        input "offURI", "text", title: "Off URI", required: false
        input "openURI", "text", title: "Open URI", required: false
        input "closeURI", "text", title: "Close URI", required: false
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}

def logsOff() {
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable", [value: "false", type: "bool"])
}

def updated() {
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    if (logEnable) runIn(1800, logsOff)
}

def parse(String description) {
    if (logEnable) log.debug(description)
}

def on() {
    if (logEnable) log.debug "Sending on GET request to [${settings.onURI}]"

    try {
        httpGet(settings.onURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "on", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to on failed: ${e.message}"
    }
}

def off() {
    if (logEnable) log.debug "Sending off GET request to [${settings.offURI}]"

    try {
        httpGet(settings.offURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "off", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to off failed: ${e.message}"
    }
}

def open() {
    if (logEnable) log.debug "Sending open GET request to [${settings.openURI}]"

    try {
        httpGet(settings.openURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "on", isStateChange: true)
                sendEvent(name: "windowShade", value: "open", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to open failed: ${e.message}"
    }
}

def close() {
    if (logEnable) log.debug "Sending close GET request to [${settings.closeURI}]"

    try {
        httpGet(settings.closeURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "off", isStateChange: true)
                sendEvent(name: "windowShade", value: "closed", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to close failed: ${e.message}"
    }
}

Example response from HTTP GET as seen in browser:

Blind ID: 2
Group: 1
Level: 0
Battery: 97
Voltage: 4.19
Charging: N
Success.
1 Like

If it were me I would return the result in JSON so that it's easier to work with. If you don't do that then it's fine but you are going to be doing some manual parsing and string manipulation to find your battery and voltage levels. Once you have your battery and voltage levels parsed out of the return result you simply send an event.

In the metadata of the driver you need to add the appropriate capabilities. The list is here:
https://docs.hubitat.com/index.php?title=Driver_Capability_List

So, for battery for example, you would add capability "Battery" after the capability "Sensor" line. Doing this will automatically add an attribute called "battery" to store the battery level and will make it available in a tile.

For the driver to populate the battery level to the attribute you send the event like this:

sendEvent(name: "battery", value: yourLevelVar, unit: "%")

2 Likes

Thanks for the reply,
Unfortunately JSON isn't quite that easy on my end, as it would involve deconstructing multiple window blind installations around the house, reworking their arduino code, reflashing them, and putting them back up...

I have the metadata tweaked and the driver is ready for the values, I just need to figure out how to parse a value: batteryVar and value: voltageVar out of this:

Blind ID: 1
Group: Null
Level: 0
Battery: 97
Voltage: 4.19
Charger: N
Success.

1 Like

Create a BufferedReader out of the String. Read the lines and discard until you get to battery and voltage. Split those strings on colon and then trim.

1 Like

Yes that sounds like what I need, but the reason I'm here seeking the help of others is because I do not know how to code that into this driver.

1 Like

You can split it a few times and conver to a map. Something like.

def responseMap = [:]
resp.data.split("\r").each {
    responseMap[it.split(":")[0]] = it.split(":")[1]
}

This code is totally untested and there are ways to do it better. In the end you split and the newlines, and then for each line split the : and add it to the map. You may want to add logic to things such as lines without a : in it or other things to avoid errors.

This should hopefully give you an idea to work with.

2 Likes

Those new lines have the potential to be "\r", "\n", "
", or any commination of those.

If you want help coding Google stackexchange.com solutions.

2 Likes

@gavincampbell Thank you very much, for starting me down the right path! I'm playing around with this now while watching the splits through log output comments.

@codahq Yes, I know how to google things. Thanks for the warm welcome to the hubitat community on my first topic posted here, and I'm sorry you're having a bad day...

2 Likes

Maybe I should have said this first but welcome to the community so that you could have more hints that I was being helpful.

Now that the welcome is out of the way... Just exactly where did I go wrong? I gave you the solution. I told you exactly what to do. I laid it out. What possible motivation will anybody have to help you in the future if that is how you reciprocate the help? You are lucky @gavincampbell graciously spelled out something so easily found. And you are going to turn on me because I wouldn't code out your entire solution? If you are asking questions about an arduino based anything in the developers section of the community it should be pretty safe to assume that you have some idea of what you are doing.

I'm not having a bad day but it's not as good as it was now that you have somehow managed to take offense to good will, free help and courtesy. You can't assume I'm having a bad day because I didn't do something you want me to do.

There is no reason to answer questions about simple Java here. Simple Java is covered super well on other sites. This space isn't for simple Java. Most of the questions you will find here are a developer asking other developers specific questions about the nuances of groovy or about the HE platform.

And it's pretty clear you don't know how to google... if you put absolutely no effort into your solution, why should I?

https://lmgtfy.com/?q=site%3Astackoverflow.com+read+a+multi-line+string+line+by+line+java

Top result. Tens of ways of doing this on the very first record returned.

2 Likes

I did not find this link a reason for having a bad day, it’s a great tip for learning! @codahq should be understood/interpreted in best possible way, he is a huge help and a fantastic contributor to the community.
Assumtions are dangerous, one usually miss, just ask your wife! Ever heard the saying, «you do not understand me?» Hahaha
Have a good day!
RogerThat

1 Like

Again I said I was sorry, and I truly am,
I arrived here at the "Coding Questions" section, laid out what little code I had so far, and asked if someone could help me with it. So your response of if you want coding help google stackexchange came across kinda dismissive.

I did try to search for 'BufferedReader' here on the hubitat forum, but there was zero results for the keyword. And I did browse BufferedReader posts at stackexchange, but all I found was coding examples for how to implement it in Java, not Groovy.

Maybe I should have originally asked for help with coding syntax and not theory? Maybe I should have posted in a lower level section? General Support? Maybe we should just get someone to delete this whole thread? I mean I never got it to work anyway, just throws null errors, and nothing here is going to help anyone in the future... But I am sorry, sorry for wasting your time and sorry for the confusion.

2 Likes

Nah, no need to delete the thread. I think it's clear neither of us knew where the other was coming from. It's deescalated. Now that you have context (I'm a battle hardened developer and I thought you were the same or close) and I have more context let's keep going.

Groovy is built on Java so you can write Java or Groovy really. You probably wouldn't get help on a Buffered Reader here. Like i mentioned, those types of questions are typically not required by most of us so they aren't asked. I was legitimately offering a better alternative when I mentioned stack.

Paste what you have so far and let's look again.

And sorry I cornered you. I shouldn't have assumed either.

3 Likes

Thank you @codahq for your willingness to continue, but at the end of the day its just battery percentage on some blinds lol, my wife already thinks I'm stupid for spending half my day on this so far!

But here's what I've Frankenstein-ed together from other's drivers, example code, and guess work:

/*
 * Http GET Switch
 *
 * Calls URIs with HTTP GET for switch on or off
 * 
 */
metadata {
    definition(name: "HTTP GET Switch Blinds Custom", namespace: "community", author: "Community", importUrl: "https://raw.githubusercontent.com/hubitat/HubitatPublic/master/examples/drivers/httpGetSwitch.groovy") {
        capability "Actuator"
        capability "WindowShade"
        capability "Switch"
        capability "Sensor"
        capability "Battery"
		capability "Voltage Measurement"
    }
}

preferences {
    section("URIs") {
        input "onURI", "text", title: "On URI", required: false
        input "offURI", "text", title: "Off URI", required: false
        input "openURI", "text", title: "Open URI", required: false
        input "closeURI", "text", title: "Close URI", required: false
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}

def logsOff() {
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable", [value: "false", type: "bool"])
}

def updated() {
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    if (logEnable) runIn(1800, logsOff)
}

def parse(String description) {
	log.debug "Parsing '${resp.data}'"
    
    def map = [:]
	def retResult = []
	def descMap = parseDescriptionAsMap(description)
    def msg = parseLanMessage(description)    
    log.debug "data ${resp.data}"
    def body = new String(descMap["body"].decodeBase64())
    log.debug "Body: ${body}"
    
    if(msg.body != null && msg.body.contains("Success."))
        {
        	//log.debug msg.body
            String[] parseBody = msg.body.split( '<html>' )
        	String[] lines = parseBody[1].split( '<br>' )        	
            String[] parseID = lines[0].split( ': ' )
            //log.debug "ID: ${parseID[1]}"
            String[] parseGroup = lines[1].split( ': ' )
            //log.debug "Group: ${parseGroup[1]}"
            String[] parseLevel = lines[2].split( ': ' )
            //log.debug "Level: ${parseLevel[1]}"
            String[] parseBattery = lines[3].split( ': ' )
            //log.debug "Battery: ${parseBattery[1]}"
            String[] parseVoltage = lines[4].split( ': ' )
            //log.debug "Voltage: ${parseVoltage[1]}"
            String[] parseCharger = lines[5].split( ': ' )
            //log.debug "Charger: ${parseCharger[1]}"
               
            if(childID[1] == parseID[1]) {
            	if ( parseBattery != null){
            		sendEvent(name: "battery", value: parseBattery[1])
                }
                else{
                	log.warn "Warning: Returned Battery value is Null."
                }
                if ( parseVoltage != null){
                	sendEvent(name: "voltage", value: parseVoltage[1])
                }
                else{
                	log.warn "Warning: Returned Voltage value is Null."
                }
                if ( parseCharger != null ){
                	if(parseCharger[1] == "C"){
                    	//log.debug "Charging"
                		sendEvent(name: "status", value: "charging")
                    }
                    else if(parseCharger[1] == "O"){
                    	//log.debug "FullyCharged"
                		sendEvent(name: "status", value: "charged")
                    }
                    else if(parseCharger[1] == "N"){
                    	//log.debug "Not Charging"
                		sendEvent(name: "status", value: "notCharging")
                    }
                    else if(parseCharger[1] == "E"){
                    	//log.debug "Error"
                		sendEvent(name: "status", value: "error")
                    }
                }
                else{
                	log.warn "Warning: Returned Status value is Null."
                }
            }
        }    
}



def on() {
    if (logEnable) log.debug "Sending on GET request to [${settings.onURI}]"

    try {
        httpGet(settings.onURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "on", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to on failed: ${e.message}"
    }
}

def off() {
    if (logEnable) log.debug "Sending off GET request to [${settings.offURI}]"

    try {
        httpGet(settings.offURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "off", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to off failed: ${e.message}"
    }
}

def open() {
    if (logEnable) log.debug "Sending open GET request to [${settings.openURI}]"

    try {
        httpGet(settings.openURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "on", isStateChange: true)
                sendEvent(name: "windowShade", value: "open", isStateChange: true)
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
        }
    } catch (Exception e) {
        log.warn "Call to open failed: ${e.message}"
    }
}

def close() {
    if (logEnable) log.debug "Sending close GET request to [${settings.closeURI}]"

    try {
        httpGet(settings.closeURI) { resp ->
            if (resp.success) {
                sendEvent(name: "switch", value: "off", isStateChange: true)
                sendEvent(name: "windowShade", value: "closed", isStateChange: true)
                
            }
            if (logEnable)
                if (resp.data) log.debug "${resp.data}"
            
        }
    } catch (Exception e) {
        log.warn "Call to close failed: ${e.message}"
    }
    
}



def parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
		map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
	}
}

What also makes no sense to me is that I can log the response when inside the GET command, but not further down the code, like in this example I was playing with:

metadata {
definition (
name: "Test",
namespace: "Test",
author: "Test" )
{
capability "Refresh"
}
}
def refresh() { ( sendSyncCmd() ) }

private sendSyncCmd(){
def host = "http://192.168.1.157"
def command = "/Blind/ID=2/level=0"  
    httpGet([uri: "${host}${command}",
    contentType: "text/html",
    textParser: true])
    		{ response ->
		log.info "data: ${response.getData()}"	//shows the full response in the log
		def data = response.getData()
		log.debug data	// shows in log as "java.io.StringReader@9784b7"
		}
	}

log.debug response.data // log says java.lang.NullPointerException: Cannot get property 'data' on null object
   
}

Alright, buckle up. There is a programming concept called "scope." Take the following block of code:

private someMethod() {
  def host = "10.10.10.1"
  someMethod () { someVar ->
    def result = "someResult"
  }
  def answer = "no"
}

In this small example, the following variables are declared: host, someVar, result and answer. However, they are not universally available anywhere in your code. When talking about scope in regards to variable declaration it never travels up. So... from inside the "someMethod()" call host would be available, someVar would also and obviously result since it was declared there. However, answer wouldn't. It comes after so it doesn't exist yet.

Another thing to note is that since "result" is declared inside an expression its scope is limited to that expression. "result" isn't available anymore by the time that "answer" is declared.

So, in your example, response is out of scope. It's now null because it was declared in the scope of the httpGet call. That's why it's null.

Well heck, today I've learned a thing lol
Soooo.... do I need to define the httpget as a function and call it from within the parsing part?

From your comments I can also tell you aren't sure what to do with response.getData() or the variable "data" after you store the contents of response.getData() into it because of you comment about it showing as "java.io.StringReader".

When you put something in a log.debug or a log.[anylevel] it is converting whatever that variable is (and there are an infinite number of types that a variable can be) into a String variable (which is a human readable sentence type variable) so that it can be shown in the log. What if the variable doesn't know how to turn into a String? Then you will see some nonsense like you are seeing with a class (that's what java.io.StringReader is) and then a memory address (which is what 9784b7 is).

The good news is that there is help for this in a couple of ways. When you aren't in Hubitat and there aren't other people are to help you, you can source the answer for this on your own. Java is well documented. If you go to the Java API you can find out a lot of things about the class "java.io.StringReader" and what it can do.

https://docs.oracle.com/javase/8/docs/api/java/io/StringReader.html

In this case we could do what we wanted from the StringReader just using Java and the API and we'd be perfectly fine. However, since this is Hubitat there is a better way.

We know from the documentation that the result (the type of "response") is a HttpResponseDecorator. Probably one of these.

We also know (from past experience) that depending on how we set the contentType and requestContentType the data coming back from getData() won't get parsed correctly.

Try these two things:

  • Instead of response.getData() please log response.data as well inside your httpGet method call. Sometimes the raw data is there.
  • If the above suggestion doesn't work let's try setting more content type headers. Try using "text/plain" instead of "text/html".
1 Like

httpGet is defined already. What you are trying to do is probably not a good place to start programming but it's what we have to do so we are rolling with it. There would have been some value in some easier examples leading up to this. Groovy has weird things like things called closures and neat shortcut syntax that can help programmers who are already familiar with coding go faster but also introduce a bit of a learning curve for beginners.

Try this:

metadata {
  definition(
    name: "Test",
    namespace: "Test",
    author: "Test")
    {
      capability "Refresh"
    }
}

def refresh() { (sendSyncCmd()) }

private sendSyncCmd() {
  def host = "http://192.168.1.157"
  def command = "/Blind/ID=2/level=0"

  //try 1
  httpGet([uri: "${host}${command}",
    contentType: "text/html",
    textParser: true])
    { response ->
      log.info "data: ${response.getData()}"  //shows the full response in the log
      def data = response.getData()
      log.debug "data is ${data}"  // shows in log as "java.io.StringReader@9784b7"
      log.debug "response.data is ${response.data}"

    }

  //try 2
  httpGet([uri: "${host}${command}",
    contentType: "text/plain",
    textParser: true])
    { response ->
      log.info "data: ${response.getData()}"  //shows the full response in the log
      def data = response.getData()
      log.debug "data is ${data}"  // shows in log as "java.io.StringReader@9784b7"
      log.debug "response.data is ${response.data}"

    }

}

Hrmmmm, they both throw the same exception on the log.debug "data is....

groovy.lang.StringWriterIOException: java.io.IOException: Stream closed on line 24 (refresh)

The final result may look like this:

metadata {
  definition(
    name: "Test",
    namespace: "Test",
    author: "Test")
    {
      capability "Refresh"
    }
}

def refresh() { (sendSyncCmd()) }

private sendSyncCmd() {
  def host = "http://192.168.1.157"
  def command = "/Blind/ID=2/level=0"

  def responseMap = [:]
  httpGet([uri: "${host}${command}",
    contentType: "text/plain",
    textParser: true])
    { response ->
      log.info "data: ${response.getData()}"  //shows the full response in the log

      //if response.data didn't print the entire response then change this to getData() or something that did.
      //
      response.data.split("\r").each {
        def parts = it.split(":")
        responseMap[parts[0].trim()] = parts[1].trim()
      }


    }

  //assuming everything went right up to this point you should be able to do...
  //if it didn't go correctly we probably know that the new line character is something other than "\r" and we'll try "\n" or "\r\n"
  log.debug "value of Battery is ${responseMap.Battery}"
  log.debug "value of Voltage is ${responseMap.Voltage}"
  
}

Is "http://192.168.1.157/Blind/ID=2/level=0" still working from a regular browser?