batteryinfo: A Rust-Powered Python Package for Laptop Battery Monitoring

Published April 29, 2025 · 13 min ·  

Rust Python Iot

Cover Image

Summary

Curious about my laptop’s battery details and unsatisfied with the limited info most tools provide, I built batteryinfo, a cross-platform Python package using Rust, which lets Python developers access comprehensive battery stats like state, capacity, voltage, and much more.

My journey into learning Rust led me down an interesting path: the desire to gain deeper insights into my laptop’s battery. Existing tools often provide a limited view, and I envisioned a more comprehensive solution accessible through Python. This led to the creation of batteryinfo, a cross-platform Python package built upon the excellent Rust battery crate. This module empowers Python developers to go beyond simple battery percentages and access a wealth of information, including status, capacity, temperature, and even more granular details. Let’s explore how batteryinfo can provide you with a richer understanding of your laptop’s power source.

High-level technical details

My Rust-powered Python package is called batteryinfo and the source code can be found on GitHub here.

I leveraged the battle-tested Rust battery crate and provided Python bindings to this crate in lieu of inventing yet another way to fetch laptop battery information.

The package was created using the following Rust tools:

  • PyO3: a Rust library that facilitates the creation of Python bindings for Rust code. It allows you to write code in Rust and then make that code callable from Python.
  • Maturin: a build tool specifically designed for developing and publishing Python packages that include Rust extensions built with PyO3. It simplifies the process of building, packaging, and distributing these hybrid Python-Rust projects.

I wrote the Rust code for this Python package using VS Code on my Fedora system. From there, I successfully built Python wheels for both Linux natively and Windows through cross-compilation. Perhaps I will create a separate post in the future outlining how all of this was accomplished.

Installing

I uploaded batteryinfo to PyPI, enabling it to be installed and used easily:

pip install batteryinfo

Getting started with the batteryinfo Python package

Here are some examples of how to use batteryinfo in Python:

To get started, import the package:

import batteryinfo

We can then instantiate a battery object:

battery = batteryinfo.Battery()

Available battery properties

The following properties are available on the battery object:

  • vendor: The vendor of the battery (optional).
  • model: The model of the battery (optional).
  • serial_number: The serial number of the battery (optional).
  • technology: The technology of the battery.
  • percent: The percentage of the battery that is full (as a Measurement object).
  • state: The state of the battery (Charging, Discharging, Full, Empty, Unknown).
  • capacity: The capacity of the battery (as a Measurement object).
  • temperature: The temperature of the battery (as a Measurement object).
  • cycle_count: The cycle count of the battery.
  • energy: The current energy of the battery (as a Measurement object).
  • energy_full: The full energy of the battery (as a Measurement object).
  • energy_full_design: The design energy of the battery (as a Measurement object).
  • energy_rate: The energy rate of the battery (as a Measurement object).
  • voltage: The voltage of the battery (as a Measurement object).
  • time_to_empty: The time to empty the battery.
  • time_to_full: The time to fully charge the battery.

Here’s some corresponding Python code to make it more concrete. Note that battery.temperature will return None if this property is not available and thus we can substitute our own value.

print(f"Vendor: {battery.vendor}")
print(f"Model: {battery.model}")
print(f"Serial Number: {battery.serial_number}")
print(f"Technology: {battery.technology}")
print(f"Percent Full: {battery.percent}")
print(f"State: {battery.state}")
print(f"Capacity: {battery.capacity}")
print(f"Temperature: {battery.temperature if battery.temperature else 'N/A'}")
print(f"Cycle Count: {battery.cycle_count}")
print(f"Energy: {battery.energy}")
print(f"Energy Full: {battery.energy_full}")
print(f"Energy Full Design: {battery.energy_full_design}")
print(f"Energy Rate: {battery.energy_rate}")
print(f"Voltage: {battery.voltage}")
print(f"Time to Empty: {battery.time_to_empty}")
print(f"Time to Full: {battery.time_to_full}")

Battery constructor parameters

While a Battery object can be created with default settings using battery = batteryinfo.Battery(), its behavior can also be customized during instantiation, specifically regarding the time_format and temperature units (temp_unit).

