node-red-contrib-knx-ultimate
Version:
Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.
522 lines (446 loc) • 16.1 kB
JavaScript
// Part of this code, thanks to https://github.com/Shnoo/js-CIE-1931-rgb-color-converter
const XYFromRGB = require('./XYFromRGB_Supergiovane') // Pick the specific hue color converter
class ColorConverter {
// static getBrightnessFromRGBOrHex(red, green, blue) {
// const hsv = convert.rgb.hsv(red, green, blue);
// const brightness = hsv[2];
// return brightness;
// }
static getBrightnessFromRGBOrHex (Rint, Gint, Bint) { // takes sRGB channels as 8 bit integers
const Rlin = (Rint / 255.0) ** 2.218 // Convert int to decimal 0-1 and linearize
const Glin = (Gint / 255.0) ** 2.218 // ** is the exponentiation operator, older JS needs Math.pow() instead
const Blin = (Bint / 255.0) ** 2.218 // 2.218 Gamma for sRGB linearization. 2.218 sets unity with the piecewise sRGB at #777 .... 2.2 or 2.223 could be used instead
const Ylum = Rlin * 0.2126 + Glin * 0.7156 + Blin * 0.0722 // convert to Luminance Y
let ret = Ylum ** 0.43 * 100// Convert to lightness (0 to 100)
// Boundary Check (min 0, max 100)
ret = ret < 0 ? 0 : ret
ret = ret > 100 ? 100 : ret
return ret
}
static convert_1_255_ToPercentage (number) {
const percentage = (number / 255) * 100
return percentage
}
static kelvinToMirek (_kelvin) {
return Math.floor(1000000 / _kelvin)
}
static mirekToKelvin (_mirek) {
return Math.floor(1000000 / _mirek)
}
// Linear interpolation of input y given starting and ending ranges
static scale (y, range1 = [0, 100], range2 = [0, 255]) {
const [xMin, xMax] = range2
const [yMin, yMax] = range1
const percent = (y - yMin) / (yMax - yMin)
const ans = percent * (xMax - xMin) + xMin
// const roundedVal = Math.round((ans + Number.EPSILON) * 10000) / 10000;// Round by 4 decimals
const roundedVal = ans
return roundedVal
}
// Thanks to: https://github.com/sindresorhus/rgb-hex
static rgbHex (red, green, blue, alpha) {
const toHex = (red, green, blue, alpha) => ((blue | green << 8 | red << 16) | 1 << 24).toString(16).slice(1) + alpha
const parseCssRgbString = (input) => {
const parts = input.replace(/rgba?\(([^)]+)\)/, '$1').split(/[,\s/]+/).filter(Boolean)
if (parts.length < 3) {
return
}
const parseValue = (value, max) => {
value = value.trim()
if (value.endsWith('%')) {
return Math.min(Number.parselet(value) * max / 100, max)
}
return Math.min(Number.parselet(value), max)
}
const red = parseValue(parts[0], 255)
const green = parseValue(parts[1], 255)
const blue = parseValue(parts[2], 255)
let alpha
if (parts.length === 4) {
alpha = parseValue(parts[3], 1)
}
return [red, green, blue, alpha]
}
let isPercent = (red + (alpha || '')).toString().includes('%')
if (typeof red === 'string' && !green) { // Single string parameter.
const parsed = parseCssRgbString(red)
if (!parsed) {
throw new TypeError('Invalid or unsupported color format.')
}
isPercent = false;
[red, green, blue, alpha] = parsed
} else if (alpha !== undefined) {
alpha = Number.parselet(alpha)
}
if (typeof red !== 'number' ||
typeof green !== 'number' ||
typeof blue !== 'number' ||
red > 255 ||
green > 255 ||
blue > 255
) {
throw new TypeError('Expected three numbers below 256')
}
if (typeof alpha === 'number') {
if (!isPercent && alpha >= 0 && alpha <= 1) {
alpha = Math.round(255 * alpha)
} else if (isPercent && alpha >= 0 && alpha <= 100) {
alpha = Math.round(255 * alpha / 100)
} else {
throw new TypeError(`Expected alpha value (${alpha}) as a fraction or percentage`)
}
alpha = (alpha | 1 << 8).toString(16).slice(1) // eslint-disable-line no-mixed-operators
} else {
alpha = ''
}
return toHex(red, green, blue, alpha)
}
// Thanks to: https://github.com/sindresorhus/hex-rgb
static hexRgb (hex, options = {}) {
const hexCharacters = 'a-f\\d'
const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`
const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`
const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi')
const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i')
if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) {
throw new TypeError('Expected a valid hex string')
}
hex = hex.replace(/^#/, '')
let alphaFromHex = 1
if (hex.length === 8) {
alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255
hex = hex.slice(0, 6)
}
if (hex.length === 4) {
alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255
hex = hex.slice(0, 3)
}
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
const number = Number.parseInt(hex, 16)
const red = number >> 16
const green = (number >> 8) & 255
const blue = number & 255
const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex
if (options.format === 'array') {
return [red, green, blue, alpha]
}
if (options.format === 'css') {
const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`
return `rgb(${red} ${green} ${blue}${alphaString})`
}
return {
red, green, blue, alpha
}
}
/**
* Checked 01/02/2024
* PERFETTO
* Calculate XY color points for a given RGB value.
* @param {number} red RGB red value (0-255)
* @param {number} green RGB green value (0-255)
* @param {number} blue RGB blue value (0-255)
* @param {object} lampGamut Hue bulb gamut range (the lamp provides a gamut object red:{x:1,y:1}, etc)
* @returns {number[]}
*/
static calculateXYFromRGB (red, green, blue, lampGamut) {
return XYFromRGB.calculateXYFromRGB(red, green, blue, lampGamut)
}
/**
* Converts an XY + brightness color value to RGB. Conversion formula
* Checked 01/02/2024
* QUASI PERFETTO !!! Preso da qui:
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes x, and y are contained in the set [0, 1] and bri [0,1]
* returns a Json with r,g,b
*
* @param Number x The x [0,1]
* @param Number y The y [0,1]
* @param Number bri The brightness [0,1]
* @return json The r,g,b [0,255]
*/
static xyBriToRgb (x, y, bri, colorGamut) {
if (colorGamut !== null && colorGamut !== undefined) {
if (!xyIsInGamutRange({ x, y }, colorGamut)) {
const xy = getClosestColor({ x, y }, colorGamut)
x = xy.x
y = xy.y
}
}
function getReversedGammaCorrectedValue (value) {
return Math.abs(value) <= 0.0031308 ? 12.92 * value : (1.0 + 0.055) * Math.pow(value, (1.0 / 2.4)) - 0.055
}
// To make RGB more similar to what ISE Connect HUE does, think to add here the row: bri = bri / 4.5
const z = 1.0 - x - y
// let Y = bri / 4.5;
const Y = Math.min(x, y) * bri
const X = (Y / y) * x
const Z = (Y / y) * z
// let r = 3.2404542 * X - 1.5371385 * Y - 0.4985314 * Z
// let g = -0.9692660 * X + 1.8760108 * Y + 0.0415560 * Z
// let b = 0.0556434 * X - 0.2040259 * Y + 1.0572252 * Z
let r = X * 1.656492 + Y * -0.354851 + Z * -0.255038
let g = X * -0.707196 + Y * 1.655397 + Z * 0.036152
let b = X * 0.051713 + Y * -0.121364 + Z * 1.011530
// Correction for negative values is missing from Philips' documentation.
const min = Math.min(r, Math.min(g, b))
if (min < 0.0) {
r -= min
g -= min
b -= min
}
// Rescale
let max = Math.max(r, Math.max(g, b))
if (max > 1.0) {
r /= max
g /= max
b /= max
}
r = getReversedGammaCorrectedValue(r)
g = getReversedGammaCorrectedValue(g)
b = getReversedGammaCorrectedValue(b)
// Rescale again
max = Math.max(r, Math.max(g, b))
if (max > 1.0) {
r /= max
g /= max
b /= max
}
// // Bring all negative components to zero
// r = Math.max(r, 0);
// g = Math.max(g, 0);
// b = Math.max(b, 0);
// // If one component is greater than 1, weight components by that value
// let max = Math.max(r, g, b);
// if (max > 1) {
// r = r / max;
// g = g / max;
// b = b / max;
// }
return {
r: Math.floor(r * 255.0),
g: Math.floor(g * 255.0),
b: Math.floor(b * 255.0)
}
}
/**
* Converts an RGB color value to HSV. Conversion formula
* Checked 31/01/2024
* PERFETTO !!!!!!
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns the HSV representation {hPercent:0-100%, h:0-360°, s:0-100%, v(brightness):0-100%}
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Object The HSV representation {hPercent:0-100%, h:0-360°, s:0-100%, v(brightness):0-100%}
*/
static rgbToHsv (r, g, b) {
// Sample
// ISE comando RGB: 182,0,20 HSV: 353°, 100%, 71% (HSV è stato calcolato da ETS automaticamente)
// Questa funzione restituisce l'HSV preciso.
r /= 255, g /= 255, b /= 255
const max = Math.max(r, g, b); const
min = Math.min(r, g, b)
let h
let s
const v = max
const d = max - min
s = max === 0 ? 0 : d / max
if (max === min) {
h = 0 // achromatic
} else {
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break
case g: h = (b - r) / d + 2; break
case b: h = (r - g) / d + 4; break
default:
}
h /= 6
}
const hPercent_rounded = Math.round((h + Number.EPSILON) * 10000) / 100
const s_rounded = Math.round((s + Number.EPSILON) * 10000) / 100
const v_rounded = Math.round((v + Number.EPSILON) * 10000) / 100
// const hGrad_rounded = ColorConverter.scale(hPercent_rounded, [0, 100], [0, 360]);
return { h: Math.floor(hPercent_rounded), s: Math.floor(s_rounded), v: Math.floor(v_rounded) }
}
/**
* Converts an HSV color value to RGB. Conversion formula
* Checked 30/01/2024
* PERFETTO !!!!!!!!!!!!!
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number v The value
* @return Array The RGB representation
*/
static hsvToRgb (h, s, v) {
let r
let g
let b
const i = Math.floor(h * 6)
const f = h * 6 - i
const p = v * (1 - s)
const q = v * (1 - f * s)
const t = v * (1 - (1 - f) * s)
switch (i % 6) {
case 0: r = v, g = t, b = p; break
case 1: r = q, g = v, b = p; break
case 2: r = p, g = v, b = t; break
case 3: r = p, g = q, b = v; break
case 4: r = t, g = p, b = v; break
case 5: r = v, g = p, b = q; break
default:
}
// Round!
r = Math.round(r * 255)
g = Math.round(g * 255)
b = Math.round(b * 255)
if (r > 255) r = 255
if (r < 0) r = 0
if (g > 255) g = 255
if (g < 0) g = 0
if (b > 255) b = 255
if (b < 0) b = 0
return { r, g, b }
}
/**
* Converts an HSV color value to XY Bri. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* returns h,s,v in object
*
* @param Object An object {h,s,v} in 0-100% values
* @return Object The JSON Object XYBri representation
*/
static hsvToxyBrightness (_hsvInput, _oGamut) {
// Get the XY from HSV
try {
const hsvInput = {}
hsvInput.h = ColorConverter.scale(_hsvInput.h, [0, 100], [0, 1])
hsvInput.s = ColorConverter.scale(_hsvInput.s, [0, 100], [0, 1])
hsvInput.v = ColorConverter.scale(_hsvInput.v, [0, 100], [0, 1])
const hsvToRgb = ColorConverter.hsvToRgb(hsvInput.h, hsvInput.s, hsvInput.v)
// Get the XY
return ColorConverter.calculateXYFromRGB(hsvToRgb.r, hsvToRgb.g, hsvToRgb.b, _oGamut)
} catch (error) { /* empty */ }
}
/**
* Converts an XY and Brightness color value to XY. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* returns x, and y in the set [0, 1] and brightness [0, 1].
*
* @param Number x The x (see _xyInScaleZeroUndred)
* @param Number y The y (see _xyInScaleZeroUndred)
* @param Number brightness The brightness with scale 0-100%
* @param Boolean _xyInScaleZeroUndred If true, the x and y will be scaled from 0-100 to 0-1, else, x and y remains as is (0-1)
* @return Object The HSV {h,s,v} representation, all in 0-10
*/
static xyBrightnessToHsv (x, y, brightness, _xyInScaleZeroUndred = true) {
try {
if (_xyInScaleZeroUndred) {
x = ColorConverter.scale(x, [0, 100], [0, 1])
y = ColorConverter.scale(y, [0, 100], [0, 1])
}
const rgb = ColorConverter.xyBriToRgb(x, y, brightness)
const hsv = ColorConverter.rgbToHsv(rgb.r, rgb.g, rgb.b)
return hsv
} catch (error) { }
}
}
exports.ColorConverter = ColorConverter
function xyIsInGamutRange (xy, gamut) {
if (Array.isArray(xy)) {
xy = {
x: xy[0],
y: xy[1]
}
}
const v0 = [gamut.blue[0] - gamut.red[0], gamut.blue[1] - gamut.red[1]]
const v1 = [gamut.green[0] - gamut.red[0], gamut.green[1] - gamut.red[1]]
const v2 = [xy.x - gamut.red[0], xy.y - gamut.red[1]]
const dot00 = (v0[0] * v0[0]) + (v0[1] * v0[1])
const dot01 = (v0[0] * v1[0]) + (v0[1] * v1[1])
const dot02 = (v0[0] * v2[0]) + (v0[1] * v2[1])
const dot11 = (v1[0] * v1[0]) + (v1[1] * v1[1])
const dot12 = (v1[0] * v2[0]) + (v1[1] * v2[1])
const invDenom = 1 / (dot00 * dot11 - dot01 * dot01)
const u = (dot11 * dot02 - dot01 * dot12) * invDenom
const v = (dot00 * dot12 - dot01 * dot02) * invDenom
return ((u >= 0) && (v >= 0) && (u + v < 1))
}
function getClosestColor (xy, gamut) {
function getLineDistance (pointA, pointB) {
return Math.hypot(pointB.x - pointA.x, pointB.y - pointA.y)
}
function getClosestPoint (xy, pointA, pointB) {
const xy2a = [xy.x - pointA.x, xy.y - pointA.y]
const a2b = [pointB.x - pointA.x, pointB.y - pointA.y]
const a2bSqr = Math.pow(a2b[0], 2) + Math.pow(a2b[1], 2)
const xy2a_dot_a2b = xy2a[0] * a2b[0] + xy2a[1] * a2b[1]
const t = xy2a_dot_a2b / a2bSqr
return {
x: pointA.x + a2b[0] * t,
y: pointA.y + a2b[1] * t
}
}
const greenBlue = {
a: {
x: gamut.green.x,
y: gamut.green.y
},
b: {
x: gamut.blue.x,
y: gamut.blue.y
}
}
const greenRed = {
a: {
x: gamut.green.x,
y: gamut.green.y
},
b: {
x: gamut.red.x,
y: gamut.red.y
}
}
const blueRed = {
a: {
x: gamut.red.x,
y: gamut.red.y
},
b: {
x: gamut.blue.x,
y: gamut.blue.y
}
}
const closestColorPoints = {
greenBlue: getClosestPoint(xy, greenBlue.a, greenBlue.b),
greenRed: getClosestPoint(xy, greenRed.a, greenRed.b),
blueRed: getClosestPoint(xy, blueRed.a, blueRed.b)
}
const distance = {
greenBlue: getLineDistance(xy, closestColorPoints.greenBlue),
greenRed: getLineDistance(xy, closestColorPoints.greenRed),
blueRed: getLineDistance(xy, closestColorPoints.blueRed)
}
let closestDistance
let closestColor
for (const i in distance) {
if (distance.hasOwnProperty(i)) {
if (!closestDistance) {
closestDistance = distance[i]
closestColor = i
}
if (closestDistance > distance[i]) {
closestDistance = distance[i]
closestColor = i
}
}
}
return closestColorPoints[closestColor]
}