Close
0%
0%

DSC Neo Alarm Integration for Home Assistant

An ESP32 based virtual keypad integration for DSC Neo Alarm Systems

Similar projects worth following
The DSC Neo Alarm system uses an encrypted data bus that prevents third party home automation integrations from communicating directly with the panel.

I built an ESP32 based virtual keypad integration for the DSC Neo Alarm system which works by sniffing the keypad's lcd data bus and taking control of the the keypad's 5x5 button matrix.

Where it all started:

In late 2023, Chamberlain started blocking the MyQ garage door integration from Home Assistant. This move confirmed my concerns about relying on integrations that communicate through cloud based APIs. I begrudgingly overcame MyQ's blockade by soldering smart relays to the pushbuttons of a wired garage door opener and using contact sensors to receive the position status. However, MyQ left a sour taste in my mouth, and since I was also running the alarmdotcom HACS integration for my DSC Neo panel - an integration that warns it "communicates with Alarm.com over an unofficial channel that can be broken or shut down at any time" - I was determined to find a way to integrate my encrypted DSC Neo panel locally so that it would not become the next MyQ. 

Neo's encrypted bus blocks any direct integration from non-partners, but I learned from the MyQ blockade that if you can interact with a device physically, i.e. by reading a simple lcd screen and pushing some buttons, no matter how strong the encryption is between that device and its mothership, the information to and from your eyes and fingers must be in an unencrypted form that can then be intercepted or injected remotely. I focused on the keypads, and on building an ESP32 based virtual keypad integration that would sniff the Neo Keypad's lcd screen data bus and hijack the keypad's button matrix. I first documented my journey on home-assistant.io's forum, and now decided to document it here as I continue to refine this project.


Part 1 - Sniffing the LCD Databus:

I knew the H2SLCD Full Message Keypad contained a 16x2 lcd display. I suspected it contained the ubiquitous HD44780 LCD Controller.  On opening the keypad I discovered the lcd was on its own pcb which connects to the mainboard with an spongy flexible connector that when examined with a magnifier essentially contains a large number of parallel wires or plates sandwiched between two soft pads that connect the contacts on the mainboard  to the lcd when they are pressed together. 

I first mapped the contacts on the mainboard to the contacts on the lcd by simply looking where the two boards lined up across the connector. Then with my multimeter I started probing to find what I could. I was able to find a number of Gnd contacts, a +12V contact, a +3.3V contact, and a number of contacts reading varying + voltages between 0-3V.  I then turned to the various test points on the lcd pcb. I determined that 6 of the test points connected to an unused footprint labeled "IC3", leaving 12 contacts actively communicating with the mainboard. Of the 12 remaining test points, the first that were easily identified were the cathode/anode of the LCD backlight (labeled K/A at the top right of the board), Gnd, and +3.3V. That left 8 test points unknown. If I was dealing with an HD44780 LCD controller, it would have to be working in 4 bit mode, with the remaining 8 contacts likely being Vo (contrast control), RS (Register Select), RW (Read/Write), E (Enable), and D4, D5, D6, D7 (4 data lines).  

I ordered a 20$ USB logic analyzer off Amazon and started analyzing the signals. I used Logic 2's built in HD44780 analyzer and attempted to identify which signal might be which. I had a good idea which signals were likely RW, RS, and E based on the datasheet, but I was not sure the order of the 4 possible data lines. There could be 24 different combinations in the order of the 4 data lines, and through trial of error, I ran through all the combinations (I went through the 24 combinations a few times only to discover they were in the correct order on the pcb to begin with) and while changing various settings in the analyzer settings, eventually "\x80System Is \xC0Ready to Arm" appeared across the top of the E line! I had successfully decoded the LCD controller and was reading the keypads status with my logic analyzer. 

RW = white, RS = purple, E = brown, DB4-7...

Read more »

Gerber_Neb.zip

Carrier PCB Gerber Files

x-zip-compressed - 15.14 kB - 02/20/2024 at 07:01

Download

BOM_Neb.csv

Carrier PCB BOM File

Comma-Separated Values - 1.35 kB - 02/20/2024 at 07:01

Download

PickAndPlace_Neb.csv

Carrier PCB Pick and Place File

Comma-Separated Values - 5.29 kB - 02/20/2024 at 07:02

