/smart-bus

Node.js implementation of HDL SmartBus protocol

Primary LanguageJavaScriptMIT LicenseMIT

SmartBus Build Status

Node.js implementation of HDL SmartBus protocol http://hdlautomation.com

❗️ Read migration guide before upgrading to v0.6

Contents

Initialization

Create instance of SmartBus connector

var SmartBus = require('smart-bus');

var bus = new SmartBus({
  gateway: '192.168.1.250', // HDL SmartBus gateway IP
  port: 6000                // and port, default: 6000
});

In addition to passing configuration as an object, you can use an url string:

var bus = new SmartBus('hdl://192.168.1.250:6000');

HDL gateway port also would be used as listening port of udp server to receive broadcast messages.

Multiple gateways

If you have several physical HDL gateways create Bus instance for each:

var firstGateway = new SmartBus('hdl://192.168.1.250:6000');
var secondGateway = new SmartBus('hdl://192.168.1.251:6000');

Alternatively UDP broadcasting can be used if gateways sharing same network. To do this create Bus instance with broadcast address as gateway IP and enable broadcast mode:

// Bus instance with broadcast address as gateway IP
var bus = new SmartBus('hdl://192.168.1.255:6000');

bus.on('listening', function() {
  bus.setBroadcast(true);
});

Receive commands

Add handler to intercept all commands across bus

bus.on('command', function(command) {

  // Integer with command operation code
  command.code;

  // Device objects
  command.sender;
  command.target;

  // Buffer with command payload
  command.payload;

  // Object with decoded data from payload
  // if it could be parsed automatically
  command.data;

});

Handlers can be added for broadcast messages or specific commands

bus.on('broadcast', function(command) { /* ... */ });
bus.on(0x0032, function(command) { /* ... */ });

Listen for commands from specific device

var sensor = bus.device('1.20');

sensor.on(0x1647, function(command) { /* ... */ });

Command payload

This library has built-in parses and encoders for payload of some HDL commands. You can find it with examples in fixtures.

Parser-function will be invoked on access to command.data property:

bus.on(0x0032, function(command) {
  command.payload; // => <Buffer 0a f8 64>

  command.data; // => { channel: 10, level: 100, success: true }
});

command.data is a short-hand getter for command.parse(command.payload).

Most of these parsers was built refering to official document "Operation Code of HDL Buspro v1.111", tested on real HDL setup and should work out-of-the-box with your installation.

But this is unofficial library and you could get errors when accessing command.data property due to malformed command payload or undocumented features.

In case if you expiriencing unexpected errors on access to command.data property, you can fallback to parse command.payload manually:

bus.on(0x0032, function(command) {
  var data;

  try {
    data = command.data;
  } catch(err) {
    // Parse command.payload manually in case of error
  }
});

Feel free to make a PR with your fix or Issue with example of malformed command payload and other details.

Send commands

There are several ways to send commands into HDL Bus, for all of them you need to define sender device.

Controller

Easiest way to send commands via IP gateway is to create a virtual HDL device representing nodejs application in HDL environment:

var controller = bus.controller('1.50');

controller.send({ target: '1.4', command: 0x0004 }, callback);

controller.send({
  target: '1.4',
  command: 0x0031,
  data: { channel: 1, level: 100 }
}, callback);

function callback(err) {
  // Command sent...
}

controller function simply returns device instance with type 0xFFFE which means "PC" for HDL Bus and it's a shortcut for bus.device() function described below.

Custom device

It's possible to create device object with custom type or without type at all and use it for sending messages

var sender = bus.device({
  address: '1.42',
  type: 0x0269
});

var logic = bus.device('1.4');

sender.send({
  target: logic,
  command: 0xE01C,
  data: { switch: 1, status: 1 }
}, function(err) { /* ... */ });

Optionally both controller and device methods accepts address param as subnet and id properties:

bus.device({ subnet: 1, id: 50 });

Using Bus instance directly

Bus instance could be used directly to get full control

bus.send({
  sender: '1.50', // Sender device subnet and id
  target: '1.4',  // Receiver device subnet and id

  command: 0x0031,   // Command code
  data: { channel: 1, level: 100 } // Optional command data
}, function(err) { /* ... */ });

sender and target properties could be a device instances

var sender = bus.device('1.50');
var dimmer = bus.device('1.4');

bus.send({
  sender: sender,
  target: dimmer,

  command: 0x0031,
  data: { channel: 1, level: 100 }
}, function(err) { /* ... */ });

In case if data object can not be encoded error will be passed into callback.

Built-in parsers and encoders for command payload with examples described in fixtures.

Use payload param if you need to send raw buffer as command payload:

var sender = bus.device('1.50');
var dimmer = bus.device('1.4');

bus.send({
  sender: sender,
  target: dimmer,

  command: 0x0031,
  payload: new Buffer('04640190', 'hex')
}, function(err) { /* ... */ });

Complete example

