diff --git a/CODEOWNERS b/CODEOWNERS index 82aa071dc4..5a8ef76c44 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -188,6 +188,7 @@ esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core +esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @hwstar esphome/components/pcf85063/* @brogon esphome/components/pid/* @OttoWinter diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py new file mode 100644 index 0000000000..574d8dce91 --- /dev/null +++ b/esphome/components/pca6416a/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@Mat931"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +pca6416a_ns = cg.esphome_ns.namespace("pca6416a") + +PCA6416AComponent = pca6416a_ns.class_("PCA6416AComponent", cg.Component, i2c.I2CDevice) +PCA6416AGPIOPin = pca6416a_ns.class_( + "PCA6416AGPIOPin", cg.GPIOPin, cg.Parented.template(PCA6416AComponent) +) + +CONF_PCA6416A = "pca6416a" +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)}) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) + + +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) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +PCA6416A_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(PCA6416AGPIOPin), + cv.Required(CONF_PCA6416A): cv.use_id(PCA6416AComponent), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=16), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register("pca6416a", PCA6416A_PIN_SCHEMA) +async def pca6416a_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_PCA6416A]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp new file mode 100644 index 0000000000..1f4e315644 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -0,0 +1,174 @@ +#include "pca6416a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pca6416a { + +enum PCA6416AGPIORegisters { + // 0 side + PCA6416A_INPUT0 = 0x00, + PCA6416A_OUTPUT0 = 0x02, + PCA6416A_INVERT0 = 0x04, + PCA6416A_CONFIG0 = 0x06, + PCAL6416A_PULL_EN0 = 0x46, + PCAL6416A_PULL_DIR0 = 0x48, + // 1 side + PCA6416A_INPUT1 = 0x01, + PCA6416A_OUTPUT1 = 0x03, + PCA6416A_INVERT1 = 0x05, + PCA6416A_CONFIG1 = 0x07, + PCAL6416A_PULL_EN1 = 0x47, + PCAL6416A_PULL_DIR1 = 0x49, +}; + +static const char *const TAG = "pca6416a"; + +void PCA6416AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCA6416A..."); + // Test to see if device exists + uint8_t value; + if (!this->read_register_(PCA6416A_INPUT0, &value)) { + ESP_LOGE(TAG, "PCA6416A not available under 0x%02X", this->address_); + this->mark_failed(); + return; + } + + // Test to see if the device supports pull-up resistors + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == esphome::i2c::ERROR_OK) { + this->has_pullup_ = true; + } + + // No polarity inversion + this->write_register_(PCA6416A_INVERT0, 0); + this->write_register_(PCA6416A_INVERT1, 0); + // Set all pins to input + this->write_register_(PCA6416A_CONFIG0, 0xff); + this->write_register_(PCA6416A_CONFIG1, 0xff); + // Read current output register state + this->read_register_(PCA6416A_OUTPUT0, &this->output_0_); + this->read_register_(PCA6416A_OUTPUT1, &this->output_1_); + + ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void PCA6416AComponent::dump_config() { + if (this->has_pullup_) { + ESP_LOGCONFIG(TAG, "PCAL6416A:"); + } else { + ESP_LOGCONFIG(TAG, "PCA6416A:"); + } + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCA6416A failed!"); + } +} + +bool PCA6416AComponent::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; + uint8_t value = 0; + this->read_register_(reg_addr, &value); + return value & (1 << bit); +} + +void PCA6416AComponent::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; + this->update_register_(pin, value, reg_addr); +} + +void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t io_dir = pin < 8 ? PCA6416A_CONFIG0 : PCA6416A_CONFIG1; + uint8_t pull_en = pin < 8 ? PCAL6416A_PULL_EN0 : PCAL6416A_PULL_EN1; + uint8_t pull_dir = pin < 8 ? PCAL6416A_PULL_DIR0 : PCAL6416A_PULL_DIR1; + if (flags == gpio::FLAG_INPUT) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, false, pull_en); + } + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, true, pull_en); + } else { + ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors"); + } + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_register_(pin, false, io_dir); + } +} + +bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->read_register(reg, value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == PCA6416A_OUTPUT0) { + reg_value = this->output_0_; + } else if (reg_addr == PCA6416A_OUTPUT1) { + reg_value = this->output_1_; + } else { + this->read_register_(reg_addr, ®_value); + } + + if (pin_value) { + reg_value |= 1 << bit; + } else { + reg_value &= ~(1 << bit); + } + + this->write_register_(reg_addr, reg_value); + + if (reg_addr == PCA6416A_OUTPUT0) { + this->output_0_ = reg_value; + } else if (reg_addr == PCA6416A_OUTPUT1) { + this->output_1_ = reg_value; + } +} + +float PCA6416AComponent::get_setup_priority() const { return setup_priority::IO; } + +void PCA6416AGPIOPin::setup() { pin_mode(flags_); } +void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string PCA6416AGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_); + return buffer; +} + +} // namespace pca6416a +} // namespace esphome diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h new file mode 100644 index 0000000000..247f443e87 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pca6416a { + +class PCA6416AComponent : public Component, public i2c::I2CDevice { + public: + PCA6416AComponent() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + protected: + bool read_register_(uint8_t reg, uint8_t *value); + bool write_register_(uint8_t reg, uint8_t value); + void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_0_{0x00}; + uint8_t output_1_{0x00}; + /// Storage for last I2C error seen + esphome::i2c::ErrorCode last_error_; + /// Only the PCAL6416A has pull-up resistors + bool has_pullup_{false}; +}; + +/// Helper class to expose a PCA6416A pin as an internal input GPIO pin. +class PCA6416AGPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(PCA6416AComponent *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; } + + protected: + PCA6416AComponent *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pca6416a +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index a235ff1502..c5cca7aa59 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1449,6 +1449,13 @@ binary_sensor: number: 1 mode: INPUT inverted: true + - platform: gpio + name: PCA6416A binary sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true - platform: gpio name: MCP21 binary sensor pin: @@ -2934,6 +2941,11 @@ pca9554: address: 0x3F i2c_id: i2c_bus +pca6416a: + - id: pca6416a_hub + address: 0x21 + i2c_id: i2c_bus + mcp23017: - id: mcp23017_hub open_drain_interrupt: true