Inital commit of Orvibo socket server code

This commit is contained in:
Sandy Milne 2017-10-15 13:43:38 +11:00
parent 196493e7e2
commit 4ad59b2a46
10 changed files with 628 additions and 6 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea/
node_modules

64
Example.js Normal file
View File

@ -0,0 +1,64 @@
const Orvibo = require('./Orvibo');
const http = require('http');
const url = require('url');
const httpPort = 3000;
let orvbio = new Orvibo();
// When a socket first connects and initiates the handshake it will emit the connected event with the uid of the socket;
orvbio.on('plugConnected', ({uid, name}) => {
console.log(`Connected ${uid} name = ${name}`);
});
// If the socket state is updated this event will fire
orvbio.on('plugStateUpdated', ({uid, state , name}) => {
console.log(`Plug ${name} ${uid} updated state ${state}`);
});
// The plug sends a hearbeat to let the server know it's still alive
orvbio.on('gotHeartbeat', ({uid, state , name}) => {
console.log(`Plug ${name} ${uid} sent heartbeat`);
});
// Called when the plug disconnects
orvbio.on('plugDisconnected', ({uid, name }) => {
console.log(`Plug ${uid} - ${name} disconnected`);
});
// Called when the plug disconnects with an error ie it's been unplugged
orvbio.on('plugDisconnectedWithError', ({uid, name }) => {
console.log(`Plug ${uid} - ${name} disconnected with error`);
});
// Start the Orvibo socket server
orvbio.startServer();
// Create a basic example HTTP server
// If there are no parameters it will return the uid, state, modelId and name of the socket
// You can then use the uid to toggle the state of the switch like
// http://localhost:3000?uid=5dcf7ff76e7a
const requestHandler = (request, response) => {
response.writeHead(200, {'Content-Type': 'application/json'});
let q = url.parse(request.url, true).query;
if (q.uid != null) {
orvbio.toggleSocket(q.uid);
}
// Get all currently connected sockets, their names and states
let sockets = orvbio.getConnectedSocket();
response.end(JSON.stringify(sockets));
};
const httpServer = http.createServer(requestHandler)
httpServer.listen(httpPort, (err) => {
if (err) {
return console.log('something bad happened', err)
}
console.log(`http server is listening on ${httpPort}`)
});

226
Orvibo.js Normal file
View File

