mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[bq25186] Add bq25186 1-Cell, 1A Linear Battery Charger component (part 1) (#15700)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
88668f448e
commit
d08d324a83
@@ -101,6 +101,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bq25186/* @Rapsssito
|
||||
esphome/components/bq27220/* @rggammon
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, ID, Lambda
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@Rapsssito"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "bq25186"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_CONFIGURE_ON_BOOT = "configure_on_boot"
|
||||
CONF_BATTERY_REGULATION_VOLTAGE = "battery_regulation_voltage"
|
||||
CONF_FAST_CHARGE_CURRENT = "fast_charge_current"
|
||||
CONF_CHARGE_ENABLED = "charge_enabled"
|
||||
CONF_FLASH_CHARGING_MODE = "flash_charging_mode"
|
||||
CONF_PRECHARGE_IS_ITERM = "precharge_is_iterm"
|
||||
CONF_TERMINATION_PERCENT = "termination_percent"
|
||||
CONF_VINDPM_MODE = "vindpm_mode"
|
||||
CONF_THERMAL_REGULATION = "thermal_regulation"
|
||||
CONF_BATTERY_OCP_LIMIT = "battery_ocp_limit"
|
||||
CONF_BATTERY_UVLO = "battery_uvlo"
|
||||
CONF_MASK_CHARGE_STATUS_INTERRUPT = "mask_charge_status_interrupt"
|
||||
CONF_MASK_ILIM_INTERRUPT = "mask_ilim_interrupt"
|
||||
CONF_MASK_VINDPM_INTERRUPT = "mask_vindpm_interrupt"
|
||||
CONF_TS_AUTO_FUNCTION = "ts_auto_function"
|
||||
CONF_VLOWV = "vlowv"
|
||||
CONF_RECHARGE_THRESHOLD = "recharge_threshold"
|
||||
CONF_DOUBLE_TIMER_DURING_DPM = "double_timer_during_dpm"
|
||||
CONF_SAFETY_TIMER = "safety_timer"
|
||||
CONF_LONG_PRESS_DURATION = "long_press_duration"
|
||||
CONF_HW_RESET_REQUIRES_VIN = "hw_reset_requires_vin"
|
||||
CONF_AUTOWAKE = "autowake"
|
||||
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION = "push_button_long_press_action"
|
||||
CONF_WAKE1_TIMER = "wake1_timer"
|
||||
CONF_WAKE2_TIMER = "wake2_timer"
|
||||
CONF_ENABLE_PUSH_BUTTON = "enable_push_button"
|
||||
CONF_SYSTEM_REGULATION = "system_regulation"
|
||||
CONF_SYS_MODE = "sys_mode"
|
||||
CONF_DISABLE_VDPPM = "disable_vdppm"
|
||||
CONF_TS_HOT = "ts_hot"
|
||||
CONF_TS_COLD = "ts_cold"
|
||||
CONF_TS_WARM_DISABLE = "ts_warm_disable"
|
||||
CONF_TS_COOL_DISABLE = "ts_cool_disable"
|
||||
CONF_TS_ICHG = "ts_ichg"
|
||||
CONF_TS_VRCG = "ts_vrcg"
|
||||
CONF_MASK_TS_INTERRUPT = "mask_ts_interrupt"
|
||||
CONF_MASK_TREG_INTERRUPT = "mask_treg_interrupt"
|
||||
CONF_MASK_BAT_INTERRUPT = "mask_bat_interrupt"
|
||||
CONF_MASK_PG_INTERRUPT = "mask_pg_interrupt"
|
||||
|
||||
bq25186_ns = cg.esphome_ns.namespace("bq25186")
|
||||
BQ25186Component = bq25186_ns.class_("BQ25186Component", cg.Component, i2c.I2CDevice)
|
||||
|
||||
REG_VBAT_CTRL = 0x03
|
||||
REG_ICHG_CTRL = 0x04
|
||||
REG_CHARGECTRL0 = 0x05
|
||||
REG_CHARGECTRL1 = 0x06
|
||||
REG_IC_CTRL = 0x07
|
||||
REG_TMR_ILIM = 0x08
|
||||
REG_SHIP_RST = 0x09
|
||||
REG_SYS_REG = 0x0A
|
||||
REG_TS_CONTROL = 0x0B
|
||||
REG_MASK_ID = 0x0C
|
||||
REGISTER_COUNT = REG_MASK_ID - REG_VBAT_CTRL + 1
|
||||
WATCHDOG_DISABLED = 0x03
|
||||
|
||||
TERMINATION_PERCENT_OPTIONS = {"disabled": 0, "5%": 1, "10%": 2, "20%": 3}
|
||||
VINDPM_MODE_OPTIONS = {"battery_tracking": 0, "4.5v": 1, "4.7v": 2, "disabled": 3}
|
||||
THERM_REGULATION_OPTIONS = {"100c": 0, "80c": 1, "60c": 2, "disabled": 3}
|
||||
BATTERY_OCP_LIMIT_OPTIONS = {"500ma": 0, "1000ma": 1, "1500ma": 2, "3000ma": 3}
|
||||
BATTERY_UVLO_OPTIONS = {
|
||||
"3.0v": 0,
|
||||
"2.8v": 3,
|
||||
"2.6v": 4,
|
||||
"2.4v": 5,
|
||||
"2.2v": 6,
|
||||
"2.0v": 7,
|
||||
}
|
||||
VLOWV_OPTIONS = {"3.0v": 0, "2.8v": 1}
|
||||
RECHARGE_THRESHOLD_OPTIONS = {"100mv": 0, "200mv": 1}
|
||||
SAFETY_TIMER_OPTIONS = {"3h": 0, "6h": 1, "12h": 2, "disabled": 3}
|
||||
LONG_PRESS_DURATION_OPTIONS = {"5s": 0, "10s": 1, "15s": 2, "20s": 3}
|
||||
AUTOWAKE_OPTIONS = {"0.5s": 0, "1s": 1, "2s": 2, "4s": 3}
|
||||
INPUT_CURRENT_LIMIT_OPTIONS = {
|
||||
"50ma": 0,
|
||||
"100ma": 1,
|
||||
"200ma": 2,
|
||||
"300ma": 3,
|
||||
"400ma": 4,
|
||||
"500ma": 5,
|
||||
"665ma": 6,
|
||||
"1050ma": 7,
|
||||
}
|
||||
PUSH_BUTTON_LONG_PRESS_ACTION_OPTIONS = {
|
||||
"none": 0,
|
||||
"hardware_reset": 1,
|
||||
"ship_mode": 2,
|
||||
"shutdown_mode": 3,
|
||||
}
|
||||
WAKE1_TIMER_OPTIONS = {"300ms": 0, "1s": 1}
|
||||
WAKE2_TIMER_OPTIONS = {"2s": 0, "3s": 1}
|
||||
SYSTEM_REGULATION_OPTIONS = {
|
||||
"battery_tracking": 0,
|
||||
"4.4v": 1,
|
||||
"4.5v": 2,
|
||||
"4.6v": 3,
|
||||
"4.7v": 4,
|
||||
"4.8v": 5,
|
||||
"4.9v": 6,
|
||||
"pass_through": 7,
|
||||
}
|
||||
SYS_MODE_OPTIONS = {
|
||||
"normal": 0,
|
||||
"battery_only": 1,
|
||||
"floating_off": 2,
|
||||
"pulldown_off": 3,
|
||||
}
|
||||
TS_HOT_OPTIONS = {"60c": 0, "65c": 1, "50c": 2, "45c": 3}
|
||||
TS_COLD_OPTIONS = {"0c": 0, "3c": 1, "5c": 2, "-3c": 3}
|
||||
TS_ICHG_OPTIONS = {"0.5x": 0, "0.2x": 1}
|
||||
TS_VRCG_OPTIONS = {"-100mv": 0, "-200mv": 1}
|
||||
|
||||
|
||||
def _on_voltage_step(volts: float) -> float:
|
||||
millivolts = volts * 1000
|
||||
if abs(millivolts - round(millivolts / 10) * 10) > 1e-6:
|
||||
raise cv.Invalid("battery_regulation_voltage must be a multiple of 10 mV")
|
||||
return volts
|
||||
|
||||
|
||||
def _on_current_step(amps: float) -> float:
|
||||
milliamps = amps * 1000
|
||||
step = 1 if milliamps < 36 else 10
|
||||
if abs(milliamps - round(milliamps / step) * step) > 1e-6 or 35 < milliamps < 40:
|
||||
raise cv.Invalid(
|
||||
"fast_charge_current must be a whole number of mA up to 35mA, then a multiple of 10 mA"
|
||||
)
|
||||
return amps
|
||||
|
||||
|
||||
# (config key, register, bit shift, validator or enum options, default).
|
||||
# VBAT_CTRL bit 7 (pg_mode) and SYS_REG bit 4 (pg_gpo_level) stay 0; they are
|
||||
# driven at runtime, not from configuration.
|
||||
FIELDS: tuple[tuple[str, int, int, Any, Any], ...] = (
|
||||
(
|
||||
CONF_BATTERY_REGULATION_VOLTAGE,
|
||||
REG_VBAT_CTRL,
|
||||
0,
|
||||
cv.All(cv.voltage, cv.Range(min=3.5, max=4.65), _on_voltage_step),
|
||||
"4.2V",
|
||||
),
|
||||
(CONF_CHARGE_ENABLED, REG_ICHG_CTRL, 7, cv.boolean, True),
|
||||
(
|
||||
CONF_FAST_CHARGE_CURRENT,
|
||||
REG_ICHG_CTRL,
|
||||
0,
|
||||
cv.All(cv.current, cv.Range(min=0.005, max=1.0), _on_current_step),
|
||||
"10mA",
|
||||
),
|
||||
(CONF_FLASH_CHARGING_MODE, REG_CHARGECTRL0, 7, cv.boolean, False),
|
||||
(CONF_PRECHARGE_IS_ITERM, REG_CHARGECTRL0, 6, cv.boolean, False),
|
||||
(CONF_TERMINATION_PERCENT, REG_CHARGECTRL0, 4, TERMINATION_PERCENT_OPTIONS, "10%"),
|
||||
(CONF_VINDPM_MODE, REG_CHARGECTRL0, 2, VINDPM_MODE_OPTIONS, "4.5v"),
|
||||
(CONF_THERMAL_REGULATION, REG_CHARGECTRL0, 0, THERM_REGULATION_OPTIONS, "100c"),
|
||||
(CONF_BATTERY_OCP_LIMIT, REG_CHARGECTRL1, 6, BATTERY_OCP_LIMIT_OPTIONS, "3000ma"),
|
||||
(CONF_BATTERY_UVLO, REG_CHARGECTRL1, 3, BATTERY_UVLO_OPTIONS, "3.0v"),
|
||||
(CONF_MASK_CHARGE_STATUS_INTERRUPT, REG_CHARGECTRL1, 2, cv.boolean, True),
|
||||
(CONF_MASK_ILIM_INTERRUPT, REG_CHARGECTRL1, 1, cv.boolean, True),
|
||||
(CONF_MASK_VINDPM_INTERRUPT, REG_CHARGECTRL1, 0, cv.boolean, False),
|
||||
(CONF_TS_AUTO_FUNCTION, REG_IC_CTRL, 7, cv.boolean, True),
|
||||
(CONF_VLOWV, REG_IC_CTRL, 6, VLOWV_OPTIONS, "3.0v"),
|
||||
(CONF_RECHARGE_THRESHOLD, REG_IC_CTRL, 5, RECHARGE_THRESHOLD_OPTIONS, "100mv"),
|
||||
(CONF_DOUBLE_TIMER_DURING_DPM, REG_IC_CTRL, 4, cv.boolean, False),
|
||||
(CONF_SAFETY_TIMER, REG_IC_CTRL, 2, SAFETY_TIMER_OPTIONS, "6h"),
|
||||
(CONF_LONG_PRESS_DURATION, REG_TMR_ILIM, 6, LONG_PRESS_DURATION_OPTIONS, "10s"),
|
||||
(CONF_HW_RESET_REQUIRES_VIN, REG_TMR_ILIM, 5, cv.boolean, False),
|
||||
(CONF_AUTOWAKE, REG_TMR_ILIM, 3, AUTOWAKE_OPTIONS, "1s"),
|
||||
(CONF_INPUT_CURRENT_LIMIT, REG_TMR_ILIM, 0, INPUT_CURRENT_LIMIT_OPTIONS, "500ma"),
|
||||
(
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION,
|
||||
REG_SHIP_RST,
|
||||
3,
|
||||
PUSH_BUTTON_LONG_PRESS_ACTION_OPTIONS,
|
||||
"ship_mode",
|
||||
),
|
||||
(CONF_WAKE1_TIMER, REG_SHIP_RST, 2, WAKE1_TIMER_OPTIONS, "300ms"),
|
||||
(CONF_WAKE2_TIMER, REG_SHIP_RST, 1, WAKE2_TIMER_OPTIONS, "2s"),
|
||||
(CONF_ENABLE_PUSH_BUTTON, REG_SHIP_RST, 0, cv.boolean, True),
|
||||
(CONF_SYSTEM_REGULATION, REG_SYS_REG, 5, SYSTEM_REGULATION_OPTIONS, "4.5v"),
|
||||
(CONF_SYS_MODE, REG_SYS_REG, 2, SYS_MODE_OPTIONS, "normal"),
|
||||
(CONF_DISABLE_VDPPM, REG_SYS_REG, 0, cv.boolean, False),
|
||||
(CONF_TS_HOT, REG_TS_CONTROL, 6, TS_HOT_OPTIONS, "60c"),
|
||||
(CONF_TS_COLD, REG_TS_CONTROL, 4, TS_COLD_OPTIONS, "0c"),
|
||||
(CONF_TS_WARM_DISABLE, REG_TS_CONTROL, 3, cv.boolean, False),
|
||||
(CONF_TS_COOL_DISABLE, REG_TS_CONTROL, 2, cv.boolean, False),
|
||||
(CONF_TS_ICHG, REG_TS_CONTROL, 1, TS_ICHG_OPTIONS, "0.5x"),
|
||||
(CONF_TS_VRCG, REG_TS_CONTROL, 0, TS_VRCG_OPTIONS, "-100mv"),
|
||||
(CONF_MASK_TS_INTERRUPT, REG_MASK_ID, 7, cv.boolean, False),
|
||||
(CONF_MASK_TREG_INTERRUPT, REG_MASK_ID, 6, cv.boolean, True),
|
||||
(CONF_MASK_BAT_INTERRUPT, REG_MASK_ID, 5, cv.boolean, False),
|
||||
(CONF_MASK_PG_INTERRUPT, REG_MASK_ID, 4, cv.boolean, False),
|
||||
)
|
||||
|
||||
|
||||
def _battery_regulation_code(volts: float) -> int:
|
||||
"""VBAT_CTRL[6:0]: 3.5 V plus 10 mV per step."""
|
||||
return (round(volts * 1000) - 3500) // 10
|
||||
|
||||
|
||||
def _fast_charge_code(amps: float) -> int:
|
||||
"""ICHG_CTRL[6:0]: 1 mA steps from 5 mA to 35 mA, then 10 mA steps from 40 mA."""
|
||||
milliamps = round(amps * 1000)
|
||||
if milliamps <= 35:
|
||||
return milliamps - 5
|
||||
return milliamps // 10 + 27
|
||||
|
||||
|
||||
_ENCODERS: dict[str, Callable[[Any], int]] = {
|
||||
CONF_BATTERY_REGULATION_VOLTAGE: _battery_regulation_code,
|
||||
CONF_FAST_CHARGE_CURRENT: _fast_charge_code,
|
||||
CONF_CHARGE_ENABLED: lambda enabled: not enabled,
|
||||
}
|
||||
|
||||
|
||||
def pack_registers(config: ConfigType) -> list[int]:
|
||||
"""Pack the validated field values into the VBAT_CTRL..MASK_ID register bytes."""
|
||||
regs = [0] * REGISTER_COUNT
|
||||
# Watchdog disabled (IC_CTRL[1:0] = 3): nothing talks to the chip after boot yet, and a running
|
||||
# watchdog restores the power-on defaults after 160 s. Part 2 adds the option with the polling.
|
||||
regs[REG_IC_CTRL - REG_VBAT_CTRL] = WATCHDOG_DISABLED
|
||||
for key, register, shift, validator, _ in FIELDS:
|
||||
value = config[key]
|
||||
if isinstance(validator, dict):
|
||||
value = validator[value]
|
||||
elif (encode := _ENCODERS.get(key)) is not None:
|
||||
value = encode(value)
|
||||
regs[register - REG_VBAT_CTRL] |= int(value) << shift
|
||||
return regs
|
||||
|
||||
|
||||
def _field_schema(with_defaults: bool) -> cv.Schema:
|
||||
schema = {}
|
||||
for key, _, _, validator, default in FIELDS:
|
||||
if isinstance(validator, dict):
|
||||
validator = cv.enum(validator, lower=True)
|
||||
optional = (
|
||||
cv.Optional(key, default=default) if with_defaults else cv.Optional(key)
|
||||
)
|
||||
schema[optional] = validator
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
BQ25186_CONFIG_SCHEMA = _field_schema(with_defaults=True)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ25186Component),
|
||||
cv.Optional(CONF_CONFIGURE_ON_BOOT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(BQ25186_CONFIG_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x6A))
|
||||
)
|
||||
|
||||
CONFIGURE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BQ25186Component),
|
||||
}
|
||||
).extend(_field_schema(with_defaults=False))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if config[CONF_CONFIGURE_ON_BOOT]:
|
||||
cg.add(var.set_setup_registers(cg.ArrayInitializer(*pack_registers(config))))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bq25186.apply_configuration",
|
||||
automation.LambdaAction,
|
||||
CONFIGURE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def bq25186_configure_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> cg.MockObj:
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# Options the action leaves out fall back to the component's configuration
|
||||
path = CORE.config.get_path_for_id(config[CONF_ID])[:-1]
|
||||
parent_config = CORE.config.get_config_for_path(path)
|
||||
regs = pack_registers({**parent_config, **config})
|
||||
|
||||
text = str(cg.statement(parent.apply_config(cg.ArrayInitializer(*regs))))
|
||||
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
|
||||
return automation.new_lambda_pvariable(
|
||||
action_id, lambda_, automation.StatelessLambdaAction, template_arg
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "bq25186.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq25186");
|
||||
|
||||
// Bits the configuration owns in each register; the rest are runtime state (VBAT_CTRL pg_mode,
|
||||
// SYS_REG GPO level), reset triggers (SHIP_RST) or the device id (MASK_ID) and are kept as read
|
||||
static const uint8_t BQ25186_CONFIG_MASKS[BQ25186_CONFIG_REGISTER_COUNT] PROGMEM = {
|
||||
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0xEF, 0xFF, 0xF0,
|
||||
};
|
||||
|
||||
bool BQ25186Component::write_register_(uint8_t reg, uint8_t value) {
|
||||
if (!this->write_byte(reg, value)) {
|
||||
ESP_LOGW(TAG, "Failed to write register 0x%02X", reg);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BQ25186Component::update_register_(uint8_t reg, uint8_t mask, uint8_t value) {
|
||||
uint8_t reg_value;
|
||||
if (!this->read_byte(reg, ®_value)) {
|
||||
ESP_LOGW(TAG, "Failed to read register 0x%02X", reg);
|
||||
return false;
|
||||
}
|
||||
reg_value = (reg_value & ~mask) | (value & mask);
|
||||
return this->write_register_(reg, reg_value);
|
||||
}
|
||||
|
||||
bool BQ25186Component::apply_config(const BQ25186ConfigRegisters ®s) {
|
||||
// Setup rejects an unknown chip; the action must not program it before that check or after it failed
|
||||
if (!this->is_ready() || this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Not applying configuration, setup has not succeeded");
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < regs.size(); i++) {
|
||||
const uint8_t reg = BQ25186_REG_VBAT_CTRL + i;
|
||||
const uint8_t mask = progmem_read_byte(&BQ25186_CONFIG_MASKS[i]);
|
||||
const bool ok = mask == 0xFF ? this->write_register_(reg, regs[i]) : this->update_register_(reg, mask, regs[i]);
|
||||
if (!ok) {
|
||||
this->status_set_warning(LOG_STR("Configuration partly applied"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BQ25186Component::setup() {
|
||||
uint8_t mask_id;
|
||||
if (!this->read_byte(BQ25186_REG_MASK_ID, &mask_id)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Another charger at this address could lay its registers out differently, so do not write into it
|
||||
const uint8_t device_id = mask_id & 0x0F;
|
||||
if (device_id != 0x01) {
|
||||
ESP_LOGE(TAG, "Unexpected device id 0x%X (expected 0x1)", device_id);
|
||||
this->mark_failed(LOG_STR("Not a BQ25186"));
|
||||
return;
|
||||
}
|
||||
|
||||
// A failed write only sets the warning status: the chip has identified itself, so a later
|
||||
// apply_configuration may retry
|
||||
if (this->configure_on_boot_)
|
||||
this->apply_config(this->setup_regs_);
|
||||
}
|
||||
|
||||
void BQ25186Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ25186:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
static constexpr uint8_t BQ25186_REG_VBAT_CTRL = 0x03;
|
||||
static constexpr uint8_t BQ25186_REG_ICHG_CTRL = 0x04;
|
||||
static constexpr uint8_t BQ25186_REG_CHARGECTRL0 = 0x05;
|
||||
static constexpr uint8_t BQ25186_REG_CHARGECTRL1 = 0x06;
|
||||
static constexpr uint8_t BQ25186_REG_IC_CTRL = 0x07;
|
||||
static constexpr uint8_t BQ25186_REG_TMR_ILIM = 0x08;
|
||||
static constexpr uint8_t BQ25186_REG_SHIP_RST = 0x09;
|
||||
static constexpr uint8_t BQ25186_REG_SYS_REG = 0x0A;
|
||||
static constexpr uint8_t BQ25186_REG_TS_CONTROL = 0x0B;
|
||||
static constexpr uint8_t BQ25186_REG_MASK_ID = 0x0C;
|
||||
static constexpr size_t BQ25186_CONFIG_REGISTER_COUNT = BQ25186_REG_MASK_ID - BQ25186_REG_VBAT_CTRL + 1;
|
||||
|
||||
// Register values for VBAT_CTRL..MASK_ID, packed by code generation
|
||||
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
|
||||
|
||||
class BQ25186Component : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_setup_registers(const BQ25186ConfigRegisters ®s) {
|
||||
this->setup_regs_ = regs;
|
||||
this->configure_on_boot_ = true;
|
||||
}
|
||||
bool apply_config(const BQ25186ConfigRegisters ®s);
|
||||
|
||||
protected:
|
||||
bool write_register_(uint8_t reg, uint8_t value);
|
||||
bool update_register_(uint8_t reg, uint8_t mask, uint8_t value);
|
||||
|
||||
BQ25186ConfigRegisters setup_regs_{};
|
||||
bool configure_on_boot_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Tests for the bq25186 register packer."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.bq25186 import (
|
||||
BQ25186_CONFIG_SCHEMA,
|
||||
CONF_BATTERY_REGULATION_VOLTAGE,
|
||||
CONF_CHARGE_ENABLED,
|
||||
CONF_FAST_CHARGE_CURRENT,
|
||||
CONF_MASK_TS_INTERRUPT,
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION,
|
||||
REG_ICHG_CTRL,
|
||||
REG_MASK_ID,
|
||||
REG_SHIP_RST,
|
||||
REG_VBAT_CTRL,
|
||||
pack_registers,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Datasheet power-on values for VBAT_CTRL..MASK_ID, except that IC_CTRL has the
|
||||
# watchdog disabled (0x87 instead of 0x84) and MASK_ID carries only its
|
||||
# configurable bits 7:4 (0x40 instead of 0x41 with the device id).
|
||||
DEFAULT_REGISTERS = [0x46, 0x05, 0x24, 0xC6, 0x87, 0x4D, 0x11, 0x40, 0x00, 0x40]
|
||||
|
||||
|
||||
def _packed(reg: int, config: ConfigType | None = None) -> int:
|
||||
regs = pack_registers(BQ25186_CONFIG_SCHEMA(config or {}))
|
||||
return regs[reg - REG_VBAT_CTRL]
|
||||
|
||||
|
||||
def test_defaults_match_power_on_values() -> None:
|
||||
assert pack_registers(BQ25186_CONFIG_SCHEMA({})) == DEFAULT_REGISTERS
|
||||
|
||||
|
||||
def test_battery_regulation_voltage_code() -> None:
|
||||
assert _packed(REG_VBAT_CTRL, {CONF_BATTERY_REGULATION_VOLTAGE: "4.1V"}) == 0x3C
|
||||
assert _packed(REG_VBAT_CTRL, {CONF_BATTERY_REGULATION_VOLTAGE: "3.5V"}) == 0x00
|
||||
assert _packed(REG_VBAT_CTRL, {CONF_BATTERY_REGULATION_VOLTAGE: "4.65V"}) == 0x73
|
||||
|
||||
|
||||
def test_fast_charge_current_code() -> None:
|
||||
# 1 mA steps from 5 mA to 35 mA
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "5mA"}) == 0x00
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "20mA"}) == 0x0F
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "35mA"}) == 0x1E
|
||||
# 10 mA steps from 40 mA
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "40mA"}) == 0x1F
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "200mA"}) == 0x2F
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_FAST_CHARGE_CURRENT: "1A"}) == 0x7F
|
||||
|
||||
|
||||
def test_charge_disabled_sets_bit_7() -> None:
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_CHARGE_ENABLED: False}) == 0x85
|
||||
assert _packed(REG_ICHG_CTRL, {CONF_CHARGE_ENABLED: True}) == 0x05
|
||||
|
||||
|
||||
def test_masked_registers_only_carry_configurable_bits() -> None:
|
||||
config: ConfigType = {
|
||||
CONF_PUSH_BUTTON_LONG_PRESS_ACTION: "shutdown_mode",
|
||||
CONF_MASK_TS_INTERRUPT: True,
|
||||
}
|
||||
# SHIP_RST bits 7:5 are reset triggers
|
||||
ship_rst = _packed(REG_SHIP_RST, config)
|
||||
assert ship_rst == 0x19
|
||||
assert ship_rst & ~0x1F == 0
|
||||
# MASK_ID bits 3:0 are the device id
|
||||
mask_id = _packed(REG_MASK_ID, config)
|
||||
assert mask_id == 0xC0
|
||||
assert mask_id & ~0xF0 == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key, value",
|
||||
[
|
||||
(CONF_BATTERY_REGULATION_VOLTAGE, "4.105V"),
|
||||
(CONF_FAST_CHARGE_CURRENT, "36mA"),
|
||||
(CONF_FAST_CHARGE_CURRENT, "39mA"),
|
||||
(CONF_FAST_CHARGE_CURRENT, "49mA"),
|
||||
(CONF_FAST_CHARGE_CURRENT, "5.5mA"),
|
||||
(CONF_FAST_CHARGE_CURRENT, "34.6mA"),
|
||||
(CONF_BATTERY_REGULATION_VOLTAGE, "4.2005V"),
|
||||
],
|
||||
)
|
||||
def test_values_between_steps_are_rejected(key: str, value: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
BQ25186_CONFIG_SCHEMA({key: value})
|
||||
@@ -0,0 +1,25 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
# Just the defined configuration
|
||||
- bq25186.apply_configuration: bq25186_charger
|
||||
# Just the defined configuration with some overrides
|
||||
- bq25186.apply_configuration:
|
||||
id: bq25186_charger
|
||||
battery_regulation_voltage: 4.1V
|
||||
fast_charge_current: 200mA
|
||||
|
||||
bq25186:
|
||||
- id: bq25186_charger
|
||||
configure_on_boot: false
|
||||
fast_charge_current: 150mA
|
||||
battery_regulation_voltage: 4.2V
|
||||
input_current_limit: 500ma
|
||||
system_regulation: 4.5v
|
||||
charge_enabled: true
|
||||
# Second charger with the boot-time write enabled
|
||||
- id: bq25186_boot
|
||||
address: 0x6B
|
||||
configure_on_boot: true
|
||||
fast_charge_current: 20mA
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
bq25186: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
bq25186: !include common.yaml
|
||||
Reference in New Issue
Block a user