Questions on using Raw Socket Interface

My first attempt at doing a driver in Hubitat..

With some help here (thanks tomw!) I quickly got a trivial driver working for controlling my old x10 devices via a raw socket interface to the existing mochad server. for reference, it is pasted below.

But, I have some questions about the Raw Socket interface.

  1. The signature for the connect method has no return. I'd expect a "handle" of some sort to be used for subsequent sendMessage invocations. I think this implies there can be one and only one raw socket connection?

  2. When should the connect be done? I did it in a capability "Initialize", but I don't know if that makes sense, or when "Inititalize" will be called. For testing, I'm just clicking on Initialize where the device prefs and buttons for on and off are presented.

  3. When should the close be done? I do a close in Initialize before doing the connect. Should I invoke close somewhere else?

  4. There is no feedback about success when the connect is done. Should socketStatus I provided be called to indicate success or failure of the connection attempt?

  5. parse IS called when a response is sent from the connected (mochad) server. When I log it, it looks like this:

dev:842021-05-27 08:58:39.053 am debug30352F32372030383A35383A333820547820524620486F757365556E69743A2041342046756E633A204F66660A

The response looks like this when I send the message using netcat (nc):

05/27 08:29:23 Tx RF HouseUnit: A4 Func: Off

So, apparently, parse is getting called with a string that is the ascii version of the string. Should I check the groovy docs for how to convert it, or should I use a different signature for my implementation of parse"

Thanks!

The driver code follows...

def version() {"v0.1"}

import hubitat.helper.InterfaceUtils

metadata {
    definition (name: "X10 via MochaD", namespace: "Flying-Nerd", author: "Dave Thomas") {
        capability "Initialize"
        capability "Switch"
      

    }
}

preferences {
    input("ip", "text", title: "IP Address", description: "[IP Address of your mochad server", required: true)
    input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    input name: "station", title:"X10 Station", description: "X10 Station ID"
    input name :"port", title:"Mochad port", type:"text", description: "Port for Mocha Server", required:true
}

void updated() {
    log.info "Updated..."
   
}

void installed() {
    log.info "Installed..."
    refresh()
}

def refresh() {
    
    
}
def Close() {
    if (logEnable) log.debug "closing socket"
    interfaces.rawSocket.close()
}
def on() {
    if (logEnable) log.debug "Sending on request to [${settings.ip}], station ${settings.station}"
    sendEvent(name: "switch", value: "on", isStateChange: true)
    
    interfaces.rawSocket.sendMessage("rf ${settings.station} on\n")
   
    //refresh()
}
def off() {
    if (logEnable) log.debug "Sending off request to [${settings.ip}, station ${settings.station}]"
    sendEvent(name: "switch", value: "off", isStateChange: true)
    
    interfaces.rawSocket.sendMessage("rf ${settings.station} off\n")
    //refresh()
}
def initialize() {

    if (logEnable) log.info "initialize() called"
    Close()
    interfaces.rawSocket.connect(ip, port.toInteger())
    //refresh()
}
def socketStatus(String message) {
    if (logEnable) log.info "socketStatus() called"
    log.info message
}

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

I would modify your updated() method to include a call to initialize(). This way, if a user changes the IP address or Port in the settings, the connection will ne reestablished with the correct settings. "updated()" is called when the user clicks "SAVE" in the settings section.

I see you have an empty 'refresh()' command, but that will need the "capability "Refresh"' added if you actually decide to use it.

I have never used the raw sockets interface, personally. So I really don't have answers to your other questions.

2 Likes

Thanks for the tips! I'll add the initialize to the update method.

Re: refresh. I was looking at driver code that had calls to refresh but didn't have an implementation. So, I just stubbed it out.

When will refresh be called? What should my code do? Or, maybe it doesn't make sense for my scenario.

It probably does not make sense in your specific scenario. Refresh is only called either manually (clicking on the button on the device details page) or programmatically via an App, like Rule Machine. It is most often used to request from a physical device a status update. In your case, since you're only issuing commands to the remote server, there is probably no need for the refresh() command.

I have a driver that polls an IoTaWatt energy meter via a http interface, since there is no way to set up a subscription. Thus, clicking refresh forces that poll to happen immediately, versus waiting for the next automatic scheduled poll event to occur. I use it only for debugging the driver.

1 Like

Converted the hex to a string using using a Java routine I found. Works, but I'd like to understand why the string passed to parse is hex while I get a readable string when I use netcat to send the message.

Also, when I send the message using netcat (nc) the reply not only shows up on the command line, but also gets passed to the parse function in my Hubitat driver! So, it's like the socket opened by my Hubitat driver is shared by netcat? That really confuses me, and I'd like to understand better.