The Battery constructor accepts the following parameters:

  • index (optional): The index of the battery to interact with. Default is 0. An index of 1 is used if you have a second battery in your system.
  • time_format (optional): The format to display time. Possible values are:
    • TimeFormat.Seconds: Display time in seconds.
    • TimeFormat.Minutes: Display time in minutes.
    • TimeFormat.Human: Display time in a human-readable format. For example, 1h,25m,52s. (Default)
  • temp_unit (optional): The unit to display temperature. Possible values are:
    • TempUnit.DegC: Display temperature in degrees Celsius.
    • TempUnit.DegF: Display temperature in degrees Fahrenheit. (Default)
  • refresh_interval (optional): The interval in milliseconds to refresh battery information. Default is 500 milliseconds.

This allows for tailored Battery object creation, as illustrated here:

# Create an instance of Battery with specific time format and temperature unit
battery = batteryinfo.Battery(index=0, time_format=batteryinfo.TimeFormat.Human, temp_unit=batteryinfo.TempUnit.DegC)

# Create an instance of Battery with specific time format, temperature unit, and refresh interval
battery = batteryinfo.Battery(time_format=batteryinfo.TimeFormat.Human, temp_unit=batteryinfo.TempUnit.DegF, refresh_interval=600)

Setting the refresh interval

The refresh_interval controls how frequently the battery data is updated, defaulting to 500 milliseconds. If needed, you can configure this parameter during object creation or later. It’s only relevant for applications that repeatedly query battery information, as a single-use instance won’t benefit from it.

Note: The 500ms refresh_interval (or value you have requested) acts as a cache timeout. When you request battery data like voltage, the system updates the values only if they’re older than 500ms. If they’re more recent, the cached values are returned.

Example usage:

# Passing refresh_interval in the constructor
battery = batteryinfo.Battery(refresh_interval=1000)

# Setting refresh_interval after creating the Battery object
battery.refresh_interval = 1000

There is also an option to manually refresh the battery information, but the refresh_interval (and likely the default value of 500 ms) will accomplish the goal well in most situations.

battery.refresh()

Measurement object

The Measurement object has the following properties and methods:

  • value: The value of the measurement.
  • units: The units of the measurement.

Example usage:

# This first option provides the value and the units together.
print(f"Percent full: {battery.percent}")
# Provides the numeric value on its own so it can be used for comparisons
# and calculations.
print(f"Percent full raw value: {battery.percent.value}")
# Provides the units of measure such "%" or "V" for volts.
print(f"Percent full units: {battery.percent.units}")

Output

Percent full: 88.2%
Percent full raw value: 88.2
Percent full units: %

Enums

The following enums are available:

TimeFormat

  • Seconds: Display time in seconds.
  • Minutes: Display time in minutes.
  • Human: Display time in a human-readable format.

TempUnit

  • DegC: Display temperature in degrees Celsius.
  • DegF: Display temperature in degrees Fahrenheit.

Using the as_dict method

The as_dict method returns all battery information as a Python dictionary. For fields represented by Measurement objects, the method returns a tuple (value, units).

# Get all battery information as a dictionary
battery_info = battery.as_dict()

# Print the dictionary
print(battery_info)

# Example output:
# {
#     "vendor": "BatteryVendor",
#     "model": "BatteryModel",
#     "serial_number": "123456789",
#     "technology": "Li-ion",
#     "percent": (71.1, "%"),
#     "state": "Charging",
#     "capacity": (95.0, "%"),
#     "temperature": (86.2, "°F"),
#     "cycle_count": 300,
#     "energy": (50.0, "Wh"),
#     "energy_full": (60.0, "Wh"),
#     "energy_full_design": (65.0, "Wh"),
#     "energy_rate": (10.0, "W"),
#     "voltage": (12.5, "V"),
#     "time_to_empty": None,
#     "time_to_full": "1h,5m,19s",
#     "battery_index": 0
# }

# Access specific fields
print("Vendor:", battery_info.get("vendor"))
print("Percent:", battery_info.get("percent"))  # Example: (71.1, "%")
print("Energy:", battery_info.get("energy"))    # Example: (50.0, "Wh")

Examples

Display battery information based on state

In this first example, we provide key battery info in a terse format including the battery percent full, whether we are charging or discharging, and the time to full/empty in a human time format that is easy to understand:

import batteryinfo

battery = batteryinfo.Battery()

state = battery.state
percent = f"{battery.percent.value}%"
if state == "Charging":
    time_to_full = battery.time_to_full
    print(f"Battery: {percent} (⇡ charging - full in {time_to_full})")
elif state == "Discharging":
    time_to_empty = battery.time_to_empty
    print(f"Battery: {percent} (⇣ discharging - empty in {time_to_empty})")
elif state == "Full":
    print(f"Battery: {percent} (✓ full)")
else:
    print(f"Battery: {percent} (state: {state})")

