Free daily AI brief on your Garmin data — full-history stats, Gemini insight, push to your phone. $0 to run.
Fully automated pipeline: pull Garmin Connect into SQLite, compute percentiles and trends over your whole history, and deliver a short coached brief via ntfy, Telegram, or email. Runs on GitHub Actions; no server to maintain.
Example notification layout — your numbers and wording change daily.
WATCH
• Stress 34 vs 30d avg 23 — worsening
• Sleep 6.15h — under 7h target
WINS
• Intensity 63 min — all-time high
• Resting HR improving, 2-day streak near ATL
TODAY
In bed 30 minutes earlier tonight.
—
Poor sleep raises cortisol and daily stress.
Cost: $0 (Garmin via garminconnect
, Gemini free tier, GitHub Actions, ntfy/Telegram/email).
python3.12 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python scripts/mint_token.py
Use your Garmin email, password, and MFA code. Tokens are saved to ~/.garminconnect
(valid ~1 year).
BACKFILL_DAYS=180 python -m src.backfill
Creates garmin.db
. Commit it to your repo after backfill completes (re-run backfill after schema changes to refresh columns).
Package tokens for CI:
bash scripts/pack_tokens.sh
Copy the single long line of output into repository secret GARMIN_TOKENS_B64
(no quotes, no spaces, no line breaks in the secret value).
If Actions fails with base64: invalid input
, the secret was pasted wrong — re-run pack_tokens.sh
and replace the secret entirely.
Manual alternative:
tar -cf tokens.tar -C ~/.garminconnect .
base64 < tokens.tar | tr -d '\n\r '
rm -f tokens.tar
| Secret | Purpose |
|---|---|
GARMIN_TOKENS_B64 |
|
base64 tar of ~/.garminconnect |
|
GEMINI_API_KEY |
|
NOTIFIER
ntfy
(default), telegram
, or email
NTFY_TOPIC
TELEGRAM_BOT_TOKEN
/ TELEGRAM_CHAT_ID
EMAIL_USER
/ EMAIL_APP_PASSWORD
/ EMAIL_TO
ntfy (default): Install the ntfy app, subscribe to a long random topic, set NTFY_TOPIC
.
Telegram: Create a bot via @BotFather, get your chat id.
Email: Gmail app password (not account password).
cp .env.example .env # fill in secrets
python -m src.main
Create a GitHub repo, commit everything including garmin.db
, add secrets, run workflow_dispatch on garmin-daily
once to verify CI. The repo can be public if you accept that garmin.db
exposes daily health numbers (see below).
Only scalar daily wellness metrics are stored. There is no raw JSON, no GPS, no activity routes, no maps, and no activity list/workout details.
| Stored | Not stored |
|---|---|
| Steps, resting HR, sleep (duration/score/stages), stress, Body Battery high/low, HRV, training readiness, intensity minutes, active kcal | Location, lat/long, track polyline |
| SpO2, VO2 max, Garmin fitness age, weight (g), body fat % | |
| Activity names, routes, timestamps per lap | |
Date (YYYY-MM-DD ) only |
|
| Full API responses, heart-rate streams |
Fitness age is Garmin’s estimated fitness age (a single number), not your birthdate or home address.
python -m src.main
— pull recent days → digest → Gemini brief → notification.
GitHub Actions runs at 13:00 UTC (~8am US Eastern), commits updated garmin.db
, and pushes.
Cron drift: Scheduled runs may start a few minutes late; fine for a daily brief.Workflow auto-disable: Repos with no commits for 60 days disable scheduled workflows. Committinggarmin.db
each run prevents this.Token expiry: Re-runscripts/mint_token.py
when Actions fail auth (~yearly).Gemini privacy: Free tier may use inputs for training. The digest contains only aggregated numbers — no names or emails.Rate limits: One Gemini call/day; flash → flash-lite fallback on 429.Thin data: Metrics with fewer than 14 days skip percentile/record claims.NULL: Missing Garmin fields are stored as NULL, never zero.Gemini quota 0: If you seelimit: 0
429 errors, link a billing account on the GCP project tied to your API key (still $0 for one call/day within free tier).
src/
config.py # env, metrics registry, goals
db.py # SQLite
garmin_client.py
pull.py / backfill.py
features.py # statistics digest
insight.py # Gemini brief
notify.py
main.py
scripts/mint_token.py
.github/workflows/daily.yml
docs/sample-notification.png