Philippe Libioulle
Published © GPL3+

Insteon Gateway

An Insteon-to-IP gateway without dependency to Insteon Cloud.

IntermediateFull instructions provided8 hours5,648
Insteon Gateway

Things used in this project

Story

Read more

Schematics

Layout

Code

Arduino sketch

Arduino
#include <ArduinoJson.h>
#include <avr/wdt.h>
#include <Ethernet.h>
#include <LiquidCrystal.h>
#include <Network.h>
#include <PubSubClient.h>
#include <QueueArray.h>
#include "SoftwareSerial.h"
#include <SPI.h>
#include <Timer.h>

// HW pinout
const int LCD_E_PIN PROGMEM = 2;                                             // LCD
const int RESET_BTN_PIN PROGMEM = 3;                                         // goes to LOW when someone press on the button and then goes to HIGH when button is released, otherwise pull-down to GND
const int SD_CARD_PIN PROGMEM = 4;                                           // SD card on ethernet shield, not used
const int LCD_DB4_PIN PROGMEM = 5;                                           // LCD
const int LCD_DB5_PIN PROGMEM = 6;                                           // LCD
const int LCD_DB6_PIN PROGMEM = 7;                                           // LCD
const int LCD_DB7_PIN PROGMEM = 8;                                           // LCD
const int LCD_RS_PIN PROGMEM = 9;                                            // LCD
const int INSTEON_TX PROGMEM = 16;                                           // TX to INSTEON PLM
const int INSTEON_RX PROGMEM = 17;                                           // RX from INSTEON PLM

// Network
const byte mac[] = { 0xDE, 0xED, 0xBE, 0xBB, 0xFC, 0xAC };                   // Arduino's MAC address
IPAddress ip(192, 168, 1, 221);                                              // Arduino's IP address
IPAddress server(192, 168, 1, 100);                                          // MQTT broker's address  (Orechestrator)
EthernetClient ethClient;

// MQTT 
PubSubClient client(ethClient); 
const int mqttInterval PROGMEM = 300;                                        // determines how often the system will report to MQTT broker (ie. every mqttInterval * mainLoopDelay ms )
int mqttIntervalCnt = 0;                                                     // local variable used to count down
int isConnectedToBroker = -1;                                                // 1 when connected, -1 = unknown, 0 = unable to connected
#define INSTEONGATEWAY "PLM"

// Insteon 
SoftwareSerial InsteonSerial(INSTEON_RX, INSTEON_TX, true);                 // This is what we use to send/receive Insteon commands using the home automation shield
const int PLMtimeOut PROGMEM = 1000;                                        // in millisec 
int inByte[26]= {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};      // for storing incoming serial bytes
int outByte[26]= {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};     // for storing ougoing serial bytes
const int zeroByte PROGMEM= 0x00;                                           // I use this because if I do "Serial.write (0);" instead I get a compile error
const int startByte PROGMEM = 0x02;                                         // Every Insteon message begins with this
const int msgLength PROGMEM = 30;                                           // Used to assign an expected length for the message... starts out high so that we don't prematurely trigger an end-of-message
int i = 0;                                                                  // Looping variable for incoming messages
int j = 0;                                                                  // Looping variable for outgoing messages

// Display and log
const int mode PROGMEM = 1;                                                 // 0 = normal, 1 = verbose
LiquidCrystal lcd (LCD_RS_PIN, LCD_E_PIN, LCD_DB4_PIN, LCD_DB5_PIN ,LCD_DB6_PIN, LCD_DB7_PIN); 
const int LCD_ROWS = 2;  
const int LCD_COLUMNS = 16;  

// Local queue
volatile QueueArray <byte> cmdQueue;                                        // each command received from MQTT broker is stored here
byte* incoming;                                                             // an incoming message to route to Insteon network

// Manual RESET button
volatile boolean isResetRequested = 0;                                      // this one will change when button triggers an interrupt

// Logic
const int mainLoopDelay = 500;                                              // a fixed delay within main loop, in ms
void(* resetFunc) (void) = 0;

