From a2120953b9c07199d95785fe2370746a2d1345a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 08:43:16 -1000 Subject: [PATCH 1/3] [gpio_expander] Rename interrupt_driven_ to invalidate_on_read_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base class doesn't need to know about interrupts — it just controls whether digital_read() self-invalidates cache entries. Rename the flag and invert the polarity for clarity. --- .../components/gpio_expander/cached_gpio.h | 24 ++++++++++--------- esphome/components/pca9554/pca9554.cpp | 4 ++-- esphome/components/pcf8574/pcf8574.cpp | 4 ++-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index 522444e9c9f..dbdc0753fd4 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -29,10 +29,10 @@ template 256 class CachedGpioExpander { public: /// @brief Read the state of the given pin. - /// In polling mode (default), each read invalidates the pin's cache entry so - /// the next read of the same pin triggers a fresh hardware read. - /// In interrupt mode, the cache stays valid until explicitly invalidated by - /// reset_pin_cache_() (called from loop() when an interrupt fires). + /// @brief Read the state of the given pin. + /// By default, each read invalidates the pin's cache entry so the next read + /// of the same pin triggers a fresh hardware read. When invalidate_on_read + /// is disabled, the cache stays valid until explicitly cleared via reset_pin_cache_(). /// @param pin Pin number to read /// @return Pin state bool digital_read(P pin) { @@ -40,8 +40,8 @@ class CachedGpioExpander { const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { - if (!this->interrupt_driven_) { - // Polling mode: invalidate pin so next read triggers hardware read + if (this->invalidate_on_read_) { + // Invalidate pin so next read triggers hardware read this->read_cache_valid_[bank] &= ~pin_mask; } } else { @@ -49,8 +49,8 @@ class CachedGpioExpander { if (!this->digital_read_hw(pin)) return false; // Mark bank cache as valid except the pin that is being returned now - // (in interrupt mode, mark all pins including this one as valid) - this->read_cache_valid_[bank] = std::numeric_limits::max() & ~(this->interrupt_driven_ ? 0 : pin_mask); + // (when not invalidating on read, mark all pins including this one as valid) + this->read_cache_valid_[bank] = std::numeric_limits::max() & ~(this->invalidate_on_read_ ? pin_mask : 0); } return this->digital_read_cache(pin); } @@ -78,8 +78,10 @@ class CachedGpioExpander { /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } - /// @brief Enable interrupt-driven mode. Cache stays valid until reset_pin_cache_() is called. - void set_interrupt_driven_(bool interrupt_driven) { this->interrupt_driven_ = interrupt_driven; } + /// @brief Control whether digital_read() invalidates the pin's cache entry after reading. + /// When enabled (default), each read self-invalidates so the next read triggers a hardware read. + /// When disabled, cache stays valid until reset_pin_cache_() is explicitly called. + void set_invalidate_on_read_(bool invalidate) { this->invalidate_on_read_ = invalidate; } static constexpr uint16_t BITS_PER_BYTE = 8; static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; @@ -87,7 +89,7 @@ class CachedGpioExpander { static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); T read_cache_valid_[BANKS]{0}; - bool interrupt_driven_{false}; + bool invalidate_on_read_{true}; }; } // namespace esphome::gpio_expander diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index befb19d9948..9b300eaac28 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -38,8 +38,8 @@ void PCA9554Component::setup() { if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); - // Cache stays valid until interrupt fires and loop() invalidates it - this->set_interrupt_driven_(true); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); // With interrupt pin, only run loop when interrupt fires this->disable_loop(); } diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 4d078c1620c..1eeef663b0d 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -19,8 +19,8 @@ void PCF8574Component::setup() { if (this->interrupt_pin_ != nullptr) { this->interrupt_pin_->setup(); this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); - // Cache stays valid until interrupt fires and loop() invalidates it - this->set_interrupt_driven_(true); + // Don't invalidate cache on read — only invalidate when interrupt fires + this->set_invalidate_on_read_(false); // With interrupt pin, only run loop when interrupt fires this->disable_loop(); } From 0afdc6df26bc2315014b75f5512df5edcf315a7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 08:58:02 -1000 Subject: [PATCH 2/3] [pcf8574][pca9554] Add interrupt_pin to compile tests --- tests/components/pca9554/common.yaml | 5 +++++ tests/components/pca9554/test.esp32-idf.yaml | 3 +++ tests/components/pca9554/test.esp8266-ard.yaml | 3 +++ tests/components/pca9554/test.rp2040-ard.yaml | 3 +++ tests/components/pcf8574/common.yaml | 5 +++++ tests/components/pcf8574/test.esp32-idf.yaml | 3 +++ tests/components/pcf8574/test.esp8266-ard.yaml | 3 +++ tests/components/pcf8574/test.rp2040-ard.yaml | 3 +++ 8 files changed, 28 insertions(+) diff --git a/tests/components/pca9554/common.yaml b/tests/components/pca9554/common.yaml index 9e5e7f3342b..82a88b90aae 100644 --- a/tests/components/pca9554/common.yaml +++ b/tests/components/pca9554/common.yaml @@ -3,6 +3,11 @@ pca9554: i2c_id: i2c_bus pin_count: 8 address: 0x3F + - id: pca9554_hub_int + i2c_id: i2c_bus + pin_count: 8 + address: 0x3E + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pca9554/test.esp32-idf.yaml b/tests/components/pca9554/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pca9554/test.esp32-idf.yaml +++ b/tests/components/pca9554/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/pca9554/test.esp8266-ard.yaml b/tests/components/pca9554/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pca9554/test.esp8266-ard.yaml +++ b/tests/components/pca9554/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/pca9554/test.rp2040-ard.yaml b/tests/components/pca9554/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pca9554/test.rp2040-ard.yaml +++ b/tests/components/pca9554/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/pcf8574/common.yaml b/tests/components/pcf8574/common.yaml index 09fa33164e3..8a26b930152 100644 --- a/tests/components/pcf8574/common.yaml +++ b/tests/components/pcf8574/common.yaml @@ -3,6 +3,11 @@ pcf8574: i2c_id: i2c_bus address: 0x21 pcf8575: false + - id: pcf8574_hub_int + i2c_id: i2c_bus + address: 0x22 + pcf8575: false + interrupt_pin: ${interrupt_pin} binary_sensor: - platform: gpio diff --git a/tests/components/pcf8574/test.esp32-idf.yaml b/tests/components/pcf8574/test.esp32-idf.yaml index b47e39c3898..8c3b341dce0 100644 --- a/tests/components/pcf8574/test.esp32-idf.yaml +++ b/tests/components/pcf8574/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/pcf8574/test.esp8266-ard.yaml b/tests/components/pcf8574/test.esp8266-ard.yaml index 4a98b9388ab..69b243bfd80 100644 --- a/tests/components/pcf8574/test.esp8266-ard.yaml +++ b/tests/components/pcf8574/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/pcf8574/test.rp2040-ard.yaml b/tests/components/pcf8574/test.rp2040-ard.yaml index 319a7c71a65..b8ad1e47925 100644 --- a/tests/components/pcf8574/test.rp2040-ard.yaml +++ b/tests/components/pcf8574/test.rp2040-ard.yaml @@ -1,3 +1,6 @@ +substitutions: + interrupt_pin: GPIO2 + packages: i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml From 9f3fb37d1256857fba91544df41689c8b9f144c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 08:59:09 -1000 Subject: [PATCH 3/3] Remove duplicate @brief line in cached_gpio.h --- esphome/components/gpio_expander/cached_gpio.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index dbdc0753fd4..ddb9e63686f 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -28,7 +28,6 @@ namespace esphome::gpio_expander { template 256), uint16_t, uint8_t>::type> class CachedGpioExpander { public: - /// @brief Read the state of the given pin. /// @brief Read the state of the given pin. /// By default, each read invalidates the pin's cache entry so the next read /// of the same pin triggers a fresh hardware read. When invalidate_on_read