Powered by AppSignal & Oban Pro

Battery and charging

priv/samples/04_battery.livemd

Battery and charging

Overview

Linux describes every power source under /sys/class/power_supply, one directory per supply and one small text file per value. The phone has three:

Supply What it is
qg-battery The battery's fuel gauge: level, voltage, current, temperature
pmi632-charger The charger in the PMI632 power chip
tcpm-source-psy-… The USB-C port
File.ls!("/sys/class/power_supply")

Reading a value

Each value is a file. This helper reads one and turns numbers into integers:

read = fn supply, name ->
  [dir] = Path.wildcard("/sys/class/power_supply/#{supply}*")

  case File.read(Path.join(dir, name)) do
    {:ok, text} ->
      text = String.trim(text)

      case Integer.parse(text) do
        {number, ""} -> number
        _ -> text
      end

    {:error, reason} ->
      {:error, reason}
  end
end

read.("qg-battery", "capacity")

The battery at a glance

The kernel reports voltages in microvolts (µV), currents in microamps (µA) and the temperature in tenths of a degree.

%{
  level: "#{read.("qg-battery", "capacity")} %",
  status: read.("qg-battery", "status"),
  health: read.("qg-battery", "health"),
  voltage: "#{Float.round(read.("qg-battery", "voltage_now") / 1_000_000, 2)} V",
  current: "#{div(read.("qg-battery", "current_now"), 1000)} mA",
  temperature: "#{read.("qg-battery", "temp") / 10} °C",
  technology: read.("qg-battery", "technology")
}

status is "Charging", "Discharging", "Not charging" or "Full".

Is it plugged in?

%{
  cable_plugged_in: read.("pmi632-charger", "online") == 1,
  charger_status: read.("pmi632-charger", "status"),
  charger_input: "#{Float.round(read.("pmi632-charger", "voltage_now") / 1_000_000, 2)} V",
  usb_c_mode: read.("tcpm-source-psy", "usb_type")
}

In usb_c_mode the value in brackets is the one in use.

How long will it last?

The battery was designed to hold 3000 mAh. The level tells how much of that is left, and the average current tells how fast it is going in or out. Dividing one by the other gives a rough time.

design_mah = div(read.("qg-battery", "charge_full_design"), 1000)
level = read.("qg-battery", "capacity")
current_ma = abs(div(read.("qg-battery", "current_avg"), 1000))
status = read.("qg-battery", "status")

left_mah = design_mah * level / 100

hours =
  cond do
    current_ma == 0 -> nil
    status == "Charging" -> (design_mah - left_mah) / current_ma
    true -> left_mah / current_ma
  end

case {status, hours} do
  {_, nil} -> "No current flowing right now."
  {"Charging", h} -> "About #{Float.round(h, 1)} hours to full."
  {_, h} -> "About #{Float.round(h, 1)} hours left at the current drain."
end

This is an estimate: an ageing battery holds less than its design capacity, and the charger slows down as the battery gets full.

Live view

Plug and unplug the USB cable while this runs for thirty seconds.

frame = Kino.Frame.new() |> Kino.render()

# 30 rounds; each reads the battery, then waits one second.
for _round <- 1..30 do
  text =
    "#{read.("qg-battery", "capacity")} %   " <>
      "#{read.("qg-battery", "status")}   " <>
      "#{div(read.("qg-battery", "current_now"), 1000)} mA   " <>
      "#{Float.round(read.("qg-battery", "voltage_now") / 1_000_000, 2)} V"

  Kino.Frame.render(frame, Kino.Markdown.new("`#{text}`"))
  Process.sleep(1000)
end

:done

Everything the kernel reports

dir = hd(Path.wildcard("/sys/class/power_supply/qg-battery"))

for name <- Enum.sort(File.ls!(dir)), File.regular?(Path.join(dir, name)), name != "uevent" do
  {name, read.("qg-battery", name)}
end

Try the same with "pmi632-charger".