Compare commits

..
Author SHA1 Message Date
373224c141 [npm1300] Add the Nordic nPM1300 PMIC component (#22661)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-15 12:52:15 +01:00
19 changed files with 1586 additions and 97 deletions

No files matched your search

+1
View File
@@ -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
+26 -30
View File
@@ -49,40 +49,36 @@ void BME680BSECComponent::setup() {
this->load_state_();
}
// BSEC configuration blobs, kept in flash on ESP8266
static constexpr uint8_t CONFIG_33V_300S[] PROGMEM = {
#include "config/generic_33v_300s_28d/bsec_iaq.txt"
};
static constexpr uint8_t CONFIG_18V_300S[] PROGMEM = {
#include "config/generic_18v_300s_28d/bsec_iaq.txt"
};
static constexpr uint8_t CONFIG_33V_3S[] PROGMEM = {
#include "config/generic_33v_3s_28d/bsec_iaq.txt"
};
static constexpr uint8_t CONFIG_18V_3S[] PROGMEM = {
#include "config/generic_18v_3s_28d/bsec_iaq.txt"
};
static_assert(sizeof(CONFIG_33V_300S) <= BSEC_MAX_PROPERTY_BLOB_SIZE &&
sizeof(CONFIG_18V_300S) <= BSEC_MAX_PROPERTY_BLOB_SIZE &&
sizeof(CONFIG_33V_3S) <= BSEC_MAX_PROPERTY_BLOB_SIZE &&
sizeof(CONFIG_18V_3S) <= BSEC_MAX_PROPERTY_BLOB_SIZE);
void BME680BSECComponent::set_config_() {
const bool v33 = this->supply_voltage_ == SUPPLY_VOLTAGE_3V3;
const uint8_t *source;
size_t size;
if (this->sample_rate_ == SAMPLE_RATE_ULP) {
source = v33 ? CONFIG_33V_300S : CONFIG_18V_300S;
size = v33 ? sizeof(CONFIG_33V_300S) : sizeof(CONFIG_18V_300S);
if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) {
const uint8_t config[] = {
#include "config/generic_33v_300s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
} else { // SUPPLY_VOLTAGE_1V8
const uint8_t config[] = {
#include "config/generic_18v_300s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
}
} else { // SAMPLE_RATE_LP
source = v33 ? CONFIG_33V_3S : CONFIG_18V_3S;
size = v33 ? sizeof(CONFIG_33V_3S) : sizeof(CONFIG_18V_3S);
if (this->supply_voltage_ == SUPPLY_VOLTAGE_3V3) {
const uint8_t config[] = {
#include "config/generic_33v_3s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
} else { // SUPPLY_VOLTAGE_1V8
const uint8_t config[] = {
#include "config/generic_18v_3s_28d/bsec_iaq.txt"
};
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
}
}
// BSEC reads the blob with plain loads, so copy the selected one to RAM
uint8_t config[BSEC_MAX_PROPERTY_BLOB_SIZE]{};
progmem_memcpy(config, source, size);
this->bsec_status_ =
bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, this->work_buffer_, sizeof(this->work_buffer_));
}
float BME680BSECComponent::calc_sensor_sample_rate_(SampleRate sample_rate) {
+303
View File
@@ -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),
)
+106
View File
@@ -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),
),
)
+483
View File
@@ -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
+145
View File
@@ -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
+151
View File
@@ -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)
+95
View File
@@ -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",
)
+63
View File
@@ -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),
),
)
+2
View File
@@ -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
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)
+3 -8
View File
@@ -7,8 +7,7 @@ from esphome.helpers import write_file_if_changed
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
# set_config_() includes one of these library blobs; each gets distinct bytes so test_config.cpp can tell
# which one reached bsec_set_configuration().
# set_config_() includes one of these library blobs; the double only needs them to exist.
_CONFIG_BLOBS = (
"generic_33v_300s_28d",
"generic_18v_300s_28d",
@@ -24,12 +23,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
async def to_code_testing(config: ConfigType) -> None:
blobs = CORE.relative_build_path("bsec_config")
for index, name in enumerate(_CONFIG_BLOBS):
first = 0x10 * (index + 1)
write_file_if_changed(
blobs / "config" / name / "bsec_iaq.txt",
f"{first}, {first + 1}, {first + 2}\n",
)
for name in _CONFIG_BLOBS:
write_file_if_changed(blobs / "config" / name / "bsec_iaq.txt", "0\n")
cg.add_build_flag(f"-I{blobs}")
cg.add_build_flag(f"-I{CORE.relative_src_path('bme680_bsec', '__sdk')}")
cg.add_define("USE_BSEC")
+1 -8
View File
@@ -57,9 +57,6 @@ inline uint8_t num_outputs = 0;
inline bool armed = false;
// Counts bsec_get_state calls, which the component makes only when it saves its state.
inline int get_state_calls = 0;
// The blob and length the last bsec_set_configuration call received.
inline uint8_t config[BSEC_MAX_PROPERTY_BLOB_SIZE]{};
inline uint32_t config_size = 0;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
inline void arm(const bsec_output_t *reading, uint8_t count) {
@@ -74,11 +71,7 @@ inline bsec_library_return_t bsec_get_version(bsec_version_t *version) {
*version = {};
return BSEC_OK;
}
inline bsec_library_return_t bsec_set_configuration(const uint8_t *config, uint32_t size, uint8_t *, uint32_t) {
std::memcpy(bsec_double::config, config, size < sizeof(bsec_double::config) ? size : sizeof(bsec_double::config));
bsec_double::config_size = size;
return BSEC_OK;
}
inline bsec_library_return_t bsec_set_configuration(const uint8_t *, uint32_t, uint8_t *, uint32_t) { return BSEC_OK; }
inline bsec_library_return_t bsec_update_subscription(const bsec_sensor_configuration_t *, uint8_t,
bsec_sensor_configuration_t *, uint8_t *) {
return BSEC_OK;
@@ -1,51 +0,0 @@
#include <gtest/gtest.h>
#include <cstring>
#include <tuple>
#include "esphome/components/bme680_bsec/bme680_bsec.h"
// The unit test build defines USE_BSEC and builds against the library double in __sdk; clang-tidy does not.
#ifdef USE_BSEC
namespace esphome::bme680_bsec::testing {
namespace {
// Sample rate, supply voltage, and the first byte of the blob __init__.py writes for that pair.
using ConfigCase = std::tuple<SampleRate, SupplyVoltage, uint8_t>;
class BSECConfigTest : public ::testing::TestWithParam<ConfigCase> {};
TEST_P(BSECConfigTest, SetupPassesTheSelectedBlob) {
auto [sample_rate, supply_voltage, first] = GetParam();
bsec_double::config_size = 0;
std::memset(bsec_double::config, 0xEE, sizeof(bsec_double::config));
BME680BSECComponent component;
component.set_sample_rate(sample_rate);
component.set_supply_voltage(supply_voltage);
component.setup();
// setup() registers the component; drop it so later tests do not see a dangling instance
ASSERT_FALSE(BME680BSECComponent::instances.empty());
ASSERT_EQ(BME680BSECComponent::instances.back(), &component);
BME680BSECComponent::instances.pop_back();
// The whole blob, copied out of the selected table, padded with zeros to the blob size
EXPECT_EQ(bsec_double::config_size, BSEC_MAX_PROPERTY_BLOB_SIZE);
EXPECT_EQ(bsec_double::config[0], first);
EXPECT_EQ(bsec_double::config[1], first + 1);
EXPECT_EQ(bsec_double::config[2], first + 2);
for (size_t i = 3; i < BSEC_MAX_PROPERTY_BLOB_SIZE; i++)
EXPECT_EQ(bsec_double::config[i], 0) << "byte " << i;
}
INSTANTIATE_TEST_SUITE_P(AllBlobs, BSECConfigTest,
::testing::Values(ConfigCase{SAMPLE_RATE_ULP, SUPPLY_VOLTAGE_3V3, 0x10},
ConfigCase{SAMPLE_RATE_ULP, SUPPLY_VOLTAGE_1V8, 0x20},
ConfigCase{SAMPLE_RATE_LP, SUPPLY_VOLTAGE_3V3, 0x30},
ConfigCase{SAMPLE_RATE_LP, SUPPLY_VOLTAGE_1V8, 0x40}));
} // namespace
} // namespace esphome::bme680_bsec::testing
#endif // USE_BSEC
+84
View File
@@ -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
+3
View File
@@ -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