New Dev: Request for httpPost Groovy Examples

Sorry, I am a little lost. Is this particle code to run a simple webserver on the photon? That's cool!

But I already have my particle code, I am just looking for a way to create a custom groovy app so I can control my Particle device by pressing buttons on the hubitat server that will hit the particle cloud functions and pass an argument:

For example:

https://api.particle.io/v1/devices/<DEVICEID>/control?access_token=<ACCESS_TOKEN>&arg={"color":"red","Brightness":5}

The reason for this is somewhat simple: My controller has some simple parameters that I set like once a year for the holidays, and I need a simple GUI for it. Might as well build the app in Hubitat so it's all in one place.

Ah. Sorry, I thought you were looking for the Particle Photon side and then were just going to shoot simple httpGet commands at it via an app/driver or the Rule Machine to tell it to do what you need it to.

Yes, it is a simple little webserver. I run them on pretty much all my Particle Photons so I can tell what the things are doing (since it is tough to keep a serial port connected) behind the scenes.

1 Like

Is there a reason you don't use the cloud functions? There's a limitation of about 600Bytes in those strings, but it's not terrible.

I was going to point you towards my CoCoHue apps and drivers as an example of how to do HTTP GETs and PUTs with Groovy in Hubitat, but in most cases, particularly the PUTs, there are a couple pieces that get put together from the parent app and child devices, so it might not be an easy example to start out with.

So, instead I'll walk through a specific, highly commented and slightly modified near-example from the RGBW bulb driver:

   // This really gets passed into the function this code lives in, but since it's not
  // part of this example I'll just declare it here:
  Map cmd = ["on": true]
   // This is a method on the parent app that returns a Map that will look something
  // like: [username: "abc123", fullHost: "http://1.2.3.4:80"]:
   Map<String,String> data = parent.getBridgeData()
   Map params = [
      uri: data.fullHost,
      path: "/api/${data.username}/lights/${getHueDeviceNumber()}/state",
      contentType: "application/json",
      body: cmd,    // Groovy/Hubitat is nice and converts this to JSON for you
      timeout: 15
      ]
   asynchttpPut("parseSendCommandResponse", params, cmd)

If you care to parse the response or at least know when it happens, then you'll need a callback method, which is parseSendCommandResponse() per my call above:

void parseSendCommandResponse(resp, data) {
  // Do things here, like check if `resp` indicates an error state or went OK;
  // in my case, I generate events if the response from the Hue Bridge is HTTP 200 OK,
 // and I do that based on the data Map that gets passed in as `cmd` from my asynchttpPut
// call (it's the command sent to the Bridge), but data is optional and will only be what you
// provide, if anything. Your system's API should tell you what to expect back.
}

The callback method is necessary here because I'm using an async call, which means it will not hold things up while it waits for a response. Instead, it will send the HTTP PUT, then the app or driver will go to sleep until it wakes up for the callback (ideally because it heard back from the device, but it could also be after the specified timeout). Synchronous calls are a bit easier to write for but less advised if you don't need them.

So while this was really written with the Hue API in mind (yours looks somewhat similar in that it's RESTful but has the JSON in the URL instead of the body; in that case, you may need to use Groovy's URL encoding methods--unless all the raw JSON doesn't make the URL invalid--and possibly stringify the Map into JSON yourself before that...I forget if that's one of the things that is automagically done for you or not) and a parent app that creates "child" devices, hopefully it still illustrates the point. But I don't know how helpful it will be. Hubitat's docs on the HTTP methods will probably be just as helpful. See this doc for those: Common Methods Object - Hubitat Documentation

There is also a community HTTP driver out there that creates a switch that, when turned on, can send these commands on its own with a bit of configuration on your part. It may be easier than writing everything on your own, but it may also be less desirable in that you'd have to specify your access token, IP, and whatnot in each driver (and do the same if either ever changes). You'll also have to think about your end goal; I know you said "app," but it's unlikely you'll want to have to go into a Hubitat app and press a button there each time to send a command to your devices. You'll probably want a Hubitat device (which may or may not be created/managed/communicated with via an app) so you can use the device in automations (apps, including rules/etc.) rather than needing to go into the admin UI to do anything with it. That could just be a terminology slip, but since they are also distinct concepts in the platform, I figured it was worth mentioning.

