From 1a4c6d10e52b2db4019a45f094533b089dc42325 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Mon, 20 May 2024 08:56:15 +0200 Subject: [PATCH] Fix Philips Hue polling in color temperature only mode (#2081) --- .../philips-hue/lib/light/light.poll.js | 17 +++++- .../philips-hue/light/light.poll.test.js | 19 ++++++- .../test/services/philips-hue/mocks.test.js | 19 +++++++ server/test/utils/colors.test.js | 40 +++++++++++++- server/utils/colors.js | 55 +++++++++++++++++++ 5 files changed, 145 insertions(+), 5 deletions(-) diff --git a/server/services/philips-hue/lib/light/light.poll.js b/server/services/philips-hue/lib/light/light.poll.js index 0da231fb55..4cd1bbb39c 100644 --- a/server/services/philips-hue/lib/light/light.poll.js +++ b/server/services/philips-hue/lib/light/light.poll.js @@ -1,6 +1,6 @@ const { EVENTS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); const { LIGHT_EXTERNAL_ID_BASE } = require('../utils/consts'); -const { xyToInt, hsbToRgb, rgbToInt } = require('../../../../utils/colors'); +const { xyToInt, hsbToRgb, rgbToInt, kelvinToRGB, miredToKelvin } = require('../../../../utils/colors'); const logger = require('../../../../utils/logger'); const { parseExternalId } = require('../utils/parseExternalId'); @@ -37,6 +37,8 @@ async function poll(device) { let currentColorState; switch (state.colormode) { case 'ct': + currentColorState = rgbToInt(kelvinToRGB(miredToKelvin(state.ct))); + break; case 'xy': currentColorState = xyToInt(state.xy[0], state.xy[1]); break; @@ -72,6 +74,19 @@ async function poll(device) { state: newBrightnessValue, }); } + + const colorTemperatureFeature = getDeviceFeature( + device, + DEVICE_FEATURE_CATEGORIES.LIGHT, + DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE, + ); + if (colorTemperatureFeature && colorTemperatureFeature.last_value !== state.ct) { + logger.debug(`Polling Philips Hue ${lightId}, new color temperature value = ${state.ct}`); + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${lightId}:${DEVICE_FEATURE_TYPES.LIGHT.TEMPERATURE}`, + state: state.ct, + }); + } } module.exports = { diff --git a/server/test/services/philips-hue/light/light.poll.test.js b/server/test/services/philips-hue/light/light.poll.test.js index 3bb3d0ca53..3a5dca6571 100644 --- a/server/test/services/philips-hue/light/light.poll.test.js +++ b/server/test/services/philips-hue/light/light.poll.test.js @@ -1,7 +1,7 @@ const { expect } = require('chai'); const { assert, fake } = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); -const { MockedPhilipsHueClient, hueApiHsColorMode } = require('../mocks.test'); +const { MockedPhilipsHueClient, hueApiHsColorMode, hueApiCtColorMode } = require('../mocks.test'); const { EVENTS } = require('../../../../utils/constants'); const PhilipsHueService = proxyquire('../../../../services/philips-hue/index', { @@ -182,9 +182,14 @@ describe('PhilipsHueService Poll', () => { state: 11534095, }); }); - it('should poll light with mode ct and use xy', async () => { + it('should poll light with mode ct and update color temperature', async () => { // PREPARE const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + philipsHueService.device.hueClient.api = { + createLocal: () => ({ + connect: () => hueApiCtColorMode, + }), + }; await philipsHueService.device.init(); // EXECUTE await philipsHueService.device.poll({ @@ -194,12 +199,20 @@ describe('PhilipsHueService Poll', () => { category: 'light', type: 'color', }, + { + category: 'light', + type: 'temperature', + }, ], }); // ASSERT assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { device_feature_external_id: 'philips-hue-light:1234:1:color', - state: 16187362, + state: 16759424, + }); + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:temperature', + state: 305, }); }); it('should poll light and update bri', async () => { diff --git a/server/test/services/philips-hue/mocks.test.js b/server/test/services/philips-hue/mocks.test.js index 22fcfc618a..263d79e8f3 100644 --- a/server/test/services/philips-hue/mocks.test.js +++ b/server/test/services/philips-hue/mocks.test.js @@ -85,6 +85,24 @@ const hueApiHsColorMode = { }, }; +const hueApiCtColorMode = { + lights: { + getLightState: fake.resolves({ + on: true, + bri: 90, + hue: 16203, + sat: 76, + effect: 'none', + xy: [0.4181, 0.3975], + ct: 305, + alert: 'select', + colormode: 'ct', + mode: 'homeautomation', + reachable: true, + }), + }, +}; + const MockedPhilipsHueClient = { v3: { lightStates: { @@ -141,4 +159,5 @@ module.exports = { fakes, hueApi, hueApiHsColorMode, + hueApiCtColorMode, }; diff --git a/server/test/utils/colors.test.js b/server/test/utils/colors.test.js index 2a695f0c9c..fc60f6630f 100644 --- a/server/test/utils/colors.test.js +++ b/server/test/utils/colors.test.js @@ -1,5 +1,14 @@ const { expect } = require('chai'); -const { intToHex, hexToInt, intToRgb, rgbToInt, xyToInt, hsbToRgb, rgbToHsb } = require('../../utils/colors'); +const { + intToHex, + hexToInt, + intToRgb, + rgbToInt, + xyToInt, + hsbToRgb, + rgbToHsb, + kelvinToRGB, +} = require('../../utils/colors'); describe('colors', () => { const matchingTable = { @@ -67,3 +76,32 @@ describe('colors', () => { } }); }); + +describe('ColorTemperature', () => { + const kelvinInRgb = [ + { kelvin: 0, rgb: [255, 0, 0] }, + { kelvin: 1901, rgb: [255, 132, 0] }, + { kelvin: 2000, rgb: [255, 137, 14] }, + { kelvin: 5000, rgb: [255, 228, 206] }, + { kelvin: 8000, rgb: [221, 230, 255] }, + { kelvin: 6600, rgb: [255, 255, 255] }, + ]; + kelvinInRgb.forEach((color) => { + it(`color ${color.kelvin}K should equal ${color.rgb} in RGB`, () => { + const value = kelvinToRGB(color.kelvin); + expect(value).to.deep.equal(color.rgb); + }); + }); + it('should try all Kelvin from 0 to 50k', function Test() { + this.timeout(10000); + for (let i = 0; i < 50000; i += 1) { + const [r, g, b] = kelvinToRGB(i); + expect(r).to.be.greaterThanOrEqual(0); + expect(g).to.be.greaterThanOrEqual(0); + expect(b).to.be.greaterThanOrEqual(0); + expect(r).to.be.lessThanOrEqual(255); + expect(g).to.be.lessThanOrEqual(255); + expect(b).to.be.lessThanOrEqual(255); + } + }); +}); diff --git a/server/utils/colors.js b/server/utils/colors.js index 1d7387c2b9..e60656fa1e 100644 --- a/server/utils/colors.js +++ b/server/utils/colors.js @@ -103,6 +103,60 @@ function hexToInt(hexColor) { return parseInt(hexColor, 16); } +/** + * @description Convert Kelvin to RGB temp color. + * @param {number} kelvin - Color temperature in Kelvin. + * @returns {Array} - Return array of RGB. + * @example const [r, g, b] = kelvinToRGB(2500); + */ +function kelvinToRGB(kelvin) { + const temperature = kelvin / 100; + let red; + let green; + let blue; + + // Calculate Red + if (temperature <= 66) { + red = 255; + } else { + red = temperature - 60; + red = 329.698727446 * red ** -0.1332047592; + if (red > 255) { + red = 255; + } + } + + // Calculate Green + if (temperature <= 66) { + green = temperature; + green = 99.4708025861 * Math.log(green) - 161.1195681661; + if (green < 0) { + green = 0; + } + if (green > 255) { + green = 255; + } + } else { + green = temperature - 60; + green = 288.1221695283 * green ** -0.0755148492; + } + + // Calculate Blue + if (temperature >= 66) { + blue = 255; + } else if (temperature <= 19) { + blue = 0; + } else { + blue = temperature - 10; + blue = 138.5177312231 * Math.log(blue) - 305.0447927307; + if (blue < 0) { + blue = 0; + } + } + + return [Math.round(red), Math.round(green), Math.round(blue)]; +} + /** * @description Reverse Gamma correction applied in rgbToXy. * @param {number} value - Color value. @@ -191,4 +245,5 @@ module.exports = { rgbToHsb, miredToKelvin, kelvinToMired, + kelvinToRGB, };