From e0f00341d128ffd9d58c07b7a88e7b7595aed35a Mon Sep 17 00:00:00 2001 From: aSbiEL0 <76663314+aSbiEL0@users.noreply.github.com> Date: Sat, 7 Mar 2026 16:51:55 -0800 Subject: [PATCH 1/2] docs: canonize service names and add compatibility matrix --- README.md | 328 +++++++++--------------------------- scripts/piholestats_v1.1.py | 1 + systemd/day.timer | 22 +-- systemd/night.timer | 22 +-- 4 files changed, 101 insertions(+), 272 deletions(-) diff --git a/README.md b/README.md index 0483aa6..9e98b78 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,106 @@ -Minimal framebuffer-based Pi-hole dashboard for Raspberry Pi (320×240 SPI TFT). +# zero2dash -No X11 -No SDL -Direct /dev/fb1 RGB565 rendering -Suggested project structure +Lightweight framebuffer dashboards for a 320×240 SPI TFT on Raspberry Pi. + +- No X11/Wayland +- No SDL +- Direct rendering to `/dev/fb1` (RGB565) + +## Canonical service names and script targets + +Use the following names as the **source of truth** for systemd-managed runtime modes. + +| Service name | Script target | Mode | +| --- | --- | --- | +| `display.service` | `display_rotator.py` | **Day rotator** (multi-page cycle, touch navigation) | +| `pihole-display-dark.service` | `scripts/piholestats_v1.2.py` | **Night dark mode** (single Pi-hole dashboard) | + +### Compatibility table (service → script → mode) + +| Service | Script path | Mode / status | +| --- | --- | --- | +| `display.service` | `/opt/zero2dash/display_rotator.py` | Canonical day mode | +| `pihole-display-dark.service` | `/opt/zero2dash/scripts/piholestats_v1.2.py` | Canonical night mode | +| `day-mode.service` | *(legacy alias; not shipped in this repo)* | Legacy naming; replace with `display.service` | +| `dark-mode.service` | *(legacy alias; not shipped in this repo)* | Legacy naming; replace with `pihole-display-dark.service` | + +## Repository layout + +```text zero2dash/ +├── display_rotator.py ├── scripts/ │ ├── pihole-display-pre.sh -│ ├── piholestats_v1.0.py -│ ├── piholestats_v1.1.py -│ ├── piholestats_v1.2.py -│ └── test.py +│ ├── piholestats_v1.1.py # legacy daytime variant +│ ├── piholestats_v1.2.py # canonical dark-mode service target +│ ├── calendash-api.py +│ ├── calendash-img.py +│ ├── google-photos.py +│ ├── photos-shuffle.py +│ ├── tram-info.py +│ └── weather-dash.py ├── systemd/ │ ├── display.service │ ├── pihole-display-dark.service │ ├── day.timer │ └── night.timer └── README.md -Requirements -Raspberry Pi OS (SPI enabled) -Python 3 -Pillow -systemd -Install -1. Install TFT driver -sudo rm -rf LCD-show -git clone https://github.com/goodtft/LCD-show.git -cd LCD-show -sudo ./LCD24-show -Reboot → display active on /dev/fb1. - -2. Install Python dependency -sudo apt install -y python3-pip python3-pil -3. Deploy project files -sudo mkdir -p /opt/zero2dash -sudo cp -r . /opt/zero2dash/ -sudo chmod +x /opt/zero2dash/scripts/pihole-display-pre.sh -sudo chmod +x /opt/zero2dash/scripts/test.py -Configure -Create an environment file and keep secrets out of source control: - -cp /opt/zero2dash/.env.example /opt/zero2dash/.env -chmod 600 /opt/zero2dash/.env - -Edit `/opt/zero2dash/.env` and set: - -PIHOLE_HOST -PIHOLE_PASSWORD -PIHOLE_API_TOKEN (optional; enables legacy /admin/api.php fallback) -REFRESH_SECS -ACTIVE_HOURS -Run via systemd -If you run scripts manually, load env vars first: - -set -a -source /opt/zero2dash/.env -set +a - -sudo cp /opt/zero2dash/systemd/display.service /etc/systemd/system/ -sudo cp /opt/zero2dash/systemd/pihole-display-dark.service /etc/systemd/system/ -sudo systemctl daemon-reload -sudo systemctl enable --now display.service -Check logs: - -A lightweight Pi‑hole dashboard designed for small TFT displays (320×240) on a Raspberry Pi. It renders directly to the framebuffer (`/dev/fb1`) without requiring X11 or SDL, making it ideal for headless setups. The project provides a set of dashboard scripts that display Pi‑hole statistics, CPU temperature and device uptime. A companion rotator script allows multiple dashboards to cycle on a timer with simple touch controls for navigation and screen power management. +``` -## Features +## Stale / legacy references removed -- **Direct framebuffer rendering** – uses the Pillow library to draw RGB565 frames directly to `/dev/fb1` with no desktop environment or SDL dependency. -- **Rotating dashboards** – `display_rotator.py` scans a directory for dashboard scripts and cycles through them on a configurable interval. -- **Touch navigation** – taps on the left/right side of the screen move to the previous/next dashboard; double‑tapping turns the screen on or off. -- **Dark and day modes** – separate dashboard scripts and systemd services for day and night display profiles. -- **Systemd integration** – sample `*.service` and `*.timer` units to start the dashboards on boot and automatically switch between day and night modes. -- **Test mode** – a `test.py` script renders a placeholder image for quick verification without connecting to the Pi‑hole API. +- `scripts/piholestats_v1.0.py` and `scripts/test.py` are not part of this repository and should not be used in deployment docs. +- `day-mode.service` and `dark-mode.service` are treated as **legacy names** only. ## Requirements -- Raspberry Pi OS with SPI and the TFT display enabled -- Python 3 -- [Pillow](https://python-pillow.org/) library -- `systemd` for service management -- Pi‑hole v6 API accessible over the network +- Raspberry Pi OS (SPI enabled) +- Python 3 +- Pillow (`python3-pil`) +- systemd +- Pi-hole API connectivity ## Installation -1. **Prepare the TFT display** - - ```sh - sudo rm -rf LCD-show - git clone https://github.com/goodtft/LCD-show.git - cd LCD-show - sudo ./LCD24-show - # reboot to activate /dev/fb1 - ``` - -2. **Install dependencies** - - ```sh - sudo apt update - sudo apt install -y python3-pip python3-pil - ``` - -3. **Deploy the application** - - ```sh - sudo mkdir -p /opt/zero2dash - sudo cp -r . /opt/zero2dash/ - sudo chmod +x /opt/zero2dash/scripts/pihole-display-pre.sh - sudo chmod +x /opt/zero2dash/scripts/test.py - ``` - -## Configuration - -Edit `scripts/piholestats_v1.2.py` (or the version you use) to point at your Pi‑hole instance: +```sh +sudo rm -rf LCD-show +git clone https://github.com/goodtft/LCD-show.git +cd LCD-show +sudo ./LCD24-show +# reboot to activate /dev/fb1 +``` -```python -PIHOLE_HOST = "192.168.0.x" # address of your Pi‑hole -PIHOLE_PASSWORD = "your_password" # Pi‑hole v6 admin password -REFRESH_SECS = 3 # seconds between API updates -ACTIVE_HOURS = (22, 7) # hours to run this dark mode dashboard +```sh +sudo apt update +sudo apt install -y python3-pip python3-pil ``` -The `display_rotator.py` script uses environment variables to discover and control dashboards: +```sh +sudo mkdir -p /opt/zero2dash +sudo cp -r . /opt/zero2dash/ +sudo chmod +x /opt/zero2dash/scripts/pihole-display-pre.sh +``` -- `ROTATOR_PAGES_DIR` – directory containing dashboard scripts (default: `scripts`) -- `ROTATOR_PAGE_GLOB` – glob pattern for dashboard filenames (default: `*.py`) -- `ROTATOR_EXCLUDE_PATTERNS` – comma‑separated list of patterns to ignore (default: `piholestats_v1.2.py,calendash-api.py`) -- `ROTATOR_SECS` – seconds to show each page before rotating (minimum 5) -- `ROTATOR_TOUCH_WIDTH` – touch‑sensitive width threshold for navigation -- `ROTATOR_PAGES` – optional explicit comma‑separated list of scripts to rotate +## Configuration -To experiment locally without a framebuffer, run: +Create and secure an env file: ```sh -python3 scripts/test.py --output /tmp/test.png --no-framebuffer +cp /opt/zero2dash/.env.example /opt/zero2dash/.env +chmod 600 /opt/zero2dash/.env ``` -## Running via systemd +Set at minimum: -Two service units are provided to run the dashboards: +- `PIHOLE_HOST` +- `PIHOLE_PASSWORD` +- `PIHOLE_API_TOKEN` (optional fallback) +- `REFRESH_SECS` +- `ACTIVE_HOURS` -- **Day display** (`display.service`) – launches `display_rotator.py` and cycles through dashboard pages. A `day.timer` can start this service at 07:00 each day. -- **Night display** (`pihole-display-dark.service`) – runs the dark‑mode dashboard script. A `night.timer` can start this service at 22:00 each night. +## Run via systemd -Copy the desired units into `/etc/systemd/system` and enable them: +Install and enable canonical units: ```sh sudo cp /opt/zero2dash/systemd/*.service /etc/systemd/system/ @@ -153,143 +110,14 @@ sudo systemctl enable --now display.service sudo systemctl enable --now day.timer night.timer ``` -Check logs with: +Useful checks: ```sh journalctl -u display.service -n 50 --no-pager +journalctl -u pihole-display-dark.service -n 50 --no-pager ``` -## Development - -The project is written in Python with an emphasis on readability and minimal dependencies. Dashboard scripts follow a simple pattern: fetch data from the Pi‑hole API, draw onto a PIL `Image`, then write the frame to the framebuffer using the `rgb888_to_rgb565` helper. Additional dashboards can be created by following the structure in `scripts/piholestats_v1.2.py`. - -## License - -Default backgrounds are loaded from the `images/` directory. - - -Touch controls and static pages -- Single tap left/right changes page. -- Double tap toggles screen power: OFF blanks/powers down the panel output, ON restores panel output. The rotator and page scripts keep running in both states (falls back to `vcgencmd display_power` when framebuffer blanking is unsupported). -- Image-only scripts can exit immediately; the rotator now keeps each page on-screen for `ROTATOR_SECS` unless you tap to switch. - -## Google Calendar image generator (`scripts/calendash-api.py`) - -This script creates a daily 320×240 PNG summary of upcoming Google Calendar events. It is designed to run separately from the rotator so image generation work is done once at 06:00 instead of every page cycle. - -### Install dependencies - -```sh -python3 -m pip install google-api-python-client google-auth-oauthlib python-dotenv pillow pytz -``` - -### Configure - -1. Copy and edit env file: - - ```sh - cp .env.example .env - chmod 600 .env - ``` - -2. Set these variables in `.env`: - - `GOOGLE_CLIENT_ID` - - `GOOGLE_CLIENT_SECRET` - - Optional: `GOOGLE_CALENDAR_CLIENT_ID`, `GOOGLE_CALENDAR_CLIENT_SECRET` (dedicated Calendar OAuth client) - - `GOOGLE_CALENDAR_ID` (for personal calendar use `primary`) - - `OUTPUT_PATH` (recommended: `~/zero2dash/images/calendash.png`) - - `BACKGROUND_IMAGE` - - `ICON_IMAGE` - - `CALENDASH_FONT_PATH` (optional comma-separated font file paths; first existing path is used) - - `TIMEZONE` (example: `Europe/London`) - - `OAUTH_PORT` (optional, default `8080`) - -3. Place your assets: - - `BACKGROUND_IMAGE`: 320×240 background containing the Google Calendar logo/header. - - `ICON_IMAGE`: small calendar icon used in each event row. - - `CALENDASH_FONT_PATH`: optional fallback list to try alternate fonts, e.g. `/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf,/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf`. - -### First run (OAuth) - -Before first run, in Google Cloud Console open your OAuth client and ensure this redirect URI is allowed (replace `8080` if you set `OAUTH_PORT` differently): - -```text -http://localhost:8080/ -``` - -Run once manually to complete OAuth2 login (local server flow) and create `token.json` in the repo root. When prompted, open the printed `http://localhost:/` URL in a browser on the same machine (or via SSH port forwarding), approve access, and wait for the terminal to confirm the token was saved: - -```sh -python3 scripts/calendash-api.py -``` - -After first auth, future runs refresh tokens automatically and are suitable for headless cron execution. - - -### Token separation (important) - -Use separate OAuth token files per script to avoid scope conflicts: - -- `scripts/calendash-api.py` (Calendar): `GOOGLE_TOKEN_PATH` (default `token.json`) -- `scripts/photos-shuffle.py` (Photos): `GOOGLE_TOKEN_PATH_PHOTOS` (default `~/zero2dash/token_photos.json`) - -Do **not** point the Photos script at `token.json`. The Photos script now blocks that configuration and asks for a separate token path. - -If you see **"app restricted"** in browser consent, create **separate Desktop OAuth clients** in Google Cloud: one for Calendar and one for Photos. Then set: - -- Calendar: `GOOGLE_CALENDAR_CLIENT_ID` / `GOOGLE_CALENDAR_CLIENT_SECRET` -- Photos: `GOOGLE_PHOTOS_CLIENT_ID` / `GOOGLE_PHOTOS_CLIENT_SECRET` (or `GOOGLE_PHOTOS_CLIENT_SECRETS_PATH`) - -Also keep the consent screen in **Testing** and add your Google account as a **Test user**. - -### OAuth troubleshooting (`localhost` connection refused after clicking **Continue**) - -If Google sign-in works but the final redirect page fails with `localhost refused to connect`, the browser and `calendash-api.py` are usually not on the same network namespace. - -- If script and browser are on the same machine, re-run the script and immediately open the exact URL it prints. -- If script runs on a remote Pi/VM over SSH and browser runs on your laptop, create an SSH tunnel before authorizing: - - ```sh - ssh -L 8080:localhost:8080 @ - ``` - - Keep that SSH session open, then run `python3 scripts/calendash-api.py` on the remote host and complete Google consent from your local browser. - -- Ensure `.env` `OAUTH_PORT` and Google OAuth redirect URI match exactly, including trailing slash: - - ```text - http://localhost:8080/ - ``` - -- If port `8080` is already used, set another value for `OAUTH_PORT` (for example `8090`) and add the matching URI in Google Cloud Console. -- If you are using an SSH tunnel, a message like `channel 3: open failed: connect failed: Connection refused` can appear after consent when the browser makes an extra request (for example `/favicon.ico`) after the local OAuth server already shut down. If `Saved OAuth token...` and `Wrote image: ...` are logged, OAuth completed successfully. - -### Daily cron at 06:00 (local time) - -```cron -0 6 * * * cd /opt/zero2dash && /usr/bin/python3 /opt/zero2dash/scripts/calendash-api.py >> /var/log/calendash.log 2>&1 -``` - -The script fetches events from **today 00:00** to **+3 calendar days 23:59**, retries API/network failures with exponential backoff, and writes either the event summary image, a no-events image, or an error image. - -## Calendar split workflow (generator + image-only display) - -Use two independent scripts to reduce steady-state CPU and memory use: - -1. `scripts/calendash-api.py` (scheduled, e.g. 06:00) - - Calls Google Calendar API - - Renders `images/calendash.png` -2. `scripts/calendash-img.py` (runtime display script) - - Displays the pre-rendered image - - Waits for either a touch event or a timeout, then exits - -`calendash-api.py` is excluded from rotator discovery by default because it is a generator, but `calendash-img.py` is **not** excluded so you can rotate it like any other display page. - -Example: - -```sh -python3 scripts/calendash-api.py -python3 scripts/calendash-img.py --timeout 30 --touch-device /dev/input/event0 -``` +## Notes -If `--touch-device` does not exist, `calendash-img.py` safely falls back to timeout-only behavior. +- `display_rotator.py` excludes `piholestats_v1.2.py` by default so day mode and night mode stay distinct. +- Static image scripts (for example `tram-info.py`, `weather-dash.py`, `calendash-img.py`) are rotator-friendly page scripts, not systemd service units by themselves. diff --git a/scripts/piholestats_v1.1.py b/scripts/piholestats_v1.1.py index 78b98f2..d40e927 100644 --- a/scripts/piholestats_v1.1.py +++ b/scripts/piholestats_v1.1.py @@ -2,6 +2,7 @@ # Pi-hole TFT Dashboard -> direct framebuffer RGB565 (no X, no SDL) # v6 auth handled elsewhere; this file only renders and calls API # Version 1.1 - Introducing dark mode +# LEGACY: kept for compatibility/manual use; canonical night service uses piholestats_v1.2.py import os, sys, time, json, urllib.request, urllib.parse, mmap, struct, argparse from pathlib import Path diff --git a/systemd/day.timer b/systemd/day.timer index cf2c24e..76c5095 100644 --- a/systemd/day.timer +++ b/systemd/day.timer @@ -1,11 +1,11 @@ -[Unit] -Description=Start DAY GUI at 07:00 - -[Timer] -OnCalendar=*-*-* 07:00:00 -Persistent=true -AccuracySec=1min -Unit=day-mode.service - -[Install] -WantedBy=timers.target +[Unit] +Description=Start DAY GUI at 07:00 + +[Timer] +OnCalendar=*-*-* 07:00:00 +Persistent=true +AccuracySec=1min +Unit=display.service + +[Install] +WantedBy=timers.target diff --git a/systemd/night.timer b/systemd/night.timer index b71fefb..47de248 100644 --- a/systemd/night.timer +++ b/systemd/night.timer @@ -1,11 +1,11 @@ -[Unit] -Description=Start NIGHT GUI at 22:00 - -[Timer] -OnCalendar=*-*-* 22:00:00 -Persistent=true -AccuracySec=1min -Unit=dark-mode.service - -[Install] -WantedBy=timers.target +[Unit] +Description=Start NIGHT GUI at 22:00 + +[Timer] +OnCalendar=*-*-* 22:00:00 +Persistent=true +AccuracySec=1min +Unit=pihole-display-dark.service + +[Install] +WantedBy=timers.target From a87850471d95dd09ddcf2157d9ad596dc8414d23 Mon Sep 17 00:00:00 2001 From: Codex <242516109+Codex@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:03:15 +0000 Subject: [PATCH 2/2] Initial plan (#57) Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com>