Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated for AirPodsPro2 #16

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
.idea
__pycache__
__pycache__
*.pkg.tar.zst
/pkg
/src
/AirStatusLinux
35 changes: 35 additions & 0 deletions PKGBUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Maintainer: Mansour Behabadi <mansour@oxplot.com>
# modified by blackbunt

pkgname=airstatus-git
pkgver=20230124.b123b95
pkgrel=1
pkgdesc="Check AirPods battery levels on Linux"
arch=('i686' 'x86_64')
url="https://github.com/blackbunt/AirStatusLinux"
license=('GPL')
depends=('python36' 'python-bleak')
makedepends=('git')
provides=('airstatus')
conflicts=('airstatus')
source=("git+https://github.com/blackbunt/AirStatusLinux.git"
"airstatus.service"
"time_ns.py"
)
sha256sums=('SKIP'
'13ea0ae4760febf5b5f01cc2c64e39ede61ba6cce3514d3c6e17cebe2b574ebc'
'aad9238ddaae6de9cfe57e643485da440af65de9fd86140ed9a90e9b0ca533d7')

pkgver() {
cd AirStatusLinux
printf "%s.%s" "$(git show -s --format=%cs | tr -d -)" "$(git rev-parse --short HEAD)"
}

package() {
install -Dm644 airstatus.service -t "${pkgdir}/usr/lib/systemd/system"

cd AirStatusLinux
install -Dm644 main.py "${pkgdir}/usr/lib/airstatus.py"
install -Dm644 time_ns.py "${pkgdir}/usr/lib/time_ns.py"
install -Dm644 LICENSE -t "${pkgdir}/usr/share/licenses/airstatus"
}
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# **AirStatus for Linux**
#### Check your AirPods battery level on Linux
forked from [delphiki/AirStatus](https://github.com/delphiki/AirStatus), I addded PKGBUILD for arch and updated the main file for newer Airpod models

#### What is it?
This is a Python 3.6 script, forked from [faglo/AirStatus](https://github.com/faglo/AirStatus) that allows you to check AirPods battery level from your terminal, as JSON output.
This is a Python 3.6 script, forked from [faglo/AirStatus](https://github.com/faglo/AirStatus) that allows you to check AirPods battery level from your terminal, as JSON output.

### Usage

Expand All @@ -17,6 +18,22 @@ Output will be stored in `output_file` if specified.
```
{"status": 1, "charge": {"left": 95, "right": 95, "case": -1}, "charging_left": false, "charging_right": false, "charging_case": false, "model": "AirPodsPro", "date": "2021-12-22 11:09:05"}
```
### Installing AirStatus as a service on Arch with Pacman

clone the Repo, make package and install the package

```
git clone https://github.com/blackbunt/AirStatusLinux
cd AirStatusLinux
makepkg
sudo pacman -U <package-name>.pkg.tar.zst
```
output of the service is located here:

```
/tmp/airstatus.out
```


### Installing as a service

Expand Down
121 changes: 121 additions & 0 deletions airpods.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash
#
# needs AirStatus installed and running
#
# displays airpod info
# get airpod device info
# get last line of output from /tmp/airstatus.out
function get_airpod_info {
airstatus=$(tail -n 1 /tmp/airstatus.out)
case=$(echo $airstatus | jq -r '.charge | .case')
left=$(echo $airstatus | jq -r '.charge | .left')
right=$(echo $airstatus | jq -r '.charge | .right')
ch_case=$(echo $airstatus | jq -r '.charging_case')
ch_left=$(echo $airstatus | jq -r '.charging_left')
ch_right=$(echo $airstatus | jq -r '.charging_right')
model=$(echo $airstatus | jq -r '.model')
date=$(echo $airstatus | jq -r '.date')
echo $left $right $case $ch_left $ch_right $ch_case $model $date
}

# if value is -1 then return " "
function fix_value {
value=$1
# if value is -1
if [ $value -eq -1 ]; then
value=" "
echo $value
# if value is gretaer equal than 1 and smaller equal to 100
elif [ $value -ge 1 ] && [ $value -le 100 ]; then
value=$(echo $value | awk '{printf "%3d", $1}')
echo $value
fi
}

function get_state {
#if value is "true" than return 1
#if value is "false" than return 0
value=$1
if [ "$value" = "true" ]; then
echo 1
elif [ "$value" = "false" ]; then
echo 0
fi
}

function display_airpods {
# has parameter for left, right, case
# has parameter for charging_left_charging_right, charging_case
# has parameter for model
# has parameter for date
data=$(get_airpod_info)
left=$(fix_value $(echo $data | awk '{print $1}'))
right=$(fix_value $(echo $data | awk '{print $2}'))
case=$(fix_value $(echo $data | awk '{print $3}'))
ch_left=$(echo $data | awk '{print $4}')
ch_right=$(echo $data | awk '{print $5}')
ch_case=$(echo $data | awk '{print $6}')
model=$(echo $data | awk '{print $7}')
date=$(echo $data | awk '{print $8}')

# if get_state is 1 than return ⚡️
# if get_state is 0 than return " "
if [ $(get_state $ch_left) -eq 1 ]; then
ch_left="⚡️"
elif [ $(get_state $ch_left) -eq 0 ]; then
ch_left=" "
fi

if [ $(get_state $ch_right) -eq 1 ]; then
ch_right="⚡️"
elif [ $(get_state $ch_right) -eq 0 ]; then
ch_right=" "
fi

if [ $(get_state $ch_case) -eq 1 ]; then
ch_case="⚡️"
elif [ $(get_state $ch_case) -eq 0 ]; then
ch_case=" "
fi

#echo $(get_airpod_info)

# if model is AirPodsPro2
if [ "$model" = "AirPodsPro2" ]; then
echo ""
echo ""
echo "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC4nYF5eXl5eXl5eXl5eXl5eXl5eXmAnLiAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC5eIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiJeYCAgCiAgICcuICcnICAgICAgICAgIC5gICAnICAgICAgICAuIl5eLCwsLCwsLCwsLCwsLCwsLCwsLCwsImBeXiAKICAnYCh4ISIsYCAgICAgIGAsOjpceCInLiAgICAgIGAsXiI6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6ImAiYAogLl5gPi9fOiwiYGAuIF5gIiI6IXQtYGAuICAgICAgYCJeLDo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6OjosXixgCiAgJywsImk7LCw+fSwuWy0sLCwhLF4iYCAgICAgICBgIl4sOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6OixeLGAKICAgIF5pPDohPmk6ICBeaT5pOzxpXiAgICAgICAgIGAiXiw6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Il4sYAogICAgJzpJICAgICAgICAgICAgbDouICAgICAgICAgYCxeLDo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6OjosXixgCiAgICBgO0kuICAgICAgICAgIC5JSScgICAgICAgICAuOjo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Oi4KICAgIGBpSS4gICAgICAgICAgLmxpYCAgICAgICAgICAnO2whIWxsbGxsbGxsbGxsbGxsbGxsbCEhbDsnIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYDpJIWlpaWlpaWlpaWlpaWlpaWkhSTpgICAgCg==" \
| base64 --decode
echo ""
echo " $ch_left $left % $ch_right $right % $ch_case $case %"
echo ""
# if model is Airpods1
elif [ "$model" = "AirPods1" ]; then
echo ""
echo ""
echo "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAuLicnJycnJycnJycnJycnJy4gICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYGAnJycnJycnJycnJycnYGBgYGAuICAgCiAgIC4gLicuLCcgICAgICxpLicuICAgICAgICAgICAgICAgIF5eLidgYGBgYGBgYGBgYGBgYGAgJyIuICAKICA6J0k6JydeXi4gICAnIiwuYF5sYCAgICAgICAgICAgICAgImAuYGBgYGBgYGBgYGBgYGBgXi4nIicgIAogIDo6aWwsSSwnYC4gIGAuJzo6O2ksLiAgICAgICAgICAgICAiYC5gYGBgYGBgYGBgYGBgYF5eLiciJyAgCiAgIF4sO0lJLF5gJyAuXmBebEk6YC4gICAgICAgICAgICAgICJgLmBgYGBgYGBgYF5eXl5eXl4uJyInICAKICAgICAgICAuXl5gIC5eXicgICAgICAgICAgICAgICAgICAgImAuYGBgYGBgYGBgXl5eXl5eXi4nIicgIAogICAgICAgIC5eXmAgLiJeJyAgICAgICAgICAgICAgICAgICAiYC5gYGBgYGBgYF5eXl5eXl5eLiciJyAgCiAgICAgICAgLl5eYCAuIl4nICAgICAgICAgICAgICAgICAgICJgLl5gYGBgYGBeXl5eXl5eXl4uJyInICAKICAgICAgICAuXl5gIC5eYCcgICAgICAgICAgICAgICAgICAgIl4uXmBgYGBgXl5eXl5eXl5eXi4nIicgIAogICAgICAgIC4iLF4gLiwsYCAgICAgICAgICAgICAgICAgICAiXi5gYGBgYGBgYGBgYGBgXl5eLiciLiAgCiAgICAgICAgJywhOiAuSTw6ICAgICAgICAgICAgICAgICAgIGAsYF5eXl5eXl5eXl5eXl5eXl5gIiIgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGA6Ozs7Ozs7Ozs7Ozs7Ozs7OjpeICAgIAo=" \
| base64 --decode
echo ""
echo " $ch_left $left % $ch_right $right % $ch_case $case %"
echo ""
fi
}

function is_connected {
# get bluetooth device name and check if it is AirPods......
bluetooth=$(bluetoothctl info | grep Name | grep -oP '(?<=Name: ).*' | grep -oP '(^AirPods)')
#echo $bluetooth
if [ "$bluetooth" = "AirPods" ]; then
return 1
else
return 0
fi
}

# if is_connected returns 1 then display_airpods
# if is_connected returns 0 then echo "AirPods: disconnected"
if is_connected; then
echo "AirPods: disconnected"
else
display_airpods
fi
11 changes: 11 additions & 0 deletions airstatus.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Unit]
Description=AirPods Battery Monitor

[Service]
ExecStart=/usr/bin/python3 /usr/lib/airstatus.py /tmp/airstatus.out
User=nobody
Restart=always
RestartSec=3

[Install]
WantedBy=default.target
141 changes: 141 additions & 0 deletions get_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
from bleak import discover
from asyncio import new_event_loop, set_event_loop, get_event_loop
from time import sleep
from binascii import hexlify
from json import dumps
from sys import argv
from datetime import datetime
import time_ns

# Configure update duration (update after n seconds)
UPDATE_DURATION = 1
MIN_RSSI = -60
AIRPODS_MANUFACTURER = 76
AIRPODS_DATA_LENGTH = 54
RECENT_BEACONS_MAX_T_NS = 10000000000 # 10 Seconds

recent_beacons = []


def get_best_result(device):
recent_beacons.append({
"time": time_ns.time_ns(),
"device": device
})
strongest_beacon = None
i = 0
while i < len(recent_beacons):
if (time_ns.time_ns() - recent_beacons[i]["time"] > RECENT_BEACONS_MAX_T_NS):
recent_beacons.pop(i)
continue
if (strongest_beacon == None or strongest_beacon.rssi < recent_beacons[i]["device"].rssi):
strongest_beacon = recent_beacons[i]["device"]
i += 1

if (strongest_beacon != None and strongest_beacon.address == device.address):
strongest_beacon = device

return strongest_beacon


# Getting data with hex format
async def get_device():
# Scanning for devices
devices = await discover()
for d in devices:
# Checking for AirPods
d = get_best_result(d)
if d.rssi >= MIN_RSSI and AIRPODS_MANUFACTURER in d.metadata['manufacturer_data']:
data_hex = hexlify(bytearray(d.metadata['manufacturer_data'][AIRPODS_MANUFACTURER]))
data_length = len(hexlify(bytearray(d.metadata['manufacturer_data'][AIRPODS_MANUFACTURER])))
if data_length == AIRPODS_DATA_LENGTH:
return data_hex
return False


# Same as get_device() but it's standalone method instead of async
def get_data_hex():
new_loop = new_event_loop()
set_event_loop(new_loop)
loop = get_event_loop()
a = loop.run_until_complete(get_device())
loop.close()
return a


# Getting data from hex string and converting it to dict(json)
# Getting data from hex string and converting it to dict(json)
def get_data():
raw = get_data_hex()

# Return blank data if airpods not found
if not raw:
return dict(status=0, model="AirPods not found")

flip: bool = is_flipped(raw)

# On 7th position we can get AirPods model, gen1, gen2, Pro or Max
if chr(raw[7]) == 'e':
model = "AirPodsPro"
elif chr(raw[7]) == 'f':
model = "AirPods2"
elif chr(raw[7]) == '2':
model = "AirPods1"
elif chr(raw[7]) == 'a':
model = "AirPodsMax"
elif chr(raw[7]) == '4':
model = "AirPodsPro2"
else:
model = f"unknown model, edit the main file @ line 77++, character = {chr(raw[7])}"

# Checking left AirPod for availability and storing charge in variable
status_tmp = int("" + chr(raw[12 if flip else 13]), 16)
left_status = (100 if status_tmp == 10 else (status_tmp * 10 + 5 if status_tmp <= 10 else -1))

# Checking right AirPod for availability and storing charge in variable
status_tmp = int("" + chr(raw[13 if flip else 12]), 16)
right_status = (100 if status_tmp == 10 else (status_tmp * 10 + 5 if status_tmp <= 10 else -1))

# Checking AirPods case for availability and storing charge in variable
status_tmp = int("" + chr(raw[15]), 16)
case_status = (100 if status_tmp == 10 else (status_tmp * 10 + 5 if status_tmp <= 10 else -1))

# On 14th position we can get charge status of AirPods
charging_status = int("" + chr(raw[14]), 16)
charging_left: bool = (charging_status & (0b00000010 if flip else 0b00000001)) != 0
charging_right: bool = (charging_status & (0b00000001 if flip else 0b00000010)) != 0
charging_case: bool = (charging_status & 0b00000100) != 0

# Return result info in dict format
return dict(status=1,
model=model,
date=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
raw=raw.decode("utf-8")
)


# Return if left and right is flipped in the data
def is_flipped(raw):
return (int("" + chr(raw[10]), 16) & 0x02) == 0


def run():
output_file = argv[-1]

while True:
data = get_data()

if data["status"] == 1:
json_data = dumps(data)
if len(argv) > 1:
f = open(output_file, "a")
f.write(json_data + "\n")
f.close()
else:
print(json_data)

sleep(UPDATE_DURATION)


if __name__ == '__main__':
run()
9 changes: 6 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from bleak import discover
from asyncio import new_event_loop, set_event_loop, get_event_loop
from time import sleep, time_ns
from time import sleep
from binascii import hexlify
from json import dumps
from sys import argv
from datetime import datetime
import time_ns

# Configure update duration (update after n seconds)
UPDATE_DURATION = 1
Expand All @@ -18,13 +19,13 @@

def get_best_result(device):
recent_beacons.append({
"time": time_ns(),
"time": time_ns.time_ns(),
"device": device
})
strongest_beacon = None
i = 0
while i < len(recent_beacons):
if(time_ns() - recent_beacons[i]["time"] > RECENT_BEACONS_MAX_T_NS):
if(time_ns.time_ns() - recent_beacons[i]["time"] > RECENT_BEACONS_MAX_T_NS):
recent_beacons.pop(i)
continue
if (strongest_beacon == None or strongest_beacon.rssi < recent_beacons[i]["device"].rssi):
Expand Down Expand Up @@ -82,6 +83,8 @@ def get_data():
model = "AirPods1"
elif chr(raw[7]) == 'a':
model = "AirPodsMax"
elif chr(raw[7]) == '4':
model = "AirPodsPro2"
else:
model = "unknown"

Expand Down
1 change: 1 addition & 0 deletions src/airstatus.service
Loading