Download

  • 1
    Keypad Connection Points

    Connection points on keypad LCD pcb

    Connection Points on button Matrix (there are also corresponding connection points on the 100 pin micro controller on opposite side of pcb but requires very fine soldering). The solder points were chosen to cause least interference with the numpad and most used function buttons to allow continued use of physical keypad. Keep solder low and flat and wires running off to the side and away from center of button contacts to maintain physical keypad use:

  • 2
    Wiring Schematic

    Optocouplers = PC817

    Resistors = 210 Ohm

    Note the opposite direction of X vs Y connections to the optocouplers due to the opposite direction in current flow.


  • 3
    ESP32S3-DevkitC1 Code (BETA)

    With this code, the ESP32s3-devkit-c1 will first boot into AP mode. Connect to the ESP32 AP with WIFI and open a browser to http://192.168.4.1/ (will load automatically on most phones) to set up the WIFI login/pass and MQTT server login and password. Can re-enter AP mode at any time by pressing the BOOT button when running. Will have to have MQTT setup first in Home Assistant (recommend the Mosquitto Broker add on).

    On board RGB LED indicator status:

    Blue = WIFI and MQTT connected
    Flashing Purple = AP Setup Mode
    Flashing White = Attempting to connect to WIFI
    Flashing Yellow = Attempting to connect to MQTT
    Green = Rebooting device

    Will Attempt to connect to WIFI and MQTT for 30 seconds each and then reboot itself. 

    Can confirm the LCD decoder is working using serial monitor. 

    //ESP32 DSC NEO KEYPAD LCD DECODER & BUTTON PUSHER BY VALDEZ - NOV/2023
    //CODE IS DESIGNED FOR USE WITH ESP32S3-devkitc-1 AS PER SCHEMATIC
    //DECODER CODE MODIFIED FROM LEONARDO MARTINS 19/01/2019 HD44780 DECODER CODE
    //WIFI AP CONFIG USES ESP-WiFi Settings by Juerd - https://github.com/Juerd/ESP-WiFiSettings
    //MQTT communication USES PubSubClient by knolleary - https://github.com/knolleary/pubsubclient
    
    #include <PubSubClient.h>
    #include <Wire.h>
    #include <WiFi.h>
    #include <SPIFFS.h>
    #include <WiFiSettings.h>
    
    //DEFINE HD44780 DECODER INPUT PINS (RS, RW, EN, DB4-7)
    # define RS_PIN 2 // RS (Register Select) pin
    # define RW_PIN 42 // RW (Read/Write) pin
    # define EN_PIN 1 // EN (Enable) pin
    # define DATA_PINS {4, 5, 6, 7} // Data pins D4, D5, D6 and D7
    
    //Define Pushbutton Matrix Pins
    # define X1 10
    # define X2 11
    # define X3 12
    # define X4 13
    # define X5 14
    # define Y1 18
    # define Y2 8
    # define Y3 3
    # define Y4 46
    # define Y5 9
    
    # define BOOT 0 //boot button for switching wifi to AP config mode
    
    //DECLARE GLOBAL VARIABLES FOR HD44780 DECODER
    volatile byte data = 0b00000000; // Storage for data
    volatile bool highBits = true;  // flag to indicate high or low 4 bits being read 
    volatile bool dataReady = false; // Flag to indicate when data is ready to read
    volatile char dataBuffer[64]; //databuffer to store the lcd 32 char message
    volatile byte charCount = 0; // character counter
    
    //WIRELESS SETUP
    WiFiClient espClient;
    PubSubClient client(espClient);
    long lastMsg = 0;
    char msg[50];
    int value = 0;
    bool portalmode = false;
    int countx = 0;
    
    //MQTT SETUP
        String host;
        int mqttport;
        String mqttusername;
        String mqttpassword;
        IPAddress mqtt_IP; 
        bool xIP;
    
    //INTERRUPT FUNCTION FOR WIFI SWITCH TO AP Portal When Pushing BOOT button
    void IRAM_ATTR portal(){
    neopixelWrite(RGB_BUILTIN, 32, 0, 0); //change LED to red to indicate entering AP mode
    portalmode = true;
    }
    
    //MAIN PROGRAM INITAL SETUP
    void setup() {
      SPIFFS.begin(true); //intialize spiffs filesystem for storing wifi/mqtt password data
      Serial.begin(115200); // Start serial communication
      
      //sets up mqtt server settings from AP portal fields
        host = WiFiSettings.string("MQTT Server", "homeassistant.local");
        mqttport = WiFiSettings.integer("MQTT Port", 1883);
        mqttusername = WiFiSettings.string("MQTT username");
        mqttpassword = WiFiSettings.string("MQTT password","");
        xIP = mqtt_IP.fromString(host);
      
      neopixelWrite(RGB_BUILTIN,0,0,0); // initialize onboard led to off
      
      //initializes keypad matrix output pins
      pinMode(X1, OUTPUT);
      pinMode(X2, OUTPUT);
      pinMode(X3, OUTPUT);
      pinMode(X4, OUTPUT);
      pinMode(X5, OUTPUT);
      pinMode(Y1, OUTPUT);
      pinMode(Y2, OUTPUT);
      pinMode(Y3, OUTPUT);
      pinMode(Y4, OUTPUT);
      pinMode(Y5, OUTPUT);
      digitalWrite(X1, LOW);
      digitalWrite(X2, LOW);
      digitalWrite(X3, LOW);
      digitalWrite(X4, LOW);
      digitalWrite(X5, LOW);
      digitalWrite(Y1, LOW);
      digitalWrite(Y2, LOW);
      digitalWrite(Y3, LOW);
      digitalWrite(Y4, LOW);
      digitalWrite(Y5, LOW);
      attachInterrupt(BOOT, portal, HIGH); //attaches interrupt to enter WIFI AP Config mode when boot pushed
      decoderSetup(); //setup the LCD decoder
    }
    
    
    //MAIN PROGRAM LOOP
    void loop() {
      if (portalmode) {
        WiFiSettings.portal(); //puts controller into WIFI AP Portal mode for setting up wifi/mqtt
      } 
    
    //initialize or reconnect wifi if disconnected  
      if (!WiFi.isConnected()) {
        setup_wifi();
      }
    
     //initialize or reconnect mqtt if disconnected 
      if (!client.connected()) {
        reconnect();
      }
      
      client.loop(); //mqtt loop 
      dataSpill(); //checks LCD message changes 
    }
    
    //WIFI & MQTT SETUP FUNCTION
    void setup_wifi() {
    
        WiFiSettings.onSuccess  = []() { 
          countx = 0; //reset counter to track wifi connection attempts
          neopixelWrite(RGB_BUILTIN,0,0,32); }; 
        
        WiFiSettings.onFailure  = []() { 
            neopixelWrite(RGB_BUILTIN,32,0,0); //if wifi fails indicate with red LED and reboot
            Serial.println("WIFI FAILED");
            delay(5000);
            ESP.restart(); 
        };
        
        //background loops for while wifi is attempting to connect or entered AP mode
        WiFiSettings.onWaitLoop = []() {
            countx = countx+1;
            if (countx >= 30){
              ESP.restart(); // run the wifi wait loop for 30 secs and then reboot
            } else if(portalmode){
              WiFiSettings.portal();  // check if if boot button has been pushed to switch into AP portal mode
            }
            neopixelWrite(RGB_BUILTIN, 32,32,32); //blink LED white while attempting to connect to wifi at 1 sec interval
            delay(500);
            neopixelWrite(RGB_BUILTIN, 0,0,0);
            return 500; 
        };
            WiFiSettings.onPortalWaitLoop = []() {
            neopixelWrite(RGB_BUILTIN, 32,0,32); //when in AP setup mode blink the LED purple at 1 sec intervals
            delay(500);
            neopixelWrite(RGB_BUILTIN, 0,0,0);
            delay(500);
        };
        neopixelWrite(RGB_BUILTIN, 0,32,0); 
        delay(1000);
          if (portalmode) {
            WiFiSettings.portal();
            } else{
              WiFiSettings.connect(true, -1);
            }
    }
    
    //MQTT SUBSCRIBED TOPIC RESPONSE
    //PUSHES BUTTONS DEPENDING ON COMMAND RECEIVED
    void callback(char* topic, byte* message, unsigned int length) {
      Serial.print("Message arrived on topic: ");
      Serial.print(topic);
      Serial.print(". Message: ");
      String messageTemp;
    
      //PRINT OUT THE RECEIVED MSG TO SERIAL MONITOR
      for (int i = 0; i < length; i++) {
        Serial.print((char)message[i]);
        messageTemp += (char)message[i];
       }
        Serial.println("");
    
      //CODE TO CALL BUTTONPUSH CONDITIONAL ON THE MESSAGE RECEIVED   
       if (messageTemp == "1"){
          buttonPush(X1, Y1, 100);
        } else if (messageTemp == "2"){
          buttonPush(X2,Y1,100);
        } else if (messageTemp == "3"){
          buttonPush(X3,Y1,100);
        } else if (messageTemp == "4"){
          buttonPush(X1,Y2,100);
        } else if (messageTemp == "5"){
          buttonPush(X2,Y2,100);
        } else if (messageTemp == "6"){
          buttonPush(X3,Y2,100);
        } else if (messageTemp == "7"){
          buttonPush(X1,Y3,100);
        } else if (messageTemp == "8"){
          buttonPush(X2,Y3,100);
        } else if (messageTemp == "9"){
          buttonPush(X3,Y3,100);
        } else if (messageTemp == "*"){
          buttonPush(X1,Y4,100);
        } else if (messageTemp == "0"){
          buttonPush(X2,Y4,100);
        } else if (messageTemp == "#"){
          buttonPush(X3,Y4,100);
        } else if (messageTemp == "STAY"){
          buttonPush(X4,Y1,2000);
        }  else if (messageTemp == "AWAY"){
          buttonPush(X4,Y2,2000);
        } else if (messageTemp == "CHIME"){
          buttonPush(X4,Y3,2000);
        } else if (messageTemp == "F4"){ 
          buttonPush(X4,Y4,2000);
        } else if (messageTemp == "<"){
          buttonPush(X5,Y1,100);
        } else if (messageTemp == ">"){
          buttonPush(X5,Y2,100);
        } else if (messageTemp == "NIGHT"){
          buttonPush(X5,Y3,2000);
        }
       
     }
    
    //RECONNECT MQTT IF DISCONNECTED
    void reconnect() {
        //checks if boot has been pushed to enter AP mode first
        if (portalmode) {
        WiFiSettings.portal();
        } 
        
        countx = countx + 1; //increases counter each time it attempts a connection
    
        //if mqtt host is entered as IP address use IP, otherwise use domain name string entered  
        if (xIP) {
        client.setServer(mqtt_IP, mqttport);  
        } else {
        client.setServer(host.c_str(), mqttport);
        }
    
        client.setCallback(callback);
        Serial.print("Attempting MQTT connection...");
        neopixelWrite(RGB_BUILTIN,32,16,0); // turn on orange LED while attempting to connect MQTT
    
        // Attempt to connect with settings inputted in AP mode
        client.connect(WiFiSettings.hostname.c_str(), mqttusername.c_str(), mqttpassword.c_str());
      
        //attempt 15 connection attempts (30 seconds) then restart
        if (client.connected()) {
          Serial.println("connected");
          // Subscribe
          client.subscribe("esp32/output"); //topic subscribed to for button pushing messages
          neopixelWrite(RGB_BUILTIN,0,0,32); //if MQTT connects change LED to purple
          countx = 0;
        } else {
          if (countx >= 15) {
          ESP.restart();
          }
          Serial.print("failed, rc=");
          Serial.print(client.state());
          Serial.println(" try again in 2 seconds");
          // Wait 2 seconds while blinking orange 
          delay(500);
          neopixelWrite(RGB_BUILTIN,0,0,0);
          delay(500);
          neopixelWrite(RGB_BUILTIN,32,16,0);
          delay(500);
          neopixelWrite(RGB_BUILTIN,0,0,0);
          delay(500);
          
        }
      
    }
    
    //BUTTON PUSHING FUNCTION
    void buttonPush(uint8_t X, uint8_t Y, int time){
      neopixelWrite(RGB_BUILTIN,32,0,32); // blink purple each time a button is pushed
      digitalWrite(X, HIGH);
      digitalWrite(Y, HIGH);
      delay(time); //length of button push
      digitalWrite(X, LOW);
      digitalWrite(Y, LOW);
      neopixelWrite(RGB_BUILTIN,0,0,32);
      delay(100); //IF PUSHING MULTIPLE BUTTONS QUICKLY BY AUTOMATION NEED TO HAVE A DELAY IN BETWEEN
    }
    
    //INITIALIZATION OF HD44780 DECODER
      void decoderSetup() {
    
      //SETUP INPUT PINS
      pinMode(RS_PIN, INPUT);
      pinMode(RW_PIN, INPUT);
      pinMode(EN_PIN, INPUT);
    
      int dataPins[] = DATA_PINS; //setup input array for 4 bit parrallel data pins
      for (int i = 0; i < 4; i++) {
        pinMode(dataPins[i], INPUT);
      }
    
      // Setup the interrupt to read data when enable pin starts to rise
      attachInterrupt(digitalPinToInterrupt(EN_PIN), readData, RISING);
    
    }
    
    
    //FUNCTION TO READ THE DATA FROM WHEN THE ENABLE PIN TRIGGERS THE INTERRUPT 
    void readData() {
    
      // Check that data is being sent (RS == 1) and it is a write instruction (RW == 0)
      if ((digitalRead(RS_PIN) == HIGH) && (digitalRead(RW_PIN) == LOW)) {
        
        // store the 4-bit parallel data into the array
        int dataPins[] = DATA_PINS;
        byte part = 0b00000000; //reset the 8 bit byte 
        for (int i = 0; i < 4; i++) {
        part |= digitalRead(dataPins[i]) << i; //copy each bit in and shift the bits left for the next bit
        } 
     
        if (highBits) {
        // This is the high bits (xxxx0000) part of the 8-bit data
            
            if((part << 4) != 0b00000000){ //only allows non 0 data for the first 4 bits - corrects any timing issues
            data = part << 4; //shift the 4 bit part to the high end positions of the 8 bit byte
            }
            else{
            highBits = false; //if receives all 0s it will flip the highbit flag so that it checks for high bit data again 
            }
    
        } else {
        // This is the low bits part (0000xxxx) 
          data |= part; //copy the 4 bit part to the low 4 bit end of the 8 bit byte using bitwise 
          dataBuffer[charCount] = data; //store the full 8 bit character data byte in the buffer array
    
        // once 32 characters have been stored reset buffer to 1st char; otherwise increment the counter; prevents overflowing the array.      
            if (charCount < 31){
            charCount++;
            } else {
            charCount = 0;
            dataReady = true; //once 32 characters are stored, new message is ready
            }
    
        }
        
        // Flip the highBits flag to alternate inputting the 4 bits from the high bits to low bits
        highBits = !highBits;
        
         
      }
    }
    
    //FUNCTION TO WORK WITH THE STORED DATABUFFER AFTER INTERRUPT COMPLETES
    //PRINT OR SEND IT TO MQTT WHEN THE DATA READY FLAG IS TRUE
    void dataSpill(){
      if (!dataReady)
        return;
      dataReady = false; //set flag to false so message is not re-sent until next message arrives
      char *outputx = (char *) dataBuffer; // setup string pointing to the databuffer char array
      client.publish("esp32/alarm", outputx); //send the string to mqtt
    //  for(int i = 0; i < 32; i++){        //debugging purposes, prints the hex codes of each char to serial.
    // Serial.printf("%02x ", outputx[i] ); //debugging purposes, prints the hex codes of each char to serial .
    //  }                                   //debugging purposes, prints the hex codes of each char to serial.
    //  Serial.printf("\n");                //debugging purposes, prints the hex codes of each char to serial.
      for(int i = 0; i < 32; i++){
      Serial.print(outputx[i]); // prints the lcd message also to Serial Monitor
      }
      Serial.printf("\n");
      charCount = 0; //resync the databuffer for next message
    }

View all 5 instructions

Enjoy this project?

Share

Discussions

spekkio wrote 3 days ago point

Just wanted to mention I implemented this project with some help from Valdez and it has been great!  Used it to get my installer code too.

Pro tip, you can mute the beeps in the user menu.

  Are you sure? yes | no

Dan Maloney wrote 01/19/2024 at 01:28 point

Outstanding hack! As a former owner of a DSC system, and a current Home Assistant fan, this is pretty slick. I love the fact that the control panel provides a complete end run around DSC's encryption. Wrote this up for the blog, should publish soon. Thanks for posting this here!

  Are you sure? yes | no

Valdez wrote 01/19/2024 at 22:28 point

Thanks Dan. I look forward to reading it!

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates