Update epaper-pihole.py
This commit is contained in:
612
epaper-pihole.py
612
epaper-pihole.py
@@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pi-hole E-Paper Display - Binglab Style
|
||||
=======================================
|
||||
Displays Pi-hole and system statistics on a Waveshare 2.7" E-Paper display
|
||||
with a professional 6-block grid layout (3x2) showing:
|
||||
- Ads blocked today
|
||||
- DNS queries today
|
||||
- Ad blocking percentage
|
||||
- Number of devices protected
|
||||
- CPU usage
|
||||
- System uptime
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Data Source Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
USE_CLI = True # True = use pihole command, False = use HTTP API
|
||||
|
||||
# CLI Mode Settings
|
||||
USE_SSH = False # Set True to fetch stats from remote Pi-hole via SSH
|
||||
SSH_HOST = "pi@192.168.10.44" # SSH connection string (user@host)
|
||||
|
||||
# HTTP API Mode Settings
|
||||
PIHOLE_HOST = "192.168.10.44" # Pi-hole server IP address
|
||||
PIHOLE_API_KEY = "" # Optional API key for authenticated requests
|
||||
USE_NEW_API = False # Reserved for future Pi-hole API versions
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Display Hardware Settings
|
||||
# -----------------------------------------------------------------------------
|
||||
DISPLAY_WIDTH = 264 # E-Paper display width in pixels
|
||||
DISPLAY_HEIGHT = 176 # E-Paper display height in pixels
|
||||
REFRESH_INTERVAL = 300 # Time between updates in seconds (5 minutes)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# File Path Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
WAVESHARE_LIB_PATH = '/home/mbtech/pihole-epaper/waveshare_epd' # Path to Waveshare library
|
||||
PREVIEW_IMAGE_PATH = '/home/mbtech/pihole_display.png' # Path for preview image output
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Branding and Customization
|
||||
# -----------------------------------------------------------------------------
|
||||
DISPLAY_TITLE = "Binglab Sinkhole" # Display header title - customize this!
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# E-Paper Library Import
|
||||
# -----------------------------------------------------------------------------
|
||||
# Attempt to import the Waveshare E-Paper library
|
||||
# If not found, the script will run in preview mode (saves PNG files instead)
|
||||
try:
|
||||
sys.path.append(WAVESHARE_LIB_PATH)
|
||||
from waveshare_epd import epd2in7b_V2
|
||||
EPD_AVAILABLE = True
|
||||
except ImportError:
|
||||
print("Warning: Waveshare e-Paper library not found. Running in preview mode.")
|
||||
EPD_AVAILABLE = False
|
||||
|
||||
# ============================================================================
|
||||
# DATA COLLECTION FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def run_pihole_command(command):
|
||||
"""
|
||||
Execute a Pi-hole CLI command either locally or via SSH
|
||||
|
||||
Args:
|
||||
command (str): The command to execute
|
||||
|
||||
Returns:
|
||||
str: Command output if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
# Execute via SSH if configured
|
||||
if USE_SSH:
|
||||
ssh_cmd = ['ssh', SSH_HOST, command]
|
||||
result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=10)
|
||||
# Execute locally
|
||||
else:
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10)
|
||||
|
||||
# Return output if command succeeded
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error running command: {e}")
|
||||
return None
|
||||
|
||||
def get_pihole_stats_cli():
|
||||
"""
|
||||
Fetch Pi-hole statistics using the CLI method
|
||||
Attempts newer API format first, falls back to legacy format
|
||||
|
||||
Returns:
|
||||
dict: Statistics dictionary with keys:
|
||||
- dns_queries_today: Total DNS queries
|
||||
- ads_blocked_today: Number of blocked queries
|
||||
- ads_percentage_today: Percentage of queries blocked
|
||||
- unique_clients: Number of active devices
|
||||
None if data fetch fails
|
||||
"""
|
||||
try:
|
||||
# Try newer Pi-hole API format first (v6.0+)
|
||||
output = run_pihole_command('sudo pihole api stats/summary')
|
||||
|
||||
if output:
|
||||
try:
|
||||
data = json.loads(output)
|
||||
# Check if response has new API structure
|
||||
if 'queries' in data and isinstance(data['queries'], dict):
|
||||
return {
|
||||
'dns_queries_today': data.get('queries', {}).get('total', 0),
|
||||
'ads_blocked_today': data.get('queries', {}).get('blocked', 0),
|
||||
'ads_percentage_today': data.get('queries', {}).get('percent_blocked', 0),
|
||||
'unique_clients': data.get('clients', {}).get('active', 0),
|
||||
}
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
# If parsing fails, try legacy format below
|
||||
pass
|
||||
|
||||
# Fallback to legacy Pi-hole format (v5.x and earlier)
|
||||
output = run_pihole_command('sudo pihole -c -j')
|
||||
if output:
|
||||
data = json.loads(output)
|
||||
return {
|
||||
'dns_queries_today': data.get('dns_queries_today', 0),
|
||||
'ads_blocked_today': data.get('ads_blocked_today', 0),
|
||||
'ads_percentage_today': data.get('ads_percentage_today', 0),
|
||||
'unique_clients': data.get('unique_clients', 0),
|
||||
}
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error getting CLI stats: {e}")
|
||||
return None
|
||||
|
||||
def get_pihole_stats_http():
|
||||
"""
|
||||
Fetch Pi-hole statistics using the HTTP API method
|
||||
|
||||
Returns:
|
||||
dict: Statistics dictionary with keys:
|
||||
- dns_queries_today: Total DNS queries
|
||||
- ads_blocked_today: Number of blocked queries
|
||||
- ads_percentage_today: Percentage of queries blocked
|
||||
- unique_clients: Number of active devices
|
||||
None if data fetch fails
|
||||
"""
|
||||
import requests
|
||||
try:
|
||||
# Build API endpoint URL
|
||||
url = f"http://{PIHOLE_HOST}/admin/api.php"
|
||||
params = {'summaryRaw': ''}
|
||||
|
||||
# Add authentication if API key is provided
|
||||
if PIHOLE_API_KEY:
|
||||
params['auth'] = PIHOLE_API_KEY
|
||||
|
||||
# Fetch data from Pi-hole web API
|
||||
response = requests.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Extract and return statistics
|
||||
return {
|
||||
'dns_queries_today': data.get('dns_queries_today', 0),
|
||||
'ads_blocked_today': data.get('ads_blocked_today', 0),
|
||||
'ads_percentage_today': data.get('ads_percentage_today', 0),
|
||||
'unique_clients': data.get('unique_clients', 0),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting HTTP stats: {e}")
|
||||
return None
|
||||
|
||||
def get_pihole_stats():
|
||||
"""
|
||||
Fetch Pi-hole statistics using the configured method (CLI or HTTP)
|
||||
|
||||
Returns:
|
||||
dict: Pi-hole statistics or None if fetch fails
|
||||
"""
|
||||
if USE_CLI:
|
||||
return get_pihole_stats_cli()
|
||||
else:
|
||||
return get_pihole_stats_http()
|
||||
|
||||
def get_system_stats():
|
||||
"""
|
||||
Fetch system statistics (CPU usage and uptime)
|
||||
Works on both local and SSH configurations
|
||||
|
||||
Returns:
|
||||
dict: System statistics with keys:
|
||||
- cpu_usage: CPU usage percentage (float)
|
||||
- uptime: System uptime as formatted string (e.g., "5d 12h")
|
||||
None if data fetch fails
|
||||
"""
|
||||
try:
|
||||
stats = {}
|
||||
|
||||
# Get CPU usage
|
||||
if USE_SSH:
|
||||
# Remote system via SSH
|
||||
cpu_output = run_pihole_command("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1")
|
||||
if cpu_output:
|
||||
stats['cpu_usage'] = float(cpu_output)
|
||||
else:
|
||||
stats['cpu_usage'] = 0.0
|
||||
else:
|
||||
# Local system - use psutil if available, fallback to top command
|
||||
try:
|
||||
import psutil
|
||||
stats['cpu_usage'] = psutil.cpu_percent(interval=1)
|
||||
except ImportError:
|
||||
# Fallback to top command
|
||||
cpu_output = run_pihole_command("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1")
|
||||
if cpu_output:
|
||||
stats['cpu_usage'] = float(cpu_output)
|
||||
else:
|
||||
stats['cpu_usage'] = 0.0
|
||||
|
||||
# Get uptime - format as "Xd Xh" or "Xh Xm" depending on duration
|
||||
uptime_output = run_pihole_command("uptime -p")
|
||||
if uptime_output:
|
||||
# Parse "up X days, Y hours" format
|
||||
uptime_str = uptime_output.replace('up ', '')
|
||||
# Simplify the format for display
|
||||
parts = []
|
||||
if 'day' in uptime_str:
|
||||
days = uptime_str.split('day')[0].strip().split()[-1]
|
||||
parts.append(f"{days}d")
|
||||
if 'hour' in uptime_str:
|
||||
hours = uptime_str.split('hour')[0].strip().split()[-1]
|
||||
parts.append(f"{hours}h")
|
||||
if 'minute' in uptime_str and 'day' not in uptime_str and 'hour' not in uptime_str:
|
||||
minutes = uptime_str.split('minute')[0].strip().split()[-1]
|
||||
parts.append(f"{minutes}m")
|
||||
|
||||
stats['uptime'] = ' '.join(parts[:2]) if parts else 'N/A'
|
||||
else:
|
||||
stats['uptime'] = 'N/A'
|
||||
|
||||
return stats
|
||||
except Exception as e:
|
||||
print(f"Error getting system stats: {e}")
|
||||
return {'cpu_usage': 0.0, 'uptime': 'N/A'}
|
||||
|
||||
# ============================================================================
|
||||
# DISPLAY LAYOUT FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def find_fonts():
|
||||
"""
|
||||
Automatically discover and select the best available TrueType fonts
|
||||
Searches system font directories and prioritizes high-quality fonts
|
||||
|
||||
Returns:
|
||||
dict: Dictionary with 'bold' and 'regular' font paths
|
||||
Priority order: DejaVu > Liberation > FreeSans
|
||||
"""
|
||||
import os
|
||||
|
||||
# Common font directory locations across different systems
|
||||
font_dirs = [
|
||||
'/usr/share/fonts/',
|
||||
'/usr/local/share/fonts/',
|
||||
'/home/pi/.fonts/',
|
||||
'/home/mbtech/.fonts/',
|
||||
os.path.expanduser('~/.fonts/'),
|
||||
]
|
||||
|
||||
# Scan all font directories for TTF files
|
||||
found_fonts = []
|
||||
for font_dir in font_dirs:
|
||||
if os.path.exists(font_dir):
|
||||
for root, dirs, files in os.walk(font_dir):
|
||||
for file in files:
|
||||
if file.endswith(('.ttf', '.TTF')):
|
||||
found_fonts.append(os.path.join(root, file))
|
||||
|
||||
fonts = {}
|
||||
|
||||
# Priority 1: Look for DejaVu fonts (best quality, widely available)
|
||||
for font in found_fonts:
|
||||
if 'DejaVuSans-Bold.ttf' in font or 'DejaVu-Sans-Bold.ttf' in font:
|
||||
fonts['bold'] = font
|
||||
if 'DejaVuSans.ttf' in font and 'Bold' not in font:
|
||||
fonts['regular'] = font
|
||||
|
||||
# Priority 2: Fallback to Liberation fonts
|
||||
if 'bold' not in fonts:
|
||||
for font in found_fonts:
|
||||
if 'LiberationSans-Bold' in font or 'Liberation-Sans-Bold' in font:
|
||||
fonts['bold'] = font
|
||||
break
|
||||
|
||||
if 'regular' not in fonts:
|
||||
for font in found_fonts:
|
||||
if ('LiberationSans.ttf' in font or 'Liberation-Sans.ttf' in font) and 'Bold' not in font:
|
||||
fonts['regular'] = font
|
||||
break
|
||||
|
||||
# Priority 3: Fallback to FreeSans
|
||||
if 'bold' not in fonts:
|
||||
for font in found_fonts:
|
||||
if 'FreeSansBold' in font or 'FreeSans-Bold' in font:
|
||||
fonts['bold'] = font
|
||||
break
|
||||
|
||||
if 'regular' not in fonts:
|
||||
for font in found_fonts:
|
||||
if 'FreeSans.ttf' in font and 'Bold' not in font:
|
||||
fonts['regular'] = font
|
||||
break
|
||||
|
||||
return fonts
|
||||
|
||||
def create_display_image(stats, system_stats):
|
||||
"""
|
||||
Generate the E-Paper display image with Pi-hole and system statistics
|
||||
Creates a 6-block grid layout (3x2) with header section
|
||||
|
||||
Args:
|
||||
stats (dict): Pi-hole statistics dictionary or None
|
||||
system_stats (dict): System statistics (CPU, uptime) or None
|
||||
|
||||
Returns:
|
||||
PIL.Image: Generated display image (1-bit black/white)
|
||||
"""
|
||||
# Create blank white image (1-bit mode for e-paper)
|
||||
image = Image.new('1', (DISPLAY_WIDTH, DISPLAY_HEIGHT), 255)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# Discover and load system fonts
|
||||
fonts = find_fonts()
|
||||
|
||||
# Load TrueType fonts with different sizes for hierarchy
|
||||
try:
|
||||
if 'bold' in fonts and 'regular' in fonts:
|
||||
# Best case: Both bold and regular fonts available
|
||||
print(f"Using fonts: {os.path.basename(fonts['bold'])}, {os.path.basename(fonts['regular'])}")
|
||||
font_title = ImageFont.truetype(fonts['bold'], 20) # Header title
|
||||
font_huge = ImageFont.truetype(fonts['bold'], 28) # Main statistics
|
||||
font_large = ImageFont.truetype(fonts['bold'], 22) # Secondary stats
|
||||
font_small = ImageFont.truetype(fonts['regular'], 12) # Labels
|
||||
elif 'bold' in fonts:
|
||||
# Use bold for everything
|
||||
print(f"Using font: {os.path.basename(fonts['bold'])}")
|
||||
font_title = ImageFont.truetype(fonts['bold'], 20)
|
||||
font_huge = ImageFont.truetype(fonts['bold'], 28)
|
||||
font_large = ImageFont.truetype(fonts['bold'], 22)
|
||||
font_small = ImageFont.truetype(fonts['bold'], 12)
|
||||
elif 'regular' in fonts:
|
||||
# Use regular for everything
|
||||
print(f"Using font: {os.path.basename(fonts['regular'])}")
|
||||
font_title = ImageFont.truetype(fonts['regular'], 20)
|
||||
font_huge = ImageFont.truetype(fonts['regular'], 28)
|
||||
font_large = ImageFont.truetype(fonts['regular'], 22)
|
||||
font_small = ImageFont.truetype(fonts['regular'], 12)
|
||||
else:
|
||||
raise Exception("No TrueType fonts found")
|
||||
except Exception as e:
|
||||
# Fallback to default bitmap font (very small)
|
||||
print(f"Font loading error: {e}")
|
||||
print("Using default font (will be smaller)")
|
||||
font_title = ImageFont.load_default()
|
||||
font_huge = ImageFont.load_default()
|
||||
font_large = ImageFont.load_default()
|
||||
font_small = ImageFont.load_default()
|
||||
|
||||
# Handle case where no stats are available
|
||||
if stats is None:
|
||||
draw.text((10, 70), "No Pi-hole Data", font=font_large, fill=0)
|
||||
return image
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# HEADER SECTION (Top banner with logo, title, date, status)
|
||||
# -------------------------------------------------------------------------
|
||||
# Draw header container box
|
||||
draw.rectangle((0, 0, DISPLAY_WIDTH, 50), outline=0, width=2)
|
||||
|
||||
# Draw logo (decorative circle with arc design)
|
||||
draw.ellipse((10, 10, 35, 35), outline=0, width=2)
|
||||
draw.arc((15, 15, 30, 30), 0, 270, fill=0, width=2)
|
||||
|
||||
# Display title text
|
||||
draw.text((45, 8), DISPLAY_TITLE, font=font_title, fill=0)
|
||||
|
||||
# Display current date
|
||||
date_str = datetime.now().strftime("%A %B %-d" if sys.platform != "win32" else "%A %B %d")
|
||||
draw.text((45, 30), date_str, font=font_small, fill=0)
|
||||
|
||||
# Status indicator (filled circle in top right)
|
||||
draw.ellipse((240, 15, 254, 29), fill=0)
|
||||
draw.ellipse((242, 17, 252, 27), fill=255)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# GRID LAYOUT (6 blocks - 3 columns x 2 rows)
|
||||
# -------------------------------------------------------------------------
|
||||
# Calculate block dimensions
|
||||
block_width = DISPLAY_WIDTH // 3 # 88 pixels per column
|
||||
row_height = (DISPLAY_HEIGHT - 50) // 2 # Two rows below header
|
||||
|
||||
# Vertical dividers (create 3 columns)
|
||||
draw.line((block_width, 50, block_width, DISPLAY_HEIGHT), fill=0, width=2)
|
||||
draw.line((block_width * 2, 50, block_width * 2, DISPLAY_HEIGHT), fill=0, width=2)
|
||||
# Horizontal divider (create 2 rows)
|
||||
draw.line((0, 50 + row_height, DISPLAY_WIDTH, 50 + row_height), fill=0, width=2)
|
||||
|
||||
# Helper function to draw centered text in a block
|
||||
def draw_block(col, row, value, label, value_font):
|
||||
"""Draw a centered statistic block"""
|
||||
# Calculate block center position
|
||||
x_center = block_width * col + (block_width // 2)
|
||||
y_start = 50 + (row_height * row)
|
||||
|
||||
# Draw value (centered)
|
||||
bbox = draw.textbbox((0, 0), str(value), font=value_font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
x_pos = x_center - (text_width // 2)
|
||||
y_pos = y_start + 15
|
||||
draw.text((x_pos, y_pos), str(value), font=value_font, fill=0)
|
||||
|
||||
# Draw label (centered, below value)
|
||||
bbox = draw.textbbox((0, 0), label, font=font_small)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
x_pos = x_center - (text_width // 2)
|
||||
y_pos = y_start + row_height - 25
|
||||
draw.text((x_pos, y_pos), label, font=font_small, fill=0)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TOP ROW (Row 0)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Block 1 (Top Left): Ads Blocked
|
||||
blocked = stats['ads_blocked_today']
|
||||
blocked_str = f"{blocked:,}" if blocked < 100000 else f"{blocked//1000}k"
|
||||
draw_block(0, 0, blocked_str, "Blocked", font_large)
|
||||
|
||||
# Block 2 (Top Center): DNS Queries
|
||||
queries = stats['dns_queries_today']
|
||||
queries_str = f"{queries:,}" if queries < 100000 else f"{queries//1000}k"
|
||||
draw_block(1, 0, queries_str, "Queries", font_large)
|
||||
|
||||
# Block 3 (Top Right): Block Percentage
|
||||
percent = stats['ads_percentage_today']
|
||||
percent_str = f"{percent:.1f}%"
|
||||
draw_block(2, 0, percent_str, "Percent", font_large)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BOTTOM ROW (Row 1)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
# Block 4 (Bottom Left): Devices
|
||||
devices = stats['unique_clients']
|
||||
draw_block(0, 1, str(devices), "Devices", font_large)
|
||||
|
||||
# Block 5 (Bottom Center): CPU Usage
|
||||
if system_stats:
|
||||
cpu = system_stats['cpu_usage']
|
||||
cpu_str = f"{cpu:.1f}%"
|
||||
else:
|
||||
cpu_str = "N/A"
|
||||
draw_block(1, 1, cpu_str, "CPU", font_large)
|
||||
|
||||
# Block 6 (Bottom Right): Uptime
|
||||
if system_stats:
|
||||
uptime_str = system_stats['uptime']
|
||||
else:
|
||||
uptime_str = "N/A"
|
||||
draw_block(2, 1, uptime_str, "Uptime", font_large)
|
||||
|
||||
return image
|
||||
|
||||
def display_on_epaper(image, clear_first=True):
|
||||
"""
|
||||
Send the generated image to the physical E-Paper display
|
||||
Handles both 2-color and 3-color display variants
|
||||
|
||||
Args:
|
||||
image (PIL.Image): The image to display
|
||||
clear_first (bool): Whether to clear the display before updating
|
||||
"""
|
||||
try:
|
||||
# Initialize the E-Paper display
|
||||
epd = epd2in7b_V2.EPD()
|
||||
epd.init()
|
||||
|
||||
# Clear display on first run to remove any artifacts
|
||||
if clear_first:
|
||||
print("Clearing display...")
|
||||
try:
|
||||
epd.Clear()
|
||||
except TypeError:
|
||||
# Fallback method for displays without Clear() function
|
||||
white = Image.new('1', (264, 176), 255)
|
||||
buf = epd.getbuffer(white)
|
||||
try:
|
||||
epd.display(buf)
|
||||
except TypeError:
|
||||
# 3-color display needs red buffer
|
||||
red_buf = [0xFF] * (264 * 176 // 8)
|
||||
epd.display(buf, red_buf)
|
||||
time.sleep(1)
|
||||
|
||||
# Send image to display
|
||||
print("Updating display...")
|
||||
buffer = epd.getbuffer(image)
|
||||
try:
|
||||
epd.display(buffer)
|
||||
except TypeError:
|
||||
# 3-color display variant requires separate red channel buffer
|
||||
red_buffer = [0xFF] * (264 * 176 // 8)
|
||||
epd.display(buffer, red_buffer)
|
||||
|
||||
# Allow time for display refresh, then put display to sleep
|
||||
time.sleep(2)
|
||||
epd.sleep()
|
||||
print("✓ Display updated")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
def save_preview(image):
|
||||
"""
|
||||
Save the generated image as a PNG file for preview/testing
|
||||
Used when E-Paper hardware is not available
|
||||
|
||||
Args:
|
||||
image (PIL.Image): The image to save
|
||||
"""
|
||||
image.save(PREVIEW_IMAGE_PATH)
|
||||
print(f"Preview saved to {PREVIEW_IMAGE_PATH}")
|
||||
|
||||
# ============================================================================
|
||||
# MAIN PROGRAM LOOP
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main program loop
|
||||
Fetches Pi-hole and system stats, generates display image, and updates the display
|
||||
Runs continuously with configurable refresh interval
|
||||
"""
|
||||
print("Pi-hole E-Paper Display - Binglab Style (6-Block Layout)")
|
||||
print("=" * 60)
|
||||
|
||||
# Track if this is the first display update (to trigger full clear)
|
||||
first_run = True
|
||||
|
||||
# Main loop - runs indefinitely until interrupted
|
||||
while True:
|
||||
try:
|
||||
# Fetch current Pi-hole statistics
|
||||
print("\nFetching Pi-hole stats...")
|
||||
stats = get_pihole_stats()
|
||||
|
||||
# Fetch system statistics
|
||||
print("Fetching system stats...")
|
||||
system_stats = get_system_stats()
|
||||
|
||||
# Display fetched stats in console
|
||||
if stats:
|
||||
print(f" Queries: {stats['dns_queries_today']:,}")
|
||||
print(f" Blocked: {stats['ads_blocked_today']:,} ({stats['ads_percentage_today']:.2f}%)")
|
||||
print(f" Devices: {stats['unique_clients']}")
|
||||
if system_stats:
|
||||
print(f" CPU: {system_stats['cpu_usage']:.1f}%")
|
||||
print(f" Uptime: {system_stats['uptime']}")
|
||||
|
||||
# Generate display image from stats
|
||||
image = create_display_image(stats, system_stats)
|
||||
|
||||
# Update physical display or save preview
|
||||
if EPD_AVAILABLE:
|
||||
display_on_epaper(image, clear_first=first_run)
|
||||
first_run = False # Only clear on first update
|
||||
else:
|
||||
save_preview(image)
|
||||
|
||||
# Wait for next update cycle
|
||||
print(f"\nNext update in {REFRESH_INTERVAL} seconds...")
|
||||
time.sleep(REFRESH_INTERVAL)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
# Handle Ctrl+C gracefully
|
||||
print("\nExiting...")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
# Log any unexpected errors and continue
|
||||
print(f"Error: {e}")
|
||||
traceback.print_exc()
|
||||
time.sleep(60) # Wait 1 minute before retry
|
||||
|
||||
# Program entry point
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user