My mission lately has been to replace commercial solutions that continually harvest your personal data with self-hosted alternatives that you control.
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 hassle. Both Garmin and Strava ensure that any data you share with them becomes theirs to keep and use as they wish. One of my projects this weekend was to fix this little issue.
Both Strava and Garmin offer APIs, but there appears to be a lengthy request and approval process. Their forms are very long and require additional personal information. That ain’t happening for many reasons, but filling out forms isn’t something I have the patience for anyhow.
I then tried scraping their sites with 100% success in a browser, but they failed when flipped over to headless mode. They caught on and blocked my attempts with an impossible-to-bypass captcha. So forget you, Jobu. I’ll do it myself. Enter Python to the rescue.
When the garmin.py script below runs in the terminal, get_activity_data() will prompt you for your exercise details. If the activity involved distance, it’ll ask you additional questions. These values are then stored in an SQLite database using store_activity_data(). If you add in your Twitter API credentials, it’ll tweet out your success with funny pictures from activity_images().
Before you go and run this script for the first time, the database needs to be created. I’ve added an additional script at the very bottom of this post that needs to be run just the one time. Please note that if you copy this and add additional things to track, make sure to sync the field changes in both scripts.
garmin.py
import sqlite3
import tweepy
from datetime import datetime
import os
api_key ="API_KEY"api_secret_key ="API_SECRET_KEY"access_token ="ACCESS_TOKEN"access_token_secret ="ACCESS_TOKEN_SECRET"client = tweepy.Client(
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 = tweepy.API(auth)
activity_images = {
"cycling": "cycling.jpg",
"elliptical": "elliptical.jpg",
"pilates": "pilates.jpg",
"running": "running.jpg",
"walking": "walking.jpg",
"weights": "weights.jpg",
"default": "default.jpg"}
defget_activity_data():
activity = input("Activity: ").strip()
time = input("Time (e.g., 65 for 65 miinutes): ").strip()
avg_hr = int(input("Avg HR: "))
calories = int(input("Calories: "))
distance =None pace =Noneif activity.lower() in ['cycling', 'jogging', 'running', 'walking']:
distance = float(input("Distance (e.g., 5.2 for 5.2 km or miles): "))
pace = float(input("Pace (e.g., 5.5 for 5:30 min/km or mile): "))
return (activity, time, avg_hr, calories, distance, pace)
defstore_activity_data(data):
conn = sqlite3.connect('activity_data.db')
cur = conn.cursor()
timestamp = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ')
data_with_timestamp = data + (timestamp,)
cur.execute('''INSERT INTO activities (activity, time, avg_hr, calories, distance, pace, timestamp)
VALUES (?, ?, ?, ?, ?, ?)''', data_with_timestamp)
conn.commit()
conn.close()
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": "🏋️♂️" }
emoji = activity_emojis.get(activity.lower(), "🔥")
tweet = (
f"{emoji} Just completed {activity} for {time}.\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')}" print("Generated tweet:", tweet) # Debug image_path = get_image_for_activity(activity)
print("Selected image path:", image_path) # Debugtry:
if os.path.exists(image_path):
print("Image found, attempting to upload.") # Debug media = api.media_upload(image_path)
client.create_tweet(text=tweet, media_ids=[media.media_id_string])
print("Tweet with image posted successfully!") # Debugelse:
print("Image not found, tweeting without image.") # Debug client.create_tweet(text=tweet)
print("Tweet without image posted successfully!") # DebugexceptExceptionas e:
print("An error occurred while attempting to tweet:", e) # Debugif __name__ =="__main__":
activity_data = get_activity_data()
store_activity_data(activity_data)
print("Activity data saved successfully!")
tweet_choice = input("Would you like to tweet this activity? (y/n): ").strip().lower()
if tweet_choice =='y':
tweet_activity(activity_data)
create_table.py
import sqlite3
conn = sqlite3.connect('activity_data.db')
cur = conn.cursor()
cur.execute('''CREATE TABLE IF NOT EXISTS activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity TEXT,
time INTEGER,
avg_hr INTEGER,
calories INTEGER,
distance REAL,
pace REAL,
timestamp TEXT
)''')
conn.commit()
conn.close()
The database this script creates can be easily analyzed from Grafana. Below is my current, in-progress dashboard. Getting datetime to work with Grafana is a pain, but changing the formatting of the SQLite TEXT field with strftime('%s', strftime('%Y-%m-%d', timestamp)) as time, setting the time formatted column to time, and adding an override for the datetime field to local, seems to have addressed the SQLite storage deficiencies.
The hidden beauty of this solution is in its simplicity. You control what data you want to store and how to recall it. This database will be is currently visualized in Grafana, next to my stock portfolio. Now I can turn Wifi / Bluetooth on my watch off, and delete the Garmin app.