Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,25 @@ REFRESH_SECS=3
# Cross-midnight windows are supported, e.g. 22,7 means 22:00 through 07:59.
ACTIVE_HOURS=22,7

# --- Local / Drive-backed photos (preferred for photos-shuffle.py) ---
# Preferred primary source for shuffled photos.
LOCAL_PHOTOS_DIR=~/zero2dash/photos
# Optional: Google Drive folder ID used by scripts/drive-sync.py to populate LOCAL_PHOTOS_DIR.
GOOGLE_DRIVE_FOLDER_ID=
# Optional: service account JSON used by scripts/drive-sync.py.
GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON=~/zero2dash/drive-service-account.json
# Optional sync state path for scripts/drive-sync.py
GOOGLE_DRIVE_SYNC_STATE_PATH=~/zero2dash/cache/drive_sync_state.json
# Optional resize state path for scripts/photo-resize.py
PHOTO_RESIZE_STATE_PATH=~/zero2dash/cache/photo_resize_state.json
# Optional resize script path used by scripts/drive-sync.py after downloads
PHOTO_RESIZE_SCRIPT=~/zero2dash/scripts/photo-resize.py

# --- Google Photos (photos-shuffle.py) ---
# Required: Album ID to pull shuffled photos from.
GOOGLE_PHOTOS_ALBUM_ID=replace-with-google-photos-album-id
# Optional: app-created album ID for Google Photos fallback only.
# Normal personal/shared Google Photos albums are no longer suitable; use LOCAL_PHOTOS_DIR,
# optionally populated by Google Drive sync.
GOOGLE_PHOTOS_ALBUM_ID=
# OAuth credentials are required by one of these methods:
# Use a Desktop OAuth client. If the Google app is in testing, add your account as a consent-screen test user.
# 1) Existing client secrets file (default path shown), OR
Expand All @@ -81,3 +97,5 @@ LOGO_PATH=/images/goo-photos-icon.png
# OAUTH_OPEN_BROWSER default: 0 (false)
# Loopback OAuth only: complete sign-in on the same machine, or use SSH port forwarding for headless Pi setup.
OAUTH_OPEN_BROWSER=0


