Skip to content

Commit

Permalink
Tasmota: add components
Browse files Browse the repository at this point in the history
  • Loading branch information
atrovato committed Jan 12, 2020
1 parent 2edc7a7 commit e0c63bf
Show file tree
Hide file tree
Showing 13 changed files with 629 additions and 10 deletions.
21 changes: 21 additions & 0 deletions 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,
};
},
};
27 changes: 27 additions & 0 deletions 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`;
},
};
12 changes: 9 additions & 3 deletions server/services/tasmota/lib/features/index.js
Expand Up @@ -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,
Expand All @@ -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;
};

Expand All @@ -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);
}
Expand Down
27 changes: 27 additions & 0 deletions 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`;
},
};
7 changes: 4 additions & 3 deletions 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}`,
Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions server/services/tasmota/lib/mqtt/subStatus.js
Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions 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"
}
}
}
45 changes: 45 additions & 0 deletions 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"
}
}
}
@@ -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);
});
});

0 comments on commit e0c63bf

Please sign in to comment.