Reading your phone's battery level over Bluetooth BLE with Python Bleak

published on 08 February 2022

If you want to get started with Bluetooth BLE automation, start by trying to read your phone's battery level. It's a simple project that doesn't take long. In this post, we'll use this idea to test out the Python Bluetooth framework Bleak. Bleak appears to be the most active Bluetooth library for Python. I've never used it before writing this post. Let's see how it goes!

Installation

Starting with the ever useful pip, all we have to do is execute the following commands.

pip3 install bleak
pip3 install asyncio
pip3 install PyObjC

Notice, I snuck the asyncio library in there. Maybe you are familiar with asyncio. This one was new to me. Still, Bleak seems to rely on it heavily. Better make sure you have it up to date.

I also installed PyObjC. Bleak uses some native processes. Since I use a Mac, I needed to use some of the features of PyObjC to make this tutorial work. 

Note: For all Python related activity, do what you can to use a package manager. I once again wasted an hour fixing my Python environment because I have been slow to start using Homebrew. 

BLE Hello World

The Bleak documentation recommends running the following code as a first step.

import asyncio
from bleak import BleakScanner

async def main():
    devices = await BleakScanner.discover()
    for d in devices:
        print(d)

asyncio.run(main())

After running, I could see the following output.

5EE3C3B5-0E59-4ACF-9080-10364737EC72: Unknown
E664B1D0-8465-4E4A-A775-287AA433A889: Unknown
320AE1FA-4667-469D-9027-C60DA83BBEEA: Unknown
9D28F3B4-B433-4368-A9ED-A77E41EF842F: Unknown
93103EE6-DF08-4583-AEB7-623E0AA8CD8E: [TV] Samsung 7 Series (65)
19F01086-1C16-4699-A152-1F4C25ED5EED: Unknown
0BC39685-3D8B-43BC-B8CA-2C5C3C671921: Unknown

Looks good!

Wavecake makes this easier

Now's a good time to mention, completing the same experiment in Wavecake takes 17 seconds. Check out this gif where it happens in real time. Click here to try it out for yourself. Or continue on through the rest of the tutorial and check it out later!

read_battery-swpk5

Preparing the phone

Now we need to prepare the battery service. The preparation will be different for different phones. I'll be setting this up on an iPhone. I recommend using the nRF Connect app. You can find it for Android and iPhone in the app stores. 

First, navigate to the Peripheral tab. Then, add an advertiser.

IMG_4837-ifnlw

Next, add the battery service.

IMG_4838_small-ka2ah

The peripheral tab should look like this:

IMG_4839_small-fwy4z

Last, select the switch to enable advertising the battery service.

IMG_4840_small-eoj6o

Finding the phone

At this point, your phone is advertising a battery service. Using Bleak, you can find the phone and connect.

The Bleak library returns a device object from the discover routine. I want to explore this object so that I can implement a filter. This will help me find the exact device I'm looking for. Bluetooth devices are everywhere. Returning all the devices would be overwhelming. The device details are listed in the 'details' property of the devices object. The Bleak documentation states that these will be different depending on your OS. I'm using a Mac. Let's see what details the property returns.

I've modified the code to print the details property.

import asyncio
from bleak import BleakScanner

async def main():
    devices = await BleakScanner.discover()
    for d in devices:
        print(d.details)

asyncio.run(main())

Here's the output.

<CBPeripheral: 0x7fe4f86611d0, identifier = 188BDDC6-7017-41DA-929E-0839D95FA5A8, name = (null), mtu = 0, state = disconnected>
<CBPeripheral: 0x7fe4fb666860, identifier = CA3FB94D-0F06-4C4F-8F79-E87863312DCA, name = (null), mtu = 0, state = disconnected>
<CBPeripheral: 0x7fe4fb6681d0, identifier = 4E930BB7-63AB-4F06-B320-6493B67FFEEE, name = awesomecoolphone, mtu = 0, state = disconnected>
<CBPeripheral: 0x7fe4fb666c50, identifier = D215E0DE-02BF-4406-A640-2DB7B53D865A, name = (null), mtu = 0, state = disconnected>

From the output I see that to complete this quickly, I can look for my phone by name.

I can see that the output mentions CoreBluetooth (CBPeripheral), the Bluetooth library for Mac. I use the PyObjC utility library PyObjCTools to retrieve information from the output. I start by finding my phone by name.

import asyncio
from bleak import BleakScanner
from PyObjCTools import KeyValueCoding

async def main():
    devices = await BleakScanner.discover()
    for d in devices:
        if KeyValueCoding.getKey(d.details,'name') == 'awesomecoolphone':
            print('Found it')
            

asyncio.run(main())

Here's the output.

Found it

Huzzah! It worked!

Connecting to the phone

Now we're ready to connect to the phone. I'll add code to show the available services as well. Add a simple async function to the code.

import asyncio
from bleak import BleakScanner, BleakClient
from PyObjCTools import KeyValueCoding

async def main():
    devices = await BleakScanner.discover()
    for d in devices:
        if KeyValueCoding.getKey(d.details,'name') == 'awesomecoolphone':
            myDevice = d
            print('Found it')
    
    address = str(KeyValueCoding.getKey(myDevice.details,'identifier'))
    async with BleakClient(address) as client:
        svcs = await client.get_services()
        print("Services:")
        for service in svcs:
            print(service)


asyncio.run(main())

Here's the output.

Found it
Services:
d0611e78-bbb4-4591-a5f8-487910ae4366 (Handle: 10): Apple Continuity Service
9fa480e0-4967-4542-9390-d343dc5d04ae (Handle: 15): Apple Nearby Service
0000180f-0000-1000-8000-00805f9b34fb (Handle: 20): Battery Service
00001805-0000-1000-8000-00805f9b34fb (Handle: 24): Current Time Service
0000180a-0000-1000-8000-00805f9b34fb (Handle: 30): Device Information

Just like that! Battery service UUID 180F displays as available.

The Bluetooth spec defines the battery level characteristic UUID to be 2A19. Use the read characteristic function to read the battery level. 

import asyncio
from bleak import BleakScanner, BleakClient
from PyObjCTools import KeyValueCoding

uuid_battery_service = '0000180f-0000-1000-8000-00805f9b34fb'
uuid_battery_level_characteristic = '00002a19-0000-1000-8000-00805f9b34fb'

async def main():
    devices = await BleakScanner.discover()
    for d in devices:
        if KeyValueCoding.getKey(d.details,'name') == 'awesomecoolphone':
            myDevice = d
            print('Found it')
    
    address = str(KeyValueCoding.getKey(myDevice.details,'identifier'))
    async with BleakClient(address) as client:
        svcs = await client.get_services()
        battery_level = await client.read_gatt_char(uuid_battery_level_characteristic)
        print(int.from_bytes(battery_level,byteorder='big'))

asyncio.run(main())

The output.

Battery level:  42 %

Sweet! We did it. 

Conclusion

Hopefully, you can use this as a starting point to build from. Scripting gives you unlimited options to improve your Bluetooth development process. 

Read more