Adafruit MAX17048 LiPo battery gauge with ESPHome

Maybe someone can help, I’m struggling to get this right. I want to use Adafruit’s MAX17048 LiPo battery gauge:

I tried to take inspiration from this thread:

Ok so I tried modifying a-marcel’s code and got it to at least produce some output but the numbers seem wildly wrong:

[18:28:14][D][sensor:127]: 'Voltage': Sending state 3616.25000  with 0 decimals of accuracy
[18:28:14][D][sensor:127]: 'Percentage': Sending state 3.10937  with 0 decimals of accuracy

I confess that I really don’t understand what this is doing or why we need all these bit shift operators.

  void update() override {
    float voltage = 1.25f * (float)(read16(MAX17043_VCELL) >> 4);
    voltage_sensor->publish_state(voltage);

    uint16_t percentage_tmp = read16(MAX17043_SOC);
    float percentage = (float)((percentage_tmp >> 8) + 0.003906f * (percentage_tmp & 0x00ff));

    percentage_sensor->publish_state(percentage);
  }

Why all the complicated formulas? Does the sensor not produce a simple voltage number and percentage so the data has to be massaged like this?

Ok…so I think I have this solved but maybe someone else can check my work. I copied these formulas from the source code of the Adafruit driver:

So now my code looks like this:

  void update() override {
    //float voltage = 1.25f * (float)(read16(MAX17048_VCELL) >> 4);
    float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000;
    voltage_sensor->publish_state(voltage);

    uint16_t percentage_tmp = read16(MAX17048_SOC);
    //float percentage = (float)((percentage_tmp >> 8) + 0.003906f * (percentage_tmp & 0x00ff));
    float percentage = (float)(percentage_tmp) / 256;
    percentage_sensor->publish_state(percentage);
  }

Now it produces outputs that look like a voltage and percentage:

[06:24:58][D][sensor:127]: 'Voltage': Sending state 3.56000  with 0 decimals of accuracy
[06:24:58][D][sensor:127]: 'Percentage': Sending state 93.23047  with 0 decimals of accuracy

Does it look right to you? Am I missing anything?

The values are a bit odd, a fully charged battery started at 103% and 4.2V. Methinks my formulas aren’t quite optimized correctly.

image

but at least I can watch the values change as the battery discharges.

This is working great, if anyone else needs a solution for monitoring battery life. Here’s the full code in case anyone else needs it.

MAX17048_component.h - put this file in your esphome folder:

#include "esphome.h"

#define MAX17048_ADDRESS        0x36
#define MAX17048_VCELL          0x02 // voltage
#define MAX17048_SOC            0x04 // percentage
#define MAX17048_MODE           0x06
#define MAX17048_VERSION        0x08
#define MAX17048_CONFIG         0x0c
#define MAX17048_COMMAND        0xfe

class MAX17048Sensor : public PollingComponent, public Sensor {
 public:
  Sensor *voltage_sensor = new Sensor();
  Sensor *percentage_sensor = new Sensor();

  MAX17048Sensor() : PollingComponent(10000) {}

  void setup() override {
    // Initialize the device here. Usually Wire.begin() will be called in here,
    // though that call is unnecessary if you have an 'i2c:' entry in your config
    ESP_LOGD("custom", "Starting up MAX17048 sensor");

    Wire.begin();
  }

  uint16_t read16(uint8_t reg) {
      uint16_t temp;
      Wire.begin();
      Wire.beginTransmission(MAX17048_ADDRESS);
      Wire.write(reg);
      Wire.endTransmission();
      Wire.requestFrom(MAX17048_ADDRESS, 2);
      temp = (uint16_t)Wire.read() << 8;
      temp |= (uint16_t)Wire.read();
      Wire.endTransmission();
      return temp;
  }

  void update() override {
    float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000;
    voltage_sensor->publish_state(voltage);

    uint16_t percentage_tmp = read16(MAX17048_SOC);
    float percentage = (float)(percentage_tmp) / 256;
    percentage_sensor->publish_state(percentage);
  }
};

Put this in your ‘includes’:

esphome:
  includes:
    - MAX17048_component.h

and this in your sensor section:

  - platform: custom
    lambda: |-
      auto max17048_sensor = new MAX17048Sensor();
      App.register_component(max17048_sensor);
      return {max17048_sensor->voltage_sensor, max17048_sensor->percentage_sensor};
    sensors:
      - name: "Voltage"
        unit_of_measurement: V
        accuracy_decimals: 2
      - name: "Percentage"
        unit_of_measurement: '%'
