Custom firmware ESPHome-Xiaomi_bslamp2

A few markers, @thatthing

  • Just a little thing, but the interrupt pin is pulled low, not high, when the panel has an event available.

  • For talking to the I2C bus, the cleanest implementation is to follow the ESPHome-way of doing this. This means that you write a component that dervies from the esphome::i2c::I2CDevice class (see the code on github]. It works with the I2C bus(es) as defined in the device yaml configuration file. This parent class provides some useful functions to work with the I2C bus, and it has a built-in option for handling communication timeouts.

  • I was wondering if the PollingComponent was the correct way to handle the interrupt pin state detection. Turns out it is, and that setting up interrupt handlers is not the way to go with ESPHome. So that was a good learning point for me.

  • About comparing arrays: since we’re looking at C++ and not plain C, I would opt for using the std::array class (some docs on this one). You can use the == equality operator for two std::array objects to test if they match. Another way would of course be to use C-style arrays as you did, and write a little loop that compares the bytes one at a time yourself.

  • For parsing the bytes as received from the front panel, I would probably not setup a mapping of all possible messages, but I would write a parser class that would implement more of a decision tree to find the message type. The structure would be a starter method that would look at the first byte, to call a method for processing the next byte or bail out with an error if the message cannot be parsed. Every followup function would do the same, until eventually a conclusion is reached about the resulting message code. There are other ways to implement such parser, but the important concept here would be the use of a decision tree instead of a lookup map.Using a tree takes the least steps to get from an input to a resulting message code. I made a drawing to show the paths for this:

As you can see, I suggested a computation based on the last two bytes for the slider’s touch and release events. There is a distinct pattern there, which could be used to translate the two bytes into the correct slider value. Forking the decision tree further would be feasible too of course. It might even be easier to detect the invalid cases that way.

Of course, in such tree-based parser, you don’t need four functions in succession to handle the first four bytes. The first function can check for the first four bytes to be 04, 04, 01, 00, and then call the next function or bail out based on the fifth byte. In pseudo code:

enum EventType {
    UnknownMessage,
    PowerTouch,
    PowerRelease,
    ColorTouch,
    ColorRelease,
    Level1Touch,
    Level1Release,
    // etc....
};

EventType parse(FrontPanelMessage message) {
    if (message[0] != 0x04) return UnknownMessage;
    if (message[1] != 0x04) return UnknownMessage;
    if (message[2] != 0x01) return UnknownMessage;
    if (message[3] != 0.00) return UnknownMessage;
    if (message[4] == 0x01) return parse_power_button_event(message);
    if (message[4] == 0x02) return parse_color_button_event(message);
    if (message[4] == 0x03) return parse_slider_touch_event(message);
    if (message[4] == 0x04) return parse_slider_release_event(message);
    return UnknownMessage;
}

EventType parse_power_button_event(FrontPanelMessage message) {
    if (message[5] == 0x01) return parse_power_button_touch_event(message);
    if (message[5] == 0x02) return parse_power_button_release_event(message);
    return UnknownMessage;
}

// etc....

As for storing the resulting event: for the slider events, it would be nice to have an actual event object, in which the slider level is stored as an integer value. That makes it easier to work with the resulting event (it prevents the need of a long switch statement to translate the event type to a slider level). Another way to handle this would be to provide a translation function from your code, that takes an event as input (e.g. Level12Touch) and would return its numerical value (12).

This was a bit of a brain dump, but I hope these pointers will help!

1 Like

In crypto we do a constant time array compare as follows:

XOR the arrays with eachother into a third array
AND all the bytes in the third array into a single byte
If that byte is 0, the 2 initial arrays were identical

I know the type of implementation, and I think you meant to OR the third array bytes:

Array 1 : [ 11110000, 10101010 ]
Array 2 : [ 11110000, 10101011 ]
XOR:      [ 00000000, 00000001 ] --> AND = 00000000, OR = 00000001

The two arrays are different, but using AND, the result would be 0, incorrectly indicating identical arrays.
Normally, you’d use a runtime OR-counter while looping over the two array, so you don’t have to generate an actual third array.

For the use case of @thatthing , this way of work is causing unneeded overhead, since the code would always loop over all the bytes in the arrays to come to a conclusion. Perfect for crypto purposes, since you don’t want timing leaks there (hence the constant time requirement).

For simply comparing two arrays using the same kind of XOR approch, I’d do something more efficient like this:

bool are_equal(byte* a, byte* b, size_t n)
{
    for (size_t pos = 0; p < n; p++)
        if (a[pos] ^ b[pos])
            return false;
    return true;
}

yes… or… it appears I am too sick right now to make code suggestions

1 Like

I, like others in this thread, don’t own any Yeelight products but had to say you are doing an absolutely fantastic job of reverse engineering this device @mmakaay! I found this thread after we were alerted on Twitter that Yeelight was silently removing LAN control by request of Xiaomi and wanted to see if any intrepid hackers on our forums were aware and looking into the problem. You have gone above and beyond to say the least. I will hold this thread up in the future as an excellent example of taking back control of our devices with brute force :laughing:! Please keep up the good work!!

4 Likes

Update: We have issued an alert for the local control removal and issued a statement via Twitter. I linked our Twitter thread to this thread and thanked you @mmakaay, hope that’s okay :slightly_smiling_face:



1 Like

Of course it is okay, @robbiet480
Thanks for the shoutout :smiley: !

2 Likes