// Initialization 
void setup()
{
  wdt_disable();                                                   // disable watchdog
  Serial.begin(19200);                                             // open console port 
  Serial.println(F("Begin of setup"));  
  pinMode(SD_CARD_PIN, OUTPUT);                                    // always good to disable it, if it was left 'on' or you need init time
  digitalWrite(SD_CARD_PIN, HIGH);                                 // to disable SD card since we do not use it  
  pinMode (RESET_BTN_PIN, INPUT);                                  
  InsteonSerial.begin(19200);                                      // open another serial port for Insteon PLM
  cmdQueue.setPrinter (Serial);                                    // setup local queue
  lcd.begin(LCD_COLUMNS, LCD_ROWS);                                // set up the LCD's number of columns and rows:
  testLCD();                                                       // just a welcome message
  client.setServer(server, 1883);                                  // setup MQTT
  client.setCallback(MQTTBrokerCallback);
  Ethernet.begin(mac, ip);                                         // setup network
  Serial.print(F("Current IP is : "));
  Serial.print(Ethernet.localIP());  
  Serial.print(F(" - MQTT broker IP is : "));
  Serial.println(server);  
  enableInterruptOnResetButton();                                  // from here, interrupts are trapped 
  subscribeToToDoLists();                                           // from here, callbacks are trapped 
  delay(1500);  // allow hardware to sort itself out  
  Serial.println(F("End of setup")); Serial.println();      
}

// Main loop

void loop()
{  
  if (isResetRequested == 1) {resetAll();}    // someone pressed the Reset button 
  
  // Part 1 : deal with messages originating from Insteon PLM
  
  while (InsteonSerial.available() > 0)       
  {      
     if (receiveMessageFromPLM() == 0) // message received and compliant with message type definition
     {   
        Serial.println();    
        if (inByte[1] == 0x50 && ( (inByte[8] & 0b11100000) == 192) && inByte[9] != 6 ) // we do not support all message types currently, just group broadcast messages
        {
           displayInsteonGroupBroadcast();         
           routeGroupBroadcastMessageToBroker();   
           Serial.println(); 
        }
        else
        {           
           Serial.println(F("Message type not currently supported."));  // See http://www.madreporite.com/insteon/receiving.html
           Serial.println(); 
        }
     };          
  };
  
  // Part 2 : deal with messages originating from MQTT Broker. Those messages are waiting in a local queue

  while (!cmdQueue.isEmpty ())
  {
     if (parseMessage() == 0)
     {
        sendMessageToInsteonDevice();  
     };
  }
   
  // Part 3 : report device status to MQTT broker
  
  if (mqttIntervalCnt == 0)
  { 
      Serial.println(F("Reporting gateway status to MQTT Broker..."));  
      routeGatewayStatusMessageToBroker();        
      mqttIntervalCnt = mqttInterval;
      Serial.println();
  }    
  else
  {                   
      mqttIntervalCnt = mqttIntervalCnt - 1;      
  }       
  // Take some rest
  delay(mainLoopDelay / 2 ); 
  client.loop();      
  delay(mainLoopDelay / 2);    
}

// Parse a message stored in the local queue

int parseMessage()
{
  byte b;
  int hex_digit_to_read = 2;
  bool reading_hex = true;
  byte hex = 0;
  for(int j=0;j<26;j++) outByte[j] = 0xFF;
  while (!cmdQueue.isEmpty ())
  { 
     b = cmdQueue.dequeue(); 
     if (b == ':')   // delimiter between each set of 2 characters 
     {
         hex = 0;
         hex_digit_to_read = 2;
         reading_hex = true;
         continue;
     };
     if (hex_digit_to_read > 0)  
     {
        hex = (hex << 4) | CharToHex(b);
        hex_digit_to_read--;
     };
     if (reading_hex && hex_digit_to_read == 0)
     {
        reading_hex = false;
        if (hex == 0x02)   // end of message
        {
          hex_digit_to_read = 2;
          reading_hex = true;
          hex = 0;
          j = 0;
          return 0;
        }
        else
        {
          outByte[j] = hex;
          Serial.print(hex, HEX);
          Serial.print(' ');
          j++;
        };
     };     
  };
}
// Send commands to Insteon devices