5 Likes

Thanks, @greenleaf Appreciate you providing this code.

I tried it with my setup and the voltage is fine, but I need a way to adjust the percent range. In my case I’m using 3 AA batteries, so 100% is 4.75 V. Is there a place to adjust this in the code or is this hard coded into the sensor?

hmm, best guess would be to try tweaking this line and see if your readings line up closer to 4.75 on a full charge

    float voltage = (float)(read16(MAX17048_VCELL)) * 78.125 / 1000000;

Has anyone done anything similar for the LC709203F

Hello,

I trying to build this ESPHome 2024.3.0 and I am facing an issue. Is that the case for you too?

INFO ESPHome 2024.3.0
INFO Reading configuration /config/esphome/adafruit-qualia-esp32-s3.yaml...
INFO Generating C++ source...
INFO Compiling app...
Processing adafruit-qualia-esp32-s3 (board: esp32-s3-devkitc-1; framework: espidf; platform: platformio/[email protected])
--------------------------------------------------------------------------------
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
 - framework-espidf @ 3.40406.240122 (4.4.6) 
 - tool-cmake @ 3.16.4 
 - tool-ninja @ 1.7.1 
 - toolchain-esp32ulp @ 2.35.0-20220830 
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
Reading CMake configuration...
Dependency Graph
|-- noise-c @ 0.1.4
Compiling .pioenvs/adafruit-qualia-esp32-s3/src/main.o
/config/esphome/adafruit-qualia-esp32-s3.yaml: In lambda function:
/config/esphome/adafruit-qualia-esp32-s3.yaml:94:34: error: expected type-specifier before 'MAX17048Sensor'
       auto max17048_sensor = new MAX17048Sensor();
                                  ^~~~~~~~~~~~~~
/config/esphome/adafruit-qualia-esp32-s3.yaml:96:82: error: could not convert '{<expression error>, <expression error>}' from '<brace-enclosed initializer list>' to 'std::vector<esphome::sensor::Sensor*>'
       return {max17048_sensor->voltage_sensor, max17048_sensor->percentage_sensor};
                                                                                  ^
*** [.pioenvs/adafruit-qualia-esp32-s3/src/main.o] Error 1

When I try to use @greenleaf 's code I get the error: src/MAX17048_component.h:23:5: error: 'Wire' was not declared in this scope. Some poking around showed me that most ESPHome components are not using Wire. Any thoughts, anyone?

Apparently custom components like this one are now deprecated: Custom I²C Device — ESPHome
So maybe one of us will get a chance to create an external component for the max17048 and share it here!

1 Like

If anyone returns to this thread, I threw together a new version of the driver, which seems to work currently. I used it in this recipe:
https://community.home-assistant.io/t/recipe-esphome-adafruit-feather-esp32-s2-thinkink-e-paper-display-battery-monitor/

tl;dr:

external_components:
  - source: github://Option-Zero/esphome-components@max17048
    components: [max17048]

sensor:
  - platform: max17048
    battery_voltage:
      name: Battery voltage
    battery_level:
      name: Battery level
    rate:
      name: Battery discharge rate

Lmk if you have success with this @lboue .

7 Likes

This works perfectly, thanks! Do you have plans to make a PR and get it natively integrated on ESPHome?

2 Likes

Thanks for this. Seems to be working! I’m getting readings, will have to see how accurate they are.

Thanks, this works perfectly.

I have this working on a multi sensor that senses presence, temp and light. I can get battery level and voltage, but I can’t seem to get discharge rate. When I try:
rate:
name: discharge rate
i get a message that rate is not valid. Any idea what i am missing?

Works! Thank you

If you find this topic by searching for Adafruit ESP32-S3 TFT Feather and wonder how to use the MAX17048 LiPoly Battery Monitor available over I2C on address 0x36 with ESPHome:

The MAX17048 is a more advanced version of the MAX17043, which is already available as a sensor in ESPHome. If you just want to see the battery level, it will suffice.

Example configuration:

esphome:
  name: adafruit-esp32-s3-tft-feather
  friendly_name: Adafruit ESP32-S3 TFT Feather

esp32:
  board: adafruit_feather_esp32s3
  variant: esp32s3
  framework:
    type: esp-idf

logger:

