@pajn/node-tradfri-client
Version:
Library to talk to IKEA Trådfri Gateways without external binaries
246 lines (245 loc) • 8.71 kB
JavaScript
;
// tslint:disable:variable-name
Object.defineProperty(exports, "__esModule", { value: true });
exports.conversions = exports.deserializers = exports.serializers = void 0;
const math_1 = require("alcalzone-shared/math");
const strings_1 = require("alcalzone-shared/strings");
const math_2 = require("./math");
const predefined_colors_1 = require("./predefined-colors");
// ==========================
// WHITE SPECTRUM conversions
// interpolate from [0..100%] to [153..497]
const colorTemperature_out = (value) => {
const [min, max] = predefined_colors_1.colorTemperatureRange;
// extrapolate 0-100% to [min..max]
value = math_1.clamp(value, 0, 100);
return math_1.roundTo(min + value / 100 * (max - min), 0);
};
// interpolate from [153..497] to [0..100%]
const colorTemperature_in = (value) => {
const [min, max] = predefined_colors_1.colorTemperatureRange;
// interpolate "color percentage" from the colorTemperature range of a lightbulb
value = (value - min) / (max - min);
value = math_1.clamp(value, 0, 1);
return math_1.roundTo(value * 100, 1);
};
// ==========================
// RGB conversions
// Tradfri lights seem to be Philips Hue Gamut B, see
// https://developers.meethue.com/documentation/core-concepts#color_gets_more_complicated
// Conversion based on
// https://github.com/Q42/Q42.HueApi/blob/master/src/Q42.HueApi.ColorConverters/OriginalWithModel/HueColorConverter.cs
function rgbToCIExyY(r, g, b) {
// transform [0..255] => [0..1]
[r, g, b] = [r, g, b].map(c => c / 255);
// gamma correction
[r, g, b] = [r, g, b].map(c => (c > 0.04045) ? Math.pow(((c + 0.055) / 1.055), 2.4) : c / 12.92);
// transform using custom RGB->XYZ matrix. See comment in Q42
const X = r * 0.664511 + g * 0.154324 + b * 0.162028;
const Y = r * 0.283881 + g * 0.668433 + b * 0.047685;
const Z = r * 0.000088 + g * 0.072310 + b * 0.986039;
let [x, y] = [0, 0];
if (X + Y + Z > 0) {
// calculate CIE xy
x = X / (X + Y + Z);
y = Y / (X + Y + Z);
}
({ x, y } = mapToGamut(x, y, DEFAULT_GAMUT));
return { x, y, Y };
}
function rgbFromCIExyY(x, y, Y = 1) {
// Map the given point to lie inside the bulb gamut
({ x, y } = mapToGamut(x, y, DEFAULT_GAMUT));
// calculate X/Y/Z
const z = 1 - x - y;
const X = (Y / y) * x;
const Z = (Y / y) * z;
// convert to RGB
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;
// downscale so the maximum component is not > 1
const maxRGB = Math.max(r, g, b);
if (maxRGB > 1) {
[r, g, b] = [r, g, b].map(c => c / maxRGB);
}
// reverse gamma correction
[r, g, b] = [r, g, b].map(c => c <= 0.0031308 ? 12.92 * c : (1.0 + 0.055) * Math.pow(c, (1.0 / 2.4)) - 0.055);
// transform back to [0..255]
[r, g, b] = [r, g, b].map(c => Math.round(math_1.clamp(c, 0, 1) * 255));
return { r, g, b };
}
const DEFAULT_GAMUT = {
red: { x: 0.700607, y: 0.299301 },
green: { x: 0.172416, y: 0.746797 },
blue: { x: 0.135503, y: 0.039879 },
};
function mapToGamut(x, y, gamut) {
const point = { x, y };
const gamutAsTriangle = [gamut.red, gamut.green, gamut.blue];
if (!math_2.pointInTriangle(gamutAsTriangle, point)) {
const closestEdge = math_2.findClosestTriangleEdge(point, gamutAsTriangle);
const projected = math_2.projectPointOnEdge(point, closestEdge);
return projected;
}
else {
return { x, y };
}
}
function rgbToHSV(r, g, b) {
// transform [0..255] => [0..1]
[r, g, b] = [r, g, b].map(c => c / 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h;
let s;
const v = math_1.roundTo(max, 2);
if (r === g && g === b) {
h = 0;
}
else if (max === r) {
h = 60 * (0 + (g - b) / (max - min));
}
else if (max === g) {
h = 60 * (2 + (b - r) / (max - min));
}
else if (max === b) {
h = 60 * (4 + (r - g) / (max - min));
}
h = Math.round(h); // h is defined, we checked all possibilities
if (h < 0)
h += 360;
if (max === 0) {
s = 0;
}
else {
s = math_1.roundTo((max - min) / max, 3);
}
return { h, s, v };
}
function rgbFromHSV(h, s, v) {
let r;
let g;
let b;
if (s === 0) {
r = g = b = v;
}
else {
const hi = Math.floor(h / 60);
const f = (h / 60 - hi);
const p = v * (1 - s);
const q = v * (1 - s * f);
const t = v * (1 - s * (1 - f));
switch (hi) {
case 0:
case 6:
[r, g, b] = [v, t, p];
break;
case 1:
[r, g, b] = [q, v, p];
break;
case 2:
[r, g, b] = [p, v, t];
break;
case 3:
[r, g, b] = [p, q, v];
break;
case 4:
[r, g, b] = [t, p, v];
break;
case 5:
[r, g, b] = [v, p, q];
break;
}
}
// transform back to [0..255] - r, g and b are defined always
[r, g, b] = [r, g, b].map(c => Math.round(math_1.clamp(c, 0, 1) * 255));
return { r, g, b };
}
function rgbToString(r, g, b) {
return [r, g, b].map(c => strings_1.padStart(c.toString(16), 2, "0")).join("");
}
function rgbFromString(rgb) {
const r = parseInt(rgb.substr(0, 2), 16);
const g = parseInt(rgb.substr(2, 2), 16);
const b = parseInt(rgb.substr(4, 2), 16);
return { r, g, b };
}
// ===========================
// RGB serializers
// interpolate hue from [0..360] to [0..COLOR_MAX]
const hue_out = (value, light) => {
if (light != null && light.spectrum !== "rgb")
return null; // hue is not supported
value = math_1.clamp(value, 0, 360);
return math_1.roundTo(value / 360 * predefined_colors_1.MAX_COLOR, 0);
};
// interpolate hue from [0..COLOR_MAX] to [0..360]
const hue_in = (value /*, light: Light*/) => {
value = math_1.clamp(value / predefined_colors_1.MAX_COLOR, 0, 1);
return math_1.roundTo(value * 360, 1);
};
// interpolate saturation from [0..100%] to [0..COLOR_MAX]
const saturation_out = (value, light) => {
if (light != null && light.spectrum !== "rgb")
return null; // hue is not supported
value = math_1.clamp(value, 0, 100);
return math_1.roundTo(value / 100 * predefined_colors_1.MAX_COLOR, 0);
};
// interpolate saturation from [0..COLOR_MAX] to [0..100%]
const saturation_in = (value /*, light: Light*/) => {
value = math_1.clamp(value / predefined_colors_1.MAX_COLOR, 0, 1);
return math_1.roundTo(value * 100, 1);
};
// ===========================
// TRANSITION TIME conversions
// the sent value is in 10ths of seconds, we're working with seconds
const transitionTime_out = val => val && val * 10; // "val && " avoids sending `null` if val is null for some reason
// the sent value is in 10ths of seconds, we're working with seconds
const transitionTime_in = val => val / 10;
// ===========================
// BRIGHTNESS conversions
// interpolate from [0..100%] to [0..254]
const brightness_out = (value) => {
value = math_1.clamp(value, 0, 100);
return math_1.roundTo(value / 100 * 254, 0);
};
// interpolate from [0..254] to [0..100%]
const brightness_in = (value) => {
value = math_1.clamp(value, 0, 254);
if (value === 0)
return 0;
value = value / 254 * 100;
return math_1.roundTo(value, 1);
};
// ===========================
// BLIND POSITION conversions
// The gateway expects 0 to be open and 100 to be closed
// we do the opposite
const position_out = val => 100 - val;
// the sent value is in 10ths of seconds, we're working with seconds
const position_in = position_out;
exports.serializers = {
transitionTime: transitionTime_out,
hue: hue_out,
saturation: saturation_out,
brightness: brightness_out,
colorTemperature: colorTemperature_out,
position: position_out,
};
exports.deserializers = {
transitionTime: transitionTime_in,
hue: hue_in,
saturation: saturation_in,
brightness: brightness_in,
colorTemperature: colorTemperature_in,
position: position_in,
};
exports.conversions = {
rgbFromCIExyY,
rgbToCIExyY,
rgbFromHSV,
rgbToHSV,
rgbToString,
rgbFromString,
};