[Release] Unbranded 8-Relay Ethernet controller (HHC-N-8I8O) Lan driver

This is a LAN driver for a Unbranded 8-Relay Ethernet controller, sometimes found with the Identification HHC-N-8I8O ;
If it looks like this board, it is probably it... this can be found in abundance on eBay, Alibaba or other market places

I have a few of those configured at home, I just finished the first version of the driver.

Please install the following two drivers:

  • child-relay-switch-with-index.groovy
  • hhc-n-8180.groovy

Enjoy:
https://github.com/TheFabio/hubitat

4 Likes

Interesting but I could not find good use cases for this in my home.
What are you using this relays for? Maybe I'll get an idea and start a new project :smiley:

Sprinkler system? Low voltage lighting? Seems like lots of options at that price

8 relays might be too much for my use case.
Found this on Ebay -Ebay.
Will the driver work with a 2 relay board as well?

The main/easier use cases for home projects are for irrigation and/o sprinklers.
the number of relay's is never enough.... I got this 8-relay one to start with, then got another 3 along the way.

in my project, I have it connected to do both plus garage door remote, storm water pumps, driveway lights a few of the blinds.

@amithalp I if the board looks like the one in the picture from my repo, it probably is. I saw versions of 2, 4, 8,16 and 32 relays of this board. A good tell is the instructions the sellers give you, as the commands to turn it on/off will be the same.

Has anyone attempted to use the input side of this board as a network contact sensor?

For instance I have one of these that is connected to several original alarm door/window magnetic switches and would like to be able to add those as contact sensors in Hubitat.

Thanks for any info you might have.

2 Likes

The driver I build for it only sends commands, it would be easy enough to extend it to read the board inputs as well. In my repo's landing page I provided instructions on how to read the state of the inputs.

I liked the idea of using the inputs to read contact sensors... give me a few days and I will extend the driver to store the input states

1 Like

I just updated the driver to read the board inputs as attributes.

1 Like

Thanks Fabio, I will give it a shot and see how it goes!!

Hi Fabio,
Thank you, this is fantastic. Exactly what I need it for my underfloor heating zone control but not limited.
Well done, mate

Hi guys, I finally got the board and trying to make it work. First issue is changing the board IP (192.168.0.105) to my subnet mask (10.21.1.xxx). What tool are you using to do that?

1 Like

I built this script in ruby to configure the board, it is a little raw but it does the trick.

There is a windows based one as well, you can get from eBay sellers but I was reluctant on using it long term. I could not trust it was free of virus or malicious code because it is build in C++ (and i could only find it from the eBay seller)... and help for it was very limited and in Chinese

You will find the Ruby config scripts and usage example in this repo

Hi @fabio.hubitat , thanks for your instructions. I am afraid that I am missing some basic skills to make this work for me.
I am changing my PC IP address to match the board initial IP subnet (192.168.0.x) and I can ping the board and can even control it with a test tool I found on the internet.
I have no real programing skills and this is the first time I heard of Ruby.
I have downloaded a Ruby app called RubyMine and copy pasted your FIND script but I a not getting any result. Not sure I am even doing it right in the RubyMine app. Can you please advise?

You need to modify the script and run it with ruby. It would be a little tricky to teach how to execute a ruby script if you never did it. You would need to follow a tutorial to get started..
RubyMine is an IDE (or refined text editor) for Ruby scripts, it does not run the scripts, but only help you write them.

I just sent you the files I got from the eBay seller. Hopefully you can trust it better then I did...

Hi @fabio.hubitat ,
I see that the input side of the board can detect a 5v signal so I was thinking of using it as a device to track my alarm system. Meaning, I will connect the board in parallel to the existing alarm board to detect signals from the sensors. Say, Input 1 will be connected to a motion sensor. When the motion sensor detect motion it sends a 5V signal to the alarm board and the 8 channel board input.
Problem is the input attributes only gets updated when the board gets updated (10 seconds to my knowledge). In this time the signal from the sensor might not be there.
Is there a way to get an instant trigger when the input get the voltage signal?
I am not a developer thus not sure if this is even possible.
Thanks,

The 10s is an arbitrary value I added... you could make it report more often by changing the driver code line

schedule("0/10 * * ? * *", "refreshLoop") // every 10 seconds

to

schedule("0/1 * * ? * *", "refreshLoop") // every 1 seconds

or even to

schedule("0/0.5 * * ? * *", "refreshLoop") // every 0.5 seconds (half-second)

I would not use this board for an alarm system where you expect real-time responses, as the board has a long delay sometimes taking more then a few secs to reply. (at least the ones I have)

Making hubitat refresh too often might not be reliable because of that, but you can always try (I just never had to change this 10s refresh time).

1 Like

Hi Fabio
Thanks a lot for the driver and the board info! Exactly what I need.

I'm just starting with HE and would appreciate some help:
I can switch relays from the device GUI.
I can reach the board in RM via custom actions/actuator capability and send allOn/allOff commands, but couldn't find a child device (or RelaySwitch capability) in custom actions. I'm obviously missing something.
How do I switch an individual relay from the rule machine?
Thanks!

