mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 17:45:57 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a876276d4c | ||
|
|
0c42290b02 | ||
|
|
373224c141 |
No files matched your search
@@ -467,6 +467,7 @@ esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/npm1300/* @tomaszduda23
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/ns2009/* @tchilov
|
||||
esphome/components/ns2009/touchscreen/* @tchilov
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_CHARGE_CURRENT, CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID, CONF_MODE, CONF_VOLTAGE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@tomaszduda23"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "npm1300"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_NPM1300_ID = "npm1300_id"
|
||||
CONF_VBUS_CURRENT_LIMIT = "vbus_current_limit"
|
||||
CONF_THERMISTOR_RESISTANCE = "thermistor_resistance"
|
||||
CONF_THERMISTOR_BETA = "thermistor_beta"
|
||||
CONF_CHARGER = "charger"
|
||||
CONF_TERMINATION_VOLTAGE = "termination_voltage"
|
||||
CONF_WARM_TERMINATION_VOLTAGE = "warm_termination_voltage"
|
||||
CONF_TERMINATION_CURRENT = "termination_current"
|
||||
CONF_TRICKLE_VOLTAGE = "trickle_voltage"
|
||||
CONF_DISCHARGE_CURRENT_LIMIT = "discharge_current_limit"
|
||||
CONF_RECHARGE = "recharge"
|
||||
CONF_BUCK1 = "buck1"
|
||||
CONF_BUCK2 = "buck2"
|
||||
CONF_LDO1 = "ldo1"
|
||||
CONF_LDO2 = "ldo2"
|
||||
|
||||
npm1300_ns = cg.esphome_ns.namespace("npm1300")
|
||||
NPM1300Component = npm1300_ns.class_(
|
||||
"NPM1300Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
NPM1300Output = npm1300_ns.enum("NPM1300Output", is_class=True)
|
||||
NPM1300OutputConfig = npm1300_ns.struct("NPM1300OutputConfig")
|
||||
NPM1300ChargerConfig = npm1300_ns.struct("NPM1300ChargerConfig")
|
||||
NPM1300Config = npm1300_ns.struct("NPM1300Config")
|
||||
|
||||
NPM1300_CHILD_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(CONF_NPM1300_ID): cv.use_id(NPM1300Component)}
|
||||
)
|
||||
|
||||
# Status registers read on each update: VBUSINSTATUS, BCHGCHARGESTATUS and BCHGERRREASON
|
||||
STATUS_PUBLISHER = cg.HubFunction(
|
||||
"status",
|
||||
cg.void,
|
||||
[
|
||||
(cg.uint8, "vbus_status"),
|
||||
(cg.uint8, "charge_status"),
|
||||
(cg.uint8, "error_reason"),
|
||||
],
|
||||
define="USE_NPM1300_STATUS_PUBLISHER",
|
||||
)
|
||||
VBUS_STATUS = STATUS_PUBLISHER.param("vbus_status")
|
||||
CHARGE_STATUS = STATUS_PUBLISHER.param("charge_status")
|
||||
ERROR_REASON = STATUS_PUBLISHER.param("error_reason")
|
||||
# The ADC results and the amperes per code of the battery current
|
||||
DATA_PUBLISHER = cg.HubFunction(
|
||||
"data",
|
||||
cg.void,
|
||||
[(cg.uint8.operator("const").operator("ptr"), "adc"), (cg.float_, "ibat_scale")],
|
||||
define="USE_NPM1300_DATA_PUBLISHER",
|
||||
)
|
||||
ADC = DATA_PUBLISHER.param("adc")
|
||||
IBAT_SCALE = DATA_PUBLISHER.param("ibat_scale")
|
||||
# The dump lines of the children decoded by the two functions above
|
||||
DUMP = cg.HubFunction("dump", cg.void, [(cg.const_char_ptr, "tag")])
|
||||
DUMP_TAG = DUMP.param("tag")
|
||||
|
||||
# Config key of each regulator and its NPM1300Output enumerator
|
||||
OUTPUTS = NPM1300Output.options(
|
||||
CONF_BUCK1, CONF_BUCK2, CONF_LDO1, CONF_LDO2, prefix="NPM1300_OUTPUT_"
|
||||
)
|
||||
|
||||
# Written for a field the YAML leaves out
|
||||
NPM1300_UNSET = 0xFF
|
||||
# ADCNTCRSEL codes, by kiloohms
|
||||
THERMISTOR_RESISTANCES = {10: 1, 47: 2, 100: 3}
|
||||
LDO_MODES = {"load_switch": 0, "ldo": 1}
|
||||
TERMINATION_CURRENTS = {"10%": 0, "20%": 1}
|
||||
# BCHGVTRICKLESEL, by millivolts
|
||||
TRICKLE_VOLTAGES = {2900: 0, 2500: 1}
|
||||
# BCHGISETDISCHARGE codes, by milliamperes
|
||||
DISCHARGE_CURRENT_LIMITS = {200: 84, 1000: 415}
|
||||
# BCHGVTERM: 3.50 V to 3.65 V, then 4.00 V to 4.45 V, both in 50 mV steps
|
||||
TERMINATION_VOLTAGES = [3500 + 50 * i for i in range(4)] + [
|
||||
4000 + 50 * i for i in range(10)
|
||||
]
|
||||
# The wake-up timer counts 16 ms ticks in a 24-bit register
|
||||
HIBERNATE_TICK_MS = 16
|
||||
HIBERNATE_MAX_TICKS = 0xFFFFFF
|
||||
|
||||
|
||||
def _millis(value: float) -> int:
|
||||
return round(value * 1000)
|
||||
|
||||
|
||||
def _one_of_units(
|
||||
validator: Callable[[object], float],
|
||||
allowed: list[int],
|
||||
unit: str,
|
||||
scale: float = 1000,
|
||||
) -> Callable[[object], float]:
|
||||
"""Validate a quantity with `validator` and accept only the listed values, given in units * `scale`."""
|
||||
|
||||
def validate(value: object) -> float:
|
||||
value = validator(value)
|
||||
if round(value * scale) not in allowed:
|
||||
options = ", ".join(f"{v / scale:g}{unit}" for v in allowed)
|
||||
raise cv.Invalid(f"Must be one of {options}")
|
||||
return value
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def _stepped(
|
||||
validator: Callable[[object], float],
|
||||
minimum: int,
|
||||
maximum: int,
|
||||
step: int,
|
||||
unit: str,
|
||||
) -> Callable[[object], float]:
|
||||
"""Validate a quantity in a range of milli-units, in steps of `step`."""
|
||||
allowed = list(range(minimum, maximum + 1, step))
|
||||
in_range = cv.All(validator, cv.Range(min=minimum / 1000, max=maximum / 1000))
|
||||
|
||||
def validate(value: object) -> float:
|
||||
value = in_range(value)
|
||||
if _millis(value) not in allowed:
|
||||
raise cv.Invalid(f"Must be a multiple of {step}m{unit}")
|
||||
return value
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
OUTPUT_VOLTAGE = _stepped(cv.voltage, 1000, 3300, 100, "V")
|
||||
VBUS_CURRENT_LIMIT = _stepped(cv.current, 100, 1500, 100, "A")
|
||||
|
||||
BUCK_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_VOLTAGE): OUTPUT_VOLTAGE,
|
||||
cv.Optional(CONF_ENABLED): cv.boolean,
|
||||
}
|
||||
)
|
||||
LDO_SCHEMA = BUCK_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE): cv.enum(LDO_MODES, lower=True),
|
||||
}
|
||||
)
|
||||
|
||||
CHARGER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CHARGE_CURRENT): _stepped(cv.current, 32, 800, 2, "A"),
|
||||
cv.Optional(CONF_TERMINATION_VOLTAGE, default="4.2V"): _one_of_units(
|
||||
cv.voltage, TERMINATION_VOLTAGES, "V"
|
||||
),
|
||||
cv.Optional(CONF_WARM_TERMINATION_VOLTAGE): _one_of_units(
|
||||
cv.voltage, TERMINATION_VOLTAGES, "V"
|
||||
),
|
||||
cv.Optional(CONF_TERMINATION_CURRENT, default="10%"): cv.enum(
|
||||
TERMINATION_CURRENTS
|
||||
),
|
||||
cv.Optional(CONF_TRICKLE_VOLTAGE, default="2.9V"): _one_of_units(
|
||||
cv.voltage, list(TRICKLE_VOLTAGES), "V"
|
||||
),
|
||||
cv.Optional(CONF_DISCHARGE_CURRENT_LIMIT, default="1000mA"): _one_of_units(
|
||||
cv.current, list(DISCHARGE_CURRENT_LIMITS), "A"
|
||||
),
|
||||
cv.Optional(CONF_RECHARGE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(NPM1300Component),
|
||||
cv.Optional(CONF_VBUS_CURRENT_LIMIT): VBUS_CURRENT_LIMIT,
|
||||
cv.Optional(CONF_THERMISTOR_RESISTANCE): _one_of_units(
|
||||
cv.resistance, list(THERMISTOR_RESISTANCES), "kOhm", scale=0.001
|
||||
),
|
||||
cv.Optional(CONF_THERMISTOR_BETA, default=3380): cv.int_range(
|
||||
min=1, max=65535
|
||||
),
|
||||
cv.Optional(CONF_CHARGER): CHARGER_SCHEMA,
|
||||
cv.Optional(CONF_BUCK1): BUCK_SCHEMA,
|
||||
cv.Optional(CONF_BUCK2): BUCK_SCHEMA,
|
||||
cv.Optional(CONF_LDO1): LDO_SCHEMA,
|
||||
cv.Optional(CONF_LDO2): LDO_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6B))
|
||||
)
|
||||
|
||||
|
||||
def _termination_voltage_code(volts: float) -> int:
|
||||
return TERMINATION_VOLTAGES.index(_millis(volts))
|
||||
|
||||
|
||||
def _vbus_current_limit_code(amps: float) -> int:
|
||||
"""VBUSINILIM0: 100 mA per step of the code."""
|
||||
return _millis(amps) // 100
|
||||
|
||||
|
||||
def _charger_config(config: ConfigType) -> cg.StructInitializer:
|
||||
termination = _termination_voltage_code(config[CONF_TERMINATION_VOLTAGE])
|
||||
warm = config.get(CONF_WARM_TERMINATION_VOLTAGE)
|
||||
return cg.StructInitializer(
|
||||
NPM1300ChargerConfig,
|
||||
("current", _millis(config[CONF_CHARGE_CURRENT]) // 2),
|
||||
(
|
||||
"discharge_limit",
|
||||
DISCHARGE_CURRENT_LIMITS[_millis(config[CONF_DISCHARGE_CURRENT_LIMIT])],
|
||||
),
|
||||
("termination_voltage", termination),
|
||||
(
|
||||
"warm_termination_voltage",
|
||||
termination if warm is None else _termination_voltage_code(warm),
|
||||
),
|
||||
("trickle_voltage", TRICKLE_VOLTAGES[_millis(config[CONF_TRICKLE_VOLTAGE])]),
|
||||
("termination_current", TERMINATION_CURRENTS[config[CONF_TERMINATION_CURRENT]]),
|
||||
("recharge", config[CONF_RECHARGE]),
|
||||
("enabled", config[CONF_ENABLED]),
|
||||
)
|
||||
|
||||
|
||||
def _output_config(config: ConfigType | None) -> cg.StructInitializer:
|
||||
config = config or {}
|
||||
voltage = config.get(CONF_VOLTAGE)
|
||||
mode = config.get(CONF_MODE)
|
||||
enabled = config.get(CONF_ENABLED)
|
||||
return cg.StructInitializer(
|
||||
NPM1300OutputConfig,
|
||||
(
|
||||
"voltage",
|
||||
NPM1300_UNSET if voltage is None else (_millis(voltage) - 1000) // 100,
|
||||
),
|
||||
("ldo_mode", NPM1300_UNSET if mode is None else LDO_MODES[mode]),
|
||||
("enabled", NPM1300_UNSET if enabled is None else int(enabled)),
|
||||
)
|
||||
|
||||
|
||||
def _config(config: ConfigType) -> cg.StructInitializer:
|
||||
charger = config.get(CONF_CHARGER)
|
||||
resistance = config.get(CONF_THERMISTOR_RESISTANCE)
|
||||
return cg.StructInitializer(
|
||||
NPM1300Config,
|
||||
(
|
||||
"outputs",
|
||||
cg.ArrayInitializer(*(_output_config(config.get(key)) for key in OUTPUTS)),
|
||||
),
|
||||
("charger", None if charger is None else _charger_config(charger)),
|
||||
("thermistor_beta", config[CONF_THERMISTOR_BETA]),
|
||||
(
|
||||
"thermistor",
|
||||
NPM1300_UNSET
|
||||
if resistance is None
|
||||
else THERMISTOR_RESISTANCES[round(resistance / 1000)],
|
||||
),
|
||||
("charger_configured", charger is not None),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# In flash: only setup() and dump_config() read it
|
||||
table = cg.shared_progmem_array(
|
||||
"npm1300_config", NPM1300Config, cg.ArrayInitializer(_config(config))
|
||||
)
|
||||
var = await i2c.new_i2c_component(config, table)
|
||||
if (limit := config.get(CONF_VBUS_CURRENT_LIMIT)) is not None:
|
||||
cg.add(var.set_vbus_current_limit(_vbus_current_limit_code(limit)))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"npm1300.enter_ship_mode",
|
||||
automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(NPM1300Component)}),
|
||||
automation.ApplyCall("enter_ship_mode()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"npm1300.hibernate",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(NPM1300Component),
|
||||
cv.Required(CONF_DURATION): cv.templatable(
|
||||
cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(
|
||||
min=cv.TimePeriod(milliseconds=HIBERNATE_TICK_MS),
|
||||
max=cv.TimePeriod(
|
||||
milliseconds=HIBERNATE_TICK_MS * HIBERNATE_MAX_TICKS
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_DURATION, "hibernate", cg.uint32),
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_PROBLEM,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
from esphome.cpp_generator import HexIntLiteral
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CHARGE_STATUS,
|
||||
CONF_NPM1300_ID,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
ERROR_REASON,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
STATUS_PUBLISHER,
|
||||
VBUS_STATUS,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class _StatusBits:
|
||||
"""On when any bit of mask is set in the status register."""
|
||||
|
||||
key: str
|
||||
register: cg.MockObj
|
||||
mask: int
|
||||
name: str
|
||||
device_class: str = cv.UNDEFINED
|
||||
entity_category: str = cv.UNDEFINED
|
||||
|
||||
|
||||
_FIELDS = (
|
||||
_StatusBits(
|
||||
key="vbus_present",
|
||||
register=VBUS_STATUS,
|
||||
mask=0x01,
|
||||
name="VBUS Present",
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
),
|
||||
_StatusBits(
|
||||
key="vbus_current_limit_active",
|
||||
register=VBUS_STATUS,
|
||||
mask=0x02,
|
||||
name="VBUS Current Limit Active",
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
# Trickle, constant current or constant voltage charging
|
||||
_StatusBits(
|
||||
key="charging",
|
||||
register=CHARGE_STATUS,
|
||||
mask=0x1C,
|
||||
name="Charging",
|
||||
device_class=DEVICE_CLASS_BATTERY_CHARGING,
|
||||
),
|
||||
_StatusBits(
|
||||
key="charge_complete",
|
||||
register=CHARGE_STATUS,
|
||||
mask=0x02,
|
||||
name="Charge Complete",
|
||||
),
|
||||
_StatusBits(
|
||||
key="charger_error",
|
||||
register=ERROR_REASON,
|
||||
mask=0xFF,
|
||||
name="Charger Error",
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(field.key): binary_sensor.binary_sensor_schema(
|
||||
device_class=field.device_class, entity_category=field.entity_category
|
||||
)
|
||||
for field in _FIELDS
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
sensors = binary_sensor.sub_binary_sensors(config)
|
||||
for field in _FIELDS:
|
||||
if (var := await sensors(field.key, None)) is not None:
|
||||
state = (field.register & HexIntLiteral(field.mask)) != 0
|
||||
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
|
||||
DUMP.add(
|
||||
hub,
|
||||
binary_sensor.binary_sensor_ns.log_binary_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression(f'LOG_STR_LITERAL("{field.name}")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,483 @@
|
||||
#include "npm1300.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/core/progmem.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::npm1300 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "npm1300");
|
||||
|
||||
// Registers: the base address of the peripheral in the high byte, the offset in the low byte
|
||||
static constexpr uint16_t REG_VBUSIN_TASK_UPDATE_ILIM = 0x0200;
|
||||
static constexpr uint16_t REG_VBUSIN_ILIM0 = 0x0201;
|
||||
static constexpr uint16_t REG_VBUSIN_ILIM_STARTUP = 0x0202;
|
||||
static constexpr uint16_t REG_VBUSIN_STATUS = 0x0207;
|
||||
static constexpr uint16_t REG_BCHG_TASK_RELEASE_ERR = 0x0300;
|
||||
static constexpr uint16_t REG_BCHG_TASK_CLEAR_CHG_ERR = 0x0301;
|
||||
static constexpr uint16_t REG_BCHG_ENABLE_SET = 0x0304;
|
||||
static constexpr uint16_t REG_BCHG_ENABLE_CLR = 0x0305;
|
||||
static constexpr uint16_t REG_BCHG_DISABLE_SET = 0x0306;
|
||||
static constexpr uint16_t REG_BCHG_DISABLE_CLR = 0x0307;
|
||||
static constexpr uint16_t REG_BCHG_ISET_MSB = 0x0308;
|
||||
static constexpr uint16_t REG_BCHG_ISET_DISCHARGE_MSB = 0x030A;
|
||||
static constexpr uint16_t REG_BCHG_VTERM = 0x030C;
|
||||
static constexpr uint16_t REG_BCHG_VTERM_R = 0x030D;
|
||||
static constexpr uint16_t REG_BCHG_VTRICKLE_SEL = 0x030E;
|
||||
static constexpr uint16_t REG_BCHG_ITERM_SEL = 0x030F;
|
||||
static constexpr uint16_t REG_BCHG_CHARGE_STATUS = 0x0334;
|
||||
static constexpr uint16_t REG_BCHG_ERR_REASON = 0x0336;
|
||||
static constexpr uint16_t REG_BUCK1_ENA_SET = 0x0400;
|
||||
static constexpr uint16_t REG_BUCK1_NORM_VOUT = 0x0408;
|
||||
static constexpr uint16_t REG_BUCK_SW_CTRL_SEL = 0x040F;
|
||||
static constexpr uint16_t REG_BUCK_STATUS = 0x0434;
|
||||
static constexpr uint16_t REG_ADC_TASK_VBAT = 0x0500;
|
||||
static constexpr uint16_t REG_ADC_TASK_VBUS = 0x0507;
|
||||
static constexpr uint16_t REG_ADC_NTC_R_SEL = 0x050A;
|
||||
static constexpr uint16_t REG_ADC_TASK_AUTO_TIM_UPDATE = 0x050C;
|
||||
static constexpr uint16_t REG_ADC_RESULTS = 0x0510;
|
||||
static constexpr uint16_t REG_ADC_IBAT_MEAS_EN = 0x0524;
|
||||
static constexpr uint16_t REG_TIMER_TARGET_STROBE = 0x0703;
|
||||
static constexpr uint16_t REG_TIMER_CONFIG = 0x0705;
|
||||
static constexpr uint16_t REG_TIMER_HI_BYTE = 0x0708;
|
||||
static constexpr uint16_t REG_LDSW1_TASK_SET = 0x0800;
|
||||
static constexpr uint16_t REG_LDSW_STATUS = 0x0804;
|
||||
static constexpr uint16_t REG_LDSW1_LDO_SEL = 0x0808;
|
||||
static constexpr uint16_t REG_LDSW1_VOUT_SEL = 0x080C;
|
||||
static constexpr uint16_t REG_SHIP_TASK_HIBERNATE = 0x0B00;
|
||||
static constexpr uint16_t REG_SHIP_TASK_SHIP_MODE = 0x0B02;
|
||||
|
||||
// VBUSINSTATUS
|
||||
static constexpr uint8_t VBUS_PRESENT = 0x01;
|
||||
// BCHGCHARGESTATUS; bit 0 (battery detected) is reserved since C00 silicon
|
||||
static constexpr uint8_t CHARGE_STATUS_COMPLETED = 0x02;
|
||||
static constexpr uint8_t CHARGE_STATUS_TRICKLE = 0x04;
|
||||
static constexpr uint8_t CHARGE_STATUS_CONSTANT_CURRENT = 0x08;
|
||||
static constexpr uint8_t CHARGE_STATUS_CONSTANT_VOLTAGE = 0x10;
|
||||
static constexpr uint8_t CHARGE_STATUS_DIE_TEMP_HIGH = 0x40;
|
||||
// BCHGDISABLESET and BCHGDISABLECLR
|
||||
static constexpr uint8_t CHARGER_DISABLE_RECHARGE = 0x01;
|
||||
static constexpr uint8_t CHARGER_DISABLE_NTC = 0x02;
|
||||
// BUCKSTATUS power good, LDSWSTATUS powered up as load switch or as LDO
|
||||
static constexpr uint8_t BUCK_POWER_GOOD[2] = {0x04, 0x40};
|
||||
static constexpr uint8_t LDSW_POWERED[2] = {0x03, 0x0C};
|
||||
// TIMERCONFIG: wake-up timer with the 16 ms prescaler
|
||||
static constexpr uint8_t TIMER_CONFIG_WAKEUP = 0x04;
|
||||
static constexpr uint32_t TIMER_TICK_MS = 16;
|
||||
static constexpr uint32_t TIMER_MAX_TICKS = 0xFFFFFF;
|
||||
|
||||
// BCHGISETDISCHARGE codes up to this one select the 200 mA discharge limit, the others 1000 mA
|
||||
static constexpr uint16_t DISCHARGE_LIMIT_200MA_CODE = 84;
|
||||
|
||||
static uint16_t discharge_limit_ma(uint16_t code) { return code <= DISCHARGE_LIMIT_200MA_CODE ? 200 : 1000; }
|
||||
|
||||
// BCHGISET and BCHGISETDISCHARGE hold a 9-bit code: the high 8 bits, then bit 0 in the next register
|
||||
static void split_9bit(uint16_t code, uint8_t *out) {
|
||||
out[0] = code >> 1;
|
||||
out[1] = code & 1;
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
static uint16_t join_9bit(const uint8_t *in) { return (in[0] << 1) | (in[1] & 1); }
|
||||
|
||||
// ADCIBATMEASSTATUS
|
||||
static constexpr uint8_t IBAT_MEAS_INVALID = 0x10;
|
||||
static constexpr uint8_t IBAT_MODE_SHIFT = 2;
|
||||
static constexpr uint8_t IBAT_MODE_DISCHARGE = 0x01;
|
||||
static constexpr uint8_t IBAT_MODE_CHARGE = 0x03;
|
||||
// Full scale of the battery current measurement per milliampere of the charge and discharge limits
|
||||
static constexpr float CHARGE_FULL_SCALE_FACTOR = 1.25f;
|
||||
static constexpr float DISCHARGE_FULL_SCALE_FACTOR = 1.12f;
|
||||
static constexpr float ADC_MAX_CODE = 1023.0f;
|
||||
// Five conversions of 250 us each; the loop runs the timeout later than this anyway
|
||||
static constexpr uint32_t CONVERSION_TIME_MS = 2;
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
float ntc_temperature(uint16_t code, float inv_beta) {
|
||||
if (code == 0)
|
||||
return NAN;
|
||||
// The NTC divider gives code = 1024 * R / (R + R_nominal)
|
||||
return 1.0f / (1.0f / 298.15f - logf(1024.0f / code - 1.0f) * inv_beta) - 273.15f;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
PROGMEM_STRING_TABLE(ChargerStatusStrings, "Error", "Paused, Die Too Hot", "Charge Complete", "Trickle Charge",
|
||||
"Constant Current", "Constant Voltage", "Discharging", "Not Charging");
|
||||
// BCHGCHARGESTATUS bits in order of priority; bit i is reported as string i + 1
|
||||
static const uint8_t CHARGER_STATUS_BITS[] PROGMEM = {CHARGE_STATUS_DIE_TEMP_HIGH, CHARGE_STATUS_COMPLETED,
|
||||
CHARGE_STATUS_TRICKLE, CHARGE_STATUS_CONSTANT_CURRENT,
|
||||
CHARGE_STATUS_CONSTANT_VOLTAGE};
|
||||
|
||||
static uint8_t charger_status_index(uint8_t vbus_status, uint8_t charge_status, uint8_t error_reason) {
|
||||
if (error_reason != 0)
|
||||
return 0;
|
||||
for (size_t i = 0; i < sizeof(CHARGER_STATUS_BITS); i++) {
|
||||
if ((charge_status & progmem_read_byte(&CHARGER_STATUS_BITS[i])) != 0)
|
||||
return i + 1;
|
||||
}
|
||||
return (vbus_status & VBUS_PRESENT) == 0 ? 6 : 7;
|
||||
}
|
||||
|
||||
void publish_charger_status(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t vbus_status, uint8_t charge_status,
|
||||
uint8_t error_reason) {
|
||||
const uint8_t index = charger_status_index(vbus_status, charge_status, error_reason);
|
||||
if (index == last)
|
||||
return;
|
||||
last = index;
|
||||
sensor->publish_state(ChargerStatusStrings::get_progmem_str(index, ChargerStatusStrings::LAST_INDEX));
|
||||
}
|
||||
#endif
|
||||
|
||||
NPM1300Config NPM1300Component::load_config_() const {
|
||||
NPM1300Config config;
|
||||
progmem_memcpy(&config, this->config_, sizeof(config));
|
||||
return config;
|
||||
}
|
||||
|
||||
void NPM1300Component::setup() {
|
||||
uint8_t status;
|
||||
if (!this->setup_check(this->read_byte_reg16(REG_BCHG_CHARGE_STATUS, &status)))
|
||||
return;
|
||||
|
||||
// Raise the VBUS current limit before anything turns on a regulator: at the power-on limit of 100 mA the
|
||||
// inrush current of a regulator can trip the over-current protection and brown out the board
|
||||
if (this->vbus_current_limit_ != NPM1300_UNSET &&
|
||||
!this->setup_check(this->write_byte_reg16(REG_VBUSIN_ILIM_STARTUP, this->vbus_current_limit_) &&
|
||||
this->apply_vbus_current_limit_()))
|
||||
return;
|
||||
|
||||
const NPM1300Config config = this->load_config_();
|
||||
if (config.thermistor != NPM1300_UNSET &&
|
||||
!this->setup_check(this->write_byte_reg16(REG_ADC_NTC_R_SEL, config.thermistor)))
|
||||
return;
|
||||
|
||||
if (config.charger_configured && !this->setup_check(this->configure_charger_(config)))
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < NPM1300_OUTPUT_COUNT; i++) {
|
||||
if (!this->setup_check(this->configure_output_(i, config.outputs[i])))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
if (this->data_publisher_ != nullptr) {
|
||||
// The battery current is measured after every battery voltage measurement
|
||||
if (!this->setup_check(this->write_byte_reg16(REG_ADC_IBAT_MEAS_EN, 1)))
|
||||
return;
|
||||
if (config.charger_configured) {
|
||||
this->charge_code_ = config.charger.current;
|
||||
this->discharge_limit_high_ = discharge_limit_ma(config.charger.discharge_limit) == 1000;
|
||||
} else if (!this->setup_check(this->read_charge_currents_())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool NPM1300Component::apply_vbus_current_limit_() {
|
||||
return this->write_byte_reg16(REG_VBUSIN_ILIM0, this->vbus_current_limit_) &&
|
||||
this->write_byte_reg16(REG_VBUSIN_TASK_UPDATE_ILIM, 1);
|
||||
}
|
||||
|
||||
bool NPM1300Component::configure_charger_(const NPM1300Config &config) {
|
||||
const NPM1300ChargerConfig &charger = config.charger;
|
||||
// The charge settings are only taken while the charger is off
|
||||
if (!this->write_byte_reg16(REG_BCHG_ENABLE_CLR, 1))
|
||||
return false;
|
||||
uint8_t current[2];
|
||||
uint8_t discharge[2];
|
||||
split_9bit(charger.current, current);
|
||||
split_9bit(charger.discharge_limit, discharge);
|
||||
uint8_t disable = charger.recharge ? 0 : CHARGER_DISABLE_RECHARGE;
|
||||
// Without a thermistor the charger reports an NTC error and does not charge
|
||||
if (config.thermistor == NPM1300_UNSET)
|
||||
disable |= CHARGER_DISABLE_NTC;
|
||||
const uint8_t enable = (CHARGER_DISABLE_RECHARGE | CHARGER_DISABLE_NTC) & ~disable;
|
||||
if (this->write_register16(REG_BCHG_ISET_MSB, current, sizeof(current)) != i2c::ERROR_OK ||
|
||||
this->write_register16(REG_BCHG_ISET_DISCHARGE_MSB, discharge, sizeof(discharge)) != i2c::ERROR_OK ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTERM, charger.termination_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTERM_R, charger.warm_termination_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTRICKLE_SEL, charger.trickle_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_ITERM_SEL, charger.termination_current) ||
|
||||
!this->write_byte_reg16(REG_BCHG_DISABLE_SET, disable) || !this->write_byte_reg16(REG_BCHG_DISABLE_CLR, enable) ||
|
||||
// Measure the battery temperature automatically while charging
|
||||
!this->write_byte_reg16(REG_ADC_TASK_AUTO_TIM_UPDATE, 1))
|
||||
return false;
|
||||
return !charger.enabled || this->set_charger_enabled(true);
|
||||
}
|
||||
|
||||
bool NPM1300Component::configure_output_(uint8_t index, const NPM1300OutputConfig &config) {
|
||||
const uint8_t channel = index & 1;
|
||||
if (index < 2) {
|
||||
// The software voltage replaces the one set by the VSET pin only once its SWCTRLSEL bit is set
|
||||
if (config.voltage != NPM1300_UNSET &&
|
||||
(!this->write_byte_reg16(REG_BUCK1_NORM_VOUT + channel * 2, config.voltage) ||
|
||||
!this->update_register16(REG_BUCK_SW_CTRL_SEL, 1 << channel, 1 << channel)))
|
||||
return false;
|
||||
} else if ((config.voltage != NPM1300_UNSET &&
|
||||
!this->write_byte_reg16(REG_LDSW1_VOUT_SEL + channel, config.voltage)) ||
|
||||
(config.ldo_mode != NPM1300_UNSET &&
|
||||
!this->write_byte_reg16(REG_LDSW1_LDO_SEL + channel, config.ldo_mode))) {
|
||||
return false;
|
||||
}
|
||||
return config.enabled == NPM1300_UNSET ||
|
||||
this->set_output_enabled(static_cast<NPM1300Output>(index), config.enabled != 0);
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
bool NPM1300Component::read_charge_currents_() {
|
||||
uint8_t charge[2];
|
||||
uint8_t discharge[2];
|
||||
if (this->read_register16(REG_BCHG_ISET_MSB, charge, sizeof(charge)) != i2c::ERROR_OK ||
|
||||
this->read_register16(REG_BCHG_ISET_DISCHARGE_MSB, discharge, sizeof(discharge)) != i2c::ERROR_OK)
|
||||
return false;
|
||||
this->charge_code_ = join_9bit(charge);
|
||||
this->discharge_limit_high_ = discharge_limit_ma(join_9bit(discharge)) == 1000;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool NPM1300Component::set_charger_enabled(bool enabled) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
if (!enabled)
|
||||
return this->write_byte_reg16(REG_BCHG_ENABLE_CLR, 1);
|
||||
// A charger error stops the charger until the error is cleared and the charger released
|
||||
return this->write_byte_reg16(REG_BCHG_TASK_CLEAR_CHG_ERR, 1) &&
|
||||
this->write_byte_reg16(REG_BCHG_TASK_RELEASE_ERR, 1) && this->write_byte_reg16(REG_BCHG_ENABLE_SET, 1);
|
||||
}
|
||||
|
||||
bool NPM1300Component::set_output_enabled(NPM1300Output output, bool enabled) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
const uint8_t index = static_cast<uint8_t>(output);
|
||||
// A pair of SET and CLR task registers per output
|
||||
const uint16_t base = index < 2 ? REG_BUCK1_ENA_SET : REG_LDSW1_TASK_SET;
|
||||
const uint16_t reg = base + (index & 1) * 2 + (enabled ? 0 : 1);
|
||||
if (!this->write_byte_reg16(reg, 1)) {
|
||||
ESP_LOGW(TAG, "Turning output %u %s failed", index, enabled ? LOG_STR_LITERAL("on") : LOG_STR_LITERAL("off"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void NPM1300Component::enter_ship_mode() {
|
||||
ESP_LOGI(TAG, "Entering ship mode");
|
||||
this->write_byte_reg16(REG_SHIP_TASK_SHIP_MODE, 1);
|
||||
}
|
||||
|
||||
void NPM1300Component::hibernate(uint32_t duration_ms) {
|
||||
// A zero period would never wake the chip
|
||||
const uint32_t ticks = std::clamp<uint32_t>(duration_ms / TIMER_TICK_MS, 1, TIMER_MAX_TICKS);
|
||||
uint8_t timer[3];
|
||||
put_be24(timer, ticks);
|
||||
ESP_LOGI(TAG, "Hibernating for %" PRIu32 " ms", ticks * TIMER_TICK_MS);
|
||||
if (!this->write_byte_reg16(REG_TIMER_CONFIG, TIMER_CONFIG_WAKEUP) ||
|
||||
this->write_register16(REG_TIMER_HI_BYTE, timer, sizeof(timer)) != i2c::ERROR_OK ||
|
||||
!this->write_byte_reg16(REG_TIMER_TARGET_STROBE, 1)) {
|
||||
ESP_LOGW(TAG, "Setting the wake-up timer failed");
|
||||
return;
|
||||
}
|
||||
// The timer needs a moment to load the new period
|
||||
delay(1);
|
||||
this->write_byte_reg16(REG_SHIP_TASK_HIBERNATE, 1);
|
||||
}
|
||||
|
||||
void NPM1300Component::update() {
|
||||
uint8_t vbus_status;
|
||||
uint8_t charge_status;
|
||||
uint8_t error_reason;
|
||||
if (!this->status_comm_check(this->read_byte_reg16(REG_VBUSIN_STATUS, &vbus_status) &&
|
||||
this->read_byte_reg16(REG_BCHG_CHARGE_STATUS, &charge_status) &&
|
||||
this->read_byte_reg16(REG_BCHG_ERR_REASON, &error_reason)))
|
||||
return;
|
||||
|
||||
// The chip goes back to its default VBUS current limit whenever VBUS is removed
|
||||
const bool vbus_present = (vbus_status & VBUS_PRESENT) != 0;
|
||||
if (vbus_present && !this->vbus_present_ && this->vbus_current_limit_ != NPM1300_UNSET &&
|
||||
!this->status_comm_check(this->apply_vbus_current_limit_()))
|
||||
return;
|
||||
this->vbus_present_ = vbus_present;
|
||||
|
||||
if (error_reason != this->last_error_reason_) {
|
||||
this->last_error_reason_ = error_reason;
|
||||
if (error_reason != 0) {
|
||||
ESP_LOGW(TAG, "Charger error, BCHGERRREASON 0x%02X", error_reason);
|
||||
}
|
||||
}
|
||||
#ifdef USE_NPM1300_STATUS_PUBLISHER
|
||||
if (this->status_publisher_ != nullptr)
|
||||
this->status_publisher_(vbus_status, charge_status, error_reason);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
if (!this->status_comm_check(this->publish_switches_()))
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
if (this->vbus_current_limit_sensor_ != nullptr) {
|
||||
uint8_t code;
|
||||
if (!this->status_comm_check(this->read_byte_reg16(REG_VBUSIN_ILIM0, &code)))
|
||||
return;
|
||||
// 100 mA per step of the code, but code 0 is 500 mA
|
||||
const uint16_t milliamps = code == 0 ? 500 : code * 100;
|
||||
this->vbus_current_limit_sensor_->publish_state(milliamps / 1000.0f);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
if (this->data_publisher_ != nullptr) {
|
||||
// Start the battery voltage (followed by the battery current), NTC, die temperature and system voltage
|
||||
// conversions, then the VBUS one
|
||||
static constexpr uint8_t START[4] = {1, 1, 1, 1};
|
||||
if (!this->status_comm_check(this->write_register16(REG_ADC_TASK_VBAT, START, sizeof(START)) == i2c::ERROR_OK &&
|
||||
this->write_byte_reg16(REG_ADC_TASK_VBUS, 1)))
|
||||
return;
|
||||
this->set_timeout(READ_TIMEOUT_ID, CONVERSION_TIME_MS, [this]() { this->read_measurements_(); });
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->status_comm_result(true);
|
||||
}
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
bool NPM1300Component::publish_switches_() {
|
||||
uint8_t value;
|
||||
bool ok = true;
|
||||
if (this->charger_switch_ != nullptr) {
|
||||
if (this->read_byte_reg16(REG_BCHG_ENABLE_SET, &value)) {
|
||||
this->charger_switch_->publish_state((value & 1) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
switch_::Switch *const bucks[2] = {this->buck1_switch_, this->buck2_switch_};
|
||||
if (bucks[0] != nullptr || bucks[1] != nullptr) {
|
||||
if (this->read_byte_reg16(REG_BUCK_STATUS, &value)) {
|
||||
for (uint8_t i = 0; i < 2; i++)
|
||||
PUBLISH_STATE(bucks[i], (value & BUCK_POWER_GOOD[i]) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
switch_::Switch *const ldos[2] = {this->ldo1_switch_, this->ldo2_switch_};
|
||||
if (ldos[0] != nullptr || ldos[1] != nullptr) {
|
||||
if (this->read_byte_reg16(REG_LDSW_STATUS, &value)) {
|
||||
for (uint8_t i = 0; i < 2; i++)
|
||||
PUBLISH_STATE(ldos[i], (value & LDSW_POWERED[i]) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
void NPM1300Component::read_measurements_() {
|
||||
// ADCIBATMEASSTATUS, then the VBAT, NTC, TEMP and VSYS results, their low bits, VBAT0 to VBAT3 (VBAT2 holds the
|
||||
// battery current and VBAT3 the VBUS voltage) and their low bits
|
||||
uint8_t adc[11];
|
||||
if (!this->status_comm_result(this->read_register16(REG_ADC_RESULTS, adc, sizeof(adc)) == i2c::ERROR_OK))
|
||||
return;
|
||||
// Amperes per code of the battery current, positive while charging and negative while discharging
|
||||
float ibat_scale = NAN;
|
||||
// Any other mode means the system runs from VBUS and the battery current is not measured
|
||||
const uint8_t mode = (adc[0] >> IBAT_MODE_SHIFT) & 0x03;
|
||||
if ((adc[0] & IBAT_MEAS_INVALID) == 0) {
|
||||
if (mode == IBAT_MODE_CHARGE) {
|
||||
ibat_scale = this->charge_code_ * (2 * CHARGE_FULL_SCALE_FACTOR) / (ADC_MAX_CODE * 1000.0f);
|
||||
} else if (mode == IBAT_MODE_DISCHARGE) {
|
||||
ibat_scale = -(this->discharge_limit_high_ ? 1000 : 200) * DISCHARGE_FULL_SCALE_FACTOR / (ADC_MAX_CODE * 1000.0f);
|
||||
}
|
||||
}
|
||||
if (this->data_publisher_ != nullptr)
|
||||
this->data_publisher_(adc, ibat_scale);
|
||||
}
|
||||
#endif
|
||||
|
||||
// BCHGVTERM: 3.50 V to 3.65 V, then 4.00 V to 4.45 V, both in 50 mV steps
|
||||
static uint16_t termination_voltage_mv(uint8_t code) { return code < 4 ? 3500 + 50 * code : 4000 + 50 * (code - 4); }
|
||||
|
||||
static const LogString *output_name(uint8_t index) {
|
||||
switch (static_cast<NPM1300Output>(index)) {
|
||||
case NPM1300Output::NPM1300_OUTPUT_BUCK1:
|
||||
return LOG_STR("BUCK1");
|
||||
case NPM1300Output::NPM1300_OUTPUT_BUCK2:
|
||||
return LOG_STR("BUCK2");
|
||||
case NPM1300Output::NPM1300_OUTPUT_LDO1:
|
||||
return LOG_STR("LDO1");
|
||||
default:
|
||||
return LOG_STR("LDO2");
|
||||
}
|
||||
}
|
||||
|
||||
void NPM1300Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "nPM1300:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->vbus_current_limit_ != NPM1300_UNSET) {
|
||||
// 100 mA per step of the code
|
||||
ESP_LOGCONFIG(TAG, " VBUS current limit: %u mA", this->vbus_current_limit_ * 100u);
|
||||
}
|
||||
const NPM1300Config config = this->load_config_();
|
||||
if (config.charger_configured) {
|
||||
const NPM1300ChargerConfig &charger = config.charger;
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Charger:\n"
|
||||
" Current: %u mA\n"
|
||||
" Discharge limit: %u mA\n"
|
||||
" Termination voltage: %u mV\n"
|
||||
" Warm termination voltage: %u mV\n"
|
||||
" Termination current: %u%%\n"
|
||||
" Trickle voltage: %u mV\n"
|
||||
" Enabled: %s\n"
|
||||
" Recharge: %s",
|
||||
charger.current * 2u, discharge_limit_ma(charger.discharge_limit),
|
||||
termination_voltage_mv(charger.termination_voltage),
|
||||
termination_voltage_mv(charger.warm_termination_voltage), charger.termination_current ? 20u : 10u,
|
||||
charger.trickle_voltage ? 2500u : 2900u, YESNO(charger.enabled), YESNO(charger.recharge));
|
||||
}
|
||||
for (uint8_t i = 0; i < NPM1300_OUTPUT_COUNT; i++) {
|
||||
const NPM1300OutputConfig &output = config.outputs[i];
|
||||
if (output.voltage == NPM1300_UNSET && output.ldo_mode == NPM1300_UNSET && output.enabled == NPM1300_UNSET)
|
||||
continue;
|
||||
ESP_LOGCONFIG(TAG, " %s:", LOG_STR_ARG(output_name(i)));
|
||||
if (output.voltage != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Voltage: %u mV", 1000u + output.voltage * 100u);
|
||||
}
|
||||
if (output.ldo_mode != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s",
|
||||
output.ldo_mode != 0 ? LOG_STR_LITERAL("LDO") : LOG_STR_LITERAL("load switch"));
|
||||
}
|
||||
if (output.enabled != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Enabled at boot: %s", YESNO(output.enabled != 0));
|
||||
}
|
||||
}
|
||||
if (config.thermistor != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Thermistor B constant: %u", config.thermistor_beta);
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "VBUS Current Limit", this->vbus_current_limit_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
LOG_SWITCH(" ", "Charger", this->charger_switch_);
|
||||
LOG_SWITCH(" ", "BUCK1", this->buck1_switch_);
|
||||
LOG_SWITCH(" ", "BUCK2", this->buck2_switch_);
|
||||
LOG_SWITCH(" ", "LDO1", this->ldo1_switch_);
|
||||
LOG_SWITCH(" ", "LDO2", this->ldo2_switch_);
|
||||
#endif
|
||||
#if defined(USE_NPM1300_STATUS_PUBLISHER) || defined(USE_NPM1300_DATA_PUBLISHER)
|
||||
if (this->dump_publisher_ != nullptr)
|
||||
this->dump_publisher_(TAG);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::npm1300
|
||||
@@ -0,0 +1,145 @@
|
||||
#pragma once
|
||||
|
||||
// Product specification: https://docs.nordicsemi.com/bundle/ps_npm1300/page/keyfeatures_html5.html
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::npm1300 {
|
||||
|
||||
/// The regulators, in the order of their enable task registers
|
||||
enum class NPM1300Output : uint8_t {
|
||||
NPM1300_OUTPUT_BUCK1 = 0,
|
||||
NPM1300_OUTPUT_BUCK2 = 1,
|
||||
NPM1300_OUTPUT_LDO1 = 2,
|
||||
NPM1300_OUTPUT_LDO2 = 3,
|
||||
};
|
||||
static constexpr uint8_t NPM1300_OUTPUT_COUNT = 4;
|
||||
|
||||
/// A configuration field the YAML leaves out, so the chip keeps its own value
|
||||
static constexpr uint8_t NPM1300_UNSET = 0xFF;
|
||||
|
||||
/// Register codes for one regulator, NPM1300_UNSET where the YAML leaves the field out
|
||||
struct NPM1300OutputConfig {
|
||||
uint8_t voltage; // VOUT: (millivolts - 1000) / 100
|
||||
uint8_t ldo_mode; // LDSWxLDOSEL, LDO1 and LDO2 only: 1 LDO, 0 load switch
|
||||
uint8_t enabled; // turned on (1) or off (0) at boot
|
||||
};
|
||||
|
||||
/// Register codes for the battery charger
|
||||
struct NPM1300ChargerConfig {
|
||||
uint16_t current; // BCHGISET: milliamperes / 2
|
||||
uint16_t discharge_limit; // BCHGISETDISCHARGE: 84 for 200 mA, 415 for 1000 mA
|
||||
uint8_t termination_voltage; // BCHGVTERM
|
||||
uint8_t warm_termination_voltage; // BCHGVTERMR
|
||||
uint8_t trickle_voltage; // BCHGVTRICKLESEL: 0 for 2.9 V, 1 for 2.5 V
|
||||
uint8_t termination_current; // BCHGITERMSEL: 0 for 10 %, 1 for 20 %
|
||||
bool recharge;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
/// The settings applied in setup(), built by code generation into a PROGMEM table
|
||||
struct NPM1300Config {
|
||||
NPM1300OutputConfig outputs[NPM1300_OUTPUT_COUNT];
|
||||
NPM1300ChargerConfig charger; // only applied when charger_configured
|
||||
uint16_t thermistor_beta;
|
||||
uint8_t thermistor; // ADCNTCRSEL code: 1 for 10 kOhm, 2 for 47 kOhm, 3 for 100 kOhm, or NPM1300_UNSET
|
||||
bool charger_configured;
|
||||
};
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
/// The 10-bit ADC result whose high 8 bits are adc[msb] and low 2 bits are at `shift` in adc[lsbs]
|
||||
inline uint16_t adc_code(const uint8_t *adc, uint8_t msb, uint8_t lsbs, uint8_t shift) {
|
||||
return (adc[msb] << 2) | ((adc[lsbs] >> shift) & 0x03);
|
||||
}
|
||||
/// Battery temperature in degrees Celsius from the NTC code and 1 / B constant of the thermistor
|
||||
float ntc_temperature(uint16_t code, float inv_beta);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
/// Publish the charger status text, only when it differs from `last`
|
||||
void publish_charger_status(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t vbus_status, uint8_t charge_status,
|
||||
uint8_t error_reason);
|
||||
#endif
|
||||
|
||||
class NPM1300Component : public PollingComponent, public i2c::I2CDevice {
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR(vbus_current_limit)
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
SUB_SWITCH(charger)
|
||||
SUB_SWITCH(buck1)
|
||||
SUB_SWITCH(buck2)
|
||||
SUB_SWITCH(ldo1)
|
||||
SUB_SWITCH(ldo2)
|
||||
#endif
|
||||
|
||||
public:
|
||||
/// `config` points into PROGMEM
|
||||
explicit NPM1300Component(const NPM1300Config *config) : config_(config) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
// Right after the I2C bus: the regulators may power other peripherals
|
||||
float get_setup_priority() const override { return setup_priority::BUS - 1.0f; }
|
||||
|
||||
#ifdef USE_NPM1300_STATUS_PUBLISHER
|
||||
HUB_PUBLISHER(status, uint8_t, uint8_t, uint8_t)
|
||||
#endif
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
HUB_PUBLISHER(data, const uint8_t *, float)
|
||||
#endif
|
||||
#if defined(USE_NPM1300_STATUS_PUBLISHER) || defined(USE_NPM1300_DATA_PUBLISHER)
|
||||
HUB_PUBLISHER(dump, const char *)
|
||||
#endif
|
||||
|
||||
/// VBUSINILIM0 code: 100 mA per step (code 0, the reset value, is 500 mA)
|
||||
void set_vbus_current_limit(uint8_t code) { this->vbus_current_limit_ = code; }
|
||||
|
||||
/// Turn the battery charger on or off; turning it on also clears a charger error
|
||||
bool set_charger_enabled(bool enabled);
|
||||
/// Turn a regulator on or off
|
||||
bool set_output_enabled(NPM1300Output output, bool enabled);
|
||||
/// Enter ship mode: every output is turned off until the SHPHLD button is pressed or VBUS is connected
|
||||
void enter_ship_mode();
|
||||
/// Enter hibernate mode: every output is turned off for `duration_ms`, or until the SHPHLD button is
|
||||
/// pressed or VBUS is connected
|
||||
void hibernate(uint32_t duration_ms);
|
||||
|
||||
protected:
|
||||
NPM1300Config load_config_() const;
|
||||
bool apply_vbus_current_limit_();
|
||||
bool configure_charger_(const NPM1300Config &config);
|
||||
bool configure_output_(uint8_t index, const NPM1300OutputConfig &config);
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
bool read_charge_currents_();
|
||||
void read_measurements_();
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
/// Returns false when a status read failed
|
||||
bool publish_switches_();
|
||||
#endif
|
||||
|
||||
const NPM1300Config *config_;
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
// BCHGISET code and discharge limit, for the full scale of the battery current measurement
|
||||
uint16_t charge_code_{0};
|
||||
bool discharge_limit_high_{false};
|
||||
#endif
|
||||
uint8_t vbus_current_limit_{NPM1300_UNSET};
|
||||
uint8_t last_error_reason_{0};
|
||||
bool vbus_present_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::npm1300
|
||||
@@ -0,0 +1,151 @@
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ADC,
|
||||
CONF_NPM1300_ID,
|
||||
CONF_THERMISTOR_BETA,
|
||||
CONF_THERMISTOR_RESISTANCE,
|
||||
CONF_VBUS_CURRENT_LIMIT,
|
||||
DATA_PUBLISHER,
|
||||
DOMAIN,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
IBAT_SCALE,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
npm1300_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
CONF_BATTERY_CURRENT = "battery_current"
|
||||
CONF_BATTERY_TEMPERATURE = "battery_temperature"
|
||||
CONF_DIE_TEMPERATURE = "die_temperature"
|
||||
CONF_SYSTEM_VOLTAGE = "system_voltage"
|
||||
CONF_VBUS_VOLTAGE = "vbus_voltage"
|
||||
|
||||
ADC_MAX_CODE = 1023
|
||||
|
||||
|
||||
def _adc(msb: int, lsbs: int, shift: int) -> cg.MockObj:
|
||||
"""The 10-bit result in adc[msb] and bits shift..shift+1 of adc[lsbs]."""
|
||||
return npm1300_ns.adc_code(ADC, msb, lsbs, shift)
|
||||
|
||||
|
||||
# The decoded value and the dump name of each ADC sensor but the battery temperature, which needs the hub config.
|
||||
# Voltages scale by their full scale (product specification, section ADC).
|
||||
STATES = {
|
||||
CONF_BATTERY_VOLTAGE: (_adc(1, 5, 0) * (5.0 / ADC_MAX_CODE), "Battery Voltage"),
|
||||
CONF_BATTERY_CURRENT: (_adc(8, 10, 4) * IBAT_SCALE, "Battery Current"),
|
||||
# Product specification: T = 394.67 - 0.7926 * code
|
||||
CONF_DIE_TEMPERATURE: (394.67 - 0.7926 * _adc(3, 5, 4), "Die Temperature"),
|
||||
CONF_SYSTEM_VOLTAGE: (_adc(4, 5, 6) * (6.375 / ADC_MAX_CODE), "System Voltage"),
|
||||
CONF_VBUS_VOLTAGE: (_adc(9, 10, 6) * (7.5 / ADC_MAX_CODE), "VBUS Voltage"),
|
||||
}
|
||||
|
||||
|
||||
def _voltage_schema() -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
def _temperature_schema(**extra: str) -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_BATTERY_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_TEMPERATURE): _temperature_schema(),
|
||||
cv.Optional(CONF_DIE_TEMPERATURE): _temperature_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
),
|
||||
cv.Optional(CONF_SYSTEM_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_VBUS_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_VBUS_CURRENT_LIMIT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if CONF_BATTERY_TEMPERATURE not in config:
|
||||
return
|
||||
hub = fv.get_declaring_config(config[CONF_NPM1300_ID])
|
||||
if CONF_THERMISTOR_RESISTANCE not in hub:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_BATTERY_TEMPERATURE}' needs '{CONF_THERMISTOR_RESISTANCE}' "
|
||||
"set on the npm1300 component",
|
||||
path=[CONF_BATTERY_TEMPERATURE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _add_adc_sensor(
|
||||
hub: cg.MockObj, var: cg.MockObj, state: cg.MockObj, name: str
|
||||
) -> None:
|
||||
DATA_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
|
||||
DUMP.add(
|
||||
hub,
|
||||
sensor.sensor_ns.log_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression(f'LOG_STR_LITERAL("{name}")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub_id = config[CONF_NPM1300_ID]
|
||||
hub = await cg.get_variable(hub_id)
|
||||
sensors = sensor.sub_sensors(config)
|
||||
for key, (state, name) in STATES.items():
|
||||
if (var := await sensors(key, None)) is not None:
|
||||
_add_adc_sensor(hub, var, state, name)
|
||||
if (var := await sensors(CONF_BATTERY_TEMPERATURE, None)) is not None:
|
||||
hub_config = next(c for c in CORE.config[DOMAIN] if c[CONF_ID] == hub_id)
|
||||
state = npm1300_ns.ntc_temperature(
|
||||
_adc(2, 5, 2), 1.0 / hub_config[CONF_THERMISTOR_BETA]
|
||||
)
|
||||
_add_adc_sensor(hub, var, state, "Battery Temperature")
|
||||
await sensors(CONF_VBUS_CURRENT_LIMIT, hub.set_vbus_current_limit_sensor)
|
||||
@@ -0,0 +1,95 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CONF_BUCK1,
|
||||
CONF_BUCK2,
|
||||
CONF_CHARGER,
|
||||
CONF_LDO1,
|
||||
CONF_LDO2,
|
||||
CONF_NPM1300_ID,
|
||||
CONF_THERMISTOR_RESISTANCE,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
OUTPUTS,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
|
||||
def _switch_schema() -> cv.Schema:
|
||||
# The switches show the state of the chip, so nothing is restored at boot
|
||||
return switch.switch_schema(
|
||||
switch.ApplySwitch,
|
||||
default_restore_mode="DISABLED",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_CHARGER): _switch_schema(),
|
||||
cv.Optional(CONF_BUCK1): _switch_schema(),
|
||||
cv.Optional(CONF_BUCK2): _switch_schema(),
|
||||
cv.Optional(CONF_LDO1): _switch_schema(),
|
||||
cv.Optional(CONF_LDO2): _switch_schema(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if CONF_CHARGER not in config:
|
||||
return
|
||||
hub = fv.get_declaring_config(config[CONF_NPM1300_ID])
|
||||
# Only the charger block turns off the NTC check, without which the charger never starts
|
||||
if CONF_CHARGER not in hub and CONF_THERMISTOR_RESISTANCE not in hub:
|
||||
raise cv.Invalid(
|
||||
f"The '{CONF_CHARGER}' switch needs a '{CONF_CHARGER}' block or "
|
||||
f"'{CONF_THERMISTOR_RESISTANCE}' on the npm1300 component",
|
||||
path=[CONF_CHARGER],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _output_apply(key: str) -> str:
|
||||
return f"set_output_enabled({OUTPUTS[key]}, state)"
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(
|
||||
CONF_CHARGER,
|
||||
hub.set_charger_switch,
|
||||
apply="set_charger_enabled(state)",
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_BUCK1,
|
||||
hub.set_buck1_switch,
|
||||
apply=_output_apply(CONF_BUCK1),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_BUCK2,
|
||||
hub.set_buck2_switch,
|
||||
apply=_output_apply(CONF_BUCK2),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_LDO1,
|
||||
hub.set_ldo1_switch,
|
||||
apply=_output_apply(CONF_LDO1),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_LDO2,
|
||||
hub.set_ldo2_switch,
|
||||
apply=_output_apply(CONF_LDO2),
|
||||
publish="on_success",
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ICON_BATTERY
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
HexIntLiteral,
|
||||
VariableDeclarationExpression,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CHARGE_STATUS,
|
||||
CONF_NPM1300_ID,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
ERROR_REASON,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
STATUS_PUBLISHER,
|
||||
VBUS_STATUS,
|
||||
npm1300_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
CONF_CHARGER_STATUS = "charger_status"
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_CHARGER_STATUS): text_sensor.text_sensor_schema(
|
||||
icon=ICON_BATTERY,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if (var := await text_sensors(CONF_CHARGER_STATUS, None)) is None:
|
||||
return
|
||||
last = cg.MockObj(f"esphome__{var}__last")
|
||||
# 0xFF so the first update publishes
|
||||
declaration = VariableDeclarationExpression(cg.uint8, "", last, static=True)
|
||||
STATUS_PUBLISHER.add(
|
||||
hub, AssignmentExpression(None, None, declaration, HexIntLiteral(0xFF))
|
||||
)
|
||||
STATUS_PUBLISHER.add(
|
||||
hub,
|
||||
npm1300_ns.publish_charger_status(
|
||||
parent_ref(var), last, VBUS_STATUS, CHARGE_STATUS, ERROR_REASON
|
||||
),
|
||||
)
|
||||
DUMP.add(
|
||||
hub,
|
||||
text_sensor.text_sensor_ns.log_text_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression('LOG_STR_LITERAL("Charger Status")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
@@ -320,6 +320,8 @@
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
#define USE_NEXTION_WAVEFORM
|
||||
#define USE_NPM1300_DATA_PUBLISHER
|
||||
#define USE_NPM1300_STATUS_PUBLISHER
|
||||
#define USE_NUMBER
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OUTPUT
|
||||
|
||||
@@ -1155,6 +1155,16 @@ bool random_bytes(uint8_t *data, size_t len);
|
||||
/// @name Bit manipulation
|
||||
///@{
|
||||
|
||||
/// The high 4 bits of a byte, e.g. high_nibble(0xA5) == 0xA.
|
||||
constexpr uint8_t high_nibble(uint8_t byte) { return byte >> 4; }
|
||||
/// The low 4 bits of a byte, e.g. low_nibble(0xA5) == 0x5.
|
||||
constexpr uint8_t low_nibble(uint8_t byte) { return byte & 0x0F; }
|
||||
|
||||
/// Encode a 12-bit value from its high 8 and low 4 bits, e.g. encode_uint12(0x12, 0x3) == 0x123.
|
||||
/// Bits above the low 4 of \p low_bits are ignored.
|
||||
constexpr uint16_t encode_uint12(uint8_t high_bits, uint8_t low_bits) {
|
||||
return (static_cast<uint16_t>(high_bits) << 4) | low_nibble(low_bits);
|
||||
}
|
||||
/// Encode a 16-bit value given the most and least significant byte.
|
||||
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb) {
|
||||
return (static_cast<uint16_t>(msb) << 8) | (static_cast<uint16_t>(lsb));
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,114 @@
|
||||
"""Tests for the npm1300 register encoders and validators."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
from esphome.components.npm1300 import (
|
||||
CHARGER_SCHEMA,
|
||||
NPM1300_UNSET,
|
||||
OUTPUT_VOLTAGE,
|
||||
VBUS_CURRENT_LIMIT,
|
||||
_charger_config,
|
||||
_output_config,
|
||||
_termination_voltage_code,
|
||||
_vbus_current_limit_code,
|
||||
)
|
||||
from esphome.const import CONF_MODE, CONF_VOLTAGE
|
||||
from esphome.cpp_generator import StructInitializer
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _fields(init: StructInitializer) -> dict[str, str]:
|
||||
return {key: str(value) for key, value in init.args.items()}
|
||||
|
||||
|
||||
def _charger(config: ConfigType) -> dict[str, str]:
|
||||
return _fields(_charger_config(CHARGER_SCHEMA(config)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current", "code"), [("32mA", 16), ("150mA", 75), ("800mA", 400)]
|
||||
)
|
||||
def test_charge_current_code(current: str, code: int) -> None:
|
||||
assert _charger({"charge_current": current})["current"] == str(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("current", ["30mA", "802mA", "151mA"])
|
||||
def test_charge_current_rejected(current: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CHARGER_SCHEMA({"charge_current": current})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("voltage", "code"),
|
||||
[(3.5, 0), (3.65, 3), (4.0, 4), (4.2, 8), (4.45, 13)],
|
||||
)
|
||||
def test_termination_voltage_code(voltage: float, code: int) -> None:
|
||||
assert _termination_voltage_code(voltage) == code
|
||||
|
||||
|
||||
@pytest.mark.parametrize("voltage", ["3.7V", "3.95V", "4.5V"])
|
||||
def test_termination_voltage_gap_rejected(voltage: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CHARGER_SCHEMA({"charge_current": "100mA", "termination_voltage": voltage})
|
||||
|
||||
|
||||
def test_warm_termination_defaults_to_termination() -> None:
|
||||
fields = _charger({"charge_current": "100mA", "termination_voltage": "4.1V"})
|
||||
assert fields["termination_voltage"] == "6"
|
||||
assert fields["warm_termination_voltage"] == "6"
|
||||
|
||||
|
||||
def test_warm_termination_code() -> None:
|
||||
fields = _charger(
|
||||
{
|
||||
"charge_current": "100mA",
|
||||
"termination_voltage": "4.2V",
|
||||
"warm_termination_voltage": "3.6V",
|
||||
}
|
||||
)
|
||||
assert fields["termination_voltage"] == "8"
|
||||
assert fields["warm_termination_voltage"] == "2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("voltage", "code"), [("1.0V", 0), ("1.8V", 8), ("3.3V", 23)])
|
||||
def test_output_voltage_code(voltage: str, code: int) -> None:
|
||||
config = {CONF_VOLTAGE: OUTPUT_VOLTAGE(voltage)}
|
||||
assert _fields(_output_config(config))["voltage"] == str(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("voltage", ["0.9V", "3.4V", "1.85V"])
|
||||
def test_output_voltage_rejected(voltage: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
OUTPUT_VOLTAGE(voltage)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [None, {}])
|
||||
def test_unset_output_fields(config: ConfigType | None) -> None:
|
||||
unset = str(NPM1300_UNSET)
|
||||
assert _fields(_output_config(config)) == {
|
||||
"voltage": unset,
|
||||
"ldo_mode": unset,
|
||||
"enabled": unset,
|
||||
}
|
||||
|
||||
|
||||
def test_output_mode_and_enabled() -> None:
|
||||
fields = _fields(_output_config({CONF_MODE: "load_switch", CONF_ENABLED: True}))
|
||||
assert fields["ldo_mode"] == "0"
|
||||
assert fields["enabled"] == "1"
|
||||
assert fields["voltage"] == str(NPM1300_UNSET)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("limit", "code"), [("100mA", 1), ("300mA", 3), ("500mA", 5), ("1.5A", 15)]
|
||||
)
|
||||
def test_vbus_current_limit_code(limit: str, code: int) -> None:
|
||||
assert _vbus_current_limit_code(VBUS_CURRENT_LIMIT(limit)) == code
|
||||
|
||||
|
||||
@pytest.mark.parametrize("limit", ["50mA", "1600mA", "550mA"])
|
||||
def test_vbus_current_limit_rejected(limit: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
VBUS_CURRENT_LIMIT(limit)
|
||||
@@ -55,6 +55,26 @@ TEST(HelpersTest, Ilog10RoundTripMatchesLog10) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HelpersTest, Nibbles) {
|
||||
EXPECT_EQ(high_nibble(0xA5), 0xA);
|
||||
EXPECT_EQ(low_nibble(0xA5), 0x5);
|
||||
EXPECT_EQ(high_nibble(0x0F), 0x0);
|
||||
EXPECT_EQ(low_nibble(0xF0), 0x0);
|
||||
static_assert(high_nibble(0xFF) == 0xF && low_nibble(0xFF) == 0xF, "usable in constant expressions");
|
||||
}
|
||||
|
||||
TEST(HelpersTest, EncodeUint12) {
|
||||
EXPECT_EQ(encode_uint12(0x12, 0x3), 0x123);
|
||||
EXPECT_EQ(encode_uint12(0xFF, 0xF), 0xFFF);
|
||||
EXPECT_EQ(encode_uint12(0x00, 0x0), 0x000);
|
||||
EXPECT_EQ(encode_uint12(0x12, 0xA3), 0x123);
|
||||
// A touch record byte packing the low nibbles of X (high) and Y (low)
|
||||
const uint8_t shared = 0x5C;
|
||||
EXPECT_EQ(encode_uint12(0x34, high_nibble(shared)), 0x345);
|
||||
EXPECT_EQ(encode_uint12(0x67, low_nibble(shared)), 0x67C);
|
||||
static_assert(encode_uint12(0xAB, 0xC) == 0xABC, "usable in constant expressions");
|
||||
}
|
||||
|
||||
TEST(HelpersTest, SignExtend) {
|
||||
EXPECT_EQ(sign_extend<24>(0x000000), 0);
|
||||
EXPECT_EQ(sign_extend<24>(0x7FFFFF), 8388607);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
npm1300:
|
||||
id: pmic
|
||||
i2c_id: i2c_bus
|
||||
vbus_current_limit: 500mA
|
||||
thermistor_resistance: 10kOhm
|
||||
thermistor_beta: 3380
|
||||
charger:
|
||||
charge_current: 150mA
|
||||
termination_voltage: 4.2V
|
||||
warm_termination_voltage: 4.0V
|
||||
termination_current: 20%
|
||||
trickle_voltage: 2.5V
|
||||
discharge_current_limit: 1000mA
|
||||
recharge: true
|
||||
buck2:
|
||||
voltage: 3.3V
|
||||
ldo1:
|
||||
voltage: 3.3V
|
||||
mode: ldo
|
||||
enabled: true
|
||||
ldo2:
|
||||
mode: load_switch
|
||||
enabled: false
|
||||
|
||||
sensor:
|
||||
- platform: npm1300
|
||||
battery_voltage:
|
||||
name: Battery Voltage
|
||||
battery_current:
|
||||
name: Battery Current
|
||||
battery_temperature:
|
||||
name: Battery Temperature
|
||||
die_temperature:
|
||||
name: PMIC Temperature
|
||||
system_voltage:
|
||||
name: System Voltage
|
||||
vbus_voltage:
|
||||
name: VBUS Voltage
|
||||
vbus_current_limit:
|
||||
name: VBUS Current Limit
|
||||
|
||||
binary_sensor:
|
||||
- platform: npm1300
|
||||
vbus_present:
|
||||
name: VBUS Present
|
||||
vbus_current_limit_active:
|
||||
name: VBUS Current Limit Active
|
||||
charging:
|
||||
name: Charging
|
||||
charge_complete:
|
||||
name: Charge Complete
|
||||
charger_error:
|
||||
name: Charger Error
|
||||
|
||||
text_sensor:
|
||||
- platform: npm1300
|
||||
charger_status:
|
||||
name: Charger Status
|
||||
|
||||
switch:
|
||||
- platform: npm1300
|
||||
charger:
|
||||
name: Charger
|
||||
buck1:
|
||||
name: BUCK1
|
||||
ldo1:
|
||||
name: LDO1
|
||||
ldo2:
|
||||
name: LDO2
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Ship Mode
|
||||
on_press:
|
||||
- npm1300.enter_ship_mode: pmic
|
||||
- platform: template
|
||||
name: Hibernate
|
||||
on_press:
|
||||
- npm1300.hibernate:
|
||||
id: pmic
|
||||
duration: 1h
|
||||
- npm1300.hibernate:
|
||||
id: pmic
|
||||
duration: !lambda return 60000;
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
npm1300: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/host.yaml
|
||||
npm1300: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
npm1300: !include common.yaml
|
||||
Reference in new issue
Block a user