diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index e83a6847d6..abc74b9e6d 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -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(*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(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(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(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(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(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(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(terminal_connector) == 0 ? 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; @@ -471,6 +598,12 @@ void Mcp4461Component::enable_terminal_(Mcp4461WiperIdx wiper, char terminal) { return; } uint8_t wiper_idx = static_cast(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(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': diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h index a577a4b482..933d92c1fa 100644 --- a/esphome/components/mcp4461/mcp4461.h +++ b/esphome/components/mcp4461/mcp4461.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"); } diff --git a/esphome/components/mcp4461/output/__init__.py b/esphome/components/mcp4461/output/__init__.py index 0d145d81d3..1642f6149a 100644 --- a/esphome/components/mcp4461/output/__init__.py +++ b/esphome/components/mcp4461/output/__init__.py @@ -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] + ) diff --git a/esphome/components/mcp4461/output/automation.h b/esphome/components/mcp4461/output/automation.h new file mode 100644 index 0000000000..4be317b2f8 --- /dev/null +++ b/esphome/components/mcp4461/output/automation.h @@ -0,0 +1,56 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "mcp4461_output.h" + +namespace esphome::mcp4461 { + +template class WiperIncreaseAction : public Action { + public: + explicit WiperIncreaseAction(Mcp4461Wiper *wiper) : wiper_(wiper) {} + void play(Ts... x) override { this->wiper_->increase_wiper(); } + + protected: + Mcp4461Wiper *wiper_; +}; + +template class WiperDecreaseAction : public Action { + 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 class WiperStoreNonvolatileAction : public Action { + public: + explicit WiperStoreNonvolatileAction(Mcp4461Wiper *wiper) : wiper_(wiper) {} + void play(Ts... x) override { this->wiper_->store_nonvolatile(); } + + protected: + Mcp4461Wiper *wiper_; +}; + +template class WiperSetTerminalAction : public Action { + 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 diff --git a/esphome/components/mcp4461/output/mcp4461_output.cpp b/esphome/components/mcp4461/output/mcp4461_output.cpp index 3892372cab..5c373ddc7d 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.cpp +++ b/esphome/components/mcp4461/output/mcp4461_output.cpp @@ -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(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); } diff --git a/esphome/components/mcp4461/output/mcp4461_output.h b/esphome/components/mcp4461/output/mcp4461_output.h index 20d81d825a..c8d1ef1ec5 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.h +++ b/esphome/components/mcp4461/output/mcp4461_output.h @@ -36,6 +36,9 @@ class Mcp4461Wiper final : public output::FloatOutput, public Parented