Skip to content

elduty/velomate

Repository files navigation

VeloMate 🚴

Tests codecov Python 3.10+ License: MIT Grafana 12.4

A self-hosted cycling data platform — automatic ride ingestion from Strava, Grafana dashboards for analytics, and intelligent route planning. No Strava Premium required — all metrics (fitness, power zones, training load, TRIMP) are computed locally from raw data.

Inspired by TeslaMate. Works with any device that syncs to Strava.

Overview Dashboard

Features

Data Ingestion

  • Polls Strava every 10 minutes for new cycling rides (non-cycling activities filtered)
  • Stores full per-second telemetry (HR, power, cadence, speed, altitude, GPS)
  • Calculates CTL/ATL/TSB fitness metrics locally (no Strava Premium needed)
  • TRIMP (Training Impulse) computed from HR stream data (no Strava Premium needed)
  • Normalized Power (NP), Efficiency Factor (EF), and Work (kJ) pre-calculated per activity from stream data
  • FTP auto-estimated from rolling 90-day best 20-minute power, or configured manually
  • Daily fitness recalculation at 00:05 (rest days show CTL/ATL decay)
  • Smart deduplication when multiple devices record the same ride

Grafana Dashboards

Three dashboards with 98 panels across 12 visualization types.

Overview (34 panels) — your training hub

  • 12 stat cards with period comparison + sport type filter
  • 8 delta comparison cards (vs previous period)
  • Fitness section: CTL/ATL/TSB with fill-between shading, FTP, TSB gauge, weekly streak, 6-week fitness delta
  • 10 daily charts split by ride type (Outdoor/Zwift/E-Bike/Indoor)
  • Ride type donut, ride frequency bar chart
  • Outdoor records table (period best vs all-time best)
  • Activities table with drill-down to Activity Details
  • Lifetime ride heatmap
  • Manual annotations for marking events (races, FTP tests, injuries)

Activity Details

Activity Details (32 panels) — per-ride deep dive

  • 12 summary stat cards + 7 advanced metrics (NP, IF, VI, EF, Work, TRIMP, aerobic decoupling)
  • GPS route map with speed/HR/power color overlay
  • HR and power zones by kilometer (stacked bar charts)
  • HR and power zone distribution (Coggan model, zone-colored)
  • Power vs HR scatter plot (cardiac drift detection)
  • Power zone bands on HR & Power telemetry
  • Speed & elevation / HR & power / cadence & grade telemetry (distance-based x-axis)
  • Per-km splits table with best/worst markers
  • Power duration curve

All Time Progression

All Time Progression (32 panels) — long-term trends

  • 6 stat cards: total distance, elevation, rides, hours, current FTP, peak CTL
  • 6 progression scatter plots with 10-ride rolling averages and regression lines (speed, power, NP, EF, HR, distance)
  • FTP progression (monthly estimated from stream data)
  • Best efforts (1min/5min/20min peak power per ride)
  • Weekly power range (candlestick — week-over-week comparison)
  • Training zone polarization (monthly power + HR zone stacked bars)
  • CTL/ATL/TSB fitness history with fill-between shading
  • 6 cumulative totals (distance, elevation, duration, rides, TSS, calories)
  • Monthly trends stacked by ride type
  • Year-over-year distance comparison
  • Annual totals table
  • Personal records with drill-down links
  • All-time ride map

Route Preview

Intelligent Route Planning

  • Generates real road-following GPX loops via Valhalla (free, OpenStreetMap-based)
  • Generates GPX files with interactive browser preview (map + route stats + download/share buttons)
  • On iOS/Android, the share button opens the native share sheet to send the GPX directly to any bike computer app
  • --output DIR saves the preview HTML to a directory for headless/server use
  • Smart waypoint selection from 10 data sources (see below)
  • Weather-aware: best ride time, UV warnings, wind direction analysis
  • Safety control: --safety flag adjusts preference for bike lanes vs main roads
  • Configurable avoid zones for roads/areas you don't want to ride

