[SOLVED] Adding support for Feit WiFi LED bulbs from Costco

I picked up a couple of WiFi LED bulbs at Costco (2 for $30) this week, only to find out they are neither supported in openHAB nor HomeKit. With a little research, I was able to determine that they are rebranded (and slightly modified) tuya bulbs. Since I already had Node-RED set up to allow for better HomeKit support (see Other homekit types through node-red and My node-red + HomeKit + OpenHAB setup) I figured I could use it to add support for these bulbs. So, I installed the tuya-smart node (https://flows.nodered.org/node/node-red-contrib-tuya-smart). I also had to install the colorsys module into node-red for use in converting the color values. Just follow the instructions here to load an additional module: (https://nodered.org/docs/writing-functions.html#loading-additional-modules).

Then, I followed the instructions here to use tuya-cli to find the bulb (https://github.com/AMoo-Miki/homebridge-tuya-lan/wiki/Setup-Instructions) but got no results. I had to modify tuya-cli a bit to get the right information from the Feit bulbs. (I’ve forked it and made my changes available here: https://github.com/rrgeorge/cli .)

Once I got the device id and local key, I added the I was able to set up a “tuya smart” node on Node-RED. Then it was a matter of creating the various functions to communicate with openHAB (and HomeKit) properly.

Reading data from the bulb is fairly straight forward, I created a function node with the following code to get the proper data to the appropriate openHAB items (I have 3, one as a Dimmer for the bulb as a regular light bulb, one as a Dimmer to control the color temperature, and one as Color to control its color:

var bulbData = msg.payload.data;
var bulb = { payload: {} };
var bulbTemp = { payload: {} };
var bulbColor = { payload: {} };

if (bulbData.dps[2] !== undefined) {
    if (bulbData.dps[2] == "white") {
        bulbColor.payload = "NULL";
        bulbTemp.payload = bulbData.dps[4]/255.0*100;
        bulb.payload = (bulbData.dps[3]/255)*100;
        if (bulbData.dps[1] === true && bulbData.dps[3] > 0) {
            bulb.payload = (bulbData.dps[3]/255)*100;
        } else {
            bulb.payload = "OFF";
        }
        
        return [bulb,bulbTemp,bulbColor];
    } else if (bulbData.dps[2] == "colour") {
        bulbTemp.payload = "NULL";
        if (bulbData.dps[1] === true) {
            bulb.payload = "ON";
        } else {
            bulb.payload = "OFF";
        }
        var hue = bulbData.dps[5].substr(6, 4);
        var saturation = bulbData.dps[5].substr(10, 2);
        var brightness = bulbData.dps[5].substr(12, 2);
        bulbColor.payload = parseInt(hue,16) + "," + (parseInt(saturation,16)/255.0*100) + "," + (parseInt(brightness,16)/255.0*100);
    
        return [bulb,bulbTemp,bulbColor];
    }
}

Since the tuya-smart node just repeatedly queries the bulb, I needed to make sure that and changes were only sent to the bulb from commands and not updates. So I sent the raw output from the openhab-in nodes through this function node:

var payload = msg.payload;
var item = msg.item;
msg = { "payload": null };
if (payload.type == "ItemCommandEvent" ) {
    var thePayload = payload.payload;
    if (item == "DiningRoomLight") {
        if (thePayload.type == "OnOff") {
            if (thePayload.value == "ON") {
                msg.payload = { "set": true, "dpsIndex": 1};
            } else {
                msg.payload = { "set": false, "dpsIndex": 1};
            }
            return msg;
        } else if (thePayload.type == "Percent") {
            var v = thePayload.value;
    
            if (v === 0) {
                msg.payload = { "set": 0, "dpsIndex": 3};
            } else {
                var theBrightness = Number.parseInt((v/100) * 255);
                msg.payload = { "set": theBrightness, "dpsIndex": 3};
            }
            return msg;
        }
    } else if (item == "DiningRoomLightColorTemperature") {
        if (thePayload.type == "Percent") {
            msg.payload = { "set": "white", "dpsIndex": 2};
            node.send( msg );
            var theTemp = Number.parseInt((thePayload.value/100) * 255);
            msg.payload = { "set": theTemp, "dpsIndex": 4};
            return msg;
        }
    } else if (item == "DiningRoomLightColor") {
        if (thePayload.type == "HSB") {
            msg.payload = { "set": "colour", "dpsIndex": 2};
            node.send( msg );
            var hsb = thePayload.value.split(",");
            var h = parseInt(hsb[0]);
            var s = parseInt(hsb[1]);
            var v = parseInt(hsb[2]);
            var colorsys = global.get('colorsys');
            var rgb = colorsys.hsvToRgb(h,s,v);
            var r = rgb.r;
            var g = rgb.g;
            var b = rgb.b;
            node.error(rgb);
            node.error(hsb);
            var setVal = ("00" + r.toString(16)).substr(-2) + ("00" + g.toString(16)).substr(-2) + ("00" + b.toString(16)).substr(-2) + ("0000" + h.toString(16)).substr(-4) + ("00" + parseInt(s/100*255).toString(16)).substr(-2) + ("00" + parseInt(v/100*255).toString(16)).substr(-2);
            msg.payload = { "set": setVal, "dpsIndex": 5};
            return msg;
        }
    }
}

As for supporting the bulb in HomeKit, i used the following function node to set the HomeKit node values:

var bulbData = msg.payload.data;
if (bulbData.dps[2] !== undefined) {
if (bulbData.dps[2] == "white") {
    flow.set("colormode","white");
    var colortemp = 2700 + (bulbData.dps[4]/255)*3800;
    
    msg.payload = {
        "On": bulbData.dps[1],
        "Brightness": (bulbData.dps[3]/255)*100,
        "ColorTemperature": Number.parseInt(1000000/colortemp)
    };
    return msg;
} else if (bulbData.dps[2] == "colour") {
    var hue = bulbData.dps[5].substr(6, 4);
    var saturation = bulbData.dps[5].substr(10, 2);
    var brightness = bulbData.dps[5].substr(12, 2);
    flow.set("colormode","colour");
    flow.set('b',(parseInt(brightness,16)/255*100));
    msg.payload = {
        "On": bulbData.dps[1],
        "Hue": parseInt(hue,16),
        "Saturation": (parseInt(saturation,16)/255.0*100),
        "Brightness": (parseInt(brightness,16)/255.0*100)
    };
    return msg;
}
}

And the following function node to set the changes from HomeKit:

if (msg.hap.context !== undefined )
{
    if (msg.payload.On !== undefined) {
        if(msg.payload.On === false){
            msg.payload = { "set": false, "dpsIndex": 1};
        } else if(msg.payload.On === true){
            msg.payload = { "set": true, "dpsIndex": 1};
        }
        return msg
    } else if (msg.payload.Brightness !== undefined) {
        var v = msg.payload.Brightness;
        flow.set('b',v);
        if (flow.get("colormode") == "white") {
            if (msg.payload.Brightness === 0) {
                msg.payload = { "set": 0, "dpsIndex": 3};
            } else {
                var theBrightness = Number.parseInt((msg.payload.Brightness/100) * 255);
                msg.payload = { "set": theBrightness, "dpsIndex": 3};
            }
            return msg;
        } else if (flow.get("colormode") == "colour") {
            var h = flow.get('h')||0;
            var s = flow.get('s')||0;

            var colorsys = global.get('colorsys');
            var rgb = colorsys.hsvToRgb(h,s,v);
            var r = rgb.r;
            var g = rgb.g;
            var b = rgb.b;
                
            var setVal = ("00" + r.toString(16)).substr(-2) + ("00" + g.toString(16)).substr(-2) + ("00" + b.toString(16)).substr(-2) + ("0000" + h.toString(16)).substr(-4) + ("00" + parseInt(s/100*255).toString(16)).substr(-2) + ("00" + parseInt(v/100*255).toString(16)).substr(-2);
            msg.payload = { "set": setVal, "dpsIndex": 5};
            
            return msg;
        }
    } else if (msg.payload.ColorTemperature !== undefined) {
        var mirek = msg.payload.ColorTemperature;
        if (mirek > 370) mirek = 370;
        var colortemp = 1000000/mirek;
        var colorValue = Number.parseInt(((colortemp-2700)/3800) * 255);
        if (flow.get("colormode") == "colour") {
            msg.payload = { "set": "white", "dpsIndex": 2};
            node.send(msg);
        }
        msg.payload = { "set": colorValue, "dpsIndex": 4};
        return msg;
    } else if (msg.payload.Hue !== undefined) {
        var h = msg.payload.Hue;
            context.set('h',h);
        var s = context.get('s');
        if (s !== null) {
            if (flow.get("colormode") == "white") {
                msg.payload = { "set": "colour", "dpsIndex": 2};
                node.send(msg);
            }
            var v = flow.get('b')||0;
            
            var colorsys = global.get('colorsys');
            var rgb = colorsys.hsvToRgb(h,s,v);
            var r = rgb.r;
            var g = rgb.g;
            var b = rgb.b;
            var setVal = ("00" + r.toString(16)).substr(-2) + ("00" + g.toString(16)).substr(-2) + ("00" + b.toString(16)).substr(-2) + ("0000" + h.toString(16)).substr(-4) + ("00" + parseInt(s/100*255).toString(16)).substr(-2) + ("00" + parseInt(v/100*255).toString(16)).substr(-2);
            msg.payload = { "set": setVal, "dpsIndex": 5};
            return msg;
        }
    } if (msg.payload.Saturation !== undefined) {
        var h = context.get('h');
        var s = msg.payload.Saturation;
        context.set('s',s);
        if (h !== null) {
            if (flow.get("colormode") == "white") {
                msg.payload = { "set": "colour", "dpsIndex": 2};
                node.send(msg);
            }
            var v = flow.get('b')||0;
            
            var colorsys = global.get('colorsys');
            var rgb = colorsys.hsvToRgb(h,s,v);
            var r = rgb.r;
            var g = rgb.g;
            var b = rgb.b;
            var setVal = ("00" + r.toString(16)).substr(-2) + ("00" + g.toString(16)).substr(-2) + ("00" + b.toString(16)).substr(-2) + ("0000" + h.toString(16)).substr(-4) + ("00" + parseInt(s/100*255).toString(16)).substr(-2) + ("00" + parseInt(v/100*255).toString(16)).substr(-2);
            msg.payload = { "set": setVal, "dpsIndex": 5};
            return msg;
        }
    }
}

Hopefully this helps save some people the headaches I endured to get it working well within my environment.

5 Likes

A snapshot of the node-red nodes would be worth a thousand words too. Thanks

1 Like

Here’s a screenshot of the Node-RED spaghetti/

1 Like

I actually intended to in my original post, but I forgot.

George,

Am I stuck using HomeKit and HomeBridge? I am currently only using OpenHAB and have neither of the others installed.

Thanks,

Todd

No. I don’t use homebridge at all, either. You will however, need Node-RED installed to use this method. I happen to have it installed already to expose my openhab items to HomeKit more flexibly than the HomeKit binding. If you don’t have any use for HomeKit, just ignore those elements above.

Cool. Thanks! I’ll try it when I get home.

Hi Robert,
I tried to implement your process, but I haven’t been able to figure it out. I am running OpenHAB on a headless RPi3+ without a graphical interface. I’m not sure how to run Node Red on such a system. I access it with my workstation, which is a Linux system.
Also, I haven’t figured out where to put the Node Red files that you posted.
In other words, I am at a complete loss. Can you help?

Thanks,

Todd

If you are running openHABian, use the setup tool to install node-red.
If not: https://nodered.org/docs/hardware/raspberrypi

NodeRed is installed.

Good. Spend some time with it. Play around.
In the menu, manage pallete, you can install more nodes.
I recommend the following
node-red-contrib-bigtimer
node-red-contrib-openhab2
node-red-contrib-ramp-thermostat

As a starting point
There are many nodes available for many technologies (A bit like bindings…)

My issue is how do I run it? I’m running a headless Openhabian without the GUI, and I access it from a Linux system (ssh). Do I run it from the Linux system? I’m lost here…

You run it as a service as is described in the installation instructions autostart at boot. You access the interface through a web browser.

1 Like

OK, I got Node Red running. In proceeding with the instructions to instruction iv:


    On Linux, do the following:

    sudo mkdir /usr/share/ca-certificates/extra/
    sudo cp $HOME/.anyproxy/certificates/rootCA.crt /usr/share/ca-certificates/extra/
    sudo dpkg-reconfigure ca-certificates

    The last command will open up a GUI; hit yes on the first prompt. Using the arrows on your keyboard, locate extra/rootCA.crt, place an * next to it by hitting the space bar, hit the Tab key on your keyboard to highlight OK, and press the Enter key.


(I removed the Windows and Mac OS commands, for brevity,  because I'm running a Linux system)


iv. Execute tuya-cli list-app. Within a few seconds, you would be shown (a) a QR code and (b) details of the proxy server.

v. Scan the barcode with your iPhone. If you don't know how to do that, just say "Hey Siri, scan a QR code!" and point the camera at the barcode.

vi. Your phone will ask you your permission to open Safari; let it.

I get this error every time:

[19:34:42] openhabian@openHABianPi:/usr/bin$ tuya-cli list-app
(node:13495) UnhandledPromiseRejectionWarning: RangeError: Array buffer allocation failed
    at new ArrayBuffer (<anonymous>)
    at Object.<anonymous> (/usr/lib/node_modules/anyproxy/node_modules/brotli/build/encode.js:21:207)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/usr/lib/node_modules/anyproxy/node_modules/brotli/compress.js:1:76)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
(node:13495) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:13495) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

It won’t show the QR code so I can set up the proxy server
What a frustrating process…

Sorry Robert, but I was unable to make the instructions work. The AMoo-Miki/ homebridge-tuya-lan instructions gave me errors (see above).
When I ran this command I got the following errors:

npm WARN saveError ENOENT: no such file or directory, open '/home/openhabian/package.json'
npm WARN enoent ENOENT: no such file or directory, open '/home/openhabian/package.json'
npm WARN openhabian No description
npm WARN openhabian No repository field.
npm WARN openhabian No README data
npm WARN openhabian No license field.

+ tuyapi@3.2.2
added 11 packages from 16 contributors and audited 11 packages in 18.755s
found 0 vulnerabilities

When I run this command I get:

(node:9222) UnhandledPromiseRejectionWarning: RangeError: Array buffer allocation failed
    at new ArrayBuffer (<anonymous>)
    at Object.<anonymous> (/usr/lib/node_modules/anyproxy/node_modules/brotli/build/encode.js:21:207)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/usr/lib/node_modules/anyproxy/node_modules/brotli/compress.js:1:76)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)
(node:9222) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:9222) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

And the command freezes and I have to enter ^C to get the system back.
I haven’t been able to figure out the tuya-cli get --id or the tuya-cli get --key commands.
If I run tuya-cli list, I get {}

error: option `--id <id>' argument missing
[19:46:51] openhabian@openHABianPi:~$ tuya-cli list
{}

Without being able to run the tuya-cli commands I can’t get the ID or Key, nor do I get the QR code so I can set up the reverse proxy.
This isn’t working…

Just to let you all know, this is NOT SOLVED. The procedures that are listed didn’t work for me and my light bulbs are still NOT accessable through OpenHAB.

Thank you.

7/2/2020: It’s been a year and still no change.

There are a few different methods for connecting Tuya devices to openHAB. Personally, I favour flashing them with Tasmota firmware using Tuya-convert.

1 Like

Hi Russ,

That appears to be for plugs. I looked at the device list and didn’t see bulbs anywhere. Is the process the same for light bulbs?

Thanks,

Todd

You have to look at the Tasmota templates.

1 Like

Awesome! Thanks. I didn’t notice the list on the left. As soon as I get my system functional again I will do some testing. I have 2 bulbs and I’d like to get them functioning through OH.