From 3854c30ab945874487cfe03e0c477cd8ab48b34c Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Thu, 9 May 2024 11:31:29 +0200 Subject: [PATCH] Philips Hue : Poll color/brightness, reduce poll time & update dependencies (#2071) Co-authored-by: Cyril Beslay --- server/lib/device/device.setupPoll.js | 2 + .../philips-hue/lib/light/light.getLights.js | 2 +- .../philips-hue/lib/light/light.poll.js | 48 +++++- .../services/philips-hue/lib/models/color.js | 2 +- .../lib/models/colorWithTemperature.js | 2 +- .../philips-hue/lib/models/plugOnOff.js | 2 +- .../services/philips-hue/lib/models/white.js | 2 +- .../lib/models/whiteWithTemperature.js | 2 +- server/services/philips-hue/package-lock.json | 14 +- server/services/philips-hue/package.json | 2 +- .../philips-hue/light/light.poll.test.js | 163 ++++++++++++++++-- .../test/services/philips-hue/mocks.test.js | 20 ++- server/utils/constants.js | 1 + 13 files changed, 227 insertions(+), 35 deletions(-) diff --git a/server/lib/device/device.setupPoll.js b/server/lib/device/device.setupPoll.js index 409f0feae8..a0f7f68240 100644 --- a/server/lib/device/device.setupPoll.js +++ b/server/lib/device/device.setupPoll.js @@ -10,6 +10,8 @@ function setupPoll() { setInterval(this.pollAll(DEVICE_POLL_FREQUENCIES.EVERY_MINUTES), DEVICE_POLL_FREQUENCIES.EVERY_MINUTES); // poll devices who need to be polled every 30 seconds setInterval(this.pollAll(DEVICE_POLL_FREQUENCIES.EVERY_30_SECONDS), DEVICE_POLL_FREQUENCIES.EVERY_30_SECONDS); + // poll devices who need to be polled every 15 seconds + setInterval(this.pollAll(DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS), DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS); // poll devices who need to be polled every 10 seconds setInterval(this.pollAll(DEVICE_POLL_FREQUENCIES.EVERY_10_SECONDS), DEVICE_POLL_FREQUENCIES.EVERY_10_SECONDS); // poll devices who need to be polled every 2 seconds diff --git a/server/services/philips-hue/lib/light/light.getLights.js b/server/services/philips-hue/lib/light/light.getLights.js index 5591babc86..386a8320de 100644 --- a/server/services/philips-hue/lib/light/light.getLights.js +++ b/server/services/philips-hue/lib/light/light.getLights.js @@ -54,7 +54,7 @@ async function getLights() { selector: `${LIGHT_EXTERNAL_ID_BASE}:${serialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [], not_handled: true, raw_philips_hue_device: philipsHueLight, diff --git a/server/services/philips-hue/lib/light/light.poll.js b/server/services/philips-hue/lib/light/light.poll.js index a079469274..0da231fb55 100644 --- a/server/services/philips-hue/lib/light/light.poll.js +++ b/server/services/philips-hue/lib/light/light.poll.js @@ -1,10 +1,11 @@ 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 logger = require('../../../../utils/logger'); const { parseExternalId } = require('../utils/parseExternalId'); const { NotFoundError } = require('../../../../utils/coreErrors'); -const { getDeviceFeature } = require('../../../../utils/device'); +const { getDeviceFeature, normalize } = require('../../../../utils/device'); /** * @description Poll value of a Philips hue. @@ -19,17 +20,58 @@ async function poll(device) { throw new NotFoundError(`HUE_API_NOT_FOUND`); } const state = await hueApi.lights.getLightState(lightId); + + // if the binary value is different from the value we have, save new state const currentBinaryState = state.on ? 1 : 0; const binaryFeature = getDeviceFeature(device, DEVICE_FEATURE_CATEGORIES.LIGHT, DEVICE_FEATURE_TYPES.LIGHT.BINARY); - // if the value is different from the value we have, save new state if (binaryFeature && binaryFeature.last_value !== currentBinaryState) { - logger.debug(`Polling Philips Hue ${lightId}, new value = ${currentBinaryState}`); + logger.debug(`Polling Philips Hue ${lightId}, new binary value = ${currentBinaryState}`); this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { device_feature_external_id: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${lightId}:${DEVICE_FEATURE_TYPES.LIGHT.BINARY}`, state: currentBinaryState, }); } + + // if the color value is different from the value we have, save new state + let currentColorState; + switch (state.colormode) { + case 'ct': + case 'xy': + currentColorState = xyToInt(state.xy[0], state.xy[1]); + break; + case 'hs': + currentColorState = rgbToInt(hsbToRgb([state.hue, state.sat, state.bri])); + break; + default: + } + const colorFeature = getDeviceFeature(device, DEVICE_FEATURE_CATEGORIES.LIGHT, DEVICE_FEATURE_TYPES.LIGHT.COLOR); + + if (colorFeature && colorFeature.last_value !== currentColorState) { + logger.debug( + `Polling Philips Hue ${lightId}, new color value = ${currentColorState} from color mode ${state.colormode}`, + ); + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${lightId}:${DEVICE_FEATURE_TYPES.LIGHT.COLOR}`, + state: currentColorState, + }); + } + + // if the brightness value is different from the value we have, save new state + const brightnessColorState = state.bri; + const brightnessFeature = getDeviceFeature( + device, + DEVICE_FEATURE_CATEGORIES.LIGHT, + DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS, + ); + const newBrightnessValue = Math.round(normalize(brightnessColorState, 0, 254, 0, 100)); + if (brightnessFeature && brightnessFeature.last_value !== newBrightnessValue) { + logger.debug(`Polling Philips Hue ${lightId}, new brightness value = ${newBrightnessValue}`); + this.gladys.event.emit(EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${lightId}:${DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS}`, + state: newBrightnessValue, + }); + } } module.exports = { diff --git a/server/services/philips-hue/lib/models/color.js b/server/services/philips-hue/lib/models/color.js index 2ec3a5d3f0..ffadee167d 100644 --- a/server/services/philips-hue/lib/models/color.js +++ b/server/services/philips-hue/lib/models/color.js @@ -13,7 +13,7 @@ const getPhilipsHueColorLight = (philipsHueLight, bridgeSerialNumber, serviceId) selector: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [ { name: `${philipsHueLight.name} On/Off`, diff --git a/server/services/philips-hue/lib/models/colorWithTemperature.js b/server/services/philips-hue/lib/models/colorWithTemperature.js index e31f85c9a4..72374b95f6 100644 --- a/server/services/philips-hue/lib/models/colorWithTemperature.js +++ b/server/services/philips-hue/lib/models/colorWithTemperature.js @@ -13,7 +13,7 @@ const getPhilipsHueColorTemperatureLight = (philipsHueLight, bridgeSerialNumber, selector: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [ { name: `${philipsHueLight.name} On/Off`, diff --git a/server/services/philips-hue/lib/models/plugOnOff.js b/server/services/philips-hue/lib/models/plugOnOff.js index fd122e954a..da27ada440 100644 --- a/server/services/philips-hue/lib/models/plugOnOff.js +++ b/server/services/philips-hue/lib/models/plugOnOff.js @@ -13,7 +13,7 @@ const getPlugOnOff = (philipsHueLight, bridgeSerialNumber, serviceId) => ({ selector: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [ { name: `${philipsHueLight.name} On/Off`, diff --git a/server/services/philips-hue/lib/models/white.js b/server/services/philips-hue/lib/models/white.js index 58efb3820e..6dabe86a7f 100644 --- a/server/services/philips-hue/lib/models/white.js +++ b/server/services/philips-hue/lib/models/white.js @@ -13,7 +13,7 @@ const getPhilipsHueWhiteLight = (philipsHueLight, bridgeSerialNumber, serviceId) selector: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [ { name: `${philipsHueLight.name} On/Off`, diff --git a/server/services/philips-hue/lib/models/whiteWithTemperature.js b/server/services/philips-hue/lib/models/whiteWithTemperature.js index 09aeb822be..619b04e309 100644 --- a/server/services/philips-hue/lib/models/whiteWithTemperature.js +++ b/server/services/philips-hue/lib/models/whiteWithTemperature.js @@ -13,7 +13,7 @@ const getPhilipsHueWhiteTemperatureLight = (philipsHueLight, bridgeSerialNumber, selector: `${LIGHT_EXTERNAL_ID_BASE}:${bridgeSerialNumber}:${philipsHueLight.id}`, should_poll: true, model: philipsHueLight.modelid, - poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_MINUTES, + poll_frequency: DEVICE_POLL_FREQUENCIES.EVERY_15_SECONDS, features: [ { name: `${philipsHueLight.name} On/Off`, diff --git a/server/services/philips-hue/package-lock.json b/server/services/philips-hue/package-lock.json index 750d60335f..15ab692fa9 100644 --- a/server/services/philips-hue/package-lock.json +++ b/server/services/philips-hue/package-lock.json @@ -18,7 +18,7 @@ "win32" ], "dependencies": { - "bluebird": "^3.7.0", + "bluebird": "^3.7.2", "bottleneck": "^2.19.5", "node-hue-api": "^4.0.11" } @@ -32,9 +32,9 @@ } }, "node_modules/bluebird": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", - "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/bottleneck": { "version": "2.19.5", @@ -89,9 +89,9 @@ } }, "bluebird": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.0.tgz", - "integrity": "sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bottleneck": { "version": "2.19.5", diff --git a/server/services/philips-hue/package.json b/server/services/philips-hue/package.json index efbad9698c..e91e9e1839 100644 --- a/server/services/philips-hue/package.json +++ b/server/services/philips-hue/package.json @@ -13,7 +13,7 @@ "arm64" ], "dependencies": { - "bluebird": "^3.7.0", + "bluebird": "^3.7.2", "bottleneck": "^2.19.5", "node-hue-api": "^4.0.11" } 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 abccc98e65..3bb3d0ca53 100644 --- a/server/test/services/philips-hue/light/light.poll.test.js +++ b/server/test/services/philips-hue/light/light.poll.test.js @@ -1,17 +1,13 @@ -const { assert } = require('chai'); -const { fake } = require('sinon'); -const EventEmitter = require('events'); +const { expect } = require('chai'); +const { assert, fake } = require('sinon'); const proxyquire = require('proxyquire').noCallThru(); -const { MockedPhilipsHueClient } = require('../mocks.test'); +const { MockedPhilipsHueClient, hueApiHsColorMode } = require('../mocks.test'); +const { EVENTS } = require('../../../../utils/constants'); const PhilipsHueService = proxyquire('../../../../services/philips-hue/index', { 'node-hue-api': MockedPhilipsHueClient, }); -const StateManager = require('../../../../lib/state'); - -const event = new EventEmitter(); -const stateManager = new StateManager(event); const deviceManager = { get: fake.resolves([ { @@ -59,13 +55,35 @@ const deviceManager = { ]), }; -const gladys = { - device: deviceManager, - stateManager, - event, -}; +describe('PhilipsHueService Poll', () => { + let gladys; + let initialApi; + + beforeEach(() => { + gladys = { + job: { + wrapper: (type, func) => { + return async () => { + return func(); + }; + }, + }, + event: { + emit: fake.resolves(null), + }, + device: deviceManager, + stateManager: { + get: fake.resolves(true), + }, + }; + initialApi = MockedPhilipsHueClient.v3.api; + }); + + afterEach(() => { + // Reset API after each test to avoid bugs + MockedPhilipsHueClient.v3.api = initialApi; + }); -describe('PhilipsHueService', () => { it('should poll light', async () => { const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); await philipsHueService.device.init(); @@ -82,8 +100,28 @@ describe('PhilipsHueService', () => { it('should return hue api not found', async () => { const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); await philipsHueService.device.init(); - const promise = philipsHueService.device.poll({ - external_id: 'light:not-found:1', + try { + await philipsHueService.device.poll({ + external_id: 'light:not-found:1', + features: [ + { + category: 'light', + type: 'binary', + }, + ], + }); + expect.fail(); + } catch (e) { + expect(e.message).eq('HUE_API_NOT_FOUND'); + } + }); + it('should poll light and update binary', async () => { + // PREPARE + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.init(); + // EXECUTE + await philipsHueService.device.poll({ + external_id: 'light:1234:1', features: [ { category: 'light', @@ -91,6 +129,97 @@ describe('PhilipsHueService', () => { }, ], }); - return assert.isRejected(promise, 'HUE_API_NOT_FOUND'); + // ASSERT + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:binary', + state: 0, + }); + }); + it('should poll light and update color for color mode xy', async () => { + // PREPARE + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.init(); + // EXECUTE + await philipsHueService.device.poll({ + external_id: 'light:1234:1', + features: [ + { + category: 'light', + type: 'color', + }, + ], + }); + // ASSERT + // Color is xy: [0.3321, 0.3605], so 16187362 in Int + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:color', + state: 16187362, + }); + }); + it('should poll light and update color for color mode hs', async () => { + // PREPARE + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + philipsHueService.device.hueClient.api = { + createLocal: () => ({ + connect: () => hueApiHsColorMode, + }), + }; + await philipsHueService.device.init(); + // EXECUTE + await philipsHueService.device.poll({ + external_id: 'light:1234:1', + features: [ + { + category: 'light', + type: 'color', + }, + ], + }); + // ASSERT + // Color is hsb: 67, so 16187362 in Int + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:color', + state: 11534095, + }); + }); + it('should poll light with mode ct and use xy', async () => { + // PREPARE + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.init(); + // EXECUTE + await philipsHueService.device.poll({ + external_id: 'light:1234:1', + features: [ + { + category: 'light', + type: 'color', + }, + ], + }); + // ASSERT + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:color', + state: 16187362, + }); + }); + it('should poll light and update bri', async () => { + // PREPARE + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.init(); + // EXECUTE + await philipsHueService.device.poll({ + external_id: 'light:1234:1', + features: [ + { + category: 'light', + type: 'brightness', + }, + ], + }); + // ASSERT + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'philips-hue-light:1234:1:brightness', + state: 22, + }); }); }); diff --git a/server/test/services/philips-hue/mocks.test.js b/server/test/services/philips-hue/mocks.test.js index a9a05a04a6..22fcfc618a 100644 --- a/server/test/services/philips-hue/mocks.test.js +++ b/server/test/services/philips-hue/mocks.test.js @@ -29,7 +29,7 @@ const hueApi = { setLightState: fake.resolves(null), getLightState: fake.resolves({ on: false, - bri: 0, + bri: 56, hue: 38191, sat: 94, effect: 'none', @@ -68,6 +68,23 @@ const hueApi = { }, }; +const hueApiHsColorMode = { + lights: { + getLightState: fake.resolves({ + on: false, + bri: 100, + hue: 35000, + sat: 94, + effect: 'none', + hs_color: [0.4, 0.1], + alert: 'select', + colormode: 'hs', + mode: 'homeautomation', + reachable: true, + }), + }, +}; + const MockedPhilipsHueClient = { v3: { lightStates: { @@ -123,4 +140,5 @@ module.exports = { STATE_OFF, fakes, hueApi, + hueApiHsColorMode, }; diff --git a/server/utils/constants.js b/server/utils/constants.js index 7aafa24b7d..587cad5ab9 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -886,6 +886,7 @@ const ACTIONS_STATUS = { const DEVICE_POLL_FREQUENCIES = { EVERY_MINUTES: 60 * 1000, EVERY_30_SECONDS: 30 * 1000, + EVERY_15_SECONDS: 15 * 1000, EVERY_10_SECONDS: 10 * 1000, EVERY_2_SECONDS: 2 * 1000, EVERY_SECONDS: 1 * 1000,