Here is a very simple driver for controlling a Sonoff switch running the Tasmota firmware. This is based off a SmartThings device driver developed by Brett Sheleski, which I ported to Hubitat and modified to support multi-switch devices. I am currently using this to control a single switch on a Sonoff 4CH Pro.
To add a device, create a virtual device with this driver. Edit the device settings and enter in the IP address and HTTP port of your Sonoff device. If you're using a device with multiple outputs, set the switch number parameter, otherwise leave it blank.
The driver does not automatically poll the switch to synchronize with state changes that occur outside Hubitat. If you wish to keep the state in sync, you can use Rule Machine to periodically Refresh the device.
// Original driver developed by Brett Sheleski.
// Ported to Hubitat and updated to include switchNumber by dkkohler.
metadata {
definition(name: "Sonoff-Tasmota", namespace: "DKK", author: "dkkohler", ocfDeviceType: "oic.d.smartplug") {
capability "Actuator"
capability "Switch"
capability "Momentary"
capability "Polling"
capability "Refresh"
}
preferences {
section("Sonoff Host") {
input(name: "ipAddress", type: "string", title: "IP Address", displayDuringSetup: true, required: true)
input(name: "port", type: "number", title: "Port", displayDuringSetup: true, required: true, defaultValue: 80)
input(name: "switchNumber", type: "number", title: "Switch Number", displayDuringSetup: true, required: false, defaultValue: null)
}
section("Authentication") {
input(name: "username", type: "string", title: "Username", displayDuringSetup: false, required: false)
input(name: "password", type: "password", title: "Password (sent cleartext)", displayDuringSetup: false, required: false)
}
}
}
String testLegacyInput() {
String prefix = 'index:15, mac:5CCF7FBD413C, ip:C0A80206, port:0050, requestId:9f55a327-be15-43fb-91ce-c04a035a3217'
String multiBody = '''RESULT = {"POWER":"ON"}
POWER = ON'''
def contentLength = multiBody.length()
String multiHeaders = """HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: $contentLength
Connection: close"""
String multilineString = prefix + ', headers:' + multiHeaders.bytes.encodeBase64() + ', body:' + multiBody.bytes.encodeBase64()
return multilineString
}
def parse(String description) {
def message = parseLanMessage(description)
// parse result from current and legacy formats
def resultJson = {}
if (message?.json) {
// current json data format
resultJson = message.json
log.debug resultJson
}
else {
// legacy Content-Type: text/plain
// with json embedded in body text
def STATUS_PREFIX = "STATUS = "
def RESULT_PREFIX = "RESULT = "
if (message?.body?.startsWith(STATUS_PREFIX)) {
resultJson = new groovy.json.JsonSlurper().parseText(message.body.substring(STATUS_PREFIX.length()))
}
else if (message?.body?.startsWith(RESULT_PREFIX)) {
resultJson = new groovy.json.JsonSlurper().parseText(message.body.substring(RESULT_PREFIX.length()))
}
}
// consume and set switch state
if ((resultJson?."POWER$switchNumber" in ["ON", 1, "1"])) {
setSwitchState(true)
}
else if ((resultJson?."POWER$switchNumber" in ["OFF", 0, "0"])) {
setSwitchState(false)
}
else {
log.error "can not parse result with header: $message.header"
log.error "...and raw body: $message.body"
}
}
def setSwitchState(Boolean on) {
log.info "switch is " + (on ? "ON" : "OFF")
sendEvent(name: "switch", value: on ? "on" : "off")
}
def push() {
sendCommand("Power$switchNumber", "Toggle")
}
def on() {
sendCommand("Power$switchNumber", "On")
}
def off() {
sendCommand("Power$switchNumber", "Off")
}
def poll() {
sendCommand("Power$switchNumber", null)
}
def refresh() {
sendCommand("Power$switchNumber", null)
}
private def sendCommand(String command, String payload) {
log.debug "sendCommand(${command}:${payload}) to device at $ipAddress:$port"
if (!ipAddress || !port) {
log.warn "aborting. ip address or port of device not set"
return null;
}
def hosthex = convertIPtoHex(ipAddress)
def porthex = convertPortToHex(port)
device.deviceNetworkId = "$hosthex:$porthex:$switchNumber"
def path = "/cm"
if (payload){
path += "?cmnd=${command}%20${payload}"
}
else{
path += "?cmnd=${command}"
}
if (username){
path += "&user=${username}"
if (password){
path += "&password=${password}"
}
}
def result = new hubitat.device.HubAction(
method: "GET",
path: path,
headers: [
HOST: "${ipAddress}:${port}"
]
)
return result
}
private String convertIPtoHex(ipAddress) {
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
return hex
}
private String convertPortToHex(port) {
String hexport = port.toString().format('%04x', port.toInteger())
return hexport
}
This is a very basic driver that suits my needs. If you're looking for something with more bells and whistles, check out Brett's Github repo, which has some more sophisticated drivers that could easily be ported to Hubitat.