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