Skip to content

Commit e3bbd82

Browse files
- Updating the RGB implementation to utilize a conversion to XY co-ordinates, which should be more accurate and fix #17
1 parent 04bb156 commit e3bbd82

File tree

9 files changed

+370
-19
lines changed

9 files changed

+370
-19
lines changed

Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Change Log
22

3+
## 0.2.5
4+
- Fixes for RGB conversion into XY co-ordinates for lamps to give better accuracy compared to previous implementation using HSL
5+
36
## 0.2.4
47
- Added ability to configure the timeout when communicating with the Hue Bridge
58

hue-api/index.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"use strict";
22

3-
var Q = require("q"),
4-
http = require("./httpPromise"),
5-
ApiError = require("./errors").ApiError,
6-
utils = require("./utils"),
7-
lightsApi = require("./commands/lights-api"),
8-
groupsApi = require("./commands/groups-api"),
9-
schedulesApi = require("./commands/schedules-api"),
10-
configurationApi = require("./commands/configuration-api"),
11-
scheduledEvent = require("./scheduledEvent"),
12-
bridgeDiscovery = require("./bridge-discovery");
3+
var Q = require("q")
4+
, http = require("./httpPromise")
5+
, ApiError = require("./errors").ApiError
6+
, utils = require("./utils")
7+
, rgb = require("./rgb")
8+
, lightsApi = require("./commands/lights-api")
9+
, groupsApi = require("./commands/groups-api")
10+
, schedulesApi = require("./commands/schedules-api")
11+
, configurationApi = require("./commands/configuration-api")
12+
, scheduledEvent = require("./scheduledEvent")
13+
, bridgeDiscovery = require("./bridge-discovery")
14+
;
1315

1416

