Skip to content

Instantly share code, notes, and snippets.

@muzll0dr
Last active April 28, 2021 02:59
Show Gist options
  • Save muzll0dr/8145bec5d8af8a80faf4e613c89a44c6 to your computer and use it in GitHub Desktop.
Save muzll0dr/8145bec5d8af8a80faf4e613c89a44c6 to your computer and use it in GitHub Desktop.
A better stats script for the Adafruit PiHole tutorial using the Mini Color PiTFT display.
# -*- coding: utf-8 -*-
# Import Python System Libraries
import time
import json
import subprocess
import math
# 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_url = 'http://localhost/admin/api.php'
api_pass = '{apikey}'
api_disable = api_url + '?disable&auth=' + api_pass
api_enable = api_url + '?enable&auth=' + api_pass
# Used for a temp disable
disable_minutes = 5
disable_seconds = disable_minutes * 60
api_temp_disable = 'http://localhost/admin/api.php?disable=' + str(disable_seconds) + '&auth=' + api_pass
# 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()
buttonB = digitalio.DigitalInOut(board.D24)
buttonB.switch_to_input()
# Get the IP and hostname one time.
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")
# FUNCTIONS
def showStats():
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)
return
y = top
draw.rectangle((0, 0, width, height), outline=0, fill=0)
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), "Queries: {}".format(str(DNSQUERIES)), font=font, fill="#00FF00")
y += font.getsize(str(DNSQUERIES))[1]
draw.text((x, y), "Blocked: {}".format(str(ADSBLOCKED)), font=font, fill="#FF0000")
y += font.getsize(str(ADSBLOCKED))[1]
draw.text((x, y), "Clients: {}".format(str(CLIENTS)), font=font, fill="#FF00FF")
y += font.getsize(str(CLIENTS))[1]
disp.image(image, rotation)
def showPaused(seconds_left):
y = top
y += font.getsize(IP)[1]
draw.rectangle((0, 0, width, height), outline=0, fill=0)
msg = "DISABLED FOR" #define the first line
mywidth, hei = draw.textsize(msg, font=font) # determine how wide the first line is
draw.text(((width-mywidth)/2 ,y), msg, font=font, fill="#FF0000") # center and draw the first line
y += font.getsize(IP)[1] * 2 # create a line-height space (using IP because why not)
if (seconds_left < 61):
msg = str(seconds_left) + " SECONDS"
else:
minutes_left = math.ceil(seconds_left / 60)
msg = str(minutes_left) + " MINUTE"
if (minutes_left > 1):
msg += "S"
mywidth, hei = draw.textsize(msg, font=font)
draw.text(((width-mywidth)/2 ,y), msg, font=font, fill="#FF0000")
disp.image(image, rotation)
def showDisabled():
y = top
# Move it down a tidge. "I" here is just a random full-height character.
y += font.getsize('I')[1]
# Draw black display.
draw.rectangle((0, 0, width, height), outline=0, fill=0)
msg = "PIHOLE IS" #define the first line
mywidth, hei = draw.textsize(msg, font=font) # determine how wide the first line is
draw.text(((width-mywidth)/2 ,y), msg, font=font, fill="#FF0000") # center and draw the first line
y += font.getsize('I')[1] * 2 # create a line-height space (using IP because why not)
msg = "DISABLED"
mywidth, hei = draw.textsize(msg, font=font)
draw.text(((width-mywidth)/2 ,y), msg, font=font, fill="#FF0000")
disp.image(image, rotation)
def showCPUStats():
draw.rectangle((0, 0, width, height), outline=0, fill=0)
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\", $3, $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")
y = top
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), Temp, font=font, fill="#FF00FF")
disp.image(image, rotation)
buttonTimer = math.floor(time.time()) # used to ensure we don't get repeat button presses
def resetButtonTimer():
buttonTimer = math.floor(time.time())
## Main Loop
cycleSeconds = 3
cycleTime = math.ceil(time.time()) + cycleSeconds
shouldCycle = True
screen = 'stats'
status = 'enabled'
showStats()
resumeTime = False
while True:
### Button Presses
if not buttonA.value and not buttonB.value:
# use this to toggle the flipflop or just show stats
# this will ensure a delay so it doesn't cycle between true/false rapidly
if (buttonTimer < math.floor(time.time())):
shouldCycle = not shouldCycle
resetButtonTimer()
elif not buttonA.value:
# if ((math.floor(time.time()) - buttonTimer) <= 1):
# continue;
if (status == 'paused' or status == 'disabled'):
requests.get(api_enable)
status = 'enabled'
elif (status == 'enabled'):
requests.get(api_disable)
resumeTime = math.ceil(time.time()) + disable_seconds
status = 'paused'
resetButtonTimer()
elif not buttonB.value:
if (status == 'disabled'):
requests.get(api_enable)
status = 'enabled'
else:
requests.get(api_disable)
status = 'disabled'
showDisabled() # putting this here since this is a static screen
resetButtonTimer()
### Logic
# this is probably all goign to have to move below the buttons, yeah??
if (status == 'enabled' and shouldCycle):
if (cycleTime < math.ceil(time.time())):
if (screen != 'cpu'):
screen = 'cpu'
else:
screen = 'stats'
cycleTime = math.ceil(time.time()) + cycleSeconds
if (screen == 'cpu'):
showCPUStats()
elif (screen == 'stats'):
showStats()
elif (status == 'enabled'):
showStats()
elif (status == 'paused'):
# first check to see if we need to unpause, otherwise show the pause screen
seconds_left = resumeTime - (math.ceil(time.time()))
if (seconds_left <= 0):
requests.get(api_enable)
status = 'enabled'
resumeTime = False
else:
showPaused(seconds_left)
else:
# we show the diabled screen when we hit the disable
# button, and since it's a static screen, we're not
# going to do anything unless the status changes.
doNothing = True
time.sleep(.2)
@muzll0dr
Copy link
Author

muzll0dr commented Apr 27, 2021

This will give your Adafruit Mini Color PiTFT maximum flexibility over the tutorial script.

When started, the display will flip flop between showing PiHole stats and CPU stats every 3 seconds. These are the PiHole stats:

  • IP
  • Hostname
  • Queries
  • Blocked
  • Clients

And these are the CPU stats:

  • IP
  • CPU Load
  • Memory Usage
  • Disk Usage
  • CPU Temp

If you press both buttons while the PiHole is enabled, it will lock the display to only PiHole stats.

Pressing the top button will start a 5 minute pause of your PiHole and the screen will indicate as such. It will count down as the timer progresses.

Pressing the bottom button will completely disable the PiHole until you press either button again.

@muzll0dr
Copy link
Author

TODO:

  • make the pressed button timer work better

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment