mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[mcp4461] nonvolatile-by-default persistence, TCON boot sync, wiper actions (#17561)
Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Oliver Kleinecke <kleinecke.oliver@googlemail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude
pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Oliver Kleinecke
Copilot Autofix powered by AI
parent
9bfce75bc5
commit
4627f07a7b
@@ -21,7 +21,18 @@ void Mcp4461Component::setup() {
|
||||
auto init_val = this->reg_[i].initial_value;
|
||||
if (init_val.has_value()) {
|
||||
uint16_t initial_state = static_cast<uint16_t>(*init_val * 256.0f);
|
||||
this->write_wiper_level_(i, initial_state);
|
||||
if (i > 3) {
|
||||
// NV wiper: an unconditional write would cost one EEPROM erase/write cycle on EVERY
|
||||
// boot. Only write when the stored value actually differs — and always write when
|
||||
// the read itself failed (a failed read returns 0, which would silently skip the
|
||||
// write whenever initial_value is 0).
|
||||
bool read_ok = false;
|
||||
if (this->read_wiper_level_(i, &read_ok) != initial_state || !read_ok) {
|
||||
this->write_wiper_level_(i, initial_state);
|
||||
}
|
||||
} else {
|
||||
this->write_wiper_level_(i, initial_state);
|
||||
}
|
||||
}
|
||||
if (this->reg_[i].enabled) {
|
||||
this->reg_[i].state = this->read_wiper_level_(i);
|
||||
@@ -34,6 +45,23 @@ void Mcp4461Component::setup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Push the YAML terminal configuration to the TCON registers. TCON is volatile — on POR
|
||||
// the chip restores wiper levels from the NV registers but resets TCON to "all terminals
|
||||
// connected", so any terminal_a/b/w disables from the config MUST be written here.
|
||||
for (uint8_t t = 0; t < 2; t++) {
|
||||
Mcp4461TerminalIdx terminal_connector = static_cast<Mcp4461TerminalIdx>(t);
|
||||
uint8_t terminal_byte = this->calc_terminal_connector_byte_(terminal_connector);
|
||||
this->set_terminal_register_(terminal_connector, terminal_byte);
|
||||
}
|
||||
}
|
||||
|
||||
void Mcp4461Component::set_nonvolatile(Mcp4461WiperIdx wiper, uint32_t write_delay_ms) {
|
||||
uint8_t wiper_idx = static_cast<uint8_t>(wiper);
|
||||
if (wiper_idx > 3) {
|
||||
return; // NV channels E-H are the persistence target themselves
|
||||
}
|
||||
this->reg_[wiper_idx].nonvolatile = true;
|
||||
this->reg_[wiper_idx].nonvolatile_write_delay_ms = write_delay_ms;
|
||||
}
|
||||
|
||||
void Mcp4461Component::set_initial_value(Mcp4461WiperIdx wiper, float initial_value) {
|
||||
@@ -77,9 +105,12 @@ void Mcp4461Component::dump_config() {
|
||||
// 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].enabled), ONOFF(this->reg_[i].terminal_hw),
|
||||
ONOFF(this->reg_[i].terminal_a), ONOFF(this->reg_[i].terminal_b), ONOFF(this->reg_[i].terminal_w));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" ├── Volatile wiper [%u] level: %u, Status: %s, HW: %s, "
|
||||
"A: %s, B: %s, W: %s, NV: %s",
|
||||
i, this->reg_[i].state, ONOFF(this->reg_[i].enabled), 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].nonvolatile));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " ├── Nonvolatile wiper [%u] level: %u", i, this->reg_[i].state);
|
||||
}
|
||||
@@ -92,8 +123,10 @@ void Mcp4461Component::loop() {
|
||||
}
|
||||
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)) {
|
||||
// set wiper i state if changed — a failed read (returns 0) must not suppress the
|
||||
// write when the target state is 0, same hardening as the NV read-compare paths
|
||||
bool read_ok = false;
|
||||
if (this->reg_[i].state != this->read_wiper_level_(i, &read_ok) || !read_ok) {
|
||||
this->write_wiper_level_(i, this->reg_[i].state);
|
||||
}
|
||||
}
|
||||
@@ -112,6 +145,67 @@ void Mcp4461Component::loop() {
|
||||
}
|
||||
this->reg_[i].update_terminal = false;
|
||||
}
|
||||
this->process_nonvolatile_dirty_();
|
||||
}
|
||||
|
||||
void Mcp4461Component::process_nonvolatile_dirty_() {
|
||||
const uint32_t now = millis();
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (!this->reg_[i].nonvolatile || !this->reg_[i].nonvolatile_dirty) {
|
||||
continue;
|
||||
}
|
||||
if ((now - this->reg_[i].last_level_change_ms) < this->reg_[i].nonvolatile_write_delay_ms) {
|
||||
continue; // still settling — debounce window not over yet
|
||||
}
|
||||
// Never block the loop on a still-running EEPROM cycle (t_WC up to 10 ms); datasheet:
|
||||
// during an EEPROM write only volatile commands are accepted. Retry on the next loop.
|
||||
if (this->is_writing_()) {
|
||||
continue;
|
||||
}
|
||||
// Clear the dirty flag on success — and equally when WP or WiperLock block the write
|
||||
// permanently, instead of retrying forever.
|
||||
if (this->store_level_nonvolatile_(static_cast<Mcp4461WiperIdx>(i)) || this->write_protected_ ||
|
||||
this->reg_[i].wiper_lock_active) {
|
||||
this->reg_[i].nonvolatile_dirty = false;
|
||||
} else {
|
||||
// Transient failure (e.g. I2C error): without this, the retry fires on every single
|
||||
// loop() iteration, spamming a warning each time. Re-arming the timestamp reuses the
|
||||
// stability delay as a natural retry backoff.
|
||||
this->reg_[i].last_level_change_ms = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Mcp4461Component::store_level_nonvolatile_(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<uint8_t>(wiper);
|
||||
if (wiper_idx > 3) {
|
||||
// E-H ARE the nonvolatile registers — keep this consistent with the other guards
|
||||
// instead of failing silently (reachable via the store_nonvolatile action).
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
|
||||
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;
|
||||
}
|
||||
const uint16_t level = this->reg_[wiper_idx].state;
|
||||
// Skip the EEPROM cycle entirely when the NV register already holds the value. A failed
|
||||
// read must NOT count as a match (it returns 0): fall through to the write instead — if
|
||||
// the bus is really down, the write fails too and the dirty flag stays set for a retry.
|
||||
bool read_ok = false;
|
||||
if (this->read_wiper_level_(wiper_idx + 4, &read_ok) == level && read_ok) {
|
||||
return true;
|
||||
}
|
||||
ESP_LOGV(TAG, "Persisting wiper %u level %u to nonvolatile register", wiper_idx, level);
|
||||
if (!this->mcp4461_write_(this->get_wiper_address_(wiper_idx + 4), level, true)) {
|
||||
ESP_LOGW(TAG, "Error persisting wiper %u level %u", wiper_idx, level);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t Mcp4461Component::get_status_register_() {
|
||||
@@ -210,7 +304,10 @@ uint16_t Mcp4461Component::get_wiper_level_(Mcp4461WiperIdx wiper) {
|
||||
return this->read_wiper_level_(wiper_idx);
|
||||
}
|
||||
|
||||
uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) {
|
||||
uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx, bool *ok) {
|
||||
if (ok != nullptr) {
|
||||
*ok = false;
|
||||
}
|
||||
uint8_t addr = this->get_wiper_address_(wiper_idx);
|
||||
uint8_t reg = addr | static_cast<uint8_t>(Mcp4461Commands::READ);
|
||||
if (wiper_idx > 3) {
|
||||
@@ -225,6 +322,9 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) {
|
||||
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx);
|
||||
return 0;
|
||||
}
|
||||
if (ok != nullptr) {
|
||||
*ok = true;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -265,6 +365,10 @@ bool Mcp4461Component::set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value) {
|
||||
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;
|
||||
if (this->reg_[wiper_idx].nonvolatile) {
|
||||
this->reg_[wiper_idx].nonvolatile_dirty = true;
|
||||
this->reg_[wiper_idx].last_level_change_ms = millis();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -335,6 +439,12 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
|
||||
return false;
|
||||
}
|
||||
if (wiper_idx > 3) {
|
||||
// Datasheet: increment commands are only valid for the volatile wiper registers —
|
||||
// the chip NACKs them on nonvolatile addresses.
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
|
||||
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;
|
||||
@@ -349,6 +459,10 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) {
|
||||
return false;
|
||||
}
|
||||
this->reg_[wiper_idx].state++;
|
||||
if (this->reg_[wiper_idx].nonvolatile) {
|
||||
this->reg_[wiper_idx].nonvolatile_dirty = true;
|
||||
this->reg_[wiper_idx].last_level_change_ms = millis();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -366,6 +480,12 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_WIPER_LOCKED)));
|
||||
return false;
|
||||
}
|
||||
if (wiper_idx > 3) {
|
||||
// Datasheet: decrement commands are only valid for the volatile wiper registers —
|
||||
// the chip NACKs them on nonvolatile addresses.
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
|
||||
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;
|
||||
@@ -380,11 +500,18 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) {
|
||||
return false;
|
||||
}
|
||||
this->reg_[wiper_idx].state--;
|
||||
if (this->reg_[wiper_idx].nonvolatile) {
|
||||
this->reg_[wiper_idx].nonvolatile_dirty = true;
|
||||
this->reg_[wiper_idx].last_level_change_ms = millis();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t Mcp4461Component::calc_terminal_connector_byte_(Mcp4461TerminalIdx terminal_connector) {
|
||||
uint8_t i = static_cast<uint8_t>(terminal_connector) <= 1 ? 0 : 2;
|
||||
// TCON0 covers wipers 0/1 (A/B), TCON1 covers wipers 2/3 (C/D). The enum only holds
|
||||
// 0 and 1, so the old `<= 1 ? 0 : 2` collapsed to always-0 and built TCON1 from
|
||||
// channels A/B's flags — mirror the (correct) read path in update_terminal_register_().
|
||||
uint8_t i = static_cast<uint8_t>(terminal_connector) == 0 ? 0 : 2;
|
||||
uint8_t new_value_byte = 0;
|
||||
new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_b);
|
||||
new_value_byte += static_cast<uint8_t>(this->reg_[i].terminal_w) << 1;
|
||||
@@ -471,6 +598,12 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) {
|
||||
return;
|
||||
}
|
||||
uint8_t wiper_idx = static_cast<uint8_t>(wiper);
|
||||
if (wiper_idx > 3) {
|
||||
// Terminal control only exists for the volatile wipers; loop() would otherwise emit
|
||||
// an unrelated TCON write and silently drop the request.
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Enabling terminal %c of wiper %u", terminal, wiper_idx);
|
||||
switch (terminal) {
|
||||
case 'h':
|
||||
@@ -498,6 +631,10 @@ void Mcp4461Component::disable_terminal_(Mcp4461WiperIdx wiper, char terminal) {
|
||||
return;
|
||||
}
|
||||
uint8_t wiper_idx = static_cast<uint8_t>(wiper);
|
||||
if (wiper_idx > 3) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(this->get_message_string(MCP4461_PROHIBITED_FOR_NONVOLATILE)));
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Disabling terminal %c of wiper %u", terminal, wiper_idx);
|
||||
switch (terminal) {
|
||||
case 'h':
|
||||
|
||||
@@ -17,6 +17,16 @@ struct WiperState {
|
||||
bool wiper_lock_active = false;
|
||||
bool update_level = false;
|
||||
bool update_terminal = false;
|
||||
// Nonvolatile persistence (volatile wipers 0-3 only): when enabled, every level change is
|
||||
// mirrored into the chip's NV wiper register after nonvolatile_write_delay of stability, so
|
||||
// the chip restores it on power-on. The delay both debounces bursts (e.g. light transitions
|
||||
// writing dozens of levels per second) and protects the EEPROM's limited endurance —
|
||||
// without it, every intermediate step would cost one of the ~1M erase/write cycles and
|
||||
// stall the bus for up to t_WC (10 ms) each.
|
||||
bool nonvolatile = false;
|
||||
uint32_t nonvolatile_write_delay_ms = 1000;
|
||||
bool nonvolatile_dirty = false;
|
||||
uint32_t last_level_change_ms = 0;
|
||||
};
|
||||
|
||||
// default wiper state is 128 / 0x80h
|
||||
@@ -86,6 +96,11 @@ class Mcp4461Component final : public Component, public i2c::I2CDevice {
|
||||
/// @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 enable nonvolatile persistence for a volatile wiper (0-3): every level change is
|
||||
/// mirrored to the corresponding NV wiper register after the given stability delay
|
||||
/// @param[in] wiper - the (volatile) wiper to persist
|
||||
/// @param[in] write_delay_ms - stability delay before the NV write (debounce / EEPROM wear)
|
||||
void set_nonvolatile(Mcp4461WiperIdx wiper, uint32_t write_delay_ms);
|
||||
/// @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']
|
||||
@@ -98,7 +113,10 @@ class Mcp4461Component final : public Component, public i2c::I2CDevice {
|
||||
bool read_16_(uint8_t address, uint16_t *buf);
|
||||
void update_write_protection_status_();
|
||||
uint8_t get_wiper_address_(uint8_t wiper);
|
||||
uint16_t read_wiper_level_(uint8_t wiper);
|
||||
/// Read a wiper register. On I2C failure returns 0 — callers that must distinguish
|
||||
/// a real 0 from a failed read pass `ok` (added for the NV read-compare paths, where
|
||||
/// acting on a failed read would skip a required write or drop a pending persist).
|
||||
uint16_t read_wiper_level_(uint8_t wiper, bool *ok = nullptr);
|
||||
uint8_t get_status_register_();
|
||||
uint16_t get_wiper_level_(Mcp4461WiperIdx wiper);
|
||||
bool set_wiper_level_(Mcp4461WiperIdx wiper, uint16_t value);
|
||||
@@ -110,6 +128,11 @@ class Mcp4461Component final : public Component, public i2c::I2CDevice {
|
||||
void enable_terminal_(Mcp4461WiperIdx wiper, char terminal);
|
||||
void disable_terminal_(Mcp4461WiperIdx, char terminal);
|
||||
bool is_writing_();
|
||||
/// Copy the current volatile level of wiper 0-3 into its NV register (immediate, blocking
|
||||
/// only for a pending previous EEPROM cycle). Returns false while WP is active or on error.
|
||||
bool store_level_nonvolatile_(Mcp4461WiperIdx wiper);
|
||||
/// Deferred NV mirroring driven from loop() — see WiperState::nonvolatile.
|
||||
void process_nonvolatile_dirty_();
|
||||
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);
|
||||
@@ -139,6 +162,9 @@ class Mcp4461Component final : public Component, public i2c::I2CDevice {
|
||||
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");
|
||||
case MCP4461_PROHIBITED_FOR_NONVOLATILE:
|
||||
return LOG_STR(
|
||||
"Increment/decrement, store, and terminal control are prohibited on the nonvolatile wipers (E-H).");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
@@ -26,6 +27,43 @@ CHANNEL_OPTIONS = {
|
||||
CONF_TERMINAL_A = "terminal_a"
|
||||
CONF_TERMINAL_B = "terminal_b"
|
||||
CONF_TERMINAL_W = "terminal_w"
|
||||
CONF_NONVOLATILE = "nonvolatile"
|
||||
CONF_NONVOLATILE_WRITE_DELAY = "nonvolatile_write_delay"
|
||||
|
||||
# Volatile wiper channels that have a nonvolatile shadow register on the chip
|
||||
VOLATILE_CHANNELS = ("A", "B", "C", "D")
|
||||
|
||||
|
||||
def _validate_nonvolatile(config):
|
||||
channel = str(config[CONF_CHANNEL])
|
||||
|
||||
# Channels E-H address the nonvolatile registers directly — the mirroring options only
|
||||
# make sense for the volatile channels A-D.
|
||||
if channel not in VOLATILE_CHANNELS:
|
||||
# Only reject what the user EXPLICITLY asked for and cannot have: enabling the
|
||||
# mirroring or tuning its delay on E-H. An explicit `nonvolatile: false` is a
|
||||
# harmless no-op and stays valid; bare configs (no key at all) must keep working.
|
||||
# NOTE: FINAL_VALIDATE_SCHEMA intentionally mutates `config` in-place (uses setdefault) to apply defaults for callers.
|
||||
if config.get(CONF_NONVOLATILE) or CONF_NONVOLATILE_WRITE_DELAY in config:
|
||||
raise cv.Invalid(
|
||||
f"enabling '{CONF_NONVOLATILE}' or setting '{CONF_NONVOLATILE_WRITE_DELAY}' is only valid for the "
|
||||
f"volatile channels A-D; channels E-H are the nonvolatile registers themselves"
|
||||
)
|
||||
return config
|
||||
|
||||
config.setdefault(CONF_NONVOLATILE, True)
|
||||
if config[CONF_NONVOLATILE]:
|
||||
config.setdefault(
|
||||
CONF_NONVOLATILE_WRITE_DELAY,
|
||||
cv.positive_time_period_milliseconds("1s"),
|
||||
)
|
||||
elif CONF_NONVOLATILE_WRITE_DELAY in config:
|
||||
# Same consistency as the E-H rejection above: never silently ignore user input.
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_NONVOLATILE_WRITE_DELAY}' requires '{CONF_NONVOLATILE}: true'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
@@ -36,9 +74,21 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
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),
|
||||
# No schema defaults here: a default would materialize the keys on EVERY channel,
|
||||
# making existing bare E-H configs fail final validation. The effective defaults
|
||||
# (nonvolatile: true, delay 1s) are applied for the volatile channels A-D inside
|
||||
# _validate_nonvolatile instead. Default-on rationale: the chip restores the
|
||||
# nonvolatile wiper levels at power-on, so persisting every settled level change is
|
||||
# the least surprising behavior — the pot simply comes back where it was. The write
|
||||
# is deferred by nonvolatile_write_delay to debounce transitions and protect the
|
||||
# EEPROM's endurance.
|
||||
cv.Optional(CONF_NONVOLATILE): cv.boolean,
|
||||
cv.Optional(CONF_NONVOLATILE_WRITE_DELAY): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_nonvolatile
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
parent = await cg.get_variable(config[CONF_MCP4461_ID])
|
||||
@@ -57,5 +107,71 @@ async def to_code(config):
|
||||
cg.add(
|
||||
parent.set_initial_value(config[CONF_CHANNEL], config[CONF_INITIAL_VALUE])
|
||||
)
|
||||
if str(config[CONF_CHANNEL]) in VOLATILE_CHANNELS and config[CONF_NONVOLATILE]:
|
||||
cg.add(
|
||||
parent.set_nonvolatile(
|
||||
config[CONF_CHANNEL],
|
||||
config[CONF_NONVOLATILE_WRITE_DELAY],
|
||||
)
|
||||
)
|
||||
await output.register_output(var, config)
|
||||
await cg.register_parented(var, config[CONF_MCP4461_ID])
|
||||
|
||||
|
||||
# ---- Actions ----
|
||||
WiperIncreaseAction = mcp4461_ns.class_("WiperIncreaseAction", automation.Action)
|
||||
WiperDecreaseAction = mcp4461_ns.class_("WiperDecreaseAction", automation.Action)
|
||||
WiperStoreNonvolatileAction = mcp4461_ns.class_(
|
||||
"WiperStoreNonvolatileAction", automation.Action
|
||||
)
|
||||
WiperSetTerminalAction = mcp4461_ns.class_("WiperSetTerminalAction", automation.Action)
|
||||
|
||||
WIPER_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.Required(CONF_ID): cv.use_id(Mcp4461Wiper)}
|
||||
)
|
||||
|
||||
CONF_TERMINAL = "terminal"
|
||||
CONF_ENABLE = "enable"
|
||||
|
||||
TERMINAL_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(Mcp4461Wiper),
|
||||
cv.Required(CONF_TERMINAL): cv.one_of("a", "b", "w", "h", lower=True),
|
||||
cv.Required(CONF_ENABLE): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"mcp4461.wiper.increase", WiperIncreaseAction, WIPER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"mcp4461.wiper.decrease", WiperDecreaseAction, WIPER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def mcp4461_wiper_step_to_code(config, action_id, template_arg, args):
|
||||
wiper = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, wiper)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"mcp4461.wiper.store_nonvolatile",
|
||||
WiperStoreNonvolatileAction,
|
||||
WIPER_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def mcp4461_wiper_store_to_code(config, action_id, template_arg, args):
|
||||
wiper = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, wiper)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"mcp4461.wiper.set_terminal",
|
||||
WiperSetTerminalAction,
|
||||
TERMINAL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def mcp4461_wiper_terminal_to_code(config, action_id, template_arg, args):
|
||||
wiper = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(
|
||||
action_id, template_arg, wiper, ord(config[CONF_TERMINAL]), config[CONF_ENABLE]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "mcp4461_output.h"
|
||||
|
||||
namespace esphome::mcp4461 {
|
||||
|
||||
template<typename... Ts> class WiperIncreaseAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit WiperIncreaseAction(Mcp4461Wiper *wiper) : wiper_(wiper) {}
|
||||
void play(Ts... x) override { this->wiper_->increase_wiper(); }
|
||||
|
||||
protected:
|
||||
Mcp4461Wiper *wiper_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class WiperDecreaseAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit WiperDecreaseAction(Mcp4461Wiper *wiper) : wiper_(wiper) {}
|
||||
void play(Ts... x) override { this->wiper_->decrease_wiper(); }
|
||||
|
||||
protected:
|
||||
Mcp4461Wiper *wiper_;
|
||||
};
|
||||
|
||||
// Persist the current level to the chip's nonvolatile register immediately — useful with
|
||||
// nonvolatile: false to persist only at deliberate moments (e.g. on a button press), or to
|
||||
// bypass the stability delay of the automatic mirroring.
|
||||
template<typename... Ts> class WiperStoreNonvolatileAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit WiperStoreNonvolatileAction(Mcp4461Wiper *wiper) : wiper_(wiper) {}
|
||||
void play(Ts... x) override { this->wiper_->store_nonvolatile(); }
|
||||
|
||||
protected:
|
||||
Mcp4461Wiper *wiper_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class WiperSetTerminalAction : public Action<Ts...> {
|
||||
public:
|
||||
WiperSetTerminalAction(Mcp4461Wiper *wiper, char terminal, bool enable)
|
||||
: wiper_(wiper), terminal_(terminal), enable_(enable) {}
|
||||
void play(Ts... x) override {
|
||||
if (this->enable_) {
|
||||
this->wiper_->enable_terminal(this->terminal_);
|
||||
} else {
|
||||
this->wiper_->disable_terminal(this->terminal_);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
Mcp4461Wiper *wiper_;
|
||||
char terminal_;
|
||||
bool enable_;
|
||||
};
|
||||
|
||||
} // namespace esphome::mcp4461
|
||||
@@ -66,6 +66,12 @@ void Mcp4461Wiper::decrease_wiper() {
|
||||
}
|
||||
}
|
||||
|
||||
void Mcp4461Wiper::store_nonvolatile() {
|
||||
if (this->parent_->store_level_nonvolatile_(this->wiper_)) {
|
||||
ESP_LOGV(TAG, "Stored wiper %u level to nonvolatile register", static_cast<uint8_t>(this->wiper_));
|
||||
}
|
||||
}
|
||||
|
||||
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); }
|
||||
|
||||
@@ -36,6 +36,9 @@ class Mcp4461Wiper final : public output::FloatOutput, public Parented<Mcp4461Co
|
||||
/// @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);
|
||||
/// @brief Immediately persist the current wiper level to the chip's nonvolatile register
|
||||
/// (independent of the deferred nonvolatile mirroring / its stability delay)
|
||||
void store_nonvolatile();
|
||||
|
||||
protected:
|
||||
void write_state(float state) override;
|
||||
|
||||
@@ -3,30 +3,61 @@ mcp4461:
|
||||
i2c_id: i2c_bus
|
||||
|
||||
output:
|
||||
# All-terminals-off coverage lives here (folded from a former second channel-A
|
||||
# output — one output per channel keeps the reg_ state deterministic).
|
||||
- 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
|
||||
|
||||
- platform: mcp4461
|
||||
id: digipot_wiper_5
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: A
|
||||
terminal_a: false
|
||||
terminal_b: false
|
||||
terminal_w: false
|
||||
|
||||
- platform: mcp4461
|
||||
id: digipot_wiper_2
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: B
|
||||
nonvolatile: false
|
||||
|
||||
- platform: mcp4461
|
||||
id: digipot_wiper_3
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: C
|
||||
nonvolatile_write_delay: 5s
|
||||
initial_value: 0.5
|
||||
|
||||
# TCON1 coverage: terminal flags on a channel D output exercise the
|
||||
# calc_terminal_connector_byte_() write path for wipers 2/3.
|
||||
- platform: mcp4461
|
||||
id: digipot_wiper_4
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: D
|
||||
terminal_a: false
|
||||
terminal_w: false
|
||||
|
||||
# Bare NV-channel output — the pre-existing persistence workaround; must
|
||||
# keep validating without any nonvolatile key (regression: schema default
|
||||
# used to materialize the key on every channel and fail final validation).
|
||||
- platform: mcp4461
|
||||
id: digipot_nv_wiper_1
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: E
|
||||
|
||||
# Explicit opt-out on an NV channel is a harmless no-op and stays valid.
|
||||
- platform: mcp4461
|
||||
id: digipot_nv_wiper_2
|
||||
mcp4461_id: mcp4461_digipot_01
|
||||
channel: F
|
||||
nonvolatile: false
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Digipot test actions"
|
||||
on_press:
|
||||
- mcp4461.wiper.increase: digipot_wiper_1
|
||||
- mcp4461.wiper.decrease: digipot_wiper_1
|
||||
- mcp4461.wiper.store_nonvolatile: digipot_wiper_2
|
||||
- mcp4461.wiper.set_terminal:
|
||||
id: digipot_wiper_1
|
||||
terminal: a
|
||||
enable: false
|
||||
|
||||
Reference in New Issue
Block a user