node-red-contrib-homekit-rgb
Advanced tools
Comparing version
462
index.js
@@ -0,119 +1,385 @@ | ||
/** | ||
* @description HSB/V to RGB and XY | ||
* @package node-red-contrib-homekit-rgb | ||
* @param {Object} RED | ||
* @author Christopher Aitken 2023 | ||
*/ | ||
module.exports = function(RED) | ||
{ | ||
function HomekitRGB(config) | ||
{ | ||
RED.nodes.createNode(this,config); | ||
var node = this; | ||
var nodeContext = this.context(); | ||
node.on('input', function(msg) | ||
{ | ||
// Load cached values or fall-back to defaults | ||
let hue = nodeContext.get('h') || 0; | ||
let sat = nodeContext.get('s') || 0; | ||
let bri = nodeContext.get('v') || 100; | ||
let on = (nodeContext.get('o') === true); | ||
function HomekitRGB(config) | ||
{ | ||
RED.nodes.createNode(this, config); | ||
// Determine input | ||
if ('Hue' in msg.payload) hue = msg.payload.Hue; | ||
if ('Saturation' in msg.payload) sat = msg.payload.Saturation; | ||
if ('Brightness' in msg.payload) bri = msg.payload.Brightness; | ||
const node = this; | ||
const nodeContext = this.context(); | ||
// Update cache | ||
nodeContext.set('h', hue); | ||
nodeContext.set('s', sat); | ||
nodeContext.set('v', bri); | ||
// Restore brightness | ||
if ('On' in msg.payload) | ||
{ | ||
on = msg.payload.On; | ||
if (on === true && bri === 0) bri = 100; | ||
} | ||
/** | ||
* @description Handle incoming messages | ||
* @memberof node-red-contrib-homekit-rgb | ||
* @param {object} msg | ||
* @return {void} | ||
*/ | ||
node.on('input', function(msg) | ||
{ | ||
// Load cached values or fall-back to defaults | ||
let hue = nodeContext.get('h') || 0; | ||
let sat = nodeContext.get('s') || 0; | ||
let bri = nodeContext.get('b') || 100; | ||
// Cache on/off | ||
nodeContext.set('o', on); | ||
// Prep cache from input | ||
if ('Hue' in msg.payload) hue = msg.payload.Hue; | ||
if ('Saturation' in msg.payload) sat = msg.payload.Saturation; | ||
if ('Brightness' in msg.payload) bri = msg.payload.Brightness; | ||
// Emulate "off" by setting brightness to zero | ||
if (!on) bri = 0; | ||
// Convert from kelvin | ||
if ('ColorTemperature' in msg.payload) | ||
{ | ||
// Conversions | ||
const rgb = kelvin2rgb(msg.payload.ColorTemperature * 10); | ||
const hsv = rgb2hsv(rgb.r, rgb.g, rgb.b); | ||
const xy = rgb2xy(rgb.r, rgb.g, rgb.b); | ||
// Convert HSV to RGB | ||
hue = parseFloat(hue); | ||
sat = parseFloat(sat); | ||
bri = parseFloat(bri); | ||
// Prep output | ||
msg.payload.r = rgb.r; | ||
msg.payload.g = rgb.g; | ||
msg.payload.b = rgb.b; | ||
if (hue < 0) hue = 0; | ||
if (sat < 0) sat = 0; | ||
if (bri < 0) bri = 0; | ||
msg.payload.x = xy.x; | ||
msg.payload.y = xy.y; | ||
if (hue >= 360) hue = 359; | ||
if (sat > 100) sat = 100; | ||
if (bri > 100) bri = 100; | ||
msg.payload.Hue = hsv.h; | ||
msg.payload.Saturation = hsv.s; | ||
msg.payload.Brightness = hsv.v; | ||
sat /= 100; | ||
bri /= 100; | ||
// Update cache | ||
nodeContext.set('h', hsv.h); | ||
nodeContext.set('s', hsv.s); | ||
nodeContext.set('b', hsv.v); | ||
} | ||
// Convert from XY | ||
else if (('x' in msg.payload) && ('y' in msg.payload)) | ||
{ | ||
// Conversions | ||
const rgb = xy2rgb(msg.payload.x, msg.payload.y); | ||
const kelvin = rgb2kelvin(rgb.r, rgb.g, rgb, rgb.b); | ||
const hsv = rgb2hsv(rgb.r, rgb.g, rgb.b); | ||
let c = bri * sat; | ||
let hh = hue / 60; | ||
let x = c * (1 - Math.abs(hh % 2 - 1)); | ||
// Prep output | ||
msg.payload.r = rgb.r; | ||
msg.payload.g = rgb.g; | ||
msg.payload.b = rgb.b; | ||
let r = 0; | ||
let g = 0; | ||
let b = 0; | ||
msg.payload.ColorTemperature = kelvin / 10; | ||
if (hh >= 0 && hh < 1) | ||
{ | ||
r = c; | ||
g = x; | ||
} | ||
else if (hh >= 1 && hh < 2) | ||
{ | ||
r = x; | ||
g = c; | ||
} | ||
else if (hh >= 2 && hh < 3) | ||
{ | ||
g = c; | ||
b = x; | ||
} | ||
else if (hh >= 3 && hh < 4) | ||
{ | ||
g = x; | ||
b = c; | ||
} | ||
else if (hh >= 4 && hh < 5) | ||
{ | ||
r = x; | ||
b = c; | ||
} | ||
else | ||
{ | ||
r = c; | ||
b = x; | ||
} | ||
msg.payload.Hue = hsv.h; | ||
msg.payload.Saturation = hsv.s; | ||
msg.payload.Brightness = hsv.v; | ||
let m = bri - c; | ||
// Update cache | ||
nodeContext.set('h', hsv.h); | ||
nodeContext.set('s', hsv.s); | ||
nodeContext.set('b', hsv.v); | ||
} | ||
// Convert from RGB | ||
else if (('r' in msg.payload) && ('g' in msg.payload) && ('b' in msg.payload)) | ||
{ | ||
// Conversions | ||
const hsv = rgb2hsv(msg.payload.r, msg.payload.g, msg.payload.b); | ||
const xy = rgb2xy(msg.payload.r, msg.payload.g, msg.payload.b); | ||
const kelvin = rgb2kelvin(msg.payload.r, msg.payload.g, msg.payload.b); | ||
r += m; | ||
g += m; | ||
b += m; | ||
// Prep output | ||
msg.payload.x = xy.x; | ||
msg.payload.y = xy.y; | ||
r *= 255.0; | ||
g *= 255.0; | ||
b *= 255.0; | ||
msg.payload.Hue = hsv.h; | ||
msg.payload.Saturation = hsv.s; | ||
msg.payload.Brightness = hsv.v; | ||
r = Math.round(r); | ||
g = Math.round(g); | ||
b = Math.round(b); | ||
msg.payload.ColorTemperature = kelvin / 10; | ||
// Update status | ||
let fill = 'grey'; | ||
if (on) fill = 'green'; | ||
node.status({fill:fill,shape:"dot",text: r + ',' + g + ',' + b}); | ||
// Update cache | ||
nodeContext.set('h', hsv.h); | ||
nodeContext.set('s', hsv.s); | ||
nodeContext.set('b', hsv.v); | ||
} | ||
// Convert from HSB | ||
else | ||
{ | ||
// Conversions | ||
const rgb = hsv2rgb(hue, sat, bri); | ||
const kelvin = rgb2kelvin(rgb.r, rgb.g, rgb.b); | ||
const xy = rgb2xy(rgb.r, rgb.g, rgb.b); | ||
// Send output | ||
msg.payload = [r,g,b]; | ||
node.send(msg); | ||
}); | ||
} | ||
RED.nodes.registerType("homekit-rgb",HomekitRGB); | ||
// Prep output | ||
msg.payload.r = rgb.r; | ||
msg.payload.g = rgb.g; | ||
msg.payload.b = rgb.b; | ||
msg.payload.x = xy.x; | ||
msg.payload.y = xy.y; | ||
msg.payload.ColorTemperature = kelvin / 10; | ||
msg.payload.Hue = hue; | ||
msg.payload.Saturation = sat; | ||
msg.payload.Brightness = bri; | ||
// Update cache | ||
nodeContext.set('h', hue); | ||
nodeContext.set('s', sat); | ||
nodeContext.set('b', bri); | ||
} | ||
node.send(msg); | ||
}); | ||
/** | ||
* @description Convert HSV to RGB colour values | ||
* @memberof node-red-contrib-homekit-rgb | ||
* @param {int} h Hue. `0` to `360` | ||
* @param {int} s Saturation. `0` to `100` | ||
* @param {int} v Brightness. `0` to `100` | ||
* @returns {Object}: | ||
* r: {int} Red channel. `0` to `255` | ||
* g: {int} Green channel. `0` to `255` | ||
* b: {int} Blue channel. `0` to `255` | ||
*/ | ||
function hsv2rgb(h,s,v) | ||
{ | ||
s /= 100; | ||
v /= 100; | ||
let f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0); | ||
return { | ||
r: Math.round(f(5) * 255), | ||
g: Math.round(f(3) * 255), | ||
b: Math.round(f(1) * 255) | ||
} | ||
} | ||
/** | ||
* @description Convert RGB to HSV | ||
* @memberof node-red-contrib-homekit-rgb | ||
* @param {int} r Red channel. `0` to `255` | ||
* @param {int} g Green channel. `0` to `255` | ||
* @param {int} b Blue channel. `0` to `255` | ||
* @returns {Object} | ||
* h: {int} Hue. `0` to `360` | ||
* s: {int} Saturation. `0` to `100` | ||
* v: {int} Brightness. `0` to `100` | ||
*/ | ||
function rgb2hsv(r, g, b) | ||
{ | ||
r /= 255; | ||
g /= 255; | ||
b /= 255; | ||
let v = Math.max(r, g, b); | ||
let c = v - Math.min(r, g, b); | ||
let h = c && ((v == r) ? (g - b) / c : ((v == g) ? 2 + (b - r) / c : 4 + (r - g) / c)); | ||
return { | ||
h: Math.round(60 * (h < 0 ? h + 6 : h)), | ||
s: Math.round((v && c / v) * 100), | ||
v: Math.round(v * 100) | ||
} | ||
} | ||
/** | ||
* @description Return inverse gamma correction to given value | ||
* @memberof node-red-contrib-homekit-rgb | ||
* @param {float} val | ||
* @returns {float} | ||
*/ | ||
function invGammaCorrection(val) | ||
{ | ||
return val <= 0.0404482362771076 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); | ||
} | ||
/** | ||
* @description Convert RGB to XY colour values | ||
* @param {int} r Red channel. `0` to `255` | ||
* @param {int} g Green channel. `0` to `255` | ||
* @param {int} b Blue channel. `0` to `255` | ||
* @returns {Object}: | ||
* x: {float} | ||
* y: {float} | ||
*/ | ||
function rgb2xy(r, g, b) | ||
{ | ||
r = invGammaCorrection(r); | ||
g = invGammaCorrection(g); | ||
b = invGammaCorrection(b); | ||
let x = 0.4123955889674142161 * r + 0.3575834307637148171 * g + 0.1804926473817015735 * b; | ||
let y = 0.2125862307855955516 * r + 0.7151703037034108499 * g + 0.07220049864333622685 * b; | ||
let z = 0.01929721549174694484 * r + 0.1191838645808485318 * g + 0.9504971251315797660 * b; | ||
return { | ||
x: isNaN(x / (x + y + z)) ? 0 : x / (x + y + z), | ||
y: isNaN(y / (x + y + z)) ? 0 : y / (x + y + z), | ||
}; | ||
} | ||
/** | ||
* @description Convert XY to RGB colour values | ||
* @param {float} x X channel | ||
* @param {float} y Y channel | ||
* @returns {Object}: | ||
* r: {int} Red channel. `0` to `255` | ||
* g: {int} Green channel. `0` to `255` | ||
* b: {int} Blue channel. `0` to `255` | ||
*/ | ||
function xy2rgb(x, y) | ||
{ | ||
y = y || 0.00000000001; | ||
const new_y = 1; | ||
const new_x = (new_y / y) * x; | ||
const new_z = (new_y / y) * (1 - x - y); | ||
// Convert to RGB using Wide RGB D65 conversion. | ||
let rgb = [ | ||
new_x * 1.656492 - new_y * 0.354851 - new_z * 0.255038, | ||
-new_x * 0.707196 + new_y * 1.655397 + new_z * 0.036152, | ||
new_x * 0.051713 - new_y * 0.121364 + new_z * 1.011530 | ||
]; | ||
// Apply reverse gamma correction. | ||
rgb = rgb.map(x => | ||
x <= 0.0031308 ? 12.92 * x : (1.0 + 0.055) * Math.pow(x, 1.0 / 2.4) - 0.055 | ||
); | ||
// Bring all negative components to zero. | ||
rgb = rgb.map(x => Math.max(0, x)); | ||
// If one component is greater than 1, weight components by that value. | ||
const max = Math.max(rgb); | ||
if (max > 1) rgb = rgb.map(x => x / max); | ||
rgb = rgb.map(x => Math.round(x * 255)); | ||
return { | ||
r: rgb[0], | ||
g: rgb[1], | ||
b: rgb[2] | ||
}; | ||
} | ||
/** | ||
* @description Converts degrees Kelvin to RGB values | ||
* @param {int} kelvin | ||
* @return {Object}: | ||
* r: {int} Red channel. `0` to `255` | ||
* g: {int} Green channel. `0` to `255` | ||
* b: {int} Blue channel. `0` to `255` | ||
*/ | ||
function kelvin2rgb(kelvin) | ||
{ | ||
let temperature = kelvin / 100.0; | ||
let red; | ||
let green; | ||
let blue; | ||
// Calculate red | ||
if (temperature < 66.0) | ||
{ | ||
red = 255; | ||
} | ||
else | ||
{ | ||
red = temperature - 55.0; | ||
red = 351.97690566805693+ 0.114206453784165 * red - 40.25366309332127 * Math.log(red); | ||
if (red < 0) red = 0; | ||
if (red > 255) red = 255; | ||
} | ||
// Calculate green | ||
if (temperature < 66.0) | ||
{ | ||
green = temperature - 2; | ||
green = -155.25485562709179 - 0.44596950469579133 * green + 104.49216199393888 * Math.log(green); | ||
if (green < 0) green = 0; | ||
if (green > 255) green = 255; | ||
} | ||
else | ||
{ | ||
green = temperature - 50.0; | ||
green = 325.4494125711974 + 0.07943456536662342 * green - 28.0852963507957 * Math.log(green); | ||
if (green < 0) green = 0; | ||
if (green > 255) green = 255; | ||
} | ||
// Calculate blue | ||
if (temperature >= 66.0) | ||
{ | ||
blue = 255; | ||
} | ||
else | ||
{ | ||
if (temperature <= 20.0) | ||
{ | ||
blue = 0; | ||
} | ||
else | ||
{ | ||
blue = temperature - 10; | ||
blue = -254.76935184120902 + 0.8274096064007395 * blue + 115.67994401066147 * Math.log(blue); | ||
if (blue < 0) blue = 0; | ||
if (blue > 255) blue = 255; | ||
} | ||
} | ||
return { | ||
r: Math.round(red), | ||
g: Math.round(green), | ||
b: Math.round(blue) | ||
}; | ||
} | ||
/** | ||
* @description Convert RGB to degrees kelvin | ||
* @param {int} r Red channel. `0` to `255` | ||
* @param {int} g Green channel. `0` to `255` | ||
* @param {int} b Blue channel. `0` to `255` | ||
* @returns {int} degrees kelvin. | ||
*/ | ||
function rgb2kelvin(r, g, b) | ||
{ | ||
let kelvin; | ||
let rgb; | ||
const epsilon = 0.4; | ||
let minTemperature = 1000; | ||
let maxTemperature = 40000; | ||
while (maxTemperature - minTemperature > epsilon) | ||
{ | ||
kelvin = (maxTemperature + minTemperature) / 2; | ||
rgb = kelvin2rgb(kelvin); | ||
if ((rgb.b / rgb.r) >= (b / r)) | ||
{ | ||
maxTemperature = kelvin; | ||
} | ||
else | ||
{ | ||
minTemperature = kelvin; | ||
} | ||
} | ||
return Math.round(kelvin); | ||
} | ||
} | ||
RED.nodes.registerType("homekit-rgb",HomekitRGB); | ||
} |
{ | ||
"name": "node-red-contrib-homekit-rgb", | ||
"version": "1.0.3", | ||
"description": "Node-RED node to convert HomeKit light Accessory's HSB values to RGB", | ||
"main": "index.js", | ||
"dependencies": {}, | ||
"keywords": [ | ||
"rgb", | ||
"homekit", | ||
"hsv", | ||
"hsb", | ||
"node-red" | ||
], | ||
"author": "caitken", | ||
"homepage": "https://github.com/caitken-com", | ||
"license": "Apache-2.0", | ||
"node-red": { | ||
"nodes": { | ||
"homekit-rgb": "index.js" | ||
} | ||
} | ||
"name": "node-red-contrib-homekit-rgb", | ||
"version": "2.0.0", | ||
"description": "Node-RED node to convert HomeKit light Accessory's HSB values to RGB", | ||
"main": "index.js", | ||
"dependencies": {}, | ||
"keywords": [ | ||
"rgb", | ||
"homekit", | ||
"hsv", | ||
"hsb", | ||
"kelvin", | ||
"xy", | ||
"node-red" | ||
], | ||
"engines": { | ||
"node": ">=14.18.1" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/caitken-com/node-red-contrib-homekit-rgb/issues" | ||
}, | ||
"author": "Christopher Aitken", | ||
"homepage": "https://github.com/caitken-com/node-red-contrib-homekit-rgb", | ||
"license": "MIT", | ||
"node-red": { | ||
"version": ">=2.0.0", | ||
"nodes": { | ||
"homekit-rgb": "index.js" | ||
} | ||
} | ||
} |
@@ -5,7 +5,5 @@ # Node-RED node to convert HomeKit Light Accessory's HSB values to RGB values | ||
 | ||
