Reading Aranet4 sensor data from Python

7 minute read

I got myself an Aranet4 device to monitor CO2 levels in my office.

Aranet4 CO2 monitor

The monitor gives pretty accurate readings and has very low energy demands (due to the e-ink display) (here’s the datasheet).

While the intended way to read the measurements is to use the mobile app, it’s always good to be able to access the raw data yourself; you get to store as many readings as you like (otherwise limited to past 7 days) and can analyse them in any way you want.

Get the readings using Python (via BLE)

The Aranet4 communicates with the app via Bluetooth Low Energy (BLE). So my plan was to find a BLE library for Python and then figure out how to get the current readings.

This turned out to be much easier than I thought it would, as there are already a couple of clients out there that identified the data format and GATT characteristics.

To get the current readings of the device essentially requires two steps:

  1. Figure out the device address
  2. Ask the device for the current data using the GATT characteristic UUID (I found it on this list).

Using bleak for the BLE communication we can first scan for nearby devices:

async def main():
    devices = await BleakScanner.discover()
    for device in devices:
        print(device.address, device.name)

asyncio.run(main())

The Aranet4 device will show up with the name (wait for it…) “Aranet4” and an address.

Next, we connect to it, ask for the current reading using the characteristic UUID mentioned above (f0cd3001-95da-4f4b-9ac8-aa55d312af0c) and interpret the returned data to get the individual readings (unsigned char/short to Python int): CO2, temperature, atmospheric pressure, humidity, battery level, status (the three levels shown on the device), measurement interval (default 5 min.), age (seconds since measurements were taken).

import asyncio
import struct
from bleak import BleakClient

CURRENT_READING = 'f0cd3001-95da-4f4b-9ac8-aa55d312af0c'

async def main(address):
    async with BleakClient(address) as client:
        data = await client.read_gatt_char(CURRENT_READING)
        readings = struct.unpack('<HHHBBBHH', data)
        print(f'co2: {readings[0]}ppm')
        print(f'temperature: {readings[1]/20}C')
        print(f'pressure: {readings[2]/10}hPa')
        print(f'humidity: {readings[3]}%')
        print(f'battery: {readings[4]}%')
        print(f'status: {readings[5]}')
        print(f'interval: {readings[6]}s')
        print(f'age: {readings[7]}s')

asyncio.run(main('<address identified during scan>'))

Running the code will give us:

co2: 1443ppm
temperature: 22.95C
pressure: 1031.3hPa
humidity: 38%
battery: 93%
status: 3
interval: 300s
age: 38s

… which means I should get some fresh air in :-)

Like to comment? Feel free to send me an email or reach out on Twitter.

Did this or another article help you? If you like and can afford it, you can buy me a coffee (3 EUR) ☕️ to support me in writing more posts. In case you would like to contribute more or I helped you directly via email or coding/troubleshooting session, you can opt to give a higher amount through the following links or adjust the quantity: 50 EUR, 100 EUR, 500 EUR. All links redirect to Stripe.