This is for automating the syncing of sniffspot and Swimply calendars and adding 30 minute buffers before and after the reservations. I still have to add the buffers for Sniffspot, but it automates 75% of reservations on my calendar
Sniffspot sends reservation events to my Google Calendar. Swimply pulls from that same calendar to determine when I’m available, and opens up booking slots around what it sees. Left alone, this let people book back-to-back with zero turnaround time between guests — no time to clean up, reset, or travel between sessions.
This automation scans the calendar every 15 minutes, finds reservation events matching target platforms (currently: Sniffspot), and injects two independent 30-minute “buffer” events — one immediately before, one immediately after — so Swimply sees those slots as unavailable too. It also now cleans up buffers whose original reservation was cancelled or rescheduled, so cancellations don’t permanently eat availability (see “Cleanup pass” below — added 2026-07-19).
Current limitation: this only protects Swimply from booking over a Sniffspot buffer. It does not yet push buffer info back to Sniffspot, so a Sniffspot booking can still land inside a buffer created for a different Sniffspot reservation. I still manually block those on Sniffspot’s side. This automation removes an estimated 75% of the double-booking problem, not all of it.
token.json if present. If the access token is expired but a refresh token exists, refreshes silently. If there’s no valid token at all, launches an interactive browser-based OAuth flow (flow.run_local_server) — this is the step that fails with webbrowser.Error: could not locate runnable browser when it happens under cron with no display available.CALENDAR_ID, a dedicated Google Calendar — not your primary one)."sniffspot"? (case-insensitive, so “Sniffspot”, “SNIFFSPOT”, etc. all match)"buffer"? (this is what stops the script from creating buffers around its own buffer events, which would otherwise cascade forever)Sniffspot Before Buffer (MMDD-HHMM) / Sniffspot After Buffer (MMDD-HHMM), keyed off the original reservation’s start time. Before creating one, the script checks it against the list of titles already on the calendar in the scan window. If it’s already there, it skips — this is why running the script extra times (or every 15 min) doesn’t create duplicate buffers.America/Phoenix timezone.CALENDAR_ID and the OAuth values in credentials.json / token.json are all sensitive — none of these three should ever be committed to git. Worth double-checking your .gitignore in cyber-kb (or wherever this script’s repo lives) explicitly excludes credentials.json and token.json, the same way you already handle the backup password file.What prompted the fix: cancelled Sniffspot reservations left their buffer events behind permanently, which Swimply then read as unavailable time — silently costing real bookable availability with no way to notice except manually cross-checking the calendar.
First real run after deploying, output:
🗑️ Deleted orphaned buffer: Sniffspot Before Buffer (0718-1730) (original reservation no longer exists)
🗑️ Deleted orphaned buffer: Sniffspot After Buffer (0718-1730) (original reservation no longer exists)
🗑️ Deleted orphaned buffer: Sniffspot Before Buffer (0719-1200) (original reservation no longer exists)
🗑️ Deleted orphaned buffer: Sniffspot After Buffer (0719-1200) (original reservation no longer exists)
🗑️ Deleted orphaned buffer: Sniffspot Before Buffer (0719-1700) (original reservation no longer exists)
🗑️ Deleted orphaned buffer: Sniffspot After Buffer (0719-1700) (original reservation no longer exists)
✅ Created buffer: Sniffspot Before Buffer (0719-1100) (10:30 - 11:00)
✅ Created buffer: Sniffspot After Buffer (0719-1100) (12:00 - 12:30)
Caught 3 stale reservation-pairs (6 orphaned buffer events) immediately on first run, and correctly still created a fresh pair for a genuinely live reservation in the same pass — confirming cleanup and creation don’t interfere with each other.
Rollback: a pre-change backup is kept at buffer_automation.py.bak in the project folder. If the cleanup pass ever needs to be reverted, restore that file.
Manual test procedure (for future changes to this logic):
cd ~/Projects/calendar-automation
source .venv/bin/activate
python3 buffer_automation.py
deactivate
Look for 🗑️ Deleted orphaned buffer: lines to confirm cleanup is firing correctly.
Sniffspot ──(writes reservation)──> Google Calendar <──(reads availability)── Swimply
▲
│
buffer_automation.py (cron, every 15 min)
cleans up orphaned buffers, then creates new ones
~/Projects/calendar-automation/
├── .venv/ # isolated Python environment (see below)
├── buffer_automation.py # main script
├── buffer_automation.py.bak # pre-cleanup-pass backup, kept for rollback
├── token.json # OAuth2 refresh/access token (DO NOT commit to git)
├── credentials.json # OAuth2 client secret from Google Cloud (DO NOT commit to git)
└── automation.log.old # retired log from before Discord alerting was added
Related files elsewhere on the system:
| What | Where |
|---|---|
| Wrapper script (adds Discord alerting) | /usr/local/bin/run-buffer-automation.sh |
| Current log (from wrapper, timestamped) | ~/calendar-automation.log |
| Discord webhook URL (chmod 600) | ~/.config/backup/discord_webhook.txt |
| Old pre-wrapper log | ~/Projects/calendar-automation/automation.log.old |
Modern Ubuntu marks the system Python as externally-managed-environment, blocking global pip install. This project uses an isolated virtual environment instead.
One-time setup:
cd ~/Projects/calendar-automation
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade google-auth-oauthlib google-api-python-client
deactivate
Manual testing / running by hand — always activate the venv first:
cd ~/Projects/calendar-automation
source .venv/bin/activate
python3 buffer_automation.py
deactivate # when done
Note: deactivate only affects your terminal’s shell state (which Python it uses next) — it has no effect on the script that already ran, since that already completed against the actual Google Calendar independent of the shell. Safe to skip if you’re closing the terminal anyway.
Cron / background execution — cron does NOT use source activate. Instead, the venv’s own Python binary is called directly by absolute path, which achieves the same isolation without needing an interactive shell:
/home/aztechguy/Projects/calendar-automation/.venv/bin/python
Hosting-Automation (Google Cloud Console)Calendar-Script (Desktop app type), created via APIs & Services → CredentialsIf Publishing status is set to Testing, Google issues refresh tokens that expire after 7 days — this is a hard platform rule, not a bug in the script. Symptom: automation runs fine for about a week, then every run fails with:
invalid_grant: Token has been expired or revoked.
Fix (one-time): Cloud Console → OAuth consent screen → Audience → Publish app. For personal-use scopes like Calendar, this does not trigger Google’s formal verification review — it just removes the 7-day cap. After publishing, delete the old token and re-auth once:
cd ~/Projects/calendar-automation
source .venv/bin/activate
rm token.json
python3 buffer_automation.py # opens browser, click through consent (click "Advanced" > "Go to app" if it warns unverified)
Once re-authed under Production status, the refresh token should persist indefinitely. It only dies if: manually revoked (Google Account → Security → Third-party access), the Google account password changes, the token goes fully unused for 6+ months, or the OAuth client is deleted/reset in Cloud Console. None of these happen incidentally.
This cron job also doubles as your Google OAuth token health check — since it runs every 15 minutes and needs a working token to function, a dead/revoked token gets caught and Discord-alerted within ~15 minutes. No separate token monitoring needed for this credential. (See the “Token & Auth Health — Unified Overview” KB page for the full picture across Google + Robinhood.)
# Gemini calendar add on for Swimply/sniffspot - wrapped with Discord failure alerting
*/15 * * * * /usr/local/bin/run-buffer-automation.sh
Runs every 15 minutes. Points at the wrapper script, not buffer_automation.py directly — the wrapper is what adds logging and Discord alerting without modifying the original script.
run-buffer-automation.sh wraps the actual script:
buffer_automation.py via the venv’s Python.~/calendar-automation.log, success or failure, so run duration is visible even when something hangs partway through.invalid_grant, token revoked/expired, traceback, or AuthenticationError. Successful runs stay silent — no noise in Discord.The webhook URL lives in ~/.config/backup/discord_webhook.txt (chmod 600), same pattern as the backup password file — never hardcoded in the script, never exported as an environment variable.
Test the alert path (simulates a real headless auth failure, since cron has no browser to fall back on):
mv ~/Projects/calendar-automation/token.json ~/Projects/calendar-automation/token.json.bak
echo "invalid" > ~/Projects/calendar-automation/token.json
/usr/local/bin/run-buffer-automation.sh
# check Discord for the alert, then restore immediately:
mv ~/Projects/calendar-automation/token.json.bak ~/Projects/calendar-automation/token.json
automation.log.old can eventually be deleted once it’s no longer useful for comparison against future failures.