We've updated our popular PiOLED script for use with the Mini PiTFT, a 135x240 Color TFT add-on for your Raspberry Pi. This cute little display has two tactile buttons on GPIO pins that we'll use to make a simple user-interface display for your Pi-Hole.

Install 2x20 Header
If you are using a Pi Zero, you'll need to solder on or somehow attach a 2x20 header so you can plug in the Pi OLED. Use either a plain 2x20 header and solder it in using an iron + some solder...

Or you can use Hammer headers which do not need soldering.

Either way, you'll want to end up with something like this:
Setup Virtual Environment
Starting with the Bookworm version of Raspberry Pi OS, you will need to install your python modules in a virtual environment. Even on earlier versions, it's still recommended to install a virtual environment. You can find more information in the Python Virtual Environment Usage on Raspberry Pi guide. To Install and activate the virtual environment, use the following commands:
sudo apt install python3-venv python3 -m venv pihole --system-site-packages
You will need to activate the virtual environment every time the Pi is rebooted. To activate it:
source pihole/bin/activate
To deactivate, you can use deactivate
, but leave it active for now.
Python Setup
This guide assumes that you've gotten your Raspberry Pi up and running, have CircuitPython installed, and have installed CircuitPython libraries for the Mini PiTFT. If not, follow the steps in the guide below and come back to this page when you've completed them.
Update the stats.py program
Here's the new stats.py code which uses the Mini PiTFT.
Create a new file using nano ~pi/stats.py
and paste the code below in. Then, save the code.
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries # # SPDX-License-Identifier: MIT # -*- coding: utf-8 -*- # Import Python System Libraries import time import json import subprocess # Import Requests Library import requests #Import Blinka import digitalio import board # Import Python Imaging Library from PIL import Image, ImageDraw, ImageFont import adafruit_rgb_display.st7789 as st7789 API_TOKEN = "YOUR_API_TOKEN_HERE" api_url = "http://localhost/admin/api.php?summaryRaw&auth="+API_TOKEN # Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4): cs_pin = digitalio.DigitalInOut(board.CE0) dc_pin = digitalio.DigitalInOut(board.D25) reset_pin = None # Config for display baudrate (default max is 24mhz): BAUDRATE = 64000000 # Setup SPI bus using hardware SPI: spi = board.SPI() # Create the ST7789 display: disp = st7789.ST7789(spi, cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE, width=135, height=240, x_offset=53, y_offset=40) # Create blank image for drawing. # Make sure to create image with mode 'RGB' for full color. height = disp.width # we swap height/width to rotate it to landscape! width = disp.height image = Image.new('RGB', (width, height)) rotation = 90 # Get drawing object to draw on image. draw = ImageDraw.Draw(image) # Draw a black filled box to clear the image. draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0)) disp.image(image, rotation) # Draw some shapes. # First define some constants to allow easy resizing of shapes. padding = -2 top = padding bottom = height-padding # Move left to right keeping track of the current x position for drawing shapes. x = 0 # Alternatively load a TTF font. Make sure the .ttf font file is in the # same directory as the python script! # Some other nice fonts to try: http://www.dafont.com/bitmap.php font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 24) # Turn on the backlight backlight = digitalio.DigitalInOut(board.D22) backlight.switch_to_output() backlight.value = True # Add buttons as inputs buttonA = digitalio.DigitalInOut(board.D23) buttonA.switch_to_input() while True: # Draw a black filled box to clear the image. draw.rectangle((0, 0, width, height), outline=0, fill=0) # Shell scripts for system monitoring from here: # https://unix.stackexchange.com/questions/119126/command-to-display-memory-usage-disk-usage-and-cpu-load cmd = "hostname -I | cut -d\' \' -f1" IP = "IP: "+subprocess.check_output(cmd, shell=True).decode("utf-8") cmd = "hostname | tr -d \'\\n\'" HOST = subprocess.check_output(cmd, shell=True).decode("utf-8") cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'" CPU = subprocess.check_output(cmd, shell=True).decode("utf-8") cmd = "free -m | awk 'NR==2{printf \"Mem: %s/%s MB %.2f%%\", $3,$2,$3*100/$2 }'" MemUsage = subprocess.check_output(cmd, shell=True).decode("utf-8") cmd = "df -h | awk '$NF==\"/\"{printf \"Disk: %d/%d GB %s\", $3,$2,$5}'" Disk = subprocess.check_output(cmd, shell=True).decode("utf-8") cmd = "cat /sys/class/thermal/thermal_zone0/temp | awk \'{printf \"CPU Temp: %.1f C\", $(NF-0) / 1000}\'" # pylint: disable=line-too-long Temp = subprocess.check_output(cmd, shell=True).decode("utf-8") # Pi Hole data! try: r = requests.get(api_url) data = json.loads(r.text) DNSQUERIES = data['dns_queries_today'] ADSBLOCKED = data['ads_blocked_today'] CLIENTS = data['unique_clients'] except KeyError: time.sleep(1) continue y = top if not buttonA.value: # just button A pressed draw.text((x, y), IP, font=font, fill="#FFFF00") y += font.getbbox(IP)[3] draw.text((x, y), CPU, font=font, fill="#FFFF00") y += font.getbbox(CPU)[3] draw.text((x, y), MemUsage, font=font, fill="#00FF00") y += font.getbbox(MemUsage)[3] draw.text((x, y), Disk, font=font, fill="#0000FF") y += font.getbbox(Disk)[3] draw.text((x, y), Temp, font=font, fill="#FF00FF") y += font.getbbox(Temp)[3] else: draw.text((x, y), IP, font=font, fill="#FFFF00") y += font.getbbox(IP)[3] draw.text((x, y), HOST, font=font, fill="#FFFF00") y += font.getbbox(HOST)[3] draw.text((x, y), "Ads Blocked: {}".format(str(ADSBLOCKED)), font=font, fill="#00FF00") y += font.getbbox(str(ADSBLOCKED))[3] draw.text((x, y), "Clients: {}".format(str(CLIENTS)), font=font, fill="#0000FF") y += font.getbbox(str(CLIENTS))[3] draw.text((x, y), "DNS Queries: {}".format(str(DNSQUERIES)), font=font, fill="#FF00FF") y += font.getbbox(str(DNSQUERIES))[3] # Display image. disp.image(image, rotation) time.sleep(.1)
You'll notice it's very similar to the original stats.py, but we've added PiHole API support. Here's how we did that:
First up, Pi Hole stats are available through the web server, in JSON format, so we need to add web requests and JSON parsing to Python. Then set the URL for the API access, which is localhost (the same computer) and through the admin page:
# Import Python System Libraries import time import json import subprocess # Import Requests Library import requests api_url = 'http://localhost/admin/api.php'
We load up the nice DejaVuSans font here. Note that we have to have the full path of the file.
# Alternatively load a TTF font. Make sure the .ttf font file is in the # same directory as the python script! # Some other nice fonts to try: http://www.dafont.com/bitmap.php font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 24)
This is where we grab the API data. I put it in a try block, so it would retry in case the API access failed for some reason
# Pi Hole data! try: r = requests.get(api_url) data = json.loads(r.text) DNSQUERIES = data['dns_queries_today'] ADSBLOCKED = data['ads_blocked_today'] CLIENTS = data['unique_clients'] except KeyError: time.sleep(1) continue
If you want to print out different info, run this small script in python to see what is available:
import json import requests api_url = 'http://localhost/admin/api.php' r = requests.get(api_url) data = json.loads(r.text) print(data)
Since the MiniTFT has two tactile push-buttons, we modified the script to print out extra information from the Raspberry Pi when you press the top button.
if not buttonA.value: # just button A pressed draw.text((x, y), IP, font=font, fill="#FFFF00") y += font.getsize(IP)[1] draw.text((x, y), CPU, font=font, fill="#FFFF00") y += font.getsize(CPU)[1] draw.text((x, y), MemUsage, font=font, fill="#00FF00") y += font.getsize(MemUsage)[1] draw.text((x, y), Disk, font=font, fill="#0000FF") y += font.getsize(Disk)[1] draw.text((x, y), "DNS Queries: {}".format(DNSQUERIES), font=font, fill="#FF00FF") else: draw.text((x, y), IP, font=font, fill="#FFFF00") y += font.getsize(IP)[1] draw.text((x, y), HOST, font=font, fill="#FFFF00") y += font.getsize(HOST)[1] draw.text((x, y), "Ads Blocked: {}".format(str(ADSBLOCKED)), font=font, fill="#00FF00") y += font.getsize(str(ADSBLOCKED))[1] draw.text((x, y), "Clients: {}".format(str(CLIENTS)), font=font, fill="#0000FF") y += font.getsize(str(CLIENTS))[1] draw.text((x, y), "DNS Queries: {}".format(str(DNSQUERIES)), font=font, fill="#FF00FF") y += font.getsize(str(DNSQUERIES))[1]
Set API Token
Newer releases of Pi-Hole have added a authentication requirement for using the API. An API token must be provided when making an API request. This token was created when Pi-Hole was installed above. It can be found via the Web Admin interface under:
Settings > API > Show API token
Once found, update the stats.py code by changing this line:
API_TOKEN = "YOUR_API_TOKEN_HERE"
and replacing YOUR_API_TOKEN_HERE
with the API token found above. It'll be a big long string of numbers and letters that looks like gibberish. Leave the double quotes so it ends up looking something like this:
API_TOKEN = "1234567890ABCDEF1234567890ABCDEF"
Once you have the script saved, you can run it with
sudo ~pi/pihole/bin/python3 ~pi/stats.py
Look on the Mini PiTFT to make sure you see your IP address along with some statistics from Pi-Hole.
Pushing the top button should display extra statistics about the Pi such as its hostname, CPU load, memory utilization, disk usage, and DNS queries.
Lastly we just want to make this run at boot. We'll do that the easy way by editing /etc/rc.local with sudo nano /etc/rc.local and adding sudo ~pi/pihole/bin/python3 ~pi/stats.py & before exit 0
Then save and you can reboot to test it out
Page last edited January 21, 2025
Text editor powered by tinymce.