From def6bf1d0b60e51a1e73828ea952f83ab562a0ae Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 29 Jun 2020 22:16:16 +0800 Subject: [PATCH] Implement external converters (#3810) --- lib/controller.js | 4 + lib/extension/externalConverters.js | 31 +++++ lib/util/settings.js | 5 + .../mock-external-converter-multiple.js | 7 ++ test/assets/mock-external-converter.js | 5 + test/externalConverters.test.js | 114 ++++++++++++++++++ test/stub/data.js | 3 +- 7 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 lib/extension/externalConverters.js create mode 100644 test/assets/mock-external-converter-multiple.js create mode 100644 test/assets/mock-external-converter.js create mode 100644 test/externalConverters.test.js diff --git a/lib/controller.js b/lib/controller.js index d684bb7d..15877b28 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -26,6 +26,7 @@ const ExtensionBind = require('./extension/bind'); const ExtensionReport = require('./extension/report'); const ExtensionOnEvent = require('./extension/onEvent'); const ExtensionOTAUpdate = require('./extension/otaUpdate'); +const ExtensionExternalConverters = require('./extension/externalConverters'); class Controller { constructor() { @@ -75,6 +76,9 @@ class Controller { if (settings.get().advanced.availability_timeout) { this.extensions.push(new ExtensionAvailability(...args)); } + if (settings.get().external_converters.length) { + this.extensions.push(new ExtensionExternalConverters(...args)); + } const extensionPath = data.joinPath('extension'); if (fs.existsSync(extensionPath)) { diff --git a/lib/extension/externalConverters.js b/lib/extension/externalConverters.js new file mode 100644 index 00000000..643fee64 --- /dev/null +++ b/lib/extension/externalConverters.js @@ -0,0 +1,31 @@ +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const settings = require('../util/settings'); +const Extension = require('./extension'); +const data = require('../util/data'); + + +class ExternalConverters extends Extension { + constructor(zigbee, mqtt, state, publishEntityState, eventBus) { + super(zigbee, mqtt, state, publishEntityState, eventBus); + + const externalConverters = settings.get().external_converters; + + externalConverters.forEach((moduleName) => { + let converterModule = moduleName; + + if (moduleName.endsWith('.js')) { + converterModule = data.joinPath(moduleName.split('.')[0]); + } + const converter = require(converterModule); + if (Array.isArray(converter)) { + converter.forEach((mod) => { + zigbeeHerdsmanConverters.addDeviceDefinition(mod); + }); + } else { + zigbeeHerdsmanConverters.addDeviceDefinition(converter); + } + }); + } +} + +module.exports = ExternalConverters; diff --git a/lib/util/settings.js b/lib/util/settings.js index 978b231a..6e9c422f 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -122,6 +122,7 @@ const defaults = { */ timestamp_format: 'YYYY-MM-DD HH:mm:ss', }, + external_converters: [], }; const schema = { @@ -271,6 +272,10 @@ const schema = { }, }, }, + external_converters: { + type: 'array', + items: {type: 'string'}, + }, required: ['homeassistant', 'permit_join', 'mqtt'], }; diff --git a/test/assets/mock-external-converter-multiple.js b/test/assets/mock-external-converter-multiple.js new file mode 100644 index 00000000..148f1329 --- /dev/null +++ b/test/assets/mock-external-converter-multiple.js @@ -0,0 +1,7 @@ +const mockDevices = [{ + mock: 1 +}, { + mock: 2 +}]; + +module.exports = mockDevices; \ No newline at end of file diff --git a/test/assets/mock-external-converter.js b/test/assets/mock-external-converter.js new file mode 100644 index 00000000..d6d0f74c --- /dev/null +++ b/test/assets/mock-external-converter.js @@ -0,0 +1,5 @@ +const mockDevice = { + mock: true +}; + +module.exports = mockDevice; \ No newline at end of file diff --git a/test/externalConverters.test.js b/test/externalConverters.test.js new file mode 100644 index 00000000..1ccfb514 --- /dev/null +++ b/test/externalConverters.test.js @@ -0,0 +1,114 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); +const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const path = require('path'); +const fs = require('fs'); + +zigbeeHerdsmanConverters.addDeviceDefinition = jest.fn(); + +const mocksClear = [zigbeeHerdsmanConverters.addDeviceDefinition, zigbeeHerdsman.permitJoin, + mockExit, MQTT.end, zigbeeHerdsman.stop, logger.debug, + MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork, + zigbeeHerdsman.devices.bulb.removeFromNetwork, logger.error, +]; + +jest.mock( + 'mock-external-converter-module', () => { + return { + mock: true + }; + }, { + virtual: true + }); + +jest.mock( + 'mock-multiple-external-converter-module', () => { + return [{ + mock: 1 + }, { + mock: 2 + }]; + }, { + virtual: true + }); + +describe('Loads external converters', () => { + let controller; + + beforeEach(async () => { + jest.useRealTimers(); + await flushPromises(); + mocksClear.forEach((m) => m.mockClear()); + data.writeDefaultConfiguration(); + data.writeEmptyState(); + settings._reRead(); + }); + + it('Does not load external converters', async () => { + settings.set(['external_converters'], []); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(0); + }); + + it('Loads external converters', async () => { + fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter.js'), path.join(data.mockDir, 'mock-external-converter.js')); + const devicesCount = zigbeeHerdsman.devices.lenght; + settings.set(['external_converters'], ['mock-external-converter.js']); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledWith({ + mock: true + }); + }); + + it('Loads multiple external converters', async () => { + fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js')); + const devicesCount = zigbeeHerdsman.devices.lenght; + settings.set(['external_converters'], ['mock-external-converter-multiple.js']); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(2); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(1, { + mock: 1 + }); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(2, { + mock: 2 + }); + }); + + it('Loads external converters from package', async () => { + settings.set(['external_converters'], ['mock-external-converter-module']); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledWith({ + mock: true + }); + }); + + it('Loads multiple external converters from package', async () => { + settings.set(['external_converters'], ['mock-multiple-external-converter-module']); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenCalledTimes(2); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(1, { + mock: 1 + }); + expect(zigbeeHerdsmanConverters.addDeviceDefinition).toHaveBeenNthCalledWith(2, { + mock: 2 + }); + }); +}); \ No newline at end of file diff --git a/test/stub/data.js b/test/stub/data.js index 6d0e43d5..0f1b7036 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -174,7 +174,8 @@ function writeDefaultConfiguration() { retain: false, devices: ['bulb_2'] }, - } + }, + external_converters: [], }; yaml.writeIfChanged(path.join(mockDir, 'configuration.yaml'), config);