You should end up with 8 devices, each using the "child-relay-switch-with-index" driver


I do not remember if they were created automatically, but I think they did.
Did you make sure you installed the "child-relay-switch-with-index" before you created the parent device?

yes, I did, and I see all 8 of them, and I can switch a relay from its interface by clicking on/off.
My problem is that a child device is not exposed in 'custom actions' section of rule machine.
In other words, I can set up a rule trigger, but when trying to set up an action, only the board is available, not a child device (or I cannot find it)

OK this got me digging deeper.
If you look at the device driver (parent device that is) you can add commands to trigger ON or OFF for each relay.
For example, you can define a command to trigger relay # 1 ON like this:
Relay1ON
def Relay1ON() {
sendCommand('all10000000')
}

Each one of the 8 numbers represent a relay so to trigger relay 2 ON you will use - all01000000 and so on. 0 is OFF and 1 is ON.

I am no developer but this is what I did when testing this device.
Look at the below file as you have to define the command in 2 places.

Summary

metadata {
definition (
author: "The Fabio",
description: "Driver for Unbranded 8-Relay board with Ethernet - HHC-N-8180\n Requires the device 'child-relay-switch-with-index",
importUrl: "https://raw.githubusercontent.com/TheFabio/hubitat/main/src/hhc-n-8180.groovy",
name: "8-Port Relay HHC-N-8180",
namespace: "thefabio"
) {
capability "Initialize" // adds initialize() callback which is called when hubitat restarts
capability "Actuator" // does not add functions or variables, but the Actuator capability allows the custom commands to be called via Rule Machine

 command "allOn"
 command "allOff"
  // Amit - created the relay 1 on command
  command "Relay1ON"

 attribute "connectionStatus", "enum", ['connected', 'disconnected']
 attribute "input1", "number"
 attribute "input2", "number"
 attribute "input3", "number"
 attribute "input4", "number"
 attribute "input5", "number"
 attribute "input6", "number"
 attribute "input7", "number"
 attribute "input8", "number"

}
preferences {
input title: "Unit Address",
name: "ipAddress",
type: "text",
description: "The ip address configure at the unit or its dns name",
required: true,
defaultValue: "192.168.0.105"

input title: "Unit Address Port Number",
    name: "portNumber",
    type: "number",
    description: "The network port number configure at the unit. (note that 65535 is reserved)",
    required: true,
    defaultValue: 5000,
    range: "1..65534"

input title: "Disable Device",
    name: "disabled",
    type: "bool",
    description: "Disable the refresh loop and sending commands",
    required: true,
    defaultValue: false

input title: "Debug Mode",
    name: "debugMode",
    type: "bool",
    description: "Write debug mode log entries",
    required: true,
    defaultValue: false

input title: "Relay delay settings explained",
    description: "Number of seconds for that relay to switch 'off' after it has switched 'on' (leave it as 0 when there is no need to switch back). only the 'on' action is affected by this configuration",
    name: "unused1", // this is not used, it is used to explain the remainder parameters
    type: "text"

 input title: "Relay 1 delay",
     name: "relay1Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 2 delay",
     name: "relay2Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 3 delay",
     name: "relay3Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 4 delay",
     name: "relay4Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 5 delay",
     name: "relay5Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 6 delay",
     name: "relay6Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 7 delay",
     name: "relay7Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

 input title: "Relay 8 delay",
     name: "relay8Delay",
     type: "number",
     required: true,
     defaultValue: 0,
     range: "0..99"

}
}

// default methods - begin

// This method is called when the device is first created and can be used to initialize any device specific configuration and setup
def installed() {
log.info "Device ${device.getName()} Created"
setupDevice()
}

// This method is called when the device is removed to allow for any necessary cleanup.
def uninstalled() {
log.info "Device ${device.getName()} Removed"
}

// This method is called when the preferences of a device are updated.
def updated() {
writeLog "Device ${device.getName()} Updated"

// so we can redo scheduled tasks should the driver be updated
setupScheduledTasks()
}

//This method is called in response to a message received by the device driver
def parse(msg) {
writeLog "Parse received: ${msg}"
if (stateValue(device, 'connectionStatus') != 'connected'){
sendEvent(name: "connectionStatus", value: 'connected')
writeLog "Connected"
}
processStatusUpdate(msg)
}
// default methods - end

// capability initialize - start
def initialize() {
writeLog "Initializing..."
sendEvent(name: "connectionStatus", value: 'disconnected')
reconnect()
}
// capability initialize - End

// interfaces.rawSocket requirements - start
def socketStatus(socketStatusMsg){
writeLog "socketStatus: ${socketStatusMsg}"

sendEvent(name: "connectionStatus", value: 'disconnected')

}
// interfaces.rawSocket requirements - End

def writeLog(logLine){
if (!debugMode){
return
}

log.debug logLine

}

def setupScheduledTasks() {
unschedule()
schedule("0/10 * * ? * *", "refreshLoop") // every 10 seconds
}

