diff --git a/README.md b/README.md index 8b0be5b..518c865 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ This repository is for integration of DitroniX boards into Home Assistant using The development and integration repository is work in progress as various projects evolve. It will also expand on route. +[![DitroniX Wiki Pages](https://github.com/DitroniX/Home-Assistant-Dev/blob/main/Datasheets%20and%20Information/GitHub%20WiKi.png?raw=true)](https://github.com/DitroniX/Home-Assistant-Dev/wiki) + +[![DitroniX Discussions](https://github.com/DitroniX/DitroniX/blob/main/Files/GitHub%20Discussions.png?raw=true)](https://github.com/DitroniX/Home-Assistant-Dev/discussions) + #### User contributions User contributions to the HA integration is welcome and of course, acknowledgements would be included. diff --git a/YAML Examples/tca6408-io-example.yaml b/YAML Examples/tca6408-io-example.yaml new file mode 100644 index 0000000..0df7a77 --- /dev/null +++ b/YAML Examples/tca6408-io-example.yaml @@ -0,0 +1,105 @@ +esphome: + name: tca6408-node + friendly_name: TCA6408 Node + +esp32: + board: esp32-c5-devkitc-1 # Change to your board (or esp32dev for S3/C3) + framework: + type: esp-idf + +# Enable logging +logger: + level: DEBUG + +# WiFi & API (Home Assistant) +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +api: +ota: + +# ====================== I2C & TCA6408 ====================== +i2c: + sda: GPIO2 # Adjust pins for your board + scl: GPIO3 + scan: true + frequency: 400kHz + +external_components: + - source: + type: git + url: https://github.com/DitroniX/Home-Assistant-Dev + ref: main + refresh: 0s #optional, how often to download fresh files from source, defaults to 1 day, use 0 to force updates + components: [ TCA6408 ] + + +tca6408: + - id: tca6408_hub + address: 0x20 # Common default (0x20 - 0x27 depending on ADDR pins) DitroniX boards sometimes use 0x21 as default to avoid conflicts + interrupt_pin: GPIO6 # Recommended - connect to TCA6408 INT pin + +# ====================== OUTPUTS (Switches) ====================== +switch: + - platform: gpio + name: "TCA6408 Output 0" + id: tca_out0 + pin: + tca6408: tca6408_hub + number: 0 + mode: OUTPUT + + - platform: gpio + name: "TCA6408 Output 1 (Relay)" + id: tca_out1 + pin: + tca6408: tca6408_hub + number: 1 + mode: OUTPUT + inverted: false + + - platform: gpio + name: "TCA6408 Output 2" + id: tca_out2 + pin: + tca6408: tca6408_hub + number: 2 + mode: OUTPUT + +# ====================== INPUTS (Binary Sensors) ====================== +binary_sensor: + - platform: gpio + name: "TCA6408 Input 3 (Door Sensor)" + pin: + tca6408: tca6408_hub + number: 3 + mode: INPUT + inverted: true # Use hardware polarity inversion if active-low + + - platform: gpio + name: "TCA6408 Input 4 (Button)" + pin: + tca6408: tca6408_hub + number: 4 + mode: INPUT + + - platform: gpio + name: "TCA6408 Input 5" + pin: + tca6408: tca6408_hub + number: 5 + mode: INPUT + inverted: false + + - platform: gpio + name: "TCA6408 Input 6" + pin: + tca6408: tca6408_hub + number: 6 + mode: INPUT + +# Optional: Status LED or other outputs +status_led: + pin: + number: GPIO2 \ No newline at end of file diff --git a/YAML Examples/tca6408-outputs-example.yaml b/YAML Examples/tca6408-outputs-example.yaml new file mode 100644 index 0000000..c2e7d49 --- /dev/null +++ b/YAML Examples/tca6408-outputs-example.yaml @@ -0,0 +1,121 @@ +esphome: + name: tca6408-outputs + friendly_name: TCA6408 Outputs + +esp32: + board: esp32-c5-devkitc-1 + framework: + type: esp-idf + +logger: + level: DEBUG + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + +api: +ota: + +# ====================== I2C & TCA6408 ====================== +i2c: + sda: GPIO2 # Adjust pins for your board + scl: GPIO3 + scan: true + frequency: 400kHz + +external_components: + - source: + type: git + url: https://github.com/DitroniX/Home-Assistant-Dev + ref: main + refresh: 0s #optional, how often to download fresh files from source, defaults to 1 day, use 0 to force updates + components: [ TCA6408 ] + +tca6408: + - id: tca6408_hub + address: 0x20 # Common default (0x20 - 0x27 depending on ADDR pins) DitroniX boards sometimes use 0x21 as default to avoid conflicts + # interrupt_pin: GPIO6 # Optional for outputs only. Interrupt pin is commented out as not needed when using only outputs. + +# ====================== OUTPUTS (Switches) ====================== +switch: + - platform: gpio + name: "TCA6408 Output 0" + id: tca_out0 + pin: + tca6408: tca6408_hub + number: 0 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 1 (Relay)" + id: tca_out1 + pin: + tca6408: tca6408_hub + number: 1 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + + - platform: gpio + name: "TCA6408 Output 2" + id: tca_out2 + pin: + tca6408: tca6408_hub + number: 2 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 3" + id: tca_out3 + pin: + tca6408: tca6408_hub + number: 3 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 4" + id: tca_out4 + pin: + tca6408: tca6408_hub + number: 4 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 5" + id: tca_out5 + pin: + tca6408: tca6408_hub + number: 5 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 6" + id: tca_out6 + pin: + tca6408: tca6408_hub + number: 6 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion + + - platform: gpio + name: "TCA6408 Output 7" + id: tca_out7 + pin: + tca6408: tca6408_hub + number: 7 + mode: OUTPUT + # inverted: false # Default behavior + # inverted: true # Hardware/software inversion \ No newline at end of file diff --git a/components/TCA6408/__init__.py b/components/TCA6408/__init__.py new file mode 100644 index 0000000..57b1581 --- /dev/null +++ b/components/TCA6408/__init__.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, gpio +from esphome.const import CONF_ID, CONF_ADDRESS, CONF_INTERRUPT_PIN + +DEPENDENCIES = ["i2c"] + +tca6408_ns = cg.esphome_ns.namespace("tca6408") +TCA6408Component = tca6408_ns.class_("TCA6408Component", cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCA6408Component), + cv.Optional(CONF_ADDRESS, default=0x20): cv.i2c_address, + cv.Optional(CONF_INTERRUPT_PIN): cv.All( + gpio.validate_gpio_pin("internal"), + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema()) +) + +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_address(config[CONF_ADDRESS])) + + if CONF_INTERRUPT_PIN in config: + pin = await gpio.register_gpio_pin(var, config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(pin)) diff --git a/components/TCA6408/tca6408.cpp b/components/TCA6408/tca6408.cpp new file mode 100644 index 0000000..657f5f2 --- /dev/null +++ b/components/TCA6408/tca6408.cpp @@ -0,0 +1,165 @@ +#include "tca6408.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tca6408 { + +static const char *const TAG = "tca6408"; + +static const uint8_t TCA6408_INPUT_PORT = 0x00; +static const uint8_t TCA6408_OUTPUT_PORT = 0x01; +static const uint8_t TCA6408_POLARITY_PORT = 0x02; +static const uint8_t TCA6408_CONFIG_PORT = 0x03; + +void TCA6408Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA6408..."); + + if (this->write_register(TCA6408_POLARITY_PORT, 0x00) != i2c::ERROR_OK || + this->write_register(TCA6408_CONFIG_PORT, this->mode_mask_) != i2c::ERROR_OK || + !this->read_gpio_outputs_()) { + this->mark_failed(); + return; + } + + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->attach_interrupt(&TCA6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE); + this->set_invalidate_on_read_(false); + ESP_LOGCONFIG(TAG, " Interrupt mode enabled"); + } else { + this->enable_loop(); + ESP_LOGCONFIG(TAG, " Polling mode enabled"); + } +} + +void IRAM_ATTR TCA6408Component::gpio_intr(TCA6408Component *arg) { + arg->interrupt_triggered_ = true; + arg->enable_loop_soon_any_context(); +} + +void TCA6408Component::loop() { + if (this->interrupt_triggered_) { + this->interrupt_triggered_ = false; + + this->digital_read_hw(0); + this->reset_pin_cache_(); + + if (this->interrupt_pin_ && this->interrupt_pin_->digital_read()) { + this->disable_loop(); + } + } +} + +void TCA6408Component::dump_config() { + ESP_LOGCONFIG(TAG, "TCA6408:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} + +bool TCA6408Component::read_gpio_outputs_() { + if (this->is_failed()) return false; + uint8_t data; + if (this->read_register(TCA6408_OUTPUT_PORT, &data) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + this->output_mask_ = data; + this->status_clear_warning(); + return true; +} + +bool TCA6408Component::digital_read_hw(uint8_t pin) { + if (this->is_failed()) return false; + + uint8_t data = 0; + if (this->read_register(TCA6408_INPUT_PORT, &data) != i2c::ERROR_OK) { + this->status_set_warning(); + return false; + } + this->input_mask_ = data; + this->status_clear_warning(); + return true; +} + +void TCA6408Component::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_register(TCA6408_OUTPUT_PORT, this->output_mask_) != i2c::ERROR_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void TCA6408Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (flags & gpio::FLAG_OUTPUT) { + this->mode_mask_ &= ~(1 << pin); + } else { + this->mode_mask_ |= (1 << pin); + if (this->interrupt_pin_ == nullptr) { + this->enable_loop(); + } + } + this->write_register(TCA6408_CONFIG_PORT, this->mode_mask_); +} + +bool TCA6408Component::digital_read_cache(uint8_t pin) { + bool value = (this->input_mask_ & (1 << pin)) != 0; + if (this->polarity_mask_ & (1 << pin)) { + value = !value; + } + return value; +} + +void TCA6408Component::set_polarity_inversion(uint8_t pin, bool inverted) { + if (inverted) { + this->polarity_mask_ |= (1 << pin); + } else { + this->polarity_mask_ &= ~(1 << pin); + } + this->write_register(TCA6408_POLARITY_PORT, this->polarity_mask_); +} + +// ==================== TCA6408GPIOPin ==================== + +void TCA6408GPIOPin::setup() { + this->pin_mode(this->flags_); + if (this->inverted_) { + this->parent_->set_polarity_inversion(this->pin_, true); + } +} + +void TCA6408GPIOPin::set_inverted(bool inverted) { + this->inverted_ = inverted; + if (this->parent_ != nullptr) { + this->parent_->set_polarity_inversion(this->pin_, inverted); + } +} + +void TCA6408GPIOPin::pin_mode(gpio::Flags flags) { + this->flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +bool TCA6408GPIOPin::digital_read() { + return this->parent_->digital_read(this->pin_); +} + +void TCA6408GPIOPin::digital_write(bool value) { + this->parent_->digital_write(this->pin_, value); +} + +size_t TCA6408GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "%u via TCA6408", this->pin_); +} + +} // namespace tca6408 +} // namespace esphome diff --git a/components/TCA6408/tca6408.h b/components/TCA6408/tca6408.h new file mode 100644 index 0000000..072d7c5 --- /dev/null +++ b/components/TCA6408/tca6408.h @@ -0,0 +1,62 @@ +#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 tca6408 { + +class TCA6408Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { + public: + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::IO; } + + void set_address(uint8_t address) { this->address_ = address; } + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + + void set_polarity_inversion(uint8_t pin, bool inverted); + + protected: + static void IRAM_ATTR gpio_intr(TCA6408Component *arg); + + 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; + void pin_mode(uint8_t pin, gpio::Flags flags) override; + + uint8_t mode_mask_{0xFF}; // 1 = input, 0 = output + uint8_t output_mask_{0x00}; + uint8_t input_mask_{0x00}; + uint8_t polarity_mask_{0x00}; // 1 = inverted + + InternalGPIOPin *interrupt_pin_{nullptr}; + bool interrupt_triggered_{false}; +}; + +class TCA6408GPIOPin : public gpio::GPIOPin, public Parented { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + size_t dump_summary(char *buffer, size_t len) const override; + + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted); + void set_flags(gpio::Flags flags) { flags_ = flags; } + gpio::Flags get_flags() const override { return flags_; } + + protected: + uint8_t pin_{0}; + bool inverted_{false}; + gpio::Flags flags_{0}; +}; + +} // namespace tca6408 +} // namespace esphome diff --git a/components/readme.md b/components/readme.md index 3acf4bc..5f85c65 100644 --- a/components/readme.md +++ b/components/readme.md @@ -1,4 +1,16 @@ ## Home Assistant - ESPHome Components +These ESPHome Components are under development and are subject to change as feedback is provided from the community. + +The components have been created in order to assist users of DitroniX boards and ESPHome development. Hopefully these will eventually be incorporated within HA/ESPHome. +[![DitroniX Wiki Pages](https://github.com/DitroniX/Home-Assistant-Dev/blob/main/Datasheets%20and%20Information/GitHub%20WiKi.png?raw=true)](https://github.com/DitroniX/Home-Assistant-Dev/wiki) + +[![DitroniX Discussions](https://github.com/DitroniX/DitroniX/blob/main/Files/GitHub%20Discussions.png?raw=true)](https://github.com/DitroniX/Home-Assistant-Dev/discussions) + - [**ATM90E36**](https://github.com/DitroniX/Home-Assistant-Dev/tree/main/components/ATM90E36 "ATM90E36") - - This is for use on the [EPEM ATM90E36](https://github.com/DitroniX/EPEM-Ethernet-Power-Energy-Monitor) Ethernet Power Energy Monitor + - This is for use on the ATM90E36 based Mains Energy Monitors + - [EPEM](https://github.com/DitroniX/EPEM-Ethernet-Power-Energy-Monitor) + - [IPEM Plus](https://github.com/DitroniX/IPEM-Plus-ESP32-C5-LoRa-LoRaWAN-IoT-Mains-Power-Energy-Monitor) + - [**TCA6408**](https://github.com/DitroniX/Home-Assistant-Dev/tree/main/components/TCA6408"TCA6408") + - 260612 - This is a test component and is work in progress + - [WREN](https://github.com/DitroniX/WREN-Recliner-Linear-Motor-Controller-IoT-Home-Automation)