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 aMeasurement
object).state
: The state of the battery (Charging, Discharging, Full, Empty, Unknown).capacity
: The capacity of the battery (as aMeasurement
object).temperature
: The temperature of the battery (as aMeasurement
object).cycle_count
: The cycle count of the battery.energy
: The current energy of the battery (as aMeasurement
object).energy_full
: The full energy of the battery (as aMeasurement
object).energy_full_design
: The design energy of the battery (as aMeasurement
object).energy_rate
: The energy rate of the battery (as aMeasurement
object).voltage
: The voltage of the battery (as aMeasurement
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 is0
. An index of1
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 is500
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.