Route Intelligence — 10 Data Sources

When planning a route, VeloMate selects waypoints and enriches the output using:

# Source Data API
1 OpenStreetMap POIs Viewpoints, cafes, peaks, water fountains, bike shops Overpass (free)
2 Strava segments Popular cycling roads near you Strava API
3 Komoot highlights Community-curated cycling POIs Vector tiles (free, no auth)
4 Your ride history 30-day GPS density grid — variety or comfort mode Local DB
5 OSM surface tags Road surface verification (asphalt, gravel, etc.) Overpass (free)
6 OSM cycling infrastructure Bike lanes, speed limits, traffic calming → safety score Overpass (free)
7 Open-Meteo weather Temperature, wind, UV, rain + hourly forecast Open-Meteo (free)
8 Open-Meteo air quality European AQI, PM2.5, PM10 Open-Meteo (free)
9 Open Topo Data Elevation profile, climb/descent, max gradient Open Topo Data (free)
10 Sunrise/Sunset Daylight safety, golden hour sunrise-sunset.org (free)

Additionally, the route planner detects waymarked cycling trails (EuroVelo, national routes) along the generated path.

Deduplication — Data Richness Scoring

When multiple devices record the same ride (e.g., a bike computer and a watch both syncing to Strava), VeloMate keeps the record with the richest data:

Field Score
Power data +3
Heart rate +2
Distance > 0 +1
Cadence +1
Calories +1
Elevation > 0 +1

The record with the higher total score wins. Missing fields from the losing record (e.g., HR from a watch when a bike computer wins on power) are merged into the winner. This works with any device brand — no hardcoded priorities.

Architecture

Any device → Strava → [Ingestor] → PostgreSQL → Grafana dashboards
                                        ↑
                            VeloMate CLI (route planning + recommendations)
                                        ↓
                              Valhalla → GPX file

Three Docker Compose services:

Service Image Port
PostgreSQL postgres:15 5423
Ingestor custom Python 3.11
Grafana grafana/grafana:12.4 3021

The CLI runs locally and connects to the database over the network.

Quick Start

1. Clone and configure

git clone https://github.com/elduty/velomate.git
cd velomate
cp .env.example .env
# Edit .env with your Strava API credentials and passwords

2. Get a Strava refresh token

# Open in browser (replace YOUR_CLIENT_ID):
# https://www.strava.com/oauth/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=activity:read_all

# After authorizing, exchange the code:
curl -X POST https://www.strava.com/oauth/token \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d code=CODE_FROM_REDIRECT \
  -d grant_type=authorization_code
# Use the refresh_token from the response

3. Start services

docker compose up -d

On first run, the ingestor backfills the last 12 months of Strava activities.

4. Set up the CLI

pip install -r requirements.txt
cp config.example.yaml ~/.config/velomate/config.yaml
# Edit with your home coordinates, DB host, and Strava credentials

Credentials support three methods: direct values, environment variables, or shell commands (password_cmd) for secret managers like Keychain, 1Password, or Vault.

CLI Usage

# Weekly ride recommendation (fitness + weather + past routes)
python3 -m velomate.cli

# Plan a route
python3 -m velomate.cli plan --duration 2h
python3 -m velomate.cli plan --distance 50km --surface gravel
python3 -m velomate.cli plan --duration 3h --waypoints "Sintra,Cascais"
python3 -m velomate.cli plan --duration 1h --surface mtb --safety 1.0
python3 -m velomate.cli plan --distance 30 --preference comfort

Plan flags

Flag Default Description
--duration * Ride time (2h, 1h30m, 90min)
--distance * Target distance (30, 50km)
--surface road road, gravel, or mtb
--safety 0.5 0.0 = fastest, 1.0 = safest (prefers bike lanes)
--preference variety variety (new roads) or comfort (familiar)
--waypoints Comma-separated place names
--date tomorrow When to ride (today, saturday, 2026-03-20)
--time Start time (14:00, 2pm, 9am)
--start from config Override start as lat,lng
--loop true Round-trip route
--output DIR Save preview HTML to directory (instead of opening browser)

