App and driver porting to Hubitat

I thought I'd jot down a few of the driver porting changes I've encountered. Please add additional ones you find to this list.

Replace physicalgraph with hubitat
Replace variables beginning with data with device.data
Add to params calls that give a java error the following before headers:
requestContentType: "application/json",

Replace pause(int) with pauseExecution(long) (units are milliseconds)

include ‘asynchttp_v1’ is not supported - comment this out and replace affected code with synchronous httpPut, httpGet, etc... calls or use async method calls documented below.

If you get an error trying to schedule a runIn(t, command) function, try changing it to runIn(t.toInteger(), command) to see if that solves it.

Button Devices are handled differently in Hubitat. Please see this post for details - Hubitat's button implementation

Hubitat Safety Monitor api calls detailed here - Hubitat Safety Monitor API - #3 by bravenel

Getting Hub Information is slightly changed... See following as an example

	def hub = location.hubs[0]
	log.debug "id: ${hub.id}"
	log.debug "zigbeeId: ${hub.zigbeeId}"
	log.debug "zigbeeEui: ${hub.zigbeeEui}"
	log.debug "type: ${hub.type}"
	log.debug "name: ${hub.name}"
	log.debug "localIP: ${hub.getDataValue("localIP")}"
	log.debug "localSrvPortTCP: ${hub.getDataValue("localSrvPortTCP")}"

Getting an OAuth2 endpoint is a little different as well. Also, both LOCAL and CLOUD Endpoints are supported:

Cloud

  • getApiServerUrl()
  • getFullApiServerUrl()
  • apiServerUrl(String url)
  • fullApiServerUrl(String url)

Local

  • getLocalApiServerUrl()
  • getFullLocalApiServerUrl()
  • localApiServerUrl(String url)
  • fullLocalApiServerUrl(String url)

So, an example for a local endpoint would be:

    def localEndPoint = fullLocalApiServerUrl("<YOUR_PATH_HERE>") + "?access_token=${state.accessToken}"

JsonSlurper . The JsonSlurper function will not work in habitat without including (at the beginning of the file): import groovy.json.JsonSlurper

sendHubCommand does not appear to work within drivers; however, it works in applications.

runEvery1Minute does not work in Hubitat. I have tested 5, 10, 15, and 30 minutes. Others need testing.


Telnet
All of this goes in a driver for a device that will connect to telnet. You open the connection this way:

telnetConnect(String ip, int port, String username, String password)

or

telnetConnect(Map options, String ip, int port, String username, String password)

Only option is "termChars" which changes what the termination character is. You can also pass in null for the password and userid if your endpoint doesn't require a login.

Then, to send/receive messages:

def sendMsg(String msg) {
	return new hubitat.device.HubAction(msg, hubitat.device.Protocol.TELNET)
}

and

def parse(String msg) {
// process incoming telnet messages
}

And, finally,

telnetClose()


ASYNC HTTP Calls

Asynchronous calls are now available in the Hubitat Elevation system.

The following methods are available:

void asynchttpGet(String callbackMethod, Map params, Map data = null)
void asynchttpPost(String callbackMethod, Map params, Map data = null)
void asynchttpPut(String callbackMethod, Map params, Map data = null)
void asynchttpDelete(String callbackMethod, Map params, Map data = null)
void asynchttpPatch(String callbackMethod, Map params, Map data = null)
void asynchttpHead(String callbackMethod, Map params, Map data = null)

Possible values for params are as such:

name value
uri (required) A URI or URL of the endpoint to send a request to
path Request path that is merged with the URI.
query Map of URL query parameters.
headers Map of HTTP headers.
requestContentType The value of the Content-Type request header. Defaults to 'application/json'.
contentType The value of the Accept request header. Defaults to the value of the requestContentType parameter if not specified.
body The request body to send. Can be a string, or if the requestContentType is "application/json", a Map or List (will be serialized to JSON).

The callback method should be defined as such:

def processCallBack(response, data) {
}

the name of the callback method can be whatever you choose, just pass it's name to the async method and the system will call it with the results of the http call and also any data you wish to pass to the method.

The first parameter to the callback method (response) is an AsyncResponse object. The methods available on this object are as follows:

Method Description
int getStatus() The status code of the response from the call
Map<String, String> getHeaders() A map of the headers returned from the call
String getData() String value of the response body from the call
String getErrorData()
String getErrorJson()
String getErrorMessage()
GPathResult getErrorXml()
Object getJson()
GPathResult getXml()
boolean hasError()

