Skip to main content
Koen van Gilst
AboutLabTalksPhotography
←Back to Lab

Displaying Aranet4 CO2 readings on my website

January 7, 2024

2 min read

  • side-project
  • python
  • nextjs
Edit on GitHub
blueskymastodongithublinkedin
© 2026 Koen van Gilst
v. 9.0.0 | 299bb3d

Co2 monitor and it's a delightful little gadget. It's small and simple and after inserting the 2 AA batteries I could immediately see the CO2 levels on a clear little screen. I've been told it's also one of the most accurate monitors on the market. Using an unofficial Python library and my Raspberry Pi I was also able to show the measurements on my website. Follow along if you're interested in doing something similar yourself.

Requirements

The setup I'm using involves the following:

  • an Aranet4 CO2 monitor
  • a Raspberry Pi
  • a PostgreSQL database - I'm using Supabase
  • a Next.JS website

Getting started

Install the following packages on your Raspberry Pi:

sudo apt install bluetooth pi-bluetooth bluez blueman

Then use bluetoothctl to pair with your Aranet4:

sudo bluetoothctl
> scan on
> pair $MAC
> scan off

Python script

I adapted the Python script I found on this blog to get the current measurements of my Aranet4 monitor. I’m storing the data in a PostgreSQL database (hosted on Supabase) so that I can easily access it on my website. First, install the Python libraries aranet4 and psycopg2:

pip install aranet4 psycopg2

Then I use the following Python script to store the latest measurement in a PostgreSQL database:

import configparser
import time
import aranet4
import psycopg2
import os
from datetime import datetime

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

config = configparser.ConfigParser()
config.read(os.path.join(BASE_DIR, 'config.ini'))

NUM_RETRIES = int(config['SETTINGS']['num_retries'])
DEVICES = dict(config['DEVICES'])

DATABASE = {
    'database': config['DATABASE']['database'],
    'host': config['DATABASE']['host'],
    'user': config['DATABASE']['user'],
    'password': config['DATABASE']['password'],
    'port': config['DATABASE']['port']
}

def create_table(cursor):
    cursor.execute('''CREATE TABLE IF NOT EXISTS readings(
      device TEXT,
      timestamp INTEGER,
      temperature REAL,
      humidity INTEGER,
      pressure REAL,
      CO2 INTEGER,
      PRIMARY KEY(device, timestamp)
    )''')

def get_readings(mac):
    for attempt in range(NUM_RETRIES):
        try:
            return aranet4.client.get_current_readings(mac)
        except Exception as e:
            print('Attempt', attempt, 'failed, retrying:', e)
    return None

def insert_readings(cursor, name, readings):
    data = (
        name,
        time.mktime(datetime.now().timetuple()),
        readings.temperature,
        readings.humidity,
        readings.pressure,
        readings.co2
    )

    print('Inserting', data)

    cursor.execute(
        'INSERT INTO readings VALUES(%s, %s, %s, %s, %s, %s) ON CONFLICT DO NOTHING', data)

def main():
    with psycopg2.connect(**DATABASE) as conn:
        cursor = conn.cursor()

        create_table(cursor)
        conn.commit()

        for name, mac in DEVICES.items():
            readings = get_readings(mac)
            if readings is not None:
                insert_readings(cursor, name, readings)
                conn.commit()

if __name__ == "__main__":
    main()

The database credentials and configuration for the Aranet4 MAC addresses can be found in the config.ini file. Here's an example:

[DATABASE]
database = postgres
host = db.<your-id-here.supabase.co
user = postgres
password = <your db password here>
port = 5432

[DEVICES]
office = <your-aranet4-address-here>

[SETTINGS]
num_retries = 2

The script runs every minute in a cronjob to make sure the measurements are always up to date.

* * * * * /usr/bin/python3 /home/pi/Code/aranet4/load_data.py >> /home/pi/Code/aranet4/logs/load_data.txt 2>&1
0 9,21 * * * /usr/bin/python3 /home/pi/Code/aranet4/cleanup.py >> /home/pi/Code/aranet4/logs/cleanup.txt 2>&1

As you can see I've also included a cleanup script that deletes any entries older than a day. This is to make sure I don't run out of disk space on my free Supabase subscription.

Result

I'll leave out the front-end code in this tutorial as it is quite straightforward. For those interested in how it works it's a combination of a React component and a Next.JS api route that calls the following service.

The live demo has been retired. The tutorial and example code remain available above.