8.7 KiB
Zwift Activity Monitor
Automatic monitoring service that continuously watches for new Zwift activities and downloads their FIT files.
Features
- ✅ Continuous Monitoring: Automatically checks for new activities every 5 minutes (configurable)
- ✅ Automatic Downloads: Downloads FIT files for new activities as soon as they're available
- ✅ Docker Support: Easy deployment with Docker and Docker Compose
- ✅ Persistent State: Tracks downloaded activities to prevent duplicates
- ✅ Rate Limiting: Respects API rate limits with built-in throttling
- ✅ Graceful Shutdown: Handles SIGTERM/SIGINT for clean container restarts
- ✅ Structured Logging: JSON logging with configurable levels
- ✅ Retry Logic: Automatic retry with exponential backoff for transient failures
Quick Start
Using Docker Compose (Recommended)
-
Clone the repository
git clone <your-repo-url> cd zwift -
Create environment file
cat > .env <<EOF ZWIFT_USERNAME=your@email.com ZWIFT_PASSWORD=yourpassword TZ=Europe/Berlin EOF -
Create data directory
mkdir -p data/activities -
Start the service
docker-compose up -d -
View logs
docker-compose logs -f -
Check downloaded files
ls -lh data/activities/
Using Docker
docker build -t zwift-monitor .
docker run -d \
--name zwift-monitor \
--restart unless-stopped \
-e ZWIFT_USERNAME=your@email.com \
-e ZWIFT_PASSWORD=yourpassword \
-e TZ=Europe/Berlin \
-v $(pwd)/data:/data \
zwift-monitor
Local Development
# Install dependencies
go mod download
# Set credentials
export ZWIFT_USERNAME=your@email.com
export ZWIFT_PASSWORD=yourpassword
export ZWIFT_OUTPUT_DIR=./data/activities
export ZWIFT_STATE_FILE=./data/state.json
# Run the service
go run .
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
ZWIFT_USERNAME |
required | Your Zwift email address |
ZWIFT_PASSWORD |
required | Your Zwift password |
ZWIFT_OUTPUT_DIR |
/data/activities |
Directory for FIT files |
ZWIFT_STATE_FILE |
/data/zwift-state.json |
State persistence file |
ZWIFT_TOKEN_CACHE |
/data/.zwift-token.json |
OAuth token cache |
ZWIFT_POLL_INTERVAL |
5m |
How often to check for new activities |
ZWIFT_RATE_LIMIT |
5 |
Maximum requests per second |
ZWIFT_MAX_RETRIES |
3 |
Number of retries for failed requests |
ZWIFT_LOG_LEVEL |
info |
Logging level (debug, info, warn, error) |
ZWIFT_LOG_FILE |
`` | Optional log file path |
TZ |
UTC |
Timezone for timestamps |
Configuration File (Optional)
You can also use a config.yaml file:
username: your@email.com
password: yourpassword
output_dir: /data/activities
poll_interval: 5m
rate_limit: 5
max_retries: 3
log_level: info
Place the config file:
- Local:
config.yamlin the project directory - Docker: Mount as
/app/config.yaml
Note: Environment variables take precedence over config file values.
File Naming
Downloaded FIT files are named using the format:
{activityID}_{date}_{activity-name}.fit
Example:
12345678_2025-02-10_Morning-Ride.fit
Logging
The service uses structured logging with the following levels:
- DEBUG: Detailed API calls and responses
- INFO: Normal operation, downloads, statistics
- WARN: Retries, non-fatal errors
- ERROR: Failed downloads, authentication issues
Example Log Output
2025-02-10T10:00:00Z INFO Starting Zwift Activity Monitor poll_interval=5m0s output_dir=/data/activities
2025-02-10T10:00:01Z INFO Loaded state total_downloaded=42 last_check=2025-02-10T09:55:00Z
2025-02-10T10:00:02Z INFO Running initial check...
2025-02-10T10:00:03Z INFO Checking for new activities...
2025-02-10T10:00:05Z INFO Found new activities count=1
2025-02-10T10:00:06Z INFO Downloading activity activity_id=12345678 name="Morning Ride"
2025-02-10T10:00:08Z INFO Successfully downloaded activity activity_id=12345678 filepath=/data/activities/12345678_2025-02-10_Morning-Ride.fit
2025-02-10T10:00:08Z INFO Download complete downloaded=1 total_new=1
State Management
The service maintains a JSON state file (zwift-state.json) that tracks:
- All downloaded activity IDs
- Activity metadata (name, date, file path)
- Last check timestamp
- Total downloads
Example State File
{
"downloaded_activities": {
"12345678": {
"id": 12345678,
"name": "Morning Ride",
"date": "2025-02-10T08:30:00Z",
"file_path": "/data/activities/12345678_2025-02-10_Morning-Ride.fit",
"downloaded_at": "2025-02-10T10:00:08Z",
"sport_type": "cycling"
}
},
"last_check": "2025-02-10T10:00:08Z",
"total_downloaded": 1
}
This state file ensures:
- No duplicate downloads after restarts
- Resume capability after crashes
- Historical tracking of all downloads
Troubleshooting
Authentication Fails
ERROR Failed to create service error="failed to create Zwift client: ..."
Solution: Verify your Zwift credentials are correct.
No New Activities Detected
INFO No new activities found
Possible causes:
- No new activities since last check
- Activities already downloaded
- Activity privacy settings
Check: View your state file to see what's already downloaded:
cat data/zwift-state.json | jq .
Rate Limiting Issues
WARN Rate limited by Zwift API
Solution: Increase the poll interval:
ZWIFT_POLL_INTERVAL=10m # Check every 10 minutes instead of 5
Disk Space Issues
ERROR Failed to save FIT file error="no space left on device"
Solution: Free up disk space or mount a larger volume.
Docker Commands
# View logs
docker-compose logs -f
# Restart service
docker-compose restart
# Stop service
docker-compose down
# Rebuild after code changes
docker-compose up -d --build
# View container stats
docker stats zwift-monitor
# Access container shell
docker-compose exec zwift-monitor sh
# Check state file
docker-compose exec zwift-monitor cat /data/zwift-state.json
Building from Source
# Build binary
go build -o zwift-monitor .
# Run tests (when available)
go test ./...
# Build Docker image
docker build -t zwift-monitor .
# Cross-compile for different platforms
GOOS=linux GOARCH=amd64 go build -o zwift-monitor-linux-amd64
GOOS=darwin GOARCH=arm64 go build -o zwift-monitor-darwin-arm64
Architecture
zwift/
├── main.go # Entry point with signal handling
├── internal/
│ ├── service/ # Main service loop
│ │ └── service.go # Ticker-based monitoring
│ ├── monitor/ # Activity monitoring logic
│ │ └── monitor.go # Check and download orchestration
│ ├── client/ # Zwift API client
│ │ ├── client.go # API wrapper with rate limiting
│ │ └── auth.go # OAuth token management
│ ├── state/ # State persistence
│ │ └── state.go # Thread-safe JSON state
│ ├── storage/ # File system operations
│ │ └── storage.go # Atomic FIT file writes
│ └── config/ # Configuration management
│ └── config.go # Env vars + YAML loading
└── Dockerfile # Multi-stage Docker build
Security Considerations
- Credentials: Never commit credentials to git. Use environment variables or Docker secrets.
- Token Cache: Stored with 0600 permissions (owner read/write only).
- State File: Contains activity IDs but no sensitive data.
- Container: Runs as non-root user for security.
API Usage
This service uses the unofficial Zwift API via the gravl library. The API:
- Is not officially supported by Zwift
- May change without notice
- Requires regular Zwift credentials (no special API key needed)
- Respects rate limiting (5 requests/second by default)
License
[Your License Here]
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
Support
For issues or questions:
- Create an issue in the repository
- Check existing issues for similar problems
- Include logs and configuration (redact credentials!)