Skip to content

Commit 07967a2

Browse files
authored
Merge pull request #3049 from adafruit/pi-hole-stats
Updates for Pi Hole 6.x release New API from Pi Hole Dropped API authentication Mini PiTFT library changes Pi OLED library changes
2 parents 8d0262f + 5eff1ad commit 07967a2

File tree

2 files changed

+109
-105
lines changed

2 files changed

+109
-105
lines changed
Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,34 @@
11
# SPDX-FileCopyrightText: 2019 Brent Rubell for Adafruit Industries
2+
# SPDX-FileCopyrightText: 2025 Mikey Sklar for Adafruit Industries
23
#
34
# SPDX-License-Identifier: MIT
45

6+
# Copyright (c) 2017 Adafruit Industries
7+
# Author: Brent Rubell, Mikey Sklar
8+
#
9+
# Permission is hereby granted, free of charge, to any person obtaining a copy
10+
# of this software and associated documentation files (the "Software"), to deal
11+
# in the Software without restriction, including without limitation the rights
12+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
# copies of the Software, and to permit persons to whom the Software is
14+
# furnished to do so, subject to the following conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be included in
17+
# all copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
# THE SOFTWARE.
26+
27+
# This example is for use on (Linux) computers that are using CPython with
28+
# Adafruit Blinka to support CircuitPython libraries. CircuitPython does
29+
# not support PIL/pillow (python imaging library)!
30+
31+
532
# -*- coding: utf-8 -*-
633
# Import Python System Libraries
734
import time
@@ -19,11 +46,10 @@
1946
from PIL import Image, ImageDraw, ImageFont
2047
import adafruit_rgb_display.st7789 as st7789
2148

22-
API_TOKEN = "YOUR_API_TOKEN_HERE"
23-
api_url = "http://localhost/admin/api.php?summaryRaw&auth="+API_TOKEN
49+
API_URL = "http://localhost/api/stats/summary"
2450

2551
# Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4):
26-
cs_pin = digitalio.DigitalInOut(board.CE0)
52+
cs_pin = digitalio.DigitalInOut(board.D17)
2753
dc_pin = digitalio.DigitalInOut(board.D25)
2854
reset_pin = None
2955

@@ -34,77 +60,70 @@
3460
spi = board.SPI()
3561

3662
# Create the ST7789 display:
37-
disp = st7789.ST7789(spi, cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE,
38-
width=135, height=240, x_offset=53, y_offset=40)
63+
disp = st7789.ST7789(
64+
spi,
65+
dc_pin,
66+
cs_pin,
67+
reset_pin,
68+
135,
69+
240,
70+
baudrate=BAUDRATE,
71+
x_offset=53,
72+
y_offset=40,
73+
rotation=90
74+
)
3975

4076
# Create blank image for drawing.
4177
# Make sure to create image with mode 'RGB' for full color.
42-
height = disp.width # we swap height/width to rotate it to landscape!
43-
width = disp.height
44-
image = Image.new('RGB', (width, height))
45-
rotation = 90
46-
47-
# Get drawing object to draw on image.
78+
CANVAS_WIDTH = disp.height
79+
CANVAS_HEIGHT = disp.width
80+
image = Image.new('RGB', (CANVAS_WIDTH, CANVAS_HEIGHT))
4881
draw = ImageDraw.Draw(image)
4982

50-
# Draw a black filled box to clear the image.
51-
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
52-
disp.image(image, rotation)
53-
# Draw some shapes.
54-
# First define some constants to allow easy resizing of shapes.
55-
padding = -2
56-
top = padding
57-
bottom = height-padding
58-
# Move left to right keeping track of the current x position for drawing shapes.
59-
x = 0
60-
61-
62-
# Alternatively load a TTF font. Make sure the .ttf font file is in the
63-
# same directory as the python script!
64-
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
65-
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 24)
66-
67-
# Turn on the backlight
68-
backlight = digitalio.DigitalInOut(board.D22)
69-
backlight.switch_to_output()
70-
backlight.value = True
71-
72-
# Add buttons as inputs
83+
# Load default font (or replace with a TTF if desired)
84+
FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
85+
font = ImageFont.truetype(FONT_PATH, 24)
86+
7387
buttonA = digitalio.DigitalInOut(board.D23)
7488
buttonA.switch_to_input()
7589

7690
while True:
7791
# Draw a black filled box to clear the image.
78-
draw.rectangle((0, 0, width, height), outline=0, fill=0)
92+
draw.rectangle((0, 0, CANVAS_WIDTH, CANVAS_HEIGHT), outline=0, fill=(0, 0, 0))
7993

