Sonoff B1 RGBCW WiFi Bulb w/ Sonoff-Tasmota

Here’s a solution for integrating a Sonoff B1 bulb into OpenHAB. Consider it a work in progress, it’s functional as it stands but I would welcome any and all feedback/improvements.
B1 support in Sonoff-Tasmota is still in development so things will probably change there too, although probably extending features rather than breaking changes.

What’s needed:

  • Sonoff B1 WiFi Bulb(s), flashed with Sonoff-Tasmota, set up with a unique MQTT root topic (“light1” in this example), and connected to your WiFi network and MQTT server.
  • MQTT server such as Mosquitto.
  • OpenHAB installation. (I’m working with 2.1 here)
  • MQTT binding and action.
  • JSONPATH transformation.

The B1 bulbs are RGBCW LED lamps of moderate brightness that can be had for <$20 from iTead. Inside the lamp is an array of RGB LEDs along with Cool White and Warm White LEDs that can be mixed in any amount desired. They’re based on the ESP8285, a stablemate of the ever-popular ESP8266 WiFi SoC. I have never used the eWeLink app that iTead offers because Sonoff-Tasmota and MQTT provides a far more flexible solution. The colour output string is expressed as 5 bytes of hex in the format RRGGBBCCWW, which presents a slight problem to integrate with the controls and formats provided by OpenHAB.

Items:

Dimmer dBedroom_Lamp "Bedroom Lamp" <dimmablelight> ["Lighting"] {mqtt=">[MQTT:cmnd/light1/DIMMER:command:*:default],<[MQTT:stat/light1/RESULT:state:JSONPATH($.Dimmer)]",autoupdate="false"}
Switch sBedroom_Lamp "Bedroom Lamp Switch" <light> (gAllLights) {mqtt=">[MQTT:cmnd/light1/POWER:command:*:default],<[MQTT:stat/light1/RESULT:state:JSONPATH($.POWER)]",autoupdate="false"}
Color cBedroom_Lamp "Bedroom Lamp Colour" <colorlight> {autoupdate="false"}
Dimmer ctBedroom_Lamp "Bedroom Lamp Colour Temperature" <light> {autoupdate="false"}
String mqBR_Lamp_Colour "Bedroom Lamp Colour MQTT Target" <colorlight> {mqtt=">[MQTT:cmnd/light1/COLOR:command:*:default],<[MQTT:stat/light1/RESULT:state:JSONPATH($.Color)]",autoupdate="false"}

Here we see the main controls exposed to MQTT, Dimmer, Power and Color. Additionally the CT (colour temperature) command is used via the action in the rules:

import java.awt.Color
import java.util.List

rule "Bedroom Lamp Colour Return"
when
	Item mqBR_Lamp_Colour changed
then
	var String RGB = mqBR_Lamp_Colour.state.toString
	var String red_s = RGB.charAt(0).toString + RGB.charAt(1).toString
	var Number red = Integer::parseInt(red_s, 16)
	var String green_s = RGB.charAt(2).toString + RGB.charAt(3).toString
	var Number green = Integer::parseInt(green_s, 16)
	var String blue_s = RGB.charAt(4).toString + RGB.charAt(5).toString
	var Number blue = Integer::parseInt(blue_s, 16)
	val List<Float> hsb = Color.RGBtoHSB(red.intValue, green.intValue, blue.intValue, null)
	var float hue = hsb.get(0)
	hue *= 360
	var float sat = hsb.get(1)
	sat *= 100
	var float bri = hsb.get(2)
	bri *= 100
	val String hsbString = hue + "," + sat + "," + bri
	var HSBType HSB = new HSBType(hsbString)
	cBedroom_Lamp.postUpdate(HSB)
	var String ww_s = RGB.charAt(8).toString + RGB.charAt(9).toString
	var Number ww = Integer::parseInt(ww_s, 16)
	var String cc_s = RGB.charAt(6).toString + RGB.charAt(7).toString
	var Number cc = Integer::parseInt(cc_s, 16)
	var Number ct
	if (ww == 0 && cc == 0) {
		ct = 50
	} else if (ww == 0) {
		ct = 0
	} else if (cc == 0) {
		ct = 100
	} else {
		ct = ww / (ww + cc) * 100
	}
	ctBedroom_Lamp.postUpdate(ct)
end

rule "Bedroom Lamp Colour Output"
when
	Item cBedroom_Lamp received command
