node-lightify-soef
Version:
lightify node module
481 lines (445 loc) • 16.9 kB
JavaScript
var net = require('net');
var moment = require('moment');
var Promise = require('promise');
const
COMMAND_LIST_ALL_NODE = 0x13,
COMMAND_LIST_ALL_ZONE = 0x1E,
COMMAND_BRIGHTNESS = 0x31,
COMMAND_ONOFF = 0x32,
COMMAND_TEMP = 0x33,
COMMAND_COLOR = 0x36,
COMMAND_SOFT_ON = 0xDB,
COMMAND_SOFT_OFF = 0xDC,
COMMAND_GET_ZONE_INFO = 0x26,
COMMAND_GET_STATUS = 0x68,
COMMAND_ACTIVATE_SCENE = 0x52;
Buffer.prototype.getOurUTF8String = function (start, end) {
for (var i=start; i<end && this[i]!==0; i++) {}
return this.toString('utf-8', start, i);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// use this to get & set the mac-value as hex string
/*
Buffer.prototype.writeDoubleLE1 = function (val, pos) {
return this.write(val.toLowerCase(), 0, 8, 'hex');
};
Buffer.prototype.readDoubleLE1 = function (pos, len) {
return this.toString('hex', pos, pos+len).toUpperCase();
};
*/
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
function defaultBuffer(mac, len) {
if(len == undefined) len = 9;
var body = new Buffer(len);
body.fill(0);
if (typeof mac == 'string') {
body.write(mac.substr(0, 16), 0, 'hex');
} else {
body.writeDoubleLE(mac, 0);
}
return body;
}
function isBigEndian() {
var a = new Uint32Array([0x12345678]);
var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
return (b[0] == 0x12);
}
var seq = 0;
var lightify = function(ip, logger) {
this.ip = ip;
this.commands = [];
this.logger = logger;
this.groupCommands = [
COMMAND_BRIGHTNESS,
COMMAND_ONOFF,
COMMAND_TEMP,
COMMAND_COLOR,
COMMAND_GET_STATUS
];
this.connectErrorCount = 0;
};
lightify.prototype.processData = function(cmd, data) {
var fail = data.readUInt8(8);
if(fail && cmd.reject) {
return cmd.reject({
cmd : cmd,
fail : fail,
response : data.toString('hex')
});
}
var num = data.readUInt16LE(9);
var result = { result: [] };
var packageSize = cmd.packageSize || (num && (data.length - 11) / num);
for (var i = 0; i < num; i++) {
var pos = 11 + i * packageSize;
result.result.push(cmd.cb(data, pos));
}
result.request = cmd.request;
result.response = data.toString('hex');
if (cmd.resolve) {
cmd.resolve(result);
}
}
lightify.prototype.connect = function() {
var self = this;
return new Promise(function(resolve, reject) {
self.client = new net.Socket();
self.connectTimeout = setTimeout(function () {
reject('connect timeout');
self.logger && self.logger.debug('can not connect to lightify bridge, timeout');
self.client.destroy();
}, 4000);
self.client.on('data', function(data) {
self.logger && self.logger.debug('socket data: [%s]', data.toString('hex'))
if(self.readBuffer && self.readBuffer.length) {
data = Buffer.concat([self.readBuffer, data]);
}
var expectedLen = data.readUInt16LE(0) + 2;
self.logger && self.logger.debug('Expected len [%s]', expectedLen);
self.logger && self.logger.debug('len = [%s]', data.length);
if(expectedLen > data.length) {
self.readBuffer = new Buffer(data); // I think you can use data directly (without new Buffer())
return;
} else if(expectedLen === data.length){
self.readBuffer = undefined;
} else {
self.readBuffer = new Buffer(data.slice(data.length - expectedLen));
}
self.onData(data);
});
self.client.on('error', function(error) {
self.onError();
});
self.client.connect(4000, self.ip, function() {
clearTimeout(self.connectTimeout);
resolve();
});
});
};
lightify.prototype.onData = function (data) {
var seq = data.readUInt32LE(4);
this.logger && this.logger.debug('got response for seq [%s][%s]', seq, data.toString('hex'));
for(var i = 0; i < this.commands.length; i++) {
if(this.commands[i].seq === seq) {
clearTimeout(this.commands[i].timer);
this.logger && this.logger.debug('found request');
this.processData(this.commands[i], data)
this.commands.splice(i, 1);
this.sendNextRequest();
break;
}
}
};
lightify.prototype.dispose = function () {
this.commands = [];
this.client.destroy();
if (this.buffers) this.buffers.length = 0;
this.client.connected = undefined;
};
lightify.prototype.onError = function (error) {
this.logger && this.logger.debug('connection has error', error);
for(var i = 0; i < this.commands.length; i++) {
if(this.commands[i] && this.commands[i].reject) {
this.commands[i].reject(error);
}
}
this.dispose();
};
lightify.prototype.setDisconnectTimer = function () {
var self = this;
var _setTimer = function () {
if (self.disconnectTimer) clearTimeout (self.disconnectTimer);
self.disconnectTimer = setTimeout (function () {
self.disconnectTimer = undefined;
if (self.buffers.length) return _setTimer ();
self.client.end ();
self.client.connected = undefined;
self.logger && self.logger.debug ('setDisconnectTimer: client.end() called');
}, self.timeToStayConnected);
};
if (!self.disconnectTimer || !self.buffers.length) _setTimer ();
};
lightify.sendNextRequest = lightify.prototype.sendNextRequestNormal = function (buffer) {
if (buffer) this.client.write(buffer); // to overwrite it to use serialization
};
lightify.prototype.sendNextRequestAutoClose = function (buffer) {
var self = this;
var nextTimer;
var checkSend = function () {
switch (self.client.connected) {
case false:
break;
case true:
if (self.buffers.length > 0) {
self.client.write (self.buffers.shift ());
self.logger && self.logger.debug('sendNextRequest: sent buffer done, remaining length=' + self.buffers.length);
if (self.buffers.length) {
if (nextTimer) clearTimeout(nextTimer);
nextTimer = setTimeout (checkSend, 1000);
}
}
break;
case undefined:
self.client.connected = false;
self.logger && self.logger.debug('sendNextRequest: trying to connect');
//self.client.connect (4000, self.ip, function () {
self.connectEx(function() {
self.logger && self.logger.debug('sendNextRequest: connected! buffers.length=' + self.buffers.length);
checkSend ();
});
break;
}
};
if (buffer) {
this.buffers = this.buffers || [];
self.logger && self.logger.debug('sendNextRequest: push(buffer) length=' + (this.buffers.length+1));
this.buffers.push (buffer);
} else {
if (nextTimer) clearTimeout(nextTimer);
nextTimer = undefined;
this.setDisconnectTimer();
}
checkSend();
};
lightify.prototype.setAutoCloseConnection = function (on) {
if (on) {
if (this.timeToStayConnected === undefined) this.timeToStayConnected = 3000;
this.buffers = this.buffers || [];
this.buffers.length = 0;
this.sendNextRequest = this.sendNextRequestAutoClose;
} else {
delete this.buffers;
this.sendNextRequest = this.sendNextRequestNormal;
}
}
lightify.prototype.connectEx = function (cb, errorCb) {
var self = this;
function func() {
self.connectErrorCount = 0;
self.client.connected = true;
cb && cb();
}
if (!this.client) { // || this.client.destroyed || !this.client.readable) {
return this.connect().then(func).catch(function(error) {
if (self.connectErrorCount++ < 5) setTimeout(function() {
self.client = undefined;
self.connectEx(cb);
}, 1000 * self.connectErrorCount);
else {
errorCb && errorCb('Can not connect to Lightify Gateway ' + self.ip, error);
self.logger && self.logger.debug ('Can not connect to Lightify Gateway ' + self.ip);
}
});
}
return this.client.connect(4000, self.ip, func);
};
lightify.prototype.isGroupCommand = function (cmdId, body) {
if (this.groupCommands.indexOf(cmdId) >= 0) {
// in case mac was not a string (in your case the group-id). Otherwise this function is overwritable
for (var i = 0; i < 7 && body[i] == 0; i++);
if (i==6) return 2;
}
return 0;
};
lightify.prototype.sendCommand = function(cmdId, body, flag, cb, packageSize) {
var self = this;
if (typeof flag == 'function') { cb = flag; flag = undefined; }
if (flag === undefined) flag = this.isGroupCommand(cmdId, body);
return new Promise(function(resolve, reject) {
var buffer = new Buffer(8 + body.length);
buffer.fill(0);
buffer.writeUInt16LE(8 + body.length - 2, 0);// length
buffer.writeUInt8(flag || 0x00, 2); // Flag, 0:node, 2:zone
buffer.writeUInt8(cmdId, 3); // command
buffer.writeUInt32LE(++seq, 4); // request id
body.copy(buffer, 8);
var cmd = {
seq : seq,
createTime : moment().format('x'),
resolve : resolve,
reject : reject,
packageSize : packageSize,
cb :(cb || function(data, pos) {
return {
mac : data.readDoubleLE(pos, 8),
friendlyMac : data.toString('hex', pos, pos + 8),
success : data.readUInt8(pos + 8)
};
}),
request : buffer.toString('hex')
};
cmd.timer = setTimeout(function() {
self.logger && self.logger.debug('send command timeout [%s][%s]', cmd.seq, buffer.toString('hex'));
cmd.reject('timeout');
cmd.resolve = undefined;
cmd.reject = undefined;
self.sendNextRequest();
}, 1000);
self.logger && self.logger.debug('command sent [%s][%s]', cmd.seq, buffer.toString('hex'));
self.commands.push(cmd);
self.sendNextRequest(buffer);
//self.client.write(buffer);
});
}
lightify.prototype.discover = function() {
var self = this;
return this.sendCommand(COMMAND_LIST_ALL_NODE, new Buffer([0x1]), function(data, pos) {
return {
id: data.readUInt16LE(pos),
mac: data.readDoubleLE(pos + 2, 8),
friendlyMac : data.toString('hex', pos + 2, pos + 10),
type: data.readUInt8(pos + 10),
firmware_version: data.readUInt32BE(pos + 11),
online: data.readUInt8(pos + 15),
groupid: data.readUInt16LE(pos + 16),
status: data.readUInt8(pos + 18), // 0 == off, 1 == on
brightness: data.readUInt8(pos + 19),
temperature: data.readUInt16LE(pos + 20),
red: data.readUInt8(pos + 22),
green: data.readUInt8(pos + 23),
blue: data.readUInt8(pos + 24),
alpha: data.readUInt8(pos + 25),
name: data.getOurUTF8String(pos + 26, pos + 50)
};
}, 50);
}
lightify.prototype.discoverZone = function() {
return this.sendCommand(COMMAND_LIST_ALL_ZONE, new Buffer([0x0]), 2, function(data, pos) {
var id = data.readUInt16LE(pos);
var buffer = new Buffer(8);
buffer.fill(0);
buffer.writeUInt16LE(id, 0);
return {
id : id,
mac : buffer.readDoubleLE(0), // use id as mac
name: data.getOurUTF8String(pos + 2, pos + 18)
};
});
}
lightify.prototype.nodeOnOff = function(mac, on, isGroup) {
var body = defaultBuffer(mac);
body.writeUInt8(on ? 1 : 0, 8);
return this.sendCommand(COMMAND_ONOFF, body, isGroup !== undefined ? isGroup ? 0x2 : 0 : undefined);
}
lightify.prototype.nodeSoftOnOff = function(mac, on, transitiontime) {
var body = defaultBuffer(mac, 10);
body.writeUInt16LE(transitiontime || 0, 8);
return this.sendCommand (on ? COMMAND_SOFT_ON : COMMAND_SOFT_OFF, body);
}
lightify.prototype.activateScene = function(sceneId) {
var body = new Buffer(2);
body.writeUInt8(sceneId, 0);
body.writeUInt8(0, 1);
return this.sendCommand (COMMAND_ACTIVATE_SCENE, body);
}
lightify.prototype.getZoneInfo = function(zone) {
var body = new Buffer(2);
body.writeUInt16LE(zone, 0);
return this.sendCommand (COMMAND_GET_ZONE_INFO, body, 2,
function(data, pos) {
var o = {
groupNo: zone,
name: data.getOurUTF8String(pos, pos + 15),
devices: []
}
var cnt = data.readUInt8(pos + 16);
for (var i= 0; i < cnt; i++) {
var ipos = pos + 17 + i * 8;
o.devices.push(data.readDoubleLE(ipos, 8));
}
return o;
}
);
}
lightify.prototype.getStatus = function(mac) {
var self = this;
var body = defaultBuffer(mac, 8);
return this.sendCommand(COMMAND_GET_STATUS, body, function(data, pos) {
var o = {
mac: data.readDoubleLE(11, 8),
requestStatus: data.readUInt8(19),
online: 0
}
if (o.requestStatus == 0x00) { //0xFF) {
o.online = data.readUInt8(20);
o.status = data.readUInt8(21);
o.brightness = data.readUInt8(22);
o.temperature = data.readUInt16LE(23);
o.red = data.readUInt8(25);
o.green = data.readUInt8(26);
o.blue = data.readUInt8(27);
o.alpha = data.readUInt8(28);
}
return o;
}).then(function(device) {
return Promise.resolve(device.result.length && device.result[0]);
});
}
lightify.prototype.nodeBrightness = function(mac, brightness, stepTime) {
var buffer = defaultBuffer(mac, 11);
buffer.writeUInt8(brightness, 8);
buffer.writeUInt16LE(stepTime || 0, 9);
return this.sendCommand(COMMAND_BRIGHTNESS, buffer);
}
lightify.prototype.nodeTemperature = function(mac, temperature, stepTime) {
var buffer = defaultBuffer(mac, 12);
buffer.writeUInt16LE(temperature, 8);
buffer.writeUInt16LE(stepTime || 0, 10);
return this.sendCommand(COMMAND_TEMP, buffer);
}
lightify.prototype.nodeColor = function(mac, red, green, blue, alpha, stepTime) {
var buffer = defaultBuffer(mac, 14);
buffer.writeUInt8(red, 8);
buffer.writeUInt8(green, 9);
buffer.writeUInt8(blue, 10);
buffer.writeUInt8(alpha, 11);
buffer.writeUInt16LE(stepTime || 0, 12);
return this.sendCommand(COMMAND_COLOR, buffer);
}
function getNodeType(type) {
return isPlug(type) ? 16 : type;
}
var tf = {
ONOFF_LIGHT : 0x01,
COLORTEMP_DIMMABLE_LIGHT : 0x02,
DIMMABLE_LIGHT : 0x04,
COLOR_LIGHT : 0x08,
EXT_COLOR_LIGHT : 0x0A,
PLUG: 0x10,
SENSOR : 0x20,
TWO_BTN_SWITCH : 0x40,
FOUR_BTN_SWITCH : 0x41,
BRI: 0xae, // ((~FT_SWITCH) & (~FT_PLUG)) & 0xff, //0xffee,
ALL: 0xff,
LIGHT: 0xae
};
function isSwitch(type) {
return type === tf.TWO_BTN_SWITCH || type === tf.FOUR_BTN_SWITCH;
}
function isPlug(type) {
return type === tf.PLUG;
}
function isSensor(type) {
return type === tf.SENSOR;
}
var exports = module.exports = {
lightify : lightify,
isPlug : isPlug,
isSwitch : isSwitch,
isSensor : isSensor,
is2BSwitch : function(type) { return type === tf.TWO_BTN_SWITCH;},
is4BSwitch : function(type) { return type === tf.FOUR_BTN_SWITCH;},
isBrightnessSupported : function(type) {
return getNodeType(type) === tf.COLORTEMP_DIMMABLE_LIGHT ||
getNodeType(type) === tf.DIMMABLE_LIGHT ||
(!isPlug(type) && getNodeType(type) != tf.ONOFF_LIGHT);
},
isTemperatureSupported : function(type) {
return getNodeType(type) === tf.COLORTEMP_DIMMABLE_LIGHT ||
getNodeType(type) === tf.EXT_COLOR_LIGHT; },
isColorSupported : function(type) {
return getNodeType(type) === tf.EXT_COLOR_LIGHT ||
getNodeType(type) === tf.COLOR_LIGHT; },
isLight : function(type) { return !isSwitch(type) && !isPlug(type) && !isSensor(type); },
tf: tf
};