Skip to content

Commit

Permalink
Sonoff: split methods
Browse files Browse the repository at this point in the history
  • Loading branch information
atrovato committed Dec 14, 2019
1 parent 34a19b3 commit b283721
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 87 deletions.
1 change: 0 additions & 1 deletion server/package.json
Expand Up @@ -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'",
Expand Down
7 changes: 7 additions & 0 deletions 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,
};
38 changes: 38 additions & 0 deletions 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,
};
24 changes: 24 additions & 0 deletions 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,
};
74 changes: 6 additions & 68 deletions 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.
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions 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,
};
26 changes: 26 additions & 0 deletions 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,
};
41 changes: 41 additions & 0 deletions 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,
};
23 changes: 23 additions & 0 deletions 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,
};
42 changes: 42 additions & 0 deletions 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,
};
36 changes: 24 additions & 12 deletions 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.
Expand All @@ -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 = {
Expand Down

0 comments on commit b283721

Please sign in to comment.