[sy6970] Service the I2C watchdog so charge/LED settings persist (#19424)

Co-authored-by: Willem Oldemans <>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
willumpie82
2026-10-13 16:19:58 -10:00
committed by GitHub
co-authored by Willem Oldemans Claude Sonnet 5 J. Nick Koston J. Nick Koston
parent f45e6f0bf4
commit 7b591b26d6
4 changed files with 101 additions and 4 deletions
+14
View File
@@ -17,12 +17,22 @@ CONF_CHARGE_CURRENT = "charge_current"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_ENABLE_ADC = "enable_adc"
CONF_I2C_WATCHDOG_TIMEOUT = "i2c_watchdog_timeout"
sy6970_ns = cg.esphome_ns.namespace("sy6970")
SY6970Component = sy6970_ns.class_(
"SY6970Component", cg.PollingComponent, i2c.I2CDevice
)
SY6970Listener = sy6970_ns.class_("SY6970Listener")
I2CWatchdogTimeout = sy6970_ns.enum("I2CWatchdogTimeout")
# REG07[5:4]; the component kicks the watchdog itself, so the chip's 40S default can stay on
I2C_WATCHDOG_TIMEOUTS = {
"DISABLED": I2CWatchdogTimeout.I2C_WATCHDOG_DISABLED,
"40S": I2CWatchdogTimeout.I2C_WATCHDOG_40S,
"80S": I2CWatchdogTimeout.I2C_WATCHDOG_80S,
"160S": I2CWatchdogTimeout.I2C_WATCHDOG_160S,
}
CONFIG_SCHEMA = (
cv.Schema(
@@ -43,6 +53,9 @@ CONFIG_SCHEMA = (
),
cv.Optional(CONF_CHARGE_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_ENABLE_ADC, default=True): cv.boolean,
cv.Optional(CONF_I2C_WATCHDOG_TIMEOUT, default="40S"): cv.enum(
I2C_WATCHDOG_TIMEOUTS, upper=True
),
}
)
.extend(cv.polling_component_schema("5s"))
@@ -60,6 +73,7 @@ async def to_code(config: ConfigType) -> None:
config[CONF_PRECHARGE_CURRENT],
config[CONF_CHARGE_ENABLED],
config[CONF_ENABLE_ADC],
config[CONF_I2C_WATCHDOG_TIMEOUT],
)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
+68 -2
View File
@@ -6,6 +6,25 @@ namespace esphome::sy6970 {
ESPHOME_LOG_TAG(TAG, "sy6970");
static const LogString *i2c_watchdog_timeout_to_string(I2CWatchdogTimeout timeout) {
switch (timeout) {
case I2C_WATCHDOG_DISABLED:
return LOG_STR("Disabled");
case I2C_WATCHDOG_40S:
return LOG_STR("40s");
case I2C_WATCHDOG_80S:
return LOG_STR("80s");
case I2C_WATCHDOG_160S:
return LOG_STR("160s");
default:
return LOG_STR("Unknown");
}
}
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_ID = 0;
// Well under the 40 s default, so a failed kick or a timeout the chip did not accept still leaves margin
static constexpr uint32_t I2C_WATCHDOG_KICK_INTERVAL_MS = 10000;
bool SY6970Component::read_all_registers_() {
// Read all registers from 0x00 to 0x14 in one transaction (21 bytes)
// This includes unused registers 0x0F, 0x10 for performance
@@ -51,6 +70,11 @@ void SY6970Component::setup() {
ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x00)", chip_id);
}
// Service the watchdog first, so the settings below are not reverted while they are written
ESP_LOGV(TAG, "Setting I2C watchdog timeout to %s",
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
this->set_i2c_watchdog_timeout(this->i2c_watchdog_timeout_);
// Apply configuration options (all have defaults now)
ESP_LOGV(TAG, "Setting LED enabled to %s", ONOFF(this->led_enabled_));
this->set_led_enabled(this->led_enabled_);
@@ -85,9 +109,11 @@ void SY6970Component::dump_config() {
" Charge Current: %u mA\n"
" Precharge Current: %u mA\n"
" Charge Enabled: %s\n"
" ADC Enabled: %s",
" ADC Enabled: %s\n"
" I2C Watchdog Timeout: %s",
ONOFF(this->led_enabled_), this->input_current_limit_, this->charge_voltage_, this->charge_current_,
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_));
this->precharge_current_, ONOFF(this->charge_enabled_), ONOFF(this->enable_adc_),
LOG_STR_ARG(i2c_watchdog_timeout_to_string(this->i2c_watchdog_timeout_)));
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
if (this->is_failed()) {
@@ -198,4 +224,44 @@ void SY6970Component::set_enable_adc_measure(bool enabled) {
this->update_register_(SY6970_REG_ADC_CONTROL, 0xC0, enabled ? 0xC0 : 0x00);
}
void SY6970Component::set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout) {
if (this->is_failed())
return;
if (timeout != I2C_WATCHDOG_DISABLED) {
// Changing the timeout does not restart the chip's timer, which may be close to expiring
this->reset_i2c_watchdog();
}
// REG07 bits 5:4 (WATCHDOG[1:0])
bool written = this->update_register_(SY6970_REG_TIMER_CONTROL, 0x30, static_cast<uint8_t>(timeout) << 4);
if (!written) {
ESP_LOGW(TAG, "Failed to set I2C watchdog timeout to %s", LOG_STR_ARG(i2c_watchdog_timeout_to_string(timeout)));
}
if (timeout == I2C_WATCHDOG_DISABLED && written) {
this->i2c_watchdog_timeout_ = timeout;
this->cancel_interval(I2C_WATCHDOG_KICK_INTERVAL_ID);
return;
}
if (timeout != I2C_WATCHDOG_DISABLED) {
this->i2c_watchdog_timeout_ = timeout;
}
// Keeps kicking after a failed write too: the chip may still be on its 40 s default
this->set_interval(I2C_WATCHDOG_KICK_INTERVAL_ID, I2C_WATCHDOG_KICK_INTERVAL_MS,
[this]() { this->reset_i2c_watchdog(); });
}
bool SY6970Component::reset_i2c_watchdog() {
if (this->is_failed())
return false;
// WD_RST, self-clearing
if (!this->update_register_(SY6970_REG_SYS_CONTROL, 0x40, 0x40)) {
ESP_LOGW(TAG, "I2C watchdog reset failed, settings may revert");
return false;
}
return true;
}
} // namespace esphome::sy6970
+18 -2
View File
@@ -62,6 +62,15 @@ enum ChargeStatus {
CHARGE_STATUS_CHARGE_DONE = 3,
};
// REG07[5:4]. When the watchdog elapses the chip reverts CHG_CONFIG and STAT_DIS to their defaults,
// so an enabled watchdog is kicked on its own interval.
enum I2CWatchdogTimeout {
I2C_WATCHDOG_DISABLED = 0,
I2C_WATCHDOG_40S = 1,
I2C_WATCHDOG_80S = 2,
I2C_WATCHDOG_160S = 3,
};
// Structure to hold all register data read in one transaction
struct SY6970Data {
uint8_t registers[21]; // Registers 0x00-0x14 (includes unused 0x0F, 0x10)
@@ -76,14 +85,16 @@ class SY6970Listener {
class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
public:
SY6970Component(bool led_enabled, uint16_t input_current_limit, uint16_t charge_voltage, uint16_t charge_current,
uint16_t precharge_current, bool charge_enabled, bool enable_adc)
uint16_t precharge_current, bool charge_enabled, bool enable_adc,
I2CWatchdogTimeout i2c_watchdog_timeout)
: led_enabled_(led_enabled),
input_current_limit_(input_current_limit),
charge_voltage_(charge_voltage),
charge_current_(charge_current),
precharge_current_(precharge_current),
charge_enabled_(charge_enabled),
enable_adc_(enable_adc) {}
enable_adc_(enable_adc),
i2c_watchdog_timeout_(i2c_watchdog_timeout) {}
void setup() override;
void dump_config() override;
void update() override;
@@ -99,6 +110,10 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
void set_charge_enabled(bool enabled);
void set_led_enabled(bool enabled);
void set_enable_adc_measure(bool enabled = true);
void set_i2c_watchdog_timeout(I2CWatchdogTimeout timeout);
/// Kick the I2C watchdog (REG03 bit 6); runs on its own interval while enabled. Returns false when not written.
bool reset_i2c_watchdog();
protected:
bool read_all_registers_();
@@ -116,6 +131,7 @@ class SY6970Component final : public PollingComponent, public i2c::I2CDevice {
uint16_t precharge_current_;
bool charge_enabled_;
bool enable_adc_;
I2CWatchdogTimeout i2c_watchdog_timeout_;
};
} // namespace esphome::sy6970
+1
View File
@@ -9,6 +9,7 @@ sy6970:
precharge_current: 128
charge_enabled: true
enable_adc: true
i2c_watchdog_timeout: 80S
update_interval: 5s
sensor: