From 41e09c1e6c1afc8c0419ea42d0c1a47625fa96eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 09:12:09 -1000 Subject: [PATCH 01/10] [mcp23xxx][pi4ioe5v6408] Add optional interrupt pin to eliminate polling Extend interrupt_pin support to MCP23008, MCP23017, MCP23S08, MCP23S17, and PI4IOE5V6408 GPIO expander components. Same approach as PCF8574/PCA9554: when configured, the component disables its loop and only wakes on interrupt, and the cache stays valid between interrupts so binary sensors return from cache instead of doing I2C/SPI reads every loop iteration. For MCP23xxx, the interrupt_pin config and C++ logic lives in the shared MCP23XXXBase template class, so all four variants (I2C 8/16-pin, SPI 8/16-pin) inherit it automatically. Inspired by jesserockz's work in #11959 which proposed a more comprehensive interrupt-driven approach with per-pin interrupt status register reading. This implementation takes a simpler path by leveraging the existing CachedGpioExpander cache invalidation mechanism. --- esphome/components/mcp23008/mcp23008.cpp | 2 ++ esphome/components/mcp23017/mcp23017.cpp | 2 ++ esphome/components/mcp23s08/mcp23s08.cpp | 2 ++ esphome/components/mcp23s17/mcp23s17.cpp | 2 ++ esphome/components/mcp23xxx_base/__init__.py | 4 ++++ .../components/mcp23xxx_base/mcp23xxx_base.h | 20 ++++++++++++++++++- esphome/components/pi4ioe5v6408/__init__.py | 4 ++++ .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 16 ++++++++++++++- .../components/pi4ioe5v6408/pi4ioe5v6408.h | 4 ++++ tests/components/mcp23008/common.yaml | 8 ++++++-- tests/components/mcp23008/test.esp32-idf.yaml | 3 +++ .../components/mcp23008/test.esp8266-ard.yaml | 3 +++ .../components/mcp23008/test.rp2040-ard.yaml | 3 +++ tests/components/mcp23017/common.yaml | 8 ++++++-- tests/components/mcp23017/test.esp32-idf.yaml | 3 +++ .../components/mcp23017/test.esp8266-ard.yaml | 3 +++ .../components/mcp23017/test.rp2040-ard.yaml | 3 +++ tests/components/mcp23s08/common.yaml | 1 + tests/components/mcp23s08/test.esp32-idf.yaml | 1 + .../components/mcp23s08/test.esp8266-ard.yaml | 1 + .../components/mcp23s08/test.rp2040-ard.yaml | 1 + tests/components/mcp23s17/common.yaml | 1 + tests/components/mcp23s17/test.esp32-idf.yaml | 1 + .../components/mcp23s17/test.esp8266-ard.yaml | 1 + .../components/mcp23s17/test.rp2040-ard.yaml | 1 + tests/components/pi4ioe5v6408/common.yaml | 10 +++++++--- .../pi4ioe5v6408/test.esp32-idf.yaml | 1 + .../pi4ioe5v6408/test.rp2040-ard.yaml | 1 + 28 files changed, 101 insertions(+), 9 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 64b120daa49..c3c82f36483 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -22,6 +22,8 @@ void MCP23008::setup() { // enable open-drain interrupt pins, 3.3V-safe this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index e14e317d440..556ef96ac5e 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -24,6 +24,8 @@ void MCP23017::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR); this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 1c17b66637f..514d7c8db35 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -34,6 +34,8 @@ void MCP23S08::setup() { // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23S08::dump_config() { diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index c6abd7ad594..be775bc7c99 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -42,6 +42,8 @@ void MCP23S17::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } + + this->setup_interrupt_pin_(); } void MCP23S17::dump_config() { diff --git a/esphome/components/mcp23xxx_base/__init__.py b/esphome/components/mcp23xxx_base/__init__.py index d6e82101adf..cd952099c05 100644 --- a/esphome/components/mcp23xxx_base/__init__.py +++ b/esphome/components/mcp23xxx_base/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_ID, CONF_INPUT, CONF_INTERRUPT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -32,6 +33,7 @@ MCP23XXX_INTERRUPT_MODES = { MCP23XXX_CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ).extend(cv.COMPONENT_SCHEMA) @@ -43,6 +45,8 @@ async def register_mcp23xxx(config, num_pins): await cg.register_component(var, config) CORE.data.setdefault(CONF_MCP23XXX, {})[id.id] = num_pins cg.add(var.set_open_drain_ints(config[CONF_OPEN_DRAIN_INTERRUPT])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) return var diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index fb992466d5d..9c50ee42f13 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -15,11 +15,28 @@ template class MCP23XXXBase : public Component, public gpio_expander: virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode); void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } float get_setup_priority() const override { return setup_priority::IO; } - void loop() override { this->reset_pin_cache_(); } + void setup_interrupt_pin_() { + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + this->disable_loop(); + } + } + + void loop() override { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } + } protected: + static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } + // read a given register virtual bool read_reg(uint8_t reg, uint8_t *value) = 0; // write a value to a given register @@ -28,6 +45,7 @@ template class MCP23XXXBase : public Component, public gpio_expander: virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0; bool open_drain_ints_; + InternalGPIOPin *interrupt_pin_{nullptr}; }; template class MCP23XXXGPIOPin : public GPIOPin { diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py index c64f923823e..d5b19dab1c8 100644 --- a/esphome/components/pi4ioe5v6408/__init__.py +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, + CONF_INTERRUPT_PIN, CONF_INVERTED, CONF_MODE, CONF_NUMBER, @@ -33,6 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), cv.Optional(CONF_RESET, default=True): cv.boolean, + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, } ) .extend(cv.COMPONENT_SCHEMA) @@ -46,6 +48,8 @@ async def to_code(config): await i2c.register_i2c_device(var, config) cg.add(var.set_reset(config[CONF_RESET])) + if interrupt_pin := config.get(CONF_INTERRUPT_PIN): + cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin))) def validate_mode(value): diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 9247e114f04..4155e52b393 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -33,9 +33,18 @@ void PI4IOE5V6408Component::setup() { return; } } + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + this->disable_loop(); + } } +void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); } void PI4IOE5V6408Component::dump_config() { ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); LOG_I2C_DEVICE(this) if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); @@ -60,7 +69,12 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { this->write_gpio_modes_(); } -void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } +void PI4IOE5V6408Component::loop() { + this->reset_pin_cache_(); + if (this->interrupt_pin_ != nullptr) { + this->disable_loop(); + } +} bool PI4IOE5V6408Component::read_gpio_outputs_() { if (this->is_failed()) diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h index 4dc31201ce4..ff2474fe994 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -22,8 +22,11 @@ class PI4IOE5V6408Component : public Component, /// Indicate if the component should reset the state during setup void set_reset(bool reset) { this->reset_ = reset; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } protected: + static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg); + bool digital_read_hw(uint8_t pin) override; bool digital_read_cache(uint8_t pin) override; void digital_write_hw(uint8_t pin, bool value) override; @@ -40,6 +43,7 @@ class PI4IOE5V6408Component : public Component, uint8_t pull_up_down_mask_{0x00}; bool reset_{true}; + InternalGPIOPin *interrupt_pin_{nullptr}; bool read_gpio_modes_(); bool write_gpio_modes_(); diff --git a/tests/components/mcp23008/common.yaml b/tests/components/mcp23008/common.yaml index 4a407adfd82..dfd543540d5 100644 --- a/tests/components/mcp23008/common.yaml +++ b/tests/components/mcp23008/common.yaml @@ -1,6 +1,10 @@ mcp23008: - i2c_id: i2c_bus - id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub + - i2c_id: i2c_bus + id: mcp23008_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/mcp23008/test.esp32-idf.yaml b/tests/components/mcp23008/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23008/test.esp32-idf.yaml +++ b/tests/components/mcp23008/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23008/test.esp8266-ard.yaml b/tests/components/mcp23008/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23008/test.esp8266-ard.yaml +++ b/tests/components/mcp23008/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23008/test.rp2040-ard.yaml b/tests/components/mcp23008/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23008/test.rp2040-ard.yaml +++ b/tests/components/mcp23008/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23017/common.yaml b/tests/components/mcp23017/common.yaml index 54a97e911fe..2f90335e169 100644 --- a/tests/components/mcp23017/common.yaml +++ b/tests/components/mcp23017/common.yaml @@ -1,6 +1,10 @@ mcp23017: - i2c_id: i2c_bus - id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub + - i2c_id: i2c_bus + id: mcp23017_hub_int + address: 0x21 + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/mcp23017/test.esp32-idf.yaml b/tests/components/mcp23017/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/mcp23017/test.esp32-idf.yaml +++ b/tests/components/mcp23017/test.esp32-idf.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/mcp23017/test.esp8266-ard.yaml b/tests/components/mcp23017/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/mcp23017/test.esp8266-ard.yaml +++ b/tests/components/mcp23017/test.esp8266-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO15 + packages: i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml diff --git a/tests/components/mcp23017/test.rp2040-ard.yaml b/tests/components/mcp23017/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/mcp23017/test.rp2040-ard.yaml +++ b/tests/components/mcp23017/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml diff --git a/tests/components/mcp23s08/common.yaml b/tests/components/mcp23s08/common.yaml index 2170ae04596..327bf02ca9b 100644 --- a/tests/components/mcp23s08/common.yaml +++ b/tests/components/mcp23s08/common.yaml @@ -2,3 +2,4 @@ mcp23s08: - id: mcp23s08_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s08/test.esp32-idf.yaml b/tests/components/mcp23s08/test.esp32-idf.yaml index a3352cf880d..eeb69416453 100644 --- a/tests/components/mcp23s08/test.esp32-idf.yaml +++ b/tests/components/mcp23s08/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml index 595f31046ab..472fdb25821 100644 --- a/tests/components/mcp23s08/test.esp8266-ard.yaml +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO13 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s08/test.rp2040-ard.yaml b/tests/components/mcp23s08/test.rp2040-ard.yaml index 79ea6ce90b0..09b87ca3f85 100644 --- a/tests/components/mcp23s08/test.rp2040-ard.yaml +++ b/tests/components/mcp23s08/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/mcp23s17/common.yaml b/tests/components/mcp23s17/common.yaml index a89beeb16b7..150ecca325d 100644 --- a/tests/components/mcp23s17/common.yaml +++ b/tests/components/mcp23s17/common.yaml @@ -2,3 +2,4 @@ mcp23s17: - id: mcp23s17_hub cs_pin: ${cs_pin} deviceaddress: 0 + interrupt_pin: ${interrupt_pin} diff --git a/tests/components/mcp23s17/test.esp32-idf.yaml b/tests/components/mcp23s17/test.esp32-idf.yaml index a3352cf880d..eeb69416453 100644 --- a/tests/components/mcp23s17/test.esp32-idf.yaml +++ b/tests/components/mcp23s17/test.esp32-idf.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO15 packages: spi: !include ../../test_build_components/common/spi/esp32-idf.yaml diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml index 595f31046ab..472fdb25821 100644 --- a/tests/components/mcp23s17/test.esp8266-ard.yaml +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO15 + interrupt_pin: GPIO13 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s17/test.rp2040-ard.yaml b/tests/components/mcp23s17/test.rp2040-ard.yaml index 79ea6ce90b0..09b87ca3f85 100644 --- a/tests/components/mcp23s17/test.rp2040-ard.yaml +++ b/tests/components/mcp23s17/test.rp2040-ard.yaml @@ -1,5 +1,6 @@ substitutions: cs_pin: GPIO5 + interrupt_pin: GPIO2 packages: spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml index 2344622081a..2c96fa0d9a4 100644 --- a/tests/components/pi4ioe5v6408/common.yaml +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -1,7 +1,11 @@ pi4ioe5v6408: - i2c_id: i2c_bus - id: pi4ioe1 - address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1 + address: 0x44 + - i2c_id: i2c_bus + id: pi4ioe1_int + address: 0x45 + interrupt_pin: ${interrupt_pin} switch: - platform: gpio diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml index 9a4779d822a..a6eb3c1cb14 100644 --- a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO21 i2c_scl: GPIO22 + interrupt_pin: GPIO15 packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml index 3429a2952a3..cd6fef30421 100644 --- a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -1,6 +1,7 @@ substitutions: i2c_sda: GPIO4 i2c_scl: GPIO5 + interrupt_pin: GPIO2 packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 3773ff9a981182a04daf93ae0bed787218e0d119 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 09:17:29 -1000 Subject: [PATCH 02/10] Fix clang-tidy: rename setup_interrupt_pin_ to setup_interrupt_pin --- esphome/components/mcp23008/mcp23008.cpp | 2 +- esphome/components/mcp23017/mcp23017.cpp | 2 +- esphome/components/mcp23s08/mcp23s08.cpp | 2 +- esphome/components/mcp23s17/mcp23s17.cpp | 2 +- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index c3c82f36483..1023c06eb55 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -23,7 +23,7 @@ void MCP23008::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } - this->setup_interrupt_pin_(); + this->setup_interrupt_pin(); } void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 556ef96ac5e..ae1c2d216d6 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -25,7 +25,7 @@ void MCP23017::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); } - this->setup_interrupt_pin_(); + this->setup_interrupt_pin(); } void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 514d7c8db35..d48b9156a10 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -35,7 +35,7 @@ void MCP23S08::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } - this->setup_interrupt_pin_(); + this->setup_interrupt_pin(); } void MCP23S08::dump_config() { diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index be775bc7c99..24753dbf2a9 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -43,7 +43,7 @@ void MCP23S17::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } - this->setup_interrupt_pin_(); + this->setup_interrupt_pin(); } void MCP23S17::dump_config() { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 9c50ee42f13..7ddfd267681 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -18,7 +18,7 @@ template class MCP23XXXBase : public Component, public gpio_expander: void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } float get_setup_priority() const override { return setup_priority::IO; } - void setup_interrupt_pin_() { + void setup_interrupt_pin() { if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); From d1e737280f08d1326c9d97e6abb1846b364776f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 09:18:04 -1000 Subject: [PATCH 03/10] Fix ESP8266 SPI test pin conflict: use GPIO0 for interrupt_pin --- tests/components/mcp23s08/test.esp8266-ard.yaml | 2 +- tests/components/mcp23s17/test.esp8266-ard.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/mcp23s08/test.esp8266-ard.yaml b/tests/components/mcp23s08/test.esp8266-ard.yaml index 472fdb25821..ffc40b35953 100644 --- a/tests/components/mcp23s08/test.esp8266-ard.yaml +++ b/tests/components/mcp23s08/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: cs_pin: GPIO15 - interrupt_pin: GPIO13 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml diff --git a/tests/components/mcp23s17/test.esp8266-ard.yaml b/tests/components/mcp23s17/test.esp8266-ard.yaml index 472fdb25821..ffc40b35953 100644 --- a/tests/components/mcp23s17/test.esp8266-ard.yaml +++ b/tests/components/mcp23s17/test.esp8266-ard.yaml @@ -1,6 +1,6 @@ substitutions: cs_pin: GPIO15 - interrupt_pin: GPIO13 + interrupt_pin: GPIO0 packages: spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml From 8234512a896755ebff63c59ec2732a373af28f2f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 09:44:59 -1000 Subject: [PATCH 04/10] Auto-enable chip-level interrupts for input pins - MCP23x08/x17: auto-enable CHANGE interrupt mode in pin_mode() when interrupt_pin is configured, so users don't need to manually set interrupt: CHANGE on every pin - PI4IOE5V6408: write interrupt enable mask for input pins in write_gpio_modes_() when interrupt_pin is configured --- esphome/components/mcp23x08_base/mcp23x08_base.cpp | 5 +++++ esphome/components/mcp23x17_base/mcp23x17_base.cpp | 5 +++++ esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index 92228be62c9..e4f4d50aae1 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -32,6 +32,11 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } } void MCP23X08Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 6f95ee98fdb..42613053de7 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -44,6 +44,11 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { } else if (flags == gpio::FLAG_OUTPUT) { this->update_reg(pin, false, iodir); } + // When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins + // so the chip's INT output fires on any input state change + if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) { + this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE); + } } void MCP23X17Base::pin_interrupt_mode(uint8_t pin, mcp23xxx_base::MCP23XXXInterruptMode interrupt_mode) { diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 4155e52b393..b09567ba1e1 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -156,6 +156,13 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } + // Enable interrupts for input pins when interrupt pin is configured + // (input pins have mode_mask_ bit cleared) + if (this->interrupt_pin_ != nullptr && + !this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, ~this->mode_mask_)) { + this->status_set_warning(LOG_STR("Failed to write interrupt enable mask")); + return false; + } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Wrote GPIO config:\n" From 85036d64d99cfd1963e98b2c88ac174e1e80311c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:03:40 -1000 Subject: [PATCH 05/10] Fix MCP23x17 MIRROR bit, add dump_config logging, and clear PI4IOE5V6408 latched interrupts - MCP23017/MCP23S17: Enable MIRROR bit in IOCON when interrupt_pin is configured so INTA and INTB are ORed together, allowing a single interrupt wire to catch changes on all 16 pins - All MCP23xxx variants: Log interrupt_pin in dump_config for debugging - PI4IOE5V6408: Read interrupt status register during setup to clear any latched state that could hold INT line low - MCP23XXXBase: Move setup_interrupt_pin_() to protected visibility --- esphome/components/mcp23008/mcp23008.cpp | 7 ++++-- esphome/components/mcp23017/mcp23017.cpp | 23 ++++++++++++++----- esphome/components/mcp23s08/mcp23s08.cpp | 3 ++- esphome/components/mcp23s17/mcp23s17.cpp | 23 +++++++++++++------ .../components/mcp23xxx_base/mcp23xxx_base.h | 17 +++++++------- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 3 +++ 6 files changed, 51 insertions(+), 25 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 1023c06eb55..5f73e03f6f6 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -23,10 +23,13 @@ void MCP23008::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } - this->setup_interrupt_pin(); + this->setup_interrupt_pin_(); } -void MCP23008::dump_config() { ESP_LOGCONFIG(TAG, "MCP23008:"); } +void MCP23008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23008:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23008::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index ae1c2d216d6..212c15ccf27 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -6,7 +6,8 @@ namespace mcp23017 { static const char *const TAG = "mcp23017"; -static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin void MCP23017::setup() { uint8_t iocon; @@ -19,16 +20,26 @@ void MCP23017::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = 0; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | IOCON_ODR); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | IOCON_ODR); + iocon_flags |= IOCON_ODR; + } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (iocon_flags != 0) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon | iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags); } - this->setup_interrupt_pin(); + this->setup_interrupt_pin_(); } -void MCP23017::dump_config() { ESP_LOGCONFIG(TAG, "MCP23017:"); } +void MCP23017::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23017:"); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} bool MCP23017::read_reg(uint8_t reg, uint8_t *value) { if (this->is_failed()) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index d48b9156a10..983c1aa6008 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -35,12 +35,13 @@ void MCP23S08::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } - this->setup_interrupt_pin(); + this->setup_interrupt_pin_(); } void MCP23S08::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S08:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index 24753dbf2a9..db9a34e2302 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -7,9 +7,10 @@ namespace mcp23s17 { static const char *const TAG = "mcp23s17"; // IOCON register bits -static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode -static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable -static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin +static constexpr uint8_t IOCON_SEQOP = 0x20; // Sequential operation mode +static constexpr uint8_t IOCON_MIRROR = 0x40; // Mirror INTA/INTB pins +static constexpr uint8_t IOCON_HAEN = 0x08; // Hardware address enable +static constexpr uint8_t IOCON_ODR = 0x04; // Open-drain output for INT pin void MCP23S17::set_device_address(uint8_t device_addr) { if (device_addr != 0) { @@ -37,18 +38,26 @@ void MCP23S17::setup() { this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); + uint8_t iocon_flags = IOCON_SEQOP | IOCON_HAEN; if (this->open_drain_ints_) { - // enable open-drain interrupt pins, 3.3V-safe (addressed, only this chip) - this->write_reg(mcp23x17_base::MCP23X17_IOCONA, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); - this->write_reg(mcp23x17_base::MCP23X17_IOCONB, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); + iocon_flags |= IOCON_ODR; + } + if (this->interrupt_pin_ != nullptr) { + // Mirror INTA/INTB so either pin fires for changes on any port + iocon_flags |= IOCON_MIRROR; + } + if (this->open_drain_ints_ || this->interrupt_pin_ != nullptr) { + this->write_reg(mcp23x17_base::MCP23X17_IOCONA, iocon_flags); + this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags); } - this->setup_interrupt_pin(); + this->setup_interrupt_pin_(); } void MCP23S17::dump_config() { ESP_LOGCONFIG(TAG, "MCP23S17:"); LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); } bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 7ddfd267681..7838e301276 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -18,15 +18,6 @@ template class MCP23XXXBase : public Component, public gpio_expander: void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } float get_setup_priority() const override { return setup_priority::IO; } - void setup_interrupt_pin() { - if (this->interrupt_pin_ != nullptr) { - this->interrupt_pin_->setup(); - this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); - this->set_invalidate_on_read_(false); - this->disable_loop(); - } - } - void loop() override { this->reset_pin_cache_(); if (this->interrupt_pin_ != nullptr) { @@ -35,6 +26,14 @@ template class MCP23XXXBase : public Component, public gpio_expander: } protected: + void setup_interrupt_pin_() { + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + this->disable_loop(); + } + } static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); } // read a given register diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index b09567ba1e1..69de3997290 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -35,6 +35,9 @@ void PI4IOE5V6408Component::setup() { } if (this->interrupt_pin_ != nullptr) { + // Clear any latched interrupt status from before boot + uint8_t status; + this->read_byte(PI4IOE5V6408_REGISTER_INTERRUPT_STATUS, &status); this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); this->set_invalidate_on_read_(false); From 1e9e5ca763a4802f3aa310bf788e8fbd2fa0dca1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:05:00 -1000 Subject: [PATCH 06/10] Fix pin_interrupt_mode overriding auto-enabled CHANGE interrupt MCP23XXXGPIOPin::setup() calls pin_interrupt_mode(pin, NO_INTERRUPT) after pin_mode() already auto-enabled CHANGE, undoing the auto-enable. Skip the explicit pin_interrupt_mode call when interrupt_pin is configured and the user didn't override the default. --- esphome/components/mcp23xxx_base/mcp23xxx_base.cpp | 7 ++++++- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 535119fc5c1..4c1daac5623 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -7,7 +7,12 @@ namespace mcp23xxx_base { template void MCP23XXXGPIOPin::setup() { this->pin_mode(flags_); - this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + // When interrupt_pin is configured, pin_mode() already auto-enables CHANGE + // interrupt for input pins, so skip the explicit call if the user didn't + // override the default (NO_INTERRUPT) + if (this->interrupt_mode_ != MCP23XXX_NO_INTERRUPT || this->parent_->get_interrupt_pin() == nullptr) { + this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); + } } template void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } template bool MCP23XXXGPIOPin::digital_read() { diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 7838e301276..5e2d9e9c688 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -16,6 +16,7 @@ template class MCP23XXXBase : public Component, public gpio_expander: void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; } void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; } float get_setup_priority() const override { return setup_priority::IO; } void loop() override { From aeb8e9413b138a4954ee2fa3e4259b74cb27321d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:21:28 -1000 Subject: [PATCH 07/10] Clear latched MCP23xxx interrupts during setup, fix uint8_t truncation - MCP23008/MCP23S08: Read GPIO register before attaching ISR to clear any latched interrupt from a previous MCU run (chip may stay powered) - MCP23017/MCP23S17: Read both GPIOA and GPIOB to clear latched state - PI4IOE5V6408: Add explicit static_cast for ~mode_mask_ to avoid implicit int-to-uint8_t truncation --- esphome/components/mcp23008/mcp23008.cpp | 5 +++++ esphome/components/mcp23017/mcp23017.cpp | 6 ++++++ esphome/components/mcp23s08/mcp23s08.cpp | 5 +++++ esphome/components/mcp23s17/mcp23s17.cpp | 6 ++++++ esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 2 +- 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index 5f73e03f6f6..b7a4276c46d 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -23,6 +23,11 @@ void MCP23008::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } + // Clear any latched interrupt by reading GPIO before attaching ISR + if (this->interrupt_pin_ != nullptr) { + uint8_t val; + this->read_reg(mcp23x08_base::MCP23X08_GPIO, &val); + } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 212c15ccf27..b1775f24024 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -33,6 +33,12 @@ void MCP23017::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags); } + // Clear any latched interrupt by reading GPIO before attaching ISR + if (this->interrupt_pin_ != nullptr) { + uint8_t val; + this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &val); + this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &val); + } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 983c1aa6008..22913816464 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -35,6 +35,11 @@ void MCP23S08::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } + // Clear any latched interrupt by reading GPIO before attaching ISR + if (this->interrupt_pin_ != nullptr) { + uint8_t val; + this->read_reg(mcp23x08_base::MCP23X08_GPIO, &val); + } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index db9a34e2302..f27b55c6f6a 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -51,6 +51,12 @@ void MCP23S17::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags); } + // Clear any latched interrupt by reading GPIO before attaching ISR + if (this->interrupt_pin_ != nullptr) { + uint8_t val; + this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &val); + this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &val); + } this->setup_interrupt_pin_(); } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 69de3997290..610015bbe6b 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -162,7 +162,7 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { // Enable interrupts for input pins when interrupt pin is configured // (input pins have mode_mask_ bit cleared) if (this->interrupt_pin_ != nullptr && - !this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, ~this->mode_mask_)) { + !this->write_byte(PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK, static_cast(~this->mode_mask_))) { this->status_set_warning(LOG_STR("Failed to write interrupt enable mask")); return false; } From af87f21e681f7fbb8b571fb7f518ef3e517bb9ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:22:19 -1000 Subject: [PATCH 08/10] Remove unnecessary latched interrupt clearing during setup A latched interrupt at boot is harmless - the ISR fires immediately, loop runs, cache invalidates, and a normal read cycle clears it. The extra register reads added unnecessary complexity. --- esphome/components/mcp23008/mcp23008.cpp | 5 ----- esphome/components/mcp23017/mcp23017.cpp | 6 ------ esphome/components/mcp23s08/mcp23s08.cpp | 5 ----- esphome/components/mcp23s17/mcp23s17.cpp | 6 ------ esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 3 --- 5 files changed, 25 deletions(-) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp index b7a4276c46d..5f73e03f6f6 100644 --- a/esphome/components/mcp23008/mcp23008.cpp +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -23,11 +23,6 @@ void MCP23008::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, iocon | IOCON_ODR); } - // Clear any latched interrupt by reading GPIO before attaching ISR - if (this->interrupt_pin_ != nullptr) { - uint8_t val; - this->read_reg(mcp23x08_base::MCP23X08_GPIO, &val); - } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index b1775f24024..212c15ccf27 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -33,12 +33,6 @@ void MCP23017::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon | iocon_flags); } - // Clear any latched interrupt by reading GPIO before attaching ISR - if (this->interrupt_pin_ != nullptr) { - uint8_t val; - this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &val); - this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &val); - } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp index 22913816464..983c1aa6008 100644 --- a/esphome/components/mcp23s08/mcp23s08.cpp +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -35,11 +35,6 @@ void MCP23S08::setup() { this->write_reg(mcp23x08_base::MCP23X08_IOCON, IOCON_SEQOP | IOCON_HAEN | IOCON_ODR); } - // Clear any latched interrupt by reading GPIO before attaching ISR - if (this->interrupt_pin_ != nullptr) { - uint8_t val; - this->read_reg(mcp23x08_base::MCP23X08_GPIO, &val); - } this->setup_interrupt_pin_(); } diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp index f27b55c6f6a..db9a34e2302 100644 --- a/esphome/components/mcp23s17/mcp23s17.cpp +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -51,12 +51,6 @@ void MCP23S17::setup() { this->write_reg(mcp23x17_base::MCP23X17_IOCONB, iocon_flags); } - // Clear any latched interrupt by reading GPIO before attaching ISR - if (this->interrupt_pin_ != nullptr) { - uint8_t val; - this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &val); - this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &val); - } this->setup_interrupt_pin_(); } diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 610015bbe6b..fe1df6cbb0f 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -35,9 +35,6 @@ void PI4IOE5V6408Component::setup() { } if (this->interrupt_pin_ != nullptr) { - // Clear any latched interrupt status from before boot - uint8_t status; - this->read_byte(PI4IOE5V6408_REGISTER_INTERRUPT_STATUS, &status); this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); this->set_invalidate_on_read_(false); From 173195ee1bf1ebeb3b7d395e6c6104a912dfed7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:22:50 -1000 Subject: [PATCH 09/10] Add comments explaining why latched interrupts are not cleared at setup --- esphome/components/mcp23xxx_base/mcp23xxx_base.h | 3 +++ esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.h b/esphome/components/mcp23xxx_base/mcp23xxx_base.h index 5e2d9e9c688..e77eac87e74 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.h +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.h @@ -27,6 +27,9 @@ template class MCP23XXXBase : public Component, public gpio_expander: } protected: + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the GPIO read clears the latch. One harmless extra read at most. void setup_interrupt_pin_() { if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index fe1df6cbb0f..8e38e7fa1d6 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -34,6 +34,9 @@ void PI4IOE5V6408Component::setup() { } } + // No need to clear latched interrupts before attaching the ISR — if INT is + // already low the ISR fires immediately, loop runs, cache invalidates, and + // the read clears the latch. One harmless extra read at most. if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); From 18ad20a1bd08813d6cce2e0b59699d2336264b1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 10:45:46 -1000 Subject: [PATCH 10/10] Add binary sensors on interrupt-enabled hubs to exercise pin_mode path --- tests/components/mcp23008/common.yaml | 6 ++++++ tests/components/mcp23017/common.yaml | 6 ++++++ tests/components/pi4ioe5v6408/common.yaml | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/tests/components/mcp23008/common.yaml b/tests/components/mcp23008/common.yaml index dfd543540d5..7eeee409fff 100644 --- a/tests/components/mcp23008/common.yaml +++ b/tests/components/mcp23008/common.yaml @@ -13,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23008_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23008_binary_sensor_int + pin: + mcp23xxx: mcp23008_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/mcp23017/common.yaml b/tests/components/mcp23017/common.yaml index 2f90335e169..8b1e32600ef 100644 --- a/tests/components/mcp23017/common.yaml +++ b/tests/components/mcp23017/common.yaml @@ -13,6 +13,12 @@ binary_sensor: mcp23xxx: mcp23017_hub number: 0 mode: INPUT + - platform: gpio + id: mcp23017_binary_sensor_int + pin: + mcp23xxx: mcp23017_hub_int + number: 0 + mode: INPUT switch: - platform: gpio diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml index 2c96fa0d9a4..77a77fa3e4f 100644 --- a/tests/components/pi4ioe5v6408/common.yaml +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -20,3 +20,8 @@ binary_sensor: pin: pi4ioe5v6408: pi4ioe1 number: 1 + - platform: gpio + id: sensor1_int + pin: + pi4ioe5v6408: pi4ioe1_int + number: 1