Below is an example of code which would be used to send a POST to a server and the callback method to process the response:

def sendAsynchttpPost() {
    def postParams = [
		uri: "http://httpbin.org/post",
		requestContentType: 'application/json',
		contentType: 'application/json',
		headers: ['CustomHeader':'CustomHeaderValue'],
		body : ["name": "value"]
	]
    
	asynchttpPost('myCallbackMethod', postParams, [dataitem1: "datavalue1"])
}

def myCallbackMethod(response, data) {
    if(data["dataitem1"] == "datavalue1")
    	log.debug "data was passed successfully"
    log.debug "status of post call is: ${response.status}"
}

Composite Device Drivers
Hubitat now supports devices having child devices. The methods that are available are as follows:

Parent Device Methods:

ChildDeviceWrapper addChildDevice(String namespace, String typeName, String deviceNetworkId, Map properties = [:])
ChildDeviceWrapper addChildDevice(String typeName, String deviceNetworkId, Map properties = [:])

Creates a new child device and returns that device from the method call.

Parameters:

Type Parameter description
String namespace The namespace of the child driver to add as a child device (optional, if not specified it will default the the namespace of the parent)
String typeName The name of the child driver to add as a child device
String deviceNetworkId unique identifier for this device
Map properties optional parameters for this child device. Possible values listed below:

properties:

boolean isComponent true or false, if true, device will still show up in device list but will not be able to be deleted or edited in the UI. If false, device can be modified/deleted on the UI.
String name name of child device, if not specified, driver name is used.
String label label of child device, if not specified it is left blank.
List<ChildDeviceWrapper> getChildDevices()

Gets a list of all child devices for this device.

ChildDeviceWrapper getChildDevice(String deviceNetworkId)

Gets a specific child device with the device network id specified.

void deleteChildDevice(String deviceNetworkId)

Deletes a specific child device with the device network id sepcified.

Child Device Methods:

ParentDeviceWrapper getParent()

Returns the parent device when called from a child device.


How to get Latitude and Longtitude of the hub programatically

location.getLatitude()
location.getLongitude()

Converting an object to JSON
Replace uses of util.toJson with groovy.json.JsonOutput.toJson

74 Likes

Found this one…

evt.doubleValue causes errors when porting a temp app from ST

changing to event.value as double fixed the issue

3 Likes

Driver code should have all simulator and tile sections removed.
App code should have all icon urls set to empty string.
Legacy enum input elements using the metaData key are not supported, replace with options.
Map type options need the following form currently [["a":"opta"],...], non map options use the usual list form.

4 Likes

Async HTTP calls are now supported, but appears they are called differently.

3 Likes

Hub UUID. In SmartThings, the hub.id returned the UUID of the hub. In Hubitat, the same appears to return something else (for me, a "1").

1 Like

yup, that's what it is, since we're local we don't need a uuid for this, it's a BigInteger though, not a string.

3 Likes

I wander if an automated conversion tool could be created ?

8 Likes

LAN WiFi Devices not Throttled.

In SmartThings, there is a restrictor (throttle) that sends Hub LAN commands about every 100 ms. I believe that Hubitat does not have this restrictor. Impact is that if you are sending commands close together, then you will need a "pause" execution on some devices (i.e., Samsung Speakers). Otherwise, return message are missed or confused, especially if you are using explicit callbacks.

Mike, Is this true. It is what I am seeing while developing the Samsung WiFi speaker driver. I get some really weird results until I insert a pauseExecution.

Dave*

1 Like

We don't throttle or rate limit anything, and the hub is fast enough that you need to consider device response latency and event ordering.

WiFi Command Firing. For some reason, WiFi commands are not transmitted if the firing command is followed by a log command. Example

This works:

private sendUpnpCmd(String action, Map body=[InstanceID:0, Speed:1]){
    logTrace("sendUpnpCmd: upnpAction = ${action}, upnpBody = ${body}")
    def deviceIP = getDataValue("deviceIP")
    def host = "${deviceIP}:9197"
    def path = "/upnp/control/AVTransport1"
    def hubCmd = new hubitat.device.HubSoapAction(
        path:    path,
        urn:     "urn:schemas-upnp-org:service:AVTransport:1",
        action:  action,
        body:    body,
        headers: [Host: host, CONNECTION: "close"]
    )
    log.info hubCmd
    hubCmd
}