While the driver "works" for my purposes, I still have the questions on when/where to connect and close.

Any suggestion on where to get info on how to use the rawSocket interface properly?

Tagging @mike.maxwell and @gopher.ny from Hubitat, as they may be better able to answer your remaining questions.

Thanks, ogiewon. Hope they have the time to comment.

So, it's like the socket opened by my Hubitat driver is shared by netcat?

Maybe, the "connection" is UDP instead of a TCP connection I'm more familiar with. If so, a bit of a misnomer, I think, since UPD is connectionless.

Also, whenever I create a new virtual device using the new type provided by the driver pasted above, I get a server 500 error.

But, the device gets added, I can change preferences, and the device works like I expect.

Logs have nothing indicating what caused the 500 error.

How do I investigate?

You probably need to make sure the IP and Port settings are populated before making the raw socket connection. Just trap that condition and maybe log something to the logs to let the user know that they need to fill in those settings.

But what is triggering the connection? Is initialize called when the device is first created? If so, makes sense.

(I don't see the print indicating initialize or connect method was called in the logs)

I have always assumed the answer to this is yes...per virtual device instance. If you need multiple connections, you could have a parent with multiple children which each have their own connections.

I usually do it in Initialize. It will get called when the hub boots. As @ogiewon pointed out, you can also call Initialize yourself at various points, like in Updated or in error handling cases elsewhere (e.g. socketStatus)

This one is less clear. I usually close in error handling where I'm going to re-initialize (and open a new connection). This might practically be at the top of Initialize as you said, but you may want to do it discretely elsewhere for various reasons. I usually tend to do it in Uninstalled, but that's sort of a special case (that may happen implicitly, anyway).

It'll be called automatically by the framework as needed. I haven't used raw socket much, but the other interfaces I have used (websocket, etc) do seem to report a status on successful or failed connection.

I guess netcat has a more user-friendly implementation that handles the parsing behind the scenes for you. Here's what I use in the case of what you're seeing from the response in Hubitat:

def hexToAscii(hexStr)
{
    return new String(hubitat.helper.HexUtils.hexStringToByteArray(hexStr))
}

I'd guess that your connection is failing in Initialize and throwing an exception, maybe because the IP and port settings aren't valid yet. I'd check for those in Initialize, then bail out before connecting if they're not valid (for example, if they're null). Then have Updated call Initialize again once the user enters those values.

2 Likes

Possible that logEnable is null at that point during installation, so there's no print. Then connect() gets invalid inputs (also null?) and throws an exception, which you're not handling and which results in the 500 error. Running it again while saving inputs makes it all work correctly because they're all valid. Just a hunch (or a bunch of hunches, really :wink: ).

1 Like

Thanks!

Makes sense now. I'll try doing the trap think as ogiwon suggested.

Also, thanks for the tip on formatting the hex characters. The code snippet I have works, but your "one liner" is much nicer.

2 Likes

In case it is useful as an example of making and maintaining the connection, here is a driver that I worked on with @ymerj and @ogiewon . I picked up some useful snippets for my other network drivers from @ogiewon's parts, especially the reconnection logic. There are also some ideas shown for handling exceptions raised by failed connections and other error conditions.

1 Like

I'll study that example. Thanks!

I'm now getting many calls to the parse function when a single message is sent. Probably one for every virtual device I created with this driver.

I suspect each time a new device was created, a duplicate interface to the remote server was created. So, doing it initialize might not be a good idea?

It's like the mochad server thinks it has multiple connected clients and sends the response back to each one. I really need only one open socket for all devices and it seems like maybe there is a new one created each time a new device is created.

Also, I removed the

if (logEnable)

in Initialize where the logging should occur.

There's nothing logged when the device is created, but IS logged when the preferences are first saved.

I'm now getting many calls to the parse function when a single message is sent. Probably one for every virtual device I created with this driver.

I confirmed there's one call for each device. After deleting devices, I get one less callback to parse after each device deletion.

So, I think I need a place to do the connect ONCE, not each time preferences are saved for the device.

How can I do that? Sorry, I'm ignorant on how drivers should get initiallized.

I don't get any callback to socketStatus on connection, so I can't use that to set an "AlreadyConnected" flag.

So, the typical design for an integration such as yours would be to have a Parent Device perform all communications with the LAN connected device. The parent would then create a Generic Component Child Switch device for each of your old X-10 switches. When the child device is turned on or off, it simply calls the parent device to handle the network communications.

2 Likes

Got it, (I think)

Thanks again!

1 Like