8094
# Shell scripts for system monitoring from here:
8195
# https://unix.stackexchange.com/questions/119126/command-to-display-memory-usage-disk-usage-and-cpu-load
82-
cmd = "hostname -I | cut -d\' \' -f1"
83-
IP = "IP: "+subprocess.check_output(cmd, shell=True).decode("utf-8")
84-
cmd = "hostname | tr -d \'\\n\'"
96+
cmd = "hostname -I | cut -d' ' -f1"
97+
IP = "IP: " + subprocess.check_output(cmd, shell=True).decode("utf-8").strip()
98+
cmd = "hostname | tr -d '\\n'"
8599
HOST = subprocess.check_output(cmd, shell=True).decode("utf-8")
86100
cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'"
87101
CPU = subprocess.check_output(cmd, shell=True).decode("utf-8")
88102
cmd = "free -m | awk 'NR==2{printf \"Mem: %s/%s MB %.2f%%\", $3,$2,$3*100/$2 }'"
89103
MemUsage = subprocess.check_output(cmd, shell=True).decode("utf-8")
90104
cmd = "df -h | awk '$NF==\"/\"{printf \"Disk: %d/%d GB %s\", $3,$2,$5}'"
91105
Disk = subprocess.check_output(cmd, shell=True).decode("utf-8")
92-
cmd = "cat /sys/class/thermal/thermal_zone0/temp | awk \'{printf \"CPU Temp: %.1f C\", $(NF-0) / 1000}\'" # pylint: disable=line-too-long
106+
cmd = (
107+
"cat /sys/class/thermal/thermal_zone0/temp | "
108+
"awk '{printf \"CPU Temp: %.1f C\", $(NF-0) / 1000}'"
109+
)
93110
Temp = subprocess.check_output(cmd, shell=True).decode("utf-8")
94111

95-
96112
# Pi Hole data!
97113
try:
98-
r = requests.get(api_url)
99-
data = json.loads(r.text)
100-
DNSQUERIES = data['dns_queries_today']
101-
ADSBLOCKED = data['ads_blocked_today']
102-
CLIENTS = data['unique_clients']
103-
except KeyError:
104-
time.sleep(1)
105-
continue
106-
107-
y = top
114+
r = requests.get(API_URL, timeout=5)
115+
r.raise_for_status()
116+
data = r.json()
117+
DNSQUERIES = data["queries"]["total"]
118+
ADSBLOCKED = data["queries"]["blocked"]
119+
CLIENTS = data["clients"]["total"]
120+
except (KeyError, requests.RequestException, json.JSONDecodeError):
121+
DNSQUERIES = None
122+
ADSBLOCKED = None
123+
CLIENTS = None
124+
125+
y = top = 5
126+
x = 5
108127
if not buttonA.value: # just button A pressed
109128
draw.text((x, y), IP, font=font, fill="#FFFF00")
110129
y += font.getbbox(IP)[3]
@@ -121,13 +140,19 @@
121140
y += font.getbbox(IP)[3]
122141
draw.text((x, y), HOST, font=font, fill="#FFFF00")
123142
y += font.getbbox(HOST)[3]
124-
draw.text((x, y), "Ads Blocked: {}".format(str(ADSBLOCKED)), font=font, fill="#00FF00")
125-
y += font.getbbox(str(ADSBLOCKED))[3]
126-
draw.text((x, y), "Clients: {}".format(str(CLIENTS)), font=font, fill="#0000FF")
127-
y += font.getbbox(str(CLIENTS))[3]
128-
draw.text((x, y), "DNS Queries: {}".format(str(DNSQUERIES)), font=font, fill="#FF00FF")
129-
y += font.getbbox(str(DNSQUERIES))[3]
143+
if ADSBLOCKED is not None:
144+
txt = f"Ads Blocked: {ADSBLOCKED}"
145+
draw.text((x, y), txt, font=font, fill="#00FF00")
146+
y += font.getbbox(txt)[3]
147+
if CLIENTS is not None:
148+
txt = f"Clients: {CLIENTS}"
149+
draw.text((x, y), txt, font=font, fill="#0000FF")
150+
y += font.getbbox(txt)[3]
151+
if DNSQUERIES is not None:
152+
txt = f"DNS Queries: {DNSQUERIES}"
153+
draw.text((x, y), txt, font=font, fill="#FF00FF")
154+
y += font.getbbox(txt)[3]
130155

131156
# Display image.
132-
disp.image(image, rotation)
157+
disp.image(image)
133158
time.sleep(.1)

Pi_Hole_Ad_Blocker/stats.py

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries
22
# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries
33
# SPDX-FileCopyrightText: 2017 James DeVito for Adafruit Industries
4+
# SPDX-FileCopyrightText: 2025 Mikey Sklar for Adafruit Industries
45
#
56
# SPDX-License-Identifier: MIT
67

@@ -29,44 +30,33 @@
2930
# Adafruit Blinka to support CircuitPython libraries. CircuitPython does
3031
# not support PIL/pillow (python imaging library)!
3132

32-
# Import Python System Libraries
33-
import json
33+
3434
import subprocess
3535
import time
3636

37-
# Import Requests Library
3837
import requests
39-
40-
# Import Blinka
4138
from board import SCL, SDA
4239
import busio
4340
import adafruit_ssd1306
44-
45-
# Import Python Imaging Library
4641
from PIL import Image, ImageDraw, ImageFont
4742

48-
API_TOKEN = "YOUR_API_TOKEN_HERE"
49-
api_url = "http://localhost/admin/api.php?summaryRaw&auth="+API_TOKEN
43+
api_url = "http://localhost/api/stats/summary"
5044

