diff --git a/server/lib/device/device.create.js b/server/lib/device/device.create.js index 846d4e7c02..adbb2b8a95 100644 --- a/server/lib/device/device.create.js +++ b/server/lib/device/device.create.js @@ -1,6 +1,7 @@ const Promise = require('bluebird'); const { BadParameters } = require('../../utils/coreErrors'); const db = require('../../models'); +const { EVENTS } = require('../../utils/constants'); const getByExternalId = async (externalId) => { return db.Device.findOne({ @@ -61,6 +62,8 @@ async function create(device) { delete device.features; delete device.params; + let actionEvent = EVENTS.DEVICE.CREATE; + // we execute the whole insert in a transaction to avoir inconsistent state await db.sequelize.transaction(async (transaction) => { // external_id is a required parameter @@ -76,6 +79,8 @@ async function create(device) { if (deviceInDb === null) { deviceInDb = await db.Device.create(device, { transaction }); } else { + actionEvent = EVENTS.DEVICE.UPDATE; + // or update it await deviceInDb.update(device, { transaction }); @@ -152,6 +157,9 @@ async function create(device) { this.poll(newDevice); } + // notify device is succesfully created or updated + this.notify(newDevice, actionEvent); + return newDevice; } diff --git a/server/lib/device/device.destroy.js b/server/lib/device/device.destroy.js index 68c2f88204..3ad29c2c80 100644 --- a/server/lib/device/device.destroy.js +++ b/server/lib/device/device.destroy.js @@ -1,5 +1,5 @@ const { NotFoundError } = require('../../utils/coreErrors'); -const { DEVICE_POLL_FREQUENCIES } = require('../../utils/constants'); +const { DEVICE_POLL_FREQUENCIES, EVENTS } = require('../../utils/constants'); const db = require('../../models'); /** @@ -51,6 +51,10 @@ async function destroy(selector) { } } }); + + // notify device removal + this.notify(device, EVENTS.DEVICE.DELETE); + return null; } diff --git a/server/lib/device/device.notify.js b/server/lib/device/device.notify.js new file mode 100644 index 0000000000..e4f4f9c25a --- /dev/null +++ b/server/lib/device/device.notify.js @@ -0,0 +1,44 @@ +const get = require('get-value'); + +const logger = require('../../utils/logger'); +const { EVENTS } = require('../../utils/constants'); + +const FUNC_BY_EVENT = { + [EVENTS.DEVICE.CREATE]: 'onNewDevice', + [EVENTS.DEVICE.UPDATE]: 'onUpdateDevice', + [EVENTS.DEVICE.DELETE]: 'onDeleteDevice', +}; + +/** + * @description Notify a device is created. + * @param {Object} device - Created device. + * @param {string} event - Event to send. + * @example + * device.notify({ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279'}, 'device.create'); + */ +function notify(device, event) { + logger.debug(`Notify device ${device.selector} creation`); + + const serviceFuncName = FUNC_BY_EVENT[event]; + + if (!serviceFuncName) { + logger.warn(`Event ${event} not handled to notify device changes`); + } else { + // send global event + this.eventManager.emit(event, device); + + // notify concerned service + const service = this.serviceManager.getServiceById(device.service_id); + if (service === null) { + logger.warn(`Service ${device.service_id} was not found.`); + } else if (typeof get(service, `device.${serviceFuncName}`) !== 'function') { + logger.info(`Function device.${serviceFuncName} in service ${service.name} does not exist.`); + } else { + service.device[serviceFuncName](device); + } + } +} + +module.exports = { + notify, +}; diff --git a/server/lib/device/index.js b/server/lib/device/index.js index 13b33ab71a..ae6d792476 100644 --- a/server/lib/device/index.js +++ b/server/lib/device/index.js @@ -24,6 +24,7 @@ const { setParam } = require('./device.setParam'); const { setValue } = require('./device.setValue'); const { setupPoll } = require('./device.setupPoll'); const { newStateEvent } = require('./device.newStateEvent'); +const { notify } = require('./device.notify'); const DeviceManager = function DeviceManager( eventManager, @@ -71,5 +72,6 @@ DeviceManager.prototype.saveStringState = saveStringState; DeviceManager.prototype.setParam = setParam; DeviceManager.prototype.setupPoll = setupPoll; DeviceManager.prototype.setValue = setValue; +DeviceManager.prototype.notify = notify; module.exports = DeviceManager; diff --git a/server/lib/service/index.js b/server/lib/service/index.js index 498c206df9..6f2cc5ba83 100644 --- a/server/lib/service/index.js +++ b/server/lib/service/index.js @@ -3,6 +3,7 @@ const { start } = require('./service.start'); const { startAll } = require('./service.startAll'); const { getByName } = require('./service.getByName'); const { getService } = require('./service.getService'); +const { getServiceById } = require('./service.getServiceById'); const { getServices } = require('./service.getServices'); const { getMessageServices } = require('./service.getMessageServices'); const { getLocalServiceByName } = require('./service.getLocalServiceByName'); @@ -16,6 +17,7 @@ Service.prototype.load = load; Service.prototype.start = start; Service.prototype.startAll = startAll; Service.prototype.getService = getService; +Service.prototype.getServiceById = getServiceById; Service.prototype.getByName = getByName; Service.prototype.getServices = getServices; Service.prototype.getMessageServices = getMessageServices; diff --git a/server/lib/service/service.getServiceById.js b/server/lib/service/service.getServiceById.js new file mode 100644 index 0000000000..a5f521476d --- /dev/null +++ b/server/lib/service/service.getServiceById.js @@ -0,0 +1,15 @@ +/** + * @public + * @description Get service by its id. + * @param {string} id - Id of the service to get. + * @returns {Object} Return the service or null if not present. + * @example + * service.getServiceById('99dc10bb-14ab-49dc-bd11-f724e98fc97c'); + */ +function getServiceById(id) { + return this.stateManager.get('serviceById', id); +} + +module.exports = { + getServiceById, +}; diff --git a/server/lib/service/service.load.js b/server/lib/service/service.load.js index d04ed29702..0c43bfe621 100644 --- a/server/lib/service/service.load.js +++ b/server/lib/service/service.load.js @@ -38,6 +38,7 @@ async function load(gladys) { const newServiceObject = this.servicesFromFiles[service](gladys, serviceInDb.id); // saving service in stateManager this.stateManager.setState('service', service, newServiceObject); + this.stateManager.setState('serviceById', serviceInDb.id, newServiceObject); if (newServiceObject.message && newServiceObject.message.send) { serviceToInsertOrUpdate.has_message_feature = true; } diff --git a/server/lib/state/index.js b/server/lib/state/index.js index 11e35a96e2..e5a15198ec 100644 --- a/server/lib/state/index.js +++ b/server/lib/state/index.js @@ -72,6 +72,7 @@ const StateManager = function StateManager(event) { deviceFeature: {}, deviceFeatureByExternalId: {}, service: {}, + serviceById: {}, sun: {}, system: {}, variable: {}, diff --git a/server/test/lib/device/device.create.test.js b/server/test/lib/device/device.create.test.js index b83ffdde30..9124cdb107 100644 --- a/server/test/lib/device/device.create.test.js +++ b/server/test/lib/device/device.create.test.js @@ -2,13 +2,15 @@ const { expect } = require('chai'); const EventEmitter = require('events'); const Device = require('../../../lib/device'); const StateManager = require('../../../lib/state'); +const ServiceManager = require('../../../lib/service'); const event = new EventEmitter(); describe('Device', () => { it('should create device alone', async () => { const stateManager = new StateManager(event); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const newDevice = await device.create({ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', name: 'Philips Hue 1', @@ -36,7 +38,8 @@ describe('Device', () => { }, ], }); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const newDevice = await device.create({ id: '7f85c2f8-86cc-4600-84db-6c074dadb4e8', name: 'RENAMED_DEVICE', @@ -69,7 +72,8 @@ describe('Device', () => { }); it('should update device which already exist, update a feature and a param', async () => { const stateManager = new StateManager(event); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const newDevice = await device.create({ id: '7f85c2f8-86cc-4600-84db-6c074dadb4e8', name: 'RENAMED_DEVICE', @@ -151,7 +155,8 @@ describe('Device', () => { }, ], }); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); await device.create({ id: '7f85c2f8-86cc-4600-84db-6c074dadb4e8', name: 'RENAMED_DEVICE', @@ -194,7 +199,8 @@ describe('Device', () => { }, ], }); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const newDevice = await device.create({ id: '7f85c2f8-86cc-4600-84db-6c074dadb4e8', name: 'RENAMED_DEVICE', @@ -215,7 +221,8 @@ describe('Device', () => { }); it('should create device, one feature and one param', async () => { const stateManager = new StateManager(event); - const device = new Device(event, {}, stateManager); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const newDevice = await device.create({ service_id: 'a810b8db-6d04-4697-bed3-c4b72c996279', name: 'Philips Hue 1', diff --git a/server/test/lib/device/device.destroy.test.js b/server/test/lib/device/device.destroy.test.js index 33d8a1a9f3..6610bb4c2a 100644 --- a/server/test/lib/device/device.destroy.test.js +++ b/server/test/lib/device/device.destroy.test.js @@ -2,13 +2,15 @@ const EventEmitter = require('events'); const { assert } = require('chai'); const Device = require('../../../lib/device'); const StateManager = require('../../../lib/state'); +const ServiceManager = require('../../../lib/service'); const event = new EventEmitter(); describe('Device.destroy', () => { it('should destroy device', async () => { const stateManager = new StateManager(event); - const device = new Device(event, {}, stateManager, {}); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); device.devicesByPollFrequency[60000] = [ { selector: 'test-device', @@ -18,7 +20,8 @@ describe('Device.destroy', () => { }); it('should return device not found', async () => { const stateManager = new StateManager(event); - const device = new Device(event, {}, stateManager, {}); + const serviceManager = new ServiceManager({}, stateManager); + const device = new Device(event, {}, stateManager, serviceManager); const promise = device.destroy('doesnotexist'); return assert.isRejected(promise); }); diff --git a/server/test/lib/device/device.notify.test.js b/server/test/lib/device/device.notify.test.js new file mode 100644 index 0000000000..6ce27e35a1 --- /dev/null +++ b/server/test/lib/device/device.notify.test.js @@ -0,0 +1,165 @@ +const sinon = require('sinon'); + +const { fake, assert } = sinon; +const { EVENTS } = require('../../../utils/constants'); + +const Device = require('../../../lib/device'); +const StateManager = require('../../../lib/state'); +const ServiceManager = require('../../../lib/service'); + +const event = { + emit: fake.returns(null), + on: fake.returns(null), +}; +const messageManager = {}; + +describe('Device.notify', () => { + afterEach(() => { + sinon.reset(); + }); + + it('should notify service on device creation', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + const service = { + device: { + onNewDevice: fake.returns(null), + onUpdateDevice: fake.returns(null), + onDeleteDevice: fake.returns(null), + }, + }; + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + stateManager.setState('serviceById', serviceId, service); + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, EVENTS.DEVICE.CREATE); + + assert.calledWith(event.emit, EVENTS.DEVICE.CREATE, newDevice); + assert.calledWith(service.device.onNewDevice, newDevice); + assert.notCalled(service.device.onUpdateDevice); + assert.notCalled(service.device.onDeleteDevice); + }); + + it('should notify service on device update', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + const service = { + device: { + onNewDevice: fake.returns(null), + onUpdateDevice: fake.returns(null), + onDeleteDevice: fake.returns(null), + }, + }; + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + stateManager.setState('serviceById', serviceId, service); + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, EVENTS.DEVICE.UPDATE); + + assert.calledWith(event.emit, EVENTS.DEVICE.UPDATE, newDevice); + assert.notCalled(service.device.onNewDevice); + assert.calledWith(service.device.onUpdateDevice, newDevice); + assert.notCalled(service.device.onDeleteDevice); + }); + + it('should notify service on device deletion', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + const service = { + device: { + onNewDevice: fake.returns(null), + onUpdateDevice: fake.returns(null), + onDeleteDevice: fake.returns(null), + }, + }; + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + stateManager.setState('serviceById', serviceId, service); + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, EVENTS.DEVICE.DELETE); + + assert.calledWith(event.emit, EVENTS.DEVICE.DELETE, newDevice); + assert.calledWith(service.device.onDeleteDevice, newDevice); + assert.notCalled(service.device.onNewDevice); + assert.notCalled(service.device.onUpdateDevice); + }); + + it('should notify service on device creation, but no service', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, EVENTS.DEVICE.CREATE); + + assert.calledWith(event.emit, EVENTS.DEVICE.CREATE, newDevice); + }); + + it('should notify service on device creation, but no service function', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + const service = {}; + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + stateManager.setState('serviceById', serviceId, service); + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, EVENTS.DEVICE.CREATE); + + assert.calledWith(event.emit, EVENTS.DEVICE.CREATE, newDevice); + }); + + it('should notify service on device unknown event', async () => { + const stateManager = new StateManager(event); + const serviceManager = new ServiceManager({}, stateManager); + const service = {}; + + const serviceId = 'a810b8db-6d04-4697-bed3-c4b72c996279'; + stateManager.setState('serviceById', serviceId, service); + + const device = new Device(event, messageManager, stateManager, serviceManager); + const newDevice = { + service_id: serviceId, + name: 'Philips Hue 1', + external_id: 'philips-hue-new', + }; + + device.notify(newDevice, 'UNKNOWN_EVENT'); + + assert.notCalled(event.emit); + }); +}); 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 b4d2d84702..3f9cbec7db 100644 --- a/server/test/services/philips-hue/light/light.configureBridge.test.js +++ b/server/test/services/philips-hue/light/light.configureBridge.test.js @@ -8,11 +8,13 @@ const PhilipsHueService = proxyquire('../../../../services/philips-hue/index', { }); const StateManager = require('../../../../lib/state'); +const ServiceManager = require('../../../../lib/service'); const DeviceManager = require('../../../../lib/device'); const event = new EventEmitter(); const stateManager = new StateManager(event); -const deviceManager = new DeviceManager(event, {}, stateManager, {}); +const serviceManager = new ServiceManager({}, stateManager); +const deviceManager = new DeviceManager(event, {}, stateManager, serviceManager); const gladys = { device: deviceManager, diff --git a/server/utils/constants.js b/server/utils/constants.js index 1dda1eaded..689b4104ae 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -38,6 +38,9 @@ const SYSTEM_VARIABLE_NAMES = { const EVENTS = { DEVICE: { NEW: 'device.new', + CREATE: 'device.create', + UPDATE: 'device.update', + DELETE: 'device.delete', ADD_FEATURE: 'device.add-feature', ADD_PARAM: 'device.add-param', NEW_STATE: 'device.new-state',