From e0c63bf2dc026d24fb945bdc67d5558072801978 Mon Sep 17 00:00:00 2001 From: atrovato <1839717+atrovato@users.noreply.github.com> Date: Sun, 12 Jan 2020 19:17:39 +0100 Subject: [PATCH] Tasmota: add components --- .../services/tasmota/lib/features/counter.js | 21 +++ .../services/tasmota/lib/features/humidity.js | 27 ++++ server/services/tasmota/lib/features/index.js | 12 +- .../tasmota/lib/features/temperature.js | 27 ++++ .../tasmota/lib/mqtt/featureStatus.js | 7 +- server/services/tasmota/lib/mqtt/subStatus.js | 8 +- .../tasmota/lib/device-creation/AM2301.json | 45 ++++++ .../tasmota/lib/device-creation/DHT11.json | 45 ++++++ .../device-creation/counter-feature.test.js | 121 +++++++++++++++ .../tasmota/lib/device-creation/counter.json | 43 ++++++ .../temperture_humdity_AM2301.test.js | 141 ++++++++++++++++++ .../temperture_humdity_DHT11.test.js | 141 ++++++++++++++++++ server/utils/constants.js | 1 + 13 files changed, 629 insertions(+), 10 deletions(-) create mode 100644 server/services/tasmota/lib/features/counter.js create mode 100644 server/services/tasmota/lib/features/humidity.js create mode 100644 server/services/tasmota/lib/features/temperature.js create mode 100644 server/test/services/tasmota/lib/device-creation/AM2301.json create mode 100644 server/test/services/tasmota/lib/device-creation/DHT11.json create mode 100644 server/test/services/tasmota/lib/device-creation/counter-feature.test.js create mode 100644 server/test/services/tasmota/lib/device-creation/counter.json create mode 100644 server/test/services/tasmota/lib/device-creation/temperture_humdity_AM2301.test.js create mode 100644 server/test/services/tasmota/lib/device-creation/temperture_humdity_DHT11.test.js diff --git a/server/services/tasmota/lib/features/counter.js b/server/services/tasmota/lib/features/counter.js new file mode 100644 index 0000000000..a4cb842360 --- /dev/null +++ b/server/services/tasmota/lib/features/counter.js @@ -0,0 +1,21 @@ +const { DEVICE_FEATURE_CATEGORIES, DEVICE_FEATURE_TYPES } = require('../../../../utils/constants'); + +module.exports = { + // Tasmota matcher + keyMatcher: /^StatusSNS\.COUNTER\.C\d+$/, + // Gladys feature + generateFeature: (device, key) => { + const position = key.replace(/C/i, ''); + const name = `Counter ${position}`.trim(); + + return { + category: DEVICE_FEATURE_CATEGORIES.COUNTER_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + name, + read_only: true, + has_feedback: false, + min: 0, + max: 10000, + }; + }, +}; diff --git a/server/services/tasmota/lib/features/humidity.js b/server/services/tasmota/lib/features/humidity.js new file mode 100644 index 0000000000..721db080bf --- /dev/null +++ b/server/services/tasmota/lib/features/humidity.js @@ -0,0 +1,27 @@ +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, +} = require('../../../../utils/constants'); + +module.exports = { + // Tasmota matcher + keyMatcher: /^StatusSNS\.(DHT11|AM2301)\.Humidity$/, + // Gladys feature + generateFeature: () => { + return { + category: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + name: 'Humidity', + read_only: true, + has_feedback: false, + min: 0, + max: 100, + unit: DEVICE_FEATURE_UNITS.PERCENT, + }; + }, + generateExternalId: (key, fullKey) => { + const parts = fullKey.split('.'); + return `${parts[1]}:Humidity`; + }, +}; diff --git a/server/services/tasmota/lib/features/index.js b/server/services/tasmota/lib/features/index.js index 7c2e8c86ba..74e1e9f8d6 100644 --- a/server/services/tasmota/lib/features/index.js +++ b/server/services/tasmota/lib/features/index.js @@ -11,6 +11,9 @@ const colorChannel = require('./colorChannel'); const colorScheme = require('./colorScheme'); const colorSpeed = require('./colorSpeed'); const colorTemperature = require('./colorTemperature'); +const counter = require('./counter'); +const humidity = require('./humidity'); +const temperature = require('./temperature'); const FEATURE_TEMPLATES = [ power, @@ -22,15 +25,18 @@ const FEATURE_TEMPLATES = [ colorScheme, colorSpeed, colorTemperature, + counter, + humidity, + temperature, ]; const generateValue = (featureTemplate, value) => { return typeof featureTemplate.readValue === 'function' ? featureTemplate.readValue(value) : value; }; -const generateExternalId = (featureTemplate, command) => { +const generateExternalId = (featureTemplate, command, fullKey) => { return typeof featureTemplate.generateExternalId === 'function' - ? featureTemplate.generateExternalId(command) + ? featureTemplate.generateExternalId(command, fullKey) : command; }; @@ -44,7 +50,7 @@ const recursiveSearch = (message, callback, key = undefined) => { }); if (featureTemplate) { - callback(featureTemplate, subKey, currentObj); + callback(featureTemplate, fullKey, subKey, currentObj); } else if (typeof currentObj === 'object') { recursiveSearch(currentObj, callback, fullKey); } diff --git a/server/services/tasmota/lib/features/temperature.js b/server/services/tasmota/lib/features/temperature.js new file mode 100644 index 0000000000..76857ac1b6 --- /dev/null +++ b/server/services/tasmota/lib/features/temperature.js @@ -0,0 +1,27 @@ +const { + DEVICE_FEATURE_CATEGORIES, + DEVICE_FEATURE_TYPES, + DEVICE_FEATURE_UNITS, +} = require('../../../../utils/constants'); + +module.exports = { + // Tasmota matcher + keyMatcher: /^StatusSNS\.(DHT11|AM2301)\.Temperature$/, + // Gladys feature + generateFeature: () => { + return { + category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + name: 'Temperature', + read_only: true, + has_feedback: false, + min: -100, + max: 200, + unit: DEVICE_FEATURE_UNITS.CELSIUS, + }; + }, + generateExternalId: (key, fullKey) => { + const parts = fullKey.split('.'); + return `${parts[1]}:Temperature`; + }, +}; diff --git a/server/services/tasmota/lib/mqtt/featureStatus.js b/server/services/tasmota/lib/mqtt/featureStatus.js index cc58018ce4..2de8a21893 100644 --- a/server/services/tasmota/lib/mqtt/featureStatus.js +++ b/server/services/tasmota/lib/mqtt/featureStatus.js @@ -1,8 +1,8 @@ const { EVENTS } = require('../../../../utils/constants'); const { recursiveSearch, generateExternalId, generateValue } = require('../features'); -const sendEvent = (gladysEvent, deviceExternalId, featureTemplate, command, value) => { - const featureExternalId = generateExternalId(featureTemplate, command); +const sendEvent = (gladysEvent, deviceExternalId, featureTemplate, fullKey, command, value) => { + const featureExternalId = generateExternalId(featureTemplate, command, fullKey); gladysEvent.emit(EVENTS.DEVICE.NEW_STATE, { device_feature_external_id: `tasmota:${deviceExternalId}:${featureExternalId}`, @@ -25,7 +25,8 @@ function featureStatus(deviceExternalId, message, gladysEvent, key) { recursiveSearch( sensorMsg, - (featureTemplate, command, value) => sendEvent(gladysEvent, deviceExternalId, featureTemplate, command, value), + (featureTemplate, fullKey, command, value) => + sendEvent(gladysEvent, deviceExternalId, featureTemplate, fullKey, command, value), key, ); return null; diff --git a/server/services/tasmota/lib/mqtt/subStatus.js b/server/services/tasmota/lib/mqtt/subStatus.js index 997516caec..8bc987294e 100644 --- a/server/services/tasmota/lib/mqtt/subStatus.js +++ b/server/services/tasmota/lib/mqtt/subStatus.js @@ -3,8 +3,8 @@ const { addSelector } = require('../../../../utils/addSelector'); const logger = require('../../../../utils/logger'); const { recursiveSearch, generateExternalId, generateValue } = require('../features'); -const addFeature = (device, featureTemplate, command, value) => { - const featureExternalId = generateExternalId(featureTemplate, command); +const addFeature = (device, featureTemplate, fullKey, command, value) => { + const featureExternalId = generateExternalId(featureTemplate, command, fullKey); const externalId = `${device.external_id}:${featureExternalId}`; const existingFeature = device.features.find((f) => f.external_id === externalId); @@ -45,8 +45,8 @@ const addFeature = (device, featureTemplate, command, value) => { function subStatus(device, message, gladysEvent) { const statusMsg = JSON.parse(message); - recursiveSearch(statusMsg, (featureTemplate, command, value) => { - const feature = addFeature(device, featureTemplate, command, value); + recursiveSearch(statusMsg, (featureTemplate, fullKey, command, value) => { + const feature = addFeature(device, featureTemplate, fullKey, command, value); if (feature) { gladysEvent.emit(EVENTS.DEVICE.NEW_STATE, { device_feature_external_id: feature.external_id, diff --git a/server/test/services/tasmota/lib/device-creation/AM2301.json b/server/test/services/tasmota/lib/device-creation/AM2301.json new file mode 100644 index 0000000000..cba885ae7e --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/AM2301.json @@ -0,0 +1,45 @@ +{ + "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": { + "AM2301": { + "Humidity": 65, + "Temperature": 23 + }, + "TempUnit": "C" + } + } +} diff --git a/server/test/services/tasmota/lib/device-creation/DHT11.json b/server/test/services/tasmota/lib/device-creation/DHT11.json new file mode 100644 index 0000000000..1dc8a535f1 --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/DHT11.json @@ -0,0 +1,45 @@ +{ + "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": { + "DHT11": { + "Humidity": 65, + "Temperature": 23 + }, + "TempUnit": "C" + } + } +} diff --git a/server/test/services/tasmota/lib/device-creation/counter-feature.test.js b/server/test/services/tasmota/lib/device-creation/counter-feature.test.js new file mode 100644 index 0000000000..69ec8a372c --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/counter-feature.test.js @@ -0,0 +1,121 @@ +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, + EVENTS, + WEBSOCKET_MESSAGE_TYPES, +} = require('../../../../../utils/constants'); + +const messages = require('./counter.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('TasmotaHandler - create device with COUNTER features', () => { + const tasmotaHandler = new TasmotaHandler(gladys, serviceId); + + beforeEach(() => { + tasmotaHandler.mqttService = mqttService; + sinon.reset(); + }); + + it('decode STATUS message', () => { + tasmotaHandler.handleMqttMessage('stat/tasmota-device-topic/STATUS', JSON.stringify(messages.STATUS)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS11', JSON.stringify(messages.STATUS11)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS8', JSON.stringify(messages.STATUS8)); + + const expectedDevice = { + name: 'Tasmota', + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.COUNTER_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.INTEGER, + external_id: 'tasmota:tasmota-device-topic:C1', + selector: 'tasmota-tasmota-device-topic-c1', + name: 'Counter 1', + read_only: true, + has_feedback: false, + min: 0, + max: 10000, + last_value: 57, + }, + ], + }; + expect(tasmotaHandler.mqttDevices).to.deep.eq({ + 'tasmota-device-topic': expectedDevice, + }); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({}); + + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'tasmota:tasmota-device-topic:C1', + state: 57, + }); + assert.calledWith(gladys.stateManager.get, 'deviceByExternalId', 'tasmota:tasmota-device-topic'); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.TASMOTA.NEW_DEVICE, + payload: expectedDevice, + }); + assert.notCalled(mqttService.device.publish); + }); +}); diff --git a/server/test/services/tasmota/lib/device-creation/counter.json b/server/test/services/tasmota/lib/device-creation/counter.json new file mode 100644 index 0000000000..4d9e709cb3 --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/counter.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": { + "COUNTER": { + "C1": 57 + } + } + } +} diff --git a/server/test/services/tasmota/lib/device-creation/temperture_humdity_AM2301.test.js b/server/test/services/tasmota/lib/device-creation/temperture_humdity_AM2301.test.js new file mode 100644 index 0000000000..f6d937c5b4 --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/temperture_humdity_AM2301.test.js @@ -0,0 +1,141 @@ +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('./AM2301.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('TasmotaHandler - create device with AM2301 temp/humidity features', () => { + const tasmotaHandler = new TasmotaHandler(gladys, serviceId); + + beforeEach(() => { + tasmotaHandler.mqttService = mqttService; + sinon.reset(); + }); + + it('decode STATUS message', () => { + tasmotaHandler.handleMqttMessage('stat/tasmota-device-topic/STATUS', JSON.stringify(messages.STATUS)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS11', JSON.stringify(messages.STATUS11)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS8', JSON.stringify(messages.STATUS8)); + + const expectedDevice = { + name: 'Tasmota', + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.PERCENT, + external_id: 'tasmota:tasmota-device-topic:AM2301:Humidity', + selector: 'tasmota-tasmota-device-topic-am2301-humidity', + name: 'Humidity', + read_only: true, + has_feedback: false, + min: 0, + max: 100, + last_value: 65, + }, + { + category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.CELSIUS, + external_id: 'tasmota:tasmota-device-topic:AM2301:Temperature', + selector: 'tasmota-tasmota-device-topic-am2301-temperature', + name: 'Temperature', + read_only: true, + has_feedback: false, + min: -100, + max: 200, + last_value: 23, + }, + ], + }; + expect(tasmotaHandler.mqttDevices).to.deep.eq({ + 'tasmota-device-topic': expectedDevice, + }); + expect(tasmotaHandler.pendingMqttDevices).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:AM2301:Temperature', + state: 23, + }); + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'tasmota:tasmota-device-topic:AM2301:Humidity', + state: 65, + }); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.TASMOTA.NEW_DEVICE, + payload: expectedDevice, + }); + }); +}); diff --git a/server/test/services/tasmota/lib/device-creation/temperture_humdity_DHT11.test.js b/server/test/services/tasmota/lib/device-creation/temperture_humdity_DHT11.test.js new file mode 100644 index 0000000000..0cf66dc131 --- /dev/null +++ b/server/test/services/tasmota/lib/device-creation/temperture_humdity_DHT11.test.js @@ -0,0 +1,141 @@ +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('./DHT11.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('TasmotaHandler - create device with DHT11 temp/humidity features', () => { + const tasmotaHandler = new TasmotaHandler(gladys, serviceId); + + beforeEach(() => { + tasmotaHandler.mqttService = mqttService; + sinon.reset(); + }); + + it('decode STATUS message', () => { + tasmotaHandler.handleMqttMessage('stat/tasmota-device-topic/STATUS', JSON.stringify(messages.STATUS)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS11', JSON.stringify(messages.STATUS11)); + + expect(tasmotaHandler.mqttDevices).to.deep.eq({}); + expect(tasmotaHandler.pendingMqttDevices).to.deep.eq({ + 'tasmota-device-topic': { + name: 'Tasmota', + 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.handleMqttMessage('stat/tasmota-device-topic/STATUS8', JSON.stringify(messages.STATUS8)); + + const expectedDevice = { + name: 'Tasmota', + model: 18, + external_id: 'tasmota:tasmota-device-topic', + selector: 'tasmota-tasmota-device-topic', + service_id: serviceId, + should_poll: false, + features: [ + { + category: DEVICE_FEATURE_CATEGORIES.HUMIDITY_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.PERCENT, + external_id: 'tasmota:tasmota-device-topic:DHT11:Humidity', + selector: 'tasmota-tasmota-device-topic-dht11-humidity', + name: 'Humidity', + read_only: true, + has_feedback: false, + min: 0, + max: 100, + last_value: 65, + }, + { + category: DEVICE_FEATURE_CATEGORIES.TEMPERATURE_SENSOR, + type: DEVICE_FEATURE_TYPES.SENSOR.DECIMAL, + unit: DEVICE_FEATURE_UNITS.CELSIUS, + external_id: 'tasmota:tasmota-device-topic:DHT11:Temperature', + selector: 'tasmota-tasmota-device-topic-dht11-temperature', + name: 'Temperature', + read_only: true, + has_feedback: false, + min: -100, + max: 200, + last_value: 23, + }, + ], + }; + expect(tasmotaHandler.mqttDevices).to.deep.eq({ + 'tasmota-device-topic': expectedDevice, + }); + expect(tasmotaHandler.pendingMqttDevices).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:DHT11:Temperature', + state: 23, + }); + assert.calledWith(gladys.event.emit, EVENTS.DEVICE.NEW_STATE, { + device_feature_external_id: 'tasmota:tasmota-device-topic:DHT11:Humidity', + state: 65, + }); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.TASMOTA.NEW_DEVICE, + payload: expectedDevice, + }); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index 405ffe21ef..8fdccf39c1 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -217,6 +217,7 @@ const DEVICE_FEATURE_CATEGORIES = { OPENING_SENSOR: 'opening-sensor', HUMIDITY_SENSOR: 'humidity-sensor', VIBRATION_SENSOR: 'vibration-sensor', + COUNTER_SENSOR: 'counter-sensor', LEAK_SENSOR: 'leak-sensor', CAMERA: 'camera', SWITCH: 'switch',