Using a raspberry pi to block ads via Pi-hole is the most effective way to scrub trackers and video pre-rolls from your entire network. But when a legitimate site breaks because of an overzealous blocklist, logging into a web dashboard from your phone to toggle the DNS filter is tedious. This guide builds a dedicated Pi-hole appliance with a physical hardware kill-switch and an I2C OLED status monitor, giving you instant visual feedback and one-button troubleshooting.

The Verdict: Which Raspberry Pi to Block Ads?

Pi-hole is incredibly lightweight. It does not need the raw compute of a desktop replacement. Below is the decision matrix for selecting your board in 2026.

Board Variant Price (Approx) Power Draw Verdict
Raspberry Pi Zero 2 W $15 ~1.2W Best for advanced users. Requires soldering or micro-USB dongles for Ethernet.
Raspberry Pi 4 Model B (2GB) $45 ~2.7W Default Pick. Gigabit Ethernet, full-size GPIO, runs cool without a fan.
Raspberry Pi 5 (4GB) $60 ~5.5W+ Overkill. Requires active cooling and a 27W USB-C PD supply. Waste of silicon for DNS.
Concrete Pick: Buy the Raspberry Pi 4 Model B (2GB RAM). It natively supports Gigabit Ethernet (crucial for not bottlenecking your fiber connection) and has the standard 40-pin header for our I2C display without needing thermal throttling management.

Exact Parts List

  • Compute: Raspberry Pi 4 Model B (2GB)
  • Storage: 32GB SanDisk Extreme microSD (U3, A2 rated for high IOPS)
  • Display: 0.96" I2C OLED (SSD1306 driver, 128x64, 4-pin)
  • Switch: 6x6mm Tactile Pushbutton (Normally Open)
  • Resistor: 10kΩ through-hole (for external pull-up, optional but recommended)
  • Power: 5V 3A USB-C Power Supply (Official Raspberry Pi or Anker)

Hardware Assembly and Pin Mapping

The code provided below targets the Raspberry Pi 4 Model B, but the GPIO mapping is identical for the Pi Zero 2 W and Pi 5. We are using the hardware I2C bus for the display and a standard GPIO pin with an internal pull-up for the button.

Component Pi Physical Pin GPIO / Bus Function
OLED VCC 1 3V3 Logic Power
OLED GND 6 GND Common Ground
OLED SDA 3 GPIO 2 (I2C1) I2C Data
OLED SCL 5 GPIO 3 (I2C1) I2C Clock
Button Leg 1 11 GPIO 17 Toggle Input (Active Low)
Button Leg 2 9 GND Switch Ground
Bench Tip: While the Pi has internal pull-up resistors, breadboard jumper wires can act as antennas for EMI in a noisy router closet. Adding a physical 10kΩ resistor between GPIO 17 and 3V3 ensures your button doesn't ghost-trigger from nearby CAT6 cables.

Software Setup: Pi-hole and Python Monitor

Flash Raspberry Pi OS Lite (64-bit, Bookworm) to your SD card using the official Imager. Enable SSH and set your WiFi/hostname in the imager settings. Boot the Pi, SSH in, and install Pi-hole via the official automated script:

curl -sSL https://install.pi-hole.net | bash

Once Pi-hole is running, install the Python dependencies for our hardware monitor. We use luma.oled for the display and gpiozero for the button.

sudo apt update
sudo apt install python3-pip python3-full i2c-tools
sudo raspi-config nonint do_i2c 0
sudo pip3 install --break-system-packages luma.oled gpiozero requests

The Compilable Python Monitor Script

Create a file named pihole_monitor.py. This script queries the local Pi-hole CLI to avoid API token rotation issues in v6, updates the OLED, and listens for the button press to toggle blocking.

#!/usr/bin/env python3
"""
Pi-hole Hardware Monitor & Kill-Switch
Target: Raspberry Pi 4 Model B (2GB) / Pi Zero 2 W
Requires: luma.oled, gpiozero
"""
import time
import subprocess
import json
from gpiozero import Button
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from luma.core.render import canvas
from PIL import ImageFont

# --- PIN & HARDWARE DEFINITIONS ---
BUTTON_PIN = 17  # GPIO 17 (Physical Pin 11)
I2C_PORT = 1     # /dev/i2c-1
I2C_ADDR = 0x3C  # Standard SSD1306 address

# Initialize Hardware
try:
    serial = i2c(port=I2C_PORT, address=I2C_ADDR)
    device = ssd1306(serial)
    button = Button(BUTTON_PIN, pull_up=True, bounce_time=0.2)
except Exception as e:
    print(f"Hardware Init Failed: {e}")
    exit(1)

