Can Hubitat talk to a misbehaving HTTP server?

Wondering if @chuck.schwer can help since he's helped me with an HTTP enhancement to Hubitat already!

I have a misbehaving device. It has an HTTP server, but it doesn't follow the rules. It returns a body, but no http headers. That means Hubitat, being a good device, returns:

org.apache.http.client.ClientProtocolException

The server, when I make a get, literally returns:

Is there anyway to get Hubitat to be a little more lenient and allow a connection to a device that doesn't like to send headers?

As a note, I'm trying to do this from an app.

For anyone who cares, I am trying to build an integration for the Kohler DTV+ Shower System.

Presumably you are using an HTTP Get because you want to GET something back from the server. If the server isn't replying, maybe it's expecting a POST message instead.

The server is replying. The server requires a GET and returns data. It returns a body, but as I said, no headers which violates the HTTP protocol and Hubitat throws an error.

There are a couple way that might work,, Specify the RAW_LAN Procotol or LAN_TYPE_RAW as an option. It depends if you are going to build the request yourself of if you want to pass the various values as parameters and have the platform build it for you. If the request is standard you can do the second, if the request is also non standard you should do the first.

https://docs.hubitat.com/index.php?title=HubAction_Object

First Option:
You'll need to build the entire message yourself as a String when sending if you use this signature:
HubAction(String request, Protocol protocol)

Here are the options for Protocol, you want RAW_LAN:
https://docs.hubitat.com/index.php?title=Protocol_Object

Second Option:

You can use this signature:
HubAction(Map params, String dni, Map options)
and pass [type: HubAction.LAN_TYPE_RAW] for the options

2 Likes

But isn't that asynchronous? If so how would I use this to populate a dynamicPage for my app? My goal was to query their api to get the list of detected devices...

Also, even though the async means it won't work for me I tried:

sendHubCommand(new hubitat.device.HubAction("GET /values.cgi HTTP/1.1\r\n\r\n", hubitat.device.Protocol.RAW_LAN, [callback: test, destinationAddress: "192.168.86.52", destinationPort: 80]))

My callback method never seems to get called. If I connect to that ip/port directly and send that same get command I get data back. Any thoughts? Also is there anyway to force a HubAction to be synchronous so I can use this in my app?

Yes

You didn't mention that originally, but you could store the result in state and populate the page that way.

  1. destinationPort is only used for "lan discovery" messages, you can specify port in the destinationAddress option.
  2. Try using wireshark to see what the difference is.

No

1 Like

So this kind of worked, but the callback is called with a string and there is junk before my json:

parse called index:00, mac:00146F0E58CD, ip:c0a85634, port:50, type:LAN_TYPE_RAW, payload:{"white":1,"effect":1,"fade":1,"rain_brightness":100,"v1_threeport_flow":false,"one_type":"outlet_23","two_type":"outlet_13","three_type":"outlet_13","four_type":"outlet_8","five_type":"outlet_13","six_type":"outlet_2","purge_0":true,"purge_1":true,"purge_2":true,"purge_3":false,"purge_4"..................

Other than string parsing is there a way to get rid of the stuff in bold? Thanks again, you've been super helpful!

ha! It gives you a Map back (not a String) with information about the response. You have not posted your code, so I'll just have to make up my own.

void myCallbackMethod(response) {
    log.debug "Response payload: ${response.payload}"
}

So I tried that, and I got the following:

groovy.lang.MissingPropertyException: No such property: payload for class: org.codehaus.groovy.runtime.GStringImpl on line 44 (deviceStatus)

My code is as follows:

def updateDevices()
{
    sendHubCommand(new hubitat.device.HubAction("GET /values.cgi HTTP/1.1\r\n\r\n", hubitat.device.Protocol.RAW_LAN, [destinationAddress: "${parent.dtvIP}:80", callback: deviceStatus]))
}

void deviceStatus(hubResponse)
{
    log.debug "deviceStatus called ${hubResponse}"
    log.debug "deviceStatus data ${hubResponse.payload}"  
}

Bah! Yeah I totally missed where the code pulls a String out of the map, and does not pass the entire map to the callback, my bad:

void deviceStatus(hubResponse)
{
    log.debug "deviceStatus called ${hubResponse}"
    Map hrMap = parseLanMessage(hubResponse)
    log.debug "deviceStatus data ${hrMap.payload}"  
    def payloadMap = new groovy.json.JsonSlurper().parseText(hrMap.payload)
}

This is really weird. I used your code and it printed:

deviceStatus data {"valve1_Currentstatus":"Off"

That's it, the JsonSlurper obviously said that's invalid. Just to test I did:

def data = parseJson(hubResponse.split("payload:")[1])

That's obviously terrible since it's string parsing, but it works and properly gives me the JSON.
Is this a bug?

The full hubResponse (as a string) is -

index:00, mac:00146F0E58CD, ip:c0a85634, port:50, type:LAN_TYPE_RAW, payload:{"valve1_Currentstatus":"Off","degree_symbol":"°F","valve1Setpoint":"100","valve1outlet1":false,"valve1outlet2":false,"valve1outlet3":false,"valve1outlet4":false,"valve1outlet5":false,"valve1outlet6":true,"valve1_massage":0,"valve2_Currentstatus":"","valve2Setpoint":"100","valve2outlet1":false,"valve2outlet2":false,"valve2outlet3":false,"valve2outlet4":false,"valve2outlet5":false,"valve2outlet6":false,"valve2_massage":0,"steamStatus":"","steamTempStatus":"110","steamTimeStatus":"-9:00","steamTimeMinutes":"-9","musicStatus":"Paused","volStatus":"50%","muteStatus":"Off","LZ1Status":"Off","LZ2Status":"On","LZ3Status":"On","RainpanelStatus":"Off","light_attach":"0","light_remove":"0","light_turnoff1":"0","light_turnoff2":"0","light_turnoff3":"0","spa_on":false,"ui_shower_on":false,"ui_steam_running":false,"devices_running":false}

Could be or maybe it's just me tossing out coding suggestions without testing any of them. I'm not sure anyone else has used raw messages like this so I'll take a closer look and see what is happening and should happen.

Hah, I can understand that. It'd be good to know if I screwed up or if it's a bug, but in the meantime my ugly string parsing is working correctly to grab the JSON payload. I can't yet control my shower, but I can at least detect when the shower is running... baby steps!

1 Like

@chuck.schwer did you ever get a chance to look at this? I've used sendHubCommand in a few integrations I've built now and I keep getting where the callback receives a string instead of an object with a payload property. I've worked around it doing def data = parseJson(hubResponse.split("payload:")[1]) which works but that's clearly a hack.

I just looked again. That message coming back should be in the same format that would be going to parse, so it will be a string. The problem that jumps out at me is that the payload is coming along as plain text instead of a base64 encoded string. So we can fix that but it would break your implementation.

So my proposal is that we can fix the String coming back to the callback method so that it has the payload encoded as base64, and then I think the most straightforward way for you to process this is to use parseLanMessage and the built in json parser like this:

def data = parseJson(parseLanMessage(hubResponse).payload)
1 Like

Break away! Easy enough to fix my code. I think your proposal makes sense. Think this will be in 2.1.8 or later? Just wondering when I should have to expect to fix my code.

Yeah, should be in 2.1.8

1 Like