Monday, 10 October 2016

Seeking Comfort

Embarking on the great home automation project of 2016, I wanted to get my openHAB installation talking to my legacy Comfort system.

I'd installed the comfort system 15 years ago, (it's a Comfort Pro), and it's been very reliable if somewhat basic since. At it's core, it's a home automation system that can function as a security system, telephone answering machine, lighting controller, infra-red blaster and a number of other tasks.

It's got a range of modular input and output systems and in my case, connects to the outside world via an RS232 serial interface. (USB and Ethernet options are also available but I don't have them). The system can be programmed and works on the basis of zones/inputs and responses so can be set up to, say, switch a light on if a particular sensor is tripped after dark.

I have window and door sensors connected up as well as a few PIRs as well as an X-10 interface for lighting control. There's a doorphone at the front door and the system is connected to the telephone line. This means that if we're not at home and the doorbell rings, we can set the system to call a mobile and we can speak to the person at the door. Neat.

Comfort has been updated over the years and version 2 has been out for a while with support for more modern protocols etc. but as an upgrade would require me to replace the entire system, I didn't bother.

Now with a renewed interest in home automation and a decision to pursue openHAB as the core, I needed to find a way to get Comfort and openHab linked up. I really don't want to throw out a perfectly good alarm system and have to rebuild it all again with new components just to get the same functionality. My ambition is to reuse and recycle what I have if it's fit for purpose.

Comfort can be programmed using the Comfigurator software tool. It's basic enough but it permits full programming of Comfort via it's UI. However, it's not real-time software in that it saves a configuration file that must be uploaded to the Comfort system. I needed something more refined.

In researching the issue, I came across this post in the Comfort forums that set off the lightbulb. User cab123 had set up a 'driver' in Perl to pass messages back and forth between Comfort and MQTT. MQTT is an IoT protocol  that happily is supported via a binding in openHAB so in this way, openHAB can message Comfort and all the Comfort sensors and actions can be sent through to openHAB. Additionally, he was using Node-RED to perform additional processing to the messages.

I'd already decided to run openHAB in a windows for a few reasons;
  • I couldn't get either of the openHAB unRAID dockers to work (didn't try too hard)
  • I wanted flexibility in the openHAB versions (v2 is currently in beta)
  • Comfigurator needs to run in Windows so it makes sense to use that VM for more than one task
  • I'd likely need to resurrect my M-Audio 410 8 channel card for whole house audio and it works best in windows

Happily, there are MQTT (Mosquito) and Node-Red dockers available for unRAID so I could get the infrastructure set up quite easily. Unfortunately, the sample driver was written for Linux but as I had my serial port passed into a Win8 VM (via a USB/Serial convertor), I'd need to hack it for Windows.

As the original script was written in Perl, I went ahead and installed Strawberry Perl in the windows VM and added Mosquitto for windows to facilitate issuing MQTT commands directly from the console. (this wasn't a straightforward install as there were lots of missing .dlls that needed to be hunted down afterwards).

With all that set up, I was able to run Perl scripts in my windows openHAB VM but of course the original script wasn't playing ball. It turned out I needed to use the Win32::Serialport Perl module. I ended up with a complete re-write of the driver code;



use Net::MQTT::Simple;
use Win32::SerialPort;
use IO::Select;
#use Time::HiRes qw(time);
use strict;
use warnings;

print STDERR "\n".localtime()."\nOpening Comfort Communications... \n";

#Set up & open  port
my $port = Win32::SerialPort->new("COM5") or die "Failed to open port: $!\n";
print STDERR "Port established: $port \n";


#set up mqtt & polling for inbound messages
my $mqtt_hostname = '';
my $mqtt_to_comfort = "comfort/to";
my $mqtt_from_comfort = "comfort/from";

my $mqtt = Net::MQTT::Simple->new($mqtt_hostname);

#additional control vars
my $serial_stream = ""; #container for serial input from comfort
my $ts = time(); #current time
my $defaultreadspeed = 1; #port read interval
my $trace = 1;#enable debug output to console

#setup complete
print STDERR "Starting to listen...\n";

while (1) { #main loop

  if ( time() - $ts >  $defaultreadspeed ) { #use $defaultreadspeed to throttle

      $ts = time(); #set target time for next loop
      $serial_stream = $port->read(200);  #read up to 200 bytes from Comfort

      if ($serial_stream) { #check if there's data read

          doTrace ("received from Comfort: $serial_stream");
          #split the data into an array based on STX or EOL
          #remove those extraneous characters from the resulting array elements
          my @reports = split /(?:\x03|\x0D\|x0A)/, $serial_stream;

          for my $rep (@reports) { #loop through each of the returned strings
            if (length($rep)){ #only process non-empty strings
              doTrace ("sending report to MQTT: $rep");
              $mqtt->publish($mqtt_from_comfort => $rep); #send to MQTT
              doTrace("empty string, not sending to MQTT");

      $port->lookclear; #needed to prevent blocking (?)
    } #end check if data read

    $mqtt->tick(1); #only query MQTT input if no serial data

    } #time check
}#while loop


sub callback { #process inputs from MQTT and send to Comfort
  my ($topic, $message) = @_;
  doTrace ("got message from MQTT:  $message");

  $port->lookclear; #needed to prevent blocking (?)
  my $str = chr(0x03).$message."\r\n"; #prepend STX char
  $port->write($str); #send to Comfort

  doTrace ("sent to Comfort: $str");

sub doTrace { #write trace logs to console
  my ($traceString) = @_;

  if ($trace){ #only implement if enabled
    my $timestamp = getLoggingTime(); #get timestamp
    print STDERR "$timestamp $traceString \n"; #print to console

sub getLoggingTime { #compose a decent looking timestamp

    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
    my $nice_timestamp = sprintf ( "%04d%02d%02d %02d:%02d:%02d",
    return $nice_timestamp;

This is quite different to the original but does follow the ideas and general principles of cab123's original and I couldn't have done it without that guide. Also, it's important to point out that there's a distinct lack of error checking here. The above is bare-bones functionality and could benefit from several layers of checking.

I can test this by issuing a command like this in Mosquitto;

mosquito_pub -h -p 1883 -t comfort/to -m "z?"

This causes the Comfort 'z?' query to be issued to the 'comfort/to' mqtt channel. This is picked up by the Perl driver which is monitoring that channel and send on via the serial connection to Comfort. Any responses from comfort are picked up by the Perl script, formatted correctly and send back to the 'comfort/from' channel.

You can this happening in the console running the Perl script here (I have the optional tracing switched on which is helpful for debugging);

(also here you can see some subsequent activity from Comfort - it's reporting activity on zone/input 05 - that's me opening and closing a window with a contact sensor attached).

I set up Node-RED to monitor the mqtt channels and output to the debugger;

Next step is to set up openHAB to send and receive Comfort messages to MQTT. I can also use Node-RED to further process commands if necessary.

As a fun project to get this all set up, I plan to set up a TTS engine on the VM and have openHAB speak all of the Comfort sensor activity via my Squeezebox whole house audio setup. That should be an interesting family experiment. I wonder how long before I'm asked to turn it off!

No comments: