Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for composite devices #23

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 164 additions & 71 deletions sonoff.server.module.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports.createServer = function (config) {
const CONNECTION_IS_ALIVE_CHECK_INTERVAL = 30000;

const fs = require('fs');
const path = require('path');
const ws = require("nodejs-websocket");
Expand All @@ -15,6 +15,7 @@ module.exports.createServer = function (config) {
//set initialized parameters
var state = {
knownDevices: [],
connections: [],
listeners: {
onDeviceConnectedListeners: [],
onDeviceDisconnectedListeners: [],
Expand All @@ -27,6 +28,10 @@ module.exports.createServer = function (config) {
return state.knownDevices.find(d => d.id == deviceId);
};

state.getDeviceByParentId = (deviceId) => {
return state.knownDevices.find(d => d.parentId == deviceId);
};

state.updateKnownDevice = (device) => {
var updated = false;

Expand Down Expand Up @@ -71,27 +76,30 @@ module.exports.createServer = function (config) {
};
var r = JSON.stringify(rq);
log.trace('REQ | WS | APP | ' + r);
var device = state.getDeviceById(a.target);
var device = state.getDeviceById(a.device);
if (!device.messages) device.messages = [];
device.messages.push(rq);
device.conn.sendText(r);
device.connection.conn.sendText(r);
};

function addConnectionIsAliveCheck(device) {
device.isAlive = true;

device.isAliveIntervalId = setInterval(() => {
if (device.conn.readyState == device.conn.CONNECTING) return;
if (!device.isAlive) {
clearInterval(device.isAliveIntervalId);
return device.conn.close(408, "connection timed out");
function addConnection(connection) {
var conn = connection.conn;
var connId = conn.socket.remoteAddress + ':' + conn.socket.remotePort;
connection.isAlive = true;
state.connections[connId] = connection;

connection.isAliveIntervalId = setInterval(() => {
if (connection.conn.readyState == connection.conn.CONNECTING) return;
if (!connection.isAlive) {
clearInterval(connection.isAliveIntervalId);
return connection.conn.close(408, "connection timed out");
}
device.isAlive = false;
device.conn.sendPing();
connection.isAlive = false;
conn.sendPing();
}, CONNECTION_IS_ALIVE_CHECK_INTERVAL);

device.conn.on("pong", () => {
device.isAlive = true;
connection.conn.on("pong", () => {
connection.isAlive = true;
});
}

Expand Down Expand Up @@ -143,7 +151,7 @@ module.exports.createServer = function (config) {
var wsOptions = {
secure: true,
key: config.server.privateKey,
cert: config.server.certificate
cert: config.server.certificate,
};

const wsServer = ws.createServer(wsOptions, function (conn) {
Expand All @@ -166,61 +174,121 @@ module.exports.createServer = function (config) {
//device wants information
var device = state.getDeviceById(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
} else {
/*if(data.params.includes('timers')){
log.log('INFO | WS | Device %s asks for timers',device.id);
if(device.timers){
res.params = [{timers : device.timers}];
}
}*/
res.params = {};
data.params.forEach(p => {
res.params[p] = device[p];
});
device = state.getDeviceByParentId(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
break;
}
}
/*if(data.params.includes('timers')){
log.log('INFO | WS | Device %s asks for timers',device.id);
if(device.timers){
res.params = [{timers : device.timers}];
}
}*/
res.params = {};
data.params.forEach(p => {
res.params[p] = device[p];
});
break;
case 'update':
//device wants to update its state
var device = state.getDeviceById(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
if (typeof data.params.switches == 'undefined') {
// Single switch
var device = state.getDeviceById(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
} else {
device.state = data.params.switch;
device.rawMessageLastUpdate = data;
device.rawMessageLastUpdate.timestamp = Date.now();
state.updateKnownDevice(device);
}
} else {
device.state = data.params.switch;
device.conn = conn;
device.rawMessageLastUpdate = data;
device.rawMessageLastUpdate.timestamp = Date.now();
state.updateKnownDevice(device);
// Multiple switches, look for parent
var device = state.getDeviceByParentId(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
} else {
for (i = 0; i < data.params.switches.length; i++) {
var device = state.getDeviceById(data.deviceid + '-' + i);
device.state = data.params.switches[i].switch;
device.rawMessageLastUpdate = data;
device.rawMessageLastUpdate.timestamp = Date.now();
state.updateKnownDevice(device);
}
}
}

break;
case 'register':
var device = {
id: data.deviceid
};

//this is not valid anymore?! type is not based on the first two chars
var type = data.deviceid.substr(0, 2);
if (type == '01') device.kind = 'switch';
else if (type == '02') device.kind = 'light';
else if (type == '03') device.kind = 'sensor'; //temperature and humidity. No timers here;

device.version = data.romVersion;
device.model = data.model;
device.conn = conn;
device.rawMessageRegister = data;
device.rawMessageRegister.timestamp = Date.now();
addConnectionIsAliveCheck(device);
state.updateKnownDevice(device);
log.log('INFO | WS | Device %s registered', device.id);
var connection = {
conn: conn,
devices: []
}

if (data.model == 'PSF-B04-GL') {
//register for devices appending the outlet to the deviceId
for (i = 0; i < 4; i++) {
var device = {
id: data.deviceid + '-' + i,
parentId: data.deviceid,
outlet: i
};
device.version = data.romVersion;
device.model = data.model;
device.connection = connection;
device.rawMessageRegister = data;
device.rawMessageRegister.timestamp = Date.now();
connection.devices.push(device);
state.updateKnownDevice(device);
log.log('INFO | WS | Device %s registered', device.id);
}
//All devices share connection
addConnection(connection);
} else {
var device = {
id: data.deviceid
};

device.version = data.romVersion;
device.model = data.model;
device.connection = connection;
device.rawMessageRegister = data;
device.rawMessageRegister.timestamp = Date.now();
connection.devices.push(device);
addConnection(connection);
state.updateKnownDevice(device);
log.log('INFO | WS | Device %s registered', device.id);
}
break;
default: log.error('TODO | Unknown action "%s"', data.action); break;
}
} else {
if (data.sequence && data.deviceid) {
var device = state.getDeviceById(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
// Look for parent
device = state.getDeviceByParentId(data.deviceid);
if (!device) {
log.error('ERR | WS | Unknown device ', data.deviceid);
} else {
// Look for message
for (i = 0; i < 4; i++) {
device = state.getDeviceById(data.deviceid + '-' + i);
if (device.messages) {
var message = device.messages.find(item => item.sequence == data.sequence);
if (message) {
device.messages = device.messages.filter(function (item) {
return item !== message;
})
device.state = message.params.switches[0].switch;
state.updateKnownDevice(device);
log.trace('INFO | WS | APP | action has been accnowlaged by the device ' + JSON.stringify(data));
break;;
}
}
}
}
} else {
if (device.messages) {
var message = device.messages.find(item => item.sequence == data.sequence);
Expand All @@ -246,16 +314,16 @@ module.exports.createServer = function (config) {
log.trace('RES | WS | DEV | ' + r);
conn.sendText(r);
});
conn.on("close", function (code, reason) {
log.log("Connection closed: %s (%d)", reason, code);
state.knownDevices.forEach((device, index) => {
if (device.conn != conn)
return;
conn.on("close", function (code, reason) {
var connId = conn.socket.remoteAddress + ':' + conn.socket.remotePort;
state.connections[connId].devices.forEach((device, index) => {
log.log("Device %s disconnected", device.id);
clearInterval(device.isAliveIntervalId);
callDeviceListeners(state.listeners.onDeviceDisconnectedListeners, device);
device.conn = undefined;
device.connnection = undefined;
});

clearInterval(state.connections[connId].isAliveIntervalId);
delete state.connections[connId];
});
conn.on("error", function (error) {
log.error("Connection error: ", error);
Expand All @@ -266,27 +334,40 @@ module.exports.createServer = function (config) {
//currently all known devices are returned with a hint if they are currently connected
getConnectedDevices: () => {
return state.knownDevices.map(x => {
return { id: x.id, state: x.state, model: x.model, kind: x.kind, version: x.version, isConnected: (typeof x.conn !== 'undefined'), isAlive: x.isAlive, rawMessageRegister: x.rawMessageRegister, rawMessageLastUpdate: x.rawMessageLastUpdate }
return { id: x.id, state: x.state, parentId: x.parentId, outlet: x.outlet, model: x.model, kind: x.kind, version: x.version, isConnected: (typeof x.connection !== 'undefined'), isAlive: x.connection.isAlive, rawMessageRegister: x.rawMessageRegister, rawMessageLastUpdate: x.rawMessageLastUpdate }
});
},

getDeviceState: (deviceId) => {
var d = state.getDeviceById(deviceId);
if (!d || (typeof d.conn == 'undefined')) return "disconnected";

if (!d || (typeof d.connection == 'undefined')) return "disconnected";
return d.state;
},

turnOnDevice: (deviceId) => {
var d = state.getDeviceById(deviceId);
if (!d || (typeof d.conn == 'undefined')) return "disconnected";
state.pushMessage({ action: 'update', value: { switch: "on" }, target: deviceId });
if (!d || (typeof d.connection == 'undefined')) return "disconnected";

if (typeof d.outlet == 'undefined') {
state.pushMessage({ action: 'update', value: { switch: "on" }, target: deviceId, device: deviceId });
} else {
state.pushMessage({ action: 'update', value: { switches: [{ switch: "on", outlet: Number(d.outlet) }]}, target: d.parentId, device: deviceId });
}

return "on";
},

turnOffDevice: (deviceId) => {
var d = state.getDeviceById(deviceId);
if (!d || (typeof d.conn == 'undefined')) return "disconnected";
state.pushMessage({ action: 'update', value: { switch: "off" }, target: deviceId });
if (!d || (typeof d.connection == 'undefined')) return "disconnected";

if (typeof d.outlet == 'undefined') {
state.pushMessage({ action: 'update', value: { switch: "off" }, target: deviceId, device: deviceId });
} else {
state.pushMessage({ action: 'update', value: { switches: [{ switch: "off", outlet: Number(d.outlet) }]}, target: d.parentId, device: deviceId });
}

return "off";
},

Expand All @@ -304,9 +385,21 @@ module.exports.createServer = function (config) {

close: () => {
log.log("Stopping server");
state.knownDevices.forEach(device => device.conn.close());
for(key in state.connections) {
var connection = state.connections[key];
connection.conn.socket.setTimeout(100, function() {
if(connection) {
connection.conn.socket.destroy();
}
});

connection.conn.close();
}

httpsServer.close();
wsServer.close();
wsServer.close(function () {
log.log('WS Server stopped');
});
log.log("Stopped server");
}
}
Expand Down