diff --git a/CODEOWNERS b/CODEOWNERS index c630db7948..fa7a03d48f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,6 +197,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny +esphome/components/m5stack4relay/* @KoenBreeman esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger diff --git a/esphome/components/m5stack4relay/__init__.py b/esphome/components/m5stack4relay/__init__.py new file mode 100644 index 0000000000..8ff8b29838 --- /dev/null +++ b/esphome/components/m5stack4relay/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@KoenBreeman"] +DEPENDENCIES = ["i2c"] + +MULTI_CONF = True + +CONF_M5STACK4RELAY_ID = "m5stack4relay_id" + +m5stack4relay_ns = cg.esphome_ns.namespace("m5stack4relay") +M5Stack4Relay = m5stack4relay_ns.class_("M5Stack4Relay", cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(M5Stack4Relay), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x26)) +) + + +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) diff --git a/esphome/components/m5stack4relay/m5stack4relay.cpp b/esphome/components/m5stack4relay/m5stack4relay.cpp new file mode 100644 index 0000000000..ee6d617692 --- /dev/null +++ b/esphome/components/m5stack4relay/m5stack4relay.cpp @@ -0,0 +1,65 @@ +#include "m5stack4relay.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace m5stack4relay { + +static const char *const TAG = "m5stack_4_relay"; + +void M5Stack4Relay::dump_config() { + ESP_LOGCONFIG(TAG, "M5Stack 4 Relays:"); + LOG_I2C_DEVICE(this); +} + +/*! @brief Setting the mode of the device, and turn off all relays. + * @param mode Async = 0, Sync = 1. */ +void M5Stack4Relay::init_(bool mode) { + this->write1_byte_(UNIT_4RELAY_REG, mode); + this->write1_byte_(UNIT_4RELAY_RELAY_REG, 0); +} + +/*! @brief Read a certain length of data to the specified register address. */ +uint8_t M5Stack4Relay::read1_byte_(uint8_t register_address) { + uint8_t data; + if (!this->read_byte(register_address, &data)) { + ESP_LOGW(TAG, "Read from relay failed!"); + this->status_set_warning(); + return uint8_t(0); + } + return data; +} + +/*! @brief Control the on/off of the specified relay. + * @param number Bit number of relay (0~3). + @param state OFF = 0, ON = 1 . */ +void M5Stack4Relay::relay_write(uint8_t number, bool state) { + uint8_t state_from_device = this->read1_byte_(UNIT_4RELAY_RELAY_REG); + if (state == 0) { + state_from_device &= ~(0x01 << number); + } else { + state_from_device |= (0x01 << number); + } + this->write1_byte_(UNIT_4RELAY_RELAY_REG, state_from_device); +} + +void M5Stack4Relay::setup() { + ESP_LOGCONFIG(TAG, "Setting up M5Stack_4_Relays..."); + uint8_t setupmode = 1; + this->init_(setupmode); +} + +/*! @brief Setting the mode of the device. + * @param mode Async = 0, Sync = 1. */ +void M5Stack4Relay::set_switch_mode(bool mode) { this->write1_byte_(UNIT_4RELAY_REG, mode); } + +/*! @brief Write a certain length of data to the specified register address. */ +void M5Stack4Relay::write1_byte_(uint8_t register_address, uint8_t data) { + if (!this->write_byte(register_address, data)) { + ESP_LOGW(TAG, "Write to relay failed!"); + this->status_set_warning(); + return; + } +} + +} // namespace m5stack4relay +} // namespace esphome diff --git a/esphome/components/m5stack4relay/m5stack4relay.h b/esphome/components/m5stack4relay/m5stack4relay.h new file mode 100644 index 0000000000..ab9bd73d3f --- /dev/null +++ b/esphome/components/m5stack4relay/m5stack4relay.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace m5stack4relay { + +static constexpr uint8_t UNIT_4RELAY_REG = 0X10; +static constexpr uint8_t UNIT_4RELAY_RELAY_REG = 0X11; + +enum class RelayBit : uint8_t { RELAY1 = 0, RELAY2 = 1, RELAY3 = 2, RELAY4 = 3 }; + +class M5Stack4Relay : public Component, public i2c::I2CDevice { + public: + void set_switch_mode(bool mode); + + void relay_write(uint8_t number, bool state); + + protected: + void write1_byte_(uint8_t register_address, uint8_t data); + uint8_t read1_byte_(uint8_t register_address); + + void dump_config() override; + + void init_(bool mode); + + void setup() override; +}; + +} // namespace m5stack4relay +} // namespace esphome diff --git a/esphome/components/m5stack4relay/switch/__init__.py b/esphome/components/m5stack4relay/switch/__init__.py new file mode 100644 index 0000000000..d986baa2e2 --- /dev/null +++ b/esphome/components/m5stack4relay/switch/__init__.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, switch +from esphome.const import CONF_CHANNEL, CONF_INTERLOCK # , CONF_ID, + +from .. import m5stack4relay_ns, M5Stack4Relay, CONF_M5STACK4RELAY_ID + +DEPENDENCIES = ["m5stack4relay"] + +M5StackSwitch = m5stack4relay_ns.class_( + "M5Stack4RelaySwitch", cg.Component, i2c.I2CDevice, switch.Switch +) + +CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time" + +CONF_Relay_1 = 1 +CONF_Relay_2 = 2 +CONF_Relay_3 = 3 +CONF_Relay_4 = 4 + +CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time" + +RelayBit_ = m5stack4relay_ns.enum("RelayBit", is_class=True) + +SWITCH_MAP = { + CONF_Relay_1: RelayBit_.RELAY1, + CONF_Relay_2: RelayBit_.RELAY2, + CONF_Relay_3: RelayBit_.RELAY3, + CONF_Relay_4: RelayBit_.RELAY4, +} + + +CONFIG_SCHEMA = ( + switch.switch_schema(M5StackSwitch) + .extend( + { + cv.GenerateID(): cv.declare_id(M5StackSwitch), + cv.GenerateID(CONF_M5STACK4RELAY_ID): cv.use_id(M5Stack4Relay), + cv.Required(CONF_CHANNEL): cv.enum(SWITCH_MAP), + cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional( + CONF_INTERLOCK_WAIT_TIME, default="0ms" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await switch.new_switch(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_M5STACK4RELAY_ID]) + + cg.add(var.set_channel(config[CONF_CHANNEL])) + + if CONF_INTERLOCK in config: + interlock = [] + for it in config[CONF_INTERLOCK]: + lock = await cg.get_variable(it) + interlock.append(lock) + cg.add(var.set_interlock(interlock)) + cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME])) diff --git a/esphome/components/m5stack4relay/switch/m5stack4relay_switch.cpp b/esphome/components/m5stack4relay/switch/m5stack4relay_switch.cpp new file mode 100644 index 0000000000..75eda61dee --- /dev/null +++ b/esphome/components/m5stack4relay/switch/m5stack4relay_switch.cpp @@ -0,0 +1,75 @@ +#include "esphome/core/log.h" +#include "m5stack4relay_switch.h" + +namespace esphome { +namespace m5stack4relay { + +static const char *const TAG = "switch.M5Stack_4_Relay"; + +float M5Stack4RelaySwitch::get_setup_priority() const { return setup_priority::HARDWARE; } + +void M5Stack4RelaySwitch::setup() { + ESP_LOGCONFIG(TAG, "Setting up M5Stack_4_relay Switch '%s'...", this->name_.c_str()); + + bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); + + // write state before setup + if (initial_state) { + this->turn_on(); + } else { + this->turn_off(); + } +} + +void M5Stack4RelaySwitch::dump_config() { + LOG_SWITCH("", "M5Stack4Relay Switch", this); + + if (!this->interlock_.empty()) { + ESP_LOGCONFIG(TAG, " Interlocks:"); + for (auto *lock : this->interlock_) { + if (lock == this) + continue; + ESP_LOGCONFIG(TAG, " %s", lock->get_name().c_str()); + } + } +} + +void M5Stack4RelaySwitch::write_state(bool state) { + if (state != this->inverted_) { + // Turning ON, check interlocking + + bool found = false; + for (auto *lock : this->interlock_) { + if (lock == this) + continue; + + if (lock->state) { + lock->turn_off(); + found = true; + } + } + if (found && this->interlock_wait_time_ != 0) { + this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + // Don't write directly, call the function again + // (some other switch may have changed state while we were waiting) + this->write_state(state); + }); + return; + } + } else if (this->interlock_wait_time_ != 0) { + // If we are switched off during the interlock wait time, cancel any pending + // re-activations + this->cancel_timeout("interlock"); + } + + // This will be called every time the user requests a state change. + this->parent_->relay_write(this->channel_, state); + + // Acknowledge new state by publishing it + this->publish_state(state); +} + +void M5Stack4RelaySwitch::set_interlock(const std::vector &interlock) { this->interlock_ = interlock; } + +} // namespace m5stack4relay +} // namespace esphome diff --git a/esphome/components/m5stack4relay/switch/m5stack4relay_switch.h b/esphome/components/m5stack4relay/switch/m5stack4relay_switch.h new file mode 100644 index 0000000000..db96ecdfa6 --- /dev/null +++ b/esphome/components/m5stack4relay/switch/m5stack4relay_switch.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +#include "esphome/components/m5stack4relay/m5stack4relay.h" + +namespace esphome { +namespace m5stack4relay { + +class M5Stack4RelaySwitch : public Component, public switch_::Switch, public Parented { + public: + float get_setup_priority() const override; + + void setup() override; + void dump_config() override; + void write_state(bool state) override; + + void set_channel(RelayBit channel) { this->channel_ = (uint8_t) channel; } + + void set_interlock(const std::vector &interlock); + void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } + + protected: + uint8_t channel_; + std::vector interlock_; + uint32_t interlock_wait_time_{0}; +}; + +} // namespace m5stack4relay +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index afd3359098..a350ad531a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -58,6 +58,9 @@ i2c: number: 22 frequency: 100khz +m5stack4relay: + id: M5stackrelay_ID1 + modbus: uart_id: uart_1 flow_control_pin: @@ -641,6 +644,30 @@ switch: led: 3 name: TM1638Led3 + - platform: m5stack4relay + id: m5stack4relay_1 + name: m5stack4relay1 + channel: 1 + m5stack4relay_id: M5stackrelay_ID1 + + - platform: m5stack4relay + id: m5stack4relay_2 + name: m5stack4relay2 + channel: 2 + m5stack4relay_id: M5stackrelay_ID1 + + - platform: m5stack4relay + id: m5stack4relay_3 + name: m5stack4relay3 + channel: 3 + m5stack4relay_id: M5stackrelay_ID1 + + - platform: m5stack4relay + id: m5stack4relay_4 + name: m5stack4relay4 + channel: 4 + m5stack4relay_id: M5stackrelay_ID1 + display: - platform: tm1638 id: primarydisplay