This doesn't work:

private sendUpnpCmd(String action, Map body=[InstanceID:0, Speed:1]){
    logTrace("sendUpnpCmd: upnpAction = ${action}, upnpBody = ${body}")
    def deviceIP = getDataValue("deviceIP")
    def host = "${deviceIP}:9197"
    def path = "/upnp/control/AVTransport1"
    def hubCmd = new hubitat.device.HubSoapAction(
        path:    path,
        urn:     "urn:schemas-upnp-org:service:AVTransport:1",
        action:  action,
        body:    body,
        headers: [Host: host, CONNECTION: "close"]
    )
    hubCmd
    log.info hubCmd
}

is implicitly really

log.info hubCmd 
return hubCmd

If you think of it this way, it makes more sense. In fact, I believe you can write it exactly as I have shown above to make it more clear. You would never issue another command after you 'return' from a routine.

In ST, you could use 'sendHubCommand()' to make sure the hub processed the request as desired. Hubitat currently does not support 'sendHubCommand()' in drivers, so you need to make sure that the last thing you do is return the cmd you want processed.

If I am mistaken about this, someone please clarify. This is just from my experience on both ST and Hubitat.

1 Like

You are correct. That is part of the groovy standard, the last line is considered an implicit return. The Apache Groovy programming language - Semantics

Also, sendHubCommand will be available for drivers in the next release. (it is currently available for apps)

5 Likes

A couple of basic syntax that also appear to differ between the platforms:

DOESN'T WORK:
input "fieldName", type:"text", title: "The Title", description: "The Desc"
WORKS:
input "fieldName", "text", title: "The Title", description: "The Desc"

DOESN'T WORK:
section("a title")
WORKS:
section("a title"){}

I'll add more as I test more of my code as I move it..

Can this thread be made sticky somewhere, it's been the most useful tread for me so far and Discourse search is a little painful.. :slight_smile:

4 Likes

This may be a dumb question, but I'm stumped about how to obtain OAUTH cloud endpoints using a typical OAUTH flow with Hubitat. Do I use the same calls as with SmartThings? Does it return the "code" as ST does that then requires another call to get the auth token? If so I don't see why you would need the calls above since the oath flow would return the endpoint. What am I missing? I can easily call these on the groovy side but that doesn't help me get the info into my PHP app that runs on a random server that may or may not be on the local LAN. I couldn't find any examples of OAUTH flow so any help will be appreciated. Also, will the httpPost work for local IP calls?

1 Like

I cannot find a solution to

Unable to resolve physicalgraph.app.ChildDeviceWrapper [...]

I replaced physicalgraph with hubitat, but that just changed the package name in the same error. Anyone have any idea?

The function with the error is:

private physicalgraph.app.ChildDeviceWrapper getChild(Integer childNum) {
	return childDevices.find({ it.deviceNetworkId == "${device.deviceNetworkId}-c${childNum}" })
}

you don't need any of class path, replace the above with:
private getChild(Integer childNum) { return childDevices.find({ it.deviceNetworkId == "${device.deviceNetworkId}-c${childNum}" }) }

2 Likes

Thanks for the help, but I am returning my device. The SmartThings driver will require too much time to convert and understand (I have other more pressing projects), and the Hubitat DH for Aeon Home Energy Meter does nothing. After running for several days, and not collecting a single bit of data, it is time to abandon this idea for now. Hopefully someday soon an actual DH implementers document will be published, and we can bring custom DH code mainstream. My brain just isn't plastic enough to figure out undocumented systems.

3 Likes

I hadn't realised that Telnet access was possible from Hubitat ... this alone justifies the move from Smartthings and opens a world of possibilities.

Thankyou devlopers... :smiley:

4 Likes

@mike.maxwell I'm tagging you because I believe that you were one of the original creators of this project here that links squeezebox to smartthings.

Im trying to port it over to hubitat but I think its this line that's giving me issues in the squeeze switch server driver

sendHubCommand(new hubitat.device.HubAction("${playerID} ${command}\r\n\r\n",hubitat.device.Protocol.LAN, "${device.deviceNetworkId}"))

My json slurper sees login information being sent from the python script to telenet but noting is sent when i change a player switch state... I changed the port in my python script from 3500 to 3501 and made the device id changes required so I think its this line thats holding me up... any suggestions or a workaround?

Hasn’t this already been done?

Andy

2 Likes