0.5W is fine - probably come in a pack of 10
Is gonna be a few weeks before my gear gets delivered. hopefully, you guys can make some steady progress in the meantime.
I got my M5 devices from AliExpress in the end. Good prices and cheap delivery.
Digikey wanted $24 for delivery to WA.
I paid $33.3 inc delivery from AliExpress - should arrive sometimes mid to end of next week.
You could actually use any ESP32 or even ESP8266 for this - both available on Amazon (with next day delivery) or on ebay. I only used M5 because I had available and it’s an easy form factor for testing.
I did the same. I got the m5 atom and the dac on the 25th and it got delivered yesterday. Not too bad.
Can anyone post a pic of their test bed setup for reference?
Got mine yesterday too
I have managed to simplify my config a bit - if you are using an ESP32 with DAC (like the atom lite) you don’t need the external DAC.
Schematic:
Veroboard:
Photo1
Installed:
Below is my ESPhome yaml configuration:
#BCK - this version has been adopted to use built in DAC of ESP32 no external DAC required.
esphome:
name: actron-keypad2
friendly_name: Actron Keypad2
includes:
- led_proto2.h
esp32:
board: m5stack-core-esp32
framework:
type: arduino
# Enable logging
logger:
level: DEBUG #INFO
# Enable Home Assistant API
api:
encryption:
key: !secret esphome_api_key
ota:
password: !secret esphome_ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Aircon-Keypad Fallback Hotspot"
password: "M3odvzX6U8gI"
captive_portal:
web_server:
port: 80
#Setup i2c bus to control MCP4725
#https://esphome.io/components/i2c#i2c
#i2c:
# sda: 25
# scl: 21
# scan: true
#MCP4725 output to Send Voltages for key presses
#https://esphome.io/components/output/mcp4725
output:
- platform: esp32_dac #mcp4725
id: dac_output
pin: GPIO25
#address: 0x62
##***TESTING***
# Define a number input component to output mV to the DAC
number:
- platform: template
name: "DAC Output miliVolts"
min_value: 0
max_value: 3240 # Adjust this value according to the DAC's range
step: 1 # Adjust the step size as needed
restore_value: true
optimistic: true
on_value:
then:
lambda: |-
id(dac_output).set_level((x / 3240.0));
###############################################################
#Voltages adjusted for ESP32 DAC 3.3V output
button:
- platform: template
name: "Power"
icon: mdi:power
on_press:
- logger.log: Power Button Pressed
- lambda: |-
id(dac_output).set_level((3000.0 / 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Fan"
icon: mdi:fan
on_press:
- logger.log: Fan Button Pressed
- lambda: |-
id(dac_output).set_level((686.0 / 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Temp Up"
icon: mdi:thermometer-plus
on_press:
- logger.log: Temp Up Button Pressed
- lambda: |-
id(dac_output).set_level((813.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Temp Down"
icon: mdi:thermometer-minus
on_press:
- logger.log: Temp Down Button Pressed
- lambda: |-
id(dac_output).set_level((711.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Mode"
icon: mdi:air-conditioner
on_press:
- logger.log: Mode Button Pressed
- lambda: |-
id(dac_output).set_level((889.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Timer"
icon: mdi:timer
on_press:
- logger.log: Timer Button Pressed
- lambda: |-
id(dac_output).set_level((780.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Timer Up"
icon: mdi:timer-plus
on_press:
- logger.log: Timer Up Button Pressed
- lambda: |-
id(dac_output).set_level((750.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
- platform: template
name: "Timer Down"
icon: mdi:timer-minus
on_press:
- logger.log: Timer Down Button Pressed
- lambda: |-
id(dac_output).set_level((725.0/ 3240.0));
- delay: 800ms
- lambda: |-
id(dac_output).set_level((0.0 / 3240.0));
#Template sensors will be populated from lambda custom component
sensor:
- platform: template
name: "Setpoint Temperature"
unit_of_measurement: "°C"
accuracy_decimals: 1
id: setpoint_temp
state_class: measurement
icon: mdi:thermometer
- platform: template
name: "Bit Count"
id: bit_count
state_class: measurement
accuracy_decimals: 0
text_sensor:
- platform: template
name: "Bit String"
id: bit_string
binary_sensor:
- platform: template
name: "Cool"
icon: mdi:snowflake
id: cool
- platform: template
name: "Auto"
icon: mdi:flash-auto
id: auto_md
- platform: template
name: "unk1"
id: unk1
- platform: template
name: "Run"
icon: mdi:run
id: run
- platform: template
name: "Room"
id: room
- platform: template
name: "unk2"
id: unk2
- platform: template
name: "unk3"
id: unk3
- platform: template
name: "unk4"
id: unk4
- platform: template
name: "Fan Continuos"
icon: mdi:fan-chevron-up
id: fan_cont
- platform: template
name: "Fan Hi"
icon: mdi:fan-speed-3
id: fan_hi
- platform: template
name: "Fan Mid"
icon: mdi:fan-speed-2
id: fan_mid
- platform: template
name: "Fan Low"
icon: mdi:fan-speed-1
id: fan_low
- platform: template
name: "Room3"
id: room3
- platform: template
name: "Room4"
id: room4
- platform: template
name: "Room2"
id: room2
- platform: template
name: "Heat"
icon: mdi:fire
id: heat
- platform: template
name: "Room1"
id: room1
- platform: template
name: "unk5"
id: unk5
#Populate all template sensors from lambda custom component
#First argument of KeypadStatus is the pin number of the ADC input
custom_component:
- lambda: |-
auto keypad_status = new KeypadStatus(33,
id(bit_string), id(setpoint_temp), id(bit_count),
id(cool), id(auto_md), id(unk1), id(run),
id(room), id(unk2), id(unk3), id(unk4),
id(fan_cont), id(fan_hi), id(fan_mid), id(fan_low),
id(room3), id(room4), id(room2), id(heat),
id(room1), id(unk5)
);
return {keypad_status};
components:
id: keypad_status
and the led_proto2.h file which needs to be saved in your esphome directory:
#include "esphome.h"
#define NPULSE 40 // Define the number of pulses to be captured
class clsLedProto {
public: // Add this line to change the access level to public for the following members
enum ClassStatLeds {
// Enumerations representing the indices in the pulse train for various status LEDs on the HVAC unit
COOL = 0,
AUTO = 1, //RUN_BLINK
UNK1 = 2,
RUN = 3,
ROOM = 4,
UNK2 = 5,
UNK3 = 6,
UNK4 = 7,
FAN_CONT = 8, // COOL_AUTO
FAN_HI = 9,
FAN_MID = 10,
FAN_LOW = 11,
ROOM3 = 12,
ROOM4 = 13,
ROOM2 = 14,
HEAT = 15,
_3C = 16,
_3F = 17,
_3G = 18,
_3B = 19,
_3A = 20,
ROOM1 = 21,
_3E = 22,
_3D = 23,
_2B = 24,
_2F = 25,
_2G = 26,
_2E = 27,
DP = 28,
_2C = 29,
_2D = 30,
_2A = 31,
_1D = 32,
UNK5 = 33,
_1C = 34,
_1B = 35,
_1E = 36,
_1G = 37,
_1F = 38,
_1A = 39,
UNK6 = 40
};
unsigned long last_intr_us; // Timestamp of the last interrupt in microseconds
unsigned long last_work; // Timestamp of the last work in the main loop in microseconds
char pulse_vec[NPULSE]; // Temporary storage for the pulse train read during the interrupt
volatile unsigned char nlow; // Counter for the number of low pulses read
volatile unsigned char nbits; // Counter for the number of bits read to be published to Home Assistant (volatile means can be changed externally)
volatile unsigned char dbg_nerr; // Counter for the number of errors (volatile means can be changed externally)
volatile bool do_work; // Flag indicating whether there's work to be done in the main loop
bool data_error; // Flag indicating whether there's been a data error
bool newdata; // Flag indicating whether there's new data to be processed
char p[NPULSE]; // Storage for the most recent stable pulse train read from the unit
//Interrupt Handler
void handleIntr() {
auto nowu = micros(); // Stores the current microsecond count at the time of the interrupt.
unsigned long dtu = nowu - last_intr_us; // Calculates the time difference in microseconds since the last interrupt.
last_intr_us = nowu; // Updates the last interrupt timestamp to the current timestamp.
if (dtu > 3500) {
// Do nothing, as this is assumed to be the start of the start bit
data_error = false; // Reset the data error flag
return;
}
if (dtu >= 2700) {
// Start bit detected, reset bit_count
nlow = 0;
} else {
// Data bit detected
if (nlow >= NPULSE) {
//ESP_LOGD("custom","Too many pulses: %d", nlow); //Adding this makes unstable - guess shoudln't log in ISR (maybe increment counter rather)
data_error = true; // Set the data error flag
++dbg_nerr; // Increment the error counter
nlow = NPULSE; // Ensures that nlow does not exceed NPULSE, resetting it to NPULSE if it does.
}
pulse_vec[nlow] = dtu < 1000; // Records a '1' or '0' in the pulse vector based on the time difference being less than 800 microseconds.
++nlow; // Increments the nlow counter, indicating a new pulse has been recorded.
do_work = 1; // Sets the do_work flag to true, indicating that there's work to be done in the main loop.
}
}
char decode_digit(uint8_t hex_value) {
//This function takes a hex value representing a digit on the display and returns the corresponding character
//Using conventional segment display values, the hex values are as follows:
switch (hex_value) {
case 0x3F: return '0';
case 0x06: return '1';
case 0x5B: return '2';
case 0x4F: return '3';
case 0x66: return '4';
case 0x6D: return '5';
case 0x7C: return '6';
case 0x07: return '7';
case 0x7F: return '8';
case 0x67: return '9';
case 0x73: return 'P'; // 'P' is a special case
default: return '?'; // Return '?' for unrecognized hex values
}
}
float get_display_value() {
uint8_t digit1_bits = (p[_1G] << 6) | (p[_1F] << 5) | (p[_1E] << 4) | (p[_1D] << 3) | (p[_1C] << 2) | (p[_1B] << 1) | p[_1A];
uint8_t digit2_bits = (p[_2G] << 6) | (p[_2F] << 5) | (p[_2E] << 4) | (p[_2D] << 3) | (p[_2C] << 2) | (p[_2B] << 1) | p[_2A];
uint8_t digit3_bits = (p[_3G] << 6) | (p[_3F] << 5) | (p[_3E] << 4) | (p[_3D] << 3) | (p[_3C] << 2) | (p[_3B] << 1) | p[_3A];
std::string display_str;
display_str += decode_digit(digit1_bits);
display_str += decode_digit(digit2_bits);
display_str += decode_digit(digit3_bits);
for (char c : display_str) {
if (!isdigit(c) ) return -1.0f; // return -1 if any character is not a digit
}
float display_value = std::stof(display_str); // Convert string to float
if (p[DP]) display_value *= 0.1f; // Apply decimal point if DP bit is set
return display_value;
}
void mloop() {
unsigned long now = micros(); // Get the current microsecond count
if (do_work) { // If there's work to do (set by handleIntr())
do_work = 0; // Reset the work flag
last_work = now; // Update the last work time to now
} else {
unsigned long dt = now - last_work; // Calculate the time since last work
if (dt > 40000 && nlow) { // If more than 40000 microseconds have passed and there are pulses recorded
nbits = nlow; // Set the number of bits to the number of pulses recorded
nlow = 0; // Reset the pulse counter (BCK added this line)
if (nbits == 40 && !data_error ) { // If exactly 40 pulses have been recorded (BCK changed from 42. Sometimes we get 41 bits and this has invalid data)
if(memcmp(p, pulse_vec, sizeof p) != 0) { // If the pulse data has changed
newdata = true; // Set the newdata flag for the publish_state() call
//for (int n = 0; n < 45; ++n){ // Loop through each element of the pulse vector
// if (p[n] != pulse_vec[n]) ESP_LOGD("custom","%d: %d, ", n, pulse_vec[n]); // Log the changed data
//}
memcpy(p, pulse_vec, sizeof p); // Copy the new pulse data
}
} else {
ESP_LOGD("custom","Only %d bits received (Or data error)", nbits); // Log the number of bits received
}
last_work = now; // Update the last work time to now
}
}
}
};
clsLedProto ledProto; // Instantiate a clsLedProto object named ledProto
void handleInterrupt() {
// Global function to handle interrupts and call the appropriate method on ledProto
ledProto.handleIntr();
}
class KeypadStatus : public Component{
private:
TextSensor *bitString ;
Sensor *setpoint_temp ;
Sensor *bitcount ;
BinarySensor *cool ;
BinarySensor *auto_md ;
BinarySensor *unk1 ;
BinarySensor *run ;
BinarySensor *room ;
BinarySensor *unk2 ;
BinarySensor *unk3 ;
BinarySensor *unk4 ;
BinarySensor *fan_cont ;
BinarySensor *fan_hi ;
BinarySensor *fan_mid ;
BinarySensor *fan_low ;
BinarySensor *room3 ;
BinarySensor *room4 ;
BinarySensor *room2 ;
BinarySensor *heat ;
BinarySensor *room1 ;
BinarySensor *unk5 ;
public:
int adc_pin;
float get_setup_priority() const override { return esphome::setup_priority::IO; }
KeypadStatus(int adc_pin , //Pin for ADC connection
TextSensor *bitString ,
Sensor *setpoint_temp ,
Sensor *bitcount ,
BinarySensor *cool ,
BinarySensor *auto_md ,
BinarySensor *unk1 ,
BinarySensor *run ,
BinarySensor *room ,
BinarySensor *unk2 ,
BinarySensor *unk3 ,
BinarySensor *unk4 ,
BinarySensor *fan_cont ,
BinarySensor *fan_hi ,
BinarySensor *fan_mid ,
BinarySensor *fan_low ,
BinarySensor *room3 ,
BinarySensor *room4 ,
BinarySensor *room2 ,
BinarySensor *heat ,
BinarySensor *room1 ,
BinarySensor *unk5 ) : adc_pin(adc_pin)
{
this->bitString = bitString ;
this->setpoint_temp = setpoint_temp;
this->bitcount = bitcount ;
this->cool = cool ;
this->auto_md = auto_md ;
this->unk1 = unk1 ;
this->run = run ;
this->room = room ;
this->unk2 = unk2 ;
this->unk3 = unk3 ;
this->unk4 = unk4 ;
this->fan_cont = fan_cont ;
this->fan_hi = fan_hi ;
this->fan_mid = fan_mid ;
this->fan_low = fan_low ;
this->room3 = room3 ;
this->room4 = room4 ;
this->room2 = room2 ;
this->heat = heat ;
this->room1 = room1 ;
this->unk5 = unk5 ;
}
void setup() override
{
// Setup code to configure the pin and attach the interrupt
//Send adc_pin to log
ESP_LOGD("custom","adc_pin: %d", adc_pin);
//adc_pin = 33;
pinMode(adc_pin, INPUT);
attachInterrupt(digitalPinToInterrupt(adc_pin), handleInterrupt, FALLING);
}
void loop() override {
// Main loop for the LedProto Component, processes the pulse train and publishes the state to Home Assistant
ledProto.mloop();
// Initialize an empty string
std::string text;
if (ledProto.newdata) { // Publish the text to the TextSensor
// Loop through each element of the char array
text = "";
for(int i = 0; i < NPULSE; ++i) {
// Append '0' or '1' to the string based on the value of each element
text += (ledProto.p[i] ? '1' : '0');
}
bitString->publish_state(text);
ledProto.newdata = false;
// Publish the display value as a number
float display_value = ledProto.get_display_value();
setpoint_temp->publish_state(display_value);
// bitcount->publish_state(ledProto.nbits);
bitcount->publish_state(ledProto.dbg_nerr); //Changed to publish the error count instead of the bit count
// Publish the status of each LED as a binary sensor (convert to boolean with check for 0)
cool->publish_state(ledProto.p[clsLedProto::COOL] != 0);
auto_md->publish_state(ledProto.p[clsLedProto::AUTO] != 0);
unk1->publish_state(ledProto.p[clsLedProto::UNK1] != 0);
run->publish_state(ledProto.p[clsLedProto::RUN] != 0);
room->publish_state(ledProto.p[clsLedProto::ROOM] != 0);
unk2->publish_state(ledProto.p[clsLedProto::UNK2] != 0);
unk3->publish_state(ledProto.p[clsLedProto::UNK3] != 0);
unk4->publish_state(ledProto.p[clsLedProto::UNK4] != 0);
fan_cont->publish_state(ledProto.p[clsLedProto::FAN_CONT] != 0);
fan_hi->publish_state(ledProto.p[clsLedProto::FAN_HI] != 0);
fan_mid->publish_state(ledProto.p[clsLedProto::FAN_MID] != 0);
fan_low->publish_state(ledProto.p[clsLedProto::FAN_LOW] != 0);
room3->publish_state(ledProto.p[clsLedProto::ROOM3] != 0);
room4->publish_state(ledProto.p[clsLedProto::ROOM4] != 0);
room2->publish_state(ledProto.p[clsLedProto::ROOM2] != 0);
heat->publish_state(ledProto.p[clsLedProto::HEAT] != 0);
room1->publish_state(ledProto.p[clsLedProto::ROOM1] != 0);
unk5->publish_state(ledProto.p[clsLedProto::UNK5] != 0);
}
}
};
All of this is available on Github (in a very draft format) here.
The images folder has a few more photos.
This is all currently functional - but I think there may still be a few issues with the data read back from the Actron Pulses - as I still get some invalid responses.
WOW. Tremendous progress in such a short period of time. Hopefully, I can get mine together over the next couple of days to start playing.
Thank you for sharing your progress.
Thanks for sharing your latest progress.
I will try and put mine together soon too.
Wow that’s some great stuff @brentk !!!
Really appreciate you sharing all the progress you have made. I have gotten a bit side tracked and haven’t had a chance to play around anymore but looks look with your fantastic diagrams and code that I should be able to get something working.
Really like the idea of using the DAC in the atom lite, might have to get one of those and replicate your setup.
I am looking at the breadboard view and I have a question. I understand that horizontal resistor is the 20k one. However from the 2 mounted “vertically” which one is the 1.2K and which one is the 4.7K? (see pic).
Is the one closest to the green part the 1.2K (the one circled in the picture)?
Yes - the one circled is 1.2k. You need to compare it with the circuit diagram Green plug pins are numbered 1-4 from the bottom. Bottom leg of 1.2k connects to pin 1 (follow the lines) and top leg of 4.7k connects to pin 2.
Doesn’t have to be an Atom - I think all the original ESP32 and ESP32-S2 chips include a DAC, just check if the pins are exposed on the board.
Thank for the information.
One more question - the green part what is its actual name? So i can look it up at Jaycar
Use whatever you want from here:
https://www.jaycar.com.au/cables-connectors/terminal-blocks-headers/terminal-blocks/c/1HA
I used a pluggable header and terminal block
One more question - looking at the Schematic, Veroboard, Photo1 - they all show 4 wires going to the Atom (or the intention of 4 wires).
However, Installed only shows 3 wires.
Just wanted to make sure and confirm that only 3 wires are required from the PCB to the Atom.
Only 3 wires, the top pin is +15V with status pulses. I just brought that out to a pin as it was easy to do and handy if you want to put an oscilloscope on it.
Great work!
Does it decode only 4 zones? Some wall panels have 8 zones and possibly the internals support 8 anyway, could that be where the invalid responses come from?
How did you connect the wall panel side of the wire, in parallel?
I have 8 zones active on my panel. So after I will need to find how to add the extra 4 zones - but thats after I get my going with the above config.