status_led:
  pin:
    number: GPIO13

i2c:
  sda: GPIO42
  scl: GPIO41
  scan: false # Will fail and delay 2 seconds on Feather design

sensor:
  - platform: max17043 # max17048
    id: battery
    address: 0x36
    battery_voltage:
      id: batt_voltage
      name: "Battery Voltage"
    battery_level:
      id: batt_percent
      name: "Battery Percentage"

spi:
  clk_pin: GPIO36
  mosi_pin: GPIO35
  miso_pin: GPIO37

power_supply:
  - id: tft_pwr_en
    enable_time: 0ms
    enable_on_boot: true
    pin: GPIO21

display:
  - platform: st7789v
    model: TTGO TDisplay 135x240
    backlight_pin: GPIO45
    cs_pin: GPIO7
    dc_pin: GPIO39
    reset_pin: GPIO40
    power_supply: tft_pwr_en
    rotation: 90
    update_interval: 1s
    lambda: |-
      auto blue = Color(0, 0, 255);
      auto yellow = Color(255, 255, 0);
      it.printf(10, 10, id(font1), yellow, "Battery:");
      it.printf(10, 30, id(font1),
                id(batt_voltage).has_state() ? "V: %.2f V" : "V: ---",
                id(batt_voltage).state);
      it.printf(10, 50, id(font1),
                id(batt_percent).has_state() ? "SOC: %.0f %%" : "SoC: ---",
                id(batt_percent).state);

font:
  - file: "gfonts://Roboto"
    id: robo14
    size: 14

It seems you cannot detect I2C during boot because the Feather is not designed with the ESPHome boot sequence in mind. But I2C devices still work after the boot sequence is complete, as long as you know the address.

I seem to get the ‘wire’ problem also. Has there been a solution for this yet?
This is my code:

esphome:
  name: laundry-leak
  friendly_name: laundry-leak
  includes:
    - MAX17048_component.h

esp32:
  board: esp32dev
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "...."

ota:
  - platform: esphome
    password: "...."

wifi: 
  manual_ip:
    static_ip: 192.168.1.xyz
    gateway: 192.168.1.1
    subnet: 255.255.255.0
    dns1: 192.168.1.lmn
  ssid: "xxx"
  password: "yyy"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "laundry-leak-AP"
    password: "rst"

captive_portal:

web_server:
  port: 80

binary_sensor:
  - platform: gpio
    pin: 
      number: GPIO19
      mode:
        input: true
        pullup: true
      inverted: true
    name: leak-sensor3
    filters:
        - delayed_on_off: 20ms
    icon: mdi:water  # Static icon (default)

# Configure I2C bus (required for MAX17048)
i2c:
  id: charger_bus
  sda: GPIO22
  scl: GPIO23
  scan: true

external_components:
  - source: github://Option-Zero/esphome-components@max17048
    components: [max17048]

sensor:
  - platform: max17048
    battery_voltage:
      name: Battery voltage
      id: batt_v
    battery_level:
      name: Battery level
    rate:
      name: Battery discharge rate
      id: batt_pct

text_sensor:
  - platform: version
    name: "ESPHome Version"
    id: laundry_leak_version

These are the errors I get:

In file included from src/main.cpp:36:
src/MAX17048_component.h: In member function 'virtual void MAX17048Sensor::setup()':
src/MAX17048_component.h:23:5: error: 'Wire' was not declared in this scope
   23 |     Wire.begin();
      |     ^~~~
src/MAX17048_component.h: In member function 'uint16_t MAX17048Sensor::read16(uint8_t)':
src/MAX17048_component.h:28:7: error: 'Wire' was not declared in this scope
   28 |       Wire.begin();
      |       ^~~~
*** [.pioenvs/laundry-leak/src/main.cpp.o] Error 1

Hi,

did you took a look at: [max17048] Add MAX17048 Li+ Cell Fuel Gauge IC by B48D81EFCC · Pull Request #13585 · esphome/esphome · GitHub
A few weeks ago I created a PR to add support for MAX17048.

external_components:
  - source: github://pr#13585
    components: [max17048]
    refresh: 1h

# Example config.yaml
sensor:
  - platform: max17048
    address: 0x36
    update_interval: 60s
    battery_voltage:
      name: "Battery Voltage"
    battery_level:
      name: "Battery Level"
    rate:
      name: "Battery Rate"

Feedback highly appreaciated