As I have been making dumb devices smart with the Zigbee 4-channel boards (various brands, MOES, etc), I've wanted a custom driver for these boards that I could modify into an actual device.
Example:
This is my use case for the modified drivers, with the Tower fan as an example:
I am now converting a Swamp Cooler to Zigbee using yet another one of these boards. I wanted to get out of the device <-> controller <-> virtual device setup I have been using.
I am currently rewriting my Tower Fan controller to all be in one single driver that controls the fan endpoints directly. That will be easily modified into the swamp cooler driver to control fan speed and turn the water pump on and off.
Here are some drivers AI produced for me.
This one is a drop in for the built-in driver. It creates the children in the same way, so it can literally replace the built-in driver that is already being used with a board, and still use the same children. For new devices, it will create the children:
Custom Zigbee Multi-Endpoint Switch
/* Custom Zigbee Multi-Endopoint Switch
Written by Deepseek AI - 4/10/26
*/
import hubitat.device.HubAction
import hubitat.device.Protocol
metadata {
definition(name: "Custom Zigbee Multi-Endpoint Switch", namespace: "Hubitat", author: "chrisbvt") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
capability "Switch" // <-- parent acts as a master switch
command "configure"
command "refresh"
command "on"
command "off"
}
preferences {
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
}
def logsOff() {
log.warn "debug logging disabled..."
device.updateSetting("logEnable", [value: "false", type: "bool"])
}
def configure() {
if (logEnable) runIn(1800, logsOff)
log.info "Configuring ${device.displayName}"
// Create child devices for endpoints that exist on your device
[1, 2].each { ep ->
def childId = "${device.id}-${ep.toString().padLeft(2,'0')}"
if (!childDevices.find { it.deviceNetworkId == childId }) {
try {
addChildDevice("hubitat", "Generic Component Switch", childId, [
name: "${device.displayName} Switch ${ep}",
label: "${device.displayName} Switch ${ep}",
isComponent: false
])
} catch (e) {
log.error "Failed to create child for endpoint ${ep}: ${e.message}"
}
}
}
}
def refresh() {
// optional – can be left empty
}
// Parent switch commands – control all children
def on() {
if (logEnable) log.debug "Parent on – turning on all endpoints"
childDevices.each { child ->
componentOn(child)
}
// Optimistically update parent state
sendEvent(name: "switch", value: "on", displayed: false)
}
def off() {
if (logEnable) log.debug "Parent off – turning off all endpoints"
childDevices.each { child ->
componentOff(child)
}
sendEvent(name: "switch", value: "off", displayed: false)
}
private Integer getEndpointFromChild(childDevice) {
def parts = childDevice.deviceNetworkId?.split("-")
if (parts && parts.size() == 2 && parts[0] == device.id.toString()) {
try {
return parts[1] as Integer
} catch (NumberFormatException e) {
return null
}
}
return null
}
def parse(String description) {
if (logEnable) log.debug "Parsing: ${description}"
def map = zigbee.parse(description)
if (!map) return
try {
def clusterId = map['clusterId']
def attrId = map['attrId'] ?: map['attributeId']
def endpoint = map['endpoint']
def value = map['value']
if (clusterId == 0x0006 && attrId == 0x0000) {
def state = (value == 0x01) ? "on" : "off"
def child = childDevices.find { getEndpointFromChild(it) == endpoint }
if (child) {
if (txtEnable) log.info "${device.displayName} endpoint ${endpoint} is ${state}"
child.sendEvent(name: "switch", value: state, displayed: true)
}
}
} catch (e) {
// Ignore any parsing errors – device still works
if (logEnable) log.trace "Ignored non‑On/Off message"
}
}
def componentOn(childDevice) {
def ep = getEndpointFromChild(childDevice)
if (ep == null) return
childDevice.sendEvent(name: "switch", value: "on", displayed: false)
def cmd = "he cmd 0x${device.deviceNetworkId} 0x${ep.toString().padLeft(2,'0')} 0x0006 0x01 {}"
sendHubCommand(new HubAction(cmd, Protocol.ZIGBEE))
}
def componentOff(childDevice) {
def ep = getEndpointFromChild(childDevice)
if (ep == null) return
childDevice.sendEvent(name: "switch", value: "off", displayed: false)
def cmd = "he cmd 0x${device.deviceNetworkId} 0x${ep.toString().padLeft(2,'0')} 0x0006 0x00 {}"
sendHubCommand(new HubAction(cmd, Protocol.ZIGBEE))
}
def componentRefresh(childDevice) {
// not needed
}
This Driver does not create the children. All the endpoints are controlled from the single device driver using commands.
Custom Zigbee Multi-Endpoint Switch with no Children
/* Custom Zigbee Multi-Endopoint Switch with no Children
Written by Deepseek AI - 4/10/26
*/
import hubitat.device.HubAction
import hubitat.device.Protocol
metadata {
definition(name: "Custom Zigbee Multi-Endpoint Switch No Children", namespace: "Hubitat", author: "chrisbvt") {
capability "Actuator"
capability "Configuration"
capability "Refresh"
capability "Sensor"
command "configure"
command "refresh"
command "endpointOn", [[name: "endpointNumber", type: "NUMBER", description: "Endpoint number (1-4)"]]
command "endpointOff", [[name: "endpointNumber", type: "NUMBER", description: "Endpoint number (1-4)"]]
attribute "switch1", "enum", ["on", "off"]
attribute "switch2", "enum", ["on", "off"]
attribute "switch3", "enum", ["on", "off"]
attribute "switch4", "enum", ["on", "off"]
}
preferences {
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
}
}
def logsOff() {
log.warn "debug logging disabled..."
device.updateSetting("logEnable", [value: "false", type: "bool"])
}
def configure() {
if (logEnable) runIn(1800, logsOff)
log.info "Configuring ${device.displayName}"
// Configure endpoints 1-4 directly (most multi-endpoint switches use these)
(1..4).each { ep ->
// Bind the On/Off cluster (0x0006) for this endpoint
sendZigbeeCommands(zigbee.bind(0x0006, ep))
// Configure reporting: send state change immediately, min interval 0, max 300s, change 1
sendZigbeeCommands(zigbee.configureReporting(0x0006, ep, 0x0000, 0x10, 0, 300, 1))
// Read current state so we know initial value
sendZigbeeCommands(zigbee.readAttribute(0x0006, ep, 0x0000))
}
log.info "Configuration commands sent for endpoints 1-4"
}
def refresh() {
if (logEnable) log.debug "Refreshing device"
configure()
}
def parse(String description) {
if (logEnable) log.debug "Parsing: ${description}"
def map = zigbee.parse(description)
if (!map) return
// Handle On/Off attribute reports or read responses
if (map.clusterId == 0x0006 && (map.attrId == 0x0000 || map.attributeId == 0x0000)) {
def ep = map.endpoint
if (ep == null || ep < 1 || ep > 4) return // ignore invalid endpoints
def value = map.value
def stateVal = (value == 0x01) ? "on" : "off"
def attrName = "switch${ep}"
if (txtEnable) log.info "${device.displayName} endpoint ${ep} is ${stateVal}"
sendEvent(name: attrName, value: stateVal)
return
}
if (logEnable) log.debug "Unhandled message: ${map}"
}
// Turn on a specific endpoint (1-4)
def endpointOn(endpointNumber) {
// Convert to integer in case it's passed as string (e.g., from rule)
def ep = endpointNumber as Integer
if (ep < 1 || ep > 4) {
log.warn "endpointOn: invalid endpoint number ${endpointNumber} (must be 1-4)"
return
}
sendEndpointCommand(ep, 0x01) // 0x01 = ON
sendEvent(name: "switch${ep}", value: "on", displayed: false)
if (logEnable) log.debug "Turned ON endpoint ${ep}"
}
// Turn off a specific endpoint (1-4)
def endpointOff(endpointNumber) {
def ep = endpointNumber as Integer
if (ep < 1 || ep > 4) {
log.warn "endpointOff: invalid endpoint number ${endpointNumber} (must be 1-4)"
return
}
sendEndpointCommand(ep, 0x00) // 0x00 = OFF
sendEvent(name: "switch${ep}", value: "off", displayed: false)
if (logEnable) log.debug "Turned OFF endpoint ${ep}"
}
// Helper to send Zigbee On/Off command to a specific endpoint
private void sendEndpointCommand(int endpoint, int onOff) {
def cmd = "he cmd 0x${device.deviceNetworkId} 0x${endpoint.toString().padLeft(2,'0')} 0x0006 0x${onOff.toString().padLeft(2,'0')} {}"
sendHubCommand(new HubAction(cmd, Protocol.ZIGBEE))
}
// Helper to send multiple Zigbee commands
private void sendZigbeeCommands(List<String> cmds) {
cmds.each { cmd ->
sendHubCommand(new HubAction(cmd, Protocol.ZIGBEE))
}
}
