This project involves adding two buttons to your Raspberry Pi, one to disable Pi-hole for n seconds and the other to enable it back. I run many services on one Raspberry Pi and a dummy synchronous while True loop will lock one core, that isn’t going to fly.

Raspberry GPIO

Setting

For this project we need three LEDs, two buttons, and one 330 Ω resistance. We set the green, red and blue LEDs to pins 7, 15, and 12 respectively. In this project Raspbian and a RPi 3B+ were used.

Raspberry GPIO

Check the pinout diagram of the Raspberry Pi at pinout.

Let us start by listing the dependencies

#!/usr/bin/env python3

import RPi.GPIO as GPIO  # Import Raspberry Pi GPIO library
import pihole as ph
import sys
import asyncio

Controlling the pi-hole

We control the Pi-hole with PiHole-Api, for this we need the Pi-hole IP address and the WebUI password.

pihole_addr = "192.168.0.21"
time_limit = 30 # Disable for at most 30 seconds
pihole_password = "PIHOLE_PASSWORD" # Set the password for first use

# Get password
password = pihole_password

# Pihole
pihole = ph.PiHole(pihole_addr)
pihole.authenticate(password)
version = pihole.getVersion()['core_current']
status = pihole.status
print("Runing pihole", version)
print("Status:", pihole.status)

Using python-keyring to store passwords

Alternatively, using the keyring package we can securely store the password.

import keyring

from keyrings.alt.file import PlaintextKeyring

keyring.set_keyring(PlaintextKeyring())
password = keyring.get_password("pihole", pihole_addr)
if password is None:
    keyring.set_password("pihole", pihole_addr, pihole_password)
    password = pihole_password

On the first run set pihole_password to its real value and then set it to None. The line keyring.set_keyring(PlaintextKeyring()) should be only be used if you do not want to be prompted for the keyring password every time, this requires to add

[backend]
default-keyring=keyrings.alt.file.PlaintextKeyring

to ~/.local/share/python_keyring/crypted_pass.cfg. See [3].

Setting the GPIOs

This project uses a blue LED to signal that the Pi-hole is disabled. First we create a timer class to re-enable the Pi-hole.

## GPIO setup
red = 7  # Red using pin 7
green = 15  # Green using pin 15
blue = 12  # Blue using pin 12

## Async
loop = None
timer = None

# Timer
bouncetime = 500  # in ms

class Timer:
    def __init__(self, timeout, callback):
        self._timeout = timeout
        self._callback = callback
        self._task = asyncio.ensure_future(self._job())

    async def _job(self):
        await asyncio.sleep(self._timeout)
        await self._callback()

    def cancel(self):
        self._task.cancel()


async def timeout_callback():
    pihole.enable()
    GPIO.output(blue, GPIO.LOW)

We also add callbacks for the buttons

def button_pushed(_):
    if loop is None:
        print("Loop ended")
        return  # should not come to this
    if GPIO.input(green):
        loop.call_soon_threadsafe(green_action)
    if GPIO.input(red):
        loop.call_soon_threadsafe(red_action)


def red_action():
    print("Red button pushed!")
    GPIO.output(blue, GPIO.HIGH)
    pihole.disable(time_limit)
    timer = Timer(time_limit, timeout_callback)


def green_action():
    print("Green button pushed!")
    if timer is not None:
        timer.cancel()
    GPIO.output(blue, GPIO.LOW)
    pihole.enable()

It is important to leave everything in a clean state when closing the program, this is done using the following function.

def exit_handler():
    print('py-hole-ui closed')
    GPIO.cleanup()
    pihole.enable()
    loop.close()

Now we can define the main loop

try:
    GPIO.setwarnings(True)  # Set warnings
    GPIO.setmode(GPIO.BOARD)  # Use physical pin numbering

    # Set pins to input/output
    GPIO.setup(red, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(green, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.setup(blue, GPIO.OUT)

    GPIO.add_event_detect(
        green, GPIO.RISING, callback=button_pushed, bouncetime=bouncetime,
    )
    GPIO.add_event_detect(
        red, GPIO.RISING, callback=button_pushed, bouncetime=bouncetime,
    )

    loop = asyncio.get_event_loop()
    loop.run_forever()

finally:
    exit_handler()

Now python3 does not even appear in the process list, compared to the 100% CPU usage one could get using a more naive approach.

Code at github.

References

  1. Implement A gpio function with a callback calling a asyncio method

  2. Python - timer with asyncio coroutine

  3. “Please enter password for encrypted keyring” when running Python script on Ubuntu