From ddfe4a46f02c696f5bb42d276e72ac0124105776 Mon Sep 17 00:00:00 2001 From: atrovato <1839717+atrovato@users.noreply.github.com> Date: Fri, 18 Dec 2020 17:24:22 +0100 Subject: [PATCH] [TAMOSTA] Manage distance sensor Fixes #1017 --- .../device-features/SensorDeviceFeature.jsx | 2 + front/src/config/i18n/en.json | 3 + front/src/config/i18n/fr.json | 3 + front/src/utils/consts.js | 3 + .../services/tasmota/lib/features/distance.js | 30 ++++ server/services/tasmota/lib/features/index.js | 4 +- .../tasmota/lib/device-creation/SR04.json | 43 ++++++ .../tasmota/lib/device-creation/VL53L0X.json | 43 ++++++ ...ssage.deviceCreation-distance_SR04.test.js | 143 ++++++++++++++++++ ...ge.deviceCreation-distance_VL53L0X.test.js | 143 ++++++++++++++++++ ...eation-temperature_humdity_AM2301.test.js} | 0 ...reation-temperature_humdity_DHT11.test.js} | 0 server/utils/constants.js | 3 + 13 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 server/services/tasmota/lib/features/distance.js create mode 100644 server/test/services/tasmota/lib/device-creation/SR04.json create mode 100644 server/test/services/tasmota/lib/device-creation/VL53L0X.json create mode 100644 server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_SR04.test.js create mode 100644 server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_VL53L0X.test.js rename server/test/services/tasmota/lib/mqtt/handle-message/device-creation/{tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_AM2301.test.js => tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_AM2301.test.js} (100%) rename server/test/services/tasmota/lib/mqtt/handle-message/device-creation/{tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_DHT11.test.js => tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_DHT11.test.js} (100%) diff --git a/front/src/components/boxs/device-in-room/device-features/SensorDeviceFeature.jsx b/front/src/components/boxs/device-in-room/device-features/SensorDeviceFeature.jsx index bd0441c377..c2e847e6dd 100644 --- a/front/src/components/boxs/device-in-room/device-features/SensorDeviceFeature.jsx +++ b/front/src/components/boxs/device-in-room/device-features/SensorDeviceFeature.jsx @@ -41,6 +41,8 @@ const SensorDeviceType = ({ children, ...props }) => ( {props.deviceFeature.unit === DEVICE_FEATURE_UNITS.AMPERE && 'A'} {props.deviceFeature.unit === DEVICE_FEATURE_UNITS.VOLT && 'V'} {props.deviceFeature.unit === DEVICE_FEATURE_UNITS.PPM && 'ppm'} + {props.deviceFeature.unit === DEVICE_FEATURE_UNITS.MM && 'mm'} + {props.deviceFeature.unit === DEVICE_FEATURE_UNITS.CM && 'cm'} )} diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 155f5a7b0e..502077ee24 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -1222,6 +1222,9 @@ "shortCategoryName": "Sismic sensor", "decimal": "Sismic (decimal)" }, + "distance-sensor": { + "decimal": "Distance" + }, "camera": { "shortCategoryName": "Camera", "image": "Camera" diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 84def0da09..d7526de128 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -1222,6 +1222,9 @@ "shortCategoryName": "Capteur sismique", "decimal": "Valeur sismique décimale" }, + "distance-sensor": { + "decimal": "Distance" + }, "camera": { "shortCategoryName": "Caméra", "image": "Caméra (Image)" diff --git a/front/src/utils/consts.js b/front/src/utils/consts.js index 581ba8f0ed..83288d37ff 100644 --- a/front/src/utils/consts.js +++ b/front/src/utils/consts.js @@ -174,6 +174,9 @@ export const DeviceFeatureCategoriesIcon = { [DEVICE_FEATURE_CATEGORIES.CO2_SENSOR]: { [DEVICE_FEATURE_TYPES.SENSOR.DECIMAL]: 'bar-chart-2' }, + [DEVICE_FEATURE_CATEGORIES.DISTANCE_SENSOR]: { + [DEVICE_FEATURE_TYPES.SENSOR.DECIMAL]: 'italic' + }, [DEVICE_FEATURE_CATEGORIES.CUBE]: { [DEVICE_FEATURE_TYPES.CUBE.MODE]: 'activity', [DEVICE_FEATURE_TYPES.CUBE.ROTATION]: 'rotate-cw' diff --git a/server/services/tasmota/lib/features/distance.js b/server/services/tasmota/lib/features/distance.js new file mode 100644 index 0000000000..d705dd3608 --- /dev/null +++ b/server/services/tasmota/lib/features/distance.js @@ -0,0 +1,30 @@ +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, +} = require('../../../../utils/constants'); + +module.exports = { + // Tasmota matcher + keyMatcher: /^(StatusSNS|Gladys)\.(SR04|VL53L0X)\.Distance$/, + // Gladys feature + generateFeature: (device, key, value, fullKey) => { + const keyParts = fullKey.split('.'); + const unit = keyParts[1] === 'VL53L0X' ? DEVICE_FEATURE_UNITS.MM : DEVICE_FEATURE_UNITS.CM; + + return { + category: DEVICE_FEATURE_CATEGORIES.DISTANCE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + name: 'Distance', + read_only: true, + has_feedback: false, + min: 0, + max: 10000, + unit, + }; + }, + generateExternalId: (key, fullKey) => { + const keyParts = fullKey.split('.'); + return `${keyParts[1]}:Distance`; + }, +}; diff --git a/server/services/tasmota/lib/features/index.js b/server/services/tasmota/lib/features/index.js index 1a039ed77b..784440b16a 100644 --- a/server/services/tasmota/lib/features/index.js +++ b/server/services/tasmota/lib/features/index.js @@ -7,6 +7,7 @@ const modules = require('./modules'); // Features const power = require('./power'); const dimmer = require('./dimmer'); +const distance = require('./distance'); const energyCurrent = require('./energy.current'); const energyPower = require('./energy.power'); const energyVoltage = require('./energy.voltage'); @@ -21,6 +22,7 @@ const temperature = require('./temperature'); const FEATURE_TEMPLATES = [ power, dimmer, + distance, energyCurrent, energyPower, energyVoltage, @@ -68,7 +70,7 @@ const addFeature = (device, featureTemplate, fullKey, command, value) => { if (existingFeature) { logger.debug(`Tasmota: duplicated feature handled for ${externalId}`); } else { - const generatedFeature = featureTemplate.generateFeature(device, command, value); + const generatedFeature = featureTemplate.generateFeature(device, command, value, fullKey); if (generatedFeature) { const convertedValue = generateValue(featureTemplate, value); diff --git a/server/test/services/tasmota/lib/device-creation/SR04.json b/server/test/services/tasmota/lib/device-creation/SR04.json new file mode 100644 index 0000000000..21612cc53c --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/SR04.json @@ -0,0 +1,43 @@ +{ + "STATUS": { + "Status": { + "ButtonRetain": 0, + "ButtonTopic": "0", + "FriendlyName": ["Tasmota"], + "LedMask": "FFFF", + "LedState": 1, + "Module": 18, + "Power": 0, + "PowerOnState": 1, + "PowerRetain": 0, + "SaveData": 1, + "SaveState": 1, + "SensorRetain": 0, + "SwitchMode": [0, 0, 0, 0, 0, 0, 0, 0], + "SwitchRetain": 0, + "SwitchTopic": "0", + "Topic": "tasmota" + } + }, + "STATUS11": { + "StatusSTS": { + "MqttCount": 1, + "Sleep": 50, + "SleepMode": "Dynamic", + "Wifi": { + "AP": 1, + "BSSId": "EC:BE:DD:85:1F:E0", + "Channel": 1, + "LinkCount": 1, + "SSId": "ALEX-NETWORK" + } + } + }, + "STATUS8": { + "StatusSNS": { + "SR04": { + "Distance": 3.65 + } + } + } +} diff --git a/server/test/services/tasmota/lib/device-creation/VL53L0X.json b/server/test/services/tasmota/lib/device-creation/VL53L0X.json new file mode 100644 index 0000000000..943201158b --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/VL53L0X.json @@ -0,0 +1,43 @@ +{ + "STATUS": { + "Status": { + "ButtonRetain": 0, + "ButtonTopic": "0", + "FriendlyName": ["Tasmota"], + "LedMask": "FFFF", + "LedState": 1, + "Module": 18, + "Power": 0, + "PowerOnState": 1, + "PowerRetain": 0, + "SaveData": 1, + "SaveState": 1, + "SensorRetain": 0, + "SwitchMode": [0, 0, 0, 0, 0, 0, 0, 0], + "SwitchRetain": 0, + "SwitchTopic": "0", + "Topic": "tasmota" + } + }, + "STATUS11": { + "StatusSTS": { + "MqttCount": 1, + "Sleep": 50, + "SleepMode": "Dynamic", + "Wifi": { + "AP": 1, + "BSSId": "EC:BE:DD:85:1F:E0", + "Channel": 1, + "LinkCount": 1, + "SSId": "ALEX-NETWORK" + } + } + }, + "STATUS8": { + "StatusSNS": { + "VL53L0X": { + "Distance": 3.65 + } + } + } +} diff --git a/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_SR04.test.js b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_SR04.test.js new file mode 100644 index 0000000000..b46e70d50d --- /dev/null +++ b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_SR04.test.js @@ -0,0 +1,143 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { fake, assert } = sinon; +const TasmotaHandler = require('../../../../../../../services/tasmota/lib'); +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, + EVENTS, + WEBSOCKET_MESSAGE_TYPES, +} = require('../../../../../../../utils/constants'); + +const messages = require('../../../device-creation/SR04.json'); + +const mqttService = { + device: { + publish: fake.returns(null), + }, +}; +const gladys = { + event: { + emit: fake.returns(null), + }, + stateManager: { + get: fake.returns(null), + }, +}; +const serviceId = 'service-uuid-random'; + +describe('Tasmota - MQTT - create device with SR04 distance features', () => { + const tasmota = new TasmotaHandler(gladys, serviceId); + const tasmotaHandler = tasmota.protocols.mqtt; + tasmotaHandler.mqttService = mqttService; + + beforeEach(() => { + sinon.reset(); + }); + + it('decode STATUS message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS', JSON.stringify(messages.STATUS)); + + expect(tasmotaHandler.discoveredDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [], + }, + }); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.stateManager.get); + assert.calledWith(mqttService.device.publish, 'cmnd/tasmota-device-topic/STATUS', '11'); + }); + + it('decode STATUS11 message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS11', JSON.stringify(messages.STATUS11)); + + expect(tasmotaHandler.discoveredDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [], + }, + }); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.stateManager.get); + assert.calledWith(mqttService.device.publish, 'cmnd/tasmota-device-topic/STATUS', '8'); + }); + + it('decode STATUS8 message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS8', JSON.stringify(messages.STATUS8)); + + const expectedDevice = { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.DISTANCE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.CM, + external_id: 'tasmota:tasmota-device-topic:SR04:Distance', + selector: 'tasmota-tasmota-device-topic-sr04-distance', + name: 'Distance', + read_only: true, + has_feedback: false, + min: 0, + max: 10000, + last_value: 3.65, + }, + ], + }; + expect(tasmotaHandler.discoveredDevices).to.deep.eq({ + 'tasmota-device-topic': expectedDevice, + }); + expect(tasmotaHandler.pendingDevices).to.deep.eq({}); + + assert.notCalled(mqttService.device.publish); + assert.calledWith(gladys.stateManager.get, 'deviceByExternalId', 'tasmota:tasmota-device-topic'); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'tasmota:tasmota-device-topic:SR04:Distance', + state: 3.65, + }); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.TASMOTA.NEW_MQTT_DEVICE, + payload: expectedDevice, + }); + }); +}); diff --git a/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_VL53L0X.test.js b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_VL53L0X.test.js new file mode 100644 index 0000000000..5ee5940ae6 --- /dev/null +++ b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-distance_VL53L0X.test.js @@ -0,0 +1,143 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { fake, assert } = sinon; +const TasmotaHandler = require('../../../../../../../services/tasmota/lib'); +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, + EVENTS, + WEBSOCKET_MESSAGE_TYPES, +} = require('../../../../../../../utils/constants'); + +const messages = require('../../../device-creation/VL53L0X.json'); + +const mqttService = { + device: { + publish: fake.returns(null), + }, +}; +const gladys = { + event: { + emit: fake.returns(null), + }, + stateManager: { + get: fake.returns(null), + }, +}; +const serviceId = 'service-uuid-random'; + +describe('Tasmota - MQTT - create device with VL53L0X distance features', () => { + const tasmota = new TasmotaHandler(gladys, serviceId); + const tasmotaHandler = tasmota.protocols.mqtt; + tasmotaHandler.mqttService = mqttService; + + beforeEach(() => { + sinon.reset(); + }); + + it('decode STATUS message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS', JSON.stringify(messages.STATUS)); + + expect(tasmotaHandler.discoveredDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [], + }, + }); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.stateManager.get); + assert.calledWith(mqttService.device.publish, 'cmnd/tasmota-device-topic/STATUS', '11'); + }); + + it('decode STATUS11 message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS11', JSON.stringify(messages.STATUS11)); + + expect(tasmotaHandler.discoveredDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [], + }, + }); + + assert.notCalled(gladys.event.emit); + assert.notCalled(gladys.stateManager.get); + assert.calledWith(mqttService.device.publish, 'cmnd/tasmota-device-topic/STATUS', '8'); + }); + + it('decode STATUS8 message', () => { + tasmotaHandler.handleMessage('stat/tasmota-device-topic/STATUS8', JSON.stringify(messages.STATUS8)); + + const expectedDevice = { + name: 'Tasmota', + params: [ + { + name: 'protocol', + value: 'mqtt', + }, + ], + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.DISTANCE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.MM, + external_id: 'tasmota:tasmota-device-topic:VL53L0X:Distance', + selector: 'tasmota-tasmota-device-topic-vl53l0x-distance', + name: 'Distance', + read_only: true, + has_feedback: false, + min: 0, + max: 10000, + last_value: 3.65, + }, + ], + }; + expect(tasmotaHandler.discoveredDevices).to.deep.eq({ + 'tasmota-device-topic': expectedDevice, + }); + expect(tasmotaHandler.pendingDevices).to.deep.eq({}); + + assert.notCalled(mqttService.device.publish); + assert.calledWith(gladys.stateManager.get, 'deviceByExternalId', 'tasmota:tasmota-device-topic'); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'tasmota:tasmota-device-topic:VL53L0X:Distance', + state: 3.65, + }); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.TASMOTA.NEW_MQTT_DEVICE, + payload: expectedDevice, + }); + }); +}); diff --git a/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_AM2301.test.js b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_AM2301.test.js similarity index 100% rename from server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_AM2301.test.js rename to server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_AM2301.test.js diff --git a/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_DHT11.test.js b/server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_DHT11.test.js similarity index 100% rename from server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperture_humdity_DHT11.test.js rename to server/test/services/tasmota/lib/mqtt/handle-message/device-creation/tasmota.mqtt.handleMessage.deviceCreation-temperature_humdity_DHT11.test.js diff --git a/server/utils/constants.js b/server/utils/constants.js index 6f6404fba2..effdc68924 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -255,6 +255,7 @@ const DEVICE_FEATURE_CATEGORIES = { CO2_SENSOR: 'co2-sensor', COUNTER_SENSOR: 'counter-sensor', LEAK_SENSOR: 'leak-sensor', + DISTANCE_SENSOR: 'distance-sensor', CAMERA: 'camera', SWITCH: 'switch', SIREN: 'siren', @@ -337,6 +338,8 @@ const DEVICE_FEATURE_UNITS = { AMPERE: 'ampere', VOLT: 'volt', PPM: 'ppm', + MM: 'mm', + CM: 'cm', }; const ACTIONS_STATUS = {