Release: Sonoff Tasmota Switch Driver

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.

4 Likes

I can't seem to get this driver to actually work. Has anyone used this driver? Is there something that I'm missing here? This is what I keep getting in the logs.

[dev:1323](http://192.168.1.12/logs#dev1323)2018-12-30 12:27:45.297 pm [error](http://192.168.1.12/device/edit/1323)...and raw body: {"WARNING":"Need user=&password="}

[dev:1323](http://192.168.1.12/logs#dev1323)2018-12-30 12:27:45.295 pm [error](http://192.168.1.12/device/edit/1323)can not parse result with header: HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: -1 Access-Control-Allow-Origin: * Content-Length: 54 Connection: close

[dev:1323](http://192.168.1.12/logs#dev1323)2018-12-30 12:27:45.290 pm [debug](http://192.168.1.12/device/edit/1323)[WARNING:Need user=&password=]

[dev:1323](http://192.168.1.12/logs#dev1323)2018-12-30 12:27:45.215 pm [debug](http://192.168.1.12/device/edit/1323)sendCommand(Powernull:On) to device at 192.168.1.39:80

I'm using it with a few devices running the standard Sonoff-Tasmota firmware. Do you have your devices configured to require a username and password? If so, have you set them in the Hubitat device preferences? My Tasmota devices aren't setup to require authentication, so it's possible there's a bug in that portion of the code.

Ah...yes, I do. One of them is exposed to the internet through my router as a hub reset fail-safe device. So, it has to have authentication enabled. Thanks!

Just wanted to add something here in case someone else runs into this.

For a normal sonoff basic (single channel) on tasmota 6.4.1 (or higher presumably) you need to set the Switch parameter in Hubitat to 1. Leaving it blank does nothing and the output is not zero based.

Device Settings:
IP Address: your.sonoff.ip.here
Port: 80 unless you know what you're doing
Switch Number: 1
Username: blank unless set in Tasmota
Password: blank unless set in Tasmota

1 Like

I just ran into an issue... the driver isn't reporting the status correctly back to Hubitat so I cannot set a tile for it in the Dashboard.

This is for a Sonoff Basic. Is there an updated driver that would potentially have a fix?

Here is the error from the log. Seems to be expecting a different JSON payload maybe?

This is the returned payload by the Sonoff Basic

{
POWER: "ON"
}

Any thoughts?

Never mind, my brain isn't working tonight.

I'm a web developer who mostly does PHP and JS so sometimes this ruby-esque syntax messes with me.

Since I'm using a sonoff basic, the parse function is failing because the returned JSON payload doesn't include a power switch number. Changing the parse function to omit the switch number fixes the error.

If anyone needs to use this with a Sonoff basic, you'll need to remove the instances of $switchNumber from the bottom few if/else statements near the end of the def parse() function.

Thanks for the feedback, I'll update the code to handle that case.

Any motion on this?
I have a 3 relay outlet I'd like to tasmotasize and bring in..
Thanks!

EDIT: OK I see that there is a switch number field. Maybe that will work for me.
I have to flash the thing first to figure out what firmware config works with it.

I haven't made any recent updates to the driver, as it's been working well for me as-is. Let me know if you have any questions when setting it up.

I actually pointed it towards the rbg bulb I just flashed to see if I could at least turn it on and off. It worked great. I understand the switch number, which might suit my needs perfectly.

Since this has a username and password, is it using MQTT?

Thanks!

Not MQTT just using the HTTP interface, but you can configure the device to require a username and password over HTTP for restricted access.

I believe that feature is disabled by default, so you should be able to leave those fields blank.

1 Like

Thanks for the clarification.

I look forward to using this!

Hi Noob here; I've got a few SONOFF Basics - Flashed with Tasmota - working for various devices - they are switching correctly but showing no statuses. The driver I have contains this:

// 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"
}

Is this the correct one? I am getting the same Log as darrenspence showed earlier.

Is it something I didn't do when flashing the units with tasmota?

Any help would be gratefully received as Alexa is bugging me by telling me its not responding.... when it has!

Can you try removing $switchnumber from those 2 lines, it seems like the single switch ones don't use that.

Edit: Scratch that, it actually needs to be removed from the Parse section of the code. I can look at it tomorrow if you can't get it working tonight.

I added the following lines at line 72. Fixes the issue completely.

else if ((resultJson?."POWER" in ["ON", 1, "1"])) {
setSwitchState(true)
}
else if ((resultJson?."POWER" in ["OFF", 0, "0"])) {
setSwitchState(false)
}

Thanks both of you.
corerootedxb - that worked for ON but doesn't set the state to off for OFF??? I'm confused as the code looks the same. This is what the code looks like now:

// 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 if ((resultJson?."POWER" in ["ON", 1, "1"])) {
	setSwitchState(true)
}
else if ((resultjson?."POWER" in ["OFF", 0, "0"])) {
	setSwitchState(false)
}
else {
	log.error "can not parse result with header: $message.header"
	log.error "...and raw body: $message.body"
}

}

BTW - Why all the double brackets in the "else if" statements?

Hmmm, take a look at what the Sonoff is sending back when the off command gets received. This is what shows up in my logs:

I fixed it hastily and never went back to clean up the changes I made. I literally just did a copy and paste, got it working and moved on. I gots other crap to break, erm, I mean fix!!! :wink:

[Edit: If it's just a Sonoff Basic, you can remove all references to $switchNumber and it should work just the same. I left it in my copy of the driver as I AM planning on getting some 4 gang controllers eventually.]

Shows this:

Hmmmm, that's weird. Nothing is really standing out to me in your code. Could you copy the original driver again, make your changes and then redeploy it? Maybe there's a weird character of something in there we aren't seeing.