From 88668f448e0638149e000963867867c5b0d38b88 Mon Sep 17 00:00:00 2001 From: B48D81EFCC <111175947+B48D81EFCC@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:53:16 +0900 Subject: [PATCH] [max17048] Add MAX17048 Li+ Cell Fuel Gauge IC (#13585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Martín Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/max17048/__init__.py | 2 + esphome/components/max17048/max17048.cpp | 77 +++++++++++++++++++ esphome/components/max17048/max17048.h | 20 +++++ esphome/components/max17048/sensor.py | 66 ++++++++++++++++ tests/components/max17048/common.yaml | 8 ++ tests/components/max17048/test.esp32-idf.yaml | 3 + .../components/max17048/test.esp8266-ard.yaml | 3 + .../components/max17048/test.rp2040-ard.yaml | 3 + 9 files changed, 183 insertions(+) create mode 100644 esphome/components/max17048/__init__.py create mode 100644 esphome/components/max17048/max17048.cpp create mode 100644 esphome/components/max17048/max17048.h create mode 100644 esphome/components/max17048/sensor.py create mode 100644 tests/components/max17048/common.yaml create mode 100644 tests/components/max17048/test.esp32-idf.yaml create mode 100644 tests/components/max17048/test.esp8266-ard.yaml create mode 100644 tests/components/max17048/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 23bac1efae1..050f4d19d92 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -343,6 +343,7 @@ esphome/components/m5stack_8angle/* @rnauber esphome/components/mapping/* @clydebarrow esphome/components/matrix_keypad/* @ssieb esphome/components/max17043/* @blacknell +esphome/components/max17048/* @B48D81EFCC esphome/components/max31865/* @DAVe3283 esphome/components/max31888/* @JiriPrchal esphome/components/max44009/* @berfenger diff --git a/esphome/components/max17048/__init__.py b/esphome/components/max17048/__init__.py new file mode 100644 index 00000000000..ecd6a79dd5c --- /dev/null +++ b/esphome/components/max17048/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@B48D81EFCC"] +DOMAIN = "max17048" diff --git a/esphome/components/max17048/max17048.cpp b/esphome/components/max17048/max17048.cpp new file mode 100644 index 00000000000..67f9dbaac3d --- /dev/null +++ b/esphome/components/max17048/max17048.cpp @@ -0,0 +1,77 @@ +#include "max17048.h" +#include "esphome/core/log.h" + +namespace esphome::max17048 { + +// MAX17048/MAX17049 1 and 2 cell fuel gauges with ModelGauge +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/MAX17048-MAX17049.pdf + +ESPHOME_LOG_TAG(TAG, "max17048"); + +static constexpr uint8_t REG_VCELL = 0x02; // cell voltage +static constexpr uint8_t REG_SOC = 0x04; // state of charge +static constexpr uint8_t REG_VERSION = 0x08; // 0x001x +static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed + +static constexpr uint16_t VERSION_MASK = 0xFFF0; +static constexpr uint16_t VERSION_FAMILY = 0x0010; + +static constexpr float VCELL_VOLTS_PER_LSB = 78.125e-6f; +static constexpr float SOC_PERCENT_PER_LSB = 1.0f / 256.0f; +static constexpr float CRATE_PERCENT_PER_HOUR_PER_LSB = 0.208f; + +void MAX17048Component::setup() { + uint16_t version; + if (!this->read_byte_16(REG_VERSION, &version)) { + this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); + return; + } + if ((version & VERSION_MASK) != VERSION_FAMILY) { + ESP_LOGW(TAG, "Unexpected version 0x%04X", version); + } +} + +void MAX17048Component::update() { + bool ok = true; + uint16_t raw; + if (this->battery_voltage_sensor_ != nullptr) { + if (this->read_byte_16(REG_VCELL, &raw)) { + this->battery_voltage_sensor_->publish_state(raw * VCELL_VOLTS_PER_LSB); + } else { + ok = false; + } + } + if (this->battery_level_sensor_ != nullptr) { + if (this->read_byte_16(REG_SOC, &raw)) { + this->battery_level_sensor_->publish_state(raw * SOC_PERCENT_PER_LSB); + } else { + ok = false; + } + } + if (this->rate_sensor_ != nullptr) { + if (this->read_byte_16(REG_CRATE, &raw)) { + this->rate_sensor_->publish_state(static_cast(raw) * CRATE_PERCENT_PER_HOUR_PER_LSB); + } else { + ok = false; + } + } + if (ok) { + this->status_clear_warning(); + } else { + this->status_set_warning(LOG_STR("Register read failed")); + } +} + +void MAX17048Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX17048:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_sensor_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_sensor_); + LOG_SENSOR(" ", "Rate", this->rate_sensor_); +} + +} // namespace esphome::max17048 diff --git a/esphome/components/max17048/max17048.h b/esphome/components/max17048/max17048.h new file mode 100644 index 00000000000..906d4a7c3f3 --- /dev/null +++ b/esphome/components/max17048/max17048.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::max17048 { + +class MAX17048Component final : public PollingComponent, public i2c::I2CDevice { + SUB_SENSOR(battery_voltage) + SUB_SENSOR(battery_level) + SUB_SENSOR(rate) + + public: + void setup() override; + void dump_config() override; + void update() override; +}; + +} // namespace esphome::max17048 diff --git a/esphome/components/max17048/sensor.py b/esphome/components/max17048/sensor.py new file mode 100644 index 00000000000..30cd0ec59f0 --- /dev/null +++ b/esphome/components/max17048/sensor.py @@ -0,0 +1,66 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_ID, + CONF_RATE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_VOLTAGE, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + UNIT_VOLT, +) +from esphome.types import ConfigType + +DEPENDENCIES = ["i2c"] + +UNIT_PERCENT_PER_HOUR = "%/h" + +max17048_ns = cg.esphome_ns.namespace("max17048") +MAX17048Component = max17048_ns.class_( + "MAX17048Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MAX17048Component), + cv.Optional(CONF_BATTERY_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_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_RATE): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT_PER_HOUR, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x36)) +) + + +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_BATTERY_VOLTAGE, var.set_battery_voltage_sensor) + await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor) + await sensors(CONF_RATE, var.set_rate_sensor) diff --git a/tests/components/max17048/common.yaml b/tests/components/max17048/common.yaml new file mode 100644 index 00000000000..de0247ed4e2 --- /dev/null +++ b/tests/components/max17048/common.yaml @@ -0,0 +1,8 @@ +sensor: + - platform: max17048 + battery_voltage: + name: Battery Voltage + battery_level: + name: Battery Level + rate: + name: Battery Rate diff --git a/tests/components/max17048/test.esp32-idf.yaml b/tests/components/max17048/test.esp32-idf.yaml new file mode 100644 index 00000000000..e9930c953d3 --- /dev/null +++ b/tests/components/max17048/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + max17048: !include common.yaml diff --git a/tests/components/max17048/test.esp8266-ard.yaml b/tests/components/max17048/test.esp8266-ard.yaml new file mode 100644 index 00000000000..95f0f76f143 --- /dev/null +++ b/tests/components/max17048/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + max17048: !include common.yaml diff --git a/tests/components/max17048/test.rp2040-ard.yaml b/tests/components/max17048/test.rp2040-ard.yaml new file mode 100644 index 00000000000..7f24480cdce --- /dev/null +++ b/tests/components/max17048/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + max17048: !include common.yaml