diff --git a/CODEOWNERS b/CODEOWNERS index 832c571ae4..b3c66c775b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ esphome/components/pca6416a/* @Mat931 esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman +esphome/components/pi4ioe5v6408/* @jesserockz esphome/components/pid/* @OttoWinter esphome/components/pipsolar/* @andreashergert1984 esphome/components/pm1006/* @habbie diff --git a/esphome/components/pi4ioe5v6408/__init__.py b/esphome/components/pi4ioe5v6408/__init__.py new file mode 100644 index 0000000000..c64f923823 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/__init__.py @@ -0,0 +1,84 @@ +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, + CONF_PULLDOWN, + CONF_PULLUP, + CONF_RESET, +) + +AUTO_LOAD = ["gpio_expander"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + + +pi4ioe5v6408_ns = cg.esphome_ns.namespace("pi4ioe5v6408") +PI4IOE5V6408Component = pi4ioe5v6408_ns.class_( + "PI4IOE5V6408Component", cg.Component, i2c.I2CDevice +) +PI4IOE5V6408GPIOPin = pi4ioe5v6408_ns.class_("PI4IOE5V6408GPIOPin", cg.GPIOPin) + +CONF_PI4IOE5V6408 = "pi4ioe5v6408" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component), + cv.Optional(CONF_RESET, default=True): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x43)) +) + + +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) + + cg.add(var.set_reset(config[CONF_RESET])) + + +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 + + +PI4IOE5V6408_PIN_SCHEMA = pins.gpio_base_schema( + PI4IOE5V6408GPIOPin, + cv.int_range(min=0, max=7), + modes=[ + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, + CONF_PULLDOWN, + ], + mode_validator=validate_mode, +).extend( + { + cv.Required(CONF_PI4IOE5V6408): cv.use_id(PI4IOE5V6408Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_PI4IOE5V6408, PI4IOE5V6408_PIN_SCHEMA) +async def pi4ioe5v6408_pin_schema(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_parented(var, config[CONF_PI4IOE5V6408]) + + 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/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp new file mode 100644 index 0000000000..55b8edffc8 --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -0,0 +1,171 @@ +#include "pi4ioe5v6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pi4ioe5v6408 { + +static const uint8_t PI4IOE5V6408_REGISTER_DEVICE_ID = 0x01; +static const uint8_t PI4IOE5V6408_REGISTER_IO_DIR = 0x03; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_SET = 0x05; +static const uint8_t PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE = 0x07; +static const uint8_t PI4IOE5V6408_REGISTER_IN_DEFAULT_STATE = 0x09; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_ENABLE = 0x0B; +static const uint8_t PI4IOE5V6408_REGISTER_PULL_SELECT = 0x0D; +static const uint8_t PI4IOE5V6408_REGISTER_IN_STATE = 0x0F; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_ENABLE_MASK = 0x11; +static const uint8_t PI4IOE5V6408_REGISTER_INTERRUPT_STATUS = 0x13; + +static const char *const TAG = "pi4ioe5v6408"; + +void PI4IOE5V6408Component::setup() { + ESP_LOGCONFIG(TAG, "Running setup"); + if (this->reset_) { + this->reg(PI4IOE5V6408_REGISTER_DEVICE_ID) |= 0b00000001; + this->reg(PI4IOE5V6408_REGISTER_OUT_HIGH_IMPEDENCE) = 0b00000000; + } else { + if (!this->read_gpio_modes_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO modes"); + return; + } + if (!this->read_gpio_outputs_()) { + this->mark_failed(); + ESP_LOGE(TAG, "Failed to read GPIO outputs"); + return; + } + } +} +void PI4IOE5V6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "PI4IOE5V6408:"); + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} +void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + // Set mode mask bit + this->mode_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_INPUT) { + // Clear mode mask bit + this->mode_mask_ &= ~(1 << pin); + if (flags & gpio::FLAG_PULLUP) { + this->pull_up_down_mask_ |= 1 << pin; + this->pull_enable_mask_ |= 1 << pin; + } else if (flags & gpio::FLAG_PULLDOWN) { + this->pull_up_down_mask_ &= ~(1 << pin); + this->pull_enable_mask_ |= 1 << pin; + } + } + // Write GPIO to enable input mode + this->write_gpio_modes_(); +} + +void PI4IOE5V6408Component::loop() { this->reset_pin_cache_(); } + +bool PI4IOE5V6408Component::read_gpio_outputs_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { + this->status_set_warning("Failed to read output register"); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::read_gpio_modes_() { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { + this->status_set_warning("Failed to read GPIO modes"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Read GPIO modes: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(data)); +#endif + this->mode_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) + return false; + + uint8_t data; + if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { + this->status_set_warning("Failed to read GPIO state"); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void PI4IOE5V6408Component::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); + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { + this->status_set_warning("Failed to write output register"); + return; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, "Wrote GPIO output: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(this->output_mask_)); +#endif + this->status_clear_warning(); +} + +bool PI4IOE5V6408Component::write_gpio_modes_() { + if (this->is_failed()) + return false; + + if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { + this->status_set_warning("Failed to write GPIO modes"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { + this->status_set_warning("Failed to write GPIO pullup/pulldown"); + return false; + } + if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { + this->status_set_warning("Failed to write GPIO pull enable"); + return false; + } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + ESP_LOGV(TAG, + "Wrote GPIO modes: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pullup/pulldown: 0b" BYTE_TO_BINARY_PATTERN "\n" + "Wrote GPIO pull enable: 0b" BYTE_TO_BINARY_PATTERN, + BYTE_TO_BINARY(this->mode_mask_), BYTE_TO_BINARY(this->pull_up_down_mask_), + BYTE_TO_BINARY(this->pull_enable_mask_)); +#endif + this->status_clear_warning(); + return true; +} + +bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); } + +float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; } + +void PI4IOE5V6408GPIOPin::setup() { this->pin_mode(this->flags_); } +void PI4IOE5V6408GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PI4IOE5V6408GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PI4IOE5V6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value != this->inverted_); +} +std::string PI4IOE5V6408GPIOPin::dump_summary() const { return str_sprintf("%u via PI4IOE5V6408", this->pin_); } + +} // namespace pi4ioe5v6408 +} // namespace esphome diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h new file mode 100644 index 0000000000..82b3076fab --- /dev/null +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.h @@ -0,0 +1,70 @@ +#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 pi4ioe5v6408 { +class PI4IOE5V6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + PI4IOE5V6408Component() = default; + + 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; + + /// Indicate if the component should reset the state during setup + void set_reset(bool reset) { this->reset_ = reset; } + + 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 output, 0 means input + uint8_t mode_mask_{0x00}; + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_mask_{0x00}; + /// The state read in digital_read_hw - 1 means HIGH, 0 means LOW + uint8_t input_mask_{0x00}; + /// The mask to write as input buffer state - 1 means enabled, 0 means disabled + uint8_t pull_enable_mask_{0x00}; + /// The mask to write as pullup state - 1 means pullup, 0 means pulldown + uint8_t pull_up_down_mask_{0x00}; + + bool reset_{true}; + + bool read_gpio_modes_(); + bool write_gpio_modes_(); + bool read_gpio_outputs_(); +}; + +class PI4IOE5V6408GPIOPin : 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 pi4ioe5v6408 +} // namespace esphome diff --git a/tests/components/pi4ioe5v6408/common.yaml b/tests/components/pi4ioe5v6408/common.yaml new file mode 100644 index 0000000000..4130dc2652 --- /dev/null +++ b/tests/components/pi4ioe5v6408/common.yaml @@ -0,0 +1,22 @@ +i2c: + id: i2c_pi4ioe5v6408 + sda: ${i2c_sda} + scl: ${i2c_scl} + +pi4ioe5v6408: + id: pi4ioe1 + address: 0x44 + +switch: + - platform: gpio + id: switch1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 0 + +binary_sensor: + - platform: gpio + id: sensor1 + pin: + pi4ioe5v6408: pi4ioe1 + number: 1 diff --git a/tests/components/pi4ioe5v6408/test.esp32-ard.yaml b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.esp32-idf.yaml b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml new file mode 100644 index 0000000000..55e6edfbf3 --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO21 + i2c_scl: GPIO22 + +<<: !include common.yaml diff --git a/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml new file mode 100644 index 0000000000..b7b6b13bfe --- /dev/null +++ b/tests/components/pi4ioe5v6408/test.rp2040-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + i2c_sda: GPIO4 + i2c_scl: GPIO5 + +<<: !include common.yaml