5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ __pycache__/
*.pyc
.env
.env.local
photos/*
!photos/.gitkeep
cache/drive_sync_state.json
cache/photo_resize_state.json

30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ Google OAuth notes:
- If the Google consent screen is in testing, add your account as a test user.
- `calendash-api.py` defaults `GOOGLE_TOKEN_PATH` to `token.json` relative to `/opt/zero2dash` under systemd; `photos-shuffle.py` must keep using a separate `GOOGLE_TOKEN_PATH_PHOTOS`.

Drive-backed photos notes:

- `scripts/photos-shuffle.py` now treats `LOCAL_PHOTOS_DIR` as the primary source.
- Use `scripts/drive-sync.py` to populate that directory from a shared Google Drive folder.
- `scripts/photo-resize.py` proportionally reduces changed images to 50% before they are reused locally.
- Normal personal/shared Google Photos albums are no longer a reliable headless source; if you still configure `GOOGLE_PHOTOS_ALBUM_ID`, treat it as an app-created-album fallback only.

## Run via systemd
Install and enable canonical units:

Expand Down Expand Up @@ -138,12 +145,33 @@ journalctl -u pihole-display-dark.service -n 50 --no-pager

Use `python3 scripts/photos-shuffle.py --check-config` to validate the configuration and print the credential source that will be used.

## Drive-backed photo sync

Use a shared Google Drive folder when you want remote photo management without depending on the now-hobbled Google Photos album API.

Required configuration:

- `LOCAL_PHOTOS_DIR`
- `GOOGLE_DRIVE_FOLDER_ID`
- `GOOGLE_DRIVE_SERVICE_ACCOUNT_JSON`

Recommended workflow:

```sh
python3 scripts/drive-sync.py
python3 scripts/photos-shuffle.py --test
```

`drive-sync.py` downloads images from the shared Drive folder into `LOCAL_PHOTOS_DIR` and then runs `photo-resize.py`, which shrinks new or changed images to 50% of their original width and height before reuse.

## Notes

- `display_rotator.py` excludes `piholestats_v1.2.py` by default so day mode and night mode stay distinct.
- `display_rotator.py` excludes `piholestats_v1.2.py`, `calendash-api.py`, `_config.py`, `drive-sync.py`, and `photo-resize.py` by default so helper scripts do not end up in the day rotator.
- 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.


### Framebuffer overrides in systemd

Both canonical service units now set `FB_DEVICE=/dev/fb1` by default and load `/opt/zero2dash/.env` afterward, so setting `FB_DEVICE` in `.env` overrides the unit default without editing unit files.


9 changes: 8 additions & 1 deletion display_rotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@

DEFAULT_PAGES_DIR = "scripts"
DEFAULT_PAGE_GLOB = "*.py"
DEFAULT_EXCLUDE_PATTERNS = ["piholestats_v1.2.py", "calendash-api.py"]
DEFAULT_EXCLUDE_PATTERNS = [
"piholestats_v1.2.py",
"calendash-api.py",
"_config.py",
"drive-sync.py",
"photo-resize.py",
]
DEFAULT_INCLUDE_PATTERNS: list[str] = []
DEFAULT_ROTATE_SECS = 30
SHUTDOWN_WAIT_SECS = 5
Expand Down Expand Up @@ -1012,3 +1018,4 @@ def request_stop(signum: int, _frame: object) -> None:

if __name__ == "__main__":
raise SystemExit(main())

1 change: 1 addition & 0 deletions photos/.gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

10 changes: 10 additions & 0 deletions pr-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Summary
- require an explicit scheme for remote Pi-hole hosts and validate TLS/timeout-related configuration
- improve Pi-hole auth handling in `piholestats_v1.3.py`, including v6-session vs legacy-token mode detection and clearer auth/transport failure reporting
- tighten Google Calendar and Photos OAuth handling around loopback redirects, Desktop OAuth clients, and token scope parsing
- add a Photos `--auth-only` mode and improve token-path separation from the calendar flow
- update `.env.example` and `README.md` to document the revised Pi-hole and Google OAuth setup

## Testing
- not run from this Codex environment

10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Runtime Python dependencies for zero2dash scripts.
# System tools such as git, gh, systemd, and framebuffer drivers are not pip packages.

Pillow>=10.0
python-dotenv>=1.0
pytz>=2024.1
google-auth>=2.0
google-auth-oauthlib>=1.2
google-auth-httplib2>=0.2
google-api-python-client>=2.0
5 changes: 3 additions & 2 deletions scripts/calendash-img.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
TOUCH_DEVICE_DEFAULT = os.environ.get("TOUCH_DEVICE", "/dev/input/event0")
WIDTH_DEFAULT = int(os.environ.get("FB_WIDTH", "320"))
HEIGHT_DEFAULT = int(os.environ.get("FB_HEIGHT", "240"))
DEFAULT_IMAGE = Path(__file__).resolve().parent.parent / "images" / "calendash" / "output.png"
DEFAULT_IMAGE = Path(__file__).resolve().parent.parent / "images" / "calendash.png"
INPUT_EVENT_STRUCT = struct.Struct("llHHI")
EV_KEY = 0x01
BTN_TOUCH = 0x14A
Expand All @@ -41,7 +41,7 @@ def rgb888_to_rgb565(image: Image.Image) -> bytes:

def load_frame(image_path: Path, width: int, height: int) -> Image.Image:
if not image_path.exists():
raise FileNotFoundError(f"Background image not found: {image_path}")
raise FileNotFoundError(f"Calendar image not found: {image_path}")
return Image.open(image_path).convert("RGB").resize((width, height), RESAMPLING_LANCZOS)


Expand Down Expand Up @@ -154,3 +154,4 @@ def main() -> int:

if __name__ == "__main__":
raise SystemExit(main())

Loading