int sendMessageToInsteonDevice()
{
   Serial.print(F("Sending command to Insteon devices : "));   
   sendCmd(2, false);
   sendCmd(98, false);
   sendCmd(outByte[1], true);   
   sendCmd(outByte[2], true);   
   sendCmd(outByte[3], true);   
   sendCmd(15, false);   
   sendCmd(outByte[4], false);   
   sendCmd(outByte[5], false); 
   Serial.println();         
}

void sendCmd(byte b, bool isHex)
{
   if (isHex) { Serial.print(b, HEX); } else { Serial.print(b); };
   Serial.print(F("-"));
   InsteonSerial.write(b);
}

// Gather bytes sent by Insteon PLM in order to get a well structured message

int receiveMessageFromPLM()
{
   long start_time = millis();  
   int currentIndex = 0;    
   for(int j=0;j<26;j++) inByte[j] = 0;
   byte currentByte;        
   while (true)
   {
      if ((millis() - start_time) > PLMtimeOut)  // we should get a complete message in a short period of time
      {
         displayError1(); 
         return 1;  
      };
      if (InsteonSerial.available() > 0)
      {
         if (currentIndex == 0) 
         {
            Serial.print(F("### New message entering : ")); 
         }        
         currentByte = InsteonSerial.read();
         inByte[currentIndex] = currentByte;
         displayRawData(currentByte);
         if (currentIndex == 0 && currentByte != startByte)  // a new message should always start with the specified start byte
         {
            // displayError2(currentByte); 
            return 2;
         };           
         if (currentIndex > 11)   // message looks longer than expected
         {            
            return 4; 
         };       
         if (currentIndex == 10) // message has been received as expected
         {
            return 0;  // full message received
         }; 
         currentIndex = currentIndex + 1;  // just keep going with parsing
      };
   };   
}

// Route a command issued by MQTT Broker to Insteon devices

void routeGroupBroacastToInsteon()
{   
   // displayInsteonOutgoingGroupBroadcast();
   for (int i=0;i<26;i++)
   {     
      if ( outByte[i] == 0xFF) { break;}
      Serial.print(InsteonSerial.write(outByte[i]));
   }        
}

// Reset button management

void resetAll()
{
    Serial.println(F("Someone pushed on the button to reset this device")); 
    displayInfo("Reset requested");
    routeGatewayStatusMessageToBroker();	
    wdt_enable(WDTO_1S); //enable watchdog, will fire in 1 second   
    delay(5000); 
    Serial.println(F("This message should never appear... unless this board is a zombie"));
}
void enableInterruptOnResetButton()
{
    isResetRequested = 0;
    attachInterrupt(1, onResetRequested, CHANGE);
}
void onResetRequested()
{
    detachInterrupt(1);
    isResetRequested = 1;   
}

//
// MQTT related functions
//
// NOTE : MQTT_MAX_PACKET_SIZE = 128 bytes.. therefore not more than 100 for the payload !!!!!!!
//        unless you change it in /Arduino/libraries/pubSubClient/src/PubSubClient.h

// Route broadcasts to MQTT broker (topic = GLI/XX-YY-ZZ/WhishList)

void routeGroupBroadcastMessageToBroker()
{
   char topic[30];
   strcpy(topic, "GLI");  
   strcat(topic, "/");
   strcat(topic, byteToHexaString(inByte[2]));
   strcat(topic, "-");
   strcat(topic, byteToHexaString(inByte[3]));
   strcat(topic, "-");
   strcat(topic, byteToHexaString(inByte[4]));
   strcat(topic,"/");
   strcat(topic, "WhishList");   
   char payload[100];
   strcpy(payload, "{"); 
   strcat(payload,"\n"); 
   strcat(payload,"\"Group\": ");
   strcat(payload, byteToString(inByte[7]));  
   strcat(payload, ",");
   strcat(payload,"\n"); 
   strcat(payload,"\"Command\": ");
   strcat(payload, byteToHexaString(inByte[9]));
   strcat(payload, ",");
   strcat(payload,"\n"); 
   strcat(payload,"\"Parameters\": ");
   strcat(payload, byteToString(inByte[10]));
   strcat(payload,"\n"); 
   strcat(payload,"}");    
   publishMsg( topic, payload);  
}

