Release: Sonoff Tasmota RGB Light Driver (MagicHome, H801)

Building on the simple switch driver that I recently released, here's a driver for an RGB light controller running the Sonoff Tasmota firmware. This firmware can be loaded on various ESP-based RGB controllers, such as MagicHome or H801 (the latter of which can be bought for under $10). All of my development and testing has been using the H801, but the interface should be the same for all RGBW controllers. After installing the driver, you simply need to set the connection preferences with the IP address, port, and authentication info (if necessary).

In addition to the standard commands, the driver has a custom command called setColorDirect that accepts a string. The value is passed directly to the Color command of the Tasmota device. This supports multiple arguments, such as a comma separated list of decimal RGB values, hex values in one of the following forms (RRGGBB, RRGGBBWW, RRGGBBCCWW, CCWW) or a decimal value of 1 through 12 representing a solid color. A full list of the options can be found here.

// Hubitat driver for controlling Sonoff-Tasmota RGB light
// Version 1.0.0
metadata {
	definition(name: "Sonoff-Tasmota RGB Light", namespace: "DKK", author: "dkkohler") {
		capability "Actuator"
        capability "Color Control"
        capability "Color Temperature"
		capability "Refresh"
        capability "Switch"
        capability "Switch Level"
		capability "Light"
        capability "ColorMode"
        
        command "setColorDirect", ["String"]
	}

	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)
		}

		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)
		}
	}
}

def parse(String description) {
	def message = parseLanMessage(description)
	def isParsed = false;

	// parse result from current formats
	def resultJson = {}
	if (message?.json) {
		// current json data format
		resultJson = message.json
        log.debug resultJson
	}

	// determine switch state
	if ((resultJson?."POWER" in ["ON", 1, "1"])) {
		setSwitchState(true)
	}
	else if ((resultJson?."POWER" in ["OFF", 0, "0"])) {
		setSwitchState(false)
	}
	else {
		log.error "Unable to parse POWER state"
	}
    
    // determine dimmer level
	if (resultJson?."Dimmer" != null) {
        def level = resultJson."Dimmer".toInteger()
		setLevelState(level)
	}
    
    // determine HSB value and color mode
	if (resultJson?."HSBColor" != null) {
        def hsb = resultJson."HSBColor".split(',')
		setHsbState(hsb[0].toInteger(), hsb[1].toInteger(), hsb[2].toInteger())
	}
        
    // determine color temperature
	if (resultJson?."CT" != null) {
        def ct = resultJson."CT".toInteger()
        setColorTemperatureState(((ct - 153) * (30000-1)/(500-153)).toInteger())
	}
}

def setColorModeState(String mode) {
	sendEvent(name: "colorMode", value: mode);
}

def setSwitchState(Boolean on) {
	sendEvent(name: "switch", value: on ? "on" : "off")
}

def setHsbState(Integer h, Integer s, Integer b) {
    // Convert H from 0:255 to 0:360
    h = (h*(360/255)).toInteger()
    
    // If HSB all zero, likely in color temperature mode (or off, but we handle them the same way)
    // Update last H and S values so they can be restored
    if ((h == 0) && (s == 0) && (b == 0)) {
        setColorModeState("colorTemperature")
    } else {
        setColorModeState("color")
        state.lastHue = h
        state.lastSaturation = s
    }
    
    sendEvent(name: "hue", value: h);
    sendEvent(name: "saturation", value: s);
}

def setLevelState(Integer level) {
   sendEvent(name: "level", value: level); 
}

def setColorTemperatureState(Integer value) {
	sendEvent(name: "colorTemperature", value: value)
}
        
def push() {
	sendCommand("Power", "Toggle")
}

def on() {
	sendCommand("Power", "On")
}

def off() {
	sendCommand("Power", "Off")
}

def poll() {
	sendCommand("State", null)
}

def refresh() {
	sendCommand("State", null)
}

def setLevel(value) {
    if (value == null) return
    sendCommand("Dimmer", value)
}