Example output:

Battery: 70.4% (⇣ discharging - empty in 2h,40m,38s)

Monitor battery percent full and save to a CSV file

In this example, we record the battery’s charge percentage and status (such as “Charging” or “Discharging”) to a CSV file every 60 seconds:

import batteryinfo
from datetime import datetime
import csv
import time
import os

CSV_FILE = "battery_log.csv"
INTERVAL_SECONDS = 60  # How often to check battery status

# Create CSV file with headers if it doesn't exist
if not os.path.exists(CSV_FILE):
    with open(CSV_FILE, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(['timestamp', 'percent', 'state'])

# Initialize battery
battery = batteryinfo.Battery()

try:
    while True:
        state = battery.state
        percent = battery.percent.value
        timestamp = datetime.now().isoformat()

        with open(CSV_FILE, 'a', newline='') as f:
            writer = csv.writer(f)
            writer.writerow([timestamp, percent, state])

        print(f"{timestamp} - Battery: {percent}% ({state})")

        time.sleep(INTERVAL_SECONDS)

except KeyboardInterrupt:
    print("\nMonitoring stopped by user")

After running the script, you’ll have a CSV log that looks like this:

| timestamp                  | percent | state       |
|----------------------------|---------|-------------|
| 2025-04-28T19:20:17.481182 | 97.0    | Discharging |
| 2025-04-28T19:21:17.503531 | 96.8    | Discharging |
| 2025-04-28T19:22:17.503531 | 96.7    | Discharging |

This simple logging approach is a great starting point. Although CSV is easy to work with, you could also store battery data in more robust formats like SQLite or TimescaleDB for larger or more complex monitoring tasks.

Display a battery progress bar

While we’re at it, we can enhance the user experience by displaying a simple battery progress bar in the terminal. This visual indicator turns green when charging and red if the battery level drops below a specified threshold:

import batteryinfo
import os

# ANSI color codes
GREEN = "\033[32m"
RED = "\033[31m"
WHITE = "\033[37m"
RESET = "\033[0m"

def create_battery_bar(percent: float, charging: bool = False, width: int = None,
 low_threshold: float = 20.0) -> str:
    """Create a colored battery progress bar visualization.

    Args:
        percent: Battery percentage (0-100)
        charging: Whether the battery is charging
        width: Width of the progress bar in characters. If None, uses terminal
               width - 35
        low_threshold: Percentage below which to show red (default 20%)

    Returns:
        String representation of the battery bar with color
    """
    if width is None:
        term_width = os.get_terminal_size().columns
        width = term_width - 35

    filled = int(percent / 100 * width)

    # Choose color based on state and level
    if percent <= low_threshold:
        color = RED
    elif charging:
        color = GREEN
    else:
        color = WHITE

    bar = color + '=' * filled +  RESET + '-' * (width - filled)
    return f"[{bar}]"

# Initialize battery
battery = batteryinfo.Battery()

state = battery.state
percent = battery.percent.value
charging = state == "Charging"

battery_bar = create_battery_bar(percent, charging)
print(f"Battery: {battery_bar} {percent:.1f}% ({state})")

The output looks like this:

Battery: [===================================------------] 74.4% (Discharging)

This simple but effective visual indicator adds a user-friendly touch to battery monitoring in the terminal, making it easier to spot charging status or low battery at a glance.

Alternatives

As an alternative to my batteryinfo module, you can use the sensors_battery function from psutil.

import psutil

battery = psutil.sensors_battery()
print(battery)

The output returned is a named tuple called sbattery consisting of the battery percent, seconds left before the battery is empty, and a boolean indicating if the power is plugged in (i.e. the battery is charging).

sbattery(percent=92.64834843612978, secsleft=28069, power_plugged=False)

It looks like the precision on that battery percentage is pretty high. If you are familiar with significant figures, you can be assured that we can ignore most of those digits to the right of the decimal place since the battery percen.

The batteryinfo Python module I have created contains a much richer set of information related to a given battery in a laptop. If we needed to create a drop in replacement for psutil.sensors_battery(), we could do so as shown in the code below.

While most would not have a need to emulate psutil.sensors_battery(), this provides another example of using batteryinfo including changing the time format to seconds to specify how long before the laptop battery is completely empty.

from collections import namedtuple
from typing import Optional
from enum import IntEnum
import batteryinfo

# Create a named tuple similar to psutil's sbattery
sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged'])

class BatteryTime(IntEnum):
    POWER_TIME_UNKNOWN = -1
    POWER_TIME_UNLIMITED = -2

def _parse_time(time_str: Optional[str]) -> int:
    """Parse time string to seconds."""
    if not time_str:
        return BatteryTime.POWER_TIME_UNKNOWN
    try:
        return int(float(time_str.split()[0]))
    except (ValueError, IndexError):
        return BatteryTime.POWER_TIME_UNKNOWN

def sensors_battery() -> Optional[sbattery]:
    """Get battery information in psutil-compatible format.

    Returns:
        Named tuple with fields: percent, secsleft, power_plugged
        - percent: battery percentage (0-100)
        - secsleft: seconds left for charging/discharging, -1 if unknown
        - power_plugged: True if plugged in, False if not

        Returns None if no battery is present
    """
    try:
        battery = batteryinfo.Battery(time_format=batteryinfo.TimeFormat.Seconds)
        percent = battery.percent.value
        power_plugged = battery.state in ("Charging", "Full")

        # Handle time remaining logic
        if power_plugged:
            secsleft = BatteryTime.POWER_TIME_UNLIMITED
        elif battery.state == "Discharging":
            secsleft = _parse_time(battery.time_to_empty)
        else:
            secsleft = BatteryTime.POWER_TIME_UNKNOWN

        return sbattery(percent=percent, secsleft=secsleft, power_plugged=power_plugged)
    except Exception:
        return None


battery = sensors_battery()
if battery:
    print(battery)
else:
    print("No battery found")

This successfully emulates psutil. In the case of discharging the output looks like this:

sbattery(percent=76.2, secsleft=13261, power_plugged=False)

For charging, the output also emulates psutil and looks like this since the secsleft until empty is obviously undefined:

sbattery(percent=76.3, secsleft=<BatteryTime.POWER_TIME_UNKNOWN: -1>, power_plugged=False)

Bonus: Notes on how to cross compile Rust-based Python modules

As a bonus, I wanted to share some insights into how I managed to cross-compile the Python wheels for Windows directly from my primary Linux development machine. This process allows for building software for a different operating system without needing to switch environments. Here’s a breakdown of the steps involved:

First, to enable cross-compilation for Windows on a Fedora system (or a similar Red Hat-based distribution), you’ll need to install the MinGW-w64 toolchain. This provides the necessary compiler and linker for targeting Windows:

sudo dnf install mingw64-gcc-c++

Next, since batteryinfo is written in Rust, we need to ensure that our Rust toolchain (rustup) is aware of the target architecture for Windows. You can list the currently installed targets using:

rustup target list

If the x86_64-pc-windows-gnu target (for 64-bit Windows) is not listed as installed, you’ll need to add it:

rustup target add x86_64-pc-windows-gnu

It’s also a good practice to be aware of the Python version you are targeting. You can check your system’s default Python version with:

python -V

In my case, I specifically wanted to ensure compatibility with Python 3.13 for verification purposes:

python3.13

The next crucial step involves configuring the Rust project (Cargo.toml) to properly build a Python extension. This is where the PyO3 library comes into play. You’ll need to edit your Cargo.toml file and ensure that the pyo3 dependency includes the extension-module and generate-import-lib features. Additionally, I’ve included the abi3-py311 feature, which enables the creation of a multi-version Python wheel compatible with Python 3.11 and later:

pyo3 = { version = "0.24.0", features = ["extension-module", "generate-import-lib", "abi3-py311"] }

Before building the Windows wheel, it’s important to activate your project’s virtual environment. This ensures that the build process uses the correct Python interpreter and libraries. Once the virtual environment is active, you can use Maturin, the build tool for PyO3 projects, to cross-compile for Windows. The --target flag specifies the target architecture:

# ensure in virtual env
maturin build --release --target x86_64-pc-windows-gnu

Finally, to also build a native Linux wheel for distribution, you can simply run the maturin build command without specifying a target. This will build a wheel compatible with your current Linux environment:

# build whl for linux too
maturin build --release

This series of steps allowed me to generate Python wheels for both Linux and Windows from a single Linux development setup, streamlining the distribution of the batteryinfo package across different operating systems.

Conclusion

Ultimately, batteryinfo aims to empower Python developers with comprehensive access to their laptop’s battery health and status across different platforms. This package leverages the power of Rust and provides a richer set of data compared to standard alternatives, allowing for more sophisticated monitoring and power management within your Python applications. Whether you’re building system utilities, enhancing user interfaces, or simply curious about your battery’s performance, batteryinfo offers the tools you need to gain deeper insights.

Share this Article