// Route gateway status to MQTT broker (topic = GLI/Gateway/Status)

void routeGatewayStatusMessageToBroker()
{
   char topic[30];
   strcpy(topic, "GLI");  
   strcat(topic, "/");  
   strcat(topic, "Gateway");   
   strcat(topic,"/");
   strcat(topic, "Status");   
   char payload[100];
   strcpy(payload, "{"); 
   strcat(payload,"\n"); 
   strcat(payload,"\"ManualReset\": ");
   strcat(payload,"\n"); 
   if (isResetRequested == 1) { strcat(payload, "Yes");} else {strcat(payload, "No");};     
   strcat(payload,"\n"); 
   strcat(payload, ",");
   strcat(payload,"\n"); 
   strcat(payload,"\"LocalQueueLevel\": ");
   // strcat(payload, intToString(cmdQueue.count));  
   strcat(payload,"}"); 
   publishMsg( topic, payload);  
}

// Subscribe to the MQTT broker to get list of commands to forward to Insteon devices (topic = GLI/Gateway/ToDo)

void subscribeToToDoLists()   
{ 
  if (connectToBroker() == true)
  {   	
	   char topic[50];
	   strcpy(topic, "GLI");  
     strcat(topic, "/");
     strcat(topic, "Gateway");  
     strcat(topic,"/");
     strcat(topic, "ToDo");  
	   client.subscribe(topic);                  // otherwise subscriptions will growth forever..
     if (client.subscribe(topic) == true)
     {
        isConnectedToBroker = 1;    
        Serial.print(F("Registred to MQTT broker as a subscriber for the following topic: ")); 
        Serial.println(topic);
     }
     else
     {     
        Serial.println(F("Not registred to MQTT broker as a subscriber")); 
        isConnectedToBroker = 0;
     }    
     client.loop();          
  }
  else
  {
    isConnectedToBroker = 0;
	  Serial.println(F("Cannot subscribe to any topic since connection to MQTT broker is not established"));
  } 
}

// This function will be invoked by MQTT broker each time a message is added to GLI/Gateway/ToDo queue

void MQTTBrokerCallback(char* subscribedTopic, byte* payload, unsigned int msgLength)
{
  Serial.print(F("New message received from MQTT broker. Topic = "));
  Serial.print(subscribedTopic);
  Serial.print(F(", Length = "));
  Serial.println(msgLength);
  cmdQueue.enqueue('0');
  cmdQueue.enqueue('2');
  for (int i=0;i<msgLength;i++)
  {     
     cmdQueue.enqueue(payload[i]);   // store msg in local Queue     
  }  
  Serial.println();    
}

// Report to MQTT broker

void publishMsg(const char* topic, const char* payload)  
{
  if (connectToBroker() == true)
  {          
     if (client.publish( topic, payload ) == true)
     {
        isConnectedToBroker = 1;    
        Serial.print(F("Message sent to MQTT broker using the following topic "));  
        Serial.println(topic);
     }
     else
     {     
        Serial.print(F("Message NOT sent to MQTT broker using the following topic "));  
        Serial.println(topic);
        isConnectedToBroker = 0;
     }    
     client.loop();       
  }
}

// Manage connection with MQTT broker

int connectToBroker() 
{
  // Serial.println(F(""));        
  // Serial.print(F("Connecting to network and to MQTT Broker... "));     
  if (client.connect(INSTEONGATEWAY) == true)
  {        
    // Serial.print(F("connected as ")); 
    // Serial.println(INSTEONGATEWAY);
  } 
  else
  {
     switch (client.state())
     {
        case -4:
          Serial.println(F("MQTT_CONNECTION_TIMEOUT - the server didn't respond within the keepalive time"));
          break;
        case -3:
          Serial.println(F("MQTT_CONNECTION_LOST - the network connection was broken"));
          break;
        case -2:
          Serial.println(F("MQTT_CONNECT_FAILED - the network connection failed"));
          break;
        case -1:
          Serial.println(F("MQTT_DISCONNECTED - the client is disconnected cleanly"));
          break;
        case 0:
          break;
        case 1:
          Serial.println(F("MQTT_CONNECT_BAD_PROTOCOL - the server doesn't support the requested version of MQTT"));
          break;
        case 2:
          Serial.println(F("MQTT_CONNECT_BAD_CLIENT_ID - the server rejected the client identifier"));
          break;
        case 3:
          Serial.println(F("MQTT_CONNECT_UNAVAILABLE - the server was unable to accept the connection"));
          break;
        case 4:
          Serial.println(F("MQTT_CONNECT_BAD_CREDENTIALS - the username/password were rejected"));
          break;
        case 5:
          Serial.println(F("MQTT_CONNECT_UNAUTHORIZED - the client was not authorized to connect"));
          break;
        default:
          Serial.print("failed, rc=");
          Serial.println(client.state()); 
          break;        
     }
  }
  return client.connected();    
}

