Monday, 10 April 2017

Home Automation - HomeAssistant and Clipsal C-Bus

This was an interesting one, I wanted to control C-Bus lights / Devices in the home with HomeAssistant, it took a bit to get going, but worth it..

This isn't a complete guide on HomeAssistant, MQTT, Mosquitto, C-Gate, Debian or anything else, but with the help of Google and luck, it should get you going..

Components used for the interface:
Lots of reading the C-Gate programming guide and other places:
www3.clipsal.com/cis/downloads/Toolkit/CGateServerGuide_1_0.pdf

http://addictedtopi.tumblr.com/post/96351714013/installing-c-gate-on-a-raspberry-pi


Setting up Debian:
Install other packages openjdk-7-jdk nodejs npm mosquitto mosquitto-clients

Setting up Node.js
npm install mqtt --save

C-Bus / C-Gate setup..
Install C-Gate on linux and connect to this remote C-Gate with the C-Bus toolkit, get everything working with this configuration BEFORE continuing..  The rest won't really work until you have networks / projects etc set up.  The configuration is stored on the linux server with the C-Gate install.

I used this post as a guide to install:
http://addictedtopi.tumblr.com/post/96351714013/installing-c-gate-on-a-raspberry-pi









Modifications to the code required..
I assume the excellent C-Gate / MQTT code from the1las @ github was written for an older version of C-Gate than the one I'm using (2.10.6) so I had to make some modifications to get it to work for me..  Without this code as a base, this would have taken so much longer!

modified index.js

var mqtt = require('mqtt'), url = require('url');
var net = require('net');
var events = require('events');
var settings = require('./settings.js');

var buffer = "";
var eventEmitter = new events.EventEmitter();

// MQTT URL
var mqtt_url = url.parse('mqtt://'+settings.mqtt);

// Username and password
var OPTIONS = {};
if(settings.mqttusername && settings.mqttpassword) {
  OPTIONS.username = settings.mqttusername;
  OPTIONS.password = settings.mqttpassword;
}

// Create an MQTT client connection
var client = mqtt.createClient(mqtt_url.port, mqtt_url.hostname,OPTIONS);

var HOST = settings.cbusip;
var COMPORT = 20023;
var EVENTPORT = 20025;

var logging = settings.logging;

// Connect to cgate via telnet
var command = new net.Socket();
command.connect(COMPORT, HOST, function() {

  console.log('CONNECTED TO C-GATE COMMAND PORT: ' + HOST + ':' + COMPORT);
  command.write('EVENT ON\n');

});


// Connect to cgate event port via telnet
var event = new net.Socket();
event.connect(EVENTPORT, HOST, function() {
  command.write('PROJECT LOAD '+settings.cbusname+'\n');
  command.write('PROJECT USE '+settings.cbusname+'\n');
  command.write('NET OPEN '+settings.cbusnetwork+'\n');
  command.write('PROJECT START'+'\n');

  console.log('CONNECTED TO C-GATE EVENT PORT: ' + HOST + ':' + EVENTPORT);

});


client.on('connect', function() { // When connected
  console.log('CONNECTED TO MQTT: ' + settings.mqtt);

  // Subscribe to MQTT
  client.subscribe('cbus/write/#', function() {

    // when a message arrives, do something with it
    client.on('message', function(topic, message, packet) {
      if (logging == true) {console.log('Message received on ' + topic + ' : ' + message);}

      parts = topic.split("/");
      if (parts.length > 5)

      switch(parts[5].toLowerCase()) {

        // Get updates from all groups
        case "getall":
          command.write('GET /'+parts[2]+'/'+parts[3]+'/* level\n');
          break;

        // On/Off control
        case "switch":

          if(message == "ON") {command.write('ON '+parts[2]+'/'+parts[3]+'/'+parts[4]+'\n')};
          if(message == "OFF") {command.write('OFF '+parts[2]+'/'+parts[3]+'/'+parts[4]+'\n')};
          break;

        // Ramp, increase/decrease, on/off control
        case "ramp":
          switch(message.toUpperCase()) {
            case "INCREASE":
              eventEmitter.on('level',function increaseLevel(address,level) {
                if (address == parts[2]+'/'+parts[3]+'/'+parts[4]) {
                  command.write('RAMP '+parts[2]+'/'+parts[3]+'/'+parts[4]+' '+Math.min((level+26),255)+' '+'\n');
                  eventEmitter.removeListener('level',increaseLevel);
                }
              });
              command.write('GET '+parts[2]+'/'+parts[3]+'/'+parts[4]+' level\n');

              break;

            case "DECREASE":
              eventEmitter.on('level',function decreaseLevel(address,level) {
                if (address == parts[2]+'/'+parts[3]+'/'+parts[4]) {
                  command.write('RAMP '+parts[2]+'/'+parts[3]+'/'+parts[4]+' '+Math.max((level-26),0)+' '+'\n');
                  eventEmitter.removeListener('level',decreaseLevel);
                }
              });
              command.write('GET '+parts[2]+'/'+parts[3]+'/'+parts[4]+' level\n');

              break;

            case "ON":
              command.write('ON '+parts[2]+'/'+parts[3]+'/'+parts[4]+'\n');
              break;
            case "OFF":
              command.write('OFF '+parts[2]+'/'+parts[3]+'/'+parts[4]+'\n');
              break;
            default:
              var ramp = message.split(",");
              var num = Math.round(parseInt(ramp[0])*255/100)
              if (!isNaN(num) && num < 256) {

                if (ramp.length > 1) {
                  command.write('RAMP '+parts[2]+'/'+parts[3]+'/'+parts[4]+' '+num+' '+ramp[1]+'\n');
                } else {
                  command.write('RAMP '+parts[2]+'/'+parts[3]+'/'+parts[4]+' '+num+'\n');
                }
              }
          }
          break;
        default:
      }
    });
  });

  // publish a message to a topic
  client.publish('hello/world', 'CBUS ON', function() {
  });
});