1517
function HueApi(host, username, timeout) {
@@ -301,8 +303,23 @@ HueApi.prototype.setLightState = function (id, stateValues, cb) {
301303
options.values = stateValues;
302304

303305
if (!promise) {
304-
promise = http.invoke(lightsApi.setLightState, options);
306+
// We have not errored, so check if we need to convert an rgb value
307+
308+
if (stateValues.rgb) {
309+
promise = this.lightStatus(id)
310+
.then(function(lightDetails) {
311+
options.values.xy = rgb.convertRGBtoXY(stateValues.rgb, lightDetails);
312+
delete options.values.rgb;
313+
})
314+
.then(function() {
315+
return http.invoke(lightsApi.setLightState, options);
316+
})
317+
;
318+
} else {
319+
promise = http.invoke(lightsApi.setLightState, options);
320+
}
305321
}
322+
306323
return utils.promiseOrCallback(promise, cb);
307324
};
308325

hue-api/lightstate.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use strict";
22

3-
var utils = require("./utils"),
4-
State = function () {
3+
var utils = require("./utils");
4+
5+
var State = function () {
56
};
67

78
/**
@@ -102,7 +103,11 @@ State.prototype.transition = function (seconds) {
102103
* @return {State}
103104
*/
104105
State.prototype.rgb = function (r, g, b) {
105-
utils.combine(this, _getHSLStateFromRGB(r, g, b));
106+
// The conversion to rgb is now done in the xy space, but to do so requires knowledge of the limits of the light's
107+
// color gamut.
108+
// To cater for this, we store the rgb value requested, and convert it to xy when the user applies it.
109+
utils.combine(this, {rgb: [r, g, b]});
110+
//utils.combine(this, _getHSLStateFromRGB(r, g, b)); // Was not particularly reliable conversion
106111
return this;
107112
};
108113

@@ -167,6 +172,7 @@ function _getEffectState(value) {
167172
};
168173
}
169174

175+
//TODO this is not that reliable at the extremes of ranges of values
170176
/**
171177
* Gets the HSL/HSB value from the RGB values provided
172178
* @param red

hue-api/rgb.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"use strict";
2+
3+
var XY = function (x, y) {
4+
this.x = x;
5+
this.y = y;
6+
}
7+
, hueLimits = {
8+
red: new XY(0.675, 0.322),
9+
green: new XY(0.4091, 0.518),
10+
blue: new XY(0.167, 0.04)
11+
}
12+
, livingColorsLimits = {
13+
red: new XY(0.704, 0.296),
14+
green: new XY(0.2151, 0.7106),
15+
blue: new XY(0.138, 0.08)
16+
}
17+
, defaultLimits = {
18+
red: new XY(1.0, 0),
19+
green: new XY(0.0, 1.0),
20+
blue: new XY(0.0, 0.0)
21+
}
22+
;
23+
24+
function _crossProduct(p1, p2) {
25+
return (p1.x * p2.y - p1.y * p2.x);
26+
}
27+
28+
function _isInColorGamut(p, lampLimits) {
29+
var v1 = new XY(
30+
lampLimits.green.x - lampLimits.red.x
31+
, lampLimits.green.y - lampLimits.red.y
32+
)
33+
, v2 = new XY(
34+
lampLimits.blue.x - lampLimits.red.x
35+
, lampLimits.blue.y - lampLimits.red.y
36+
)
37+
, q = new XY(p.x - lampLimits.red.x, p.y - lampLimits.red.y)
38+
, s = _crossProduct(q, v2) / _crossProduct(v1, v2)
39+
, t = _crossProduct(v1, q) / _crossProduct(v1, v2)
40+
;
41+
42+
return (s >= 0.0) && (t >= 0.0) && (s + t <= 1.0);
43+
}
44+
45+
/**
46+
* Find the closest point on a line. This point will be reproducible by the limits.
47+
*
48+
* @param start {XY} The point where the line starts.
49+
* @param stop {XY} The point where the line ends.
50+
* @param point {XY} The point which is close to the line.
51+
* @return {XY} A point that is on the line specified, and closest to the XY provided.
52+
*/
53+
function _getClosestPoint(start, stop, point) {
54+
var AP = new XY(point.x - start.x, point.y - start.y)
55+
, AB = new XY(stop.x - start.x, stop.y - start.y)
56+
, ab2 = AB.x * AB.x + AB.y * AB.y
57+
, ap_ab = AP.x * AB.x + AP.y * AB.y
58+
, t = ap_ab / ab2
59+
;
60+
61+
if (t < 0.0) {
62+
t = 0.0;
63+
} else if (t > 1.0) {
64+
t = 1.0;
65+
}
66+
67+
return new XY(
68+
start.x + AB.x * t
69+
, start.y + AB.y * t
70+
);
71+
}
72+
73+
function _getDistanceBetweenPoints(pOne, pTwo) {
74+
var dx = pOne.x - pTwo.x
75+
, dy = pOne.y - pTwo.y
76+
;
77+
return Math.sqrt(dx * dx + dy * dy);
78+
}
79+
80+
function _getXYStateFromRGB(red, green, blue, limits) {
81+
var r = _gammaCorrection(red)
82+
, g = _gammaCorrection(green)
83+
, b = _gammaCorrection(blue)
84+
, X = r * 0.4360747 + g * 0.3850649 + b * 0.0930804
85+
, Y = r * 0.2225045 + g * 0.7168786 + b * 0.0406169
86+
, Z = r * 0.0139322 + g * 0.0971045 + b * 0.7141733
87+
, cx = X / (X + Y + Z)
88+
, cy = Y / (X + Y + Z)
89+
, xyPoint
90+
;
91+
92+
cx = isNaN(cx) ? 0.0 : cx;
93+
cy = isNaN(cy) ? 0.0 : cy;
94+
95+
xyPoint = new XY(cx, cy);
96+
97+
if (!_isInColorGamut(xyPoint, limits)) {
98+
xyPoint = _resolveXYPointForLamp(xyPoint, limits);
99+
}
100+
101+
return [xyPoint.x, xyPoint.y];
102+
}
103+
104+
/**
105+
* When a color is outside the limits, find the closest point on each line in the CIE 1931 'triangle'.
106+
* @param point {XY} The point that is outside the limits
107+
* @param limits The limits of the bulb (red, green and blue XY points).
108+
* @returns {XY}
109+
*/
110+
function _resolveXYPointForLamp(point, limits) {
111+
112+
var pAB = _getClosestPoint(limits.red, limits.green, point)
113+
, pAC = _getClosestPoint(limits.blue, limits.red, point)
114+
, pBC = _getClosestPoint(limits.green, limits.blue, point)
115+
, dAB = _getDistanceBetweenPoints(point, pAB)
116+
, dAC = _getDistanceBetweenPoints(point, pAC)
117+
, dBC = _getDistanceBetweenPoints(point, pBC)
118+
, lowest = dAB
119+
, closestPoint = pAB
120+
;
121+
122+
if (dAC < lowest) {
123+
lowest = dAC;
124+
closestPoint = pAC;
125+
}
126+
127+
if (dBC < lowest) {
128+
closestPoint = pBC;
129+
}
130+
131+
return closestPoint;
132+
}
133+
134+
function _gammaCorrection(value) {
135+
var result = value;
136+
if (value > 0.04045) {
137+
result = Math.pow((value + 0.055) / (1.0 + 0.055), 2.4);
138+
} else {
139+
result = value / 12.92;
140+
}
141+
return result;
142+
}
143+
144+
function _getLimits(lightDetails) {
145+
var limits = defaultLimits
146+
, modelId
147+
;
148+
149+
if (lightDetails.modelid) {
150+
modelId = lightDetails.modelid.toLowerCase();
151+
152+
if (/^lct/.test(modelId)) {
153+
// This is a Hue bulb
154+
limits = hueLimits;
155+
} else if (/^llc/.test(modelId)) {
156+
// This is a Living Color lamp (Bloom, Iris, etc..)
157+
limits = livingColorsLimits;
158+
} else if (/^lwb/.test(modelId)) {
159+
// This is a lux bulb
160+
limits = defaultLimits;
161+
} else {
162+
limits = defaultLimits;
163+
}
164+
}
165+
166+
return limits;
167+
}
168+
169+
module.exports = {
170+
convertRGBtoXY: function(rgb, lightDetails) {
171+
var limits = _getLimits(lightDetails);
172+
173+
return _getXYStateFromRGB(rgb[0], rgb[1], rgb[2], limits);
174+
}
175+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-hue-api",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"author": "Peter Murray <peter.murray@osirisoft.com>",
55
"contributors": [
66
{

test/lightstate-tests.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ describe("Light State", function () {
6868
});
6969

7070
it("'rgb'", function () {
71-
state.rgb(200, 200, 200);
72-
expect(state).to.have.keys("hue", "sat", "bri");
73-
//TODO could put checks in for values...
71+
state.rgb(200, 100, 0);
72+
expect(state).to.have.keys("rgb");
73+
expect(state.rgb[0]).to.equal(200);
74+
expect(state.rgb[1]).to.equal(100);
75+
expect(state.rgb[2]).to.equal(0);
7476
});
7577
});
7678

0 commit comments

Comments
 (0)