node-red-contrib-knx-ultimate 2.2.20 → 2.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/nodes/hue-config.js +30 -6
- package/nodes/knxUltimateHueBattery.html +2 -2
- package/nodes/knxUltimateHueButton.html +2 -2
- package/nodes/knxUltimateHueLight.html +632 -546
- package/nodes/knxUltimateHueLight.js +89 -52
- package/nodes/knxUltimateHueLightSensor.html +3 -2
- package/nodes/knxUltimateHueMotion.html +2 -2
- package/nodes/knxUltimateHueScene.html +2 -2
- package/nodes/knxUltimateHueTapDial.html +2 -2
- package/nodes/knxUltimateHueTemperatureSensor.html +2 -2
- package/nodes/utils/hueColorConverter.js +195 -36
- package/package.json +1 -2
- package/resources/iro@5 +0 -7
|
@@ -35,6 +35,32 @@ module.exports = function (RED) {
|
|
|
35
35
|
node.isGrouped_light = config.hueDevice.split("#")[1] === "grouped_light";
|
|
36
36
|
node.hueDevice = config.hueDevice.split("#")[0];
|
|
37
37
|
node.initializingAtStart = (config.readStatusAtStartup === undefined || config.readStatusAtStartup === "yes");
|
|
38
|
+
config.specifySwitchOnBrightness === undefined ? "yes" : config.specifySwitchOnBrightness;
|
|
39
|
+
config.colorAtSwitchOnDayTime = (config.colorAtSwitchOnDayTime === '' || config.colorAtSwitchOnDayTime === undefined) ? '{ "kelvin":3000, "brightness":100 }' : config.colorAtSwitchOnDayTime;
|
|
40
|
+
config.colorAtSwitchOnNightTime = (config.colorAtSwitchOnNightTime === '' || config.colorAtSwitchOnNightTime === undefined) ? '{ "kelvin":2700, "brightness":20 }' : config.colorAtSwitchOnNightTime;
|
|
41
|
+
|
|
42
|
+
// Transform HEX in RGB and stringified json in json oblects.
|
|
43
|
+
if (config.colorAtSwitchOnDayTime.indexOf("#") !== -1) {
|
|
44
|
+
// Transform to rgb.
|
|
45
|
+
try {
|
|
46
|
+
config.colorAtSwitchOnDayTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnDayTime.replace("#", ""));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
config.colorAtSwitchOnDayTime = { kelvin: 3000, brightness: 100 };
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
config.colorAtSwitchOnDayTime = JSON.parse(config.colorAtSwitchOnDayTime);
|
|
52
|
+
}
|
|
53
|
+
// Same thing, but with night color
|
|
54
|
+
if (config.colorAtSwitchOnNightTime.indexOf("#") !== -1) {
|
|
55
|
+
// Transform to rgb.
|
|
56
|
+
try {
|
|
57
|
+
config.colorAtSwitchOnNightTime = hueColorConverter.ColorConverter.hexRgb(config.colorAtSwitchOnNightTime.replace("#", ""));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
config.colorAtSwitchOnNightTime = { kelvin: 2700, brightness: 20 };
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
config.colorAtSwitchOnNightTime = JSON.parse(config.colorAtSwitchOnNightTime);
|
|
63
|
+
}
|
|
38
64
|
|
|
39
65
|
// Used to call the status update from the config node.
|
|
40
66
|
node.setNodeStatus = ({
|
|
@@ -71,42 +97,42 @@ module.exports = function (RED) {
|
|
|
71
97
|
case config.GALightSwitch:
|
|
72
98
|
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightSwitch));
|
|
73
99
|
if (msg.payload === true) {
|
|
74
|
-
//
|
|
100
|
+
// Check wether the user selected specific color/brightness at switch on.
|
|
75
101
|
let jColorChoosen = null;
|
|
76
|
-
if (node.DayTime === true && (config.specifySwitchOnBrightness ===
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} catch (error) {
|
|
81
|
-
jColorChoosen = { red: 255, green: 255, blue: 255 };
|
|
82
|
-
}
|
|
83
|
-
} else if (node.DayTime === false && config.enableDayNightLighting === "yes") {
|
|
84
|
-
try {
|
|
85
|
-
jColorChoosen = JSON.parse(config.colorAtSwitchOnNightTime);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
jColorChoosen = { red: 10, green: 10, blue: 10 };
|
|
88
|
-
}
|
|
102
|
+
if (node.DayTime === true && (config.specifySwitchOnBrightness === "yes" || config.specifySwitchOnBrightness === "temperature")) {
|
|
103
|
+
jColorChoosen = config.colorAtSwitchOnDayTime;
|
|
104
|
+
} else if (node.DayTime === false && (config.enableDayNightLighting === "yes" || config.enableDayNightLighting === "temperature")) {
|
|
105
|
+
jColorChoosen = config.colorAtSwitchOnNightTime;
|
|
89
106
|
}
|
|
90
|
-
|
|
107
|
+
|
|
108
|
+
// Now we have a jColorChoosen. Proceed illuminating the light
|
|
109
|
+
if (jColorChoosen !== null && jColorChoosen.kelvin === undefined) {
|
|
110
|
+
// RGB
|
|
91
111
|
let gamut = null;
|
|
92
|
-
if (
|
|
93
|
-
node.currentHUEDevice !== undefined
|
|
94
|
-
&& node.currentHUEDevice.hasOwnProperty("color")
|
|
95
|
-
&& node.currentHUEDevice.color.hasOwnProperty("gamut_type")
|
|
96
|
-
) {
|
|
112
|
+
if (node.currentHUEDevice !== undefined && node.currentHUEDevice.hasOwnProperty("color") && node.currentHUEDevice.color.hasOwnProperty("gamut_type")) {
|
|
97
113
|
gamut = node.currentHUEDevice.color.gamut_type;
|
|
98
114
|
}
|
|
99
115
|
const dretXY = hueColorConverter.ColorConverter.rgbToXy(jColorChoosen.red, jColorChoosen.green, jColorChoosen.blue, gamut);
|
|
100
|
-
const dbright = hueColorConverter.ColorConverter.
|
|
101
|
-
node.currentHUEDevice.dimming.brightness = dbright;
|
|
116
|
+
const dbright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(jColorChoosen.red, jColorChoosen.green, jColorChoosen.blue);
|
|
117
|
+
node.currentHUEDevice.dimming.brightness = Math.round(dbright, 0);
|
|
102
118
|
node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness);
|
|
103
119
|
state = dbright > 0 ? { on: { on: true }, dimming: { brightness: dbright }, color: { xy: dretXY } } : { on: { on: false } };
|
|
104
|
-
}
|
|
120
|
+
} if (jColorChoosen !== null && jColorChoosen.kelvin !== undefined) {
|
|
121
|
+
// Kelvin
|
|
122
|
+
const dbright = jColorChoosen.brightness;
|
|
123
|
+
const mirek = hueColorConverter.ColorConverter.kelvinToMirek(jColorChoosen.kelvin);
|
|
124
|
+
node.currentHUEDevice.color_temperature.mirek = mirek;
|
|
125
|
+
node.currentHUEDevice.dimming.brightness = dbright;
|
|
126
|
+
node.updateKNXBrightnessState(node.currentHUEDevice.dimming.brightness);
|
|
127
|
+
// Kelvin temp
|
|
128
|
+
state = dbright > 0 ? { on: { on: true }, dimming: { brightness: dbright }, color_temperature: { mirek: mirek } } : { on: { on: false } };
|
|
129
|
+
} else if (jColorChoosen === null || jColorChoosen === undefined) {
|
|
105
130
|
state = { on: { on: true } };
|
|
106
131
|
}
|
|
107
132
|
} else {
|
|
108
133
|
state = { on: { on: false } };
|
|
109
134
|
}
|
|
135
|
+
|
|
110
136
|
node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight");
|
|
111
137
|
node.setNodeStatusHue({
|
|
112
138
|
fill: "green",
|
|
@@ -125,30 +151,37 @@ module.exports = function (RED) {
|
|
|
125
151
|
});
|
|
126
152
|
break;
|
|
127
153
|
case config.GALightKelvin:
|
|
154
|
+
let retMirek;
|
|
128
155
|
msg.payload = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptLightKelvin));
|
|
129
|
-
|
|
156
|
+
if (config.dptLightKelvin === "7.600") {
|
|
157
|
+
if (msg.payload > 65535) msg.payload = 65535;
|
|
158
|
+
if (msg.payload < 0) msg.payload = 0;
|
|
159
|
+
retMirek = hueColorConverter.ColorConverter.scale(msg.payload, [0, 65535], [500, 153]);
|
|
160
|
+
} else if (config.dptLightKelvin === "9.002") {
|
|
161
|
+
// Relative temperature in Kelvin. Use HUE scale.
|
|
162
|
+
if (msg.payload > 6535) msg.payload = 6535;
|
|
163
|
+
if (msg.payload < 2000) msg.payload = 2000;
|
|
164
|
+
retMirek = hueColorConverter.ColorConverter.scale(msg.payload, [2000, 6535], [500, 153]);
|
|
165
|
+
}
|
|
130
166
|
state = { color_temperature: { mirek: retMirek } };
|
|
131
167
|
node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight");
|
|
132
168
|
node.setNodeStatusHue({
|
|
133
169
|
fill: "green",
|
|
134
170
|
shape: "dot",
|
|
135
171
|
text: "KNX->HUE",
|
|
136
|
-
payload:
|
|
172
|
+
payload: msg.payload,
|
|
137
173
|
});
|
|
138
174
|
break;
|
|
139
175
|
case config.GADaylightSensor:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else {
|
|
150
|
-
node.DayTime = true;
|
|
151
|
-
}
|
|
176
|
+
node.DayTime = Boolean(dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(config.dptDaylightSensor)));
|
|
177
|
+
if (config.invertDayNight !== undefined && config.invertDayNight === true) node.DayTime = !node.DayTime;
|
|
178
|
+
node.setNodeStatusHue({
|
|
179
|
+
fill: "green",
|
|
180
|
+
shape: "dot",
|
|
181
|
+
text: "KNX->HUE Daytime",
|
|
182
|
+
payload: node.DayTime,
|
|
183
|
+
});
|
|
184
|
+
|
|
152
185
|
break;
|
|
153
186
|
case config.GALightHSV:
|
|
154
187
|
if (config.dptLightHSV === "3.007") {
|
|
@@ -209,7 +242,7 @@ module.exports = function (RED) {
|
|
|
209
242
|
gamut = node.currentHUEDevice.color.gamut_type;
|
|
210
243
|
}
|
|
211
244
|
const retXY = hueColorConverter.ColorConverter.rgbToXy(msg.payload.red, msg.payload.green, msg.payload.blue, gamut);
|
|
212
|
-
const bright = hueColorConverter.ColorConverter.
|
|
245
|
+
const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(msg.payload.red, msg.payload.green, msg.payload.blue);
|
|
213
246
|
// state = bright > 0 ? { on: { on: true }, dimming: { brightness: bright }, color: { xy: retXY } } : { on: { on: false } }
|
|
214
247
|
state = { dimming: { brightness: bright }, color: { xy: retXY } };
|
|
215
248
|
if (node.currentHUEDevice === undefined) {
|
|
@@ -276,7 +309,7 @@ module.exports = function (RED) {
|
|
|
276
309
|
gamut = node.currentHUEDevice.color.gamut_type;
|
|
277
310
|
}
|
|
278
311
|
const retXY = hueColorConverter.ColorConverter.rgbToXy(red, green, blue, gamut);
|
|
279
|
-
const bright = hueColorConverter.ColorConverter.
|
|
312
|
+
const bright = hueColorConverter.ColorConverter.getBrightnessFromRGBOrHex(red, green, blue);
|
|
280
313
|
state = bright > 0 ? { on: { on: true }, dimming: { brightness: bright }, color: { xy: retXY } } : { on: { on: false } };
|
|
281
314
|
node.serverHue.hueManager.writeHueQueueAdd(node.hueDevice, state, node.isGrouped_light === false ? "setLight" : "setGroupedLight");
|
|
282
315
|
} catch (error) { }
|
|
@@ -455,10 +488,10 @@ module.exports = function (RED) {
|
|
|
455
488
|
if (node.currentHUEDevice.hasOwnProperty("on") && node.currentHUEDevice.on.on === false && _event.dimming.brightness === 0) {
|
|
456
489
|
// Do nothing, because the light is off and the dimming also is 0
|
|
457
490
|
} else {
|
|
458
|
-
if (node.currentHUEDevice.on.on === false && (!_event.hasOwnProperty("on") || (_event.hasOwnProperty("on") && _event.on.on === true))) node.updateKNXLightState(_event.dimming.brightness > 0);
|
|
491
|
+
if (node.currentHUEDevice.hasOwnProperty("on") && node.currentHUEDevice.on.on === false && (!_event.hasOwnProperty("on") || (_event.hasOwnProperty("on") && _event.on.on === true))) node.updateKNXLightState(_event.dimming.brightness > 0);
|
|
459
492
|
node.updateKNXBrightnessState(_event.dimming.brightness);
|
|
460
493
|
// If the brightness reaches zero, the hue lamp "on" property must be set to zero as well
|
|
461
|
-
if (_event.dimming.brightness === 0) {
|
|
494
|
+
if (_event.dimming.brightness === 0 && node.currentHUEDevice.hasOwnProperty("on") && node.currentHUEDevice.on.on === true) {
|
|
462
495
|
node.serverHue.hueManager.writeHueQueueAdd(
|
|
463
496
|
node.hueDevice,
|
|
464
497
|
{ on: { on: false } },
|
|
@@ -481,6 +514,7 @@ module.exports = function (RED) {
|
|
|
481
514
|
shape: "dot",
|
|
482
515
|
text: `HUE->KNX error ${node.id} ${error.message}`,
|
|
483
516
|
});
|
|
517
|
+
RED.log.error(`knxUltimateHueLight: node.handleSendHUE = (_event): ${error.message}`);
|
|
484
518
|
}
|
|
485
519
|
};
|
|
486
520
|
|
|
@@ -604,17 +638,20 @@ module.exports = function (RED) {
|
|
|
604
638
|
knxMsgPayload.topic = config.GALightKelvinState;
|
|
605
639
|
knxMsgPayload.dpt = config.dptLightKelvinState;
|
|
606
640
|
if (config.dptLightKelvinState === "7.600") {
|
|
607
|
-
knxMsgPayload.payload = hueColorConverter.ColorConverter.scale(_value, [153, 500], [
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
641
|
+
knxMsgPayload.payload = hueColorConverter.ColorConverter.scale(_value, [153, 500], [65535, 0]);
|
|
642
|
+
} else if (config.dptLightKelvinState === "9.002") {
|
|
643
|
+
knxMsgPayload.payload = hueColorConverter.ColorConverter.scale(_value, [153, 500], [6535, 2000]);
|
|
644
|
+
}
|
|
645
|
+
// Send to KNX bus
|
|
646
|
+
if (knxMsgPayload.topic !== "" && knxMsgPayload.topic !== undefined) {
|
|
647
|
+
node.server.writeQueueAdd({
|
|
648
|
+
grpaddr: knxMsgPayload.topic,
|
|
649
|
+
payload: knxMsgPayload.payload,
|
|
650
|
+
dpt: knxMsgPayload.dpt,
|
|
651
|
+
outputtype: "write",
|
|
652
|
+
nodecallerid: node.id,
|
|
653
|
+
});
|
|
654
|
+
|
|
618
655
|
node.setNodeStatusHue({
|
|
619
656
|
fill: "blue",
|
|
620
657
|
shape: "ring",
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
|
+
|
|
1
3
|
<script type="text/javascript">
|
|
2
4
|
RED.nodes.registerType('knxUltimateHueLightSensor', {
|
|
3
5
|
category: "KNX Ultimate",
|
|
@@ -228,8 +230,7 @@
|
|
|
228
230
|
|
|
229
231
|
|
|
230
232
|
</script>
|
|
231
|
-
|
|
232
|
-
crossorigin="anonymous"></script>
|
|
233
|
+
|
|
233
234
|
|
|
234
235
|
<script type="text/markdown" data-help-name="knxUltimateHueLightSensor">
|
|
235
236
|
This node lets you get the events from your HUE motion device.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
|
+
|
|
1
3
|
<script type="text/javascript">
|
|
2
4
|
RED.nodes.registerType('knxUltimateHueMotion', {
|
|
3
5
|
category: "KNX Ultimate",
|
|
@@ -217,8 +219,6 @@
|
|
|
217
219
|
|
|
218
220
|
|
|
219
221
|
</script>
|
|
220
|
-
<script type="application/javascript;charset=utf-8" src="https://kit.fontawesome.com/11f26b4500.js"
|
|
221
|
-
crossorigin="anonymous"></script>
|
|
222
222
|
|
|
223
223
|
<script type="text/markdown" data-help-name="knxUltimateHueMotion">
|
|
224
224
|
This node lets you get the events from your HUE motion device.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
|
+
|
|
1
3
|
<script type="text/javascript">
|
|
2
4
|
RED.nodes.registerType('knxUltimateHueScene', {
|
|
3
5
|
category: "KNX Ultimate",
|
|
@@ -283,8 +285,6 @@
|
|
|
283
285
|
|
|
284
286
|
|
|
285
287
|
</script>
|
|
286
|
-
<script type="application/javascript;charset=utf-8" src="https://kit.fontawesome.com/11f26b4500.js"
|
|
287
|
-
crossorigin="anonymous"></script>
|
|
288
288
|
|
|
289
289
|
<script type="text/markdown" data-help-name="knxUltimateHueScene">
|
|
290
290
|
This node lets you recall a HUE scene, via KNX.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
|
+
|
|
1
3
|
<script type="text/javascript">
|
|
2
4
|
RED.nodes.registerType('knxUltimateHueTapDial', {
|
|
3
5
|
category: "KNX Ultimate",
|
|
@@ -219,8 +221,6 @@
|
|
|
219
221
|
|
|
220
222
|
|
|
221
223
|
</script>
|
|
222
|
-
<script type="application/javascript;charset=utf-8" src="https://kit.fontawesome.com/11f26b4500.js"
|
|
223
|
-
crossorigin="anonymous"></script>
|
|
224
224
|
|
|
225
225
|
<script type="text/markdown" data-help-name="knxUltimateHueTapDial">
|
|
226
226
|
This node lets you get the events from your HUE rotary device, for example the Tap Dial.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
|
|
2
|
+
|
|
1
3
|
<script type="text/javascript">
|
|
2
4
|
RED.nodes.registerType('knxUltimateHueTemperatureSensor', {
|
|
3
5
|
category: "KNX Ultimate",
|
|
@@ -227,8 +229,6 @@
|
|
|
227
229
|
|
|
228
230
|
|
|
229
231
|
</script>
|
|
230
|
-
<script type="application/javascript;charset=utf-8" src="https://kit.fontawesome.com/11f26b4500.js"
|
|
231
|
-
crossorigin="anonymous"></script>
|
|
232
232
|
|
|
233
233
|
<script type="text/markdown" data-help-name="knxUltimateHueTemperatureSensor" This node lets you get the events from
|
|
234
234
|
your HUE temperature device. Here you can get the HUE temperature events, that represents a celsius value, evetytime
|
|
@@ -1,7 +1,165 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Part of this code, thanks to https://github.com/Shnoo/js-CIE-1931-rgb-color-converter
|
|
3
2
|
class ColorConverter {
|
|
3
|
+
// static getBrightnessFromRGBOrHex(red, green, blue) {
|
|
4
|
+
// const hsv = convert.rgb.hsv(red, green, blue);
|
|
5
|
+
// const brightness = hsv[2];
|
|
6
|
+
// return brightness;
|
|
7
|
+
// }
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
static getBrightnessFromRGBOrHex(Rint, Gint, Bint) { // takes sRGB channels as 8 bit integers
|
|
11
|
+
|
|
12
|
+
var Rlin = (Rint / 255.0) ** 2.218; // Convert int to decimal 0-1 and linearize
|
|
13
|
+
var Glin = (Gint / 255.0) ** 2.218; // ** is the exponentiation operator, older JS needs Math.pow() instead
|
|
14
|
+
var 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
|
|
15
|
+
|
|
16
|
+
var Ylum = Rlin * 0.2126 + Glin * 0.7156 + Blin * 0.0722; // convert to Luminance Y
|
|
17
|
+
|
|
18
|
+
return Math.pow(Ylum, 0.43) * 100; // Convert to lightness (0 to 100)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static convert_1_255_ToPercentage(number) {
|
|
22
|
+
const percentage = (number / 255) * 100;
|
|
23
|
+
return percentage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static kelvinToMirek(_kelvin) {
|
|
27
|
+
return Math.round(1000000 / _kelvin, 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static mirekToKelvin(_mirek) {
|
|
31
|
+
return Math.round(1000000 / _mirek, 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Linear interpolation of input y given starting and ending ranges
|
|
35
|
+
static scale(y, range1 = [0, 100], range2 = [0, 255]) {
|
|
36
|
+
const [xMin, xMax] = range2;
|
|
37
|
+
const [yMin, yMax] = range1;
|
|
38
|
+
const percent = (y - yMin) / (yMax - yMin);
|
|
39
|
+
const ans = percent * (xMax - xMin) + xMin;
|
|
40
|
+
return Math.round(ans);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Thanks to: https://github.com/sindresorhus/rgb-hex
|
|
44
|
+
static rgbHex(red, green, blue, alpha) {
|
|
45
|
+
const toHex = (red, green, blue, alpha) => ((blue | green << 8 | red << 16) | 1 << 24).toString(16).slice(1) + alpha;
|
|
46
|
+
const parseCssRgbString = (input) => {
|
|
47
|
+
const parts = input.replace(/rgba?\(([^)]+)\)/, '$1').split(/[,\s/]+/).filter(Boolean);
|
|
48
|
+
if (parts.length < 3) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parseValue = (value, max) => {
|
|
53
|
+
value = value.trim();
|
|
54
|
+
|
|
55
|
+
if (value.endsWith('%')) {
|
|
56
|
+
return Math.min(Number.parseFloat(value) * max / 100, max);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Math.min(Number.parseFloat(value), max);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const red = parseValue(parts[0], 255);
|
|
63
|
+
const green = parseValue(parts[1], 255);
|
|
64
|
+
const blue = parseValue(parts[2], 255);
|
|
65
|
+
let alpha;
|
|
66
|
+
|
|
67
|
+
if (parts.length === 4) {
|
|
68
|
+
alpha = parseValue(parts[3], 1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [red, green, blue, alpha];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let isPercent = (red + (alpha || '')).toString().includes('%');
|
|
75
|
+
|
|
76
|
+
if (typeof red === 'string' && !green) { // Single string parameter.
|
|
77
|
+
const parsed = parseCssRgbString(red);
|
|
78
|
+
if (!parsed) {
|
|
79
|
+
throw new TypeError('Invalid or unsupported color format.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
isPercent = false;
|
|
83
|
+
[red, green, blue, alpha] = parsed;
|
|
84
|
+
} else if (alpha !== undefined) {
|
|
85
|
+
alpha = Number.parseFloat(alpha);
|
|
86
|
+
}
|
|
4
87
|
|
|
88
|
+
if (typeof red !== 'number'
|
|
89
|
+
|| typeof green !== 'number'
|
|
90
|
+
|| typeof blue !== 'number'
|
|
91
|
+
|| red > 255
|
|
92
|
+
|| green > 255
|
|
93
|
+
|| blue > 255
|
|
94
|
+
) {
|
|
95
|
+
throw new TypeError('Expected three numbers below 256');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof alpha === 'number') {
|
|
99
|
+
if (!isPercent && alpha >= 0 && alpha <= 1) {
|
|
100
|
+
alpha = Math.round(255 * alpha);
|
|
101
|
+
} else if (isPercent && alpha >= 0 && alpha <= 100) {
|
|
102
|
+
alpha = Math.round(255 * alpha / 100);
|
|
103
|
+
} else {
|
|
104
|
+
throw new TypeError(`Expected alpha value (${alpha}) as a fraction or percentage`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
alpha = (alpha | 1 << 8).toString(16).slice(1); // eslint-disable-line no-mixed-operators
|
|
108
|
+
} else {
|
|
109
|
+
alpha = '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return toHex(red, green, blue, alpha);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Thanks to: https://github.com/sindresorhus/hex-rgb
|
|
116
|
+
static hexRgb(hex, options = {}) {
|
|
117
|
+
const hexCharacters = 'a-f\\d';
|
|
118
|
+
const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`;
|
|
119
|
+
const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`;
|
|
120
|
+
const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi');
|
|
121
|
+
const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i');
|
|
122
|
+
|
|
123
|
+
if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) {
|
|
124
|
+
throw new TypeError('Expected a valid hex string');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
hex = hex.replace(/^#/, '');
|
|
128
|
+
let alphaFromHex = 1;
|
|
129
|
+
|
|
130
|
+
if (hex.length === 8) {
|
|
131
|
+
alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255;
|
|
132
|
+
hex = hex.slice(0, 6);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (hex.length === 4) {
|
|
136
|
+
alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255;
|
|
137
|
+
hex = hex.slice(0, 3);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hex.length === 3) {
|
|
141
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const number = Number.parseInt(hex, 16);
|
|
145
|
+
const red = number >> 16;
|
|
146
|
+
const green = (number >> 8) & 255;
|
|
147
|
+
const blue = number & 255;
|
|
148
|
+
const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex;
|
|
149
|
+
|
|
150
|
+
if (options.format === 'array') {
|
|
151
|
+
return [red, green, blue, alpha];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.format === 'css') {
|
|
155
|
+
const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`;
|
|
156
|
+
return `rgb(${red} ${green} ${blue}${alphaString})`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
red, green, blue, alpha,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
5
163
|
|
|
6
164
|
static getGamutRanges() {
|
|
7
165
|
const gamutA = {
|
|
@@ -33,31 +191,52 @@ class ColorConverter {
|
|
|
33
191
|
};
|
|
34
192
|
}
|
|
35
193
|
|
|
36
|
-
static getLightColorGamutRange(
|
|
194
|
+
static getLightColorGamutRange(modelId = null) {
|
|
37
195
|
const ranges = ColorConverter.getGamutRanges();
|
|
38
196
|
const { gamutA } = ranges;
|
|
39
197
|
const { gamutB } = ranges;
|
|
40
198
|
const { gamutC } = ranges;
|
|
41
199
|
|
|
42
200
|
const philipsModels = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
201
|
+
LST001: gamutA,
|
|
202
|
+
LLC010: gamutA,
|
|
203
|
+
LLC011: gamutA,
|
|
204
|
+
LLC012: gamutA,
|
|
205
|
+
LLC006: gamutA,
|
|
206
|
+
LLC005: gamutA,
|
|
207
|
+
LLC007: gamutA,
|
|
208
|
+
LLC014: gamutA,
|
|
209
|
+
LLC013: gamutA,
|
|
210
|
+
|
|
211
|
+
LCT001: gamutB,
|
|
212
|
+
LCT007: gamutB,
|
|
213
|
+
LCT002: gamutB,
|
|
214
|
+
LCT003: gamutB,
|
|
215
|
+
LLM001: gamutB,
|
|
216
|
+
|
|
217
|
+
LCT010: gamutC,
|
|
218
|
+
LCT014: gamutC,
|
|
219
|
+
LCT015: gamutC,
|
|
220
|
+
LCT016: gamutC,
|
|
221
|
+
LCT011: gamutC,
|
|
222
|
+
LLC020: gamutC,
|
|
223
|
+
LST002: gamutC,
|
|
224
|
+
LCT012: gamutC,
|
|
46
225
|
};
|
|
47
226
|
|
|
48
|
-
if (philipsModels[
|
|
49
|
-
return philipsModels[
|
|
227
|
+
if (philipsModels[modelId]) {
|
|
228
|
+
return philipsModels[modelId];
|
|
50
229
|
}
|
|
51
230
|
|
|
52
231
|
return ranges.default;
|
|
53
232
|
}
|
|
54
233
|
|
|
55
|
-
static rgbToXy(red, green, blue,
|
|
234
|
+
static rgbToXy(red, green, blue, modelId = null) {
|
|
56
235
|
function getGammaCorrectedValue(value) {
|
|
57
236
|
return (value > 0.04045) ? ((value + 0.055) / (1.0 + 0.055)) ** 2.4 : (value / 12.92);
|
|
58
237
|
}
|
|
59
238
|
|
|
60
|
-
const colorGamut = ColorConverter.getLightColorGamutRange(
|
|
239
|
+
const colorGamut = ColorConverter.getLightColorGamutRange(modelId);
|
|
61
240
|
|
|
62
241
|
red = parseFloat(red / 255);
|
|
63
242
|
green = parseFloat(green / 255);
|
|
@@ -83,17 +262,6 @@ class ColorConverter {
|
|
|
83
262
|
return xy;
|
|
84
263
|
}
|
|
85
264
|
|
|
86
|
-
static getBrightnessFromRGB(red, green, blue) {
|
|
87
|
-
const hsv = convert.rgb.hsv(red, green, blue);
|
|
88
|
-
const brightness = hsv[2];
|
|
89
|
-
return brightness;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
static convert_1_255_ToPercentage(number) {
|
|
93
|
-
const percentage = (number / 255) * 100;
|
|
94
|
-
return percentage;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
265
|
static xyIsInGamutRange(xy, gamut) {
|
|
98
266
|
gamut = gamut || ColorConverter.getGamutRanges().gamutC;
|
|
99
267
|
if (Array.isArray(xy)) {
|
|
@@ -211,9 +379,9 @@ class ColorConverter {
|
|
|
211
379
|
const Y = bri / 255;
|
|
212
380
|
const X = (Y / y) * x;
|
|
213
381
|
const Z = (Y / y) * z;
|
|
214
|
-
let r = X * 1.
|
|
215
|
-
let g = -X * 0.
|
|
216
|
-
let b = X * 0.
|
|
382
|
+
let r = X * 1.656492 - Y * 0.354851 - Z * 0.255038;
|
|
383
|
+
let g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152;
|
|
384
|
+
let b = X * 0.051713 - Y * 0.121364 + Z * 1.011530;
|
|
217
385
|
|
|
218
386
|
r = getReversedGammaCorrectedValue(r);
|
|
219
387
|
g = getReversedGammaCorrectedValue(g);
|
|
@@ -233,19 +401,10 @@ class ColorConverter {
|
|
|
233
401
|
}
|
|
234
402
|
|
|
235
403
|
return {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
404
|
+
r: Math.floor(r * 255),
|
|
405
|
+
g: Math.floor(g * 255),
|
|
406
|
+
b: Math.floor(b * 255),
|
|
239
407
|
};
|
|
240
408
|
}
|
|
241
|
-
|
|
242
|
-
// Linear interpolation of input y given starting and ending ranges
|
|
243
|
-
static scale(y, range1 = [0, 100], range2 = [0, 255]) {
|
|
244
|
-
const [xMin, xMax] = range2;
|
|
245
|
-
const [yMin, yMax] = range1;
|
|
246
|
-
const percent = (y - yMin) / (yMax - yMin);
|
|
247
|
-
const ans = percent * (xMax - xMin) + xMin;
|
|
248
|
-
return Math.round(ans);
|
|
249
|
-
}
|
|
250
409
|
}
|
|
251
410
|
exports.ColorConverter = ColorConverter;
|
package/package.json
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=16.0.0"
|
|
5
5
|
},
|
|
6
|
-
"version": "2.2.
|
|
6
|
+
"version": "2.2.22",
|
|
7
7
|
"description": "Control your KNX intallation via Node-Red! Single Node KNX IN/OUT with optional ETS group address importer. Easy to use and highly configurable. With integrated Philips HUE devices control.",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"binary-parser": "2.2.1",
|
|
10
|
-
"color-convert": "2.0.1",
|
|
11
10
|
"crypto-js": "4.2.0",
|
|
12
11
|
"dns-sync": "0.2.1",
|
|
13
12
|
"eventsource": "2.0.2",
|