Great News @robbiet480! You definitely put Xiaomi and Yeelight in the spotlight now :grin: And @mmakaay deserves all the credit! :+1:t2::partying_face:

While playing with the original firmware, I found another good reason for using custom firmware instead. The LAN mode apparently has a requests/minute limit (also mentioned on the Yeelight integration page). When doing a lot of requests (for example when transitioning from one color to the other), this limit is quickly reached and the light will show up as disconnected. At that point, the Home-Assistant logs will show “client quota exceeded”.

Ah THAT’s why I struggled with it, while playing around! Became unavailable out of the blue.

One question, in the default scenes in the Yeelight app, there is a Night mode. I’ve never managed to set the same low light level manually, as when I use the predefined Night mode. Is this possible with your custom code?

That’s why someone came up with the “Music Mode” I think. This tricks the lamp to accept more requests.

Edit; But good riddance! Not a clean way of handling things :+1:t2:

Yes, my plan for night mode is to turn that on when using brightness level 1.
I am working on three distinct lighting modes therefore:

  • RGB (brightness 2 - 255)
  • Night light (brightness 1)
  • White light (using temperature instead of RGB settings)

The Yeelight integration does provide a switch entity for the night light mode BTW. I thought about copying that behavior, but frankly, I do not like this method at all. It provides me with two light switches for the same light and from usability and automation perspectives, I always found it challenging to work with. Therefore I went for the brightness==1 -> night light mode logic.

If somebody does want two switches, then it can of course still be done, by implementing those in the ESPHome configuration. I am writing a light output here, so that can be hooked up to other entities in any way you like.

1 Like

Awesome, I agree with your logic :slight_smile:

1 Like

First things first, thank you for doing this. I think a lot of geeks want to use this. Second, I agree with your arguments/statements, (whatever you want to call them), I like the idea of night mode when brightness is set to 1. Hopefully it will work when you control the light manually.
Please don’t include the switch like the integration. It is very confusing.

1 Like

Ahhhhh… that was fun. After doing a ton of measurements and wondering what was going on with them, I turned around my board and found the culprit.

The green lead popped off the ESP32 pin :laughing:
A should have been connected at B! That explains a lot.

There! Cringeworthy, but more gloop will help (the duct tape was too large for this one)

1 Like

WIth the green output connected back to the board, I did a lot of new RGB measurements. I went ahead on doing this, because the simple implementation (that did already look quite good) does show some irritating flaws at a few spots when transitioning from color to color.
For example red to purple shows red, then quickly light purple, then red again, which then goes towards purple. We no like, so started an effort to get more detailed insight in the outputs for RGB.

My approach for measuring is to use the RGB color circle (as used by Home Assistant) and measure all around this circle at different distances from the middle white point. I numbered the rings from 1 to 7, outward to inward. I measured from red to blue to green and back to red (so rotating anti-clockwise along the ring).

This yielded some interesting results, that tell me that RGB is going to need more code than I envisioned. Check out these beauties:

afbeelding

From left to right is the transitioning from red to blue to green and back to red.
As you can see, these aren’t nice smooth lines that tranlate into clean functions.

The above measurements were done at 100% brightness. I also measured at 1% brightness, which resulted in this output:

afbeelding

That is a very compressed version of the 100% version. The good news here is that I also measured at 50% and that I found that I could generate the same graphs by averaging the 100% and 1% results. So the brightness scaling is linear (as expected, since I’ve seen that all over the place so far).

I also did measurements along at what I called “the 4th ring”. This is ring number 4 from the outside inwards (the preveious measurements were therefore for the 1st ring). At 100% brightness, this showed:

afbeelding

The output is a bit like the 1st ring output, but the shape of the graphs is not a plain scaled version of those for the 1ste ring.
I also measured the 7th ring. This is the small inner right that is right around the middle white point. The results for this ring are:

afbeelding

Now look at that! A whole different look from the earlier graphs.

Based on this, it is clear that computing the RGB output values might not easily be some with some straight forward formulas. I will continue with measuring values on the other rings, hoping that this might reveal some logic that can be used to compute required RGB levels.

If no logic is revealed, then I will simply go for implementing the required data tables to support this.

1 Like

Mmakaay’s seven rings of RGB! (Dante would have laughed…) Peculiar how driving LEDs seems to work, and this gives me new insight to why colours is a bit off in certain products. I can see that you have more than enough to do right now, but did you ever get the second I2C bus to talk? If this is an eeprom, as mentioned earlier, it must be some kind of static memory since it isn’t especially “chatty” during operation of the lamp.

I have not yet tried to talk to the second I2C bus, mainly because I currently don’t have a use for it. Possibly it can be used for storing light presets or so, which can be made operational independently from a connection to Home Assistant through use of the front panel use.
Once I get the core functionality working, I will solder some more wires to the original lamp to see if I can find anything about the operation and use of that bus.

I understand your priorities :+1:t2: Just dwelling in the corner of curiosity when I’m looking at your progress. Xiaomi do often keep a copy of the former version after FW upgrade(gateways anyway) as a possible fallback if something fails during update.

Some great work going on here… Thanks.
Looks as if there is a possibility to split the touch control and use it via HA rather than locally, possibly with ControllerX. That would mean that the lamp could be used as a room controller.

Good timing with Easter this weekend to take one of my ‘NoLAN’ lamps apart and investigate options.

It always struck me that given the size of the lamp, it could be used as the host for other functions and hardware extensions/ add-ons.