I should also mention that I know nothing about the other system you're using, but it sounds like others above do. So, I'm not saying any of what I said is (or isn't) a good idea as to how to integrate with that...just trying to directly answer the specific question about the part I do know. :smiley:

1 Like

As @bertabcd1234 mentioned, the HTTP Momentary driver might help you out here (both by it's functionality to test and it's code [Apache License])

2 Likes

Some good examples above. I also keep this post bookmarked for reference. It helps me remember.

Thank you Robert! This was an excellent description.

Yeah, the API I am using is pretty nice. It's essentially an arduino-esque microcontroller, but your code rides on top of a cloud-connected OS.

To interact with the device, you define cloud functions in the setup() of the sketch, which are registered with their cloud system at device startup. To invoke the function, it's essentially https://path-to-api//function; you can pass your access_token as an argument in the URL or in the POST data, along with a String argument.

In my case, I tend to use JSON for the string arguments, so I can create one function called "cmd" and pass it a more complex command that can be handled atomically.

However, for simple devices like a single relay, I will code up two functions, like /on and /off or /press?arg=3 (to turn on the relay for 3 sec). I use these for systems where it's critical that a local timer turns off the device (not as big an issue with Hubitat, but I was using ST to buzz our security gate, and the system failed to turn off the relay, which burned out our buzzer).

To receive data from the device, you can either have the device update cloud variables and then also read the latest updates with a URL, or you can define an event on the fly that can invoke a web service you set up in the platform.

So basically...

  1. Device generates event with argument (say, a bus route)
  2. Event invokes web service set up on the cloud platform (platform web service invokes nextbus API)
  3. Data returned by nextbus API is processed on the platform (JSON reduced to next bus arrival prediction in seconds) and the result is passed to an event that invokes a handler function on your device, so you can say, flash a red light when the bus is 60s away.

Of course, you can also call httpGets and such directly from the device, or have it listen for requests like any other web-connected device, but that's a lot more work, and then you have to deal with DNS, port mapping, security, etc. The cloud platform means you don't have to deal with any of it.

http://particle.io if you are interested.

For this app, I have a device that controls my holiday lights. I need to be able to send it commands to change colors, rotation speed, brightness, etc. Since this is part of the HA system, I figured HE is a good place to set up the GUI. And I can easily set up enumerated arguments for those buttons.

OK, running into trouble with the asynchttpGet()

I set this up as a Device actually (seemed to make more sense).

For testing, I made a simple call that flashes an LED for some number of milliseconds.

I am running into two problems...

  1. Variables I define at the top of the code are not accessible later in the code, (for example, apiURL and "debugging" are null and false if I access them later); is this due to the state nature of a device? This just means I will have to define them at each entry point, correct?

  2. The POST does not work. Code and error below...

My code:

import groovy.transform.Field
debugging = true
apiURL = "https://api.particle.io/v1/devices/"

metadata {

    definition(
        name: "Entry Controller",
        namespace: "ABCD",
        author:    "ArchStanton"
    ){
        command "FlashRed"
     }

    preferences {
        input "enableDebug", "bool", title: "Enable Debug Logging"
        input "deviceID", "String", title: "Particle Device ID"
        input "accessToken", "String", title: "Particle AccessToken"
    }
    
}


def FlashRed(){
    apiURL = "https://api.particle.io/v1/devices/"
    if (enableDebug) log.debug "Flash Red Pushed"
    def postParams = [
    uri: apiURL + "${deviceID}/cmd?access_token=${accessToken}",
        requestContentType: 'application/text',
        contentType: 'application/x-www-form-urlencoded',
         body : "arg={\"flashRed\":500}"
	]
    
    if(enableDebug) log.debug "Calling asyncHttpPost with parameters ${postParams}"
    asynchttpPost('nullCallback', postParams)
}

// Dummy callback, since for this we don't do anything with the response
def nullCallback(response, data){
    if (enableDebug) log.debug "nullCallback() invoked with response: ${response} and data: ${data}"
}+

And the Log:

[dev:111](http://192.168.1.148/logs#dev111)2020-12-16 11:27:30.453 am [debug](http://192.168.1.148/device/edit/111)nullCallback() invoked with response: hubitat.scheduling.AsyncResponse@1b5ea72 and data: null

[dev:111](http://192.168.1.148/logs#dev111)2020-12-16 11:27:30.390 am [debug](http://192.168.1.148/device/edit/111)Calling asyncHttpPost with parameters [uri:https://api.particle.io/v1/devices/XXX/cmd?access_token=XXX, requestContentType:application/text, contentType:application/x-www-form-urlencoded, body:arg={"flashRed":500}]

[dev:111](http://192.168.1.148/logs#dev111)2020-12-16 11:27:30.385 am [debug](http://192.168.1.148/device/edit/111)Flash Red Pushed

(I masked the tokens with XXX)

I tried to add dummy data to the callback, but that doesn't help either.

If I don't need the callback, can I just pass a null as the callback function?

The "top level" variable not working is a Groovy thing: it ultimately becomes Java-esque, so all top-level code gets shoved into a method behind the scenes (because that's how Java needs things to be), and like any variable declared inside a method, is not accessible from elsewhere. Groovy's @Field annotation solves this by "elevating" these variables to class fields. So, this should solve that prolem:

@Field String apiURL = "https://api.particle.io/v1/devices/"

(Yeah, I also specified the type...might as well if you know.) However, this value will be re-initialized to the above on each execution of the driver. You'll often see this instead:

@Field static String apiURL = "https://api.particle.io/v1/devices/"

...which only initializes the value when the driver code is saved/updated, with a new concern being that the value is shared (static) among all device instances that use this driver. In your case, this seems unlikely to be a problem (but something to keep in mind if you're using these to store data, meaning they may be modified by one instance of a driver--for all instances). I also like to make these final, but I have no idea from Hubitat's (lack of) documentation if that makes a difference in this case.

As for why the POST isn't working, I'm not sure. But I don't actually see an error in the logs you pasted: resp would indeed be an AsyncResponse object, and the (optional) data parameter was not provided, so it's null as expected. This post probably has more documentation on the AsyncResponse object than the actual docs and might be a good starting point for using it to figure out if something went wrong:

(I'm mostly familiar with the Hue Bridge here, which returns JSON, so resp.getJSON() is often what I use to see if the Bridge threw a fit or of things worked, but your API is probably different.)

I don't think the callback is happening after the data is retrieved, because the TS is much too short.

You should be able to tell by inspecting the contents of resp. The post above has methods/properties this object supports that should help you determine what is happening (note that some may be null in some cases, so be careful if/how you use them).

You mean the response passed to the callback?

The log says the reponse was this: hubitat.scheduling.AsyncResponse@1b5ea72

Is that some kind of object?

Yes, and you can use the properties or methods described in the post I linked to above to see more about it. (Its default toString() method isn't really informative, as you can see.) Still not saying it worked, but this would give you an idea of what happened if not. :slight_smile:

Thanks for walking me through this, I read that article previously, but clearly did not follow it all the way through. I'll play for a bit and see what I can find.

OK, hasError is true, and the status is 408, which is an HTTP timeout.

But it's timing out in 21ms. That doesn't seem right.

dev:1112020-12-16 12:48:32.647 pm debugResponse.hasError(): true

dev:1112020-12-16 12:48:32.643 pm debugResponse.getStatus(): 408

dev:1112020-12-16 12:48:32.640 pm debugnullCallback() invoked with response: hubitat.scheduling.AsyncResponse@15fff47 and data: [arg:{"flashRed":500}]

dev:111
2020-12-16 12:48:32.619 pm debugCalling asyncHttpPost with parameters [uri:https://api.particle.io/v1/devices/XXX/cmd?access_token=XXX, requestContentType:application/text, contentType:application/x-www-form-urlencoded, body:arg={"flashRed":500}]

You probably want a more readable log of the response.

    if (enableDebug){
 log.debug "nullCallback() response status: ${response.getStatus()} and data:${response.data}"
}

Although the response.data will likely be non-existent if the status is a failure, so that is usually better to put on a separate line for if there is a success (status # 200).
The data you had logged before corresponds to a map you could send in the original command. Example:
asynchttpPost('nullCallback', postParams, [Method: "Sent Something" ])

You put more logging as I was typing my response... :slight_smile:

I just dug a little more.

In that case, my encoding wasn't right.

Now I am getting bad request, after setting the encoding to json.

I suspect that I need another encoding type, but I am digging into the docs to find the acceptable types.

Huh. Evidently there is no other documentation that that community post?

Is there a list of encoding types? I tried 'applicaiton/text' and got an error, as well as 'application/custom'

dev:1112020-12-16 01:00:54.727 pm debugResponse.hasError(): No encoder found for request content type custom

dev:1112020-12-16 01:00:54.724 pm debugResponse.hasError(): true

dev:1112020-12-16 01:00:54.720 pm debugResponse.getStatus(): 408

dev:1112020-12-16 01:00:54.717 pm debugnullCallback() invoked with response: hubitat.scheduling.AsyncResponse@802146 and data: null

Should just be the standard HTTP content type that your API is expecting. I don't know it well, but the docs and your content make it look like application/json is what you should be using, maybe?