//
// LCD management
//


void displayInsteonGroupBroadcast()
{
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print('G');
   lcd.setCursor(2,0);
   lcd.print(inByte[2], HEX);  
   lcd.print('-');
   lcd.print(inByte[3], HEX);   
   lcd.print('-');
   lcd.print(inByte[4], HEX);  
   lcd.setCursor(0,1);   
   lcd.print("CMD");
   lcd.setCursor(5,1);   
   lcd.print(inByte[9], HEX); 
   lcd.setCursor(8,1);
   lcd.print("TO ");
   lcd.setCursor(11,1);
   lcd.print(inByte[7]); 
   delay(1000);
}

void displayInsteonOutgoingGroupBroadcast( const char* cmd, const char* dest, const char* parms )
{
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print('I');
   lcd.setCursor(2,0);
   lcd.print('MQTT BROKER');
   lcd.print("CMD");
   lcd.setCursor(5,1);   
   lcd.print(cmd); 
   lcd.setCursor(8,1);
   lcd.print("TO ");
   lcd.setCursor(11,1);
   lcd.print(dest); 
   delay(1000);
}
void displayBrokerMessage()
{    
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print('X');   
   delay(500);   
}
void displayError(const String& code, const String& message)
{    
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print('E');
   lcd.setCursor(0,2);
   lcd.print(code);
   lcd.setCursor(0,1);
   lcd.print(message);
   delay(500);
}
void displayInfo(const String& info)
{
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print(info);
   delay(500);
}
void testLCD()
{
   lcd.clear();
   lcd.setCursor(0,0);
   lcd.print("Hello PHSA !");
   delay(500);
}
void displayRawData(byte b)
{
   if (mode == 1)
   {
      Serial.print(F("["));
      Serial.print(b, HEX);   
      Serial.print(F("]"));
   };
}
void displayError1()
{
   if (mode == 1)
   {
      Serial.print(F("An error (type 1) has occured while parsing message from PLM : communication has timed out"));   
   };
   displayError("1","Parsing");
}
void displayError2(byte currentByte)
{
   if (mode == 1) 
   { 
      Serial.print(F("An error (type 2) has occured while parsing message from PLM : ["));  
      Serial.print(currentByte, HEX);
      Serial.println(F("] skipped since it does not start with 0x20 as per the standard"));
   }; 
   displayError("2","Structure");
}



// Utility functions


char* byteToHexaString(byte b)
{
    char buff[4];
    snprintf(buff, 4, "%02X", b);
    return buff;
}

char* byteToString(byte b)
{
    char buff[4];
    snprintf(buff, 4, "%d", b);
    return buff;
}

char* intToString(int i)
{
    char buff[4];
    snprintf(buff, 4, "%d", i);
    return buff;
}

int doubleBytesToInt(byte high_byte, byte low_byte)
{
    return high_byte * 256 + low_byte; 
}

int StrToHex(char str[])
{
  return (int) strtol(str, 0, 16);
}

byte CharToHex(char c)
{
    byte out = 0;
    if( c >= '0' && c <= '9'){
        out = (byte)(c - '0');
    } 
    else if ( c >= 'A' && c <= 'F'){
        out = (byte) (c - 'A') + 10;
    }
    else if ( c >= 'a' && c <= 'f'){
        out = (byte) (c - 'a') + 10;
    }

    return out;
}

Credits

Philippe Libioulle

Philippe Libioulle

7 projects • 49 followers

Comments