mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 22:56:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d0a65b90c | ||
|
|
c35cbbc92b |
@@ -133,6 +133,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
if config[CONF_USE_INTERRUPT]:
|
||||
cg.add_define("USE_GPIO_BINARY_SENSOR_INTERRUPT")
|
||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||
else:
|
||||
cg.add(var.set_use_interrupt(False))
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace esphome::gpio {
|
||||
static const char *const TAG = "gpio.binary_sensor";
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
// Interrupt type strings indexed by edge-triggered InterruptType values:
|
||||
// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0).
|
||||
PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE");
|
||||
@@ -19,7 +20,9 @@ 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) {
|
||||
bool new_state = arg->isr_pin_.digital_read();
|
||||
if (new_state != arg->state_) {
|
||||
@@ -43,28 +46,36 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
|
||||
// Attach interrupt - from this point on, any changes will be caught by the interrupt
|
||||
pin->attach_interrupt(&GPIOBinarySensorStore::gpio_intr, this, this->interrupt_type_);
|
||||
}
|
||||
#endif // USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
|
||||
void GPIOBinarySensor::setup() {
|
||||
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
if (this->store_.use_interrupt_) {
|
||||
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
|
||||
this->store_.setup(internal_pin, this);
|
||||
this->publish_initial_state(this->store_.get_state());
|
||||
} else {
|
||||
this->pin_->setup();
|
||||
this->publish_initial_state(this->pin_->digital_read());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->pin_->setup();
|
||||
this->publish_initial_state(this->pin_->digital_read());
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::dump_config() {
|
||||
LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#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
|
||||
}
|
||||
|
||||
void GPIOBinarySensor::loop() {
|
||||
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
if (this->store_.use_interrupt_) {
|
||||
if (this->store_.is_changed()) {
|
||||
// Clear the flag immediately to minimize the window where we might miss changes
|
||||
@@ -78,9 +89,10 @@ void GPIOBinarySensor::loop() {
|
||||
// No changes, disable the loop until the next interrupt
|
||||
this->disable_loop();
|
||||
}
|
||||
} else {
|
||||
this->publish_state(this->pin_->digital_read());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->publish_state(this->pin_->digital_read());
|
||||
}
|
||||
|
||||
float GPIOBinarySensor::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
@@ -10,6 +11,7 @@ namespace esphome::gpio {
|
||||
// Store class for ISR data and configuration (no vtables, ISR-safe)
|
||||
class GPIOBinarySensorStore {
|
||||
public:
|
||||
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
void setup(InternalGPIOPin *pin, Component *component);
|
||||
|
||||
static void gpio_intr(GPIOBinarySensorStore *arg);
|
||||
@@ -29,15 +31,18 @@ class GPIOBinarySensorStore {
|
||||
// Separate method to clear the flag
|
||||
this->changed_ = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
friend class GPIOBinarySensor;
|
||||
#ifdef USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
|
||||
volatile bool state_{false};
|
||||
volatile bool changed_{false};
|
||||
bool use_interrupt_{true};
|
||||
gpio::InterruptType interrupt_type_{gpio::INTERRUPT_ANY_EDGE};
|
||||
bool use_interrupt_{true};
|
||||
#endif
|
||||
};
|
||||
|
||||
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
|
||||
@@ -46,8 +51,14 @@ 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; }
|
||||
#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)
|
||||
/// Setup pin
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
#define USE_ESP32_IMPROV_STATE_CALLBACK
|
||||
#define USE_EVENT
|
||||
#define USE_FAN
|
||||
#define USE_GPIO_BINARY_SENSOR_INTERRUPT
|
||||
#define USE_GPIO_SWITCH_INTERLOCK
|
||||
#define USE_GRAPH
|
||||
#define USE_GRAPHICAL_DISPLAY_MENU
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user