@ -0,0 +1,226 @@
const net = require('net');
const util = require('util');
const Packet = require('./OrviboPacket');
const PacketBuilder = require('./PacketBuilder');
const Utils = require('./Utils');
const Settings = require('./OrviboSettings');
const EventEmitter = require('events').EventEmitter;
const ORVIBO_KEY = Settings.ORVIBO_KEY;
const LOG_PACKET = Settings.LOG_PACKET;
if (ORVIBO_KEY === '') {
console.log('Please add Orvibo PK key to OrviboSettings.js file. See Readme');
process.exit(1);
}
const Orvibo = function() {};
Object.assign(Orvibo.prototype, EventEmitter.prototype);
let HEARTBEAT = 32;
let HELLO = 0;
let HANDSHAKE = 6;
let STATE_UPDATE = 42;
let STATE_UPDATE_CONFIRM = 15;
let UNKNOWN_CMD = 'UNKNOWN_CMD';
let port = 10001;
let bindHost = '0.0.0.0';
console.log(`Starting server Orvibo socket server on port ${port}`);
let plugConnections = [];
let packetData = {};
// Put your smart socket data here and it will be matched to the uid
let plugInfo = Settings.plugInfo;
let getNameForUid = (uid) => {
let item = plugInfo.find(item => item.uid === uid);
return item != null ? item.name : 'unknown';
};
let getData = (id) => {
return packetData[id];
};
let setData = (id, data) => {
packetData[id] = data;
};
let respondAndSetData = (data, socket, packetFunction) => {
setData(socket.id, data);
socket.write(packetFunction(data));
};
let helloHandler = (plugPacket, socket) => {
let pkData = {
serial: plugPacket.getSerial(),
encryptionKey: Utils.generateRandomTextValue(16),
id: Utils.generateRandomHexValue(32),
modelId: plugPacket.getModelId(),
};
respondAndSetData(pkData, socket, PacketBuilder.helloPacket);
};
let handshakeHandler = function(plugPacket, socket, socketData) {
let uid = plugPacket.getUid();
let pkData = Object.assign({}, socketData, {
serial: plugPacket.getSerial(),
uid,
name: getNameForUid(uid)
});
respondAndSetData(pkData, socket, PacketBuilder.handshakePacket);
this.emit('plugConnected', {uid:pkData.uid, name: pkData.name});
};
let heartbeatHandler = function(plugPacket, socket, socketData) {
let pkData = Object.assign({}, socketData, {
serial: plugPacket.getSerial(),
uid: plugPacket.getUid()
});
respondAndSetData(pkData, socket, PacketBuilder.heartbeatPacket);
this.emit('gotHeartbeat', {uid:pkData.uid, name: pkData.name});
};
let stateUpdateHandler = function(plugPacket, socket, socketData) {
let pkData = Object.assign({}, socketData, {
serial: plugPacket.getSerial(),
uid: plugPacket.getUid(),
state: plugPacket.getValue1()
});
respondAndSetData(pkData, socket, PacketBuilder.comfirmStatePacket);
this.emit('plugStateUpdated', {uid:pkData.uid, state: pkData.state, name: pkData.name});
};
let stateConfirmHandler = function() {
// Do nothing at this stage
};
let unknownCmdHandler = function(plugPacket, socket, socketData) {
let pkData = Object.assign({}, socketData, {
serial: plugPacket.getSerial(),
uid: plugPacket.getUid(),
});
respondAndSetData(pkData, socket, PacketBuilder.defaultPacket);
};
Orvibo.prototype.handlers = function() {
return {
[HELLO]: helloHandler.bind(this),
[HANDSHAKE]: handshakeHandler.bind(this),
[HEARTBEAT]: heartbeatHandler.bind(this),
[STATE_UPDATE]: stateUpdateHandler.bind(this),
[STATE_UPDATE_CONFIRM] : stateConfirmHandler,
[UNKNOWN_CMD]: unknownCmdHandler.bind(this)
};
};
Orvibo.prototype.startServer = function() {
let self = this;
let handlers = this.handlers();
this.server = net.createServer(function(socket) {
socket.id = Utils.generateRandomTextValue(16);
socket.setKeepAlive(true, 10000);
plugConnections.push(socket);
socket.on('data', function (data) {
let socketData = getData(socket.id);
let plugPacket = new Packet(data);
if (!plugPacket.validCRC()) {
console.log('Got invalid CRC');
return;
}
if (plugPacket.packetTypeText() === 'pk') {
plugPacket.processPacket(ORVIBO_KEY);
} else {
plugPacket.processPacket(socketData.encryptionKey);
}
LOG_PACKET && plugPacket.logPacket("Socket -> ");
let handler = handlers[plugPacket.getCommand()];
if (handler != null) {
handler(plugPacket, socket, socketData);
} else {
handlers[UNKNOWN_CMD](plugPacket, socket, socketData);
}
});
socket.on('end', function () {
let pkData = getData(socket.id);
self.emit('plugDisconnected', {uid: pkData.uid, name: pkData.name});
delete packetData[socket.id];
plugConnections.splice(plugConnections.indexOf(socket), 1);
});
socket.on('error', (err) => {
console.log(err);
console.log('error with socket ' + socket.id);
self.emit('plugDisconnectedWithError', getData(socket.id));
delete packetData[socket.id];
plugConnections.splice(plugConnections.indexOf(socket), 1);
});
});
this.server.listen(port, bindHost);
};
Orvibo.prototype.toggleSocket = function(uid) {
let socketId = null;
for (const key of Object.keys(packetData)) {
if (packetData[key].uid === uid) {
socketId = key;
break
}
}
if (socketId === null) {
console.log('Could not find socket ' + uid);
return;
}
let socket = plugConnections.find(s => s.id === socketId);
if (socket != null) {
let socketData = getData(socketId);
let data = Object.assign({}, socketData,{
state: socketData.state === 1 ? 0 : 1,
serial: Utils.generateRandomNumber(8),
clientSessionId: socketData.clientSessionId ? socketData.clientSessionId : Utils.generateRandomHexValue(32),
deviceId: socketData.deviceId ? socketData.deviceId : Utils.generateRandomHexValue(32),
});
setData(socket.id, data);
let packet = PacketBuilder.updatePacket(data);
socket.write(packet);
} else {
console.log('Can not find socket ');
}
};
Orvibo.prototype.getConnectedSocket = function() {
let sockets = [];
for (const key of Object.keys(packetData)) {
let socketData = getData(key);
sockets.push({
name: socketData.name,
state: socketData.state,
uid: socketData.uid,
modelId: socketData.modelId
});
}
return sockets;
};
module.exports = Orvibo;

71
OrviboPacket.js Normal file
View File