def setHsb(Integer h, Integer s, Integer b) {  
    // if hue or saturation is null, use last known value
    h = ((h == null) ? state.lastHue : h)
    s = ((s == null) ? state.lastSaturation : s)
    
    // If brightness is null, use current level
    // Set to 100 if 0 so light will turn on (otherwise H and S won't be set)
    if (b == null) {
        b = device.currentValue("level").toInteger()
        if (b == 0) b = 100
    }
    // send command, converting H from 0:360 to 0:255
    sendCommand("HsbColor", "${(h*(255/360)).toInteger()},${s},${b}")
}

def setColor(value){
    if (value instanceof Map) {
        def h = value.containsKey("hue") ? value.hue : null
        def s = value.containsKey("saturation") ? value.saturation : null
        def b = value.containsKey("level") ? value.level : null
    	setHsb(h, s, b)
    } else {
        log.warn "Invalid argument for setColor: ${value}"
    }       
}

def setColorDirect(String value) {
    sendCommand("Color", value)
}

def setHue(value) {
    if (value == null) return
    setHsb(value, null, null)
}

def setSaturation(value) {
    if (value == null) return
    setHsb(null, value, null)
}

def setColorTemperature(rawValue) {
    if (rawValue == null) return
    
    // Convert color temperature range from 1:30000 to 153:500
    def scaledValue = 153 + (rawValue * (500-153)/(30000-1)).toInteger()
    sendCommand("CT", scaledValue)
}

private def sendCommand(String command, payload) {
    sendCommand(command, payload.toString())
}

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)

	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
}
2 Likes

Isn’t this @ericm project he has been working on since the beginning of the H801 device?

He has begun porting his SmartLife app and driver which has been working great for me.

What is the difference with this driver?

1 Like

This is for use with devices running the Sonoff-Tasmota firmware, which is more feature-rich than the SmartLife offering. Tasmota offers OTA firmware update, built in web server, timers and schedules, MQTT, Hue emulation, etc.

If you're happy with your current setup, I'd say stick with it. This just offers people another option. I have found the Tasmota firmware to be more stable, but YMMV.

1 Like

Can you share a step -by-step setup process? I have an extra h801 that is currently flashed but I can upload via a web browser a new firmware. Very interested in seeing what I could do.

1 Like

Take a look here for flashing instructions. I'm not the creator of the Tasmota firmware, just a user who desired Hubitat integration.

Any idea if we can upload Tasmota directly from the SmartLife firmware upload page? I've got 13 of these, not sure I want to physically reflash all of them.

1 Like

I'm not 100% sure, but I'd imagine you should be able to upload the Tasmota firmware using that method. I actually hadn't realized that the SmartLife firmware provided a web based update method.

1 Like

Welp.. I don't know why I expected it to remember my wifi credentials after I uploaded the Sonoff.bin file to it. Now my controller outside is stuck in WiFi purgatory. I'll have to read more into it.
But don't just grab the firmware and upload through the firmware page of the SmartLife config page.

Yeah, that would have been far too easy! The device should be broadcasting it's own hotspot that you can connect to and setup the wifi again.

Regarding the H801, one thing also to note is that some devices only have 512k flash, which isn't enough for the full Tasmota firmware image. I'm not sure what will happen in this case if you try to flash.

Oh good point. I didn't check to see if it threw up an AP.
It was flashing the LEDs, maybe a good sign that there's life still.

So it did work I was able to connect to the tasmota AP and do basic configuration but I couldn't get the hue integration working. It gets stuck at creating a room and asking me to press the configure button on the hue bridge... That doesn't exist lol. Trying the hubitat driver next. Will do some more research to decide whether I'm keeping it or not.

Interesting. I had the Hue hub emulation working with my Sonoff 4ch Pro, but I haven't tried it with the H801. The direct http access that my driver uses is probably much more efficient anyway.

Messed with it some more last night. I was able to get your Hubitat driver to work. On/Off, Level, and direct color (Hex) seemed to work. I have yet to test if RM can set a color properly.

Excellent, let me know how RM works for you.

1 Like

The only thing would be missing is the programs you can create. I leverage these extensively for lighting effects. Also Eric is almost finished with the SmartLife RGBw application and drivers.

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.