command.on('data',function(data) {
  var lines = (buffer+data.toString()).split("\n");
  buffer = lines[lines.length-1];
  if (lines.length > 1) {
    for (i = 0;i 1 && parts1[0] == "300") {
        var parts2 = parts1[1].toString().split(" ");

        address = (parts2[0].substring(0,parts2[0].length-1)).split("/");
        var level = parts2[1].split("=");
        if (parseInt(level[1]) == 0) {
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' OFF');}
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' 0%');}
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'OFF' );
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , '0' );
          eventEmitter.emit('level',address[3]+'/'+address[4]+'/'+address[5],0);
        } else {
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' ON');}
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' '+Math.round(parseInt(level[1])*100/255).toString()+'%');}
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'ON' );
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , Math.round(parseInt(level[1])*100/255).toString() );
          eventEmitter.emit('level',address[3]+'/'+address[4]+'/'+address[5],Math.round(parseInt(level[1])));

        }
      } else {
        var parts2 = parts1[0].toString().split(" ");
        if (parts2[0] == "300") {
        address = (parts2[1].substring(0,parts2[1].length-1)).split("/");
        var level = parts2[2].split("=");
        if (parseInt(level[1]) == 0) {
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' OFF');}
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' 0%');}
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'OFF' );
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , '0' );
          eventEmitter.emit('level',address[3]+'/'+address[4]+'/'+address[5],0);
        } else {
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' ON');}
          if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' '+Math.round(parseInt(level[1])*100/255).toString()+'%');}
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'ON' );
          client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , Math.round(parseInt(level[1])*100/255).toString() );
          eventEmitter.emit('level',address[3]+'/'+address[4]+'/'+address[5],Math.round(parseInt(level[1])));

        }

        }
      }
    }
  }
});
// Add a 'data' event handler for the client socket // data is what the server sent to this socket event.on('data', function(data) { var parts = data.toString().split(" "); if(parts[0] == "lighting") { address = parts[2].split("/"); switch(parts[1]) { case "on": if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' ON');} if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' 100%');} client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'ON' ); client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , '100' ); break; case "off": if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' OFF');} if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' 0%');} client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/state' , 'OFF' ); client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , '0' ); break; case "ramp": if(parseInt(parts[3]) > 0) { if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' ON');} if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' '+Math.round(parseInt(parts[3])*100/255).toString()+'%');} client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/status' , 'ON', function() {}); client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , Math.round(parseInt(parts[3])*100/255).toString(), function() {}); } else { if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' OFF');} if (logging == true) {console.log('C-Bus status received: '+address[3] +'/'+address[4]+'/'+address[5]+' 0%');} client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/status' , 'OFF' ); client.publish('cbus/read/'+address[3]+'/'+address[4]+'/'+address[5]+'/level' , '0' ); } break; default: } } });

The modifications are based on the reading of the C-Gate docs.
Basically I added some stuff on connect to C-Gate..  e.g. the lines:

  • PROJECT LOAD
  • PROJECT USE
  • NET OPEN
  • PROJECT START

The remainder of the modifications were around device control, Basically you now send
ON Network/Application/Group
or
OFF Network/Application/Group
to the C-Gate and it turns on the appropriate C-Bus Group.  The original code prefixed each command with the cbusname.. I also removed some of the parameters on the mqtt client.publish statements as they caused inconsistencies in my setup.

modified settings.js

//cbus ip address
exports.cbusip = '127.0.0.1';


//cbus project name
exports.cbusname = "HOME";
exports.cbusnetwork = "254";