then
	if (receivedCommand instanceof HSBType) {
    	var red_n = receivedCommand.red * 2.55
    	var green_n = receivedCommand.green * 2.55
    	var blue_n = receivedCommand.blue * 2.55
	  	var String hex = Integer::toHexString(red_n.intValue)
		if (hex.length < 2) hex = "0" + hex
		var String output = hex
		hex = Integer::toHexString(green_n.intValue)
		if (hex.length < 2) hex = "0" + hex
		output = output + hex
		hex = Integer::toHexString(blue_n.intValue)
		if (hex.length < 2) hex = "0" + hex
		output = output + hex
		var String whites = mqBR_Lamp_Colour.state.toString
		var String whitelevels
		try {
			whitelevels = whites.charAt(6).toString + whites.charAt(7).toString + whites.charAt(8).toString + whites.charAt(9).toString
		} catch(Throwable t) {
			whitelevels = "0000"
		}
	  	logInfo("BRColour","BR Colour: " + output + whitelevels)
		mqBR_Lamp_Colour.sendCommand(output + whitelevels)
  	}
  	else if (receivedCommand == ON){
		sBedroom_Lamp.sendCommand(ON)
  	}
  	else if (receivedCommand == OFF){
		sBedroom_Lamp.sendCommand(OFF)
  	}
end

rule "Bedroom Colour Temperature"
when
	Item ctBedroom_Lamp received command
then
	val Number ctValue = (receivedCommand as Number * 3.47) + 153
	publish("MQTT","cmnd/light1/CT",ctValue.intValue.toString)
end

Here we see the engine room of the integration. The “Colour Return” rule parses the string read from the MQTT status reports and feeds it back into the HSBType item representing colour and the PercentType item representing colour temperature.
The “Colour Output” rule takes the colour commands from the colourpicker and converts them into RGBCW strings for the bulb.
Finally the “Colour Temperature” rule just translates the PercentType command from the Dimmer widget into a CT value that the bulb understands.

The bulb is quite smart with the way it responds to the various commands. Of course if you send OFF and ON it will resume at the same colour and brightness it was at, but you can also send Dimmer commands and it will scale up or down both the colour LEDs and the white LEDs by the same amount.
You can also independently control the colour and white LEDs using either the colourpicker or the colour temperature slider.
You could also

publish("MQTT","cmnd/light1/Color", <Your String Here>)

to control the colour totally manually. (eg. “FF00C17F7F”)

Sitemaps:

Switch item=sBedroom_Lamp label="Lamp [%s]"
Slider item=dBedroom_Lamp label="Dimmer [%d]"
Slider item=ctBedroom_Lamp label="Colour Temp [%.1f]"
Colorpicker item=cBedroom_Lamp label="Lamp Colour [%s]"

Straightforward. Just remember to tap on the colourpicker rather than dragging your finger around, otherwise it tends to spam MQTT messages and things can get overwhelmed.

Some other useful commands exist which I will look to integrate soon, such as automatic fading of configurable speed, wake up fading, etc.

Overall, a pretty great set of features in an open and accessible framework and at a very low price. A+

5 Likes

One more firmware - ESPurna, i’m not test it.

Thank you! You have saved me a lot of time.

After rebooting OpenHAB, the state will be unknown. I think here you can add:

<[MQTT:tele/light1/STATE:state:JSONPATH($.POWER)]

When using the sBedroom_Lamp switch, the error is in the log, because in the returned value there is no information about the dimmer (<[MQTT:stat/light1/RESULT:state:JSONPATH($.Dimmer)]) → stat/light1/RESULT = {“POWER”:“OFF”}.

Have you already realized something new?

Hello,

Anyone of you is able to get the yellow color from the B1 lamp?

I started with this example a couple hours ago to get my H801 module working.

This is what I created in the end

Items

Dimmer h801_W1 "H801 W1" <dimmablelight> ["Lighting"]
Dimmer h801_W2 "H801 W2" <dimmablelight> ["Lighting"]
                                       
Color  h801_RGB "H801 RGB" <colorlight>
                                    
String MQh801 "H801 MQTT Endpoint" {
        mqtt=">[ak:cmnd/h801-alpha/COLOR:command:*:default],<[ak:stat/h801-
alpha/RESULT:state:JSONPATH($.Color)]"
}

rules

import java.awt.Color
import java.util.List
                      
rule "H801 MQTT Input"
when
    Item MQh801 received update
then
    logInfo("h801","input triggered")
    var String RGBWW = MQh801.state.toString

    var String Rs  = RGBWW.substring(0,2)
    var String Gs  = RGBWW.substring(2,4)
    var String Bs  = RGBWW.substring(4,6)
    var String W1s = RGBWW.substring(6,8)
    var String W2s = RGBWW.substring(8,10)

    var Number R = Integer::parseInt(Rs, 16)
    var Number G = Integer::parseInt(Gs, 16)
    var Number B = Integer::parseInt(Bs, 16)
    var Number W1 = Integer::parseInt(W1s, 16) * 100 / 255
    var Number W2 = Integer::parseInt(W2s, 16) * 100 / 255

    val List<Float> hsb = Color.RGBtoHSB(R.intValue, G.intValue, B.intValue, null)
    var float hue = hsb.get(0)*360
    var float sat = hsb.get(1)*100
    var float bri = hsb.get(2)*100

    var HSBType HSB = new HSBType(hue+","+sat+","+bri)
    logInfo("h801-input","updating hsb: "+hue+","+sat+","+bri)
    h801_RGB.postUpdate(HSB)
    

    logInfo("h801-input","updating w1: "+W1)
    h801_W1.postUpdate(W1)
    logInfo("h801-input","updating w2: "+W2)
    h801_W2.postUpdate(W2)
