Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Philips Hue sync with bridge button #2063

Merged
merged 2 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions front/src/config/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions front/src/config/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 2 additions & 0 deletions front/src/config/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href =\"https://community.gladysassistant.com\">Gladys community</a>.",
"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?",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@ class SetupTab extends Component {
<h1 class="card-title">
<Text id="integration.philipsHue.setup.connectedBridgesTitle" />
</h1>
<div class="page-options d-flex">
<button class="btn btn-secondary" onClick={props.syncWithBridge}>
<Text id="integration.philipsHue.setup.syncBridges" />
</button>
</div>
</div>
<div class="card-body">
<div
class={cx('dimmer', {
active: props.philipsHueGetDevicesStatus === RequestStatus.Getting
active: props.loading
})}
>
<div class="loader" />
Expand All @@ -40,6 +45,10 @@ class SetupTab extends Component {
<MarkupText id="integration.philipsHue.setup.unknownError" />
</p>
)}
<p class="alert alert-primary">
<MarkupText id="integration.philipsHue.setup.bridgeNotUpToDateInfo" />
</p>
{props.syncWithBridgeError && <p class="alert alert-danger">{props.syncWithBridgeError}</p>}
{props.philipsHueGetDevicesStatus === RequestStatus.Getting && <div class={style.emptyDiv} />}
<div class="row">
{props.philipsHueBridgesDevices &&
Expand Down
23 changes: 20 additions & 3 deletions front/src/routes/integration/all/philips-hue/setup-page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<PhilipsHuePage user={props.user}>
<SetupTab {...props} />
<SetupTab
{...props}
syncWithBridge={this.syncWithBridge}
syncWithBridgeError={syncWithBridgeError}
loading={combinedLoading}
/>
</PhilipsHuePage>
);
}
}

export default connect(
'user,philipsHueBridges,philipsHueBridgesDevices,philipsHueGetDevicesStatus,philipsHueCreateDeviceStatus,philipsHueGetBridgesStatus,philipsHueDeleteDeviceStatus',
'user,httpClient,philipsHueBridges,philipsHueBridgesDevices,philipsHueGetDevicesStatus,philipsHueCreateDeviceStatus,philipsHueGetBridgesStatus,philipsHueDeleteDeviceStatus',
actions
)(PhilipsHueSetupPage);
15 changes: 15 additions & 0 deletions server/services/philips-hue/api/hue.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions server/services/philips-hue/lib/light/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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;
30 changes: 30 additions & 0 deletions server/services/philips-hue/lib/light/light.syncWithBridge.js
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 2 additions & 0 deletions server/test/services/philips-hue/mocks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -121,4 +122,5 @@ module.exports = {
STATE_ON,
STATE_OFF,
fakes,
hueApi,
};
Loading