def get_pihole_stats():
    """Fetches stats via Pi-hole CLI JSON output to bypass v6 API auth."""
    try:
        result = subprocess.run(['pihole', '-c', '-j'], capture_output=True, text=True, check=True)
        data = json.loads(result.stdout)
        return {
            "status": "ACTIVE" if data.get("FTL", {}).get("query") else "IDLE",
            "blocked": data.get("FTL", {}).get("blocked", 0),
            "percent": data.get("FTL", {}).get("percent", 0.0)
        }
    except subprocess.CalledProcessError:
        return {"status": "ERROR", "blocked": 0, "percent": 0.0}

def toggle_pihole():
    """Toggles Pi-hole blocking state."""
    stats = get_pihole_stats()
    try:
        if stats["status"] == "ACTIVE":
            subprocess.run(['pihole', 'disable'], check=True)
        else:
            subprocess.run(['pihole', 'enable'], check=True)
    except subprocess.CalledProcessError as e:
        print(f"Toggle failed: {e}")

button.when_pressed = toggle_pihole

# --- MAIN LOOP ---
while True:
    stats = get_pihole_stats()
    with canvas(device) as draw:
        # Using default font for maximum compatibility
        draw.text((0, 0), f"Status: {stats['status']}", fill="white")
        draw.text((0, 20), f"Blocked: {stats['blocked']}", fill="white")
        draw.text((0, 40), f"Ratio: {stats['percent']}%", fill="white")
        draw.text((0, 55), "[BTN] Toggle", fill="white")
    time.sleep(2)

Run the script with root privileges: sudo python3 pihole_monitor.py. To make it run on boot, create a systemd service file at /etc/systemd/system/pihole-monitor.service.

Debugging: Exact Error Strings and Ranked Fixes

When working with I2C and system-level subprocess calls on Pi OS Bookworm, you will hit specific permission and bus errors. Here is the exact decision path for the three most common failures.

1. "OSError: [Errno 121] Remote I/O error"

  • Cause A (Most Likely): I2C is disabled in the OS. Fix: Run sudo raspi-config -> Interface Options -> I2C -> Enable.
  • Cause B: Wrong I2C address. Fix: Run i2cdetect -y 1. If your OLED shows up as 0x3D instead of 0x3C, update the I2C_ADDR variable in the script.
  • Cause C: SDA/SCL swapped. Fix: Verify physical pins 3 and 5.

2. "subprocess.CalledProcessError: Command '['pihole', 'disable']' returned non-zero exit status 1"

  • Cause A: Script is not running as root. The pihole CLI requires sudo to modify DNS services. Fix: Execute with sudo python3 pihole_monitor.py.
  • Cause B: Pi-hole FTL service is crashed. Fix: Run sudo systemctl restart pihole-FTL.

3. "gpiozero.exc.BadPinFactory: Unable to load any default pin factory"

  • Cause A: Missing backend library in your Python environment. Fix: sudo apt install python3-rpi.gpio.
  • Cause B: Running inside a standard user venv without passing system site-packages. Fix: Run the script in the global system Python environment or use --system-site-packages when creating your venv.

First Three Things to Check When the Network Fails

If you set your router's DHCP to point to the Pi and suddenly the entire house loses internet, don't panic. Run this diagnostic sequence:

  1. Verify the Pi-hole FTL Service: SSH into the Pi and run sudo systemctl status pihole-FTL. If it's dead, your DNS is dead. Restart it with sudo systemctl restart pihole-FTL.
  2. Check Upstream Resolution: Pi-hole might be running, but its upstream DNS (e.g., Cloudflare 1.1.1.1) might be failing. Run ping 1.1.1.1 from the Pi. If it times out, your Pi's internet connection is down, not Pi-hole itself.
  3. Inspect the Router DHCP Scope: Ensure your router is handing out the Pi's static IP (e.g., 192.168.1.10) as the primary DNS, and a public DNS (like 8.8.8.8) as the secondary. If you put the Pi as both primary and secondary, a Pi reboot takes the whole house offline.

Extending or Simplifying the Build

How to Simplify

If you don't want to wire hardware, strip the Python script down to just the get_pihole_stats() function and output the data to an MQTT broker (like Mosquitto). You can then read the stats in Home Assistant without needing an OLED or GPIO button. Just run Pi-hole headless and manage it via the web UI.

How to Extend (The Smart TV Bypass)

Modern Smart TVs (Samsung Tizen, LG WebOS) often ignore DNS settings and hardcode their own tracking IPs, bypassing Pi-hole entirely. To fix this, extend the build by adding a 5V Relay Module to GPIO 27. Wire the TV's power line through the relay's Normally Closed (NC) contacts. Update the Python script to trigger the relay for 5 seconds if the TV's specific MAC address is detected on the network making direct DNS requests to 8.8.8.8. It's aggressive, but it guarantees compliance.

For official network architecture guidance, refer to the Pi-hole documentation and the Raspberry Pi hardware guides.