This score renders Pi-hole state on the Tempo timeline: whether DNS blocking is enabled or disabled, whether Pi-hole is reachable, and, optionally, whether a component update is available, whether the blocklist (“gravity”) was refreshed, and whether the host is under high load. It is read-only. The actions open the admin pages or copy the server URL, and nothing writes back to Pi-hole.

Pi-hole events on the Tempo timeline

Pi-hole has no outbound webhook, so it cannot push to Tempo on its own. A small helper script polls Pi-hole’s API and posts an event to Tempo when something changes. Most of the work below is setting up that helper once.

How it works

Pi-hole  (FTL HTTP API, v6)
      |  POST /api/auth  ->  session id (SID)
      |  GET  /api/dns/blocking, /api/info/version, ...
helper  on a host near Pi-hole   (reads state, builds a Tempo event)
      |  HTTP POST, LAN
Tempo ingestion server  on <mac>:7776
      |  DELETE /api/auth   (frees the API session)

The helper is an API poller. On each run it authenticates once with POST /api/auth, reads the current state, posts a Tempo event only when the state changed since the last run, then deletes its session with DELETE /api/auth. Authenticating every run without deleting the session eventually exhausts Pi-hole’s API seats and returns api_seats_exceeded, so the cleanup matters.

The helper is stateful. It keeps the last reported value on disk (under $XDG_STATE_HOME/pihole-tempo/, or ~/.local/state/pihole-tempo/) and posts only on a transition, so a steady Pi-hole produces no feed noise. Tempo collapses the events into one row per signal through grouping.

What you need

The Pi-hole password is used only against Pi-hole to open a session. Tempo receives only the parsed state, never the password.

1. Create the Tempo token

In Tempo, open Settings > Ingestion and create a token bound to net.pi-hole.pi-hole. Copy it. Note your Tempo endpoint: http://<mac-running-tempo>:7776/ingest.

The host running the helper must reach the Mac on port 7776. Allow it in the macOS firewall (or Little Snitch), and optionally restrict the token to that host’s IP with its allowlist in Settings > Ingestion.

2. Get the helper

In the score’s Source tab (Score Editor), the Helper section has Open in Finder and Open README. Open in Finder copies the helper package to ~/Library/Application Support/Tempo/Integrations/net.pi-hole.pi-hole/ and reveals it. Copy that folder to the host that will run the poller, then follow its Open README or the steps below.

The package contains pihole-tempo.sh (one poll, posts on a state change) and pihole-run.sh (loads pihole.env and runs the watcher once, for cron or a service).

3. Configure pihole.env

Create pihole.env next to the scripts:

export TEMPO_URL="http://<mac-running-tempo>:7776/ingest"
export PIHOLE_URL="http://<pihole-host>"

# Secrets read from their own chmod 600 files, not stored in this env file:
export TEMPO_TOKEN_FILE="$HOME/.pihole-tempo/tempo-token"
export PIHOLE_PASS_FILE="$HOME/.pihole-tempo/pihole-pass"

Use a Pi-hole app-password rather than the admin password. To keep it simple you can set TEMPO_TOKEN and PIHOLE_PASS inline instead, since the env file is chmod 600 (see below).

Optional signals

Blocking state and reachability are always reported. Three more signals are on by default and each can be switched off independently:

SignalEnv flag (default on)What triggers it
Update availablePIHOLE_EMIT_UPDATEany component (core / web / FTL / docker) has local != remote
Blocklist (gravity) updatedPIHOLE_EMIT_GRAVITYgravity’s blocked-domain count changes
High loadPIHOLE_EMIT_LOAD15-minute CPU load over PIHOLE_LOAD_THRESHOLD (percent of one core, default 200)

Set a flag to 0 to silence that signal. Load is kept ignorable on purpose: CPU load is noisy on a DNS resolver, so set PIHOLE_EMIT_LOAD=0 if you already watch host load with another tool such as Beszel.

Keeping secrets out of plaintext (*_FILE)

You do not have to leave the token or the Pi-hole password in a plaintext .env. Each secret also resolves from a file: set TEMPO_TOKEN_FILE and PIHOLE_PASS_FILE to paths and the helper reads them from there, so the values never enter the environment. Point them at a Docker secret (/run/secrets/..., on tmpfs), a systemd-creds encrypted credential, or any chmod 600 file. Resolution order per secret is file, then environment variable.

4. Run the helper as a daemon

pihole-run.sh performs one poll. Run it on a roughly 15-second cadence so transitions are caught quickly. Keep pihole.env, the log, and the state file out of version control.

macOS (launchd)

Run the watcher in a loop under a LaunchAgent and let launchd restart it on logout or reboot. A minimal agent that keeps a sleep loop alive:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>app.tempo.pihole</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>while true; do /path/to/pihole-run.sh; sleep 15; done</string>
  </array>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
</dict>
</plist>

Save it as ~/Library/LaunchAgents/app.tempo.pihole.plist, then load it:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.tempo.pihole.plist
launchctl list | grep app.tempo.pihole

Linux (systemd)

Create /etc/systemd/system/tempo-pihole.service:

[Unit]
Description=Tempo Pi-hole poller
After=network-online.target

[Service]
EnvironmentFile=/opt/tempo-pihole/pihole.env
ExecStart=/bin/sh -c 'while true; do /opt/tempo-pihole/pihole-run.sh; sleep 15; done'
Restart=always

[Install]
WantedBy=multi-user.target

Then sudo systemctl enable --now tempo-pihole.

Without systemd, a flock keepalive cron works. The runner is idempotent, so staggering four runs per minute gives roughly a 15-second cadence:

* * * * * for i in 0 15 30 45; do (sleep $i; flock -n /tmp/pihole-tempo.lock /opt/tempo-pihole/pihole-run.sh) & done

Windows

The helper is a POSIX shell script and is not cross-platform on its own. Run it under WSL or a POSIX shell with python3, curl, and jq available, then keep the loop alive with a process supervisor (a Scheduled Task that runs at logon and restarts on failure, or a service wrapper such as NSSM).

What you’ll see

A Pi-hole that is down or unreachable means your network’s DNS is down, which is the highest-value signal here.

Grouping and severity

Events stack within a 6-hour window, keyed by ${metadata.ServerUrl}/${metadata.Action}. Each signal from one Pi-hole reads as a single entry rather than one row per poll. For a multi-instance setup (primary plus secondary Pi-hole), run one helper per instance with its own PIHOLE_URL and PIHOLE_PASS. Tempo lists them under the same source, but each event carries its own ServerUrl, so they group separately.

Severity comes from the score’s rules on the Status and Action metadata:

MatchSeverityBadge
Status: downcriticalDown
Status: unreachablecriticalUnreachable
Status: high_loadwarningHigh load
Status: disabledwarningDisabled
Action: blocking_disabledwarningBlocking off
Action: update_availableinfoUpdate
Action: gravity_updateinfoGravity
Action: blocking_enabledinfoBlocking on
(default)infoInfo

Metadata the score reads

Actions

Four actions are attached to every event:

The score uses only openURL and copyToClipboard. Terminal-based actions (for example pihole disable 30m) would require a local drop-in score that you author and trust yourself.

Troubleshooting and limitations

Pi-hole v5 note

v6 is the target. v5 works with adjustments: it uses the legacy PHP API (/admin/api.php) authenticated with the web API token (admin Settings > API > Show API token) rather than POST /api/auth, and the admin URL paths differ slightly. The signals and the score are otherwise the same.