def refreshLoop() {
if (disabled) {
return
}

if (stateValue(device, 'connectionStatus') == 'connected') {
    sendCommand('read') //Request relay state
    sendCommand('input') //Request input state
} else {
    connectSocket()
}

}

def childRelayNetworkId(relayIndex) {
return "${device.deviceNetworkId}-relay-${relayIndex}"
}

def stateValue(target, name) {
target.currentState(name) ? target.currentState(name).value : ''
}

def connectSocket() {
writeLog "Connecting socket ${ipAddress}:${portNumber}"
try {
interfaces.rawSocket.connect(ipAddress, (int) portNumber)
}
catch(Exception e) {
sendEvent(name: "connectionStatus", value: 'disconnected')
log.error "connectSocket (${ipAddress}:${portNumber}): ${e}"
}
}

def updateRelayState(relayIndex, switchOn){
newStatus = switchOn ? 'on' : 'off'
childRelay = getChildDevice(childRelayNetworkId(relayIndex))

if (stateValue(childRelay, 'switch') == newStatus) return;

childRelay.sendEvent(name: "switch", value: newStatus)

}

def updateInputState(inputIndex, inputState){
inputName = "input" + inputIndex
inputValue = inputState ? 1 : 0;

writeLog "received InputState ${inputName} ${inputValue}"
if (stateValue(device, inputName) == "${inputValue}") return;

writeLog "updating InputState ${inputName} from ${stateValue(device, inputName)} to ${inputValue}"
sendEvent(name: inputName, value: inputValue)

}

def setupInputs(){
for (i = 1; i <9; i++) {
updateInputState(i, false)
}
}

def setupDevice(){
// Creating 8 child devices (one per available relay)
for (i = 1; i <9; i++) {
switchDevice = addChildDevice("child-relay-switch-with-index", childRelayNetworkId(i), [
name: "${device.displayName} - Relay ${i}",
isComponent: true
])
switchDevice.updateSetting("relayIndex", i)
updateRelayState(i, false)
}

// Set disconnected state
sendEvent(name: "connectionStatus", value: 'disconnected')
setupScheduledTasks()
setupInputs()

}

def processStatusUpdate(msg) {
// a message can be
// 72656C61793030303030303030696E7075743030303030303030
// 72656C61793030303030303030
// 696E7075743030303030303030

indexOfRelay = msg.indexOf("72656C6179") // word "relay" in ASCII
indexOfInput = msg.indexOf("696E707574") // word "input" in ASCII

if (indexOfRelay >= 0 ) {
   // +10 is an offset to remove the word "relay"
   rawStatus = msg.substring(indexOfRelay + 10, indexOfRelay + 26)
   (0..7).each { n ->
       updateRelayState(n +1 , rawStatus[15 - n*2] == "1")
   }
}

if (indexOfInput >= 0) {
   // +10 is an offset to remove the word "input"
   rawStatus = msg.substring(indexOfInput + 10, indexOfInput + 26)
   (0..7).each { n ->
       updateInputState(n +1 , rawStatus[15 - n*2] == "1")
   }
}

}

def sendCommand(boardCommand) {
if (disabled) {
return
}
writeLog "sendCommand ${boardCommand}"
if (stateValue(device, 'connectionStatus') != 'connected'){
writeLog "Could not perform command, socket is disconnected"
return
}

try {
    interfaces.rawSocket.sendMessage(boardCommand)
}
catch(Exception e) {
    log.error "sendCommand Error: ${e}"
    sendEvent(name: "connectionStatus", value: 'disconnected')
}

}

def relayDelay(relayIndex) {
switch (relayIndex) {
case 1:
return relay1Delay
case 2:
return relay2Delay
case 3:
return relay3Delay
case 4:
return relay4Delay
case 5:
return relay5Delay
case 5:
return relay6Delay
case 7:
return relay7Delay
case 8:
return relay8Delay
}
}

def buildSwitchRelayCommand(relayIndex, switchOn) {
relayCmd = ""
if (switchOn) {
relayCmd = "on"
}
else {
relayCmd = "off"
}
relayCmd = "${relayCmd}${relayIndex}"

relayDelay = relayDelay(relayIndex)
if (switchOn && relayDelay > 0){
    relayCmd = "${relayCmd}:"
    if (relayDelay < 10) {
        relayCmd = "${relayCmd}0"
    }
    relayCmd = "${relayCmd}${relayDelay}"
}

return relayCmd

}

def switchRelay(relayIndex, switchOn) {
relayCommand = buildSwitchRelayCommand(relayIndex, switchOn)
sendCommand(relayCommand)
}

def allOn() {
sendCommand('all11111111')
}

def allOff() {
sendCommand('all00000000')
}

// Amit - defined the relay 1 on command

Relay1ON
def Relay1ON() {
sendCommand('all10000000')
}

def reconnect() {
//just disconnect, and the refreshLoop will connect
sendEvent(name: "connectionStatus", value: 'disconnected')
try {
interfaces.rawSocket.close()
}
catch(Exception e) {
// nothing to do, will reconnect
}
}

Once this is done you will these commands available in RM like this: