diff --git a/README.md b/README.md index 54bc16f..56fd665 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,22 @@ To download all your Garmin workouts in TCX format (basically XML), perform the Again, you should see activities downloading in a few seconds. + To download wellness data (e.g., step count, calories burned, floors ascended and descended), use the download script as follows: + + ``` + python download.py -c garmin_login.csv --start-date 2017-08-18 --end-date 2017-08-20 --displayname 1abcde23-f45a-678b-cdef-90123a45bcd + ``` + + Start date and end date can be the same date to only download one day. Displayname is the part of the url if you go to Activities > Steps on Garmin Connect and look at the part: ../daily-summary/[displayname]/2017-08-20/steps + + To visualise the downloaded wellness information, use `visualisation.py`, for example like: + + ``` + python visualisation.py -i /home/youruser/garmin/Results/youruser@example.com/Wellness -o /home/youruser/www/garmin + ``` + + This creates a file called wellness.html with graphs and statistics. Just open it with your browser, or upload it to your website. + If you run into any problems, please create a ticket! Packages @@ -48,3 +64,5 @@ If any of the following packages require dependencies, they will be listed. To i - **download.py**: A script for downloading all Garmin Connect data as TCX files for offline parsing. *Dependencies: mechanize* - **monthly.py**: A script for updating one's Twitter account with monthly statistics. Currently, the statistics and format are identical to those seen on [DailyMile](http://www.dailymile.com) for their weekly statistics. I just thought it'd be neat to have monthly updates, too. *Dependencies: tweepy, mechanize* + + - **visualisation.py**: A script to generate graphs and statistics overviews from the Wellness data downloaded with download.py. *Dependencies: jinja2* diff --git a/download.py b/download.py index 190107d..44ee26b 100644 --- a/download.py +++ b/download.py @@ -29,14 +29,18 @@ SSO = "https://sso.garmin.com/sso" CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css" REDIRECT = "https://connect.garmin.com/post-auth/login" + ACTIVITIES = "http://connect.garmin.com/proxy/activity-search-service-1.2/json/activities?start=%s&limit=%s" WELLNESS = "https://connect.garmin.com/modern/proxy/userstats-service/wellness/daily/%s?fromDate=%s&untilDate=%s" DAILYSUMMARY = "https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailySummaryChart/%s?date=%s" - +STRESS = "https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailyStress/%s" +HEARTRATE = "https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailyHeartRate/%s?date=%s" +SLEEP = "https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailySleep/user/%s?date=%s" TCX = "https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/%s" GPX = "https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/%s" + def login(agent, username, password): global BASE_URL, GAUTH, REDIRECT, SSO, CSS @@ -153,7 +157,7 @@ def wellness(agent, start_date, end_date, display_name, outdir): try: response = agent.open(url) except: - print('Wrong credentials for user {}. Skipping.'.format(username)) + print('Wrong credentials for user {}. Skipping wellness for {}.'.format(username, start_date)) return content = response.get_data() @@ -168,7 +172,7 @@ def dailysummary(agent, date, display_name, outdir): try: response = agent.open(url) except: - print('Wrong credentials for user {}. Skipping.'.format(username)) + print('Wrong credentials for user {}. Skipping daily summary for {}.'.format(username, date)) return content = response.get_data() @@ -178,6 +182,51 @@ def dailysummary(agent, date, display_name, outdir): f.write(content) +def dailystress(agent, date, outdir): + url = STRESS % (date) + try: + response = agent.open(url) + except: + print('Wrong credentials for user {}. Skipping daily stress for {}.'.format(username, date)) + return + content = response.get_data() + + file_name = '{}_stress.json'.format(date) + file_path = os.path.join(outdir, file_name) + with open(file_path, "w") as f: + f.write(content) + + +def dailyheartrate(agent, date, display_name, outdir): + url = HEARTRATE % (display_name, date) + try: + response = agent.open(url) + except: + print('Wrong credentials for user {}. Skipping daily heart rate for {}.'.format(username, date)) + return + content = response.get_data() + + file_name = '{}_heartrate.json'.format(date) + file_path = os.path.join(outdir, file_name) + with open(file_path, "w") as f: + f.write(content) + + +def dailysleep(agent, date, display_name, outdir): + url = SLEEP % (display_name, date) + try: + response = agent.open(url) + except: + print('Wrong credentials for user {}. Skipping daily sleep for {}.'.format(username, date)) + return + content = response.get_data() + + file_name = '{}_sleep.json'.format(date) + file_path = os.path.join(outdir, file_name) + with open(file_path, "w") as f: + f.write(content) + + def login_user(username, password): # Create the agent and log in. agent = me.Browser() @@ -208,8 +257,11 @@ def download_wellness_for_user(agent, username, start_date, end_date, display_na # Scrape all wellness data. wellness(agent, start_date, end_date, display_name, download_folder) - # Daily summary does not do ranges, only fetch for `startdate` + # Daily summary, stress and heart rate do not do ranges, only fetch for `startdate` dailysummary(agent, start_date, display_name, download_folder) + dailystress(agent, start_date, download_folder) + dailyheartrate(agent, start_date, display_name, download_folder) + dailysleep(agent, start_date, display_name, download_folder) if __name__ == "__main__": diff --git a/templates/wellness.html b/templates/wellness.html new file mode 100644 index 0000000..8e03e20 --- /dev/null +++ b/templates/wellness.html @@ -0,0 +1,299 @@ + + + + Garmin Wellness + + + + + + + + +

