generated from jowj/python-template
parent
f8da988313
commit
598aa6caf2
@ -0,0 +1,198 @@
|
|||||||
|
#!/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))
|
Loading…
Reference in new issue