mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[pcf8574] Add reset option to preserve outputs across MCU reboots (#20589)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
40271fb597
commit
5acbd44067
@@ -10,6 +10,7 @@ from esphome.const import (
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
CONF_RESET,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
@@ -26,11 +27,14 @@ PCF8574GPIOPin = pcf8574_ns.class_("PCF8574GPIOPin", cg.GPIOPin)
|
||||
|
||||
CONF_PCF8574 = "pcf8574"
|
||||
CONF_PCF8575 = "pcf8575"
|
||||
# Keep in sync with PCF8574Component::reset_ in pcf8574.h.
|
||||
DEFAULT_RESET = True
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
|
||||
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
|
||||
cv.Optional(CONF_RESET, default=DEFAULT_RESET): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
@@ -44,6 +48,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
cg.add(var.set_pcf8575(config[CONF_PCF8575]))
|
||||
if (reset := config[CONF_RESET]) != DEFAULT_RESET:
|
||||
cg.add(var.set_reset(reset))
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
@@ -12,8 +12,13 @@ void PCF8574Component::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
this->write_gpio_();
|
||||
this->read_gpio_();
|
||||
if (this->reset_) {
|
||||
this->write_gpio_();
|
||||
this->read_gpio_();
|
||||
} else {
|
||||
this->output_mask_ = this->input_mask_;
|
||||
this->mode_mask_ = ~this->configured_input_pins_;
|
||||
}
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
@@ -40,8 +45,9 @@ void PCF8574Component::loop() {
|
||||
void PCF8574Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"PCF8574:\n"
|
||||
" Is PCF8575: %s",
|
||||
YESNO(this->pcf8575_));
|
||||
" Is PCF8575: %s\n"
|
||||
" Reset: %s",
|
||||
YESNO(this->pcf8575_), YESNO(this->reset_));
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
@@ -82,20 +88,12 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
bool PCF8574Component::read_gpio_() {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
bool success;
|
||||
uint8_t data[2];
|
||||
if (this->pcf8575_) {
|
||||
success = this->read_bytes_raw(data, 2);
|
||||
this->input_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0);
|
||||
} else {
|
||||
success = this->read_bytes_raw(data, 1);
|
||||
this->input_mask_ = data[0];
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
uint8_t data[2]{}; // Keep the bytes zero for the PCF8574's one-byte read.
|
||||
if (!this->read_bytes_raw(data, this->pcf8575_ ? 2 : 1)) {
|
||||
this->status_set_warning();
|
||||
return false;
|
||||
}
|
||||
this->input_mask_ = (uint16_t(data[1]) << 8) | data[0];
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
@@ -123,6 +121,11 @@ bool PCF8574Component::write_gpio_() {
|
||||
float PCF8574Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
void PCF8574GPIOPin::setup() { pin_mode(flags_); }
|
||||
void PCF8574GPIOPin::set_flags(gpio::Flags flags) {
|
||||
this->flags_ = flags;
|
||||
if (flags & gpio::FLAG_INPUT)
|
||||
this->parent_->register_input_pin(this->pin_);
|
||||
}
|
||||
void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
|
||||
@@ -16,6 +16,9 @@ class PCF8574Component final : public Component,
|
||||
PCF8574Component() = default;
|
||||
|
||||
void set_pcf8575(bool pcf8575) { pcf8575_ = pcf8575; }
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
// Called by pin setters before any component's setup or GPIO writes.
|
||||
void register_input_pin(uint8_t pin) { this->configured_input_pins_ |= 1 << pin; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
@@ -44,6 +47,11 @@ class PCF8574Component final : public Component,
|
||||
uint16_t output_mask_{0x00};
|
||||
/// The state read in read_gpio_ - 1 means HIGH, 0 means LOW
|
||||
uint16_t input_mask_{0x00};
|
||||
// Pins configured as inputs before setup; 1 means input. With reset disabled, all other pins
|
||||
// (including undeclared pins) are treated as outputs seeded from their observed levels.
|
||||
uint16_t configured_input_pins_{0x00};
|
||||
// Keep in sync with DEFAULT_RESET in __init__.py; codegen omits the setter for the default.
|
||||
bool reset_{true};
|
||||
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
@@ -63,7 +71,8 @@ class PCF8574GPIOPin final : public GPIOPin {
|
||||
void set_parent(PCF8574Component *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
void set_flags(gpio::Flags flags) { flags_ = flags; }
|
||||
// Call after set_parent() and set_pin(): input flags register the pin with the parent.
|
||||
void set_flags(gpio::Flags flags);
|
||||
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@ pcf8574:
|
||||
address: 0x22
|
||||
pcf8575: false
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset: true
|
||||
- id: pcf8574_preserved
|
||||
i2c_id: i2c_bus
|
||||
address: 0x23
|
||||
reset: false
|
||||
- id: pcf8575_preserved
|
||||
i2c_id: i2c_bus
|
||||
address: 0x24
|
||||
pcf8575: true
|
||||
reset: false
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -18,6 +28,18 @@ binary_sensor:
|
||||
number: 1
|
||||
mode: INPUT
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
id: pcf8574_preserved_input
|
||||
pin:
|
||||
pcf8574: pcf8574_preserved
|
||||
number: 1
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: pcf8575_preserved_input
|
||||
pin:
|
||||
pcf8574: pcf8575_preserved
|
||||
number: 15
|
||||
mode: INPUT
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
@@ -27,3 +49,13 @@ output:
|
||||
number: 0
|
||||
mode: OUTPUT
|
||||
inverted: false
|
||||
- platform: gpio
|
||||
id: pcf8574_preserved_output
|
||||
pin:
|
||||
pcf8574: pcf8574_preserved
|
||||
number: 0
|
||||
- platform: gpio
|
||||
id: pcf8575_preserved_output
|
||||
pin:
|
||||
pcf8574: pcf8575_preserved
|
||||
number: 8
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/pcf8574/pcf8574.h"
|
||||
|
||||
namespace esphome::pcf8574::testing {
|
||||
|
||||
// Model quasi-bidirectional readback separately from the output latch.
|
||||
class PortBus : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override {
|
||||
EXPECT_EQ(address, 0x21);
|
||||
if (read_count != 0) {
|
||||
EXPECT_EQ(read_count, this->width);
|
||||
this->reads++;
|
||||
if (this->fail_read)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
uint16_t levels = this->latch & ~this->external_low;
|
||||
for (size_t i = 0; i < read_count; i++)
|
||||
read_buffer[i] = levels >> (8 * i);
|
||||
}
|
||||
if (write_count != 0) {
|
||||
EXPECT_EQ(write_count, this->width);
|
||||
this->writes++;
|
||||
this->latch = write_buffer[0];
|
||||
if (write_count == 2)
|
||||
this->latch |= uint16_t(write_buffer[1]) << 8;
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
size_t width{1};
|
||||
uint16_t latch{0};
|
||||
uint16_t external_low{0};
|
||||
unsigned reads{0};
|
||||
bool fail_read{false};
|
||||
unsigned writes{0};
|
||||
};
|
||||
|
||||
class PCF8574ResetTest : public ::testing::TestWithParam<bool> {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->bus_.width = GetParam() ? 2 : 1;
|
||||
this->mask_ = GetParam() ? 0xFFFF : 0xFF;
|
||||
this->high_pin_ = GetParam() ? 15 : 7;
|
||||
this->bus_.latch = 0xA55A & this->mask_;
|
||||
this->component_.set_i2c_bus(&this->bus_);
|
||||
this->component_.set_i2c_address(0x21);
|
||||
this->component_.set_pcf8575(GetParam());
|
||||
}
|
||||
|
||||
void configure_input_(uint8_t number) {
|
||||
PCF8574GPIOPin pin;
|
||||
pin.set_parent(&this->component_);
|
||||
pin.set_pin(number);
|
||||
pin.set_flags(gpio::FLAG_INPUT);
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
EXPECT_EQ(this->bus_.reads, 0);
|
||||
}
|
||||
|
||||
PortBus bus_;
|
||||
PCF8574Component component_;
|
||||
uint16_t mask_;
|
||||
uint8_t high_pin_;
|
||||
};
|
||||
|
||||
TEST_P(PCF8574ResetTest, DefaultResetRetainsStartupAndPinModeBehavior) {
|
||||
this->configure_input_(1);
|
||||
|
||||
this->component_.setup();
|
||||
ASSERT_EQ(this->bus_.writes, 1);
|
||||
EXPECT_EQ(this->bus_.latch, this->mask_);
|
||||
EXPECT_EQ(this->bus_.reads, 2);
|
||||
|
||||
this->component_.pin_mode(0, gpio::FLAG_OUTPUT);
|
||||
EXPECT_EQ(this->bus_.writes, 1);
|
||||
|
||||
this->component_.digital_write(0, false);
|
||||
EXPECT_EQ(this->bus_.latch, this->mask_ & ~1);
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, PreservesOtherOutputsBeforeAndAfterTheirPinSetup) {
|
||||
this->component_.set_reset(false);
|
||||
const uint16_t initial = this->bus_.latch;
|
||||
|
||||
this->component_.setup();
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
EXPECT_EQ(this->bus_.reads, 1);
|
||||
EXPECT_EQ(this->bus_.latch, initial);
|
||||
|
||||
// Consumers can write before calling their GPIO pin's setup().
|
||||
this->component_.digital_write(0, true);
|
||||
EXPECT_EQ(this->bus_.latch, initial | 1);
|
||||
|
||||
this->component_.pin_mode(0, gpio::FLAG_OUTPUT);
|
||||
this->component_.pin_mode(this->high_pin_, gpio::FLAG_OUTPUT);
|
||||
const uint16_t high_pin_mask = 1 << this->high_pin_;
|
||||
this->component_.digital_write(this->high_pin_, (initial & high_pin_mask) == 0);
|
||||
const uint16_t expected = (initial | 1) ^ high_pin_mask;
|
||||
EXPECT_EQ(this->bus_.latch, expected);
|
||||
|
||||
this->component_.digital_write(0, false);
|
||||
EXPECT_EQ(this->bus_.latch, expected & ~1);
|
||||
ASSERT_EQ(this->bus_.writes, 3);
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, MixedInputsAreReleasedBeforeTheirConsumersInitialize) {
|
||||
const uint16_t inputs = (1 << 1) | (1 << this->high_pin_);
|
||||
this->bus_.latch |= inputs;
|
||||
const uint16_t initial = this->bus_.latch;
|
||||
this->bus_.external_low = inputs;
|
||||
|
||||
this->component_.set_reset(false);
|
||||
this->configure_input_(1);
|
||||
this->configure_input_(this->high_pin_);
|
||||
this->component_.setup();
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
|
||||
this->component_.digital_write(0, true);
|
||||
EXPECT_EQ(this->bus_.latch, initial | 1);
|
||||
|
||||
this->component_.pin_mode(1, gpio::FLAG_INPUT);
|
||||
this->component_.pin_mode(this->high_pin_, gpio::FLAG_INPUT);
|
||||
EXPECT_EQ(this->bus_.latch, initial | 1);
|
||||
EXPECT_FALSE(this->component_.digital_read(1));
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, InputSetupBeforeOutputSetupPreservesOutputs) {
|
||||
this->component_.set_reset(false);
|
||||
this->configure_input_(1);
|
||||
const uint16_t initial = this->bus_.latch;
|
||||
|
||||
this->component_.setup();
|
||||
this->component_.pin_mode(1, gpio::FLAG_INPUT);
|
||||
EXPECT_EQ(this->bus_.latch, initial);
|
||||
|
||||
this->component_.pin_mode(0, gpio::FLAG_OUTPUT);
|
||||
this->component_.digital_write(0, true);
|
||||
EXPECT_EQ(this->bus_.latch, initial | 1);
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, LoadedHighOutputIsLatchedLowOnFirstWrite) {
|
||||
const uint16_t loaded_pin = 1 << this->high_pin_;
|
||||
this->bus_.latch |= loaded_pin;
|
||||
this->bus_.external_low = loaded_pin;
|
||||
const uint16_t initial = this->bus_.latch;
|
||||
this->component_.set_reset(false);
|
||||
|
||||
this->component_.setup();
|
||||
this->component_.pin_mode(this->high_pin_, gpio::FLAG_OUTPUT);
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
EXPECT_EQ(this->bus_.latch, initial);
|
||||
|
||||
this->component_.digital_write(0, true);
|
||||
EXPECT_EQ(this->bus_.latch, (initial | 1) & ~loaded_pin);
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, UndeclaredLowPinRemainsDrivenAfterExternalRelease) {
|
||||
const uint16_t undeclared_pin = 1 << this->high_pin_;
|
||||
this->bus_.latch |= undeclared_pin;
|
||||
this->bus_.external_low = undeclared_pin;
|
||||
const uint16_t initial = this->bus_.latch;
|
||||
this->component_.set_reset(false);
|
||||
|
||||
this->component_.setup();
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
EXPECT_EQ(this->bus_.latch, initial);
|
||||
|
||||
this->component_.digital_write(0, true);
|
||||
this->bus_.external_low = 0;
|
||||
EXPECT_EQ(this->bus_.latch, (initial | 1) & ~undeclared_pin);
|
||||
EXPECT_FALSE(this->component_.digital_read(this->high_pin_));
|
||||
}
|
||||
|
||||
TEST_P(PCF8574ResetTest, InitialReadFailurePreventsAllWrites) {
|
||||
this->bus_.fail_read = true;
|
||||
|
||||
this->component_.set_reset(false);
|
||||
this->component_.setup();
|
||||
EXPECT_TRUE(this->component_.is_failed());
|
||||
|
||||
this->component_.pin_mode(1, gpio::FLAG_INPUT);
|
||||
this->component_.digital_write(0, true);
|
||||
EXPECT_EQ(this->bus_.writes, 0);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(PCF8574AndPCF8575, PCF8574ResetTest, ::testing::Bool());
|
||||
|
||||
} // namespace esphome::pcf8574::testing
|
||||
@@ -0,0 +1,35 @@
|
||||
"""PCF8574 reset configuration and code generation."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import pcf8574
|
||||
from esphome.const import CONF_ID, CONF_RESET
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"options, expected",
|
||||
[
|
||||
({}, True),
|
||||
({"reset": True}, True),
|
||||
({"reset": False}, False),
|
||||
],
|
||||
)
|
||||
async def test_reset_configuration(options: dict, expected: bool) -> None:
|
||||
config = pcf8574.CONFIG_SCHEMA({CONF_ID: "hub", **options})
|
||||
assert config[CONF_RESET] is expected
|
||||
var = Mock()
|
||||
with (
|
||||
patch.object(pcf8574.cg, "new_Pvariable", return_value=var),
|
||||
patch.object(pcf8574.cg, "register_component", new_callable=AsyncMock),
|
||||
patch.object(pcf8574.i2c, "register_i2c_device", new_callable=AsyncMock),
|
||||
patch.object(pcf8574.cg, "add"),
|
||||
):
|
||||
await pcf8574.to_code(config)
|
||||
|
||||
if expected:
|
||||
var.set_reset.assert_not_called()
|
||||
else:
|
||||
var.set_reset.assert_called_once_with(False)
|
||||
Reference in New Issue
Block a user