end
                      
rule "H801 RGB Output"
when
    Item h801_RGB received command
then
    logInfo("h801","rgb output triggered")
    var String output = ""
    // collect white levels
    var String whites = MQh801.state.toString
    var String whitelevels
    try {
            whitelevels = whites.substring(6,10)
    } catch(Throwable t) {
            whitelevels = "0000"
    }

    if (receivedCommand instanceof HSBType) {
            var Rn = receivedCommand.red * 2.55
            var Gn = receivedCommand.green * 2.55
            var Bn = receivedCommand.blue * 2.55

            output = String.format("%02x%02x%02x", Rn.intValue, Gn.intValue, Bn.intValue)

    } else if (receivedCommand == ON){
            output = "FFFFFF"
    } else if (receivedCommand == OFF){
            output = "000000"
    }

    logInfo("MQh801","sending: " + output + whitelevels)
    MQh801.sendCommand(output + whitelevels)
end

rule "H801 W1 Output"
when
    Item h801_W1 received command
then
    logInfo("h801","w1 output triggered")
    var String RGBW1W2 = MQh801.state.toString
    var String RGB
    try {   
            RGB = RGBW1W2.substring(0,6)
    } catch(Throwable t){
            RGB = "000000"
    }
    
    // collect missing white level
    var String W2
    try {   
            W2 = RGBW1W2.substring(8,10)
    } catch(Throwable t) {
            W2 = "00"
    }
    
    var String W1
    if ( receivedCommand == OFF ){
            W1 = "00"
    } else if ( receivedCommand instanceof Number ){
            var rcvd = receivedCommand.intValue * 255 / 100
            W1 = String.format("%02x", rcvd)
    }
    
    var String output = RGB + W1 + W2
    logInfo("MQh801","sending: " + output)
    MQh801.sendCommand(output)
end

rule "H801 W2 Output"
when
    Item h801_W2 received command
then
    logInfo("h801","w2 output triggered")
    var String RGBW1W2 = MQh801.state.toString
    var String RGB
    try {   
            RGB = RGBW1W2.substring(0,6)
    } catch(Throwable t){
            RGB = "000000"
    }
    
    // collect missing white level
    var String W1
    try {   
            W1 = RGBW1W2.substring(6,8)
    } catch(Throwable t) {
            W1 = "00"
    }
    
    var String W2
    if( receivedCommand == OFF ){
            W2 = "00"
    } else if ( receivedCommand instanceof Number ){
            var rcvd = receivedCommand.intValue * 255 / 100
            W2 = String.format("%02x", rcvd)
    }
    
    var String output = RGB + W1 + W2
    logInfo("MQh801","sending: " + output )
    MQh801.sendCommand(output)
end

The key improvements are:

  • use String.format("%02X",...) to format into always-two-digit hex strings
  • use .substring(0, 6) to split the MQTT values into the required components
  • Separate the W1 and W2 channels out so they can be controlled individually (particular to my usecase)

I hope someone finds this useful

Yesterday I tried (I rarely go where they are installed), I could not get a yellow color.

Thanks for your try. So, the problem is the lamp itself and not only mine…

In my opinion it’s not a good product as it seems. Maybe version 2 :slight_smile:

Bye

I try to replicate this setup but the rule file gives an error when loading. It stops at the import row.

[el.core.internal.ModelRepositoryImpl] - Configuration model ‘sonoff.rules’ has errors, therefore ignoring it: [25,1]: missing EOF at ‘import’

What do I have to do?

I am using Tasmotta firmware 5.10.0 which gives out the following output,

21:10:00 MQT: stat/sonoff-8BDB90/RESULT = {"POWER":"ON","Dimmer":22,"Color":"000000191E"}
21:10:00 MQT: stat/sonoff-8BDB90/RESULT = {"POWER":"ON"}
21:10:00 MQT: stat/sonoff-8BDB90/POWER = ON

Notice, there are two RESULT outputs. Consequently, the rule fails like so