@ -0,0 +1,71 @@
const crc32 = require('buffer-crc32');
const crypto = require('crypto');
let pkt = function(packetBuffer) {
this.magic = packetBuffer.slice(0,2);
this.packetLength = packetBuffer.slice(2,4);
this.packetType = packetBuffer.slice(4,6);
this.crc32 = packetBuffer.slice(6,10);
this.packetId = packetBuffer.slice(10,42);
this.payload = packetBuffer.slice(42, packetBuffer.length);
};
pkt.prototype.logPacket = function(type) {
let output = {
PacketId : this.packetIdText(),
Payload : this.payloadJSON
};
console.log(type, JSON.stringify(this.payloadJSON));
};
pkt.prototype.getCommand = function() {
return this.payloadJSON.cmd;
};
pkt.prototype.getSerial = function() {
return this.payloadJSON.serial;
};
pkt.prototype.getUid = function() {
return this.payloadJSON.uid;
};
pkt.prototype.getValue1 = function() {
return this.payloadJSON.value1;
};
pkt.prototype.getModelId = function() {
return this.payloadJSON.modelId;
};
pkt.prototype.processPacket = function(key) {
this.payloadJSON = this.decodeJSON(key);
};
pkt.prototype.getKeyValue = function() {
return this.payloadJSON.key;
};
pkt.prototype.validCRC = function() {
return crc32(this.payload).toString('hex') === this.crc32.toString('hex');
};
pkt.prototype.packetTypeText = function() {
return this.packetType.toString('ascii');
};
pkt.prototype.packetIdText = function() {
return this.packetId.toString('ascii');
};
pkt.prototype.decodeJSON = function(key) {
let decipher = crypto.createDecipheriv('aes-128-ecb', key, '');
decipher.setAutoPadding(true);
let decrypted = decipher.update(this.payload.toString('hex'), 'hex', 'utf8');
decrypted += decipher.final('utf8');
// Sometimes there are bad chars on the end of the JSON so check here
decrypted = decrypted.substring(0, decrypted.indexOf('}') + 1);
return JSON.parse(decrypted);
};
module.exports = pkt;

13
OrviboSettings.js Normal file
View File

@ -0,0 +1,13 @@
// Settings/Config store for orvibo socket server
module.exports = {
LOG_PACKET: false,
ORVIBO_KEY: '', // put your PK key here as plain text
plugInfo : [
// Add uid and a name so you can easily identify the connected sockets
{
uid :'53dd7fe74de7',
name: "Lamp in Kitchen"
},
],
}

182
PacketBuilder.js Normal file
View File

