smart-bus
Version:
Node.js implementation of HDL SmartBus protocol
571 lines (433 loc) • 15.4 kB
JavaScript
var Bus = require('../lib/bus');
var should = require('should');
var simple = require('simple-mock');
var Device = require('../lib/device');
var Command = require('../lib/command');
var EventEmitter = require('events');
describe('Bus', function() {
var bus;
beforeEach(function(done) {
bus = new Bus({ gateway: '192.0.2.100' });
bus.socket.on('listening', done);
});
afterEach(closeSocket);
describe('initialization', function() {
describe('open socket', function() {
// Close previous bus object socket
beforeEach(closeSocket);
it('should set properties', function() {
bus = new Bus({ gateway: '192.0.2.100' });
should(bus).have.properties({
port: 6000,
gateway: '192.0.2.100',
address: Buffer.from([0, 0, 0, 0])
});
});
it('should create instance with default port', function(done) {
bus = new Bus({ gateway: '192.0.2.100' });
bus.socket.on('listening', function() {
should(bus.socket.address()).have.property('port', 6000);
done();
});
});
it('should open socket on given port', function(done) {
bus = new Bus({ port: 6500, gateway: '192.0.2.100' });
var socket = bus.socket;
socket.on('listening', function() {
should(socket.address()).have.property('port', 6500);
done();
});
});
it('should parse connection string', function() {
bus = new Bus('hdl://192.0.2.100:7000');
should(bus).have.properties({
port: 7000,
gateway: '192.0.2.100'
});
});
it('should reuse same socket address', function(done) {
var calls = 0;
bus = new Bus('hdl://192.0.2.100:6200');
bus.socket.on('listening', checkPort);
var secondBus = new Bus('hdl://192.0.2.100:6200');
secondBus.socket
.on('listening', checkPort)
.on('error', done)
.on('close', done);
function checkPort() {
should(this.address()).have.property('port', 6200);
if (++calls === 2) secondBus.socket.close();
}
});
});
it('should inherit from event emitter', function() {
should(bus).be.an.instanceOf(EventEmitter);
});
it('should close socket on error and emit event', function(done) {
var bus = new Bus('hdl://192.0.2.100:6300');
var error = new Error('Something went wrong');
bus.on('error', function(err) {
should(err).equal(error);
bus.on('close', done);
});
bus.socket.emit('error', error);
});
it('should emit listening event', function(done) {
var bus = new Bus('hdl://192.0.2.100:6300');
bus.on('listening', function() {
bus.socket.on('close', done);
bus.socket.close();
});
});
it('should close socket', function(done) {
var bus = new Bus('hdl://192.0.2.100:6300');
var spy = simple.mock(bus.socket, 'close');
var callback = function() {};
bus.socket.on('close', function() {
should(spy.callCount).equal(1);
should(spy.lastCall.arg).equal(callback);
done();
});
bus.close(callback);
});
it('should emit close event', function(done) {
var bus = new Bus('hdl://192.0.2.100:6300');
bus.on('close', done);
bus.close();
});
it('should set broadcast flag on socket', function(done) {
var bus = new Bus('hdl://192.0.1.255:6000');
var spy = simple.mock(bus.socket, 'setBroadcast');
bus.on('listening', function() {
bus.setBroadcast(true);
should(spy.callCount).equal(1);
should(spy.lastCall.arg).equal(true);
bus.on('close', done);
bus.close();
});
});
});
describe('device', function() {
var device;
beforeEach(function() {
device = bus.device('1.25');
});
it('should initialize device with address', function() {
should(device).be.an.instanceOf(Device);
should(device).have.properties({
id: 25,
subnet: 1,
bus: bus
});
});
it('should initialize device with subnet and id', function() {
device = bus.device({ subnet: 1, id: 35 });
should(device).be.an.instanceOf(Device);
should(device).have.properties({
id: 35,
subnet: 1,
bus: bus
});
});
it('should intialize device with address option', function() {
device = bus.device({ address: '1.50' });
should(device).be.an.instanceOf(Device);
should(device).have.properties({
id: 50,
subnet: 1,
bus: bus
});
});
it('should initialize device with custom type', function() {
device = bus.device({ address: '1.50', type: 0xFFFE });
should(device).be.an.instanceOf(Device);
should(device).have.properties({
id: 50,
subnet: 1,
type: 0xFFFE,
bus: bus
});
});
it('should cache device instance', function() {
should(bus.device({ subnet: 1, id: 25 })).equal(device);
});
});
describe('controller', function() {
var controller;
beforeEach(function() {
controller = bus.controller('1.50');
});
it('should create device with type 0xFFFE', function() {
should(controller).be.instanceOf(Device);
should(controller.type).eql(0xFFFE);
});
it('should cache controller instance', function() {
should(bus.device('1.50')).equal(controller);
});
it('should get address from options', function() {
controller = bus.controller({ address: '1.45' });
should(controller.subnet).eql(1);
should(controller.id).eql(45);
});
it('should get subnet and id from options', function() {
controller = bus.controller({ subnet: 1, id: 42 });
should(controller.subnet).eql(1);
should(controller.id).eql(42);
});
it('should ignore type option', function() {
controller = bus.controller({ address: '1.46', type: 0xFFFF });
should(controller.type).eql(0xFFFE);
});
});
describe('send', function() {
var send, sender;
beforeEach(function() {
send = simple.mock(bus.socket, 'send').callback();
sender = bus.device({ address: '1.50', type: 0xFFFE });
});
it('should send command to device by address', function(done) {
bus.send({
sender: '1.50',
target: '1.40',
command: 0x0031,
data: { channel: 1, level: 100 }
}, function(err) {
should(send.callCount).be.exactly(1);
should(send.lastCall.args[0]).eql(Buffer.from('0000000048444C4D49524' +
'1434C45AAAA0F0132FFFE00310128016400000803', 'hex'));
should(send.lastCall.args[1]).equal(0);
should(send.lastCall.args[2]).eql(31);
should(send.lastCall.args[3]).eql(6000);
should(send.lastCall.args[4]).eql('192.0.2.100');
done(err);
});
});
it('should send command to device object', function(done) {
var target = bus.device({ subnet: 1, id: 30 });
bus.send({
sender: sender,
target: target,
command: 0x0031,
data: { channel: 5, level: 100 }
}, function(err) {
should(send.callCount).be.exactly(1);
should(send.lastCall.args[0]).eql(Buffer.from('0000000048444C4D49524' +
'1434C45AAAA0F0132FFFE0031011E056400000399', 'hex'));
should(send.lastCall.args[1]).equal(0);
should(send.lastCall.args[2]).eql(31);
should(send.lastCall.args[3]).eql(6000);
should(send.lastCall.args[4]).eql('192.0.2.100');
done(err);
});
});
it('should accept raw buffer as data', function(done) {
bus.send({
sender: sender,
target: '1.23',
command: 0x0031,
data: Buffer.from('01640000', 'hex')
}, function(err) {
should(send.callCount).be.exactly(1);
should(send.lastCall.args[0]).eql(Buffer.from('0000000048444C4D49524' +
'1434C45AAAA0F0132FFFE00310117016400006114', 'hex'));
should(send.lastCall.args[1]).equal(0);
should(send.lastCall.args[2]).eql(31);
should(send.lastCall.args[3]).eql(6000);
should(send.lastCall.args[4]).eql('192.0.2.100');
done(err);
});
});
it('should accept payload buffer', function(done) {
bus.send({
sender: sender,
target: '1.23',
command: 0x0031,
payload: Buffer.from('01640000', 'hex')
}, function(err) {
should(send.callCount).be.exactly(1);
should(send.lastCall.args[0]).eql(Buffer.from('0000000048444C4D49524' +
'1434C45AAAA0F0132FFFE00310117016400006114', 'hex'));
should(send.lastCall.args[1]).equal(0);
should(send.lastCall.args[2]).eql(31);
should(send.lastCall.args[3]).eql(6000);
should(send.lastCall.args[4]).eql('192.0.2.100');
done(err);
});
});
it('should catch data encoding error', function(done) {
bus.send({
sender: sender,
target: '1.23',
command: -1,
data: { channel: 5 }
}, function(err) {
should(err).have.property('message',
'Data encoder for command 0x00-1 is not implemented');
done();
});
});
it('should send command without additional data', function(done) {
bus.send({
sender: sender,
target: '1.23',
command: 0x0004
}, function(err) {
should(send.callCount).be.exactly(1);
should(send.lastCall.arg).eql(Buffer.from('0000000048444C4D49524' +
'1434C45AAAA0B0132FFFE000401175360', 'hex'));
done(err);
});
});
});
describe('parse', function() {
it('should not parse invalid message', function() {
should(bus.parse(Buffer.from('TEST'))).equal(undefined);
});
it('should ignore message from another gateway by default', function() {
should(bus.parse(Buffer.from('C0A801FA48444C4D495241434C45AAAA0E010202' +
'690032FFFF06F864B5A9', 'hex'))).equal(undefined);
});
it('should accept message from any gateway when broadcasting', function(done) {
var bus = new Bus('hdl://192.168.1.255:6000');
bus.on('listening', function() {
bus.setBroadcast(true);
var message = Buffer.from('C0A801FA48444C4D495241434C45AAAA0E0' +
'10202690032FFFF06F864B5A9', 'hex');
should(bus.parse(message)).have.properties({
code: 0x0032,
sender: bus.device('1.2'),
target: bus.device('255.255')
});
bus.on('close', done);
bus.close();
});
});
it('should ignore message from another gateway if broadcast is off', function(done) {
var bus = new Bus('hdl://192.168.1.255:6000');
bus.on('listening', function() {
bus.setBroadcast(false);
should(bus.parse(Buffer.from('C0A801FA48444C4D495241434C45AAAA0E0' +
'10202690032FFFF06F864B5A9', 'hex'))).equal(undefined);
bus.on('close', done);
bus.close();
});
});
it('should initialize device type after parsing', function() {
var device = bus.device('1.2');
should(device.type).equal(undefined);
bus.parse(Buffer.from('C000026448444C4D495241434C45AAAA0E010202' +
'690032FFFF06F864B5A9', 'hex'));
should(device.type).eql(0x0269);
});
it('should overwrite device type after parsing', function() {
var device = bus.device({
address: '1.2',
type: 0xFFFE
});
should(device.type).equal(0xFFFE);
bus.parse(Buffer.from('C000026448444C4D495241434C45AAAA0E010202' +
'690032FFFF06F864B5A9', 'hex'));
should(device.type).eql(0x0269);
});
it('should return command object', function() {
var command = bus.parse(Buffer.from('C000026448444C4D495241434C45AAAA0E' +
'010202690032FFFF06F864B5A9', 'hex'));
should(command).be.an.instanceOf(Command);
should(command).have.properties({
code: 0x0032,
sender: bus.device('1.2'),
target: bus.device('255.255'),
data: { channel: 6, success: true, level: 100 }
});
});
it('should return command object without data', function() {
var command = bus.parse(Buffer.from('C000026448444C4D49524' +
'1434C45AAAA0B0132FFFE000401175360', 'hex'));
should(command).be.an.instanceOf(Command);
should(command).have.properties({
code: 0x0004,
sender: bus.device('1.50'),
target: bus.device('1.23'),
data: undefined
});
});
});
describe('events', function() {
var handler;
beforeEach(function() {
handler = simple.mock();
});
it('should emit command event', function() {
bus.on('command', handler);
emitEvents();
should(handler.callCount).be.exactly(2);
should(handler.calls[0].arg).have.properties({
code: 0x0031,
sender: bus.device('1.5'),
target: bus.device('1.4'),
data: { channel: 4, level: 100, time: 3 },
payload: Buffer.from('0464000300', 'hex')
});
should(handler.calls[1].arg).have.properties({
code: 0x0032,
sender: bus.device('1.2'),
target: bus.device('255.255'),
data: { channel: 6, success: true, level: 100 },
payload: Buffer.from('06F864', 'hex')
});
});
it('should emit broadcast event', function() {
bus.on('broadcast', handler);
emitEvents();
should(handler.callCount).be.exactly(1);
should(handler.lastCall.arg).have.properties({
code: 0x0032,
sender: bus.device('1.2'),
target: bus.device('255.255'),
data: { channel: 6, success: true, level: 100 },
payload: Buffer.from('06F864', 'hex')
});
});
it('should emit event by command code', function() {
bus.on(0x0031, handler);
emitEvents();
should(handler.callCount).be.exactly(1);
should(handler.lastCall.arg).have.properties({
code: 0x0031,
sender: bus.device('1.5'),
target: bus.device('1.4'),
data: { channel: 4, level: 100, time: 3 },
payload: Buffer.from('0464000300', 'hex')
});
});
it('should emit event on sender device', function() {
var device = bus.device('1.2');
device.on(0x0032, handler);
emitEvents();
should(handler.callCount).be.exactly(1);
should(handler.lastCall.arg).have.properties({
code: 0x0032,
sender: device,
target: bus.device('255.255'),
data: { level: 100, channel: 6, success: true },
payload: Buffer.from('06F864', 'hex')
});
});
it('should handle invalid message', function() {
should(function() {
bus.socket.emit('message', Buffer.from('invalid'));
}).not.throw();
});
function emitEvents() {
bus.socket.emit('message', Buffer.from('C000026448444C4D495241434C45AAA' +
'A10010504550031010404640003000837', 'hex'));
bus.socket.emit('message', Buffer.from('C000026448444C4D495241434C45AAA' +
'A0E010202690032FFFF06F864B5A9', 'hex'));
}
});
function closeSocket(done) {
var socket = bus.socket;
socket.on('close', done);
socket.close();
}
});