[ERROR] [.mqtt.internal.MqttMessageSubscriber] - Error processing MQTT message.
org.openhab.core.transform.TransformationException: Invalid path '$.Color' in '{"POWER":"OFF"}'
        at org.openhab.core.transform.TransformationHelper$TransformationServiceDelegate.transform(TransformationHelper.java:67) [207:org.openhab.core.compat1x:2.2.0]
        at org.openhab.binding.mqtt.internal.MqttMessageSubscriber.processMessage(MqttMessageSubscriber.java:138) [242:org.openhab.binding.mqtt:1.11.0]
        at org.openhab.io.transport.mqtt.internal.MqttBrokerConnection.messageArrived(MqttBrokerConnection.java:556) [208:org.openhab.io.transport.mqtt:1.11.0]
        at org.eclipse.paho.client.mqttv3.internal.CommsCallback.deliverMessage(CommsCallback.java:475) [208:org.openhab.io.transport.mqtt:1.11.0]
        at org.eclipse.paho.client.mqttv3.internal.CommsCallback.handleMessage(CommsCallback.java:379) [208:org.openhab.io.transport.mqtt:1.11.0]
        at org.eclipse.paho.client.mqttv3.internal.CommsCallback.run(CommsCallback.java:183) [208:org.openhab.io.transport.mqtt:1.11.0]
        at java.lang.Thread.run(Thread.java:748) [?:?]

Yeah I’m seeing this too. Anyone got a good solution? I’m thinking of posting an issue in Tasmota for a consistent format

I have many sonoff module types and have flashed them all with tasmota, but I have been unable to flash the B1 bulbs, I suspect I am failing to get them into flash mode, or the process is subtly different than the ESP 8266 based devices.

Any ideas of what I may be doing wrong?

Regards

Paul

That would be good! I wanted to do this, but my English is very bad.

No, it’s not different. Did you connect GPIO0 to ground when you connected power?

Hey @Carywin,

you can simplify a good part of your first rule with this line:

var HSBType hsb = HSBType.fromRGB(red, green, blue)

Best!

Oh nice, thanks! I’ll work this into it

Moved to Light Color RGB/RGBW/RGBCW to and from HSB+Dimmer Conversion

Hello!

Have you made any progress with B1? I’ve been unable to make it enter firmware flashing mode too, and I’m out of ideas what to try. I’ve already tried switching TX and RX, connecting GPIO0 and GND for a second, 2 seconds, holding it connected all the time, used ESPTool, Arduino IDE, PlatformIO, but it just doesn’t connect. I’ve read that some people had partial success (flashed 2 out of 4 devices), but they don’t know how to reproduce it. And they suspect it’s some kind of timing issue (I’m really not sure what they’ve meant with that). If you’ve managed to flash it, I would really appreciate an info on a ways to do it.

Best regards,
Davor

Yes I have many B1s flashed and working around my home now. When flashing them I connect GPIO0 to GND and it seems to work fine. After the initial flash you can use OTA to keep them updated.

1 Like

Moved to Light Color RGB/RGBW/RGBCW to and from HSB+Dimmer Conversion

I’m getting exceptions when using your rule:

Rule 'Arilux HSB -> RGB': Could not cast NULL to org.eclipse.smarthome.core.library.types.HSBType; line 14, column 15, length 27

It seems like my lamp is null. The item is defined like this:

Color TischColor "Tisch" <light> (LR,gLight) [ "Lighting" ]

and I modified the rule like this:

rule "Arilux HSB -> RGB"
when
    Item TischColor received command
then
    var r = ((TischColor.state as HSBType).getRed * 255 / 100).intValue
    var g = ((TischColor.state as HSBType).getGreen * 255 / 100).intValue
    var b = ((TischColor.state as HSBType).getBlue * 255 / 100).intValue
    var rgb = String.format("%02x%02x%02x", r, g, b)

    TischColorString.sendCommand(rgb)
end

What am I doing wrong?

Hey @DerEnderKeks, nothing wrong with the rule. The error claims that your item is not initialized (NULL). Did you confirm that this is NOT the case? Check your log to see the state of the Item. It might be a good idea to catch this case at the beginning of the rule:

if (TischColor.state == NULL) {
    logInfo("tisch.rules", "Item is null, cancelling...")
    return;
}

This is more of a general best practice thing and I’d recommend to always consider such a case.

Always think about which states an Item can have and how you want to react to those. Do you maybe need to initialize an Item during startup or do you want to persist its state to be restored on Startup? Almost all rule files of mine have such a rule for Items that are not automatically initialized through a Binding:

rule "Init: Kodi"
when
    System started
then
    createTimer(now.plusSeconds(120)) [ |
        if (Kodi_Summary.state == NULL) Kodi_Summary.postUpdate("(unknown) ⁉")
        if (Some_Counter.state == NULL) Some_Counter.postUpdate(0)
        // and so on
    ]
end

The timer ensures that restoreOnStartup or Binding-wise initialization have time to initialize the Item for you