//mqtt server ip:port
exports.mqtt = '127.0.0.1:1883';

//username and password (unncomment to use)
//exports.mqttusername = 'user1';
//exports.mqttpassword = 'password1';


//logging
exports.logging = true;

The only difference here was adding the cbus network variable.


Next was adding the MQTT config in HomeAssistant (configuration.yaml).  This points at the IP address where you installed Mosquitto..

mqtt:
  broker: 192.168.X.X
  port: 1883
  client_id: home-assistant-1
  keepalive: 60
  protocol: 3.1

This just allows MQTT to be used, it doesn't define anything to switch on / off...

I control some lights as follows (existing HUE lughts, then the C-Bus Lights):

light:
  - platform: hue
    host: 192.168.X.X
  - platform: mqtt
    name: Entrance (CBUS)
    state_topic: "cbus/read/254/56/2/state"
    command_topic: "cbus/write/254/56/2/switch"      
  - platform: mqtt
    name: Outside Front (CBUS)
    state_topic: "cbus/read/254/56/3/state"
    command_topic: "cbus/write/254/56/3/switch"  
  - platform: mqtt
    name: Lounge (CBUS)
    state_topic: "cbus/read/254/56/4/state"
    command_topic: "cbus/write/254/56/4/switch"

The command_topic / state topic contain the C-Bus Network/Application/Group being controlled

You can also add "Switches" rather than lights
switch:
  - platform: mqtt
    name: Mozzie Zapper (CBUS)
    state_topic: "cbus/read/254/56/11/state"
    command_topic: "cbus/write/254/56/11/switch"  

The control on these is 2-way, so if i turn on a C-Bus light with the wall switch, the change is reflected in HomeAssistant.

To get the Network/Application/Group list for my C-Bus setup:
Look in the C-Bus Toolkit..

The Network number is displayed at the top.  The Application number is displayed on the Application properties.
The Group Address is displayed on the Group to be controlled.


Starting all at boot:

C-Gate started as per this post
http://addictedtopi.tumblr.com/post/96351714013/installing-c-gate-on-a-raspberry-pi

The node.js code is loaded at boot on the debian box (jessie) by the forever module..
after doing an "npm install -g" to load the forever stuff for node.js, i had to create a symlink

ln -s /usr/bin/nodejs /usr/bin/node

or forever doesn't work (it looks for node, not nodejs).

Startup script /etc/init.d/nodeup

#!/bin/sh
#/etc/init.d/nodeup

export PATH=$PATH:/usr/local/bin
export NODE_PATH=$NODE_PATH:/usr/local/lib/node_modules

case "$1" in
  start)
  exec forever start --sourceDir=/usr/local/bin/nodejs -p /root/.forever/ index.js
  ;;
stop)
  exec forever stop --sourceDir=/usr/local/bin/nodejs index.js
  ;;
*)
  echo "Usage: /etc/init.d/nodeup {start|stop}"
  exit 1
  ;;
esac

exit 0

Nothing special, just start the init script at boot as per usual..


The future...
Oneday I might modify the node.js code to discover all C-Bus applications and devices, but for now I can control my lights from anywhere and life is good!

9 comments:

  1. Hey James,

    How do you physically connect the machine that runs C-gate to the C-Bus network?

    ReplyDelete
    Replies
    1. I used one of these... https://www.clipsal.com/products/detail?catno=5500CN2
      (there was a load lots cheaper on ebay).

      Delete
  2. Hi - thanks for posting the above, it saved me a bit of time. Just wondering if line 138 is corrupt or not? nodejs threw an error on it.

    ReplyDelete
    Replies
    1. Yeah, line 138 looks like a copy paste issue.

      Delete
    2. It was / is. I did link to the original source, you can compare / fix :) I tried to edit but blogger hates me..

      Delete
  3. Hey James - using your excellent instructions above I managed to get CBus going on a large network. However, I am having some problems which is explained here: https://github.com/the1laz/cgateweb/issues/20 . I needed to use the cgateweb from the1laz as the version above didn't work for me (version of cgate?) so I posted the issue on his Github. I am still waiting to hear a response (only put it up yesterday) but I was wondering if you might have an idea on what is going on? Interested in your thoughts. Thanks. Locky.

    ReplyDelete
    Replies
    1. Hey, I stopped using cgate / cbus a while ago so haven't looked at it much since I published the above. Apologies that I can't help more.

      Delete
  4. Hi James, I'm going through a similar journey to what you have been through. I was thinking that ideally, these sort of fixes should go back into the cgateweb project on github. I'm just wondering what the barriers are and whether, assuming I get it working, I should go to the effort of forking the original code. I'm not really even sure of the process, but it's all learning :-)

    ReplyDelete