5145
# Create the I2C interface.
5246
i2c = busio.I2C(SCL, SDA)
5347

5448
# Create the SSD1306 OLED class.
55-
# The first two parameters are the pixel width and pixel height. Change these
56-
# to the right size for your display!
5749
disp = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c)
5850

5951
# Leaving the OLED on for a long period of time can damage it
60-
# Set these to prevent OLED burn in
61-
DISPLAY_ON = 10 # on time in seconds
62-
DISPLAY_OFF = 50 # off time in seconds
52+
DISPLAY_ON = 10 # on time in seconds
53+
DISPLAY_OFF = 50 # off time in seconds
6354

6455
# Clear display.
6556
disp.fill(0)
6657
disp.show()
6758

6859
# Create blank image for drawing.
69-
# Make sure to create image with mode '1' for 1-bit color.
7060
width = disp.width
7161
height = disp.height
7262
image = Image.new('1', (width, height))
@@ -77,27 +67,21 @@
7767
# Draw a black filled box to clear the image.
7868
draw.rectangle((0, 0, width, height), outline=0, fill=0)
7969

80-
# Draw some shapes.
81-
# First define some constants to allow easy resizing of shapes.
8270
padding = -2
8371
top = padding
84-
bottom = height - padding
85-
# Move left to right keeping track of the current x position
86-
# for drawing shapes.
8772
x = 0
8873

8974
# Load nice silkscreen font
9075
font = ImageFont.truetype('/home/pi/slkscr.ttf', 8)
9176

9277
while True:
93-
# Draw a black filled box to clear the image.
78+
# Clear the image buffer
9479
draw.rectangle((0, 0, width, height), outline=0, fill=0)
9580

96-
# Shell scripts for system monitoring from here :
97-
# https://unix.stackexchange.com/questions/119126/command-to-display-memory-usage-disk-usage-and-cpu-load
98-
cmd = "hostname -I | cut -d\' \' -f1 | tr -d \'\\n\'"
81+
# Shell scripts for system monitoring
82+
cmd = "hostname -I | cut -d' ' -f1 | tr -d '\\n'"
9983
IP = subprocess.check_output(cmd, shell=True).decode("utf-8")
100-
cmd = "hostname | tr -d \'\\n\'"
84+
cmd = "hostname | tr -d '\\n'"
10185
HOST = subprocess.check_output(cmd, shell=True).decode("utf-8")
10286
cmd = "top -bn1 | grep load | awk " \
10387
"'{printf \"CPU Load: %.2f\", $(NF-2)}'"
@@ -109,35 +93,30 @@
10993
"\"Disk: %d/%dGB %s\", $3,$2,$5}'"
11094
Disk = subprocess.check_output(cmd, shell=True).decode("utf-8")
11195

112-
# Pi Hole data!
96+
# Pi-Hole data!
11397
try:
114-
r = requests.get(api_url)
115-
data = json.loads(r.text)
116-
DNSQUERIES = data['dns_queries_today']
117-
ADSBLOCKED = data['ads_blocked_today']
118-
CLIENTS = data['unique_clients']
119-
except KeyError:
120-
time.sleep(1)
121-
continue
122-
123-
draw.text((x, top), "IP: " + str(IP) +
124-
" (" + HOST + ")", font=font, fill=255)
125-
draw.text((x, top + 8), "Ads Blocked: " +
126-
str(ADSBLOCKED), font=font, fill=255)
127-
draw.text((x, top + 16), "Clients: " +
128-
str(CLIENTS), font=font, fill=255)
129-
draw.text((x, top + 24), "DNS Queries: " +
130-
str(DNSQUERIES), font=font, fill=255)
131-
132-
# skip over original stats
133-
# draw.text((x, top+8), str(CPU), font=font, fill=255)
134-
# draw.text((x, top+16), str(MemUsage), font=font, fill=255)
135-
# draw.text((x, top+25), str(Disk), font=font, fill=255)
98+
r = requests.get(api_url, timeout=2)
99+
r.raise_for_status()
100+
data = r.json()
101+
DNSQUERIES = data["queries"]["total"]
102+
ADSBLOCKED = data["queries"]["blocked"]
103+
CLIENTS = data["clients"]["total"]
104+
except (KeyError, requests.RequestException):
105+
DNSQUERIES = 0
106+
ADSBLOCKED = 0
107+
CLIENTS = 0
108+
109+
draw.text((x, top), "IP: " + IP + " (" + HOST + ")", font=font, fill=255)
110+
draw.text((x, top + 8), "Ads Blocked: " + str(ADSBLOCKED), font=font, fill=255)
111+
draw.text((x, top + 16), "Clients: " + str(CLIENTS), font=font, fill=255)
112+
draw.text((x, top + 24), "DNS Queries: " + str(DNSQUERIES), font=font, fill=255)
136113

137114
# Display image.
138115
disp.image(image)
139116
disp.show()
140117
time.sleep(DISPLAY_ON)
118+
119+
# Blank screen to prevent burn-in
141120
disp.fill(0)
142121
disp.show()
143122
time.sleep(DISPLAY_OFF)

0 commit comments

Comments
 (0)