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.