diff --git a/esphome/components/tcal6416/__init__.py b/esphome/components/tcal6416/__init__.py new file mode 100644 index 0000000000..dc85b72ee0 --- /dev/null +++ b/esphome/components/tcal6416/__init__.py @@ -0,0 +1,72 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OUTPUT, +) + +CODEOWNERS = ["@crgarcia12"] + +AUTO_LOAD = ["gpio_expander"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +tcal6416_ns = cg.esphome_ns.namespace("tcal6416") + +TCAL6416Component = tcal6416_ns.class_("TCAL6416Component", cg.Component, i2c.I2CDevice) +TCAL6416GPIOPin = tcal6416_ns.class_("TCAL6416GPIOPin", cg.GPIOPin) + +CONF_TCAL6416 = "tcal6416" +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(TCAL6416Component), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x20)) +) + + +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") + return value + + +TCAL6416_PIN_SCHEMA = pins.gpio_base_schema( + TCAL6416GPIOPin, + cv.int_range(min=0, max=15), + modes=[CONF_INPUT, CONF_OUTPUT], + mode_validator=validate_mode, + invertible=True, +).extend( + { + cv.Required(CONF_TCAL6416): cv.use_id(TCAL6416Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_TCAL6416, TCAL6416_PIN_SCHEMA) +async def tcal6416_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_TCAL6416]) + + cg.add(var.set_pin(config[CONF_NUMBER])) + 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/tcal6416/tcal6416.cpp b/esphome/components/tcal6416/tcal6416.cpp new file mode 100644 index 0000000000..e853a10351 --- /dev/null +++ b/esphome/components/tcal6416/tcal6416.cpp @@ -0,0 +1,155 @@ +#include "tcal6416.h" +#include "esphome/core/log.h" + +static const uint8_t TCAL6416_INPUT_PORT_REGISTER_0 = 0x00; +static const uint8_t TCAL6416_INPUT_PORT_REGISTER_1 = 0x01; +static const uint8_t TCAL6416_OUTPUT_PORT_REGISTER_0 = 0x02; +static const uint8_t TCAL6416_OUTPUT_PORT_REGISTER_1 = 0x03; +static const uint8_t TCAL6416_POLARITY_REGISTER_0 = 0x04; +static const uint8_t TCAL6416_POLARITY_REGISTER_1 = 0x05; +static const uint8_t TCAL6416_CONFIGURATION_PORT_0 = 0x06; +static const uint8_t TCAL6416_CONFIGURATION_PORT_1 = 0x07; + +namespace esphome { +namespace tcal6416 { + +static const char *const TAG = "tcal6416"; + +void TCAL6416Component::setup() { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + return; + } + + // Disable polarity inversion + uint8_t data[2] = {0x00, 0x00}; + if (!this->write_bytes(TCAL6416_POLARITY_REGISTER_0, data, 2)) { + this->mark_failed(); + return; + } +} + +void TCAL6416Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCAL6416:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void TCAL6416Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags == gpio::FLAG_INPUT) { + // Set mode mask bit (1 = input) + this->mode_mask_ |= 1 << pin; + } else if (flags == gpio::FLAG_OUTPUT) { + // Clear mode mask bit (0 = output) + this->mode_mask_ &= ~(1 << pin); + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void TCAL6416Component::loop() { this->reset_pin_cache_(); } + +bool TCAL6416Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + if (!this->read_bytes(TCAL6416_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning(LOG_STR("Failed to read output register")); + return false; + } + this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + this->status_clear_warning(); + return true; +} + +bool TCAL6416Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + bool success = this->read_bytes(TCAL6416_CONFIGURATION_PORT_0, data, 2); + if (!success) { + this->status_set_warning(LOG_STR("Failed to read mode register")); + return false; + } + this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); + + this->status_clear_warning(); + return true; +} + +bool TCAL6416Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + uint8_t data; + uint8_t bank_number = pin < 8 ? 0 : 1; + uint8_t register_to_read = bank_number ? TCAL6416_INPUT_PORT_REGISTER_1 : TCAL6416_INPUT_PORT_REGISTER_0; + if (!this->read_bytes(register_to_read, &data, 1)) { + this->status_set_warning(LOG_STR("Failed to read input register")); + return false; + } + uint8_t second_half = this->input_mask_ >> 8; + uint8_t first_half = this->input_mask_; + if (bank_number) { + this->input_mask_ = (data << 8) | (uint16_t(first_half) << 0); + } else { + this->input_mask_ = (uint16_t(second_half) << 8) | (data << 0); + } + + this->status_clear_warning(); + return true; +} + +void TCAL6416Component::digital_write_hw(uint8_t pin, bool value) { + if (this->is_failed()) + return; + + if (value) { + this->output_mask_ |= (1 << pin); + } else { + this->output_mask_ &= ~(1 << pin); + } + + uint8_t data[2]; + data[0] = this->output_mask_; + data[1] = this->output_mask_ >> 8; + if (!this->write_bytes(TCAL6416_OUTPUT_PORT_REGISTER_0, data, 2)) { + this->status_set_warning(LOG_STR("Failed to write output register")); + return; + } + + this->status_clear_warning(); +} + +bool TCAL6416Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + uint8_t data[2]; + + data[0] = this->mode_mask_; + data[1] = this->mode_mask_ >> 8; + if (!this->write_bytes(TCAL6416_CONFIGURATION_PORT_0, data, 2)) { + this->status_set_warning(LOG_STR("Failed to write mode register")); + return false; + } + this->status_clear_warning(); + return true; +} + +bool TCAL6416Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +float TCAL6416Component::get_setup_priority() const { return setup_priority::IO; } + +void TCAL6416GPIOPin::setup() { this->pin_mode(this->flags_); } +void TCAL6416GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool TCAL6416GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void TCAL6416GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string TCAL6416GPIOPin::dump_summary() const { return str_sprintf("%u via TCAL6416", this->pin_); } + +} // namespace tcal6416 +} // namespace esphome diff --git a/esphome/components/tcal6416/tcal6416.h b/esphome/components/tcal6416/tcal6416.h new file mode 100644 index 0000000000..baa4af77d6 --- /dev/null +++ b/esphome/components/tcal6416/tcal6416.h @@ -0,0 +1,66 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace tcal6416 { + +class TCAL6416Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + TCAL6416Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + void loop() override; + + protected: + 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; + + /// Mask for the pin mode - 1 means input, 0 means output + uint16_t mode_mask_{0xFFFF}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint16_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint16_t input_mask_{0x00}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +/// Helper class to expose a TCAL6416 pin as an internal input GPIO pin. +class TCAL6416GPIOPin : public GPIOPin, public Parented { + 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_pin(uint8_t pin) { this->pin_ = pin; } + void set_inverted(bool inverted) { this->inverted_ = inverted; } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace tcal6416 +} // namespace esphome diff --git a/tests/components/tcal6416/common.yaml b/tests/components/tcal6416/common.yaml new file mode 100644 index 0000000000..24a2ab398c --- /dev/null +++ b/tests/components/tcal6416/common.yaml @@ -0,0 +1,23 @@ +tcal6416: + - id: tcal6416_hub + i2c_id: i2c_bus + address: 0x20 + +binary_sensor: + - platform: gpio + id: tcal6416_binary_sensor + name: TCAL6416 Binary Sensor + pin: + tcal6416: tcal6416_hub + number: 1 + mode: INPUT + inverted: true + +output: + - platform: gpio + id: tcal6416_output + pin: + tcal6416: tcal6416_hub + number: 0 + mode: OUTPUT + inverted: false diff --git a/tests/components/tcal6416/test.esp32-idf.yaml b/tests/components/tcal6416/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/tcal6416/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/tcal6416/test.esp8266-ard.yaml b/tests/components/tcal6416/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/tcal6416/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/tcal6416/test.rp2040-ard.yaml b/tests/components/tcal6416/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/tcal6416/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml