Skip to content

Instantly share code, notes, and snippets.

@sonulgupta
Last active February 4, 2026 13:39
Show Gist options
  • Select an option

  • Save sonulgupta/4c1806dd702cb84fce3b9eddcd0b4d06 to your computer and use it in GitHub Desktop.

Select an option

Save sonulgupta/4c1806dd702cb84fce3b9eddcd0b4d06 to your computer and use it in GitHub Desktop.
Arduino_Port_Expander with Mage_2560
// Must disable logging if using logging in main.cpp or in other custom components for the
// __c causes a section type conflict with __c thingy
// you can enable logging and use it if you enable this in logger:
/*
logger:
level: DEBUG
esp8266_store_log_strings_in_flash: False
*/
//#define APE_LOGGING
// take advantage of LOG_ defines to decide which code to include
#ifdef LOG_BINARY_OUTPUT
#define APE_BINARY_OUTPUT
#endif
#ifdef LOG_BINARY_SENSOR
#define APE_BINARY_SENSOR
#endif
#ifdef LOG_SENSOR
#define APE_SENSOR
#endif
static const char *TAGape = "ape";
#define APE_CMD_DIGITAL_READ 0
#define APE_CMD_WRITE_ANALOG 2
#define APE_CMD_WRITE_DIGITAL_HIGH 3
#define APE_CMD_WRITE_DIGITAL_LOW 4
#define APE_CMD_SETUP_PIN_OUTPUT 5
#define APE_CMD_SETUP_PIN_INPUT_PULLUP 6
#define APE_CMD_SETUP_PIN_INPUT 7
// 16 analog registers.. A0 to A15
// A4 and A5 on Arduino Uno not supported due to I2C
#define CMD_ANALOG_READ_A0 0b1000 // 0x8 = A0
// ....
#define CMD_ANALOG_READ_A15 10111 // 17 = A15
#define CMD_SETUP_ANALOG_INTERNAL 0x10
#define CMD_SETUP_ANALOG_DEFAULT 0x11
#define get_ape(constructor) static_cast<ArduinoPortExpander *>(constructor.get_component(0))
#define ape_binary_output(ape, pin) get_ape(ape)->get_binary_output(pin)
#define ape_binary_sensor(ape, pin) get_ape(ape)->get_binary_sensor(pin)
#define ape_analog_input(ape, pin) get_ape(ape)->get_analog_input(pin)
class ArduinoPortExpander;
using namespace esphome;
#ifdef APE_BINARY_OUTPUT
class ApeBinaryOutput : public output::BinaryOutput
{
public:
ApeBinaryOutput(ArduinoPortExpander *parent, uint8_t pin)
{
this->parent_ = parent;
this->pin_ = pin;
}
void write_state(bool state) override;
uint8_t get_pin() { return this->pin_; }
protected:
ArduinoPortExpander *parent_;
uint8_t pin_;
// Pins are setup as output after the state is written, Arduino has no open drain outputs, after setting an output it will either sink or source thus activating outputs writen to false during a flick.
bool setup_{true};
bool state_{false};
friend class ArduinoPortExpander;
};
#endif
#ifdef APE_BINARY_SENSOR
class ApeBinarySensor : public binary_sensor::BinarySensor
{
public:
ApeBinarySensor(ArduinoPortExpander *parent, uint8_t pin)
{
this->pin_ = pin;
}
uint8_t get_pin() { return this->pin_; }
protected:
uint8_t pin_;
};
#endif
#ifdef APE_SENSOR
class ApeAnalogInput : public sensor::Sensor
{
public:
ApeAnalogInput(ArduinoPortExpander *parent, uint8_t pin)
{
this->pin_ = pin;
}
uint8_t get_pin() { return this->pin_; }
protected:
uint8_t pin_;
};
#endif
class ArduinoPortExpander : public Component, public I2CDevice
{
public:
static ArduinoPortExpander *instance; // Add this line!
ArduinoPortExpander(I2CBus *bus, uint8_t address, bool vref_default = false)
{
instance = this; // Add this line!
set_i2c_address(address);
set_i2c_bus(bus);
this->vref_default_ = vref_default;
}
void setup() override
{
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setting up ArduinoPortExpander at %#02x ...", address_);
#endif
/* We cannot setup as usual as arduino boots later than esp8266
Poll i2c bus for our Arduino for a n seconds instead of failing fast,
also this is important as pin setup (INPUT_PULLUP, OUTPUT it's done once)
*/
this->configure_timeout_ = millis() + 5000;
}
void loop() override
{
if (millis() < this->configure_timeout_)
{
bool try_configure = millis() % 100 > 50;
if (try_configure == this->configure_)
return;
this->configure_ = try_configure;
if (ERROR_OK == this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 9)) //changed 3 to 9
{
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "ArduinoPortExpander found at %#02x", address_);
#endif
delay(10);
if (this->vref_default_)
{
this->write_register(CMD_SETUP_ANALOG_DEFAULT, nullptr, 0); // 0: unused
}
// Config success
this->configure_timeout_ = 0;
this->status_clear_error();
#ifdef APE_BINARY_SENSOR
for (ApeBinarySensor *pin : this->input_pins_)
{
App.feed_wdt();
uint8_t pinNo = pin->get_pin();
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setup input pin %d", pinNo);
#endif
this->write_register(APE_CMD_SETUP_PIN_INPUT_PULLUP, &pinNo, 1);
delay(20);
}
#endif
#ifdef APE_BINARY_OUTPUT
for (ApeBinaryOutput *output : this->output_pins_)
{
if (!output->setup_)
{ // this output has a valid value already
this->write_state(output->pin_, output->state_, true);
App.feed_wdt();
delay(20);
}
}
#endif
#ifdef APE_SENSOR
for (ApeAnalogInput *sensor : this->analog_pins_)
{
App.feed_wdt();
uint8_t pinNo = sensor->get_pin();
#ifdef APE_LOGGING
ESP_LOGCONFIG(TAGape, "Setup analog input pin %d", pinNo);
#endif
this->write_register(APE_CMD_SETUP_PIN_INPUT, &pinNo, 1);
delay(20);
}
#endif
return;
}
// Still not answering
return;
}
if (this->configure_timeout_ != 0 && millis() > this->configure_timeout_)
{
#ifdef APE_LOGGING
ESP_LOGE(TAGape, "ArduinoPortExpander NOT found at %#02x", address_);
#endif
this->mark_failed();
return;
}
#ifdef APE_BINARY_SENSOR
if (ERROR_OK != this->read_register(APE_CMD_DIGITAL_READ, const_cast<uint8_t *>(this->read_buffer_), 9)) //Changed this from 3 to 9
{
#ifdef APE_LOGGING
ESP_LOGE(TAGape, "Error reading. Reconfiguring pending.");
#endif
this->status_set_error();
this->configure_timeout_ = millis() + 5000;
return;
}
for (ApeBinarySensor *pin : this->input_pins_)
{
uint8_t pinNo = pin->get_pin();
uint8_t bit = pinNo % 8;
uint8_t value = 0;
if(pinNo < 8){
value = this->read_buffer_[0];
}else if(pinNo < 16){
value = this->read_buffer_[1];
}else if(pinNo < 24){
value = this->read_buffer_[2];
}else if(pinNo < 32){
value = this->read_buffer_[3];
}else if(pinNo < 40){
value = this->read_buffer_[4];
}else if(pinNo < 48){
value = this->read_buffer_[5];
}else if(pinNo < 56){
value = this->read_buffer_[6];
}else if(pinNo < 64){
value = this->read_buffer_[7];
}else{
value = this->read_buffer_[8];
}
bool ret = value & (1 << bit);
if (this->initial_state_)
pin->publish_initial_state(ret);
else
pin->publish_state(ret);
}
#endif
#ifdef APE_SENSOR
for (ApeAnalogInput *pin : this->analog_pins_)
{
uint8_t pinNo = pin->get_pin();
pin->publish_state(analogRead(pinNo));
}
#endif
this->initial_state_ = false;
}
#ifdef APE_SENSOR
uint16_t analogRead(uint8_t pin)
{
bool ok = (ERROR_OK == this->read_register((uint8_t)(CMD_ANALOG_READ_A0 + pin), const_cast<uint8_t *>(this->read_buffer_), 2));
#ifdef APE_LOGGING
ESP_LOGVV(TAGape, "analog read pin: %d ok: %d byte0: %d byte1: %d", pin, ok, this->read_buffer_[0], this->read_buffer_[1]);
#endif
uint16_t value = this->read_buffer_[0] | ((uint16_t)this->read_buffer_[1] << 8);
return value;
}
#endif
#ifdef APE_BINARY_OUTPUT
output::BinaryOutput *get_binary_output(uint8_t pin)
{
ApeBinaryOutput *output = new ApeBinaryOutput(this, pin);
output_pins_.push_back(output);
return output;
}
#endif
#ifdef APE_BINARY_SENSOR
binary_sensor::BinarySensor *get_binary_sensor(uint8_t pin)
{
ApeBinarySensor *binarySensor = new ApeBinarySensor(this, pin);
input_pins_.push_back(binarySensor);
return binarySensor;
}
#endif
#ifdef APE_SENSOR
sensor::Sensor *get_analog_input(uint8_t pin)
{
ApeAnalogInput *input = new ApeAnalogInput(this, pin);
analog_pins_.push_back(input);
return input;
}
#endif
void write_state(uint8_t pin, bool state, bool setup = false)
{
if (this->configure_timeout_ != 0)
return;
#ifdef APE_LOGGING
ESP_LOGD(TAGape, "Writing %d to pin %d", state, pin);
#endif
this->write_register(state ? APE_CMD_WRITE_DIGITAL_HIGH : APE_CMD_WRITE_DIGITAL_LOW, &pin, 1);
if (setup)
{
App.feed_wdt();
delay(20);
#ifdef APE_LOGGING
ESP_LOGI(TAGape, "Setup output pin %d", pin);
#endif
this->write_register(APE_CMD_SETUP_PIN_OUTPUT, &pin, 1);
}
}
protected:
bool configure_{true};
bool initial_state_{true};
uint8_t read_buffer_[9]{0, 0, 0, 0, 0, 0, 0, 0, 0}; //changed from [3]{0, 0, 0}
unsigned long configure_timeout_{5000};
bool vref_default_{false};
#ifdef APE_BINARY_OUTPUT
std::vector<ApeBinaryOutput *> output_pins_;
#endif
#ifdef APE_BINARY_SENSOR
std::vector<ApeBinarySensor *> input_pins_;
#endif
#ifdef APE_SENSOR
std::vector<ApeAnalogInput *> analog_pins_;
#endif
};
#ifdef APE_BINARY_OUTPUT
void ApeBinaryOutput::write_state(bool state)
{
this->state_ = state;
this->parent_->write_state(this->pin_, state, this->setup_);
this->setup_ = false;
}
#endif
ArduinoPortExpander *ArduinoPortExpander::instance = nullptr;
esphome:
name: xxxxxxxxxx
friendly_name: xxxxxxxxx
includes:
- arduino_port_expander.h
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
# Enable logging
logger:
level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: "xxxxxxxxx"
ota:
- platform: esphome
password: "xxxxxxxxx"
ethernet:
type: W5500
clk_pin: GPIO13
mosi_pin: GPIO11
miso_pin: GPIO12
cs_pin: GPIO14
interrupt_pin: GPIO10
reset_pin: GPIO09
manual_ip:
static_ip: 192.168.0.127
gateway: 192.168.0.1
subnet: 255.255.255.0
modbus:
id: modbus1
uart_id: mbus
i2c:
- id: i2c_component
sda: 34
scl: 33
scan: true
external_components:
- source:
type: git
url: https://github.com/robertklep/esphome-custom-component
components: [ custom, custom_component ]
custom_component:
- lambda: |-
auto ape = new ArduinoPortExpander(id(i2c_component), 0x08);
return {ape};
id: expander1
output:
- platform: custom
type: binary
lambda: |-
return []() -> std::vector<esphome::output::BinaryOutput*> {
auto *exp = ArduinoPortExpander::instance;
if (exp == nullptr) return {};
return {
exp->get_binary_output(47), exp->get_binary_output(48),
exp->get_binary_output(49), exp->get_binary_output(50),
exp->get_binary_output(51), exp->get_binary_output(52),
exp->get_binary_output(13)};
}();
outputs:
- id: relay_1
inverted: true
- id: relay_2
inverted: true
- id: relay_3
inverted: true
- id: relay_4
inverted: true
- id: relay_5
inverted: true
- id: relay_6
inverted: true
- id: relay_7
inverted: true
switch:
- platform: output
name: "xxxxxxxxx"
output: relay_1
- platform: output
name: "xxxxxxxxx"
output: relay_2
- platform: output
name: "xxxxxxxxx"
output: relay_3
- platform: output
name: "xxxxxxxxx"
output: relay_4
- platform: output
name: "xxxxxxxxx"
output: relay_5
- platform: output
name: "xxxxxxxxx"
output: relay_6
- platform: output
name: "xxxxxxxxx"
output: relay_7
binary_sensor:
- platform: custom
lambda: |-
return []() -> std::vector<binary_sensor::BinarySensor*> {
auto *exp = ArduinoPortExpander::instance;
if (exp == nullptr) return {};
return {
exp->get_binary_sensor(0), exp->get_binary_sensor(1), exp->get_binary_sensor(2),
exp->get_binary_sensor(3), exp->get_binary_sensor(4), exp->get_binary_sensor(5),
exp->get_binary_sensor(6), exp->get_binary_sensor(7), exp->get_binary_sensor(8),
exp->get_binary_sensor(9), exp->get_binary_sensor(10), exp->get_binary_sensor(11),
exp->get_binary_sensor(12), exp->get_binary_sensor(14), exp->get_binary_sensor(15),
exp->get_binary_sensor(16), exp->get_binary_sensor(17), exp->get_binary_sensor(18),
exp->get_binary_sensor(19), exp->get_binary_sensor(22), exp->get_binary_sensor(23),
exp->get_binary_sensor(24), exp->get_binary_sensor(25), exp->get_binary_sensor(26),
exp->get_binary_sensor(27), exp->get_binary_sensor(28), exp->get_binary_sensor(29),
exp->get_binary_sensor(30), exp->get_binary_sensor(31), exp->get_binary_sensor(32),
exp->get_binary_sensor(33), exp->get_binary_sensor(34), exp->get_binary_sensor(35),
exp->get_binary_sensor(36), exp->get_binary_sensor(37), exp->get_binary_sensor(38),
exp->get_binary_sensor(39), exp->get_binary_sensor(40), exp->get_binary_sensor(41),
exp->get_binary_sensor(42), exp->get_binary_sensor(43), exp->get_binary_sensor(44),
exp->get_binary_sensor(45), exp->get_binary_sensor(46), exp->get_binary_sensor(53)};
}();
binary_sensors:
- id: spare1
name: "spare switch1"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith1 Active"
#- light.toggle: light
- id: spare2
name: "spare switch2"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith2 Active"
#- light.toggle: light
- id: spare3
name: "spare switch3"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith3 Active"
#- light.toggle: light
- id: spare4
name: "spare switch4"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith4 Active"
#- light.toggle: light
- id: spare5
name: "spare switch5"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith5 Active"
#- light.toggle: light
- id: spare6
name: "spare switch6"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith6 Active"
#- light.toggle: light
- id: spare7
name: "spare switch7"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith7 Active"
#- light.toggle: light
- id: spare8
name: "spare switch8"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith8 Active"
#- light.toggle: light
- id: spare9
name: "spare switch9"
trigger_on_initial_state: true
filters:
- invert:
on_state:
then:
- logger.log: "Spare Swith9 Active"
#- light.toggle: light
#(Continue as per your pin requirement)
# define analog sensors
sensor:
- platform: custom
lambda: |-
return []() -> std::vector<sensor::Sensor*> {
auto *exp = ArduinoPortExpander::instance;
if (exp == nullptr) return {};
return {
exp->get_analog_input(54), exp->get_analog_input(55),
exp->get_analog_input(56), exp->get_analog_input(57),
exp->get_analog_input(58), exp->get_analog_input(59),
exp->get_analog_input(60), exp->get_analog_input(61),
exp->get_analog_input(62), exp->get_analog_input(63),
exp->get_analog_input(64), exp->get_analog_input(65),
exp->get_analog_input(66), exp->get_analog_input(67),
exp->get_analog_input(68), exp->get_analog_input(69)};
}();
sensors:
- name: "Analog Voltage-1"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-2"
#attenuation: 11db
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-3"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-4"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-5"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-6"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog Voltage-7"
filters:
- throttle: 60s
- lambda:
if (x >= 3.11) {
return x * 1.60256;
} else if (x <= 0.15) {
return 0;
} else {
return x * 1.51;
}
- name: "Analog current-1"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-2"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-3"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-4"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-5"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-6"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Analog current-7"
unit_of_measurement: mA
filters:
- throttle: 60s
- multiply: 6.66666666
- name: "Living Room LDR"
id: livingroom_ldr_sensor
unit_of_measurement: "lx"
device_class: "illuminance"
state_class: "measurement"
accuracy_decimals: 1
filters:
- throttle: 60s
- calibrate_linear:
method: exact
datapoints:
- 10.0 -> 800.0
- 100.0 -> 600.0
- 9000.0 -> 50.0
- 32729.0 -> 30.0
- name: "BedRoom LDR"
id: bedroom_ldr_sensor
unit_of_measurement: "lx"
device_class: "illuminance"
state_class: "measurement"
accuracy_decimals: 1
filters:
- throttle: 60s
- calibrate_linear:
method: exact
datapoints:
- 10.0 -> 800.0
- 100.0 -> 600.0
- 9000.0 -> 50.0
- 32729.0 -> 30.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment