diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 5336c8dd83..9d044dca2d 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -15,12 +15,12 @@ PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_ED static const LogString *interrupt_type_to_string(gpio::InterruptType type) { return InterruptTypeStrings::get_log_str(static_cast(type), 0); } -#endif static const LogString *gpio_mode_to_string(bool use_interrupt) { return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling"); } #endif +#endif #ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { @@ -64,11 +64,13 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); #ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->store_.use_interrupt_))); if (this->store_.use_interrupt_) { ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->store_.interrupt_type_))); } +#else + ESP_LOGCONFIG(TAG, " Mode: polling"); #endif } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 25f6a6c57a..956443fab5 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -41,8 +41,8 @@ class GPIOBinarySensorStore { volatile bool state_{false}; volatile bool changed_{false}; gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE}; -#endif bool use_interrupt_{true}; +#endif }; class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { @@ -51,9 +51,13 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon // Interrupts are only detached on reboot when memory is cleared anyway. void set_pin(GPIOPin *pin) { this->pin_ = pin; } - void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } #ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT + void set_use_interrupt(bool use_interrupt) { this->store_.use_interrupt_ = use_interrupt; } void set_interrupt_type(gpio::InterruptType type) { this->store_.interrupt_type_ = type; } +#else + // Polling-only build: codegen still emits set_use_interrupt(false) calls, + // so keep the setter as an inlined no-op instead of storing the flag. + void set_use_interrupt(bool /*use_interrupt*/) {} #endif // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index f336a9105e..60494d9cba 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -3,10 +3,15 @@ from __future__ import annotations from collections.abc import Callable +import logging from pathlib import Path import pytest +from esphome.core import CORE + +INTERRUPT_DEFINE = "USE_GPIO_BINARY_SENSOR_INTERRUPT" + def test_gpio_binary_sensor_basic_setup( generate_main: Callable[[str | Path], str], @@ -69,3 +74,62 @@ def test_gpio_binary_sensor_explicit_polling_mode( ) assert "bs_polling->set_use_interrupt(false);" in main_cpp + + +def test_gpio_binary_sensor_interrupt_emits_define( + generate_main: Callable[[str | Path], str], +) -> None: + """ + An interrupt-mode sensor must emit the define that compiles the ISR code, + since the platform ISR pin implementation is only built when needed + """ + generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") + + assert INTERRUPT_DEFINE in {d.name for d in CORE.defines} + + +def test_gpio_binary_sensor_polling_omits_define( + generate_main: Callable[[str | Path], str], +) -> None: + """ + A polling-only config must not emit the interrupt define, so the ISR code + (and its reference to ISRInternalGPIOPin) is compiled out + """ + generate_main("tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml") + + assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines} + + +def test_gpio_binary_sensor_mixed_modes_emit_define( + generate_main: Callable[[str | Path], str], +) -> None: + """ + With one interrupt and one polling sensor, the define is emitted and the + polling instance still opts out via its setter + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml" + ) + + assert INTERRUPT_DEFINE in {d.name for d in CORE.defines} + assert "bs_polling->set_use_interrupt(false);" in main_cpp + assert "bs_interrupt->set_use_interrupt" not in main_cpp + + +def test_gpio_binary_sensor_expander_pin_omits_define( + generate_main: Callable[[str | Path], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """ + An expander pin can't use interrupts: final validation falls back to + polling and the interrupt define must not be emitted. This is the config + that fails to link if the ISR code is compiled without an internal pin + """ + with caplog.at_level(logging.INFO): + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml" + ) + + assert "bs_expander->set_use_interrupt(false);" in main_cpp + assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines} + assert "falling back to polling mode" in caplog.text diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml new file mode 100644 index 0000000000..f153ccebfd --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml @@ -0,0 +1,21 @@ +esphome: + name: test + +esp32: + board: esp32dev + +i2c: + scl: 16 + sda: 17 + +ch422g: + - id: ch422g_hub + +binary_sensor: + - platform: gpio + name: "Expander Sensor" + id: bs_expander + pin: + ch422g: ch422g_hub + number: 1 + mode: INPUT diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml new file mode 100644 index 0000000000..0e8c7ec3fd --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml @@ -0,0 +1,17 @@ +esphome: + name: test + +esp32: + board: esp32dev + +binary_sensor: + - platform: gpio + pin: 5 + name: "Interrupt Sensor" + id: bs_interrupt + + - platform: gpio + pin: 4 + name: "Polling Sensor" + id: bs_polling + use_interrupt: false diff --git a/tests/components/gpio/common.yaml b/tests/components/gpio/common.yaml index bdd71a31e3..b8e8fa81e4 100644 --- a/tests/components/gpio/common.yaml +++ b/tests/components/gpio/common.yaml @@ -3,13 +3,6 @@ binary_sensor: pin: ${binary_sensor_pin} id: gpio_binary_sensor - # Polling sensor alongside an interrupt sensor: proves the mixed build where - # both code paths are compiled and selected per instance at runtime. - - platform: gpio - pin: ${binary_sensor_pin_2} - id: gpio_binary_sensor_polling - use_interrupt: false - output: - platform: gpio pin: ${output_pin} diff --git a/tests/components/gpio/test.esp32-c3-idf.yaml b/tests/components/gpio/test.esp32-c3-idf.yaml index 25b80f7dcb..e9071b4356 100644 --- a/tests/components/gpio/test.esp32-c3-idf.yaml +++ b/tests/components/gpio/test.esp32-c3-idf.yaml @@ -1,6 +1,5 @@ substitutions: binary_sensor_pin: GPIO2 - binary_sensor_pin_2: GPIO10 output_pin: GPIO3 switch_pin: GPIO4 switch_pin_2: GPIO5 diff --git a/tests/components/gpio/test.esp32-idf.yaml b/tests/components/gpio/test.esp32-idf.yaml index e31d60cdda..862aa533ea 100644 --- a/tests/components/gpio/test.esp32-idf.yaml +++ b/tests/components/gpio/test.esp32-idf.yaml @@ -1,6 +1,5 @@ substitutions: binary_sensor_pin: GPIO12 - binary_sensor_pin_2: GPIO18 output_pin: GPIO13 switch_pin: GPIO14 switch_pin_2: GPIO15 diff --git a/tests/components/gpio/test.esp8266-ard.yaml b/tests/components/gpio/test.esp8266-ard.yaml index 3202da753a..e13b4520d1 100644 --- a/tests/components/gpio/test.esp8266-ard.yaml +++ b/tests/components/gpio/test.esp8266-ard.yaml @@ -1,6 +1,5 @@ substitutions: binary_sensor_pin: GPIO0 - binary_sensor_pin_2: GPIO4 output_pin: GPIO2 switch_pin: GPIO15 switch_pin_2: GPIO12 diff --git a/tests/components/gpio/test.rp2040-ard.yaml b/tests/components/gpio/test.rp2040-ard.yaml index b3602e37b4..e9071b4356 100644 --- a/tests/components/gpio/test.rp2040-ard.yaml +++ b/tests/components/gpio/test.rp2040-ard.yaml @@ -1,6 +1,5 @@ substitutions: binary_sensor_pin: GPIO2 - binary_sensor_pin_2: GPIO8 output_pin: GPIO3 switch_pin: GPIO4 switch_pin_2: GPIO5