Garmin Wellness

+ + {% for datestamp, summary in summaries %} + +

{{ datestamp }}

+
+ + + + + + +
+ +
+ +
+ {% endfor %} + + + diff --git a/visualisation.py b/visualisation.py new file mode 100644 index 0000000..928a6c4 --- /dev/null +++ b/visualisation.py @@ -0,0 +1,215 @@ +import argparse +from datetime import datetime +import json +import os +import sys + +import jinja2 + + +def unix_to_python(timestamp): + return datetime.utcfromtimestamp(float(timestamp)) + + +def python_to_string(timestamp, dt_format='%Y-%m-%dT%H:%M:%S.0'): + """ + Example: 2017-11-11T03:30:00.0 + """ + return datetime.fromtimestamp(int(timestamp)).strftime(dt_format) + + +def add_totalsteps_to_summary(content): + result = [] + totalsteps = 0 + for item in content: + totalsteps = totalsteps + item['steps'] + item['totalsteps'] = totalsteps + result.append(item) + return result + + +def summary_to_graphdata(content): + """Returns a list of datetime tuples with: + {'datetime': ["2017-08-12T22:00:00.0", ..], + 'totalsteps': [0, 0, 0, 10, 32, 42, 128, ..], + 'sleeping_steps': [0, 0, None, None, None, 0, None, ..], + 'active_steps': [None, None, 10, 500, 120242, None, ..], + 'sedentary_steps': [None + """ + datetimes = [] + totalsteps_list = [] + sleeping_steps_list = [] + active_steps_list = [] + highlyactive_steps_list = [] + sedentary_steps_list = [] + generic_steps_list = [] + totalsteps = 0 + + for item in content: + sleeping_steps = 0 + active_steps = 0 + highlyactive_steps = 0 + sedentary_steps = 0 + generic_steps = 0 + datetimes.append(item['startGMT'] + 'Z') + totalsteps = totalsteps + item['steps'] + totalsteps_list.append(totalsteps) + if item['primaryActivityLevel'] == 'sedentary': + sedentary_steps = item['steps'] + elif item['primaryActivityLevel'] == 'active': + active_steps = item['steps'] + elif item['primaryActivityLevel'] == 'highlyActive': + highlyactive_steps = item['steps'] + elif item['primaryActivityLevel'] == 'sleeping': + sleeping_steps = item['steps'] + elif item['primaryActivityLevel'] == 'generic' or item['primaryActivityLevel'] == 'none': + generic_steps = item['steps'] + else: + #print(item['primaryActivityLevel']) + print('Unknown activity level found:') + print(item) + + sleeping_steps_list.append(sleeping_steps) + active_steps_list.append(active_steps) + highlyactive_steps_list.append(highlyactive_steps) + sedentary_steps_list.append(sedentary_steps) + generic_steps_list.append(generic_steps) + + return {'datetime': datetimes, 'totalsteps': totalsteps_list, + 'sleeping_steps': sleeping_steps_list, 'active_steps': active_steps_list, + 'highlyactive_steps': highlyactive_steps_list, 'sedentary_steps': sedentary_steps_list, + 'generic_steps': generic_steps_list} + + +def heartrate_to_graphdata(content): + values = content['heartRateValues'] + rates = [] + for value in values: + rates.append([python_to_string(value[0]/1000), value[1]]) + return rates + + +def stress_to_graphdata(content): + values = content['stressValuesArray'] + stress = [] + for value in values: + if value[1] > 0: + stress.append([python_to_string(value[0]/1000), value[1]]) + return stress + + +def sleep_to_graphdata(content): + #return {'sleepEndTimestampLocal': python_to_string(content['sleepEndTimestampLocal']/1000), + # 'sleepStartTimestampLocal': python_to_string(content['sleepStartTimestampLocal']/1000), + # } + return {'sleepEndTimestamp': python_to_string(content['sleepEndTimestampGMT']/1000), + 'sleepStartTimestamp': python_to_string(content['sleepStartTimestampGMT']/1000), + } + + +def parse_wellness(wellness, content): + try: + content['allMetrics'] + except TypeError: + # Not a correct wellness file + return wellness + + for item in content['allMetrics']['metricsMap']: + if 'SLEEP_' in item: + key = item[len('SLEEP_'):].lower() + else: + key = item.lstrip('WELLNESS_').lower() + for value in content['allMetrics']['metricsMap'][item]: + if key not in wellness: + wellness[key] = {} + if value['value']: + wellness[key][value['calendarDate']] = int(value['value']) + else: + wellness[key][value['calendarDate']] = None + return wellness + + +def parse_files(directory, target_directory): + heartrate = {} + stress = {} + sleep = {} + summary = [] + wellness = {} + for filename in sorted(os.listdir(directory)): + if filename.endswith("_summary.json"): + # parse summary, create graph + with open(os.path.join(directory, filename), 'r') as f: + content = json.load(f) + summary.append((filename.split('_')[0], summary_to_graphdata(content))) + elif filename.endswith("_heartrate.json"): + # parse heartrate, create graph + with open(os.path.join(directory, filename), 'r') as f: + content = json.load(f) + heartrate[filename.split('_')[0]] = heartrate_to_graphdata(content) + elif filename.endswith("_stress.json"): + # parse stress, create graph + with open(os.path.join(directory, filename), 'r') as f: + content = json.load(f) + stress[filename.split('_')[0]] = stress_to_graphdata(content) + elif filename.endswith("_sleep.json"): + # parse stress, create graph + with open(os.path.join(directory, filename), 'r') as f: + content = json.load(f) + sleep[filename.split('_')[0]] = sleep_to_graphdata(content) + elif filename.endswith(".json"): + # parse wellness data + with open(os.path.join(directory, filename), 'r') as f: + content = json.load(f) + wellness = parse_wellness(wellness, content) + else: + continue + + # Reverse list so latest days are on top + summary = summary[::-1] + + return {'summaries': summary, 'wellness': wellness, 'heartrate': heartrate, 'stress': stress, 'sleep': sleep} + + +def generate_wellnesspage(template_dir, outputfile, alldata): + """ Generate graphs for the various measurements""" + loader = jinja2.FileSystemLoader(template_dir) + environment = jinja2.Environment(loader=loader, trim_blocks=True, lstrip_blocks=True) + + try: + template = environment.get_template('wellness.html') + except jinja2.exceptions.TemplateNotFound as e: + print 'E Template not found: ' + str(e) + ' in template dir ' + template_dir + sys.exit(2) + + output = template.render(alldata) + with open(outputfile, 'w') as pf: + pf.write(output) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Garmin Data Visualiser', + epilog='Because the hell with APIs!', add_help='How to use', + prog='python visualise.py -i -o ') + parser.add_argument('-i', '--input', required=False, + help='Input directory.', default=os.path.join(os.getcwd(), 'Wellness/')) + parser.add_argument('-o', '--output', required=False, + help='Output directory.', default=os.path.join(os.getcwd(), 'Graphs/')) + args = vars(parser.parse_args()) + + # Sanity check, before we do anything: + if args['input'] is None and args['output'] is None: + print("Must specify an input (-i) directory for the Wellness data, and an output (-o) directory for graphs.") + sys.exit() + + # Try to use the user argument from command line + outputdir = args['output'] + inputdir = args['input'] + alldata = parse_files(inputdir, outputdir) + template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'templates') + outputfile = os.path.join(outputdir, 'wellness.html') + + # Create output directory (if it does not already exist). + if not os.path.exists(outputdir): + os.makedirs(outputdir) + + generate_wellnesspage(template_dir, outputfile, alldata)