diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index e266644131..61c921a518 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -452,8 +452,13 @@ "connectButton": "Connect/Reconnect", "scanButton": "Scan network", "bridgeButtonNotPressed": "Bridge button not pressed: Please press the button on your Philips Hue bridge and try again.", - "unknownError": "An unknown error occured. Please try again or contact Gladys community.", - "noBridgesFound": "We didn't find any Philips Hue bridges on your network. Are you sure you are connected to the same network as your bridge and your bridge is turned on?" + "unknownError": "An unknown error occurred. Please try again or contact Gladys community.", + "noBridgesFound": "We didn't find any Philips Hue bridges on your network. Are you sure you are connected to the same network as your bridge and your bridge is turned on?", + "manualConfiguration": { + "title": "Manual configuration", + "text": "If you know the IP address of your Philips Hue bridge, you can fill it and launch a manual connection.", + "input": "IP address" + } }, "device": { "title": "Devices in Gladys", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 44626f502e..5d7a9b76a8 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -579,7 +579,12 @@ "scanButton": "Recherche sur le réseau", "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?" + "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?", + "manualConfiguration": { + "title": "Configuration manuelle", + "text": "Si vous connaissez l'adresse IP de votre pont Philips Hue, vous pouvez la saisir et lancer une configuration manuelle.", + "input": "Adresse IP" + } }, "device": { "title": "Appareils dans Gladys", 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 b7255f1e65..b7b62edc49 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 @@ -1,126 +1,166 @@ -import { MarkupText, Text } from 'preact-i18n'; +import { Component } from 'preact'; +import { MarkupText, Text, Localizer } from 'preact-i18n'; import cx from 'classnames'; import { RequestStatus } from '../../../../../utils/consts'; import style from './style.css'; -const disconnectBridge = (props, device, index) => () => { - props.deleteDevice(device, index); -}; +class SetupTab extends Component { + disconnectBridge = (props, device, index) => () => { + props.deleteDevice(device, index); + }; -const connectBridge = (props, device) => () => { - props.connectBridge(device); -}; + connectBridge = (props, device) => () => { + props.connectBridge(device); + }; -const SetupTab = ({ children, ...props }) => { - return ( -
- {props.philipsHueBridgesDevices && props.philipsHueBridgesDevices.length > 0 && ( + setmanualBridgeConfiguration = ipaddress => { + this.setState({ manualBridgeConfiguration: { ipaddress: ipaddress.target.value } }); + }; + + render(props) { + return ( +
+ {props.philipsHueBridgesDevices && props.philipsHueBridgesDevices.length > 0 && ( +
+
+

+ +

+
+
+
+
+
+ {props.philipsHueDeleteDeviceStatus === RequestStatus.Error && ( +

+ +

+ )} + {props.philipsHueGetDevicesStatus === RequestStatus.Getting &&
} +
+ {props.philipsHueBridgesDevices && + props.philipsHueBridgesDevices.map((bridge, index) => ( +
+
+
+

{bridge.name}

+
+
+ +
+
+
+ ))} +
+
+
+
+
+ )}

- +

+
+ +
- {props.philipsHueDeleteDeviceStatus === RequestStatus.Error && ( -

- -

- )} - {props.philipsHueGetDevicesStatus === RequestStatus.Getting &&
} -
- {props.philipsHueBridgesDevices && - props.philipsHueBridgesDevices.map((bridge, index) => ( -
-
-
-

{bridge.name}

+ {props.philipsHueGetBridgesStatus === RequestStatus.Getting &&
} + {props.philipsHueBridges && props.philipsHueBridges.length === 0 && ( +
+
+ +
+
+
+
+

+ +

+
+
+
+ +
+
+ + } + /> +
-
-
- ))} -
-
-
-
-
- )} -
-
-

- -

-
- -
-
-
-
-
-
- {props.philipsHueGetBridgesStatus === RequestStatus.Getting &&
} - {props.philipsHueBridges && props.philipsHueBridges.length === 0 && ( -
-
- +
-
- )} - {props.philipsHueCreateDeviceStatus === RequestStatus.PhilipsHueBridgeButtonNotPressed && ( -

- -

- )} - {props.philipsHueCreateDeviceStatus === RequestStatus.Error && ( -

- -

- )} - {props.philipsHueBridges && - props.philipsHueBridges.map(bridge => ( -
-
-
-

{bridge.name}

-
-
- + )} + {props.philipsHueCreateDeviceStatus === RequestStatus.PhilipsHueBridgeButtonNotPressed && ( +

+ +

+ )} + {props.philipsHueCreateDeviceStatus === RequestStatus.Error && ( +

+ +

+ )} + {props.philipsHueBridges && + props.philipsHueBridges.map(bridge => ( +
+
+
+

{bridge.name}

+
+
+ +
-
- ))} + ))} +
-
- ); -}; + ); + } +} export default SetupTab; diff --git a/front/src/routes/integration/all/philips-hue/setup-page/actions.js b/front/src/routes/integration/all/philips-hue/setup-page/actions.js index ed47c136c0..a0a9fc8bc7 100644 --- a/front/src/routes/integration/all/philips-hue/setup-page/actions.js +++ b/front/src/routes/integration/all/philips-hue/setup-page/actions.js @@ -66,7 +66,7 @@ const actions = store => ({ }); try { const createdDevice = await state.httpClient.post('/api/v1/service/philips-hue/bridge/configure', { - serial: bridge.model.serial + ipAddress: bridge.ipaddress }); const newState = update(state, { philipsHueBridgesDevices: { diff --git a/server/services/philips-hue/api/hue.controller.js b/server/services/philips-hue/api/hue.controller.js index 35feb0ff7f..76d1fa5878 100644 --- a/server/services/philips-hue/api/hue.controller.js +++ b/server/services/philips-hue/api/hue.controller.js @@ -14,11 +14,11 @@ module.exports = function HueController(philipsHueLightHandler) { /** * @api {post} /api/v1/service/philips-hue/bridge/configure Configure Philips Hue Bridge * @apiName ConfigureBridge - * @apiParam {String} serial Serial number of the bridge + * @apiParam {String} ipAddress IP Address of the bridge * @apiGroup PhilipsHue */ async function configureBridge(req, res) { - const bridge = await philipsHueLightHandler.configureBridge(req.body.serial); + const bridge = await philipsHueLightHandler.configureBridge(req.body.ipAddress); res.json(bridge); } diff --git a/server/services/philips-hue/lib/light/index.js b/server/services/philips-hue/lib/light/index.js index c9cedb1614..5e2eb702cf 100644 --- a/server/services/philips-hue/lib/light/index.js +++ b/server/services/philips-hue/lib/light/index.js @@ -35,7 +35,7 @@ const PhilipsHueLightHandler = function PhilipsHueLightHandler(gladys, hueClient this.serviceId = serviceId; this.bridges = []; this.connnectedBridges = []; - this.bridgesBySerialNumber = new Map(); + this.bridgesByIP = new Map(); this.hueApisBySerialNumber = new Map(); this.lights = []; }; diff --git a/server/services/philips-hue/lib/light/light.configureBridge.js b/server/services/philips-hue/lib/light/light.configureBridge.js index c3566050bd..7b48d155c5 100644 --- a/server/services/philips-hue/lib/light/light.configureBridge.js +++ b/server/services/philips-hue/lib/light/light.configureBridge.js @@ -1,5 +1,4 @@ const logger = require('../../../../utils/logger'); -const { NotFoundError } = require('../../../../utils/coreErrors'); const { Error403 } = require('../../../../utils/httpErrors'); const { HUE_DEVICE_NAME, @@ -13,34 +12,39 @@ const { /** * @description Configure the philips hue bridge. - * @param {string} serialNumber - Serial number of the Philips Hue Bridge. + * @param {string} ipAddress - IP Address of the Philips Hue Bridge. * @returns {Promise} Resolve with created device. * @example * configureBridge('162.198.1.1'); */ -async function configureBridge(serialNumber) { - const bridge = this.bridgesBySerialNumber.get(serialNumber); +async function configureBridge(ipAddress) { + const bridge = this.bridgesByIP.get(ipAddress); if (!bridge) { - throw new NotFoundError(`BRIDGE_NOT_FOUND`); + logger.info(`Connecting to hue bridge ip = ${ipAddress} manually (not from discovered bridges)...`); + } else { + logger.info(`Connecting to hue bridge "${bridge.name}", ip = ${ipAddress} (from discovered bridges)...`); } - logger.info(`Connecting to hue bridge "${serialNumber}", ip = ${bridge.ipaddress}...`); try { const hueApi = this.hueClient.api; - const unauthenticatedApi = await hueApi.createLocal(bridge.ipaddress).connect(); + const unauthenticatedApi = await hueApi.createLocal(ipAddress).connect(); const user = await unauthenticatedApi.users.createUser(HUE_APP_NAME, HUE_DEVICE_NAME); - const authenticatedApi = await hueApi.createLocal(bridge.ipaddress).connect(user.username); - this.hueApisBySerialNumber.set(serialNumber, authenticatedApi); + const authenticatedApi = await hueApi.createLocal(ipAddress).connect(user.username); + // Get configuration to fetch serialNumber + const bridgeConfig = await authenticatedApi.configuration.get(); + const bridgeSerialNumber = bridgeConfig.bridgeid; + const bridgeName = bridge ? bridge.name : bridgeConfig.name; + this.hueApisBySerialNumber.set(bridgeSerialNumber, authenticatedApi); const deviceCreated = await this.gladys.device.create({ - name: bridge.name, + name: bridgeName, service_id: this.serviceId, - external_id: `${BRIDGE_EXTERNAL_ID_BASE}:${serialNumber}`, - selector: `${BRIDGE_EXTERNAL_ID_BASE}:${serialNumber}`, + external_id: `${BRIDGE_EXTERNAL_ID_BASE}:${bridgeSerialNumber}`, + selector: `${BRIDGE_EXTERNAL_ID_BASE}:${bridgeSerialNumber}`, model: BRIDGE_MODEL, features: [], params: [ { name: BRIDGE_IP_ADDRESS, - value: bridge.ipaddress, + value: ipAddress, }, { name: BRIDGE_USERNAME, @@ -48,7 +52,7 @@ async function configureBridge(serialNumber) { }, { name: BRIDGE_SERIAL_NUMBER, - value: serialNumber, + value: bridgeSerialNumber, }, ], }); diff --git a/server/services/philips-hue/lib/light/light.getBridges.js b/server/services/philips-hue/lib/light/light.getBridges.js index 8f1e891f67..9ef12b73af 100644 --- a/server/services/philips-hue/lib/light/light.getBridges.js +++ b/server/services/philips-hue/lib/light/light.getBridges.js @@ -9,10 +9,18 @@ const TIMEOUT = 10000; * getBridges(); */ async function getBridges() { - this.bridges = await this.hueClient.discovery.upnpSearch(TIMEOUT); - logger.info(`PhilipsHueService: Found ${this.bridges.length} bridges`); + // Launch faster N-UPnP Search + this.bridges = await this.hueClient.discovery.nupnpSearch(); + logger.info(`PhilipsHueService: Found ${this.bridges.length} bridges with N-UPnP Search`); + + // Fallback on UPnP Search + if (this.bridges.length === 0) { + this.bridges = await this.hueClient.discovery.upnpSearch(TIMEOUT); + logger.info(`PhilipsHueService: Found ${this.bridges.length} bridges with UPnP Search`); + } + this.bridges.forEach((bridge) => { - this.bridgesBySerialNumber.set(bridge.model.serial, bridge); + this.bridgesByIP.set(bridge.ipaddress, bridge); }); return this.bridges; } diff --git a/server/services/philips-hue/package-lock.json b/server/services/philips-hue/package-lock.json index 87439c104d..750d60335f 100644 --- a/server/services/philips-hue/package-lock.json +++ b/server/services/philips-hue/package-lock.json @@ -20,15 +20,15 @@ "dependencies": { "bluebird": "^3.7.0", "bottleneck": "^2.19.5", - "node-hue-api": "^4.0.9" + "node-hue-api": "^4.0.11" } }, "node_modules/axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "dependencies": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "node_modules/bluebird": { @@ -42,9 +42,9 @@ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" }, "node_modules/follow-redirects": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", - "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "funding": [ { "type": "individual", @@ -66,9 +66,9 @@ "integrity": "sha512-aKYXS1S5+2IYw4W5+lKC/M+lvaNYPe0PhnQ144NWARcBg35H3ZvyVZ6y0LNGtiAxggFBHeO7LaVGO4bgHK4g1Q==" }, "node_modules/node-hue-api": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/node-hue-api/-/node-hue-api-4.0.10.tgz", - "integrity": "sha512-s+UvFttQfNXFadk8p6N9q9A5hteY2Q48W/mVze9nFPR5gwPH374cdA61ezKOx1WgBrN4btHj1z81veznhEZZAA==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/node-hue-api/-/node-hue-api-4.0.11.tgz", + "integrity": "sha512-lpnDdMjLTmm00JRsU70Mtm0Ix03cf7PRjKQAJbSg/Y0ChiIKQs+oDbSUpW2aDhEbor+wKpyfLYLGLTrjlG24pQ==", "dependencies": { "axios": "^0.21.1", "bottleneck": "^2.19.5", @@ -81,11 +81,11 @@ }, "dependencies": { "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.0" } }, "bluebird": { @@ -99,9 +99,9 @@ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" }, "follow-redirects": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", - "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, "get-ssl-certificate": { "version": "2.3.3", @@ -109,9 +109,9 @@ "integrity": "sha512-aKYXS1S5+2IYw4W5+lKC/M+lvaNYPe0PhnQ144NWARcBg35H3ZvyVZ6y0LNGtiAxggFBHeO7LaVGO4bgHK4g1Q==" }, "node-hue-api": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/node-hue-api/-/node-hue-api-4.0.10.tgz", - "integrity": "sha512-s+UvFttQfNXFadk8p6N9q9A5hteY2Q48W/mVze9nFPR5gwPH374cdA61ezKOx1WgBrN4btHj1z81veznhEZZAA==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/node-hue-api/-/node-hue-api-4.0.11.tgz", + "integrity": "sha512-lpnDdMjLTmm00JRsU70Mtm0Ix03cf7PRjKQAJbSg/Y0ChiIKQs+oDbSUpW2aDhEbor+wKpyfLYLGLTrjlG24pQ==", "requires": { "axios": "^0.21.1", "bottleneck": "^2.19.5", diff --git a/server/services/philips-hue/package.json b/server/services/philips-hue/package.json index f0c308350a..efbad9698c 100644 --- a/server/services/philips-hue/package.json +++ b/server/services/philips-hue/package.json @@ -15,6 +15,6 @@ "dependencies": { "bluebird": "^3.7.0", "bottleneck": "^2.19.5", - "node-hue-api": "^4.0.9" + "node-hue-api": "^4.0.11" } } diff --git a/server/test/services/philips-hue/controllers/configureBridge.controller.test.js b/server/test/services/philips-hue/controllers/configureBridge.controller.test.js index b24c36c6eb..91a90030b7 100644 --- a/server/test/services/philips-hue/controllers/configureBridge.controller.test.js +++ b/server/test/services/philips-hue/controllers/configureBridge.controller.test.js @@ -14,10 +14,10 @@ describe('POST /service/philips-hue/bridge/configure', () => { const philipsHueController = PhilipsHueControllers(philipsHueLightService); const req = { body: { - serial: '12345', + ipAddress: '192.168.1.10', }, }; await philipsHueController['post /api/v1/service/philips-hue/bridge/configure'].controller(req, res); - assert.calledWith(philipsHueLightService.configureBridge, '12345'); + assert.calledWith(philipsHueLightService.configureBridge, '192.168.1.10'); }); }); diff --git a/server/test/services/philips-hue/light/light.configureBridge.test.js b/server/test/services/philips-hue/light/light.configureBridge.test.js index 7ae5210a49..7a86640f51 100644 --- a/server/test/services/philips-hue/light/light.configureBridge.test.js +++ b/server/test/services/philips-hue/light/light.configureBridge.test.js @@ -26,14 +26,14 @@ describe('PhilipsHueService', () => { it('should configure bridge', async () => { const philipsHueService = PhilipsHueService(gladys, 'a810b8db-6d04-4697-bed3-c4b72c996279'); await philipsHueService.device.getBridges(); - const device = await philipsHueService.device.configureBridge('1234'); + const device = await philipsHueService.device.configureBridge('192.168.1.10'); expect(device).to.have.property('name', 'Philips Hue Bridge'); expect(device).to.have.property('selector', 'philips-hue-bridge-1234'); expect(device).to.have.property('external_id', 'philips-hue:bridge:1234'); expect(device).to.have.property('features'); expect(device).to.have.property('params'); expect(device.params[0]).to.have.property('name', 'BRIDGE_IP_ADDRESS'); - expect(device.params[0]).to.have.property('value', '192.168.2.245'); + expect(device.params[0]).to.have.property('value', '192.168.1.10'); expect(device.params[1]).to.have.property('name', 'BRIDGE_USERNAME'); expect(device.params[1]).to.have.property('value', 'username'); expect(device.params[2]).to.have.property('name', 'BRIDGE_SERIAL_NUMBER'); diff --git a/server/test/services/philips-hue/light/light.getBridges.test.js b/server/test/services/philips-hue/light/light.getBridges.test.js index 9a340c6a0e..18f9ea6a20 100644 --- a/server/test/services/philips-hue/light/light.getBridges.test.js +++ b/server/test/services/philips-hue/light/light.getBridges.test.js @@ -1,15 +1,29 @@ const { expect } = require('chai'); const proxyquire = require('proxyquire').noCallThru(); -const { MockedPhilipsHueClient } = require('../mocks.test'); +const { MockedPhilipsHueClient, MockedPhilipsHueClientUpnp } = require('../mocks.test'); const PhilipsHueService = proxyquire('../../../../services/philips-hue/index', { 'node-hue-api': MockedPhilipsHueClient, }); +const PhilipsHueServiceUpnp = proxyquire('../../../../services/philips-hue/index', { + 'node-hue-api': MockedPhilipsHueClientUpnp, +}); + describe('PhilipsHueService', () => { - it('getBridges should return bridges', async () => { + it('getBridges should return bridges with nupnp Search', async () => { const philipsHueService = PhilipsHueService(); const bridges = await philipsHueService.device.getBridges(); + expect(bridges).to.deep.equal([ + { + name: 'Philips Hue Bridge', + ipaddress: '192.168.1.10', + }, + ]); + }); + it('getBridges should return bridges with upnp Search', async () => { + const philipsHueService = PhilipsHueServiceUpnp(); + const bridges = await philipsHueService.device.getBridges(); expect(bridges).to.deep.equal([ { name: 'Philips Hue Bridge', diff --git a/server/test/services/philips-hue/mocks.test.js b/server/test/services/philips-hue/mocks.test.js index da7984ba8b..a58800ac84 100644 --- a/server/test/services/philips-hue/mocks.test.js +++ b/server/test/services/philips-hue/mocks.test.js @@ -59,6 +59,12 @@ const hueApi = { ]), activateScene: fake.resolves(null), }, + configuration: { + get: () => + Promise.resolve({ + bridgeid: '1234', + }), + }, }; const MockedPhilipsHueClient = { @@ -72,6 +78,29 @@ const MockedPhilipsHueClient = { }), }, discovery: { + nupnpSearch: () => + Promise.resolve([ + { + name: 'Philips Hue Bridge', + ipaddress: '192.168.1.10', + }, + ]), + }, + }, +}; + +const MockedPhilipsHueClientUpnp = { + v3: { + lightStates: { + LightState, + }, + api: { + createLocal: () => ({ + connect: () => hueApi, + }), + }, + discovery: { + nupnpSearch: () => Promise.resolve([]), upnpSearch: () => Promise.resolve([ { @@ -88,6 +117,7 @@ const MockedPhilipsHueClient = { module.exports = { MockedPhilipsHueClient, + MockedPhilipsHueClientUpnp, STATE_ON, STATE_OFF, fakes,