UNPKG

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 and ETS group address importer. Easy to use and highly configurable.

522 lines (446 loc) 16.1 kB
// 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] }