diff --git a/CODEOWNERS b/CODEOWNERS index 204d2b58bd..f6f7ac6f9c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -266,6 +266,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9600/* @mreditor97 diff --git a/esphome/components/mcp4461/__init__.py b/esphome/components/mcp4461/__init__.py new file mode 100644 index 0000000000..1764629ff3 --- /dev/null +++ b/esphome/components/mcp4461/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@p1ngb4ck"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +CONF_DISABLE_WIPER_0 = "disable_wiper_0" +CONF_DISABLE_WIPER_1 = "disable_wiper_1" +CONF_DISABLE_WIPER_2 = "disable_wiper_2" +CONF_DISABLE_WIPER_3 = "disable_wiper_3" + +mcp4461_ns = cg.esphome_ns.namespace("mcp4461") +Mcp4461Component = mcp4461_ns.class_("Mcp4461Component", cg.Component, i2c.I2CDevice) +CONF_MCP4461_ID = "mcp4461_id" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Mcp4461Component), + cv.Optional(CONF_DISABLE_WIPER_0, default=False): cv.boolean, + cv.Optional(CONF_DISABLE_WIPER_1, default=False): cv.boolean, + cv.Optional(CONF_DISABLE_WIPER_2, default=False): cv.boolean, + cv.Optional(CONF_DISABLE_WIPER_3, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x2C)) +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_DISABLE_WIPER_0], + config[CONF_DISABLE_WIPER_1], + config[CONF_DISABLE_WIPER_2], + config[CONF_DISABLE_WIPER_3], + ) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp new file mode 100644 index 0000000000..5393241281 --- /dev/null +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -0,0 +1,618 @@ +#include "mcp4461.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace mcp4461 { + +static const char *const TAG = "mcp4461"; +constexpr uint8_t EEPROM_WRITE_TIMEOUT_MS = 10; + +void Mcp4461Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp4461 using address (0x%02X)...", this->address_); + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // save WP/WL status + this->update_write_protection_status_(); + for (uint8_t i = 0; i < 8; i++) { + if (this->reg_[i].initial_value.has_value()) { + uint16_t initial_state = static_cast(*this->reg_[i].initial_value * 256.0f); + this->write_wiper_level_(i, initial_state); + } + if (this->reg_[i].enabled) { + this->reg_[i].state = this->read_wiper_level_(i); + } else { + // only volatile wipers can be set disabled on hw level + if (i < 4) { + this->reg_[i].state = 0; + Mcp4461WiperIdx wiper_idx = static_cast(i); + this->disable_wiper_(wiper_idx); + } + } + } +} + +void Mcp4461Component::set_initial_value(Mcp4461WiperIdx wiper, float initial_value) { + uint8_t wiper_idx = static_cast(wiper); + this->reg_[wiper_idx].initial_value = initial_value; +} + +void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char terminal) { + uint8_t wiper_idx = static_cast(wiper); + switch (terminal) { + case 'a': + this->reg_[wiper_idx].terminal_a = false; + break; + case 'b': + this->reg_[wiper_idx].terminal_b = false; + break; + case 'w': + this->reg_[wiper_idx].terminal_w = false; + break; + } +} + +void Mcp4461Component::update_write_protection_status_() { + uint8_t status_register_value = this->get_status_register_(); + this->write_protected_ = static_cast((status_register_value >> 0) & 0x01); + this->reg_[0].wiper_lock_active = static_cast((status_register_value >> 2) & 0x01); + this->reg_[1].wiper_lock_active = static_cast((status_register_value >> 3) & 0x01); + this->reg_[2].wiper_lock_active = static_cast((status_register_value >> 5) & 0x01); + this->reg_[3].wiper_lock_active = static_cast((status_register_value >> 6) & 0x01); +} + +void Mcp4461Component::dump_config() { + ESP_LOGCONFIG(TAG, "mcp4461:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + } + // log wiper status + for (uint8_t i = 0; i < 8; ++i) { + // terminals only valid for volatile wipers 0-3 - enable/disable is terminal hw + // so also invalid for nonvolatile. For these, only print current level. + // reworked to be a one-line intentionally, as output would not be in order + if (i < 4) { + ESP_LOGCONFIG(TAG, " ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, A: %s, B: %s, W: %s", i, + this->reg_[i].state, ONOFF(this->reg_[i].terminal_hw), ONOFF(this->reg_[i].terminal_a), + ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w), ONOFF(this->reg_[i].enabled)); + } else { + ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state); + } + } +} + +void Mcp4461Component::loop() { + if (this->status_has_warning()) { + this->get_status_register_(); + } + for (uint8_t i = 0; i < 8; i++) { + if (this->reg_[i].update_level) { + // set wiper i state if changed + if (this->reg_[i].state != this->read_wiper_level_(i)) { + this->write_wiper_level_(i, this->reg_[i].state); + } + } + this->reg_[i].update_level = false; + // can be true only for wipers 0-3 + // setting changes for terminals of nonvolatile wipers + // is prohibited in public methods + if (this->reg_[i].update_terminal) { + // set terminal register changes + Mcp4461TerminalIdx terminal_connector = + i < 2 ? Mcp4461TerminalIdx::MCP4461_TERMINAL_0 : Mcp4461TerminalIdx::MCP4461_TERMINAL_1; + uint8_t new_terminal_value = this->calc_terminal_connector_byte_(terminal_connector); + ESP_LOGV(TAG, "updating terminal %u to new value %u", static_cast(terminal_connector), + new_terminal_value); + this->set_terminal_register_(terminal_connector, new_terminal_value); + } + this->reg_[i].update_terminal = false; + } +} + +uint8_t Mcp4461Component::get_status_register_() { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return 0; + } + uint8_t addr = static_cast(Mcp4461Addresses::MCP4461_STATUS); + uint8_t reg = addr | static_cast(Mcp4461Commands::READ); + uint16_t buf; + if (!this->read_byte_16(reg, &buf)) { + this->error_code_ = MCP4461_STATUS_REGISTER_ERROR; + this->mark_failed(); + return 0; + } + uint8_t msb = buf >> 8; + uint8_t lsb = static_cast(buf & 0x00ff); + if (msb != 1 || ((lsb >> 7) & 0x01) != 1 || ((lsb >> 1) & 0x01) != 1) { + // D8, D7 and R1 bits are hardlocked to 1 -> a status msb bit 0 (bit 9 of status register) of 0 or lsb bit 1/7 = 0 + // indicate device/communication issues, therefore mark component failed + this->error_code_ = MCP4461_STATUS_REGISTER_INVALID; + this->mark_failed(); + return 0; + } + this->status_clear_warning(); + return lsb; +} + +void Mcp4461Component::read_status_register_to_log() { + uint8_t status_register_value = this->get_status_register_(); + ESP_LOGI(TAG, "D7: %u, WL3: %u, WL2: %u, EEWA: %u, WL1: %u, WL0: %u, R1: %u, WP: %u", + ((status_register_value >> 7) & 0x01), ((status_register_value >> 6) & 0x01), + ((status_register_value >> 5) & 0x01), ((status_register_value >> 4) & 0x01), + ((status_register_value >> 3) & 0x01), ((status_register_value >> 2) & 0x01), + ((status_register_value >> 1) & 0x01), ((status_register_value >> 0) & 0x01)); +} + +uint8_t Mcp4461Component::get_wiper_address_(uint8_t wiper) { + uint8_t addr; + bool nonvolatile = false; + if (wiper > 3) { + nonvolatile = true; + wiper = wiper - 4; + } + switch (wiper) { + case 0: + addr = static_cast(Mcp4461Addresses::MCP4461_VW0); + break; + case 1: + addr = static_cast(Mcp4461Addresses::MCP4461_VW1); + break; + case 2: + addr = static_cast(Mcp4461Addresses::MCP4461_VW2); + break; + case 3: + addr = static_cast(Mcp4461Addresses::MCP4461_VW3); + break; + default: + ESP_LOGW(TAG, "unknown wiper specified"); + return 0; + } + if (nonvolatile) { + addr = addr + 0x20; + } + return addr; +} + +uint16_t Mcp4461Component::get_wiper_level_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return 0; + } + uint8_t wiper_idx = static_cast(wiper); + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return 0; + } + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "reading from disabled volatile wiper %u, returning 0", wiper_idx); + return 0; + } + return this->read_wiper_level_(wiper_idx); +} + +uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { + uint8_t addr = this->get_wiper_address_(wiper_idx); + uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); + if (wiper_idx > 3) { + if (!this->is_eeprom_ready_for_writing_(true)) { + return 0; + } + } + uint16_t buf = 0; + if (!(this->read_byte_16(reg, &buf))) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx); + return 0; + } + return buf; +} + +bool Mcp4461Component::update_wiper_level_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t wiper_idx = static_cast(wiper); + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return false; + } + uint16_t data = this->get_wiper_level_(wiper); + ESP_LOGV(TAG, "Got value %u from wiper %u", data, wiper_idx); + this->reg_[wiper_idx].state = data; + return true; +} + +bool Mcp4461Component::set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t wiper_idx = static_cast(wiper); + if (value > 0x100) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_VALUE_INVALID))); + return false; + } + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return false; + } + if (this->reg_[wiper_idx].wiper_lock_active) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED))); + return false; + } + ESP_LOGV(TAG, "Setting MCP4461 wiper %u to %u", wiper_idx, value); + this->reg_[wiper_idx].state = value; + this->reg_[wiper_idx].update_level = true; + return true; +} + +void Mcp4461Component::write_wiper_level_(uint8_t wiper, uint16_t value) { + bool nonvolatile = wiper > 3; + if (!(this->mcp4461_write_(this->get_wiper_address_(wiper), value, nonvolatile))) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? "nonvolatile " : "", wiper, value); + } +} + +void Mcp4461Component::enable_wiper_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return; + } + uint8_t wiper_idx = static_cast(wiper); + if ((this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_ENABLED))); + return; + } + if (this->reg_[wiper_idx].wiper_lock_active) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED))); + return; + } + ESP_LOGV(TAG, "Enabling wiper %u", wiper_idx); + this->reg_[wiper_idx].enabled = true; + if (wiper_idx < 4) { + this->reg_[wiper_idx].terminal_hw = true; + this->reg_[wiper_idx].update_terminal = true; + } +} + +void Mcp4461Component::disable_wiper_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return; + } + uint8_t wiper_idx = static_cast(wiper); + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return; + } + if (this->reg_[wiper_idx].wiper_lock_active) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED))); + return; + } + ESP_LOGV(TAG, "Disabling wiper %u", wiper_idx); + this->reg_[wiper_idx].enabled = true; + if (wiper_idx < 4) { + this->reg_[wiper_idx].terminal_hw = true; + this->reg_[wiper_idx].update_terminal = true; + } +} + +bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t wiper_idx = static_cast(wiper); + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return false; + } + if (this->reg_[wiper_idx].wiper_lock_active) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED))); + return false; + } + if (this->reg_[wiper_idx].state == 256) { + ESP_LOGV(TAG, "Maximum wiper level reached, further increase of wiper %u prohibited", wiper_idx); + return false; + } + ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx); + uint8_t addr = this->get_wiper_address_(wiper_idx); + uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); + auto err = this->write(&this->address_, reg, sizeof(reg)); + if (err != i2c::ERROR_OK) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + return false; + } + this->reg_[wiper_idx].state++; + return true; +} + +bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t wiper_idx = static_cast(wiper); + if (!(this->reg_[wiper_idx].enabled)) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_DISABLED))); + return false; + } + if (this->reg_[wiper_idx].wiper_lock_active) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED))); + return false; + } + if (this->reg_[wiper_idx].state == 0) { + ESP_LOGV(TAG, "Minimum wiper level reached, further decrease of wiper %u prohibited", wiper_idx); + return false; + } + ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx); + uint8_t addr = this->get_wiper_address_(wiper_idx); + uint8_t reg = addr | static_cast(Mcp4461Commands::DECREMENT); + auto err = this->write(&this->address_, reg, sizeof(reg)); + if (err != i2c::ERROR_OK) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + return false; + } + this->reg_[wiper_idx].state--; + return true; +} + +uint8_t Mcp4461Component::calc_terminal_connector_byte_(Mcp4461TerminalIdx terminal_connector) { + uint8_t i = static_cast(terminal_connector) <= 1 ? 0 : 2; + uint8_t new_value_byte = 0; + new_value_byte += static_cast(this->reg_[i].terminal_b); + new_value_byte += static_cast(this->reg_[i].terminal_w) << 1; + new_value_byte += static_cast(this->reg_[i].terminal_a) << 2; + new_value_byte += static_cast(this->reg_[i].terminal_hw) << 3; + new_value_byte += static_cast(this->reg_[(i + 1)].terminal_b) << 4; + new_value_byte += static_cast(this->reg_[(i + 1)].terminal_w) << 5; + new_value_byte += static_cast(this->reg_[(i + 1)].terminal_a) << 6; + new_value_byte += static_cast(this->reg_[(i + 1)].terminal_hw) << 7; + return new_value_byte; +} + +uint8_t Mcp4461Component::get_terminal_register_(Mcp4461TerminalIdx terminal_connector) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return 0; + } + uint8_t reg = static_cast(terminal_connector) == 0 ? static_cast(Mcp4461Addresses::MCP4461_TCON0) + : static_cast(Mcp4461Addresses::MCP4461_TCON1); + reg |= static_cast(Mcp4461Commands::READ); + uint16_t buf; + if (this->read_byte_16(reg, &buf)) { + return static_cast(buf & 0x00ff); + } else { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + ESP_LOGW(TAG, "Error fetching terminal register value"); + return 0; + } +} + +void Mcp4461Component::update_terminal_register_(Mcp4461TerminalIdx terminal_connector) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return; + } + if ((static_cast(terminal_connector) != 0 && static_cast(terminal_connector) != 1)) { + return; + } + uint8_t terminal_data = this->get_terminal_register_(terminal_connector); + if (terminal_data == 0) { + return; + } + ESP_LOGV(TAG, "Got terminal register %u data 0x%02X", static_cast(terminal_connector), terminal_data); + uint8_t wiper_index = 0; + if (static_cast(terminal_connector) == 1) { + wiper_index = 2; + } + this->reg_[wiper_index].terminal_b = ((terminal_data >> 0) & 0x01); + this->reg_[wiper_index].terminal_w = ((terminal_data >> 1) & 0x01); + this->reg_[wiper_index].terminal_a = ((terminal_data >> 2) & 0x01); + this->reg_[wiper_index].terminal_hw = ((terminal_data >> 3) & 0x01); + this->reg_[(wiper_index + 1)].terminal_b = ((terminal_data >> 4) & 0x01); + this->reg_[(wiper_index + 1)].terminal_w = ((terminal_data >> 5) & 0x01); + this->reg_[(wiper_index + 1)].terminal_a = ((terminal_data >> 6) & 0x01); + this->reg_[(wiper_index + 1)].terminal_hw = ((terminal_data >> 7) & 0x01); +} + +bool Mcp4461Component::set_terminal_register_(Mcp4461TerminalIdx terminal_connector, uint8_t data) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t addr; + if (static_cast(terminal_connector) == 0) { + addr = static_cast(Mcp4461Addresses::MCP4461_TCON0); + } else if (static_cast(terminal_connector) == 1) { + addr = static_cast(Mcp4461Addresses::MCP4461_TCON1); + } else { + ESP_LOGW(TAG, "Invalid terminal connector id %u specified", static_cast(terminal_connector)); + return false; + } + if (!(this->mcp4461_write_(addr, data))) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + return false; + } + return true; +} + +void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return; + } + uint8_t wiper_idx = static_cast(wiper); + ESP_LOGV(TAG, "Enabling terminal %c of wiper %u", terminal, wiper_idx); + switch (terminal) { + case 'h': + this->reg_[wiper_idx].terminal_hw = true; + break; + case 'a': + this->reg_[wiper_idx].terminal_a = true; + break; + case 'b': + this->reg_[wiper_idx].terminal_b = true; + break; + case 'w': + this->reg_[wiper_idx].terminal_w = true; + break; + default: + ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); + return; + } + this->reg_[wiper_idx].update_terminal = false; +} + +void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return; + } + uint8_t wiper_idx = static_cast(wiper); + ESP_LOGV(TAG, "Disabling terminal %c of wiper %u", terminal, wiper_idx); + switch (terminal) { + case 'h': + this->reg_[wiper_idx].terminal_hw = false; + break; + case 'a': + this->reg_[wiper_idx].terminal_a = false; + break; + case 'b': + this->reg_[wiper_idx].terminal_b = false; + break; + case 'w': + this->reg_[wiper_idx].terminal_w = false; + break; + default: + ESP_LOGW(TAG, "Unknown terminal %c specified", terminal); + return; + } + this->reg_[wiper_idx].update_terminal = false; +} + +uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return 0; + } + uint8_t reg = 0; + reg |= static_cast(Mcp4461EepromLocation::MCP4461_EEPROM_1) + (static_cast(location) * 0x10); + reg |= static_cast(Mcp4461Commands::READ); + uint16_t buf; + if (!this->is_eeprom_ready_for_writing_(true)) { + return 0; + } + if (!this->read_byte_16(reg, &buf)) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + ESP_LOGW(TAG, "Error fetching EEPROM location value"); + return 0; + } + return buf; +} + +bool Mcp4461Component::set_eeprom_value(Mcp4461EepromLocation location, uint16_t value) { + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(this->get_message_string(this->error_code_))); + return false; + } + uint8_t addr = 0; + if (value > 511) { + return false; + } + if (value > 256) { + addr = 1; + } + addr |= static_cast(Mcp4461EepromLocation::MCP4461_EEPROM_1) + (static_cast(location) * 0x10); + if (!(this->mcp4461_write_(addr, value, true))) { + this->error_code_ = MCP4461_STATUS_I2C_ERROR; + this->status_set_warning(); + ESP_LOGW(TAG, "Error writing EEPROM value"); + return false; + } + return true; +} + +bool Mcp4461Component::is_writing_() { + /* Read the EEPROM write-active status from the status register */ + bool writing = static_cast((this->get_status_register_() >> 4) & 0x01); + + /* If EEPROM is no longer writing, reset the timeout flag */ + if (!writing) { + /* This is protected boolean flag in Mcp4461Component class */ + this->last_eeprom_write_timed_out_ = false; + } + + return writing; +} + +bool Mcp4461Component::is_eeprom_ready_for_writing_(bool wait_if_not_ready) { + /* Check initial write status */ + bool ready_for_write = !this->is_writing_(); + + /* Return early if no waiting is required or EEPROM is already ready */ + if (ready_for_write || !wait_if_not_ready || this->last_eeprom_write_timed_out_) { + return ready_for_write; + } + + /* Timestamp before starting the loop */ + const uint32_t start_millis = millis(); + + ESP_LOGV(TAG, "Waiting until EEPROM is ready for write, start_millis = %" PRIu32, start_millis); + + /* Loop until EEPROM is ready or timeout is reached */ + while (!ready_for_write && ((millis() - start_millis) < EEPROM_WRITE_TIMEOUT_MS)) { + ready_for_write = !this->is_writing_(); + + /* If ready, exit early */ + if (ready_for_write) { + ESP_LOGV(TAG, "EEPROM is ready for new write, elapsed_millis = %" PRIu32, millis() - start_millis); + return true; + } + + /* Not ready yet, yield before checking again */ + yield(); + } + + /* If still not ready after timeout, log error and mark the timeout */ + ESP_LOGE(TAG, "EEPROM write timeout exceeded (%u ms)", EEPROM_WRITE_TIMEOUT_MS); + this->last_eeprom_write_timed_out_ = true; + + return false; +} + +bool Mcp4461Component::mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolatile) { + uint8_t reg = data > 0xff ? 1 : 0; + uint8_t value_byte = static_cast(data & 0x00ff); + ESP_LOGV(TAG, "Writing value %u to address %u", data, addr); + reg |= addr; + reg |= static_cast(Mcp4461Commands::WRITE); + if (nonvolatile) { + if (this->write_protected_) { + ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WRITE_PROTECTED))); + return false; + } + if (!this->is_eeprom_ready_for_writing_(true)) { + return false; + } + } + return this->write_byte(reg, value_byte); +} +} // namespace mcp4461 +} // namespace esphome diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h new file mode 100644 index 0000000000..9b7f60f201 --- /dev/null +++ b/esphome/components/mcp4461/mcp4461.h @@ -0,0 +1,171 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4461 { + +struct WiperState { + bool enabled = true; + uint16_t state = 0; + optional initial_value; + bool terminal_a = true; + bool terminal_b = true; + bool terminal_w = true; + bool terminal_hw = true; + bool wiper_lock_active = false; + bool update_level = false; + bool update_terminal = false; +}; + +// default wiper state is 128 / 0x80h +enum class Mcp4461Commands : uint8_t { WRITE = 0x00, INCREMENT = 0x04, DECREMENT = 0x08, READ = 0x0C }; + +enum class Mcp4461Addresses : uint8_t { + MCP4461_VW0 = 0x00, + MCP4461_VW1 = 0x10, + MCP4461_VW2 = 0x60, + MCP4461_VW3 = 0x70, + MCP4461_STATUS = 0x50, + MCP4461_TCON0 = 0x40, + MCP4461_TCON1 = 0xA0, + MCP4461_EEPROM_1 = 0xB0 +}; + +enum class Mcp4461WiperIdx : uint8_t { + MCP4461_WIPER_0 = 0, + MCP4461_WIPER_1 = 1, + MCP4461_WIPER_2 = 2, + MCP4461_WIPER_3 = 3, + MCP4461_WIPER_4 = 4, + MCP4461_WIPER_5 = 5, + MCP4461_WIPER_6 = 6, + MCP4461_WIPER_7 = 7 +}; + +enum class Mcp4461EepromLocation : uint8_t { + MCP4461_EEPROM_0 = 0, + MCP4461_EEPROM_1 = 1, + MCP4461_EEPROM_2 = 2, + MCP4461_EEPROM_3 = 3, + MCP4461_EEPROM_4 = 4 +}; + +enum class Mcp4461TerminalIdx : uint8_t { MCP4461_TERMINAL_0 = 0, MCP4461_TERMINAL_1 = 1 }; + +class Mcp4461Wiper; + +// Mcp4461Component +class Mcp4461Component : public Component, public i2c::I2CDevice { + public: + Mcp4461Component(bool disable_wiper_0, bool disable_wiper_1, bool disable_wiper_2, bool disable_wiper_3) + : wiper_0_disabled_(disable_wiper_0), + wiper_1_disabled_(disable_wiper_1), + wiper_2_disabled_(disable_wiper_2), + wiper_3_disabled_(disable_wiper_3) { + this->reg_[0].enabled = !wiper_0_disabled_; + this->reg_[1].enabled = !wiper_1_disabled_; + this->reg_[2].enabled = !wiper_2_disabled_; + this->reg_[3].enabled = !wiper_3_disabled_; + } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + /// @brief get eeprom value from location + /// @param[in] location - eeprom location 0-4 + /// @return eeprom value of specified location (9 bits max) + uint16_t get_eeprom_value(Mcp4461EepromLocation location); + /// @brief set eeprom value at specified location + /// @param[in] location - eeprom location 0-4 + /// @param[in] value - 9 bits value to store + bool set_eeprom_value(Mcp4461EepromLocation location, uint16_t value); + /// @brief public function used to set initial value + /// @param[in] wiper - the wiper to set the value for + /// @param[in] initial_value - the initial value in range 0-1.0 as float + void set_initial_value(Mcp4461WiperIdx wiper, float initial_value); + /// @brief public function used to set disable terminal config + /// @param[in] wiper - the wiper to set the value for + /// @param[in] terminal - the terminal to disable, one of ['a','b','w','h'] + void initialize_terminal_disabled(Mcp4461WiperIdx wiper, char terminal); + /// @brief read status register to log + void read_status_register_to_log(); + + protected: + friend class Mcp4461Wiper; + void update_write_protection_status_(); + uint8_t get_wiper_address_(uint8_t wiper); + uint16_t read_wiper_level_(uint8_t wiper); + uint8_t get_status_register_(); + uint16_t get_wiper_level_(Mcp4461WiperIdx wiper); + bool set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value); + bool update_wiper_level_(Mcp4461WiperIdx wiper); + void enable_wiper_(Mcp4461WiperIdx wiper); + void disable_wiper_(Mcp4461WiperIdx wiper); + bool increase_wiper_(Mcp4461WiperIdx wiper); + bool decrease_wiper_(Mcp4461WiperIdx wiper); + void enable_terminal_(Mcp4461WiperIdx wiper, char terminal); + void disable_terminal_(Mcp4461WiperIdx, char terminal); + bool is_writing_(); + bool is_eeprom_ready_for_writing_(bool wait_if_not_ready); + void write_wiper_level_(uint8_t wiper, uint16_t value); + bool mcp4461_write_(uint8_t addr, uint16_t data, bool nonvolatile = false); + uint8_t calc_terminal_connector_byte_(Mcp4461TerminalIdx terminal_connector); + void update_terminal_register_(Mcp4461TerminalIdx terminal_connector); + uint8_t get_terminal_register_(Mcp4461TerminalIdx terminal_connector); + bool set_terminal_register_(Mcp4461TerminalIdx terminal_connector, uint8_t data); + + // Converts a status to a human readable string + static const LogString *get_message_string(int status) { + switch (status) { + case MCP4461_STATUS_I2C_ERROR: + return LOG_STR("I2C error - communication with MCP4461 failed!"); + case MCP4461_STATUS_REGISTER_ERROR: + return LOG_STR("Status register could not be read"); + case MCP4461_STATUS_REGISTER_INVALID: + return LOG_STR("Invalid status register value - bits 1,7 or 8 are 0"); + case MCP4461_VALUE_INVALID: + return LOG_STR("Invalid value for wiper given"); + case MCP4461_WRITE_PROTECTED: + return LOG_STR("MCP4461 is write protected. Setting nonvolatile wipers/eeprom values is prohibited."); + case MCP4461_WIPER_ENABLED: + return LOG_STR("MCP4461 Wiper is already enabled, ignoring cmd to enable."); + case MCP4461_WIPER_DISABLED: + return LOG_STR("MCP4461 Wiper is disabled. All actions on this wiper are prohibited."); + case MCP4461_WIPER_LOCKED: + return LOG_STR("MCP4461 Wiper is locked using WiperLock-technology. All actions on this wiper are prohibited."); + case MCP4461_STATUS_OK: + return LOG_STR("Status OK"); + default: + return LOG_STR("Unknown"); + } + } + + enum ErrorCode { + MCP4461_STATUS_OK = 0, // CMD completed successfully + MCP4461_FAILED, // component failed + MCP4461_STATUS_I2C_ERROR, // Unable to communicate with device + MCP4461_STATUS_REGISTER_INVALID, // Status register value was invalid + MCP4461_STATUS_REGISTER_ERROR, // Error fetching status register + MCP4461_PROHIBITED_FOR_NONVOLATILE, // + MCP4461_VALUE_INVALID, // Invalid value given for wiper / eeprom + MCP4461_WRITE_PROTECTED, // The value was read, but the CRC over the payload (valid and data) does not match + MCP4461_WIPER_ENABLED, // The wiper is enabled, discard additional enabling actions + MCP4461_WIPER_DISABLED, // The wiper is disabled - all actions for this wiper will be aborted/discarded + MCP4461_WIPER_LOCKED, // The wiper is locked using WiperLock-technology - all actions for this wiper will be + // aborted/discarded + } error_code_{MCP4461_STATUS_OK}; + + WiperState reg_[8]; + bool last_eeprom_write_timed_out_{false}; + bool write_protected_{false}; + bool wiper_0_disabled_{false}; + bool wiper_1_disabled_{false}; + bool wiper_2_disabled_{false}; + bool wiper_3_disabled_{false}; +}; +} // namespace mcp4461 +} // namespace esphome diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py new file mode 100644 index 0000000000..ba59f97643 --- /dev/null +++ b/esphome/components/mcp4461/output/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_INITIAL_VALUE +from .. import Mcp4461Component, CONF_MCP4461_ID, mcp4461_ns + +DEPENDENCIES = ["mcp4461"] + +Mcp4461Wiper = mcp4461_ns.class_( + "Mcp4461Wiper", output.FloatOutput, cg.Parented.template(Mcp4461Component) +) + +Mcp4461WiperIdx = mcp4461_ns.enum("Mcp4461WiperIdx", is_class=True) +CHANNEL_OPTIONS = { + "A": Mcp4461WiperIdx.MCP4461_WIPER_0, + "B": Mcp4461WiperIdx.MCP4461_WIPER_1, + "C": Mcp4461WiperIdx.MCP4461_WIPER_2, + "D": Mcp4461WiperIdx.MCP4461_WIPER_3, + "E": Mcp4461WiperIdx.MCP4461_WIPER_4, + "F": Mcp4461WiperIdx.MCP4461_WIPER_5, + "G": Mcp4461WiperIdx.MCP4461_WIPER_6, + "H": Mcp4461WiperIdx.MCP4461_WIPER_7, +} + +CONF_TERMINAL_A = "terminal_a" +CONF_TERMINAL_B = "terminal_b" +CONF_TERMINAL_W = "terminal_w" + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(Mcp4461Wiper), + cv.GenerateID(CONF_MCP4461_ID): cv.use_id(Mcp4461Component), + cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True), + cv.Optional(CONF_TERMINAL_A, default=True): cv.boolean, + cv.Optional(CONF_TERMINAL_B, default=True): cv.boolean, + cv.Optional(CONF_TERMINAL_W, default=True): cv.boolean, + cv.Optional(CONF_INITIAL_VALUE): cv.float_range(min=0.0, max=1.0), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_MCP4461_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + parent, + config[CONF_CHANNEL], + ) + if not config[CONF_TERMINAL_A]: + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "a")) + if not config[CONF_TERMINAL_B]: + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "b")) + if not config[CONF_TERMINAL_W]: + cg.add(parent.initialize_terminal_disabled(config[CONF_CHANNEL], "w")) + if CONF_INITIAL_VALUE in config: + cg.add( + parent.set_initial_value(config[CONF_CHANNEL], config[CONF_INITIAL_VALUE]) + ) + await output.register_output(var, config) + await cg.register_parented(var, config[CONF_MCP4461_ID]) diff --git a/esphome/components/mcp4461/output/mcp4461_output.cpp b/esphome/components/mcp4461/output/mcp4461_output.cpp new file mode 100644 index 0000000000..2d85a5df61 --- /dev/null +++ b/esphome/components/mcp4461/output/mcp4461_output.cpp @@ -0,0 +1,73 @@ +#include "mcp4461_output.h" +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4461 { + +static const char *const TAG = "mcp4461.output"; + +// public set_level function +void Mcp4461Wiper::set_level(float state) { + if (!std::isfinite(state)) { + ESP_LOGW(TAG, "Finite state state value is required."); + return; + } + state = clamp(state, 0.0f, 1.0f); + if (this->is_inverted()) { + state = 1.0f - state; + } + this->write_state(state); +} + +// floats from other components (like light etc.) are passed as "percentage floats" +// this function converts them to the 0 - 256 range used by the MCP4461 +void Mcp4461Wiper::write_state(float state) { + if (this->parent_->set_wiper_level_(this->wiper_, static_cast(std::roundf(state * 256)))) { + this->state_ = state; + } +} + +float Mcp4461Wiper::read_state() { return (static_cast(this->parent_->get_wiper_level_(this->wiper_)) / 256.0); } + +float Mcp4461Wiper::update_state() { + this->state_ = this->read_state(); + return this->state_; +} + +void Mcp4461Wiper::set_state(bool state) { + if (state) { + this->turn_on(); + } else { + this->turn_off(); + } +} + +void Mcp4461Wiper::turn_on() { this->parent_->enable_wiper_(this->wiper_); } + +void Mcp4461Wiper::turn_off() { this->parent_->disable_wiper_(this->wiper_); } + +void Mcp4461Wiper::increase_wiper() { + if (this->parent_->increase_wiper_(this->wiper_)) { + this->state_ = this->update_state(); + ESP_LOGV(TAG, "Increased wiper %u to %u", static_cast(this->wiper_), + static_cast(std::roundf(this->state_ * 256))); + } +} + +void Mcp4461Wiper::decrease_wiper() { + if (this->parent_->decrease_wiper_(this->wiper_)) { + this->state_ = this->update_state(); + ESP_LOGV(TAG, "Decreased wiper %u to %u", static_cast(this->wiper_), + static_cast(std::roundf(this->state_ * 256))); + } +} + +void Mcp4461Wiper::enable_terminal(char terminal) { this->parent_->enable_terminal_(this->wiper_, terminal); } + +void Mcp4461Wiper::disable_terminal(char terminal) { this->parent_->disable_terminal_(this->wiper_, terminal); } + +} // namespace mcp4461 +} // namespace esphome diff --git a/esphome/components/mcp4461/output/mcp4461_output.h b/esphome/components/mcp4461/output/mcp4461_output.h new file mode 100644 index 0000000000..4055cef30a --- /dev/null +++ b/esphome/components/mcp4461/output/mcp4461_output.h @@ -0,0 +1,49 @@ +#pragma once + +#include "../mcp4461.h" +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4461 { + +class Mcp4461Wiper : public output::FloatOutput, public Parented { + public: + Mcp4461Wiper(Mcp4461Component *parent, Mcp4461WiperIdx wiper) : parent_(parent), wiper_(wiper) {} + /// @brief Set level of wiper + /// @param[in] state - The desired float level in range 0-1.0 + void set_level(float state); + /// @brief Enables/Disables current output using bool parameter + /// @param[in] state boolean var representing desired state (true=ON, false=OFF) + void set_state(bool state) override; + /// @brief Enables current output + void turn_on() override; + /// @brief Disables current output + void turn_off() override; + /// @brief Read current device wiper state without updating internal output state + /// @return float - current device state as float in range 0 - 1.0 + float read_state(); + /// @brief Update current output state using device wiper state + /// @return float - current updated output state as float in range 0 - 1.0 + float update_state(); + /// @brief Increase wiper by 1 tap + void increase_wiper(); + /// @brief Decrease wiper by 1 tap + void decrease_wiper(); + /// @brief Enable given terminal + /// @param[in] terminal single char parameter defining desired terminal to enable, one of { 'a', 'b', 'w', 'h' } + void enable_terminal(char terminal); + /// @brief Disable given terminal + /// @param[in] terminal single char parameter defining desired terminal to disable, one of { 'a', 'b', 'w', 'h' } + void disable_terminal(char terminal); + + protected: + void write_state(float state) override; + Mcp4461Component *parent_; + Mcp4461WiperIdx wiper_; + float state_; +}; + +} // namespace mcp4461 +} // namespace esphome diff --git a/tests/components/mcp4461/common.yaml b/tests/components/mcp4461/common.yaml new file mode 100644 index 0000000000..ce1866fdb8 --- /dev/null +++ b/tests/components/mcp4461/common.yaml @@ -0,0 +1,28 @@ +i2c: + - id: i2c_mcp4461 + sda: ${sda_pin} + scl: ${scl_pin} + +mcp4461: + - id: mcp4461_digipot_01 + +output: + - platform: mcp4461 + id: digipot_wiper_1 + mcp4461_id: mcp4461_digipot_01 + channel: A + + - platform: mcp4461 + id: digipot_wiper_2 + mcp4461_id: mcp4461_digipot_01 + channel: B + + - platform: mcp4461 + id: digipot_wiper_3 + mcp4461_id: mcp4461_digipot_01 + channel: C + + - platform: mcp4461 + id: digipot_wiper_4 + mcp4461_id: mcp4461_digipot_01 + channel: D diff --git a/tests/components/mcp4461/test.esp32-ard.yaml b/tests/components/mcp4461/test.esp32-ard.yaml new file mode 100644 index 0000000000..c5deb7ca0a --- /dev/null +++ b/tests/components/mcp4461/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO16 + scl_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-c3-ard.yaml b/tests/components/mcp4461/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..a87353b78b --- /dev/null +++ b/tests/components/mcp4461/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO4 + scl_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-c3-idf.yaml b/tests/components/mcp4461/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..a87353b78b --- /dev/null +++ b/tests/components/mcp4461/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO4 + scl_pin: GPIO5 + +<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp32-idf.yaml b/tests/components/mcp4461/test.esp32-idf.yaml new file mode 100644 index 0000000000..c5deb7ca0a --- /dev/null +++ b/tests/components/mcp4461/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO16 + scl_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/mcp4461/test.esp8266-ard.yaml b/tests/components/mcp4461/test.esp8266-ard.yaml new file mode 100644 index 0000000000..a87353b78b --- /dev/null +++ b/tests/components/mcp4461/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + sda_pin: GPIO4 + scl_pin: GPIO5 + +<<: !include common.yaml