* Provide either --duration or --distance (one required, mutually exclusive).

Example output

🗺 *VeloMate 2h00m Road via Miradouro de Porto Salvo, Cotão, Viewpoint*
  📏 24 km
  📅 2026-03-16 at 09:00
  🛤 Surface: asphalt 53%, unknown 44%, paving_stones 2%
  ⛰ Climb: +260m / -284m (max gradient 10.2%)
  🌿 Scenic: wood (25), water (10), park (4) (86/100)
  🛡 Safety: bike lanes 22% (11/100)
  🌤 Mainly clear, 10-21°C, wind 12 km/h
  🕐 Best time: 09:00 (14°C, wind 10 km/h, UV 2)
  🌅 Sunrise 06:45, sunset 18:46
  💪 neutral (TSB -4)
  💾 GPX: /tmp/velomate_route_road_29km.gpx

Fitness Metrics

Power TSS = (duration_s × avg_power × IF) / (FTP × 3600) × 100   (preferred)
HR TSS    = (duration_h) × (avg_hr / threshold_hr)² × 100         (fallback)
CTL       = 42-day EMA of daily TSS   (chronic training load / fitness)
ATL       = 7-day EMA of daily TSS    (acute training load / fatigue)
TSB       = CTL − ATL                 (training stress balance / form)
  • FTP: auto-estimated from rolling 90-day best 20-minute power × 0.95, or configured via VELOMATE_FTP / config.yaml
  • NP: Normalized Power — 30s rolling average, 4th power, mean, 4th root. Pre-calculated from stream data per activity
  • EF: Efficiency Factor = NP / avg HR. Rising EF indicates improving aerobic fitness
  • Work: Total energy output in kJ = sum of per-second power from stream data
  • Threshold HR: 95th percentile of your max HRs, or configured via VELOMATE_MAX_HR / config.yaml
  • TSB interpretation: > +10 fresh · -10 to +10 neutral · < -10 fatigued

Database Schema

Table Contents
activities Every ride — distance, duration, HR, power, cadence, elevation, calories, TSS, NP, EF, Work (kJ), sport type, device
activity_streams Per-second telemetry — HR, power, cadence, speed, altitude, lat/lng
athlete_stats Daily fitness metrics — CTL, ATL, TSB, weekly volume
routes Legacy — created by schema but not actively written to
sync_state Ingestor bookmarks (last synced timestamps)

Schema is managed in code (ingestor/db.py:create_schema()) using IF NOT EXISTS / ADD COLUMN IF NOT EXISTS. No migration tool.

Configuration

Ingestor (Docker)

Configured via .env file:

Variable Required Description
POSTGRES_PASSWORD Yes Database password
STRAVA_CLIENT_ID Yes From strava.com/settings/api
STRAVA_CLIENT_SECRET Yes From Strava API settings
STRAVA_REFRESH_TOKEN Yes OAuth refresh token
GRAFANA_PASSWORD Yes Grafana admin password
VELOMATE_MAX_HR No Your max heart rate (0 = auto-estimate)
VELOMATE_FTP No Your FTP in watts (0 = auto-estimate)

CLI (local)

Configured via ~/.config/velomate/config.yaml (see config.example.yaml):

  • Home coordinates (required for route planning)
  • Database connection
  • Strava credentials (optional — enables popular segment data in route intelligence)
  • Avoid zones (lat/lng areas to exclude from routes)

Requirements

  • Docker + Docker Compose (for ingestor, PostgreSQL, Grafana)
  • Python 3.10+ (for CLI)
  • A Strava account with API access

License

MIT

About

Self-hosted cycling analytics platform — Strava ingestion, Grafana dashboards, intelligent route planning. No Strava Premium required.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors