A simple, Unix-philosophy command-line tool to check AirPods battery status on Linux.
There are projects similar to this, notably different Python implementations called AirStatus (windows original, linux fork 1, linux fork 2). They work pretty well but:
- Require Python runtime + dependencies
- Do more than one thing (continuous monitoring, fancy UI, file logging)
This Rust implementation:
- ✅ Does one thing: outputs battery status
- ✅ JSON output (composable with jq and other Unix tools)
- ✅ Single compiled binary (no runtime dependencies)
- ✅ Fast (<3 second scan time)
AirPods broadcast their battery information via Bluetooth Low Energy (BLE) advertising packets. Specifically, they include manufacturer-specific data in these broadcasts:
- Manufacturer ID:
0x004c(Apple Inc.) - Data Length: 27 bytes
- Data Structure:
- Bytes 3-4: Model identifier (2-byte identifier)
0x0220= AirPods 10x0F20= AirPods 20x1320= AirPods 30x1920= AirPods 40x0E20= AirPods Pro0x1420/0x2420= AirPods Pro 20x2720= AirPods Pro 30x0A20/0x1F20= AirPods Max
- Byte 5: Flip flag (bit 5 at 0x20 determines left/right orientation)
- Byte 6: Left and right earbud battery (nibble encoded, may be flipped)
- Byte 7: Case battery (low nibble) + Charging status flags (high nibble)
- Bytes 3-4: Model identifier (2-byte identifier)
The battery level is stored as a single hex digit (0-F):
10(0xA) = 100%0-9= (value × 10) + 5%15(0xF) = Not available/unknown
AirPods don't maintain a persistent BLE connection when idle - they just broadcast advertising packets periodically. This tool:
- Scans for BLE devices for up to 3 seconds (polling every 100ms)
- Filters for Apple manufacturer data (ID
0x004c) - Validates the 27-byte packet length
- Parses the packet and outputs the battery information
- Stops scanning as soon as AirPods are found
- Linux with BlueZ
- Bluetooth adapter (BLE capable)
- Rust toolchain (for building)
After cloning the repo, run the following command (this will create the "podpower" binary in ~/.cargo/bin/podpower):
cargo install --path .# JSON output for in-ear AirPods (standard models and Pro)
$ podpower
{
"type": "in_ear",
"model": "AirPods Pro",
"battery": 85,
"components": [
{
"name": "left",
"battery": 85,
"charging": false
},
{
"name": "right",
"battery": 90,
"charging": false
},
{
"name": "case",
"battery": 45,
"charging": false
}
]
}
# JSON output for AirPods Max (over-ear headphones)
$ podpower
{
"type": "over_ear",
"model": "AirPods Max",
"battery": 95,
"components": [
{
"name": "headphones",
"battery": 95,
"charging": false
}
]
}
# Get the main battery level (works for all AirPods types)
$ podpower | jq '.battery'
85
# Pipe through jq for formatted output
$ podpower | jq -r '"\(.model): \(.battery)%"'
AirPods Pro: 85%
# Get individual component battery levels
$ podpower | jq '.components[] | select(.name=="left") | .battery'
85
# Custom format for in-ear with all components
$ podpower | jq -r '"\(.model): L=\(.components[] | select(.name=="left") | .battery)% R=\(.components[] | select(.name=="right") | .battery)% Case=\(.components[] | select(.name=="case") | .battery)%"'
AirPods Pro: L=85% R=90% Case=45%0- Success (AirPods found and data retrieved)1- AirPods not found or error occurred
"custom/airpods": {
"exec": "podpower | jq -r '.battery // empty'",
"interval": 30,
"format": " {}%"
}Or with different icons based on type:
"custom/airpods": {
"exec": "podpower | jq -r 'if .type == \"in_ear\" then \"👂 \" + (.battery | tostring) + \"%\" elif .type == \"over_ear\" then \"🎧 \" + (.battery | tostring) + \"%\" else empty end'",
"interval": 30
}Simple version (works for all types):
#!/bin/bash
if status=$(podpower); then
battery=$(echo "$status" | jq -r '.battery // "?"')
model=$(echo "$status" | jq -r '.model')
echo "$model: $battery%"
else
echo "AirPods not found"
fiDetailed version (showing all components):
#!/bin/bash
if status=$(podpower); then
model=$(echo "$status" | jq -r '.model')
echo "$model:"
echo "$status" | jq -r '.components[] | " \(.name): \(.battery)%\(if .charging then " (charging)" else "" end)"'
else
echo "AirPods not found"
fi#!/bin/bash
# ~/.config/i3blocks/airpods
podpower | jq -r 'if .type == "in_ear" then "👂 \(.battery)%" elif .type == "over_ear" then "🎧 \(.battery)%" else "" end' || echo ""If you get D-Bus permission errors, make sure your user is in the bluetooth group:
sudo usermod -aG bluetooth $USER
# Log out and back in- Make sure AirPods are out of the case or the case is open
- Ensure they're in range and Bluetooth is enabled
- Try increasing
SCAN_TIMEOUT_SECSinsrc/main.rsif the scan is too short (default is 3 seconds)
If you see an error about "Bluetooth scan already in progress":
sudo systemctl restart bluetooth# Check if BlueZ is running
systemctl status bluetooth
# List adapters
bluetoothctl listInspired by:
- AirStatus - Python implementation
- cApod - Android implementation with detailed model identifiers
- Various reverse engineering efforts of Apple's BLE protocol
MIT