1644 lines
68 KiB
JavaScript
1644 lines
68 KiB
JavaScript
/* jshint -W097 */
|
|
/* jshint strict:true */
|
|
/* jslint node: true */
|
|
/* jslint esversion: 6 */
|
|
'use strict';
|
|
|
|
const mqtt = require('mqtt-connection');
|
|
const net = require('net');
|
|
const types = require(__dirname + '/datapoints');
|
|
//const memwatch = require('memwatch-next');
|
|
//const heapdump = require('heapdump');
|
|
const hueCalc = true;
|
|
//var heapTest = 0;
|
|
const FORBIDDEN_CHARS = /[\]\[*,;'"`<>\\?]/g;
|
|
const mappingClients = {};
|
|
/*
|
|
* HSV to RGB color conversion
|
|
*
|
|
* H runs from 0 to 360 degrees
|
|
* S and V run from 0 to 100
|
|
*
|
|
* Ported from the excellent java algorithm by Eugene Vishnevsky at:
|
|
* http://www.cs.rit.edu/~ncs/color/t_convert.html
|
|
*/
|
|
function hsvToRgb(h, s, v) {
|
|
let r, g, b;
|
|
let i;
|
|
let f, p, q, t;
|
|
|
|
// Make sure our arguments stay in-range
|
|
h = Math.max(0, Math.min(360, h));
|
|
s = Math.max(0, Math.min(100, s));
|
|
v = Math.max(0, Math.min(100, v));
|
|
|
|
// We accept saturation and value arguments from 0 to 100 because that's
|
|
// how Photoshop represents those values. Internally, however, the
|
|
// saturation and value are calculated from a range of 0 to 1. We make
|
|
// That conversion here.
|
|
s /= 100;
|
|
v /= 100;
|
|
|
|
if (s === 0) {
|
|
// Achromatic (grey)
|
|
r = g = b = v;
|
|
return [
|
|
Math.round(r * 255),
|
|
Math.round(g * 255),
|
|
Math.round(b * 255)
|
|
];
|
|
}
|
|
|
|
h /= 60; // sector 0 to 5
|
|
i = Math.floor(h);
|
|
f = h - i; // factorial part of h
|
|
p = v * (1 - s);
|
|
q = v * (1 - s * f);
|
|
t = v * (1 - s * (1 - f));
|
|
|
|
switch (i) {
|
|
case 0:
|
|
r = v;
|
|
g = t;
|
|
b = p;
|
|
break;
|
|
|
|
case 1:
|
|
r = q;
|
|
g = v;
|
|
b = p;
|
|
break;
|
|
|
|
case 2:
|
|
r = p;
|
|
g = v;
|
|
b = t;
|
|
break;
|
|
|
|
case 3:
|
|
r = p;
|
|
g = q;
|
|
b = v;
|
|
break;
|
|
|
|
case 4:
|
|
r = t;
|
|
g = p;
|
|
b = v;
|
|
break;
|
|
|
|
default: // case 5:
|
|
r = v;
|
|
g = p;
|
|
b = q;
|
|
}
|
|
|
|
return [
|
|
Math.round(r * 255),
|
|
Math.round(g * 255),
|
|
Math.round(b * 255)
|
|
];
|
|
}
|
|
|
|
function componentToHex(c) {
|
|
const hex = c.toString(16);
|
|
return hex.length === 1 ? '0' + hex : hex;
|
|
}
|
|
|
|
function toPaddedHexString(num, len) {
|
|
if (len === 2) {
|
|
if (num > 255) {
|
|
num = 255;
|
|
}
|
|
}
|
|
const str = num.toString(16);
|
|
return '0'.repeat(len - str.length) + str;
|
|
}
|
|
|
|
function MQTTServer(adapter) {
|
|
if (!(this instanceof MQTTServer)) return new MQTTServer(adapter);
|
|
|
|
const NO_PREFIX = '';
|
|
|
|
let server = new net.Server();
|
|
const clients = {};
|
|
const tasks = [];
|
|
let messageId = 1;
|
|
let persistentSessions = {};
|
|
let resending = false;
|
|
let resendTimer = null;
|
|
|
|
const cacheAddedObjects = {};
|
|
const cachedModeExor = {};
|
|
const cachedReadColors = {};
|
|
|
|
this.destroy = cb => {
|
|
if (resendTimer) {
|
|
clearInterval(resendTimer);
|
|
resendTimer = null;
|
|
}
|
|
|
|
if (server) {
|
|
let cnt = 0;
|
|
for (let id in clients) {
|
|
if (clients.hasOwnProperty(id)) {
|
|
cnt++;
|
|
adapter.setForeignState(adapter.namespace + '.' + clients[id].iobId + '.alive', false, true, () => {
|
|
if (!--cnt) {
|
|
// to release all resources
|
|
server.close(() => cb && cb());
|
|
server = null;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (!cnt) {
|
|
// to release all resources
|
|
server.close(() => cb && cb());
|
|
server = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
memwatch.on('leak', (info) => {
|
|
//console.error('Memory leak detected:\n', info);
|
|
adapter.log.info('Memory leak detected:\n', info);
|
|
var filename='/opt/iobroker/dumps/' + Date.now() + '.heapsnapshot';
|
|
heapdump.writeSnapshot((err,filename) => {
|
|
if (err) {
|
|
console.error(err);
|
|
}
|
|
else console.error('Wrote snapshot: ' + filename);
|
|
});
|
|
});
|
|
*/
|
|
/*
|
|
memwatch.on('stats', function(stats) {
|
|
adapter.log.info("stats:",stats);
|
|
});
|
|
*/
|
|
|
|
function setColor(channelId, val) {
|
|
//adapter.log.info('color write: '+ val);
|
|
const stateId = 'Color';
|
|
if (clients[channelId]._map && clients[channelId]._map[stateId]) {
|
|
setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || 'cmnd/sonoff/Color', val, adapter.config.defaultQoS);
|
|
} else if (clients[channelId]._fallBackName) {
|
|
setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS);
|
|
} else {
|
|
adapter.log.warn('Unknown mapping for "' + stateId + '"');
|
|
}
|
|
}
|
|
|
|
function setPower(channelId, val) {
|
|
const stateId = 'POWER';
|
|
if (clients[channelId]._map && clients[channelId]._map[stateId]) {
|
|
setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || 'cmnd/sonoff/POWER', val ? 'ON' : 'OFF', adapter.config.defaultQoS);
|
|
} else if (clients[channelId]._fallBackName) {
|
|
setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS);
|
|
} else {
|
|
adapter.log.warn('Unknown mapping for "' + stateId + '"');
|
|
}
|
|
}
|
|
|
|
function setStateImmediate(channelId,stateId,val) {
|
|
if (clients[channelId]._map && clients[channelId]._map[stateId]) {
|
|
setImmediate(sendState2Client, clients[channelId], clients[channelId]._map[stateId] || ('cmnd/sonoff/' + stateId), val, adapter.config.defaultQoS);
|
|
} else if (clients[channelId]._fallBackName) {
|
|
setImmediate(sendState2Client, clients[channelId], 'cmnd/' + clients[channelId]._fallBackName + '/' + stateId, val, adapter.config.defaultQoS);
|
|
} else {
|
|
adapter.log.warn('Unknown mapping for "' + stateId + '"');
|
|
}
|
|
}
|
|
|
|
function _setState(id, val) {
|
|
adapter.setForeignState(id, val, true, () => {
|
|
setImmediate(processTasks);
|
|
});
|
|
}
|
|
|
|
function updateState(task, val) {
|
|
if (val !== undefined) {
|
|
task.cb = _setState;
|
|
task.cbArg = val;
|
|
}
|
|
tasks.push(task);
|
|
if (tasks.length === 1) {
|
|
setImmediate(processTasks);
|
|
}
|
|
}
|
|
|
|
const specVars = ['Red', 'Green', 'Blue', 'WW', 'CW', 'Color', 'RGB_POWER', 'WW_POWER', 'CW_POWER', 'Hue', 'Saturation'];
|
|
function onStateChangedColors(id, state, channelId, stateId) {
|
|
if (!channelId) {
|
|
let parts = id.split('.');
|
|
stateId = parts.pop();
|
|
|
|
if (stateId === 'level' || stateId === 'state' || stateId === 'red' || stateId === 'blue' || stateId === 'green') {
|
|
stateId = parts.pop() + '.' + stateId;
|
|
}
|
|
|
|
channelId = parts.splice(2, parts.length).join('.');
|
|
}
|
|
const ledModeIdExor = adapter.namespace + '.' + channelId + '.modeLedExor';
|
|
if (cachedModeExor[ledModeIdExor] === undefined) {
|
|
return adapter.getForeignState(ledModeIdExor, (err, _state) => {
|
|
cachedModeExor[ledModeIdExor] = _state ? _state.val || false : true;
|
|
setImmediate(() => onStateChangedColors(id, state, channelId, stateId));
|
|
});
|
|
}
|
|
|
|
// ledstripe objects
|
|
const exorWhiteLeds = cachedModeExor[ledModeIdExor]; // exor for white leds and color leds => if white leds are switched on, color leds are switched off and vice versa (default on)
|
|
|
|
// now evaluate ledstripe vars
|
|
// adaptions for magichome tasmota
|
|
if (stateId.match(/Color\d?/)) {
|
|
//adapter.log.info('sending color');
|
|
// id = sonoff.0.DVES_96ABFA.Color
|
|
// statid=Color
|
|
// state = {"val":"#faadcf","ack":false,"ts":1520146102580,"q":0,"from":"system.adapter.web.0","lc":1520146102580}
|
|
|
|
// set white to rgb or rgbww
|
|
adapter.getObject(id, (err, obj) => {
|
|
if (!obj) {
|
|
adapter.log.warn('ill rgbww obj');
|
|
} else {
|
|
const role = obj.common.role;
|
|
let color;
|
|
|
|
//adapter.log.info(state.val);
|
|
if (role === 'level.color.rgbww') {
|
|
// rgbww
|
|
if (state.val.toUpperCase() === '#FFFFFF') {
|
|
// transform white to WW
|
|
//color='000000FF';
|
|
color = state.val.substring(1) + '00';
|
|
} else {
|
|
// strip # char and add ww
|
|
color = state.val.substring(1) + '00';
|
|
}
|
|
} else if (role === 'level.color.rgbcwww') {
|
|
color = state.val.substring(1) + '0000';
|
|
} else {
|
|
// rgb, strip # char
|
|
color = state.val.substring(1);
|
|
}
|
|
|
|
//adapter.log.info('color :' + color + ' : ' + role);
|
|
// strip # char
|
|
//color=state.val.substring(1);
|
|
|
|
setColor(channelId, color);
|
|
|
|
// set rgb too
|
|
const hidE = id.split('.');
|
|
const deviceDesc = hidE[0] + '.' + hidE[1] + '.' + hidE[2];
|
|
let idAlive = deviceDesc + '.Red';
|
|
adapter.setState(idAlive, 100 * parseInt(color.substring(0, 2), 16) / 255, true);
|
|
|
|
idAlive = deviceDesc + '.Green';
|
|
adapter.setState(idAlive, 100 * parseInt(color.substring(2, 4), 16) / 255, true);
|
|
|
|
idAlive = deviceDesc + '.Blue';
|
|
adapter.setState(idAlive, 100 * parseInt(color.substring(4, 6), 16) / 255, true);
|
|
}
|
|
});
|
|
} else {
|
|
const hidE = id.split('.');
|
|
const deviceDesc = hidE[0] + '.' + hidE[1] + '.' + hidE[2];
|
|
if (stateId.match(/Red\d?/)) {
|
|
// set red component
|
|
if (state.val > 100) state.val = 100;
|
|
const red = toPaddedHexString(Math.floor(255 * state.val / 100), 2);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
// replace red component
|
|
const out = red + color.substring(2, 10);
|
|
adapter.setState(idAlive, '#' + out, false);
|
|
setColor(channelId, out);
|
|
});
|
|
} else
|
|
if (stateId.match(/Green\d?/)) {
|
|
// set green component
|
|
if (state.val > 100) state.val = 100;
|
|
const green = toPaddedHexString(Math.floor(255 * state.val / 100), 2);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
// replace green component
|
|
const out = color.substring(0, 2) + green + color.substring(4, 10);
|
|
adapter.setState(idAlive, '#' + out, false);
|
|
setColor(channelId, out);
|
|
});
|
|
} else
|
|
if (stateId.match(/Blue\d?/)) {
|
|
// set blue component
|
|
if (state.val > 100) state.val = 100;
|
|
const blue = toPaddedHexString(Math.floor(255 * state.val / 100), 2);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
// replace blue component
|
|
const out = color.substring(0, 4) + blue + color.substring(6, 10);
|
|
adapter.setState(idAlive, '#' + out, false);
|
|
setColor(channelId, out);
|
|
});
|
|
} else
|
|
if (stateId.match(/RGB_POWER\d?/)) {
|
|
// set ww component
|
|
const rgbpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
//adapter.log.warn('ill state Color');
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
let rgb = '000000';
|
|
if (rgbpow === true) {
|
|
rgb = 'FFFFFF';
|
|
}
|
|
// replace rgb component
|
|
let out = rgb + color.substring(6, 10);
|
|
|
|
if (rgbpow && exorWhiteLeds) {
|
|
//adapter.log.info('reset white');
|
|
out = rgb + '0000';
|
|
let idAlive = deviceDesc + '.WW_POWER';
|
|
adapter.setState(idAlive, false, false);
|
|
idAlive = deviceDesc + '.WW';
|
|
adapter.setState(idAlive, 0, false);
|
|
idAlive = deviceDesc + '.CW_POWER';
|
|
adapter.setState(idAlive, false, false);
|
|
idAlive = deviceDesc + '.CW';
|
|
adapter.setState(idAlive, 0, false);
|
|
}
|
|
setColor(channelId, out);
|
|
adapter.setState(idAlive, '#' + out, false);
|
|
if (rgbpow) {
|
|
setPower(channelId, true)
|
|
}
|
|
// if led_mode&1, exor white leds
|
|
|
|
});
|
|
} else
|
|
// calc hue + saturation params to rgb
|
|
if (hueCalc && stateId.match(/Hue\d?/)) {
|
|
let hue = state.val;
|
|
if (hue > 359) hue = 359;
|
|
// recalc color by hue
|
|
const idAlive = deviceDesc + '.Dimmer';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
const dim = 100;
|
|
adapter.setState(idAlive, dim, true);
|
|
//adapter.log.warn('ill state Dimmer');
|
|
} else {
|
|
const dim = state.val;
|
|
let idAlive = deviceDesc + '.Saturation';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
const sat = 100;
|
|
adapter.setState(idAlive, sat, true);
|
|
} else {
|
|
const sat = state.val;
|
|
const rgb = hsvToRgb(hue, sat, dim);
|
|
const hexval = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
|
|
let idAlive = deviceDesc + '.Color';
|
|
adapter.setState(idAlive, '#' + hexval, false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} else
|
|
if (hueCalc && stateId.match(/Saturation\d?/)) {
|
|
let sat = state.val;
|
|
if (sat > 100) sat = 100;
|
|
// recalc color by saturation
|
|
const idAlive = deviceDesc + '.Dimmer';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
const dim = 100;
|
|
adapter.setState(idAlive, dim, true);
|
|
//adapter.log.warn('ill state Dimmer');
|
|
} else {
|
|
const dim = state.val;
|
|
const idAlive = deviceDesc + '.Hue';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
const hue = 100;
|
|
adapter.setState(idAlive, hue, true);
|
|
} else {
|
|
const hue = state.val;
|
|
const rgb = hsvToRgb(hue, sat, dim);
|
|
const hexval = componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.setState(idAlive, '#' + hexval, false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
// get color attributes to check other ledstripe vars
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignObject(idAlive, (err, obj) => {
|
|
if (!obj) {
|
|
// no color object
|
|
adapter.log.warn(`unknown object: ${id}: ${state}`);
|
|
} else {
|
|
const role = obj.common.role;
|
|
//if (role='level.color.rgb') return;
|
|
let wwindex;
|
|
if (role === 'level.color.rgbww') {
|
|
wwindex = 6;
|
|
} else {
|
|
wwindex = 8;
|
|
}
|
|
|
|
if (stateId.match(/WW_POWER\d?/)) {
|
|
// set ww component
|
|
const wwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.log.warn('ill state Color');
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
let ww = '00';
|
|
if (wwpow) {
|
|
ww = 'FF';
|
|
}
|
|
// replace ww component
|
|
let out = color.substring(0, wwindex) + ww;
|
|
if (wwpow && exorWhiteLeds) {
|
|
//adapter.log.info('reset white');
|
|
out = '000000' + ww;
|
|
let idAlive = deviceDesc + '.RGB_POWER';
|
|
adapter.setState(idAlive,false, false);
|
|
}
|
|
|
|
let idAlive = deviceDesc + '.Color';
|
|
adapter.setState(idAlive,'#' + out, false);
|
|
setColor(channelId, out);
|
|
|
|
// set ww channel
|
|
idAlive = deviceDesc + '.WW';
|
|
adapter.setState(idAlive, 100 * parseInt(out.substring(6, 8), 16) / 255, true);
|
|
|
|
// in case POWER is off, switch it on
|
|
wwpow && setPower(channelId, true)
|
|
});
|
|
} else
|
|
if (stateId.match(/CW_POWER\d?/)) {
|
|
// set ww component
|
|
const cwpow = state.val === 'true' || state.val === true || state.val === 1 || state.val === '1';
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.log.warn('ill state Color');
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
let cw = '00';
|
|
if (cwpow) {
|
|
cw = 'FF';
|
|
}
|
|
// replace cw component
|
|
let out = color.substring(0, 6) + cw + color.substring(8, 10);
|
|
if (cwpow && exorWhiteLeds) {
|
|
//adapter.log.info('reset white');
|
|
out = '000000' + cw + color.substring(8, 10);
|
|
let idAlive = deviceDesc + '.RGB_POWER';
|
|
adapter.setState(idAlive, false, false);
|
|
}
|
|
|
|
let idAlive = deviceDesc + '.Color';
|
|
adapter.setState(idAlive, '#' + out, false);
|
|
setColor(channelId, out);
|
|
|
|
// set cw channel
|
|
idAlive = deviceDesc + '.CW';
|
|
adapter.setState(idAlive, 100 * parseInt(out.substring(6, 8), 16) / 255, true);
|
|
|
|
// in case POWER is off, switch it on
|
|
if (cwpow) {
|
|
let idAlive = deviceDesc + '.POWER';
|
|
adapter.setState(idAlive, true, false);
|
|
}
|
|
});
|
|
} else
|
|
if (stateId.match(/WW\d?/)) {
|
|
// set ww component
|
|
const ww = toPaddedHexString(Math.floor(255 * state.val / 100), 2);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
// replace ww component
|
|
const out = color.substring(0, wwindex) + ww;
|
|
setColor(channelId, out);
|
|
});
|
|
} else
|
|
if (stateId.match(/CW\d?/)) {
|
|
// set ww component
|
|
const cw = toPaddedHexString(Math.floor(255 * state.val / 100), 2);
|
|
const idAlive = deviceDesc + '.Color';
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state) {
|
|
adapter.setState(idAlive, '#000000', false);
|
|
return;
|
|
}
|
|
const color = state.val.substring(1);
|
|
// replace cw component
|
|
const out = color.substring(0, 6) + cw + color.substring(8, 10);
|
|
setColor(channelId, out);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this.onStateChange = (id, state) => {
|
|
adapter.log.debug('onStateChange ' + id + ': ' + JSON.stringify(state));
|
|
if (server && state && !state.ack) {
|
|
// find client.id
|
|
let parts = id.split('.');
|
|
const stateId = parts.pop();
|
|
const channelId = parts.splice(2, parts.length).join('.');
|
|
if (clients[mappingClients[channelId]]) {
|
|
// check for special ledstripe vars
|
|
if (specVars.indexOf(stateId) === -1) {
|
|
// other objects
|
|
adapter.getObject(id, (err, obj) => {
|
|
if (!obj) {
|
|
adapter.log.warn(`invalid obj ${id}`);
|
|
} else {
|
|
const type = obj.common.type;
|
|
switch (type) {
|
|
case 'boolean':
|
|
setStateImmediate(mappingClients[channelId], stateId, state.val ? 'ON' : 'OFF');
|
|
break;
|
|
case 'number':
|
|
setStateImmediate(mappingClients[channelId], stateId, state.val.toString());
|
|
break;
|
|
case 'string':
|
|
setStateImmediate(mappingClients[channelId], stateId, state.val);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
onStateChangedColors(id, state, channelId, stateId);
|
|
}
|
|
} else {
|
|
//Client:"DVES_96ABFA : MagicHome" not connected => State: sonoff.0.myState - Value: 0, ack: false, time stamp: 1520369614189, last changed: 1520369614189
|
|
// if (server && state && !state.ack) {
|
|
// server = false
|
|
// or state = false
|
|
// or state.ack = true
|
|
// or clients[channelId] = false
|
|
adapter.log.warn(`Client "${channelId}" not connected`);
|
|
|
|
/*
|
|
if (!clients[channelId]) {
|
|
var idAlive='sonoff.0.'+channelId+'.INFO.IPAddress';
|
|
adapter.getForeignState(idAlive, function (err, state) {
|
|
if (!state) {
|
|
adapter.log.warn('Client "' + channelId + '" could not get ip adress');
|
|
} else {
|
|
var ip=state.val;
|
|
adapter.log.warn('Clients ip "' + ip);
|
|
|
|
request('http://'+ip+'/cm?cmnd=Restart 1', function(error, response, body) {
|
|
if (error || response.statusCode !== 200) {
|
|
log('Fehler beim Neustart von Sonoff: ' + channelId + ' (StatusCode = ' + response.statusCode + ')');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}*/
|
|
}
|
|
}
|
|
};
|
|
|
|
function processTasks() {
|
|
if (tasks && tasks.length) {
|
|
let task = tasks[0];
|
|
if (task.type === 'addObject') {
|
|
if (!cacheAddedObjects[task.id]) {
|
|
cacheAddedObjects[task.id] = true;
|
|
adapter.getForeignObject(task.id, (err, obj) => {
|
|
if (!obj) {
|
|
adapter.setForeignObject(task.id, task.data, (/* err */) => {
|
|
adapter.log.info('new object created: ' + task.id);
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
});
|
|
} else {
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
}
|
|
} else if (task.type === 'extendObject') {
|
|
adapter.extendObject(task.id, task.data, (/* err */) => {
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
});
|
|
} else if (task.type === 'deleteState') {
|
|
adapter.deleteState('', '', task.id, (/* err */) => {
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
});
|
|
} else {
|
|
adapter.log.error('Unknown task name: ' + JSON.stringify(task));
|
|
tasks.shift();
|
|
if (task.cb) {
|
|
task.cb(task.id, task.cbArg);
|
|
} else {
|
|
setImmediate(processTasks);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createClient(client) {
|
|
// mqtt.0.cmnd.sonoff.POWER
|
|
// mqtt.0.stat.sonoff.POWER
|
|
let isStart = !tasks.length;
|
|
|
|
let id = adapter.namespace + '.' + client.iobId;
|
|
let obj = {
|
|
_id: id,
|
|
common: {
|
|
name: client.id,
|
|
desc: ''
|
|
},
|
|
native: {
|
|
clientId: client.id
|
|
},
|
|
type: 'channel'
|
|
};
|
|
tasks.push({type: 'addObject', id: obj._id, data: obj});
|
|
|
|
obj = {
|
|
_id: id + '.alive',
|
|
common: {
|
|
type: 'boolean',
|
|
role: 'indicator.connected',
|
|
read: true,
|
|
write: false,
|
|
name: client.id + ' alive'
|
|
},
|
|
type: 'state'
|
|
};
|
|
tasks.push({type: 'addObject', id: obj._id, data: obj});
|
|
if (isStart) {
|
|
processTasks(tasks);
|
|
}
|
|
}
|
|
|
|
function updateClients() {
|
|
let text = '';
|
|
if (clients) {
|
|
for (let id in clients) {
|
|
text += (text ? ',' : '') + id;
|
|
}
|
|
}
|
|
|
|
adapter.setState('info.connection', {val: text, ack: true});
|
|
}
|
|
|
|
function updateAlive(client, alive) {
|
|
let idAlive = adapter.namespace + '.' + client.iobId + '.alive';
|
|
|
|
adapter.getForeignState(idAlive, (err, state) => {
|
|
if (!state || state.val !== alive) {
|
|
adapter.setForeignState(idAlive, alive, true);
|
|
}
|
|
});
|
|
}
|
|
|
|
function sendState2Client(client, topic, state, qos, retain, cb) {
|
|
if (typeof qos === 'function') {
|
|
cb = qos;
|
|
qos = undefined;
|
|
}
|
|
if (typeof retain === 'function') {
|
|
cb = retain;
|
|
retain = undefined;
|
|
}
|
|
adapter.log.debug('Send to "' + client.id + '": ' + topic + ' = ' + state);
|
|
client.publish({topic: topic, payload: state, qos: qos, retain: retain, messageId: messageId++}, cb);
|
|
messageId &= 0xFFFFFFFF;
|
|
}
|
|
|
|
function resendMessages2Client(client, messages, i) {
|
|
i = i || 0;
|
|
if (messages && i < messages.length) {
|
|
try {
|
|
messages[i].ts = Date.now();
|
|
messages[i].count++;
|
|
adapter.log.debug(`Client [${client.id}] Resend messages on connect: ${messages[i].topic} = ${messages[i].payload}`);
|
|
if (messages[i].cmd === 'publish') {
|
|
client.publish(messages[i]);
|
|
}
|
|
} catch (e) {
|
|
adapter.log.warn(`Client [${client.id}] Cannot resend message: ${e}`);
|
|
}
|
|
|
|
if (adapter.config.sendInterval) {
|
|
setTimeout(() => resendMessages2Client(client, messages, i + 1), adapter.config.sendInterval);
|
|
} else {
|
|
setImmediate(() => resendMessages2Client(client, messages, i + 1));
|
|
}
|
|
} else {
|
|
//return;
|
|
}
|
|
}
|
|
|
|
function addObject(attr, client, prefix, path) {
|
|
let replaceAttr = types[attr].replace || attr;
|
|
let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr.replace(FORBIDDEN_CHARS, '_');
|
|
let obj = {
|
|
type: 'addObject',
|
|
id: id,
|
|
data: {
|
|
_id: id,
|
|
common: Object.assign({}, types[attr]),
|
|
native: {},
|
|
type: 'state'
|
|
}
|
|
};
|
|
obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr;
|
|
return obj;
|
|
}
|
|
|
|
function checkData(client, topic, prefix, data, unit, path) {
|
|
if (!data || typeof data !== 'object') return;
|
|
const ledModeReadColorsID = adapter.namespace + '.' + client.iobId + '.modeReadColors';
|
|
if (cachedReadColors[ledModeReadColorsID] === undefined) {
|
|
return adapter.getForeignState(ledModeReadColorsID, (err, state) => {
|
|
cachedReadColors[ledModeReadColorsID] = (state && state.val) || false;
|
|
setImmediate(() => checkData(client, topic, prefix, data, unit, path));
|
|
});
|
|
}
|
|
|
|
path = path || [];
|
|
prefix = prefix || '';
|
|
|
|
// first get the units
|
|
if (data.TempUnit) {
|
|
unit = data.TempUnit;
|
|
if (unit.indexOf('°') !== 0) {
|
|
unit = '°' + unit.replace('°');
|
|
}
|
|
}
|
|
|
|
for (let attr in data) {
|
|
if (!data.hasOwnProperty(attr)) {
|
|
adapter.log.warn('[' + client.id + '] attr error: ' + attr + '' + data[attr]);
|
|
continue;
|
|
}
|
|
|
|
if (typeof data[attr] === 'object') {
|
|
// check for arrays
|
|
if (types[attr]) {
|
|
if (types[attr].type === 'array') {
|
|
// transform to array of attributes
|
|
for (let i = 1; i <= 10; i++) {
|
|
let val = data[attr][i - 1];
|
|
if (typeof val === 'undefined') break;
|
|
// define new object
|
|
let replaceAttr = attr.replace(FORBIDDEN_CHARS, '_') + i.toString();
|
|
let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr;
|
|
let obj = {
|
|
type: 'addObject',
|
|
id: id,
|
|
data: {
|
|
_id: id,
|
|
common: Object.assign({}, types[attr]),
|
|
native: {},
|
|
type: 'state'
|
|
}
|
|
};
|
|
obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr;
|
|
obj.data.common.type = 'number';
|
|
updateState(obj, val);
|
|
}
|
|
} else {
|
|
let nPath = Object.assign([], path);
|
|
nPath.push(attr.replace(FORBIDDEN_CHARS, '_'));
|
|
checkData(client, topic, prefix, data[attr], unit, nPath);
|
|
nPath = undefined;
|
|
}
|
|
} else {
|
|
let nPath = Object.assign([], path);
|
|
nPath.push(attr.replace(FORBIDDEN_CHARS, '_'));
|
|
checkData(client, topic, prefix, data[attr], unit, nPath);
|
|
nPath = undefined;
|
|
}
|
|
} else if (types[attr]) {
|
|
const allowReadColors = cachedReadColors[ledModeReadColorsID]; // allow for color read from MQTT (default off)
|
|
// create object
|
|
const obj = addObject(attr, client, prefix, path);
|
|
let replaceAttr = types[attr].replace || attr;
|
|
|
|
if (attr === 'Temperature') {
|
|
obj.data.common.unit = unit || obj.data.common.unit || '°C';
|
|
}
|
|
if (attr === 'Humidity') {
|
|
obj.data.common.unit = unit || obj.data.common.unit || '%';
|
|
}
|
|
if (obj.data.common.storeMap) {
|
|
delete obj.data.common.storeMap;
|
|
client._map[replaceAttr] = topic.replace(/$\w+\//, 'cmnd/').replace(/\/\w+$/, '/' + replaceAttr);
|
|
}
|
|
|
|
console.log(attr);
|
|
// adaptions for magichome tasmota
|
|
if (attr === 'Color') {
|
|
// read vars
|
|
// if ledFlags bit 2, read color from tasmota, else ignore
|
|
|
|
if (data[attr].length === 10) {
|
|
obj.data.common.role = 'level.color.rgbcwww'; // RGB + cold white + white???
|
|
} else if (data[attr].length === 8) {
|
|
obj.data.common.role = 'level.color.rgbww'; // RGB + White
|
|
} else {
|
|
obj.data.common.role = 'level.color.rgb';
|
|
}
|
|
|
|
if (hueCalc) {
|
|
// Create LEDs modes if required
|
|
let xObj = addObject('modeReadColors', client, prefix, path);
|
|
updateState(xObj);
|
|
|
|
xObj = addObject('modeLedExor', client, prefix, path);
|
|
updateState(xObj);
|
|
|
|
xObj = addObject('Hue', client, prefix, path);
|
|
updateState(xObj);
|
|
|
|
xObj = addObject('Saturation', client, prefix, path);
|
|
updateState(xObj);
|
|
|
|
xObj = addObject('Red', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(0, 2), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('Green', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(2, 4), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('Blue', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(4, 6), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('RGB_POWER', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
let val = parseInt(data[attr].substring(0, 6), 16);
|
|
updateState(xObj, allowReadColors ? (val > 0) : undefined);
|
|
|
|
if (obj.data.common.role === 'level.color.rgbww') {
|
|
// rgbww
|
|
xObj = addObject('WW', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(6, 8), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('WW_POWER', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? (val > 0) : undefined);
|
|
} else
|
|
if (obj.data.common.role === 'level.color.rgbcwww') {
|
|
// rgbcwww
|
|
xObj = addObject('CW', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(6, 8), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('CW_POWER', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? (val > 0) : undefined);
|
|
|
|
xObj = addObject('WW', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
updateState(xObj, allowReadColors ? 100 * parseInt(data[attr].substring(8, 10), 16) / 255 : undefined);
|
|
|
|
xObj = addObject('WW_POWER', client, prefix, path);
|
|
xObj.data.common.read = allowReadColors;
|
|
val = parseInt(data[attr].substring(8, 10), 16);
|
|
updateState(xObj, allowReadColors ? (val > 0) : undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
let val;
|
|
if (obj.data.common.type === 'number') {
|
|
val = parseFloat(data[attr]);
|
|
} else if (obj.data.common.type === 'boolean') {
|
|
val = (data[attr] || '').toUpperCase() === 'ON';
|
|
} else {
|
|
if (attr === 'Color') {
|
|
// add # char
|
|
if (allowReadColors) {
|
|
val = '#' + data[attr];
|
|
}
|
|
} else {
|
|
val = data[attr];
|
|
}
|
|
}
|
|
updateState(obj, val);
|
|
} else {
|
|
// not in list, auto insert
|
|
//if (client.id=='DVES_008ADB') {
|
|
// adapter.log.warn('[' + client.id + '] Received attr not in list: ' + attr + '' + data[attr]);
|
|
//}
|
|
// tele/sonoff/SENSOR tele/sonoff/STATE => read only
|
|
// stat/sonoff/RESULT => read,write
|
|
|
|
let parts = topic.split('/');
|
|
// auto generate objects
|
|
if ((parts[0] === 'tele' && (
|
|
(adapter.config.TELE_SENSOR && parts[2] === 'SENSOR') ||
|
|
(adapter.config.TELE_STATE && parts[2] === 'STATE'))) ||
|
|
(parts[0] === 'stat' &&
|
|
(adapter.config.STAT_RESULT && parts[2] === 'RESULT'))
|
|
) {
|
|
//adapter.log.info('[' + client.id + '] auto insert object: ' + attr + ' ' + data[attr] + ' flags: ' + autoFlags);
|
|
//if (data[attr]) {
|
|
const xdata = data[attr];
|
|
//adapter.log.info('[' + client.id + '] auto insert object: ' + attr + ' ' + data[attr]);
|
|
let attributes;
|
|
if (parts[2] === 'RESULT') {
|
|
if (xdata.isNaN) {
|
|
// string
|
|
attributes = {type: 'string', role:'value', read: true, write: true};
|
|
} else {
|
|
// number
|
|
attributes = {type: 'number', role:'value', read: true, write: true};
|
|
}
|
|
} else {
|
|
if (xdata.isNaN) {
|
|
// string
|
|
attributes = {type: 'string', role: 'value', read: true, write: false};
|
|
} else {
|
|
// number
|
|
attributes = {type: 'number', role: 'value', read: true, write: false};
|
|
}
|
|
}
|
|
let replaceAttr = attr ;
|
|
let id = adapter.namespace + '.' + client.iobId + '.' + (prefix ? prefix + '.' : '') + (path.length ? path.join('_') + '_' : '') + replaceAttr;
|
|
let obj = {
|
|
type: 'addObject',
|
|
id: id,
|
|
data: {
|
|
_id: id,
|
|
common: Object.assign({}, attributes),
|
|
native: {},
|
|
type: 'state'
|
|
}
|
|
};
|
|
obj.data.common.name = client.id + ' ' + (prefix ? prefix + ' ' : '') + (path.length ? path.join(' ') + ' ' : '') + ' ' + replaceAttr;
|
|
updateState(obj, xdata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function receivedTopic(packet, client) {
|
|
client.states = client.states || {};
|
|
client.states[packet.topic] = {
|
|
message: packet.payload,
|
|
retain: packet.retain,
|
|
qos: packet.qos
|
|
};
|
|
|
|
// update alive state
|
|
updateAlive(client, true);
|
|
|
|
if (client._will && client._will.topic && packet.topic === client._will.topic) {
|
|
client._will.payload = packet.payload;
|
|
return;
|
|
}
|
|
|
|
let val = packet.payload.toString('utf8');
|
|
adapter.log.debug('[' + client.id + '] Received: ' + packet.topic + ' = ' + val);
|
|
|
|
/*
|
|
adapter.getForeignState('system.adapter.sonoff.0.memRss', (err, state) => {
|
|
adapter.log.info('mem: ' + state.val + ' MB');
|
|
if (heapTest==0 || (heapTest==1 && state.val>37.5)) {
|
|
heapTest+=1;
|
|
heapdump.writeSnapshot('/opt/iobroker/' + Date.now() + '.heapsnapshot');
|
|
adapter.log.info('Wrote snapshot: ');
|
|
|
|
var filename='/opt/iobroker/' + Date.now() + '.heapsnapshot';
|
|
heapdump.writeSnapshot((err,filename) => {
|
|
if (err) {
|
|
adapter.log.info('heap: ' + err);
|
|
} else {
|
|
adapter.log.info('Wrote snapshot: ' + filename);
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
*/
|
|
|
|
// [DVES_BD3B4D] Received: tele/sonoff2/STATE = {
|
|
// "Time":"2017-10-01T12:37:18",
|
|
// "Uptime":0,
|
|
// "Vcc":3.224,
|
|
// "POWER":"ON",
|
|
// "POWER1":"OFF",
|
|
// "POWER2":"ON"
|
|
// "Wifi":{
|
|
// "AP":1,
|
|
// "SSId":"FuckOff",
|
|
// "RSSI":62,
|
|
// "APMac":"E0:28:6D:EC:21:EA"
|
|
// }
|
|
// }
|
|
// [DVES_BD3B4D] Received: tele/sonoff2/SENSOR = {
|
|
// "Time":"2017-10-01T12:37:18",
|
|
// "Switch1":"ON",
|
|
// "DS18B20":{"Temperature":20.6},
|
|
// "TempUnit":"C"
|
|
// }
|
|
client._map = client._map || {};
|
|
|
|
const parts = packet.topic.split('/');
|
|
|
|
if (!client._fallBackName) {
|
|
client._fallBackName = parts[1];
|
|
}
|
|
|
|
const stateId = parts.pop();
|
|
|
|
if (stateId === 'LWT') {
|
|
return;
|
|
}
|
|
|
|
if (stateId === 'RESULT') {
|
|
// ignore: stat/Sonoff/RESULT = {"POWER":"ON"}
|
|
// testserver.js reports error, so reject above cmd
|
|
const str = val.replace(/\s+/g, '');
|
|
if (str.startsWith('{"POWER":"ON"}')) return;
|
|
if (str.startsWith('{"POWER":"OFF"}')) return;
|
|
|
|
if (parts[0] === 'stat') {
|
|
try {
|
|
checkData(client, packet.topic, NO_PREFIX, JSON.parse(val));
|
|
} catch (e) {
|
|
adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e);
|
|
}
|
|
return;
|
|
}
|
|
if (parts[0] === 'tele') {
|
|
try {
|
|
checkData(client, packet.topic, NO_PREFIX, JSON.parse(val));
|
|
} catch (e) {
|
|
adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
// tele/sonoff_4ch/STATE = {"Time":"2017-10-02T19:26:06", "Uptime":0, "Vcc":3.226, "POWER1":"OFF", "POWER2":"OFF", "POWER3":"OFF", "POWER4":"OFF", "Wifi":{"AP":1, "SSId":"AAA", "RSSI": 15}}
|
|
// tele/sonoff/SENSOR = {"Time":"2017-10-05T17:43:19", "DS18x20":{"DS1":{"Type":"DS18B20", "Address":"28FF9A9876815022A", "Temperature":12.2}}, "TempUnit":"C"}
|
|
// tele/sonoff5/SENSOR = {"Time":"2017-10-03T14:02:25", "AM2301-14":{"Temperature":21.6, "Humidity":54.7}, "TempUnit":"C"}
|
|
// tele/sonoff/SENSOR = {"Time":"2018-02-23T17:36:59", "Analog0":298}
|
|
if (parts[0] === 'tele' && stateId.match(/^(STATE|SENSOR|WAKEUP)\d?$/)) {
|
|
try {
|
|
checkData(client, packet.topic, NO_PREFIX, JSON.parse(val));
|
|
//adapter.log.warn('log sensor parse"' + stateId + '": _' + val);
|
|
} catch (e) {
|
|
adapter.log.warn('Cannot parse data "' + stateId + '": _' + val + '_ - ' + e);
|
|
}
|
|
} else if (parts[0] === 'tele' && stateId.match(/^INFO\d?$/)) {
|
|
// tele/SonoffPOW/INFO1 = {"Module":"Sonoff Pow", "Version":"5.8.0", "FallbackTopic":"SonoffPOW", "GroupTopic":"sonoffs"}
|
|
// tele/SonoffPOW/INFO2 = {"WebServerMode":"Admin", "Hostname":"Sonoffpow", "IPAddress":"192.168.2.182"}
|
|
// tele/SonoffPOW/INFO3 = {"RestartReason":"Software/System restart"}
|
|
try {
|
|
checkData(client, packet.topic, 'INFO', JSON.parse(val));
|
|
} catch (e) {
|
|
adapter.log.warn('Cannot parse data"' + stateId + '": _' + val + '_ - ' + e);
|
|
}
|
|
} else if (parts[0] === 'tele' && stateId.match(/^(ENERGY)\d?$/)) {
|
|
// tele/sonoff_4ch/ENERGY = {"Time":"2017-10-02T19:24:32", "Total":1.753, "Yesterday":0.308, "Today":0.205, "Period":0, "Power":3, "Factor":0.12, "Voltage":221, "Current":0.097}
|
|
try {
|
|
checkData(client, packet.topic, 'ENERGY', JSON.parse(val));
|
|
} catch (e) {
|
|
adapter.log.warn('Cannot parse data"' + stateId + '": _' + val + '_ - ' + e);
|
|
}
|
|
} else if (types[stateId]) {
|
|
// /ESP_BOX/BM280/Pressure = 1010.09
|
|
// /ESP_BOX/BM280/Humidity = 42.39
|
|
// /ESP_BOX/BM280/Temperature = 25.86
|
|
// /ESP_BOX/BM280/Approx. Altitude = 24
|
|
|
|
// cmnd/sonoff/POWER
|
|
// stat/sonoff/POWER
|
|
|
|
if (types[stateId]) {
|
|
let id = adapter.namespace + '.' + client.iobId + '.' + stateId.replace(/[-.+\s]+/g, '_');
|
|
let obj = {
|
|
type: 'addObject',
|
|
id: id,
|
|
data: {
|
|
_id: id,
|
|
common: JSON.parse(JSON.stringify(types[stateId])),
|
|
native: {},
|
|
type: 'state'
|
|
}
|
|
};
|
|
obj.data.common.name = client.id + ' ' + stateId;
|
|
|
|
// push only new objects
|
|
updateState(obj);
|
|
|
|
if (parts[0] === 'cmnd') {
|
|
|
|
// Set Object fix
|
|
if (obj.data.common.type === 'number') {
|
|
adapter.setState(id, parseFloat(val), true);
|
|
} else if (obj.data.common.type === 'boolean') {
|
|
adapter.setState(id, val === 'ON' || val === '1' || val === 'true' || val === 'on', true);
|
|
} else {
|
|
adapter.setState(id, val, true);
|
|
}
|
|
|
|
// remember POWER topic
|
|
client._map[stateId] = packet.topic;
|
|
} else {
|
|
if (obj.data.common.type === 'number') {
|
|
adapter.setState(id, parseFloat(val), true);
|
|
} else if (obj.data.common.type === 'boolean') {
|
|
adapter.setState(id, val === 'ON' || val === '1' || val === 'true' || val === 'on', true);
|
|
} else {
|
|
adapter.setState(id, val, true);
|
|
}
|
|
}
|
|
} else {
|
|
adapter.log.debug('Cannot process: ' + packet.topic);
|
|
}
|
|
}
|
|
}
|
|
|
|
function clientClose(client, reason) {
|
|
if (!client) return;
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].connected = false;
|
|
}
|
|
|
|
if (client._resendonStart) {
|
|
clearTimeout(client._resendonStart);
|
|
client._resendonStart = null;
|
|
}
|
|
|
|
try {
|
|
if (clients[client.id] && (client.__secret === clients[client.id].__secret)) {
|
|
adapter.log.info(`Client [${client.id}] connection closed: ${reason}`);
|
|
delete clients[client.id];
|
|
updateClients();
|
|
if (client._will) {
|
|
receivedTopic(client._will, client, () => client.destroy());
|
|
} else {
|
|
client.destroy();
|
|
}
|
|
} else {
|
|
client.destroy();
|
|
}
|
|
} catch (e) {
|
|
adapter.log.warn(`Client [${client.id}] Cannot close client: ${e}`);
|
|
}
|
|
}
|
|
|
|
function checkResends() {
|
|
const now = Date.now();
|
|
resending = true;
|
|
for (const clientId in clients) {
|
|
if (clients.hasOwnProperty(clientId) && clients[clientId] && clients[clientId]._messages) {
|
|
for (let m = clients[clientId]._messages.length - 1; m >= 0; m--) {
|
|
const message = clients[clientId]._messages[m];
|
|
if (now - message.ts >= adapter.config.retransmitInterval) {
|
|
if (message.count > adapter.config.retransmitCount) {
|
|
adapter.log.warn(`Client [${clientId}] Message ${message.messageId} deleted after ${message.count} retries`);
|
|
clients[clientId]._messages.splice(m, 1);
|
|
continue;
|
|
}
|
|
|
|
// resend this message
|
|
message.count++;
|
|
message.ts = now;
|
|
try {
|
|
adapter.log.debug(`Client [${clientId}] Resend message topic: ${message.topic}, payload: ${message.payload}`);
|
|
if (message.cmd === 'publish') {
|
|
clients[clientId].publish(message);
|
|
}
|
|
} catch (e) {
|
|
adapter.log.warn(`Client [${clientId}] Cannot publish message: ${e}`);
|
|
}
|
|
|
|
if (adapter.config.sendInterval) {
|
|
setTimeout(checkResends, adapter.config.sendInterval);
|
|
} else {
|
|
setImmediate(checkResends);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// delete old sessions
|
|
if (adapter.config.storeClientsTime !== -1) {
|
|
for (const id in persistentSessions) {
|
|
if (persistentSessions.hasOwnProperty(id)) {
|
|
if (now - persistentSessions[id].lastSeen > adapter.config.storeClientsTime * 60000) {
|
|
delete persistentSessions[id];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resending = false;
|
|
}
|
|
|
|
(function _constructor(config) {
|
|
if (config.timeout === undefined) {
|
|
config.timeout = 300;
|
|
} else {
|
|
config.timeout = parseInt(config.timeout, 10);
|
|
}
|
|
|
|
server.on('connection', stream => {
|
|
let client = mqtt(stream);
|
|
// Store unique connection identifier
|
|
client.__secret = Date.now() + '_' + Math.round(Math.random() * 10000);
|
|
|
|
// client connected
|
|
client.on('connect', options => {
|
|
// acknowledge the connect packet
|
|
client.id = options.clientId;
|
|
client.iobId = client.id.replace(FORBIDDEN_CHARS, '_');
|
|
mappingClients[client.iobId] = client.id;
|
|
|
|
// get possible old client
|
|
let oldClient = clients[client.id];
|
|
|
|
if (config.user) {
|
|
if (config.user !== options.username ||
|
|
config.pass !== options.password.toString()) {
|
|
adapter.log.warn(`Client [${client.id}] has invalid password(${options.password}) or username(${options.username})`);
|
|
client.connack({returnCode: 4});
|
|
if (oldClient) {
|
|
// delete existing client
|
|
delete clients[client.id];
|
|
updateAlive(oldClient, false);
|
|
updateClients();
|
|
oldClient.destroy();
|
|
}
|
|
client.destroy();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (oldClient) {
|
|
adapter.log.info(`Client [${client.id}] reconnected. Old secret ${clients[client.id].__secret}. New secret ${client.__secret}`);
|
|
// need to destroy the old client
|
|
|
|
if (client.__secret !== clients[client.id].__secret) {
|
|
// it is another socket!!
|
|
|
|
// It was following situation:
|
|
// - old connection was active
|
|
// - new connection is on the same TCP
|
|
// Just forget him
|
|
// oldClient.destroy();
|
|
}
|
|
} else {
|
|
adapter.log.info(`Client [${client.id}] connected with secret ${client.__secret}`);
|
|
}
|
|
|
|
let sessionPresent = false;
|
|
|
|
if (!client.cleanSession && adapter.config.storeClientsTime !== 0) {
|
|
if (persistentSessions[client.id]) {
|
|
sessionPresent = true;
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
} else {
|
|
persistentSessions[client.id] = {
|
|
_subsID: {},
|
|
_subs: {},
|
|
messages: [],
|
|
lastSeen: Date.now()
|
|
};
|
|
}
|
|
client._messages = persistentSessions[client.id].messages;
|
|
persistentSessions[client.id].connected = true;
|
|
} else if (client.cleanSession && persistentSessions[client.id]) {
|
|
delete persistentSessions[client.id];
|
|
}
|
|
client._messages = client._messages || [];
|
|
|
|
client.connack({returnCode: 0, sessionPresent});
|
|
clients[client.id] = client;
|
|
updateClients();
|
|
|
|
client._will = options.will;
|
|
createClient(client);
|
|
|
|
if (persistentSessions[client.id]) {
|
|
client._subsID = persistentSessions[client.id]._subsID;
|
|
client._subs = persistentSessions[client.id]._subs;
|
|
if (persistentSessions[client.id].messages.length) {
|
|
// give to the client a little bit time
|
|
client._resendonStart = setTimeout(clientId => {
|
|
client._resendonStart = null;
|
|
resendMessages2Client(client, persistentSessions[clientId].messages);
|
|
}, 100, client.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
// timeout idle streams after 5 minutes
|
|
if (config.timeout) {
|
|
stream.setTimeout(config.timeout * 1000);
|
|
}
|
|
|
|
// connection error handling
|
|
client.on('close', had_error => clientClose(client, had_error ? 'closed because of error' : 'closed'));
|
|
client.on('error', e => clientClose(client, e));
|
|
client.on('disconnect', () => clientClose(client, 'disconnected'));
|
|
// stream timeout
|
|
stream.on('timeout', () => clientClose(client, 'timeout'));
|
|
|
|
client.on('publish', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends publish. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
if (packet.qos === 1) {
|
|
// send PUBACK to client
|
|
client.puback({
|
|
messageId: packet.messageId
|
|
});
|
|
} else if (packet.qos === 2) {
|
|
const pack = client._messages.find(e => {
|
|
return e.messageId === packet.messageId;
|
|
});
|
|
if (pack) {
|
|
// duplicate message => ignore
|
|
adapter.log.warn(`Client [${client.id}] Ignored duplicate message with ID: ${packet.messageId}`);
|
|
return;
|
|
} else {
|
|
packet.ts = Date.now();
|
|
packet.cmd = 'pubrel';
|
|
packet.count = 0;
|
|
client._messages.push(packet);
|
|
|
|
client.pubrec({messageId: packet.messageId});
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
receivedTopic(packet, client)
|
|
});
|
|
|
|
// response for QoS2
|
|
client.on('pubrec', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubrec. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
let pos = null;
|
|
// remove this message from queue
|
|
client._messages.forEach((e, i) => {
|
|
if (e.messageId === packet.messageId) {
|
|
pos = i;
|
|
return false;
|
|
}
|
|
});
|
|
if (pos !== -1) {
|
|
client.pubrel({
|
|
messageId: packet.messageId
|
|
});
|
|
} else {
|
|
adapter.log.warn(`Client [${client.id}] Received pubrec on ${client.id} for unknown messageId ${packet.messageId}`);
|
|
}
|
|
});
|
|
|
|
// response for QoS2
|
|
client.on('pubcomp', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubcomp. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
let pos = null;
|
|
// remove this message from queue
|
|
client._messages.forEach((e, i) => {
|
|
if (e.messageId === packet.messageId) {
|
|
pos = i;
|
|
return false;
|
|
}
|
|
});
|
|
if (pos !== null) {
|
|
client._messages.splice(pos, 1);
|
|
} else {
|
|
adapter.log.warn(`Client [${client.id}] Received pubcomp for unknown message ID: ${packet.messageId}`);
|
|
}
|
|
});
|
|
|
|
// response for QoS2
|
|
client.on('pubrel', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pubrel. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
let pos = null;
|
|
// remove this message from queue
|
|
client._messages.forEach((e, i) => {
|
|
if (e.messageId === packet.messageId) {
|
|
pos = i;
|
|
return false;
|
|
}
|
|
});
|
|
if (pos !== -1) {
|
|
client.pubcomp({
|
|
messageId: packet.messageId
|
|
});
|
|
receivedTopic(client._messages[pos], client);
|
|
} else {
|
|
adapter.log.warn(`Client [${client.id}] Received pubrel on ${client.id} for unknown messageId ${packet.messageId}`);
|
|
}
|
|
});
|
|
|
|
// response for QoS1
|
|
client.on('puback', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends puback. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
// remove this message from queue
|
|
let pos = null;
|
|
// remove this message from queue
|
|
client._messages.forEach((e, i) => {
|
|
if (e.messageId === packet.messageId) {
|
|
pos = i;
|
|
return false;
|
|
}
|
|
});
|
|
if (pos !== null) {
|
|
adapter.log.debug(`Client [${client.id}] Received puback for ${client.id} message ID: ${packet.messageId}`);
|
|
client._messages.splice(pos, 1);
|
|
} else {
|
|
adapter.log.warn(`Client [${client.id}] Received puback for unknown message ID: ${packet.messageId}`);
|
|
}
|
|
});
|
|
|
|
client.on('unsubscribe', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends unsubscribe. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
client.unsuback({messageId: packet.messageId});
|
|
});
|
|
|
|
client.on('subscribe', packet => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends unsubscribe. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
let granted = [];
|
|
// just confirm the request.
|
|
// we expect subscribe for 'cmnd.sonoff.#'
|
|
for (let i = 0; i < packet.subscriptions.length; i++) {
|
|
granted.push(packet.subscriptions[i].qos);
|
|
}
|
|
|
|
client.suback({granted: granted, messageId: packet.messageId});
|
|
});
|
|
|
|
client.on('pingreq', (/*packet*/) => {
|
|
if (clients[client.id] && client.__secret !== clients[client.id].__secret) {
|
|
return adapter.log.warn(`Old client ${client.id} with secret ${client.__secret} sends pingreq. Ignore! Actual secret is ${clients[client.id].__secret}`);
|
|
}
|
|
|
|
if (persistentSessions[client.id]) {
|
|
persistentSessions[client.id].lastSeen = Date.now();
|
|
}
|
|
|
|
adapter.log.debug(`Client [${client.id}] pingreq`);
|
|
client.pingresp();
|
|
});
|
|
});
|
|
|
|
config.port = parseInt(config.port, 10) || 1883;
|
|
|
|
config.retransmitInterval = config.retransmitInterval || 2000;
|
|
config.retransmitCount = config.retransmitCount || 10;
|
|
if (config.storeClientsTime === undefined) {
|
|
config.storeClientsTime = 1440;
|
|
} else {
|
|
config.storeClientsTime = parseInt(config.storeClientsTime, 10) || 0;
|
|
}
|
|
|
|
config.defaultQoS = parseInt(config.defaultQoS, 10) || 0;
|
|
|
|
// Update connection state
|
|
updateClients();
|
|
|
|
// to start
|
|
server.listen(config.port, config.bind, () => {
|
|
adapter.log.info('Starting MQTT ' + (config.user ? 'authenticated ' : '') + ' server on port ' + config.port);
|
|
|
|
resendTimer = setInterval(() => {
|
|
if (!resending) {
|
|
checkResends();
|
|
}
|
|
}, adapter.config.retransmitInterval || 2000);
|
|
});
|
|
})(adapter.config);
|
|
|
|
return this;
|
|
}
|
|
|
|
module.exports = MQTTServer;
|