Merge remote-tracking branch 'upstream/rp2040-ethernet-lwiplock-irq-defer' into integration

This commit is contained in:
J. Nick Koston
2026-04-09 21:48:20 -10:00
28 changed files with 271 additions and 37 deletions
@@ -4,6 +4,7 @@ module.exports = {
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
TOO_BIG_MARKER: '<!-- too-big-request -->',
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
MANAGED_LABELS: [
'new-component',
@@ -281,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
return { labels, deprecatedInfo };
}
// Strategy: Detect when maintainers cannot modify the PR branch
function detectMaintainerAccess(context) {
const pr = context.payload.pull_request;
// Only relevant for cross-repo PRs (forks)
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
return null;
}
if (pr.maintainer_can_modify) {
return null;
}
const isOrgFork = pr.head.repo.owner.type === 'Organization';
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
return { isOrgFork, orgName: pr.head.repo.owner.login };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels, prFiles, context) {
const labels = new Set();
@@ -329,5 +347,6 @@ module.exports = {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
};
+11 -5
View File
@@ -12,9 +12,10 @@ const {
detectTests,
detectPRTemplateCheckboxes,
detectDeprecatedComponents,
detectMaintainerAccess,
detectRequirements
} = require('./detectors');
const { handleReviews } = require('./reviews');
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
const { applyLabels, removeOldLabels } = require('./labels');
// Fetch API data
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
codeOwnerLabels,
testLabels,
checkboxLabels,
deprecatedResult
deprecatedResult,
maintainerAccess
] = await Promise.all([
detectMergeBranch(context),
detectComponentPlatforms(changedFiles, apiData),
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
detectCodeOwner(github, context, changedFiles),
detectTests(changedFiles),
detectPRTemplateCheckboxes(context),
detectDeprecatedComponents(github, context, changedFiles)
detectDeprecatedComponents(github, context, changedFiles),
detectMaintainerAccess(context)
]);
// Extract deprecated component info
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
// Handle reviews and org fork comment
await Promise.all([
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
handleMaintainerAccessComment(github, context, maintainerAccess)
]);
// Apply labels
await applyLabels(github, context, finalLabels);
+60 -2
View File
@@ -2,7 +2,8 @@ const {
BOT_COMMENT_MARKER,
CODEOWNERS_MARKER,
TOO_BIG_MARKER,
DEPRECATED_COMPONENT_MARKER
DEPRECATED_COMPONENT_MARKER,
ORG_FORK_MARKER
} = require('./constants');
// Generate review messages
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
}
}
// Handle maintainer access warning comment
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
if (!maintainerAccess) {
return;
}
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
const prAuthor = context.payload.pull_request.user.login;
// Check if we already posted the warning (iterate pages to exit early)
let existingComment;
for await (const { data: comments } of github.paginate.iterator(
github.rest.issues.listComments,
{ owner, repo, issue_number: pr_number }
)) {
existingComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body && comment.body.includes(ORG_FORK_MARKER)
);
if (existingComment) {
break;
}
}
if (existingComment) {
console.log('Maintainer access warning comment already exists, skipping');
return;
}
let body;
if (maintainerAccess.isOrgFork) {
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
} else {
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
`Hey there @${prAuthor},\n` +
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body
});
console.log('Created maintainer access warning comment');
}
module.exports = {
handleReviews
handleReviews,
handleMaintainerAccessComment
};
+1 -1
View File
@@ -8,7 +8,7 @@ from .. import hbridge_ns
CODEOWNERS = ["@DotNetDann"]
HBridgeLightOutput = hbridge_ns.class_(
"HBridgeLightOutput", cg.PollingComponent, light.LightOutput
"HBridgeLightOutput", cg.Component, light.LightOutput
)
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
@@ -1,20 +1,17 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/output/float_output.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/log.h"
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace hbridge {
// Using PollingComponent as the updates are more consistent and reduces flickering
class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
class HBridgeLightOutput : public Component, public light::LightOutput {
public:
HBridgeLightOutput() : PollingComponent(1) {}
void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; }
void set_pina_pin(output::FloatOutput *pina_pin) { this->pina_pin_ = pina_pin; }
void set_pinb_pin(output::FloatOutput *pinb_pin) { this->pinb_pin_ = pinb_pin; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
@@ -24,16 +21,16 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
return traits;
}
void setup() override { this->forward_direction_ = false; }
void setup() override { this->disable_loop(); }
void update() override {
// This method runs around 60 times per second
// We cannot do the PWM ourselves so we are reliant on the hardware PWM
if (!this->forward_direction_) { // First LED Direction
void loop() override {
// Only called when both channels are active — alternate H-bridge direction
// each iteration to multiplex cold and warm white.
if (!this->forward_direction_) {
this->pina_pin_->set_level(this->pina_duty_);
this->pinb_pin_->set_level(0);
this->forward_direction_ = true;
} else { // Second LED Direction
} else {
this->pina_pin_->set_level(0);
this->pinb_pin_->set_level(this->pinb_duty_);
this->forward_direction_ = false;
@@ -43,15 +40,32 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void write_state(light::LightState *state) override {
state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false);
float new_pina, new_pinb;
state->current_values_as_cwww(&new_pina, &new_pinb, false);
this->pina_duty_ = new_pina;
this->pinb_duty_ = new_pinb;
if (new_pina != 0.0f && new_pinb != 0.0f) {
// Both channels active — need loop to alternate H-bridge direction
this->high_freq_.start();
this->enable_loop();
} else {
// Zero or one channel active — drive pins directly, no multiplexing needed
this->high_freq_.stop();
this->disable_loop();
this->pina_pin_->set_level(new_pina);
this->pinb_pin_->set_level(new_pinb);
}
}
protected:
output::FloatOutput *pina_pin_;
output::FloatOutput *pinb_pin_;
float pina_duty_ = 0;
float pinb_duty_ = 0;
bool forward_direction_ = false;
float pina_duty_{0};
float pinb_duty_{0};
bool forward_direction_{false};
HighFrequencyLoopRequester high_freq_;
};
} // namespace hbridge
+4
View File
@@ -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,
@@ -24,6 +25,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(MCP23016),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -35,6 +37,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
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):
+14
View File
@@ -24,11 +24,22 @@ void MCP23016::setup() {
// all pins input
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
void MCP23016::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
@@ -37,6 +48,9 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
this->update_reg_(pin, true, MCP23016_IODIR1);
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_reg_(pin, false, MCP23016_IODIR1);
}
+4
View File
@@ -35,7 +35,10 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
float get_setup_priority() const override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -51,6 +54,7 @@ class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::
uint16_t olat_{0x0000};
// Cache for input values (16-bit combined for both banks)
uint16_t input_mask_{0x0000};
InternalGPIOPin *interrupt_pin_{nullptr};
};
class MCP23016GPIOPin : public GPIOPin {
+9 -1
View File
@@ -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,
@@ -25,7 +26,12 @@ PCA6416AGPIOPin = pca6416a_ns.class_(
CONF_PCA6416A = "pca6416a"
CONFIG_SCHEMA = (
cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)})
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x21))
)
@@ -35,6 +41,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
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):
+18
View File
@@ -49,11 +49,22 @@ void PCA6416AComponent::setup() {
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); }
void PCA6416AComponent::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
void PCA6416AComponent::dump_config() {
@@ -62,6 +73,7 @@ void PCA6416AComponent::dump_config() {
} else {
ESP_LOGCONFIG(TAG, "PCA6416A:");
}
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -101,6 +113,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_register_(pin, true, pull_dir);
this->update_register_(pin, false, pull_en);
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
this->update_register_(pin, true, io_dir);
if (has_pullup_) {
@@ -109,6 +124,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
} else {
ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors");
}
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
this->update_register_(pin, false, io_dir);
}
+4
View File
@@ -24,7 +24,10 @@ class PCA6416AComponent : public Component,
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
// Virtual methods from CachedGpioExpander
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
@@ -43,6 +46,7 @@ class PCA6416AComponent : public Component,
esphome::i2c::ErrorCode last_error_;
/// Only the PCAL6416A has pull-up resistors
bool has_pullup_{false};
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Helper class to expose a PCA6416A pin as an internal input GPIO pin.
+25 -6
View File
@@ -9,7 +9,7 @@
#include <WiFi.h>
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
#elif defined(USE_ETHERNET)
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
#include <lwip_wrap.h> // For LWIPMutex — LwIPLock delegates to it (see below)
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#include <hardware/structs/rosc.h>
@@ -43,9 +43,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
// assertion failures). See esphome#10681.
//
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
// WiFi uses cyw43_arch_lwip_begin/end.
//
// For wired Ethernet, taking only the async_context lock is NOT enough. The
// W5500 GPIO IRQ path (LwipIntfDev::_irq) checks arduino-pico's `__inLWIP`
// counter to decide whether to defer packet processing. If we hold the
// async_context lock without bumping `__inLWIP`, an interrupt-driven packet
// arrival re-enters lwIP from IRQ context and corrupts pbufs (the `pbuf_cat`
// assertion crash on wiznet-w5500-evb-pico). We mirror arduino-pico's
// LWIPMutex (cores/rp2040/lwip_wrap.h) exactly: bump `__inLWIP`, take the
// lock, and on release re-unmask any GPIO IRQs that were deferred while we
// held it. We can't `using LwIPLock = LWIPMutex;` in helpers.h because
// pulling lwip_wrap.h there poisons many TUs with lwIP types.
//
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
@@ -53,8 +62,18 @@ IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
#elif defined(USE_ETHERNET)
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
LwIPLock::LwIPLock() {
__inLWIP++;
ethernet_arch_lwip_begin();
}
LwIPLock::~LwIPLock() {
ethernet_arch_lwip_end();
__inLWIP--;
if (__needsIRQEN && !__inLWIP) {
__needsIRQEN = false;
ethernet_arch_lwip_gpio_unmask();
}
}
#else
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
+4
View File
@@ -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,
@@ -27,6 +28,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(TCA9555Component),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -38,6 +40,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
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):
+18 -1
View File
@@ -24,9 +24,18 @@ void TCA9555Component::setup() {
this->mark_failed();
return;
}
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&TCA9555Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
this->disable_loop();
}
void IRAM_ATTR TCA9555Component::gpio_intr(TCA9555Component *arg) { arg->enable_loop_soon_any_context(); }
void TCA9555Component::dump_config() {
ESP_LOGCONFIG(TAG, "TCA9555:");
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
@@ -36,6 +45,9 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
// Set mode mask bit
this->mode_mask_ |= 1 << pin;
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
// Clear mode mask bit
this->mode_mask_ &= ~(1 << pin);
@@ -43,7 +55,12 @@ void TCA9555Component::pin_mode(uint8_t pin, gpio::Flags flags) {
// Write GPIO to enable input mode
this->write_gpio_modes_();
}
void TCA9555Component::loop() { this->reset_pin_cache_(); }
void TCA9555Component::loop() {
this->reset_pin_cache_();
if (this->interrupt_pin_ != nullptr) {
this->disable_loop();
}
}
bool TCA9555Component::read_gpio_outputs_() {
if (this->is_failed())
+5
View File
@@ -24,7 +24,10 @@ class TCA9555Component : public Component,
void loop() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
static void IRAM_ATTR gpio_intr(TCA9555Component *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;
@@ -39,6 +42,8 @@ class TCA9555Component : public Component,
bool read_gpio_modes_();
bool write_gpio_modes_();
bool read_gpio_outputs_();
InternalGPIOPin *interrupt_pin_{nullptr};
};
/// Helper class to expose a TCA9555 pin as an internal input GPIO pin.
+6 -2
View File
@@ -1,6 +1,10 @@
mcp23016:
i2c_id: i2c_bus
id: mcp23016_hub
- i2c_id: i2c_bus
id: mcp23016_hub
- i2c_id: i2c_bus
id: mcp23016_hub_int
address: 0x21
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
+4
View File
@@ -2,6 +2,10 @@ pca6416a:
- id: pca6416a_hub
i2c_id: i2c_bus
address: 0x21
- id: pca6416a_hub_int
i2c_id: i2c_bus
address: 0x22
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
+4
View File
@@ -2,6 +2,10 @@ tca9555:
- id: tca9555_hub
i2c_id: i2c_bus
address: 0x21
- id: tca9555_hub_int
i2c_id: i2c_bus
address: 0x22
interrupt_pin: ${interrupt_pin}
binary_sensor:
- platform: gpio
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO15
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
@@ -1,3 +1,6 @@
substitutions:
interrupt_pin: GPIO2
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml