From b283721e963ba54894a4ce5906184d73fc522b9c Mon Sep 17 00:00:00 2001 From: atrovato <1839717+atrovato@users.noreply.github.com> Date: Sat, 14 Dec 2019 09:48:40 +0100 Subject: [PATCH] Sonoff: split methods --- server/package.json | 1 - server/services/sonoff/lib/cmdValue/index.js | 7 ++ .../sonoff/lib/cmdValue/light.setValue.js | 38 +++++++ .../sonoff/lib/cmdValue/switch.setValue.js | 24 +++++ .../services/sonoff/lib/handleMqttMessage.js | 74 ++----------- server/services/sonoff/lib/mqttStat/index.js | 11 ++ server/services/sonoff/lib/mqttStat/power.js | 26 +++++ server/services/sonoff/lib/mqttStat/sensor.js | 41 +++++++ server/services/sonoff/lib/mqttStat/state.js | 23 ++++ server/services/sonoff/lib/mqttStat/status.js | 42 ++++++++ server/services/sonoff/lib/setValue.js | 36 ++++--- .../test/services/sonoff/lib/setValue.test.js | 101 ++++++++++++++++-- 12 files changed, 337 insertions(+), 87 deletions(-) create mode 100644 server/services/sonoff/lib/cmdValue/index.js create mode 100644 server/services/sonoff/lib/cmdValue/light.setValue.js create mode 100644 server/services/sonoff/lib/cmdValue/switch.setValue.js create mode 100644 server/services/sonoff/lib/mqttStat/index.js create mode 100644 server/services/sonoff/lib/mqttStat/power.js create mode 100644 server/services/sonoff/lib/mqttStat/sensor.js create mode 100644 server/services/sonoff/lib/mqttStat/state.js create mode 100644 server/services/sonoff/lib/mqttStat/status.js diff --git a/server/package.json b/server/package.json index 57994a66ef..b75594bda2 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,6 @@ "clean:test": "shx --silent rm -f /tmp/gladys-test.db && shx echo Cleaned test database", "pretest": "cross-env SQLITE_FILE_PATH=/tmp/gladys-test.db npm run db-migrate:test && npm run eslint", "test": "cross-env SQLITE_FILE_PATH=/tmp/gladys-test.db NODE_ENV=test ./node_modules/mocha/bin/mocha --recursive ./test/bootstrap.test.js \"./test/**/*.test.js\" --exit", - "test-service": "cross-env SQLITE_FILE_PATH=/tmp/gladys-test.db NODE_ENV=test ./node_modules/mocha/bin/mocha --recursive ./test/bootstrap.test.js \"./test/services/$SERVICE/*.test.js\" \"./test/services/$SERVICE/**/*.test.js\" --exit", "coverage": "nyc npm test && nyc report --reporter=text-lcov > coverage.lcov", "push-coverage": "codecov -F server", "prettier-check": "prettier --check '**/*.js' '**/*.json'", diff --git a/server/services/sonoff/lib/cmdValue/index.js b/server/services/sonoff/lib/cmdValue/index.js new file mode 100644 index 0000000000..3848191f3c --- /dev/null +++ b/server/services/sonoff/lib/cmdValue/index.js @@ -0,0 +1,7 @@ +const { setLightValue } = require('./light.setValue'); +const { setSwitchValue } = require('./switch.setValue'); + +module.exports = { + setLightValue, + setSwitchValue, +}; diff --git a/server/services/sonoff/lib/cmdValue/light.setValue.js b/server/services/sonoff/lib/cmdValue/light.setValue.js new file mode 100644 index 0000000000..75b5e36fbd --- /dev/null +++ b/server/services/sonoff/lib/cmdValue/light.setValue.js @@ -0,0 +1,38 @@ +const { BadParameters } = require('../../../../utils/coreErrors'); +const { DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Set value for light device. + * @param {Object} feature - Feature. + * @param {string|number} value - New value. + * @returns {Object} A pair object containing command topic and value to emit. + * @example + * setLightValue(feature); + */ +function setLightValue(feature, value) { + switch (feature.type) { + case DEVICE_FEATURE_TYPES.LIGHT.BINARY: { + return { topic: 'power', value: value ? 'ON' : 'OFF' }; + } + case DEVICE_FEATURE_TYPES.LIGHT.COLOR: { + return { + topic: 'color', + value: `#${Number(value) + .toString(16) + .padStart(6, '0')}`, + }; + } + case DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS: { + return { + topic: 'dimmer', + value, + }; + } + default: + throw new BadParameters(`Sonoff device type not managed to set value on "${feature.external_id}"`); + } +} + +module.exports = { + setLightValue, +}; diff --git a/server/services/sonoff/lib/cmdValue/switch.setValue.js b/server/services/sonoff/lib/cmdValue/switch.setValue.js new file mode 100644 index 0000000000..b209a32556 --- /dev/null +++ b/server/services/sonoff/lib/cmdValue/switch.setValue.js @@ -0,0 +1,24 @@ +const { BadParameters } = require('../../../../utils/coreErrors'); +const { DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Set value for switch device. + * @param {Object} feature - Feature. + * @param {string|number} value - New value. + * @returns {Object} A pair object containing command topic and value to emit. + * @example + * setSwitchValue(feature); + */ +function setSwitchValue(feature, value) { + switch (feature.type) { + case DEVICE_FEATURE_TYPES.SWITCH.BINARY: { + return { topic: 'power', value: value ? 'ON' : 'OFF' }; + } + default: + throw new BadParameters(`Sonoff device type not managed to set value on "${feature.external_id}"`); + } +} + +module.exports = { + setSwitchValue, +}; diff --git a/server/services/sonoff/lib/handleMqttMessage.js b/server/services/sonoff/lib/handleMqttMessage.js index 553029ad03..d391912bd9 100644 --- a/server/services/sonoff/lib/handleMqttMessage.js +++ b/server/services/sonoff/lib/handleMqttMessage.js @@ -1,7 +1,6 @@ const logger = require('../../../utils/logger'); -const { EVENTS, DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../utils/constants'); -const models = require('../models'); - +const { EVENTS } = require('../../../utils/constants'); +const { status, state, sensor, power } = require('./mqttStat'); /** * @description Handle a new message receive in MQTT. * @param {string} topic - MQTT topic. @@ -20,84 +19,23 @@ function handleMqttMessage(topic, message) { case 'POWER': case 'POWER1': case 'POWER2': { - let switchNo = eventType.replace('POWER', ''); - if (switchNo.length > 0) { - switchNo = `:${switchNo}`; - } - - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}${switchNo}`, - state: message === 'ON' ? 1 : 0, - }); + power(deviceExternalId, message, eventType, events); break; } // Sensor status case 'SENSOR': { - const sensorMsg = JSON.parse(message); - - const energyMsg = sensorMsg.ENERGY; - if (energyMsg) { - if (energyMsg.Current) { - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.ENERGY}`, - state: energyMsg.Current, - }); - } - - if (energyMsg.Power) { - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.POWER}`, - state: energyMsg.Power / 1000, - }); - } - - if (energyMsg.Voltage) { - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE}`, - state: energyMsg.Voltage, - }); - } - } + sensor(deviceExternalId, message, events); break; } // Device global status case 'STATUS': { - const statusMsg = JSON.parse(message); - const statusValue = statusMsg.Status.Power; - const friendlyName = statusMsg.Status.FriendlyName[0]; - const moduleId = statusMsg.Status.Module; - - const model = models[moduleId]; - if (model) { - this.mqttDevices[deviceExternalId] = { - name: friendlyName, - external_id: `sonoff:${deviceExternalId}`, - features: model.getFeatures(), - model: model.getModel(), - service_id: this.serviceId, - should_poll: false, - }; - - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}`, - state: statusValue, - }); - } else { - logger.warn(`MQTT : Sonoff model ${moduleId} (${friendlyName}) not managed`); - } - + status(deviceExternalId, message, events, this); break; } // Device state topic case 'RESULT': case 'STATE': { - const stateMsg = JSON.parse(message); - const stateValue = stateMsg.POWER; - - events.push({ - device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}`, - state: stateValue === 'ON' ? 1 : 0, - }); + state(deviceExternalId, message, events); break; } // Online status diff --git a/server/services/sonoff/lib/mqttStat/index.js b/server/services/sonoff/lib/mqttStat/index.js new file mode 100644 index 0000000000..f32655992e --- /dev/null +++ b/server/services/sonoff/lib/mqttStat/index.js @@ -0,0 +1,11 @@ +const { status } = require('./status'); +const { state } = require('./state'); +const { sensor } = require('./sensor'); +const { power } = require('./power'); + +module.exports = { + status, + state, + sensor, + power, +}; diff --git a/server/services/sonoff/lib/mqttStat/power.js b/server/services/sonoff/lib/mqttStat/power.js new file mode 100644 index 0000000000..beb4bb5324 --- /dev/null +++ b/server/services/sonoff/lib/mqttStat/power.js @@ -0,0 +1,26 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Handle Tasmota 'stat/+/POWER' topics. + * @param {string} deviceExternalId - Device external id. + * @param {string} message - MQTT message. + * @param {string} eventType - MQTT topic. + * @param {Array} events - Resulting events. + * @example + * power('sonoff:sonoff-plug', '{"key": "value"}', 'POWER3', []); + */ +function power(deviceExternalId, message, eventType, events) { + let switchNo = eventType.replace('POWER', ''); + if (switchNo.length > 0) { + switchNo = `:${switchNo}`; + } + + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}${switchNo}`, + state: message === 'ON' ? 1 : 0, + }); +} + +module.exports = { + power, +}; diff --git a/server/services/sonoff/lib/mqttStat/sensor.js b/server/services/sonoff/lib/mqttStat/sensor.js new file mode 100644 index 0000000000..ad1a71d741 --- /dev/null +++ b/server/services/sonoff/lib/mqttStat/sensor.js @@ -0,0 +1,41 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Handle Tasmota 'stat/+/SENSOR' topics. + * @param {string} deviceExternalId - Device external id. + * @param {string} message - MQTT message. + * @param {Array} events - Resulting events. + * @example + * sensor('sonoff:sonoff-plug', '{"key": "value"}', []); + */ +function sensor(deviceExternalId, message, events) { + const sensorMsg = JSON.parse(message); + + const energyMsg = sensorMsg.ENERGY; + if (energyMsg) { + if (energyMsg.Current) { + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.ENERGY}`, + state: energyMsg.Current, + }); + } + + if (energyMsg.Power) { + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.POWER}`, + state: energyMsg.Power / 1000, + }); + } + + if (energyMsg.Voltage) { + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.VOLTAGE}`, + state: energyMsg.Voltage, + }); + } + } +} + +module.exports = { + sensor, +}; diff --git a/server/services/sonoff/lib/mqttStat/state.js b/server/services/sonoff/lib/mqttStat/state.js new file mode 100644 index 0000000000..1905e32ad3 --- /dev/null +++ b/server/services/sonoff/lib/mqttStat/state.js @@ -0,0 +1,23 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +/** + * @description Handle Tasmota 'stat/+/STATE' or 'stat/+/RESULT' topics. + * @param {string} deviceExternalId - Device external id. + * @param {string} message - MQTT message. + * @param {Array} events - Resulting events. + * @example + * state('sonoff:sonoff-plug', '{"key": "value"}', []); + */ +function state(deviceExternalId, message, events) { + const stateMsg = JSON.parse(message); + const stateValue = stateMsg.POWER; + + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}`, + state: stateValue === 'ON' ? 1 : 0, + }); +} + +module.exports = { + state, +}; diff --git a/server/services/sonoff/lib/mqttStat/status.js b/server/services/sonoff/lib/mqttStat/status.js new file mode 100644 index 0000000000..cb3f2397ba --- /dev/null +++ b/server/services/sonoff/lib/mqttStat/status.js @@ -0,0 +1,42 @@ +const logger = require('../../../../utils/logger'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); +const models = require('../../models'); + +/** + * @description Handle Tasmota 'stat/+/STATUS' topics. + * @param {string} deviceExternalId - Device external id. + * @param {string} message - MQTT message. + * @param {Array} events - Resulting events. + * @param {Object} sonoffHandler - Sonoff handler. + * @example + * status('sonoff:sonoff-plug', '{"key": "value"}', [], {}); + */ +function status(deviceExternalId, message, events, sonoffHandler) { + const statusMsg = JSON.parse(message); + const statusValue = statusMsg.Status.Power; + const friendlyName = statusMsg.Status.FriendlyName[0]; + const moduleId = statusMsg.Status.Module; + + const model = models[moduleId]; + if (model) { + sonoffHandler[deviceExternalId] = { + name: friendlyName, + external_id: `sonoff:${deviceExternalId}`, + features: model.getFeatures(), + model: model.getModel(), + service_id: this.serviceId, + should_poll: false, + }; + + events.push({ + device_feature_external_id: `sonoff:${deviceExternalId}:${DEVICE_FEATURE_CATEGORIES.SWITCH}:${DEVICE_FEATURE_TYPES.SWITCH.BINARY}`, + state: statusValue, + }); + } else { + logger.warn(`MQTT : Sonoff model ${moduleId} (${friendlyName}) not managed`); + } +} + +module.exports = { + status, +}; diff --git a/server/services/sonoff/lib/setValue.js b/server/services/sonoff/lib/setValue.js index 14fc0373db..67234970c3 100644 --- a/server/services/sonoff/lib/setValue.js +++ b/server/services/sonoff/lib/setValue.js @@ -1,4 +1,6 @@ const { BadParameters } = require('../../../utils/coreErrors'); +const { DEVICE_FEATURE_CATEGORIES } = require('../../../utils/constants'); +const { setSwitchValue, setLightValue } = require('./cmdValue'); /** * @description Send the new device value over MQTT. @@ -9,25 +11,35 @@ const { BadParameters } = require('../../../utils/coreErrors'); * setValue(device, deviceFeature, 0); */ function setValue(device, deviceFeature, value) { - // Remove first 'sonoff:' substring - const externalId = device.external_id; + const externalId = deviceFeature.external_id; + const splittedPowerId = deviceFeature.external_id.split(':'); + const [prefix, topic, , , relayId] = splittedPowerId; - if (!externalId.startsWith('sonoff:')) { - throw new BadParameters(`Sonoff device external_id is invalid : "${externalId}" should starts with "sonoff:"`); + if (prefix !== 'sonoff') { + throw new BadParameters(`Sonoff device external_id is invalid: "${externalId}" should starts with "sonoff:"`); } - const topic = externalId.substring(7); - if (topic.length === 0) { - throw new BadParameters(`Sonoff device external_id is invalid : "${externalId}" have no MQTT topic`); + if (!topic || topic.length === 0) { + throw new BadParameters(`Sonoff device external_id is invalid: "${externalId}" have no MQTT topic`); } - let powerId = ''; - const splittedPowerId = deviceFeature.external_id.split(':'); - if (splittedPowerId.length > 4) { - [, , , , powerId] = splittedPowerId; + let cmnd; + const { category } = deviceFeature; + + switch (category) { + case DEVICE_FEATURE_CATEGORIES.LIGHT: { + cmnd = setLightValue(deviceFeature, value); + break; + } + case DEVICE_FEATURE_CATEGORIES.SWITCH: { + cmnd = setSwitchValue(deviceFeature, value); + break; + } + default: + throw new BadParameters(`Sonoff device category not managed to set value on "${externalId}"`); } // Send message to Sonoff topics - this.mqttService.device.publish(`cmnd/${topic}/power${powerId}`, value ? 'ON' : 'OFF'); + this.mqttService.device.publish(`cmnd/${topic}/${cmnd.topic}${relayId || ''}`, cmnd.value); } module.exports = { diff --git a/server/test/services/sonoff/lib/setValue.test.js b/server/test/services/sonoff/lib/setValue.test.js index 2d07879589..b25d96910a 100644 --- a/server/test/services/sonoff/lib/setValue.test.js +++ b/server/test/services/sonoff/lib/setValue.test.js @@ -3,6 +3,7 @@ const sinon = require('sinon'); const { fake, assert } = sinon; const { expect } = require('chai'); const SonoffHandler = require('../../../../services/sonoff/lib'); +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); const mqttService = { device: { @@ -20,10 +21,10 @@ describe('SonoffHandler - setValue', () => { }); it('publish through invalid topic', () => { - const device = { + const device = undefined; + const feature = { external_id: 'deviceInvalidTopic', }; - const feature = undefined; const value = 1; try { @@ -32,16 +33,16 @@ describe('SonoffHandler - setValue', () => { } catch (e) { assert.notCalled(mqttService.device.publish); expect(e.message).to.eq( - 'Sonoff device external_id is invalid : "deviceInvalidTopic" should starts with "sonoff:"', + 'Sonoff device external_id is invalid: "deviceInvalidTopic" should starts with "sonoff:"', ); } }); it('publish through null topic', () => { - const device = { + const device = undefined; + const feature = { external_id: 'sonoff:', }; - const feature = undefined; const value = 1; try { @@ -49,7 +50,7 @@ describe('SonoffHandler - setValue', () => { assert.fail('Should ends on error'); } catch (e) { assert.notCalled(mqttService.device.publish); - expect(e.message).to.eq('Sonoff device external_id is invalid : "sonoff:" have no MQTT topic'); + expect(e.message).to.eq('Sonoff device external_id is invalid: "sonoff:" have no MQTT topic'); } }); @@ -59,6 +60,8 @@ describe('SonoffHandler - setValue', () => { }; const feature = { external_id: 'sonoff:deviceTopic:switch:binary', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, }; const value = 1; @@ -73,6 +76,8 @@ describe('SonoffHandler - setValue', () => { }; const feature = { external_id: 'sonoff:deviceTopic:switch:binary:1', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, }; const value = 1; @@ -87,6 +92,8 @@ describe('SonoffHandler - setValue', () => { }; const feature = { external_id: 'sonoff:deviceTopic:switch:binary:2', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, }; const value = 1; @@ -101,6 +108,40 @@ describe('SonoffHandler - setValue', () => { }; const feature = { external_id: 'sonoff:deviceTopic:switch:binary', + category: DEVICE_FEATURE_CATEGORIES.SWITCH, + type: DEVICE_FEATURE_TYPES.SWITCH.BINARY, + }; + const value = 0; + + sonoffHandler.setValue(device, feature, value); + + assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/power', 'OFF'); + }); + + it('publish ON through valid topic: light', () => { + const device = { + external_id: 'sonoff:deviceTopic', + }; + const feature = { + external_id: 'sonoff:deviceTopic:light:binary', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, + }; + const value = 1; + + sonoffHandler.setValue(device, feature, value); + + assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/power', 'ON'); + }); + + it('publish OFF through valid topic: light', () => { + const device = { + external_id: 'sonoff:deviceTopic', + }; + const feature = { + external_id: 'sonoff:deviceTopic:light:binary', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BINARY, }; const value = 0; @@ -108,4 +149,52 @@ describe('SonoffHandler - setValue', () => { assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/power', 'OFF'); }); + + it('publish black color through valid topic', () => { + const device = { + external_id: 'sonoff:deviceTopic', + }; + const feature = { + external_id: 'sonoff:deviceTopic:light:color', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.COLOR, + }; + const value = 0; + + sonoffHandler.setValue(device, feature, value); + + assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/color', '#000000'); + }); + + it('publish white color through valid topic', () => { + const device = { + external_id: 'sonoff:deviceTopic', + }; + const feature = { + external_id: 'sonoff:deviceTopic:light:color', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.COLOR, + }; + const value = 16777215; + + sonoffHandler.setValue(device, feature, value); + + assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/color', '#ffffff'); + }); + + it('publish brightness through valid topic', () => { + const device = { + external_id: 'sonoff:deviceTopic', + }; + const feature = { + external_id: 'sonoff:deviceTopic:light:brightness', + category: DEVICE_FEATURE_CATEGORIES.LIGHT, + type: DEVICE_FEATURE_TYPES.LIGHT.BRIGHTNESS, + }; + const value = 72; + + sonoffHandler.setValue(device, feature, value); + + assert.calledWith(mqttService.device.publish, 'cmnd/deviceTopic/dimmer', 72); + }); });