mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[bq27220] Add BQ27220 battery fuel gauge component (#17702)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
d7dd25a1bc
commit
b88eafcd00
@@ -101,6 +101,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bq27220/* @rggammon
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@rggammon"]
|
||||
DOMAIN = "bq27220"
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "bq27220.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::bq27220 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bq27220");
|
||||
|
||||
// Standard command registers (16-bit, little-endian), SLUUBD4A
|
||||
static constexpr uint8_t REG_CONTROL = 0x00;
|
||||
static constexpr uint8_t REG_TEMPERATURE = 0x06; // 0.1 K
|
||||
static constexpr uint8_t REG_VOLTAGE = 0x08; // mV
|
||||
static constexpr uint8_t REG_CURRENT = 0x0C; // mA, signed
|
||||
static constexpr uint8_t REG_REMAINING_CAPACITY = 0x10; // mAh
|
||||
static constexpr uint8_t REG_FULL_CHARGE_CAPACITY = 0x12; // mAh
|
||||
static constexpr uint8_t REG_TIME_TO_EMPTY = 0x16; // min, 0xFFFF when not discharging
|
||||
static constexpr uint8_t REG_STATE_OF_CHARGE = 0x2C; // %
|
||||
static constexpr uint8_t REG_STATE_OF_HEALTH = 0x2E; // %
|
||||
// The DEVICE_NUMBER subcommand (0x0001, SLUUBD4A §2.2.2) answers through the MAC data buffer
|
||||
static constexpr uint8_t REG_MAC_DATA = 0x40;
|
||||
static constexpr uint16_t DEVICE_NUMBER = 0x0220;
|
||||
static constexpr uint16_t TIME_TO_EMPTY_NOT_APPLICABLE = 0xFFFF;
|
||||
|
||||
bool BQ27220Component::read_word_(uint8_t reg, uint16_t &value) {
|
||||
uint8_t data[2];
|
||||
if (this->read_register(reg, data, 2) != i2c::ERROR_OK) {
|
||||
return false;
|
||||
}
|
||||
value = encode_uint16(data[1], data[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BQ27220Component::setup() {
|
||||
const uint8_t device_number_cmd[2] = {0x01, 0x00}; // DEVICE_NUMBER subcommand, little-endian
|
||||
if (this->write_register(REG_CONTROL, device_number_cmd, 2) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to communicate with BQ27220"));
|
||||
return;
|
||||
}
|
||||
delay(2); // SLUUBD4A §5.3: the subcommand result needs a moment before it can be read
|
||||
uint16_t device_number = 0;
|
||||
if (!this->read_word_(REG_MAC_DATA, device_number)) {
|
||||
this->mark_failed(LOG_STR("Failed to read device number from BQ27220"));
|
||||
return;
|
||||
}
|
||||
if (device_number != DEVICE_NUMBER) {
|
||||
ESP_LOGE(TAG, "Unexpected device number 0x%04X (expected 0x%04X)", device_number, DEVICE_NUMBER);
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void BQ27220Component::update() {
|
||||
bool ok = true;
|
||||
this->publish_(this->voltage_sensor_, REG_VOLTAGE, ok, [](uint16_t raw) { return raw * 0.001f; });
|
||||
this->publish_(this->current_sensor_, REG_CURRENT, ok,
|
||||
[](uint16_t raw) { return static_cast<int16_t>(raw) * 0.001f; });
|
||||
this->publish_(this->battery_level_sensor_, REG_STATE_OF_CHARGE, ok, [](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->temperature_sensor_, REG_TEMPERATURE, ok, [](uint16_t raw) { return raw * 0.1f - 273.15f; });
|
||||
this->publish_(this->remaining_capacity_sensor_, REG_REMAINING_CAPACITY, ok, [](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->full_charge_capacity_sensor_, REG_FULL_CHARGE_CAPACITY, ok,
|
||||
[](uint16_t raw) { return float(raw); });
|
||||
this->publish_(this->time_to_empty_sensor_, REG_TIME_TO_EMPTY, ok,
|
||||
[](uint16_t raw) { return raw == TIME_TO_EMPTY_NOT_APPLICABLE ? NAN : float(raw); });
|
||||
this->publish_(this->state_of_health_sensor_, REG_STATE_OF_HEALTH, ok, [](uint16_t raw) { return float(raw); });
|
||||
|
||||
if (ok) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->status_set_warning(LOG_STR("Failed to read one or more registers from BQ27220"));
|
||||
}
|
||||
}
|
||||
|
||||
void BQ27220Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BQ27220 Battery Fuel Gauge:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", this->current_sensor_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Remaining Capacity", this->remaining_capacity_sensor_);
|
||||
LOG_SENSOR(" ", "Full Charge Capacity", this->full_charge_capacity_sensor_);
|
||||
LOG_SENSOR(" ", "Time To Empty", this->time_to_empty_sensor_);
|
||||
LOG_SENSOR(" ", "State Of Health", this->state_of_health_sensor_);
|
||||
}
|
||||
|
||||
} // namespace esphome::bq27220
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
// Datasheet: https://www.ti.com/lit/ds/symlink/bq27220.pdf
|
||||
// Technical Reference Manual: https://www.ti.com/lit/ug/sluubd4a/sluubd4a.pdf
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::bq27220 {
|
||||
|
||||
class BQ27220Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(voltage)
|
||||
SUB_SENSOR(current)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(temperature)
|
||||
SUB_SENSOR(remaining_capacity)
|
||||
SUB_SENSOR(full_charge_capacity)
|
||||
SUB_SENSOR(time_to_empty)
|
||||
SUB_SENSOR(state_of_health)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
bool read_word_(uint8_t reg, uint16_t &value);
|
||||
/// Read a standard command register and publish it through the sensor, if one is configured.
|
||||
/// A failed read publishes NAN and clears `ok`.
|
||||
template<typename F> void publish_(sensor::Sensor *sensor, uint8_t reg, bool &ok, F convert) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
uint16_t raw;
|
||||
if (!this->read_word_(reg, raw)) {
|
||||
ok = false;
|
||||
sensor->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
sensor->publish_state(convert(raw));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bq27220
|
||||
@@ -0,0 +1,123 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_MILLIAMPERE_HOUR
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_CURRENT,
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_BATTERY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_MINUTE,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bq27220_ns = cg.esphome_ns.namespace("bq27220")
|
||||
BQ27220Component = bq27220_ns.class_(
|
||||
"BQ27220Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONF_REMAINING_CAPACITY = "remaining_capacity"
|
||||
CONF_FULL_CHARGE_CAPACITY = "full_charge_capacity"
|
||||
CONF_TIME_TO_EMPTY = "time_to_empty"
|
||||
CONF_STATE_OF_HEALTH = "state_of_health"
|
||||
|
||||
# Other SLUUBD4A standard commands are intentionally not exposed to keep the
|
||||
# component focused; they can be added later if there is demand: TimeToFull
|
||||
# (0x18), CycleCount (0x2A), BatteryStatus (0x0A), AveragePower (0x24),
|
||||
# DesignCapacity (0x3C).
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BQ27220Component),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIAMPERE_HOUR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_FULL_CHARGE_CAPACITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MILLIAMPERE_HOUR,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_TIME_TO_EMPTY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_MINUTE,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STATE_OF_HEALTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon=ICON_BATTERY,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x55))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_VOLTAGE, var.set_voltage_sensor)
|
||||
await sensors(CONF_CURRENT, var.set_current_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_TEMPERATURE, var.set_temperature_sensor)
|
||||
await sensors(CONF_REMAINING_CAPACITY, var.set_remaining_capacity_sensor)
|
||||
await sensors(CONF_FULL_CHARGE_CAPACITY, var.set_full_charge_capacity_sensor)
|
||||
await sensors(CONF_TIME_TO_EMPTY, var.set_time_to_empty_sensor)
|
||||
await sensors(CONF_STATE_OF_HEALTH, var.set_state_of_health_sensor)
|
||||
@@ -0,0 +1,19 @@
|
||||
sensor:
|
||||
- platform: bq27220
|
||||
id: bq27220_id
|
||||
voltage:
|
||||
name: "Battery Voltage"
|
||||
current:
|
||||
name: "Battery Current"
|
||||
battery_level:
|
||||
name: "Battery"
|
||||
temperature:
|
||||
name: "Battery Temperature"
|
||||
remaining_capacity:
|
||||
name: "Battery Remaining"
|
||||
full_charge_capacity:
|
||||
name: "Battery Full Capacity"
|
||||
time_to_empty:
|
||||
name: "Time to Empty"
|
||||
state_of_health:
|
||||
name: "Battery Health"
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
bq27220: !include common.yaml
|
||||
Reference in New Issue
Block a user