generated from jowj/python-template
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
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))
|