My mission lately has been to replace commercial health-tracking products that continually harvest your personal data.
My wife and I use Strava and Garmin for fitness tracking and Withings for weight and blood pressure measurements. The convenience is pretty solid, but getting the data out of these services is a major pain in the ass. Both Garmin and Strava ensure that any data you share with them becomes theirs forever.
Both Strava and Garmin offer APIs, but I didn’t have the patience to go through their process. I tried scraping their sites with much success but they eventually hit me with an impossible-to-bypass captcha. So forget you, Jobu. I’ll do it myself. Enter Python to the rescue!
With the following solution, we end up with a mobile-friendly dashboard that looks like this…
A data entry screen that looks like this…
… and automatic tweets that look like this:
Code
garmin.py
from flask import Flask, render_template, request, redirect, url_for
import sqlite3
from datetime import datetime
import tweepy
import os
app = Flask(__name__)
app.secret_key ='supersecretkey'# Needed for flash messagesapp.config['TEMPLATES_AUTO_RELOAD'] =True# Twitter API credentialsapi_key ="asdf"api_secret_key ="asdf"access_token ="asdf"access_token_secret ="asdf"bearer_token ="asdf"# Initialize tweepy.Client for v2 API and tweepy.API for v1.1 media uploadclient = tweepy.Client(bearer_token=bearer_token,
consumer_key=api_key,
consumer_secret=api_secret_key,
access_token=access_token,
access_token_secret=access_token_secret)
auth = tweepy.OAuth1UserHandler(api_key, api_secret_key, access_token, access_token_secret)
api_v1 = tweepy.API(auth)
# Mapping activities to imagesactivity_images = {
"cycling": "/garmin/cycling.jpg",
"elliptical": "/garmin/elliptical.jpg",
"pilates": "/garmin/pilates.jpg",
"running": "/garmin/running.jpg",
"jogging": "/garmin/running.jpg",
"walking": "/garmin/walking.jpg",
"weights": "/garmin/weights.jpg",
"kickboxing": "/garmin/kickboxing.jpg",
"default": "/garmin/default.jpg"}
defget_db_connection():
conn = sqlite3.connect('/garmin/activity_data.db')
conn.row_factory = sqlite3.Row
return conn
@app.route('/', methods=['GET', 'POST'])
defindex():
if request.method =='POST':
activity = request.form['activity']
time = request.form['time']
avg_hr = int(request.form['avg_hr'])
calories = int(request.form['calories'])
distance = float(request.form['distance']) if request.form['distance'] elseNone pace = float(request.form['pace']) if request.form['pace'] elseNone# Define the Los Angeles timezone la_tz = pytz.timezone('America/Los_Angeles')
# Get the current time in Los Angeles time zone timestamp = datetime.now(la_tz).strftime('%Y-%m-%d %H:%M:%S')
conn = get_db_connection()
conn.execute('INSERT INTO activities (activity, time, avg_hr, calories, distance, pace, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)',
(activity, time, avg_hr, calories, distance, pace, timestamp))
conn.commit()
conn.close()
if'tweet'in request.form:
tweet_activity((activity, time, avg_hr, calories, distance, pace))
return redirect(url_for('index'))
# Fetch recent activities for both GET and POST requests conn = get_db_connection()
activities = conn.execute('SELECT activity, time, avg_hr, calories, distance, pace, date(timestamp) as date FROM activities ORDER BY timestamp DESC LIMIT 5').fetchall()
conn.close()
return render_template('index.html', activities=activities)
defget_image_for_activity(activity):
return activity_images.get(activity.lower(), activity_images['default'])
deftweet_activity(data):
activity, time, avg_hr, calories, distance, pace = data
activity_emojis = {
"cycling": "🚴♂️",
"elliptical": "🏋️♂️",
"pilates": "🧘♀️",
"running": "🏃♂️",
"walking": "🚶♂️",
"weights": "🏋️♂️",
"kickboxing": "🥊" }
emoji = activity_emojis.get(activity.lower(), "🔥")
tweet = (
f"{emoji} Just completed {activity} for {time} min.\n"f"💓 Avg HR: {avg_hr}, 🔥 Calories burned: {calories}." )
if distance and pace:
tweet +=f"\n🛣️ Distance: {distance} mi, 🕒 Pace: {pace} min/mi." tweet +=f"\n\n#ActivityLog {datetime.now().strftime('%d %b')}" image_path = get_image_for_activity(activity)
try:
if os.path.exists(image_path):
media = api_v1.media_upload(image_path)
client.create_tweet(text=tweet, media_ids=[media.media_id])
else:
client.create_tweet(text=tweet)
exceptExceptionas e:
print(f"An error occurred while attempting to tweet: {e}")
if __name__ =='__main__':
app.run(host='0.0.0.0', port=5001, debug=True)
This python script uses Flask to serve a simple web page. When you enter exercise data, it updates your new SQLite activity_data.db. If you click the Tweet this activity button, it’ll tweet what you did for all your followers to see!
The SQLite database can be added to Grafana and customized for whatever you want. I use these dashboards throughout the day to track my overall health - which is critical due to my kidney diet requirements.
16 February 2025
Added index.html for the Flask template.
10 February 2025
Updated for current dashboard usage with Flask and Tweets.
28 August 2024
Stripped white space around text variable inputs. That nabbed me yesterday.
Fixed some grammar and punctuation on text in post.
22 August 2024
Added Grafana dashboard screenshot and relevant description.
18 August 2024
Added timestamp to entries (not sure why I didn't think of that earlier).
Changed time to integer, so it can be normalized as minutes.