@ -0,0 +1,182 @@
const crc32 = require('buffer-crc32');
const crypto = require('crypto');
const Settings = require('./orviboSettings');
const ORVIBO_KEY = Settings.ORVIBO_KEY;
let Packet = {
encryptionKey : ORVIBO_KEY,
magic: new Buffer("6864", 'hex'),
build: function () {
let packetId = new Buffer(this.id, 'ascii');
let payload = encodePayload(JSON.stringify(this.json), this.encryptionKey);
let crc = crc32(payload);
let length = getLength([this.magic, this.packetType, packetId, crc, payload], 2); // Extra 2 for the length field itself
return Buffer.concat([this.magic, length, this.packetType, crc, packetId, payload]);
}
};
let PKPacket = Object.assign({}, Packet, {
packetType: new Buffer('pk', 'ascii'),
});
let DKPacket = Object.assign({}, Packet, {
packetType: new Buffer('dk', 'ascii'),
});
let helloPacket = function({ serial, encryptionKey, id }) {
let json = {
cmd: 0,
status: 0,
serial: serial,
key: encryptionKey
};
let pkt = Object.assign(Object.create(PKPacket), {
json: json,
id: id
});
return pkt.build();
};
let handshakePacket = function({ serial, encryptionKey, id }) {
let json = {
cmd: 6,
status:0,
serial: serial
};
let pkt = Object.assign(Object.create(DKPacket), {
json: json,
id: id,
encryptionKey, encryptionKey
});
return pkt.build();
};
let heartbeatPacket = function({serial, uid, encryptionKey, id}) {
let json = {
cmd: 32,
status:0,
serial: serial,
uid: uid,
utc: new Date().getTime()
};
let pkt = Object.assign(Object.create(DKPacket), {
json: json,
id: id,
encryptionKey, encryptionKey
});
return pkt.build();
};
let comfirmStatePacket = function({serial, uid, state, encryptionKey, id}) {
var json = {
uid: uid,
cmd: 42,
statusType: 0,
value3: 0,
alarmType: 1,
serial: serial,
value4: 0,
deviceId: 0,
value1: state,
value2: 0,
updateTimeSec: new Date().getTime(),
status: 0
};
let pkt = Object.assign(Object.create(DKPacket), {
json: json,
id: id,
encryptionKey, encryptionKey
});
return pkt.build();
};
let defaultPacket = function({serial, uid, cmd, id, encryptionKey}) {
let json = {
uid: uid,
cmd: cmd,
serial: serial,
status: 0
};
let pkt = Object.assign(Object.create(DKPacket), {
json: json,
id: id,
encryptionKey, encryptionKey
});
return pkt.build();
};
let updatePacket = function({ uid, state, serial, id, clientSessionId , deviceId, encryptionKey}) {
let json = {
uid: uid,
delayTime: 0,
cmd: 15,
order: state === 0 ? "on" : "off",
userName: "iloveorvibo@orvibo.com",
ver: "3.0.0",
value3: 0,
serial: serial,
value4: 0,
deviceId: deviceId,
value1: state,
value2: 0,
clientSessionId: clientSessionId
};
let pkt = Object.assign(Object.create(DKPacket), {
json: json,
id: id,
encryptionKey, encryptionKey
});
return pkt.build();
};
let encodePayload = function(json, key) {
let cipher = crypto.createCipheriv('aes-128-ecb', key, '');
cipher.setAutoPadding(true);
let crypted = cipher.update(json,'utf8','hex');
crypted += cipher.final('hex');
return new Buffer(crypted, 'hex');
};
let getLength = function(items, extra) {
let length = extra || 0;
for (let i = 0; i < items.length; i++) {
length += items[i].length;
}
return getHexLengthPadded(length);
};
let getHexLengthPadded = function(lengthDecimal) {
let lengthHex = lengthDecimal.toString(16);
let paddingLength = 4 - lengthHex.length;
let padding = '';
for (let i = 0; i < paddingLength; i++) {
padding +=0;
}
return new Buffer(padding + lengthHex, 'hex');
};
module.exports.helloPacket = helloPacket;
module.exports.handshakePacket = handshakePacket;
module.exports.heartbeatPacket = heartbeatPacket;
module.exports.comfirmStatePacket = comfirmStatePacket;
module.exports.defaultPacket = defaultPacket;
module.exports.updatePacket = updatePacket;

View File

@ -16,10 +16,7 @@ You will need to add the Orvibo PK key to decrypt and encrypt the initial packet
Once you have this key you will need to add it the ``OrviboSettings.js`` file.
Because these new sockets don't use UDP packets to communicate like the older versions you will also need to redirect all traffic from the host name
```
homemate.orvibo.com
```
Because these new sockets don't use UDP packets to communicate like the older versions you will also need to redirect all traffic from the host name ``homemate.orvibo.com``
on TCP port 10001 the computer running the server.
I used an Ubuntu machine running dnsmasq and set this server as my DNS server in my router but depending on your network you might have to do it differntly.
@ -37,7 +34,6 @@ to the official Orvibo server.
Clone the repo and then run
```
npm install
```
to install the dependencies (buffer-crc32)
@ -50,7 +46,6 @@ To start the example http server run
```
npm start
```
This will start the Orvibo socket server create a basic example HTTP server used for interacting with sockets.
Calling this http server with no parameters will return the uid, state, modelId and name of the socket.

27
Utils.js Normal file
View File

@ -0,0 +1,27 @@
module.exports.generateRandomTextValue = function(length) {
let chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = length; i > 0; --i) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
};
module.exports.generateRandomHexValue = function(length) {
let chars = '0123456789abcdef';
let result = '';
for (let i = length; i > 0; --i) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
};
module.exports.generateRandomNumber = function(length) {
let numbers = '0123456789';
let result = '';
for (let i = length; i > 0; --i) {
result += numbers[Math.floor(Math.random() * numbers.length)];
}
return parseInt(result);
};

13
package-lock.json generated Normal file
View File

@ -0,0 +1,13 @@
{
"name": "orvibo-b25-server",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
}
}
}

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "orvibo-b25-server",
"version": "1.0.0",
"description": "A server to control the Orvibo B25 range of smart sockets",
"main": "Orvibo.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node Example.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/sandysound/orvibo-b25-server.git"
},
"keywords": [
"Orvibo",
"B25",
"Smart",
"Socket"
],
"author": "Sandy Milne",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/sandysound/orvibo-b25-server/issues"
},
"homepage": "https://github.com/sandysound/orvibo-b25-server#readme",
"dependencies": {
"buffer-crc32": "^0.2.13"
}
}