From a6569de47597c2ce3a29b5db1e235fd4fd0f40b3 Mon Sep 17 00:00:00 2001 From: Pierre-Gilles Leymarie Date: Mon, 29 Apr 2024 16:33:02 +0200 Subject: [PATCH] Add Philips Hue sync with bridge button (#2063) --- front/src/config/i18n/de.json | 2 + front/src/config/i18n/en.json | 2 + front/src/config/i18n/fr.json | 2 + .../all/philips-hue/setup-page/SetupTab.jsx | 11 ++++- .../all/philips-hue/setup-page/index.js | 23 ++++++++-- .../philips-hue/api/hue.controller.js | 15 +++++++ .../services/philips-hue/lib/light/index.js | 2 + .../lib/light/light.syncWithBridge.js | 30 +++++++++++++ .../syncWithBridge.controller.test.js | 19 ++++++++ .../light/light.syncWithBridge.test.js | 45 +++++++++++++++++++ .../test/services/philips-hue/mocks.test.js | 2 + 11 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 server/services/philips-hue/lib/light/light.syncWithBridge.js create mode 100644 server/test/services/philips-hue/controllers/syncWithBridge.controller.test.js create mode 100644 server/test/services/philips-hue/light/light.syncWithBridge.test.js diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index d7f3612345..8e361ecd8a 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -479,6 +479,8 @@ "disconnectButton": "Trennen", "bridgesOnNetwork": "Bridges im Netzwerk", "connectButton": "Verbinden/Erneut verbinden", + "syncBridges": "Brücken synchronisieren", + "bridgeNotUpToDateInfo": "Wenn Sie gerade eine Glühbirne zur Philips Hue-App hinzugefügt haben und diese Glühbirne nicht direkt von Gladys gesteuert werden kann, müssen Sie hierher kommen und auf die Schaltfläche \"Brücken synchronisieren\" klicken, um alle Informationen über diese neue Glühbirne wieder in Gladys zu bringen.", "scanButton": "Suchen", "bridgeButtonNotPressed": "Bridge-Taste nicht gedrückt. Bitte drücke die Taste auf deiner Philips Hue Bridge und versuche es erneut.", "unknownError": "Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut oder kontaktiere die Gladys-Community.", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 371ba6d5d5..f7d969fd6b 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -479,6 +479,8 @@ "disconnectButton": "Disconnect", "bridgesOnNetwork": "Bridges on network", "connectButton": "Connect/Reconnect", + "syncBridges": "Sync bridges", + "bridgeNotUpToDateInfo": "If you've just added a bulb to the Philips Hue app and that bulb isn't directly controllable by Gladys, you need to come here and click the \"Sync Bridges\" button, which will bring back all the information about this new bulb into Gladys.", "scanButton": "Scan network", "bridgeButtonNotPressed": "Bridge button not pressed: Please press the button on your Philips Hue bridge and try again.", "unknownError": "An unknown error occurred. Please try again or contact Gladys community.", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 42b32b83f3..6efca3b807 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -608,6 +608,8 @@ "bridgesOnNetwork": "Ponts sur le réseau", "connectButton": "Connecter/Reconnecter", "scanButton": "Recherche sur le réseau", + "syncBridges": "Synchroniser les ponts", + "bridgeNotUpToDateInfo": "Si vous venez d'ajouter une ampoule à l'application Philips Hue, et que cette ampoule n'est pas directement contrôlable par Gladys, il faut venir ici et cliquer sur le bouton \"Synchroniser les ponts\" ce qui rapatriera dans Gladys toutes les informations sur cette nouvelle lampe.", "bridgeButtonNotPressed": "Le bouton du pont n'a pas été appuyé : veuillez appuyer sur le bouton de votre pont Philips Hue et réessayer.", "unknownError": "Une erreur inconnue s'est produite. Veuillez réessayer ou contacter Gladys community.", "noBridgesFound": "Nous n'avons trouvé aucun pont Philips Hue sur votre réseau. Êtes-vous sûr que vous êtes connecté au même réseau que votre pont et que celui-ci est sous tension?", diff --git a/front/src/routes/integration/all/philips-hue/setup-page/SetupTab.jsx b/front/src/routes/integration/all/philips-hue/setup-page/SetupTab.jsx index f3a106035e..b6200b2be2 100644 --- a/front/src/routes/integration/all/philips-hue/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/philips-hue/setup-page/SetupTab.jsx @@ -26,11 +26,16 @@ class SetupTab extends Component {

+
+ +
@@ -40,6 +45,10 @@ class SetupTab extends Component {

)} +

+ +