Usually modifier HDL commands have corresponding response commands. Use them to keep an actual state of HDL devices:

// Create UDP socket with HDL IP gateway
var bus = new SmartBus('hdl://192.168.1.250:6000');

// Initialize virtual controller device to send commands
var controller = bus.controller('1.50');

// Initialize dimmer device
var dimmer = bus.device('1.4');

// Setup listener for "Response Single Channel Control" command
dimmer.on(0x0032, function(command) {
  var data = command.data;

  var success = data.success;
  var channel = data.channel;
  var level = data.level;

  if (success) console.log('Channel #%d level is %d', channel, level);
  else console.log('Failed to change level of #%d channel', channel);
});

// Use controller to send "Single Channel Control" command to dimmer
controller.send({
  target: dimmer,
  command: 0x0031,
  data: { channel: 1, level: 100, time: 5 }
}, function(err) {
  if (err) console.error('Failed to send command');
  else console.log('Sent command 0x0031 to %s', dimmer);
});

Refer to "Operation Code of HDL Buspro" (could be found on the internet) document for complete list of commands or analyze you own setup using debug mode.

Graceful shutdown

Underlying UDP socket can be closed using close method:

bus.on('close', function() {
  console.log('HDL Bus is closed');
});

bus.close();

Bus will close UDP socket on error automatically so it would be useful to attach error event handler and restart process if needed.

var error;

bus.on('error', function(err) { error = err; });

bus.on('close', function() {
  // Shutdown process with error code
  process.exit(err ? 1 : 0);
});

Debugging

This package uses debug utility and streams all intercepted via HDL IP gateway commands into console when DEBUG environment variable contains smart-bus string.

Run your script with defined DEBUG environment variable to get output:

$ DEBUG=smart-bus node your-script.js

Upgrading

v0.x -> v0.6

v0.6 is a transitional version before upgrading min version of nodejs to v6, it includes correct UDP Socket utilization, stability improvements, new methods signatures to provide more flexiliblity and other features.

In v0.6 "sender" device extracted from Bus class and must be initialized separately. This change transforms logic of device.send() method and implies appropriate changes in signatures of all other methods.

Before v0.6 device.send() method was used to send command to device, now it means "send message from device".

Check CHANGELOG.md to see complete set of changes.

Update to v0.6 from previous versions requires a lot of change

  • Update initialization of bus instance:

    /* Before v0.6 */
    
    var bus = new SmartBus({
      device: '1.50',
      gateway: '192.168.1.250',
      port: 6000
    });
    /* v0.6 */
    
    var bus = new SmartBus({ gateway: '192.168.1.250', port: 6000 });
    
    var controller = bus.controller('1.50');
  • Update bus.send() calls:

    /* Before v0.6 */
    
    bus.send('1.4', 0x0031, { channel: 1, level: 100 },
      function(err) { /* ... */ });
    
    bus.send('1.4', 0x0031, new Buffer('0164', 'hex'),
      function(err) { /* ... */ });
    /* v0.6 */
    
    controller.send({
      target: '1.4',
      command: 0x0031,
      data: { channel: 1, level: 100 }
    }, function(err) { /* ... */ });
    
    controller.send({
      target: '1.4',
      command: 0x0031,
      payload: new Buffer('0164', 'hex')
    }, function(err) { /* ... */ });
  • Update device.send() calls:

    /* Before v0.6 */
    
    var logic = bus.device('1.10');
    
    logic.send(0xE01C, { switch: 1, status: 1 }, function(err) { /* ... */ });
    /* v0.6 */
    var logic = bus.device('1.10');
    
    controller.send({
      target: logic,
      command: 0xE01C,
      data: { switch: 1, status: 1 }
    }, function(err) { /* ... */ });
  • Update signature of device event handlers:

    /* Before v0.6 */
    
    var sensor = bus.device('1.20');
    
    sensor.on(0x1647, function(data, target) { /* ... */ });
    /* v0.6 */
    
    var sensor = bus.device('1.20');
    
    sensor.on(0x1647, function(command) {
      var data = command.data;
      var target = command.target;
    
      /* ... */
    });
  • Replace device.channel() abstraction with custom listener:

    /* Before v0.6 */
    
    var dimmer = bus.device('1.4');
    var spotlights = dimmer.channel(2);
    
    spotlights.on('status', function() {
      console.log('Spotlights level is %s', spotlights.level);
    });
    
    spotlights.control(100, { time: 5 }, function(err) { /* ... */ });
    /* v0.6 */
    
    var dimmer = bus.device('1.4');
    
    dimmer.on(0x0032, function(command) {
      var data = command.data;
    
      if (!data.success) return;
    
      var level = data.level;
      var channel = data.channel;
    
      console.log('Channel #%d level is %d', channel, level);
    });
    
    controller.send({
      target: dimmer,
      command: 0x0031,
      data: { channel: 1, level: 100, time: 5 }
    }, function(err) { /* ... */ });