## Expected input | ||
Output of [HomeKit light accessory](http://auto.caitken.com/posts/2018/09/09/nodered-homekit-reference#ServiceLightbulb). Any of the following: | ||
Output of [HomeKit light accessory](https://nrchkb.github.io/wiki/service/lightbulb/). Any of the following: | ||
@@ -15,8 +13,27 @@ * *msg.payload.Hue* **{float}** `0.0` to `360.0` | ||
* *msg.payload.Brightness* **{int}** `0` to `100` | ||
* *msg.payload.On* **{bool}** `true`|`false` | ||
* *msg.payload.ColorTemperature* **{int}** `140` to `500` | ||
Or, to convert from RGB: | ||
* *msg.payload.r* **{int}** Red channel `0` to `255` | ||
* *msg.payload.g* **{int}** Green channel `0` to `255` | ||
* *msg.payload.b* **{int}** Blue channel `0` to `255` | ||
Or, to convert from XY: | ||
* *msg.payload.x* **{float}** X channel | ||
* *msg.payload.y* **{float}** Y channel | ||
## Output | ||
R,G,B channels as a single array `[0,1,2]` | ||
The original input message (pass-though), plus _all of following_ conversions (rgb, hsb, xy, megakelvin) | ||
Each channel with values of `0` to `255` | ||
* *msg.payload.r* **{int}** Red channel `0` to `255` | ||
* *msg.payload.g* **{int}** Green channel `0` to `255` | ||
* *msg.payload.b* **{int}** Blue channel `0` to `255` | ||
* *msg.payload.x* **{float}** X channel | ||
* *msg.payload.y* **{float}** Y channel | ||
* *msg.payload.Hue* **{float}** Hue `0.0` to `360.0` | ||
* *msg.payload.Saturation* **{float}** Saturation `0.0` to `100.0` | ||
* *msg.payload.Brightness* **{int}** Brightness`0` to `100` | ||
* *msg.payload.ColorTemperature* **{int}** Mega Kelvin `150` to `500` |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
13176
166.13%5
25%327
223.76%0
-100%38
80.95%1
Infinity%