You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
6.8 KiB

#!/usr/bin/env python3
"""export data I care about from garmin connect."""
import argparse
import datetime
import logging
import os
import sys
import typing
import pandas
import yaml # type: ignore
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
# Configure debug logging
logging.basicConfig(level=logging.WARN)
logger = logging.getLogger(__name__)
# configure credentials
credentials = yaml.safe_load(open("../credentials.yml"))
email = credentials["garmin"]["email"]
password = credentials["garmin"]["password"]
def resolvepath(path: str) -> str:
"""Resolves a path as absolutely as possible"""
return os.path.realpath(os.path.normpath(os.path.expanduser(path)))
@typing.no_type_check
def idb_excepthook(type, value, tb):
"""Call an interactive debugger in post-mortem mode
If you do "sys.excepthook = idb_excepthook", then an interactive debugger
will be spawned at an unhandled exception
"""
if hasattr(sys, "ps1") or not sys.stderr.isatty():
sys.__excepthook__(type, value, tb)
else:
import pdb
import traceback
traceback.print_exception(type, value, tb)
print
pdb.pm()
return None
def get_yesterday_user_statistics(path: str = "./") -> None:
"""pulls yesterday's user metrics from garmin. accepts optional path argument"""
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
yesterdays_year = yesterday.strftime("%Y")
yesterdays_month = yesterday.strftime("%m")
folder = f"{path}/{yesterdays_year}/{yesterdays_month}/"
try:
# setup the connection
api = Garmin(email, password)
api.login()
stats = api.get_stats_and_body(yesterday.isoformat())
spo_data = api.get_spo2_data(yesterday.isoformat())
max_data = api.get_max_metrics(yesterday.isoformat())
except (
GarminConnectConnectionError,
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
) as err:
logger.error("Error occurred during Garmin Connect communication: %s", err)
if not os.path.exists(folder):
os.makedirs(folder)
try:
stats = pandas.DataFrame.from_dict(stats)
spo_data = pandas.json_normalize(
spo_data
) # json normalize needed due to variable list length in dict.
max_data = pandas.DataFrame.from_dict(max_data)
stats.to_csv(f"{folder}{yesterday.strftime('%Y-%m-%d')}_summary_stats.csv")
spo_data.to_csv(f"{folder}{yesterday.strftime('%Y-%m-%d')}_summary_spo.csv")
max_data.to_csv(f"{folder}{yesterday.strftime('%Y-%m-%d')}_summary_max.csv")
except OSError:
logger.error("Unable to find folder for data: %s")
exit(
"Unable to create folder for data. Please review permissions and recite the incantation."
)
return None
def get_activity_info(path: str = ".") -> None:
"""Return activities since purchase time. If you want, you can be more specific, but I just default to "all".
I think I've captured all possible activity types.
A bunch of state happens here."""
today = datetime.date.today()
purchasetime = "2021-12-25"
try:
# setup the connection
api = Garmin(email, password)
api.login()
activity_types = [
"cycling",
"running",
"swimming",
"multi_sport",
"fitness_equipment",
"hiking",
"walking",
"other",
]
for workout in activity_types:
# Get activities data from startdate 'YYYY-MM-DD' to enddate 'YYYY-MM-DD'
activities = api.get_activities_by_date(purchasetime, today, workout)
# Download an Activity
for activity in activities:
activity_id = activity["activityId"]
start_date = datetime.datetime.strptime(
activity["startTimeLocal"], "%Y-%m-%d %H:%M:%S"
).strftime("%Y-%m-%d")
activity_year = datetime.datetime.strptime(
activity["startTimeLocal"], "%Y-%m-%d %H:%M:%S"
).strftime("%Y")
activity_month = datetime.datetime.strptime(
activity["startTimeLocal"], "%Y-%m-%d %H:%M:%S"
).strftime("%m")
folder = f"{path}/{activity_year}/{activity_month}/"
activity_type = activity["activityType"]["typeKey"]
file_name = f"{folder}{start_date}_{activity_type}_{activity_id}"
logger.info("api.download_activities(%s)", activity_id)
gpx_data = api.download_activity(activity_id, dl_fmt=api.ActivityDownloadFormat.GPX)
output_file = f"{str(file_name)}.gpx"
with open(output_file, "wb") as fb:
fb.write(gpx_data)
# TCX data is used for garmin shit only.
tcx_data = api.download_activity(activity_id, dl_fmt=api.ActivityDownloadFormat.TCX)
output_file = f"{str(file_name)}.tcx"
with open(output_file, "wb") as fb:
fb.write(tcx_data)
# FIT is another garmin only. what the fuck. This ZIP contains the .fit file, I assume due to some complicated edge case.
zip_data = api.download_activity(
activity_id, dl_fmt=api.ActivityDownloadFormat.ORIGINAL
)
output_file = f"{str(file_name)}.zip"
with open(output_file, "wb") as fb:
fb.write(zip_data)
# contains some nice data about the activity that's NOT in the GPX file
csv_data = api.download_activity(activity_id, dl_fmt=api.ActivityDownloadFormat.CSV)
output_file = f"{str(file_name)}.csv"
with open(output_file, "wb") as fb:
fb.write(csv_data)
except (
GarminConnectConnectionError,
GarminConnectAuthenticationError,
GarminConnectTooManyRequestsError,
) as err:
logger.error("Error occurred during Garmin Connect communication: %s", err)
return None
@typing.no_type_check
def main(*args, **kwargs):
parser = argparse.ArgumentParser(description="")
parser.add_argument("--debug", "-d", action="store_true", help="Include debugging output")
parsed = parser.parse_args()
if parsed.debug:
sys.excepthook = idb_excepthook
# where do you want things to go
data_path = resolvepath("/home/josiah/dhd/quantified_life/fitness")
if not os.path.exists(data_path):
exit("Unable to resolve path. Make sure your path exists and try again.")
get_yesterday_user_statistics(data_path)
get_activity_info(data_path)
return None
if __name__ == "__main__":
sys.exit(main(*sys.argv))