-
1Keypad 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:
-
2Wiring 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.
-
3ESP32S3-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 deviceWill 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 }
-
4Home Assistant Code
Once MQTT is set up in Home Assistant, set up a sensor to listen for the LCD changes
mqtt: sensor: - name: "Alarm Status" state_topic: "esp32/alarm"
Home Assistant Dashboard Card Code for Virtual Keypad. The zones must be added and set up unique to each system with zone sensors as per original project description
type: vertical-stack cards: - type: markdown content: |- <font size="4">{{ states.sensor.alarm_status.state}} </font> - square: false type: grid cards: - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '1' icon: mdi:numeric-1-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-2-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '2' - show_name: false show_icon: true type: button icon: mdi:numeric-3-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '3' - show_name: false show_icon: true type: button icon: mdi:numeric-4-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '4' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '5' icon: mdi:numeric-5-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-6-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '6' - show_name: false show_icon: true type: button icon: mdi:numeric-7-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '7' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '8' icon: mdi:numeric-8-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-9-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '9' - show_name: false show_icon: true type: button icon: mdi:asterisk-circle-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '*' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '0' icon: mdi:numeric-0-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:pound-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '#' - show_name: false show_icon: true type: button icon: mdi:door-closed-lock show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: STAY - show_name: false show_icon: true type: button icon: mdi:door-open show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: AWAY - show_name: false show_icon: true type: button icon: mdi:sleep show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: NIGHT - show_name: false show_icon: true type: button icon: mdi:arrow-left-bold show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: < - show_name: false show_icon: true type: button icon: mdi:bell show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: CHIME - show_name: false show_icon: true type: button icon: mdi:arrow-right-bold show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '>' columns: 3 - type: entities entities: - entity: binary_sensor.front_door - entity: binary_sensor.zone2 - entity: binary_sensor.alarm_triggered icon: mdi:alarm-bell name: Alarm Triggered title: Zones state_color: true show_header_toggle: false
-
5Zone Setup
This section highly depends on how each zone label is programmed on the individual keypad.
It also requires the auto scroll function for faulted zones to be enabled in the keypad settings.
To integrate the zones, create a binary sensor for each zone that responds to the keypad LCD displaying that particular zone is faulted as the faulted zones are scrolled.
For example, for a door on zone 1:
- binary_sensor: - name: Zone01 state: > {% if is_state('sensor.alarm_status', ('Front Door 01 <> '),) %} on {% else %} off {% endif %}
('Front Door 01 <> ') may display differently depending on your keypad depending zone labeling. It is best to listen in on the MQTT service to topic esp32/alarm, fault the zone, and copy and paste it exactly as it is received into your code.
However, this will turn the sensor on as "Front Door 01 <> " flashes on the keypad lcd screen but then it will turn the sensor off when the lcd displays something else even if the door is still open. Therefore the sensor blinks on/off while the door is open, which is not ideal.
We need a sensor that latched on while the door is open and then turn off when the door is closed. To latch the state add a corresponding Input Boolean and use automations to latch them on when the binary sensor is detected turning on, and off when either "System Is Ready to Arm" is displayed or when the binary sensor doesn't turn back on for at least 6 seconds.
The 6 second rule addresses when multiple zones are triggered, as Ready to Arm will not display with other zones faulted, but the zone at issue may have been restored while other zones are still faulted. This way if the automation doesn't re-see the zone within 6 seconds in the scrolling list of faulted zones it assumes its closed. You can change this time to any length but I found 6 seconds worked well for many zones being faulted. A more precise method would be to have the code check between the displaying of "Scroll to View <> Open Zones" to see if the zone re-appears in the scrolling list as that message signifies the start/end of the scrolling list.
You should also set up a bypass sensor that tells if the keypad has entered the bypass zone selection section of the menu or not. This way the virtual keypad doesn't show the zones are triggered as you are scrolling through the list of zones to bypass (as the same descriptions will appear on the lcd. The code would look like:
An example of the the input booleans for one zone and bypass flag as follows:
Configuration.yaml:
input_boolean: zone01: name: Front Door 01 icon: mdi:electric-switch zonebypassmode: name: Bypassmode Flag icon: mdi:electric-switch
Automations.yaml:
- id: '1234567890' alias: Zone 01 Front Door description: Front Door Zone 1 trigger: - platform: state entity_id: - binary_sensor.zone01 from: to: 'on' condition: - condition: state entity_id: input_boolean.zonebypassmode state: 'off' action: - service: input_boolean.turn_on data: {} target: entity_id: input_boolean.zone01 - wait_for_trigger: - platform: state entity_id: - binary_sensor.zone01 to: 'off' for: hours: 0 minutes: 0 seconds: 6 - platform: state entity_id: - sensor.alarm_status to: 'System is Ready to Arm ' - service: input_boolean.turn_off data: {} target: entity_id: input_boolean.zone01 mode: single - id: '0987654321' alias: Zone Bypass On description: '' trigger: - platform: state entity_id: - sensor.alarm_status to: 'Zone Bypass <>(*) To Bypass ' from: 'Press (*) for <>Zone Bypass ' condition: - condition: state entity_id: input_boolean.zonebypassmode state: 'off' action: - service: input_boolean.turn_on data: {} target: entity_id: input_boolean.zonebypassmode - wait_for_trigger: - platform: state entity_id: - sensor.alarm_status to: 'System is Ready to Arm ' - service: input_boolean.turn_off data: {} target: entity_id: input_boolean.zonebypassmode mode: single
Lastly I added another binary sensor to the config that tracks the state of the latching input Boolean. I do this because I don't want the on/off toggle switches on my zones and I want to utilize the various device classes of the binary sensors to show if it is a door, window, motion, garage, etc. There is probably a more elegant way to code this all using variables, but it works.
- binary_sensor: - name: Front Door Zone 01 device_class: door state: "{{ states('input_boolean.zone01') }}"
When programming motion sensors you will want the latch to stay on for a set amount of time. I found especially the wired motion sensors open and close so fast if you don't set the latch to a longer period you wont see them being triggered in your list of zones - although your automations will still be triggered.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.