From e3a66b16f07c4ee33c6c548719fd38c96fec14ce Mon Sep 17 00:00:00 2001 From: Gran Berro Date: Sun, 25 Mar 2018 21:21:42 +0200 Subject: [PATCH 1/3] Add support for T1-UK --- sonoff.server.module.js | 167 +++++++++++++++++++++++++++++----------- 1 file changed, 122 insertions(+), 45 deletions(-) diff --git a/sonoff.server.module.js b/sonoff.server.module.js index 92bb184..bce6a64 100644 --- a/sonoff.server.module.js +++ b/sonoff.server.module.js @@ -27,6 +27,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; @@ -71,7 +75,7 @@ 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); @@ -166,53 +170,93 @@ 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.conn = conn; + 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.conn = conn; + 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); + 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.conn = conn; + device.rawMessageRegister = data; + device.rawMessageRegister.timestamp = Date.now(); + state.updateKnownDevice(device); + log.log('INFO | WS | Device %s registered', device.id); + } + //Keep alive only once + addConnectionIsAliveCheck(device); + } else { + 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); + } break; default: log.error('TODO | Unknown action "%s"', data.action); break; } @@ -220,7 +264,28 @@ module.exports.createServer = function (config) { 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); @@ -247,7 +312,7 @@ module.exports.createServer = function (config) { conn.sendText(r); }); conn.on("close", function (code, reason) { - log.log("Connection closed: %s (%d)", reason, code); + log.log("Connection closed: '%s' (%d)", reason, code); state.knownDevices.forEach((device, index) => { if (device.conn != conn) return; @@ -266,7 +331,7 @@ 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.conn !== 'undefined'), isAlive: x.isAlive, rawMessageRegister: x.rawMessageRegister, rawMessageLastUpdate: x.rawMessageLastUpdate } }); }, @@ -279,14 +344,26 @@ module.exports.createServer = function (config) { 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 (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 (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"; }, From f864bbc8d7622d9f2244283ea89833077b8bb376 Mon Sep 17 00:00:00 2001 From: Gran Berro Date: Mon, 26 Mar 2018 19:45:10 +0200 Subject: [PATCH 2/3] Fixes onclose error --- sonoff.server.module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonoff.server.module.js b/sonoff.server.module.js index bce6a64..edf4b24 100644 --- a/sonoff.server.module.js +++ b/sonoff.server.module.js @@ -312,7 +312,7 @@ module.exports.createServer = function (config) { conn.sendText(r); }); conn.on("close", function (code, reason) { - log.log("Connection closed: '%s' (%d)", reason, code); + log.log("Connection closed: (%s) (%d)", reason, code); state.knownDevices.forEach((device, index) => { if (device.conn != conn) return; From c18c62e262fd3d9c020e99b1cdad503b2d31d847 Mon Sep 17 00:00:00 2001 From: Gran Berro Date: Tue, 27 Mar 2018 21:16:24 +0200 Subject: [PATCH 3/3] Fix connection closing issues Create connections map --- sonoff.server.module.js | 98 ++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/sonoff.server.module.js b/sonoff.server.module.js index edf4b24..c14448e 100644 --- a/sonoff.server.module.js +++ b/sonoff.server.module.js @@ -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"); @@ -15,6 +15,7 @@ module.exports.createServer = function (config) { //set initialized parameters var state = { knownDevices: [], + connections: [], listeners: { onDeviceConnectedListeners: [], onDeviceDisconnectedListeners: [], @@ -78,24 +79,27 @@ module.exports.createServer = function (config) { 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; }); } @@ -147,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) { @@ -196,7 +200,6 @@ module.exports.createServer = function (config) { log.error('ERR | WS | Unknown device ', data.deviceid); } else { device.state = data.params.switch; - device.conn = conn; device.rawMessageLastUpdate = data; device.rawMessageLastUpdate.timestamp = Date.now(); state.updateKnownDevice(device); @@ -210,7 +213,6 @@ module.exports.createServer = function (config) { for (i = 0; i < data.params.switches.length; i++) { var device = state.getDeviceById(data.deviceid + '-' + i); device.state = data.params.switches[i].switch; - device.conn = conn; device.rawMessageLastUpdate = data; device.rawMessageLastUpdate.timestamp = Date.now(); state.updateKnownDevice(device); @@ -219,6 +221,11 @@ module.exports.createServer = function (config) { } break; case 'register': + 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++) { @@ -229,31 +236,27 @@ module.exports.createServer = function (config) { }; device.version = data.romVersion; device.model = data.model; - device.conn = conn; + 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); } - //Keep alive only once - addConnectionIsAliveCheck(device); + //All devices share connection + addConnection(connection); } else { 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.connection = connection; device.rawMessageRegister = data; device.rawMessageRegister.timestamp = Date.now(); - addConnectionIsAliveCheck(device); + connection.devices.push(device); + addConnection(connection); state.updateKnownDevice(device); log.log('INFO | WS | Device %s registered', device.id); } @@ -311,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); @@ -331,19 +334,20 @@ 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, parentId: x.parentId, outlet: x.outlet, 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"; + if (!d || (typeof d.connection == 'undefined')) return "disconnected"; if (typeof d.outlet == 'undefined') { state.pushMessage({ action: 'update', value: { switch: "on" }, target: deviceId, device: deviceId }); @@ -356,7 +360,7 @@ module.exports.createServer = function (config) { turnOffDevice: (deviceId) => { var d = state.getDeviceById(deviceId); - if (!d || (typeof d.conn == 'undefined')) return "disconnected"; + if (!d || (typeof d.connection == 'undefined')) return "disconnected"; if (typeof d.outlet == 'undefined') { state.pushMessage({ action: 'update', value: { switch: "off" }, target: deviceId, device: deviceId }); @@ -381,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"); } }