+ {props.syncWithBridgeError &&

{props.syncWithBridgeError}

} {props.philipsHueGetDevicesStatus === RequestStatus.Getting &&
}
{props.philipsHueBridgesDevices && diff --git a/front/src/routes/integration/all/philips-hue/setup-page/index.js b/front/src/routes/integration/all/philips-hue/setup-page/index.js index 46e8e9e9d7..076fadde27 100644 --- a/front/src/routes/integration/all/philips-hue/setup-page/index.js +++ b/front/src/routes/integration/all/philips-hue/setup-page/index.js @@ -2,25 +2,42 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import actions from './actions'; import PhilipsHuePage from '../PhilipsHuePage'; +import { RequestStatus } from '../../../../../utils/consts'; import SetupTab from './SetupTab'; class PhilipsHueSetupPage extends Component { + syncWithBridge = async () => { + try { + this.setState({ syncWithBridgeError: null, loading: true }); + await this.props.httpClient.post('/api/v1/service/philips-hue/bridge/sync'); + this.setState({ loading: false }); + } catch (e) { + console.error(e); + this.setState({ syncWithBridgeError: e.toString(), loading: false }); + } + }; componentWillMount() { // this.props.getIntegrationByName('philips-hue'); this.props.getBridges(); this.props.getPhilipsHueDevices(); } - render(props, {}) { + render(props, { syncWithBridgeError, loading }) { + const combinedLoading = props.philipsHueGetDevicesStatus === RequestStatus.Getting || loading; return ( - + ); } } export default connect( - 'user,philipsHueBridges,philipsHueBridgesDevices,philipsHueGetDevicesStatus,philipsHueCreateDeviceStatus,philipsHueGetBridgesStatus,philipsHueDeleteDeviceStatus', + 'user,httpClient,philipsHueBridges,philipsHueBridgesDevices,philipsHueGetDevicesStatus,philipsHueCreateDeviceStatus,philipsHueGetBridgesStatus,philipsHueDeleteDeviceStatus', actions )(PhilipsHueSetupPage); diff --git a/server/services/philips-hue/api/hue.controller.js b/server/services/philips-hue/api/hue.controller.js index 76d1fa5878..d3bc8ad5e4 100644 --- a/server/services/philips-hue/api/hue.controller.js +++ b/server/services/philips-hue/api/hue.controller.js @@ -22,6 +22,16 @@ module.exports = function HueController(philipsHueLightHandler) { res.json(bridge); } + /** + * @api {post} /api/v1/service/philips-hue/bridge/sync Sync Philips Hue Bridge + * @apiName SyncWithBridge + * @apiGroup PhilipsHue + */ + async function syncWithBridge(req, res) { + await philipsHueLightHandler.syncWithBridge(); + res.json({ success: true }); + } + /** * @api {get} /api/v1/service/philips-hue/light Get lights * @apiName GetLights @@ -64,6 +74,11 @@ module.exports = function HueController(philipsHueLightHandler) { admin: true, controller: asyncMiddleware(configureBridge), }, + 'post /api/v1/service/philips-hue/bridge/sync': { + authenticated: true, + admin: true, + controller: asyncMiddleware(syncWithBridge), + }, 'get /api/v1/service/philips-hue/light': { authenticated: true, controller: asyncMiddleware(getLights), diff --git a/server/services/philips-hue/lib/light/index.js b/server/services/philips-hue/lib/light/index.js index 5e2eb702cf..547f864364 100644 --- a/server/services/philips-hue/lib/light/index.js +++ b/server/services/philips-hue/lib/light/index.js @@ -8,6 +8,7 @@ const { poll } = require('./light.poll'); const { getLights } = require('./light.getLights'); const { getScenes } = require('./light.getScenes'); const { setValue } = require('./light.setValue'); +const { syncWithBridge } = require('./light.syncWithBridge'); // we rate-limit the number of request per seconds to poll lights const pollLimiter = new Bottleneck({ @@ -48,5 +49,6 @@ PhilipsHueLightHandler.prototype.poll = pollLimiter.wrap(poll); PhilipsHueLightHandler.prototype.getLights = getLights; PhilipsHueLightHandler.prototype.getScenes = getScenes; PhilipsHueLightHandler.prototype.setValue = setValueLimiter.wrap(setValue); +PhilipsHueLightHandler.prototype.syncWithBridge = syncWithBridge; module.exports = PhilipsHueLightHandler; diff --git a/server/services/philips-hue/lib/light/light.syncWithBridge.js b/server/services/philips-hue/lib/light/light.syncWithBridge.js new file mode 100644 index 0000000000..c57f39c2ae --- /dev/null +++ b/server/services/philips-hue/lib/light/light.syncWithBridge.js @@ -0,0 +1,30 @@ +const Promise = require('bluebird'); +const { NotFoundError } = require('../../../../utils/coreErrors'); +const logger = require('../../../../utils/logger'); + +const { getDeviceParam } = require('../../../../utils/device'); + +const { BRIDGE_SERIAL_NUMBER } = require('../utils/consts'); + +/** + * @description Re-sync with bridge. + * @returns {Promise} Resolve when sync is finished. + * @example + * syncWithBridge(); + */ +async function syncWithBridge() { + logger.info(`Philips Hue: syncWithBridge`); + await Promise.map(this.connnectedBridges, async (device) => { + const serialNumber = getDeviceParam(device, BRIDGE_SERIAL_NUMBER); + const hueApi = this.hueApisBySerialNumber.get(serialNumber); + if (!hueApi) { + throw new NotFoundError(`HUE_API_NOT_FOUND`); + } + logger.info(`Philips Hue: Syncing with bridge ${serialNumber}`); + await hueApi.syncWithBridge(); + }); +} + +module.exports = { + syncWithBridge, +}; diff --git a/server/test/services/philips-hue/controllers/syncWithBridge.controller.test.js b/server/test/services/philips-hue/controllers/syncWithBridge.controller.test.js new file mode 100644 index 0000000000..9f3674bce9 --- /dev/null +++ b/server/test/services/philips-hue/controllers/syncWithBridge.controller.test.js @@ -0,0 +1,19 @@ +const { assert, fake } = require('sinon'); +const PhilipsHueControllers = require('../../../../services/philips-hue/api/hue.controller'); + +const philipsHueLightService = { + syncWithBridge: fake.resolves({}), +}; + +const res = { + json: fake.returns(null), +}; + +describe('POST /service/philips-hue/bridge/sync', () => { + it('should sync bridge', async () => { + const philipsHueController = PhilipsHueControllers(philipsHueLightService); + const req = {}; + await philipsHueController['post /api/v1/service/philips-hue/bridge/sync'].controller(req, res); + assert.called(philipsHueLightService.syncWithBridge); + }); +}); diff --git a/server/test/services/philips-hue/light/light.syncWithBridge.test.js b/server/test/services/philips-hue/light/light.syncWithBridge.test.js new file mode 100644 index 0000000000..ba10a081a8 --- /dev/null +++ b/server/test/services/philips-hue/light/light.syncWithBridge.test.js @@ -0,0 +1,45 @@ +const { assert, fake } = require('sinon'); +const chaiAssert = require('chai').assert; +const EventEmitter = require('events'); +const proxyquire = require('proxyquire').noCallThru(); +const { MockedPhilipsHueClient, hueApi } = require('../mocks.test'); + +const PhilipsHueService = proxyquire('../../../../services/philips-hue/index', { + 'node-hue-api': MockedPhilipsHueClient, +}); + +const StateManager = require('../../../../lib/state'); +const ServiceManager = require('../../../../lib/service'); +const DeviceManager = require('../../../../lib/device'); +const Job = require('../../../../lib/job'); + +const event = new EventEmitter(); +const stateManager = new StateManager(event); +const job = new Job(event); +const serviceManager = new ServiceManager({}, stateManager); +const brain = { + addNamedEntity: fake.returns(null), +}; +const deviceManager = new DeviceManager(event, {}, stateManager, serviceManager, {}, {}, job, brain); + +const gladys = { + device: deviceManager, +}; + +describe('PhilipsHueService', () => { + it('should sync with bridge', async () => { + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.getBridges(); + await philipsHueService.device.configureBridge('192.168.1.10'); + await philipsHueService.device.syncWithBridge(); + assert.called(hueApi.syncWithBridge); + }); + it('should reject (error with Api not found)', async () => { + const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); + await philipsHueService.device.getBridges(); + await philipsHueService.device.configureBridge('192.168.1.10'); + philipsHueService.device.hueApisBySerialNumber = new Map(); + const promise = philipsHueService.device.syncWithBridge(); + return chaiAssert.isRejected(promise); + }); +}); diff --git a/server/test/services/philips-hue/mocks.test.js b/server/test/services/philips-hue/mocks.test.js index a58800ac84..a9a05a04a6 100644 --- a/server/test/services/philips-hue/mocks.test.js +++ b/server/test/services/philips-hue/mocks.test.js @@ -18,6 +18,7 @@ LightState.prototype.rgb = fakes.rgb; LightState.prototype.brightness = fakes.brightness; const hueApi = { + syncWithBridge: fake.resolves(null), users: { createUser: fake.resolves({ username: 'username', @@ -121,4 +122,5 @@ module.exports = { STATE_ON, STATE_OFF, fakes, + hueApi, };