[pca9554] Add a device option with the PCA953x/PCA955x/TCA955x names and the PCAL9538 and PCAL9555 expanders (#15449)

This commit is contained in:
Pat Satyshur
2026-10-21 03:26:25 +00:00
committed by GitHub
parent ded94c6698
commit 940de19d3b
8 changed files with 471 additions and 47 deletions
+115 -22
View File
@@ -1,13 +1,23 @@
from typing import NamedTuple
from esphome import pins
import esphome.codegen as cg
from esphome.components import gpio_expander, i2c
from esphome.components import i2c
from esphome.components.const import CONF_DRIVE_STRENGTH
from esphome.components.gpio_expander import validate_interrupt_pin
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_DEVICE,
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_INTERRUPT_PIN,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
@@ -17,46 +27,106 @@ AUTO_LOAD = ["gpio_expander"]
DEPENDENCIES = ["i2c"]
DOMAIN = "pca9554"
MULTI_CONF = True
CONF_PIN_COUNT = "pin_count"
pca9554_ns = cg.esphome_ns.namespace("pca9554")
CONF_LATCH = "latch"
CONF_PCA9554 = "pca9554"
CONF_PIN_COUNT = "pin_count"
pca9554_ns = cg.esphome_ns.namespace("pca9554")
PCA9554Component = pca9554_ns.class_("PCA9554Component", cg.Component, i2c.I2CDevice)
PCA9554GPIOPin = pca9554_ns.class_(
"PCA9554GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9554Component)
)
CONF_PCA9554 = "pca9554"
CONFIG_SCHEMA = (
class _Device(NamedTuple):
pins: int
# Lowest I2C address, with all address pins low
address: int
# Has the PCAL registers: pull resistors, input latch, drive strength, interrupt mask
pcal: bool = False
DEVICES: dict[str, _Device] = {
"PCA9536": _Device(4, 0x41),
"PCA9534": _Device(8, 0x20),
"PCA9534A": _Device(8, 0x38),
"PCA9538": _Device(8, 0x70),
"PCA9554": _Device(8, 0x20),
"PCA9554A": _Device(8, 0x38),
"TCA9554": _Device(8, 0x20),
"PCA9535": _Device(16, 0x20),
"PCA9555": _Device(16, 0x20),
"TCA9535": _Device(16, 0x20),
"TCA9555": _Device(16, 0x20),
"PCAL9538": _Device(8, 0x70, True),
"PCAL9555": _Device(16, 0x20, True),
}
# The device a legacy `pin_count:` stands for
_LEGACY_DEVICES = {4: "PCA9536", 8: "PCA9554", 16: "PCA9555"}
# Datasheet drive levels and their register codes
DRIVE_STRENGTHS = {0.25: 0, 0.5: 1, 0.75: 2, 1.0: 3}
def _validate_hub(config: ConfigType) -> ConfigType:
count = config.get(CONF_PIN_COUNT)
if (device := config.get(CONF_DEVICE)) is None:
device = config[CONF_DEVICE] = _LEGACY_DEVICES[8 if count is None else count]
elif count is not None and count != DEVICES[device].pins:
raise cv.Invalid(
f"'{CONF_PIN_COUNT}' ({count}) does not match the {DEVICES[device].pins} "
f"pins of device {device}; remove '{CONF_PIN_COUNT}'"
)
config.setdefault(CONF_ADDRESS, DEVICES[device].address)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(PCA9554Component),
cv.Optional(CONF_PIN_COUNT, default=8): cv.one_of(4, 8, 16),
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
cv.GenerateID(CONF_I2C_ID): cv.use_id(i2c.I2CBus),
# The default depends on the device, so _validate_hub fills it in
cv.Optional(CONF_ADDRESS): cv.i2c_address,
cv.Optional(CONF_DEVICE): cv.one_of(*DEVICES, upper=True),
cv.Optional(CONF_PIN_COUNT): cv.one_of(4, 8, 16),
cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(
i2c.i2c_device_schema(0x20)
) # Note: 0x20 for the non-A part. The PCA9554A parts start at addess 0x38
).extend(cv.COMPONENT_SCHEMA),
_validate_hub,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_pin_count(config[CONF_PIN_COUNT]))
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
device = DEVICES[config[CONF_DEVICE]]
var = await i2c.new_i2c_component(config, device.pins)
if device.pcal:
# Only a build with a PCAL part carries the code for its extra registers
cg.add_define("USE_PCA9554_PCAL")
cg.add(var.set_pcal())
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
def validate_mode(value: ConfigType) -> ConfigType:
"""Reject contradictory modes; whether the device supports a mode is checked in final validation."""
pins.gpio_validate_input_xor_output(value)
if value[CONF_PULLDOWN] and value[CONF_PULLUP]:
raise cv.Invalid("Pull-up and pull-down are mutually exclusive")
return value
PCA9554_PIN_SCHEMA = pins.gpio_base_schema(
PCA9554GPIOPin,
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=pins.gpio_validate_input_xor_output,
modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP, CONF_PULLDOWN],
mode_validator=validate_mode,
).extend(
{
cv.Required(CONF_PCA9554): pins.use_id_or_address(PCA9554Component),
cv.Optional(CONF_LATCH, default=False): cv.boolean,
cv.Optional(CONF_DRIVE_STRENGTH): cv.All(
cv.percentage, cv.one_of(*DRIVE_STRENGTHS)
),
}
)
@@ -64,13 +134,36 @@ PCA9554_PIN_SCHEMA = pins.gpio_base_schema(
def pca9554_pin_final_validate(
pin_config: ConfigType, parent_config: ConfigType
) -> None:
count = parent_config[CONF_PIN_COUNT]
if pin_config[CONF_NUMBER] >= count:
raise cv.Invalid(f"Pin number must be in range 0-{count - 1}")
device_name = parent_config[CONF_DEVICE]
device = DEVICES[device_name]
if pin_config[CONF_NUMBER] >= device.pins:
raise cv.Invalid(f"Pin number must be in range 0-{device.pins - 1}")
mode = pin_config[CONF_MODE]
if pin_config[CONF_LATCH] and mode[CONF_OUTPUT]:
raise cv.Invalid(f"'{CONF_LATCH}' is not valid for an output pin")
if CONF_DRIVE_STRENGTH in pin_config and mode[CONF_INPUT]:
raise cv.Invalid(f"'{CONF_DRIVE_STRENGTH}' is not valid for an input pin")
if device.pcal:
return
for name in (CONF_PULLUP, CONF_PULLDOWN):
if mode[name]:
raise cv.Invalid(f"Pin mode '{name}' is not valid for device {device_name}")
for key in (CONF_LATCH, CONF_DRIVE_STRENGTH):
if pin_config.get(key):
raise cv.Invalid(f"'{key}' is not supported by device {device_name}")
@pins.PIN_SCHEMA_REGISTRY.register(
CONF_PCA9554, PCA9554_PIN_SCHEMA, pca9554_pin_final_validate
)
async def pca9554_pin_to_code(config: ConfigType) -> MockObj:
return await pins.new_parented_pin(config, CONF_PCA9554)
var = await pins.new_parented_pin(config, CONF_PCA9554)
parent = await cg.get_variable(config[CONF_PCA9554])
num = config[CONF_NUMBER]
# Final validation only lets these through on a PCAL hub
if config[CONF_LATCH]:
cg.add(parent.set_latch(num))
if (drive_strength := config.get(CONF_DRIVE_STRENGTH)) is not None:
cg.add(parent.set_drive_strength(num, DRIVE_STRENGTHS[drive_strength]))
return var
+88 -14
View File
@@ -4,15 +4,25 @@
namespace esphome::pca9554 {
// for 16 bit expanders, these addresses will be doubled.
const uint8_t INPUT_REG = 0;
const uint8_t OUTPUT_REG = 1;
const uint8_t INVERT_REG = 2;
const uint8_t CONFIG_REG = 3;
static constexpr uint8_t INPUT_REG = 0;
static constexpr uint8_t OUTPUT_REG = 1;
static constexpr uint8_t INVERT_REG = 2;
static constexpr uint8_t CONFIG_REG = 3;
#ifdef USE_PCA9554_PCAL
// PCAL registers, as offsets from PCAL_BASE spaced by the register width like the ones above.
static constexpr uint8_t PCAL_BASE = 0x40;
static constexpr uint8_t OUTPUT_DRIVE_0 = 0;
static constexpr uint8_t OUTPUT_DRIVE_1 = 1;
static constexpr uint8_t INPUT_LATCH = 2;
static constexpr uint8_t PUPD_ENABLE = 3;
static constexpr uint8_t PUPD_SEL = 4;
static constexpr uint8_t INTERRUPT_MASK = 5;
#endif
ESPHOME_LOG_TAG(TAG, "pca9554");
void PCA9554Component::setup() {
this->reg_width_ = (this->pin_count_ + 7) / 8;
// Test to see if device exists
if (!this->read_inputs_()) {
ESP_LOGE(TAG, "PCA95xx not detected at 0x%02X", this->address_);
@@ -29,6 +39,13 @@ void PCA9554Component::setup() {
// All outputs low
this->output_mask_ = 0;
this->write_register_(OUTPUT_REG, this->output_mask_);
#ifdef USE_PCA9554_PCAL
// The chip keeps these across an MCU reboot
if (this->pcal_ && !this->write_pcal_shadows_()) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
#endif
// Read the inputs
this->read_inputs_();
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
@@ -45,7 +62,9 @@ void PCA9554Component::setup() {
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
void PCA9554Component::loop() {
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
this->reset_pin_cache_();
@@ -62,6 +81,9 @@ void PCA9554Component::dump_config() {
"PCA9554:\n"
" I/O Pins: %d",
this->pin_count_);
#ifdef USE_PCA9554_PCAL
ESP_LOGCONFIG(TAG, " PCAL registers: %s", YESNO(this->pcal_));
#endif
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_I2C_DEVICE(this)
}
@@ -82,11 +104,16 @@ void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
} else {
this->output_mask_ &= ~(1 << pin);
}
#ifdef USE_PCA9554_PCAL
if (this->write_register_(OUTPUT_REG, this->output_mask_))
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
#else
this->write_register_(OUTPUT_REG, this->output_mask_);
#endif
}
void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
if (flags & gpio::FLAG_INPUT) {
// Clear mode mask bit
this->config_mask_ &= ~(1 << pin);
// Enable polling loop for input pins (not needed for interrupt-driven mode
@@ -94,11 +121,24 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
if (this->interrupt_pin_ == nullptr) {
this->enable_loop();
}
} else if (flags == gpio::FLAG_OUTPUT) {
} else if (flags & gpio::FLAG_OUTPUT) {
// Set mode mask bit
this->config_mask_ |= 1 << pin;
}
this->write_register_(CONFIG_REG, ~this->config_mask_);
#ifdef USE_PCA9554_PCAL
if (this->pcal_) {
const uint16_t bit = 1U << pin;
// A masked pin never asserts INT, so only inputs are unmasked
this->update_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_, bit, this->config_mask_ & bit);
const bool pull = flags & (gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN);
// The select bit means nothing while the pull is disabled
if (pull)
this->update_pcal_register_(PUPD_SEL, this->pupd_select_, bit, (flags & gpio::FLAG_PULLUP) ? bit : 0);
this->update_pcal_register_(PUPD_ENABLE, this->pupd_enable_, bit, pull ? bit : 0);
}
#endif
}
bool PCA9554Component::read_inputs_() {
@@ -109,30 +149,64 @@ bool PCA9554Component::read_inputs_() {
return false;
}
this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_);
if (!this->status_comm_result(this->last_error_ == i2c::ERROR_OK)) {
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_);
const i2c::ErrorCode err = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_);
if (!this->status_comm_result(err == i2c::ERROR_OK)) {
ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) err);
return false;
}
this->input_mask_ = inputs[0];
if (this->reg_width_ == 2) {
this->input_mask_ |= inputs[1] << 8;
}
#ifdef USE_PCA9554_PCAL
// The bus answers again, so retry the settings a failed write left off the chip
this->retry_pcal_shadows_();
#endif
return true;
}
bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) {
bool PCA9554Component::write_address_(uint8_t address, uint16_t value) {
uint8_t outputs[2];
outputs[0] = (uint8_t) value;
outputs[1] = (uint8_t) (value >> 8);
this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_);
if (!this->status_comm_result(this->last_error_ == i2c::ERROR_OK)) {
ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_);
const i2c::ErrorCode err = this->write_register(address, outputs, this->reg_width_);
if (!this->status_comm_result(err == i2c::ERROR_OK)) {
ESP_LOGE(TAG, "write_register(): I2C I/O error: %d", (int) err);
return false;
}
return true;
}
#ifdef USE_PCA9554_PCAL
bool PCA9554Component::write_pcal_register_(uint8_t reg, uint16_t value) {
if (this->write_address_(PCAL_BASE + reg * this->reg_width_, value))
return true;
this->pcal_dirty_ = true;
return false;
}
bool PCA9554Component::write_pcal_shadows_() {
this->pcal_dirty_ = false;
// Every write runs so one failure does not hide the others; each failure sets pcal_dirty_ again
bool ok = this->write_pcal_register_(PUPD_ENABLE, this->pupd_enable_);
ok &= this->write_pcal_register_(PUPD_SEL, this->pupd_select_);
ok &= this->write_pcal_register_(INPUT_LATCH, this->latch_);
ok &= this->write_pcal_register_(INTERRUPT_MASK, this->interrupt_mask_);
ok &= this->write_pcal_register_(OUTPUT_DRIVE_0, static_cast<uint16_t>(this->drive_));
ok &= this->write_pcal_register_(OUTPUT_DRIVE_1, static_cast<uint16_t>(this->drive_ >> (8 * this->reg_width_)));
return ok;
}
void PCA9554Component::update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value) {
const uint16_t updated = (shadow & ~mask) | (value & mask);
// The shadows hold the wanted state; an unchanged value is already on the chip unless a write failed
if (updated == shadow && !this->pcal_dirty_)
return;
shadow = updated;
this->write_pcal_register_(reg, shadow);
}
#endif
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
} // namespace esphome::pca9554
+48 -11
View File
@@ -12,7 +12,7 @@ class PCA9554Component final : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
public:
PCA9554Component() = default;
explicit PCA9554Component(uint8_t pin_count) : pin_count_(pin_count), reg_width_((pin_count + 7) / 8) {}
/// Check i2c availability and setup masks
void setup() override;
@@ -24,33 +24,70 @@ class PCA9554Component final : public Component,
void dump_config() override;
void set_pin_count(size_t pin_count) { this->pin_count_ = pin_count; }
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
#ifdef USE_PCA9554_PCAL
/// The PCAL parts add pull resistors, input latches, an interrupt mask and drive strength.
void set_pcal() { this->pcal_ = true; }
/// Only updates the shadow, which setup() writes.
void set_latch(uint8_t pin) { this->latch_ |= 1U << pin; }
/// Only updates the shadow, which setup() writes. 0 is a quarter of the full drive strength, 3 the full strength.
void set_drive_strength(uint8_t pin, uint8_t strength) {
const uint8_t shift = 2 * pin;
this->drive_ = (this->drive_ & ~(3UL << shift)) | (uint32_t(strength) << shift);
}
#endif
protected:
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
bool read_inputs_();
bool write_register_(uint8_t reg, uint16_t value);
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
bool write_address_(uint8_t address, uint16_t value);
#ifdef USE_PCA9554_PCAL
bool write_pcal_register_(uint8_t reg, uint16_t value);
/// Write every PCAL shadow to the chip; false when any write failed
bool write_pcal_shadows_();
/// Retry the shadows when an earlier PCAL write failed
void retry_pcal_shadows_() {
if (this->pcal_dirty_)
this->write_pcal_shadows_();
}
/// Changes the masked bits of a shadow and writes the register only when it changed.
void update_pcal_register_(uint8_t reg, uint16_t &shadow, uint16_t mask, uint16_t value);
#endif
// Virtual methods from CachedGpioExpander
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;
/// number of bits the expander has
size_t pin_count_{8};
/// width of registers
size_t reg_width_{1};
InternalGPIOPin *interrupt_pin_{nullptr};
#ifdef USE_PCA9554_PCAL
/// Drive strength shadow, two bits per pin; the low reg_width_ bytes go to OUTPUT_DRIVE_0
uint32_t drive_{0xFFFFFFFF};
#endif
/// Mask for the pin config - 1 means OUTPUT, 0 means INPUT
uint16_t config_mask_{0x00};
/// The mask to write as output state - 1 means HIGH, 0 means LOW
uint16_t output_mask_{0x00};
/// The state of the actual input pin states - 1 means HIGH, 0 means LOW
/// The state of the actual input pins - 1 means HIGH, 0 means LOW
uint16_t input_mask_{0x00};
/// Storage for last I2C error seen
esphome::i2c::ErrorCode last_error_;
InternalGPIOPin *interrupt_pin_{nullptr};
#ifdef USE_PCA9554_PCAL
// Shadows of the PCAL registers. setup() writes them, so every part starts with the pulls disconnected
// whatever its power-on state is (the PCAL9555 powers up with pull-ups enabled).
uint16_t pupd_enable_{0x0000};
uint16_t pupd_select_{0xFFFF};
uint16_t latch_{0x0000};
uint16_t interrupt_mask_{0xFFFF};
#endif
/// number of bits the expander has
const uint8_t pin_count_;
/// width of registers
const uint8_t reg_width_;
#ifdef USE_PCA9554_PCAL
bool pcal_{false};
bool pcal_dirty_{false}; // a PCAL write failed; retried after the next good read or output write
#endif
};
using PCA9554GPIOPin = gpio_expander::ExpanderGPIOPin<PCA9554Component>;
+1
View File
@@ -257,6 +257,7 @@
#define USE_OTA_STATE_LISTENER
#define USE_OUTPUT
#define USE_OUTPUT_FLOAT_POWER_SCALING
#define USE_PCA9554_PCAL
#define USE_POWER_SUPPLY
// Only defined by key-lookup preference backends; the slot-based platforms
// (esp8266, rp2040) never set it in generated builds, and their preferences
@@ -0,0 +1,68 @@
esphome:
name: test
esp32:
board: esp32dev
i2c:
id: i2c_bus
sda: GPIO21
scl: GPIO22
pca9554:
- id: plain_hub
address: 0x20
device: PCA9554
- id: pcal_hub
address: 0x70
device: PCAL9555
- id: pca9538_hub
device: PCA9538
binary_sensor:
- platform: gpio
id: plain_input
name: Plain input
pin:
pca9554: plain_hub
number: 1
mode: INPUT
- platform: gpio
id: pcal_input
name: PCAL input
pin:
pca9554: pcal_hub
number: 9
mode:
input: true
pullup: true
latch: true
switch:
- platform: gpio
id: pcal_weak_output
pin:
pca9554: pcal_hub
number: 10
mode: OUTPUT
drive_strength: 25%
- platform: gpio
id: pcal_full_output
pin:
pca9554: pcal_hub
number: 11
mode: OUTPUT
drive_strength: 100%
- platform: gpio
id: pcal_mid_output
pin:
pca9554: pcal_hub
number: 12
mode: OUTPUT
drive_strength: 75%
- platform: gpio
id: pca9538_output
pin:
pca9554: pca9538_hub
number: 7
mode: OUTPUT
@@ -0,0 +1,126 @@
"""Tests for the PCA9554 device table and its pin validation."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv
from esphome.components.pca9554 import (
CONFIG_SCHEMA,
PCA9554_PIN_SCHEMA,
pca9554_pin_final_validate,
)
from esphome.types import ConfigType
def _hub(**overrides: object) -> ConfigType:
return CONFIG_SCHEMA({"id": "hub", **overrides})
def _pin(number: int, mode: dict, **overrides: object) -> ConfigType:
return PCA9554_PIN_SCHEMA(
{"pca9554": "hub", "number": number, "mode": mode, **overrides}
)
def test_pin_count_must_match_the_device() -> None:
"""A stale pin_count next to a device would address every register wrongly."""
with pytest.raises(
cv.Invalid, match="does not match the 8 pins of device PCAL9538"
):
_hub(device="PCAL9538", pin_count=16)
@pytest.mark.parametrize(
("overrides", "device"),
[
({}, "PCA9554"),
({"pin_count": 4}, "PCA9536"),
({"pin_count": 8}, "PCA9554"),
({"pin_count": 16}, "PCA9555"),
],
)
def test_legacy_pin_count_maps_to_a_device(overrides: dict, device: str) -> None:
assert _hub(**overrides)["device"] == device
@pytest.mark.parametrize(
("device", "address"),
[
("PCA9554", 0x20),
("PCA9554A", 0x38),
("PCA9536", 0x41),
("PCA9538", 0x70),
("PCAL9538", 0x70),
],
)
def test_address_defaults_to_the_device_base(device: str, address: int) -> None:
assert _hub(device=device)["address"] == address
assert _hub(device=device, address=0x27)["address"] == 0x27
def test_pin_number_follows_the_device() -> None:
with pytest.raises(cv.Invalid, match="range 0-3"):
pca9554_pin_final_validate(_pin(4, {"input": True}), _hub(device="PCA9536"))
pca9554_pin_final_validate(_pin(15, {"input": True}), _hub(device="PCAL9555"))
@pytest.mark.parametrize(
("mode", "overrides", "message"),
[
({"input": True, "pullup": True}, {}, "Pin mode 'pullup' is not valid"),
({"input": True, "pulldown": True}, {}, "Pin mode 'pulldown' is not valid"),
({"input": True}, {"latch": True}, "'latch' is not supported"),
({"output": True}, {"drive_strength": "100%"}, "'drive_strength' is not"),
],
)
def test_pcal_only_settings_are_rejected_on_a_plain_part(
mode: dict, overrides: dict, message: str
) -> None:
pin = _pin(1, mode, **overrides)
with pytest.raises(cv.Invalid, match=f"{message}.*device PCA9554"):
pca9554_pin_final_validate(pin, _hub(device="PCA9554"))
pca9554_pin_final_validate(pin, _hub(device="PCAL9538"))
def test_outputs_cannot_be_latched() -> None:
with pytest.raises(cv.Invalid, match="'latch' is not valid for an output pin"):
pca9554_pin_final_validate(
_pin(1, {"output": True}, latch=True), _hub(device="PCAL9538")
)
def test_inputs_take_no_drive_strength() -> None:
with pytest.raises(cv.Invalid, match="'drive_strength' is not valid for an input"):
pca9554_pin_final_validate(
_pin(1, {"input": True}, drive_strength="50%"), _hub(device="PCAL9538")
)
@pytest.mark.parametrize("value", ["30%", "0%", 2])
def test_drive_strength_must_be_a_datasheet_level(value: object) -> None:
with pytest.raises(cv.Invalid):
_pin(1, {"output": True}, drive_strength=value)
def test_pcal_code_is_only_generated_for_pcal_hubs(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The define and the hub setters exist only where a PCAL part is configured."""
main_cpp = generate_main(component_config_path("pca9554.yaml"))
for call in (
"pcal_hub->set_pcal();",
"pcal_hub->set_latch(9);",
"pcal_hub->set_drive_strength(12, 2);",
"pcal_hub->set_drive_strength(10, 0);",
"pcal_hub->set_drive_strength(11, 3);",
):
assert main_cpp.count(call) == 1
for call in ("set_pcal(", "set_latch(", "set_drive_strength("):
assert f"plain_hub->{call}" not in main_cpp
assert f"pca9538_hub->{call}" not in main_cpp
# 0x70, the PCA9538 default
assert "pca9538_hub->set_i2c_address(112);" in main_cpp
+25
View File
@@ -8,6 +8,14 @@ pca9554:
pin_count: 8
address: 0x3E
interrupt_pin: ${interrupt_pin}
- id: pcal9538
i2c_id: i2c_bus
address: 0x70
device: PCAL9538
- id: pcal9555
i2c_id: i2c_bus
address: 0x71
device: PCAL9555
binary_sensor:
- platform: gpio
@@ -25,3 +33,20 @@ binary_sensor:
number: 0
mode: OUTPUT
inverted: false
- platform: gpio
id: pcal9538_input
pin:
pca9554: pcal9538
number: 3
mode:
input: true
pullup: true
latch: true
# A pin on the second port of a 16 bit PCAL part, with a non-default drive strength
- platform: gpio
id: pcal9555_output
pin:
pca9554: pcal9555
number: 12
mode: OUTPUT
drive_strength: 50%