From 4eaceb8b4de20a0b6630fedcdd331f6f0156b01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Gei=C3=9Fler?= Date: Mon, 14 Oct 2024 00:11:55 +0200 Subject: [PATCH] init V --- .../gcexport.log | 3 + garmin-connect-export/BRANCH.md | 14 + garmin-connect-export/CHANGELOG.md | 249 + garmin-connect-export/CONTRIBUTING.md | 86 + garmin-connect-export/LICENSE | 22 + garmin-connect-export/README.md | 207 + .../__pycache__/filtering.cpython-310.pyc | Bin 0 -> 1965 bytes .../csv_header_all.properties | 92 + .../csv_header_default.properties | 51 + .../csv_header_kjkjava.properties | 42 + .../csv_header_moderation.properties | 43 + garmin-connect-export/filtering.py | 77 + garmin-connect-export/filtering_test.py | 120 + garmin-connect-export/gcexport.py | 1345 ++++ garmin-connect-export/gcexport_test.py | 248 + garmin-connect-export/html/profile_email.html | 135 + .../html/profile_simple.html | 135 + garmin-connect-export/html/profile_uuid.html | 135 + garmin-connect-export/json/README.md | 5 + .../json/activities-list.json | 632 ++ .../json/activity-search-service-1.2.json | 822 +++ .../activity_154105348_gpx_device_null.json | 94 + .../json/activity_2541953812.json | 148 + .../json/activity_2541953812_overview.json | 210 + .../json/activity_2541953812_samples.json | 5525 +++++++++++++++++ .../json/activity_2541953812_zones.json | 27 + .../json/activity_995784118_gpx_device_0.json | 100 + .../json/activity_emoji.json | 153 + .../json/activity_multisport_child.json | 118 + .../json/activity_multisport_detail.json | 112 + .../json/activity_multisport_overview.json | 210 + .../json/activity_types.properties | 123 + .../json/activitylist-service.json | 165 + garmin-connect-export/json/device_856399.json | 35 + .../json/device_99280678.json | 35 + .../json/event_types.properties | 7 + garmin-connect-export/json/userstats.json | 26 + 37 files changed, 11551 insertions(+) create mode 100644 garmin-connect-export/2022-12-21_garmin_connect_export/gcexport.log create mode 100644 garmin-connect-export/BRANCH.md create mode 100644 garmin-connect-export/CHANGELOG.md create mode 100644 garmin-connect-export/CONTRIBUTING.md create mode 100644 garmin-connect-export/LICENSE create mode 100644 garmin-connect-export/README.md create mode 100644 garmin-connect-export/__pycache__/filtering.cpython-310.pyc create mode 100644 garmin-connect-export/csv_header_all.properties create mode 100644 garmin-connect-export/csv_header_default.properties create mode 100644 garmin-connect-export/csv_header_kjkjava.properties create mode 100644 garmin-connect-export/csv_header_moderation.properties create mode 100644 garmin-connect-export/filtering.py create mode 100644 garmin-connect-export/filtering_test.py create mode 100644 garmin-connect-export/gcexport.py create mode 100644 garmin-connect-export/gcexport_test.py create mode 100644 garmin-connect-export/html/profile_email.html create mode 100644 garmin-connect-export/html/profile_simple.html create mode 100644 garmin-connect-export/html/profile_uuid.html create mode 100644 garmin-connect-export/json/README.md create mode 100644 garmin-connect-export/json/activities-list.json create mode 100644 garmin-connect-export/json/activity-search-service-1.2.json create mode 100644 garmin-connect-export/json/activity_154105348_gpx_device_null.json create mode 100644 garmin-connect-export/json/activity_2541953812.json create mode 100644 garmin-connect-export/json/activity_2541953812_overview.json create mode 100644 garmin-connect-export/json/activity_2541953812_samples.json create mode 100644 garmin-connect-export/json/activity_2541953812_zones.json create mode 100644 garmin-connect-export/json/activity_995784118_gpx_device_0.json create mode 100644 garmin-connect-export/json/activity_emoji.json create mode 100644 garmin-connect-export/json/activity_multisport_child.json create mode 100644 garmin-connect-export/json/activity_multisport_detail.json create mode 100644 garmin-connect-export/json/activity_multisport_overview.json create mode 100644 garmin-connect-export/json/activity_types.properties create mode 100644 garmin-connect-export/json/activitylist-service.json create mode 100644 garmin-connect-export/json/device_856399.json create mode 100644 garmin-connect-export/json/device_99280678.json create mode 100644 garmin-connect-export/json/event_types.properties create mode 100644 garmin-connect-export/json/userstats.json diff --git a/garmin-connect-export/2022-12-21_garmin_connect_export/gcexport.log b/garmin-connect-export/2022-12-21_garmin_connect_export/gcexport.log new file mode 100644 index 0000000..c39742a --- /dev/null +++ b/garmin-connect-export/2022-12-21_garmin_connect_export/gcexport.log @@ -0,0 +1,3 @@ +2022-12-21 22:00:17,681 [INFO ] Starting gcexport.py version 4.0.0, using Python version 3.10.6 +2022-12-21 22:00:17,681 [INFO ] New logfile level: INFO +2022-12-21 22:00:17,682 [WARNING] Output directory ./2022-12-21_garmin_connect_export already exists. Will skip already-downloaded files and append to the CSV file. diff --git a/garmin-connect-export/BRANCH.md b/garmin-connect-export/BRANCH.md new file mode 100644 index 0000000..e80fb25 --- /dev/null +++ b/garmin-connect-export/BRANCH.md @@ -0,0 +1,14 @@ +# develop branch + +The [master](https://github.com/pe-st/garmin-connect-export) branch; +it is the default branch. This is where my tinkering happens, and always contains the latest version of `gcexport.py` + +Other notable branches in this repo: + +- [develop](https://github.com/pe-st/garmin-connect-export/tree/develop) + Former develop branch of my repo, kept for for the time being for compatibility + for the existing clones of my repo out there somewhere +- [moderation](https://github.com/pe-st/garmin-connect-export/tree/moderation) + Copy of the master branch of **@moderation**'s repo, kept for reference and comparison purposes +- [revert-csv-to-kjkjava](https://github.com/pe-st/garmin-connect-export/tree/feature/revert-csv-to-kjkjava) + Backport of **@moderation**'s and my changes to the original repo, using the original CSV format ([PR 42](https://github.com/kjkjava/garmin-connect-export/pull/42)) diff --git a/garmin-connect-export/CHANGELOG.md b/garmin-connect-export/CHANGELOG.md new file mode 100644 index 0000000..dc31411 --- /dev/null +++ b/garmin-connect-export/CHANGELOG.md @@ -0,0 +1,249 @@ +# Changelog for the Garmin Connect Exporter + +This changelog is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + + +## 4.0.0 - 2022-10-06 + +- added: new option `--logpath` (BREAKING change, you might need `--logpath=.` for the old behaviour) + ([Pull Request #74](https://github.com/pe-st/garmin-connect-export/pull/74) by @cristian5th and @bxsx) +- removed: Python 2 leftovers + ([Pull Request #73](https://github.com/pe-st/garmin-connect-export/pull/73) by @bxsx) +- added: configurations for flake8, pylint and black (including the needed improvements of the code for them) +- changed: Github Actions now execute also flake8, pylint and black tasks + + +## 3.3.0 - 2021-12-26 + +- added: support for time spent in HR zones (feature request) + ([Issue #68](https://github.com/pe-st/garmin-connect-export/issues/68) +- fixed: adapt display name parsing because of change Garmin Connect behaviour + ([Pull Request #69](https://github.com/pe-st/garmin-connect-export/pull/69) by @hannesweisbach) +- added: Github Action to execute the tests automatically + + +## 3.2.1 - 2021-08-15 + +- fixed: make the display name parsing work also for email addresses + ([Issue #65](https://github.com/pe-st/garmin-connect-export/issues/65) + + +## 3.2.0 - 2021-07-18 + +- changed: [Python 2.x is not supported anymore](https://github.com/pe-st/garmin-connect-export/issues/64) +- changed: the `--verbose` option now also saves some intermediate responses from Garmin +- changed: Improve logging for HTTP connection requests +- fixed: handle the case where an original file has a different extension than `.fit` + ([Pull Request #61](https://github.com/pe-st/garmin-connect-export/pull/61) by @cristian5th) +- fixed: don't write a record to the CSV file for already downloaded activities + ([Issue #34](https://github.com/pe-st/garmin-connect-export/issues/34), + [Pull Request #62](https://github.com/pe-st/garmin-connect-export/pull/62) by @cristian5th) + + +## 3.1.0 - 2021-06-03 + +- added: export of the parts of multi-sport activities +- added: `--exclude` option to exclude activities from the download + ([Pull Request #58](https://github.com/pe-st/garmin-connect-export/pull/58) by @chs8691) + + +## 3.0.3 - 2021-02-24 + +- fixed: `HTTP Error 402: Payment Required` (started appearing 2021-02-23) + ([Pull Request #55](https://github.com/pe-st/garmin-connect-export/pull/55) by @reto) +- changed: [detached from kjkjava's repo](https://github.com/pe-st/garmin-connect-export/issues/53) + + +## 3.0.2 - 2020-11-15 + +- fixed: unzipped filename was changed by Garmin, breaking redownload check (issue #48) + + +## 3.0.1 - 2020-05-23 + +- fixed: `--subdir` was broken in 3.0.0 by the migration to Python 3 (reported by @jamorris, issue #37) +- changed: moved unit tests for `resolve_path` from **unittest** framework to **pytest** + + +## 3.0.0 - 2020-05-16 + +- added: support for Python 3, thanks to @telemaxx and @bartskowron +- added: file `CONTRIBUTING.md` and moved some parts of `README.md` into it +- changed: with Windows the script now uses backslashes as path separator +- changed: default Git branch is now `master` (the old `develop` branch is kept for the time being) +- fixed: `-fp` option for `original` format + + +## 2.3.3 - 2020-01-30 + +- fixed: skipping existing `.fit` files didn't work (reported by @fellrnr, issue #25) + + +## 2.3.2 - 2019-08-25 + +- added: parent activity type "Winter Sports" + + +## 2.3.1 - 2019-08-18 + +- added: new command line switch `--subdir` / `-s` (courtesy of Christian Schulzendorff @chs8691): + Exported activity files now can be saved in subdirectories, optionally grouped by year (and/or month) of the activity start time. Usually this is used together with the parameter `directory`, which is the root directory for DIR. `--subdir` supports the two placeholders `{YYYY}` and `{MM}` which can be used within DIR. Examples: + ``` + --directory downloads --subdir {YYYY} --> downloads/2019/ + --directory downloads --subdir {YYYY}/{MM} --> downloads/2019/03/ + --directory downloads --subdir myTcxFiles/{YYYY} -f tcx --> downloads/myTcxFiles/2019/ + --directory downloads --subdir activities{YYYY}/GPX -f gpx --> downloads/activities2019/GPX/ + ``` + Note that only the activity files go into the subdirectory, the CSV and other files remain in the main directory +- added: new command line switch `--start_activity_no` / `-sa` (courtesy of Josef K @jkall): + This is a minor fix to allow user to restart downloading where it crashed. Example: + + First download: + ``` + $ python gcexport.py --count all ~/Downloads/garmin + Garmin Connect activity (657/2098) [activity_id] ... + ...some error... + ``` + Second run: + ``` + $ python gcexport.py --count all --start_activity_no 657 ~/Downloads/garmin + ... + Skipping Garmin Connect activity (656/2098) [activity_id] + Garmin Connect activity (657/2098) [activity_id] + ``` + + +## 2.3.0 - 2019-04-18 + +- changed: the HTTP request to login to Garmin Connect (the old one didn't work anymore) + + +## 2.2.1 - 2019-03-30 + +- added: new command line switch `--fileprefix` / `-fp` (courtesy of Christian Schulzendorff @chs8691): + A downloaded activity file can now have a date/time prefix, e.g. "20190301-065831-activity_3424910202.tcx". This works for all export types (tcx, gpx, json and original). + Existing downloaded files will not be touched. If downloaded twice, once with and once without the parameter, two files will be created. + + +## 2.2.0 - 2018-11-09 + +- added: new exported fields + - `vo2max` the VO2 Max (maximum volume of oxygen, cardiovascular fitness indicator) + - `aerobicEffect` aerobic training effect (value between 0 and 5) + - `anaerobicEffect` anaerobic training effect (value between 0 and 5) + - `averageRunCadence` average number of steps per minute. Excludes time spent standing + - `maxRunCadence` maximum number of steps per minute + - `strideLength` average length of the stride from one footfall to the next (in meters) + - `steps` number of steps + - `privacy` who can see your activity + - `fileFormat` the format of the original upload (fit, gpx or tcx) + - `locationName` location determined by Garmin + - `gear` the gear used (only tested for shoes); nickname if set, the brand/model otherwise + - `elevationCorrected` flag telling if the elevation correction is applied +- changed: new default CSV template with different CSV output; + to get the old CSV format use `-t csv_header_moderation.properties` +- added: Python version to the log file +- changed: improved exception logging + + +## 2.1.5 - 2018-09-24 + +- added: command line switches `-v` (verbosity) and `--desc` (description) + + +## 2.1.4 - 2018-09-21 + +- added: command line switches `-e` and `-a` to pass the CSV output to an external programm + (merged from @moderation's commit from 2018-09-09) + + +## 2.1.3 - 2018-09-17 + +- added: CHANGELOG.md file +- changed: improved detection if device information is unknown or missing + (i.e. they once were known, but the information got lost somehow) +- changed: the default CSV template (csv_header_default.properties) makes no difference + anymore between corrected and uncorrected elevation +- changed: the URL_GC_ACTIVITY_DETAIL endpoint isn't called anymore when the chosen + CSV template doesn't contain the `sampleCount` column + + +## 2.1.2 - 2018-09-11 + +- added: switch `-ot` to set file time to activity time (original + [Pull Request](https://github.com/kjkjava/garmin-connect-export/pull/8) by @tobiaslj) + + +## 2.1.1 - 2018-09-10 + +- added: Python module `logging` to write log files +- changed: console output is less verbose +- changed: remove most Pylint warning + + +## 2.1.0 - 2018-09-08 + +- added: CSV templates (csv_header_default.properties and csv_header_running.properties) + to make the CSV output configurable + + +## 2.0.3 - 2018-08-30 + +- changed: Fix regex for displayName to allow dots (original + [Pull Request](https://github.com/moderation/garmin-connect-export/pull/19) by @chmoelders) + + +## 2.0.2 - 2018-08-24 + +- changed: use the User Stats to discover the number of activities + (the old `activity-search-service-1.2` endpoint doesn't work anymore) + + +## 2.0.1 - 2018-06-15 + +- added: first unit tests +- changed: refactor into a Python module having a `main` function and using + the `if __name__ == "__main__":` incantation +- changed: fixed some Pylint issues +- changed: note about using the user name or email address for logging in +- changed: fixed user name prompt (reported by + [@TheKiteRunning](https://github.com/pe-st/garmin-connect-export/issues/6)) + + +## 2.0.0 - 2018-04-17 - pe-st | branch develop + +- changed: aligned with the current state of the **moderation** fork, but still using Python 2 for now +- changed: fixed distance and elapsedDuration parsing + ([Pull Request](https://github.com/pe-st/garmin-connect-export/pull/3) by @lindback) + + +## 2018-04-06 - pe-st | branch develop + +- changed: login ticket is now extracted from HTML response (the cookie doesn't contain the ticket anymore) + + +## 2018-03-10..2018-04-10 - pe-st | branch develop + +- changed: various tunings to the CSV output + + +## 2018-03-10 - pe-st | branch develop + +- added: using `activitylist-service` to get the list of activities +- changed: using **moderation**'s master as base using newer Garmin endpoints + (`activity-search-service-1.2`) + + +## 2017-06-14 - pe-st | branch develop + +- added: JSON export format + ([Pull Request](https://github.com/kjkjava/garmin-connect-export/pull/6) by @yohcop) +- changed: use newer endpoints for GPX/TCX downloads (`modern/.../download-service`) + ([Pull Request](https://github.com/kjkjava/garmin-connect-export/pull/30) by @julienr) +- changed: don't abort for HTTP status 204 (empty GPX file) + + +## 2015-12-23 - kjkjava | branch master + +- last commit in original repo of **kjkjava** +- using `activity-search-service-1.0` for the list and `activity-service-1.1` for GPX/TCX exports diff --git a/garmin-connect-export/CONTRIBUTING.md b/garmin-connect-export/CONTRIBUTING.md new file mode 100644 index 0000000..b7dd125 --- /dev/null +++ b/garmin-connect-export/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Welcome! + +If you are here, it means you are interested in helping this small project out. +A hearty welcome and thank you! There are many ways you can contribute: + +- Offer PR's to fix bugs or implement new features. +- Give feedback and bug reports regarding the software or the documentation. +- Setting up an automated toolchain with mock endpoints for the HTTP calls for easier testing +- Improve our examples, tutorials, and documentation. + +## Python Versions + +### Python 2 vs 3 + +At the time of this writing (2021-07) Python 2 has long passed its [sunset](https://python3statement.org/). +And the effort to still support Python 2 has become too big (see https://github.com/pe-st/garmin-connect-export/issues/64); +the script now uses features from Python 3 which Python 2 doesn't understand, +so if you try to run the script with Python 2 you will get complaints about +invalid syntax or other similar error messages. + +### Python 3.x Versions + +The script should be usable for a wide range of users and Python versions. + +For that reason the script should be compatible with the oldest still [supported +Python version](https://devguide.python.org/versions/). At the time of this writing +(July 2022) this is Python 3.7, whose end-of-life is currently scheduled for June 2023. + +The Github Actions (see `github/workflows/python-app.yml`) thus run all steps twice, +once with the oldest supported Python version, and once with the newest released version. + +## Getting started + +### Pull requests + +If you are new to GitHub [here](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) +is a detailed help source on getting involved with development on GitHub. + +### Testing + +There is a small set of unit test, using [pytest](https://docs.pytest.org/en/latest/); +these tests are executed automatically whenever a commit is pushed or when a pull request is updated. + +I found that the free [PyCharm Community](https://www.jetbrains.com/pycharm/download/) is well suited for running the +tests. + +Unfortunately there are no mocks yet for simulating Garmin Connect during development, so for real tests you'll have to +run the script against your own Garmin account. + +## REST endpoints + +As this script doesn't use the paid API, the endpoints to use are known by reverse engineering browser sessions. And as +the Garmin Connect website changes over time, chances are that this script gets broken. + +Small history of the endpoints used by `gcexport.py` to get a list of activities: + +- [activity-search-service-1.0](https://connect.garmin.com/proxy/activity-search-service-1.0/json/activities): + initial endpoint used since 2015, worked at least until January 2018 +- [activity-search-service-1.2](https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities): + endpoint introduced in `gcexport.py` in August 2016. In March 2018 this still works, but doesn't allow you to fetch + more than 20 activities, even split over multiple calls (when doing three consecutive calls with 1,19,19 as `limit` + parameter, the third one fails with HTTP error 500). + In August 2018 it stopped working altogether. The JSON returned by this endpoint however was quite rich + (see example `activity-search-service-1.2.json` in the `json` folder). +- [Profile page](https://connect.garmin.com/modern/profile) and + [User Stats page](https://connect.garmin.com/modern/proxy/userstats-service/statistics/user_name) + were introduced in August 2018 when activity-search-service-1.2 stopped working. Their purpose in this script is + solely to get the number of activities which I didn't find elsewhere. +- [activitylist-service](https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities): + endpoint introduced in `gcexport.py` in March 2018. The JSON returned by this endpoint is very different from the + activity-search-service-1.2 one (also here see the example in the `json` folder), e.g. + - it is concise and offers no redundant information (e.g. only speed, not speed and pace) + - the units are not explicitly given and must be deducted (e.g. the speed unit is m/s) + - there is less information, e.g. there is only one set of elevation values (not both corrected and uncorrected), and other values like minimum heart rate are missing. + - some other information is available only as an ID (e.g. `timeZoneId` or `deviceId`), and more complete information + is available by further REST calls (one for each activity and additional ones for device information) + +Endpoints to get information about a specific activity: + +- [activity-service](https://connect.garmin.com/modern/proxy/activity-service/activity/nnnn): A kind of summary of the activity, most values are present in their canonical format. +- [activity-service-1.3 details](https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/nnnn): A detailed list of measurements, with a list of the metrics available for each measurement. + +### Limitations of Data Provided by Current Endpoints and Choices Made + +- The timezones provided are just the names (e.g. for Central European Time CET you can get either "Europe/Paris" or "(GMT+01:00) Central European Time"), but not the exact offset. Note that the "GMT+01:00" part of the name is hardcoded, so in summer (daylight savings time) Garmin Connect still uses +01:00 in the name even if the offset then is +02:00. To get the offset you need to calculate the difference between the startTimeLocal and the startTimeGMT. + diff --git a/garmin-connect-export/LICENSE b/garmin-connect-export/LICENSE new file mode 100644 index 0000000..90bd8fc --- /dev/null +++ b/garmin-connect-export/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Kyle Krafka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/garmin-connect-export/README.md b/garmin-connect-export/README.md new file mode 100644 index 0000000..cababad --- /dev/null +++ b/garmin-connect-export/README.md @@ -0,0 +1,207 @@ +# garmin-connect-export script + +:exclamation: | This script [now requires Python 3.7 or newer](https://github.com/pe-st/garmin-connect-export/issues/64) +---|--- + +:exclamation: | There is a [report of a deactivated user account that might by caused by using this script](https://github.com/pe-st/garmin-connect-export/issues/60). The exact reasons are not known, and my account has never been deactivated. But be aware that I can give no guarantee that Garmin tolerates requests made from this script. I believe though that this script is fair use (it doesn't do anything other than automating stuff that you do in the browser). But be careful if you plan to run the script as periodical task (with `cron` etc) +---|--- + +Download a copy of your Garmin Connect data, including stats and GPX tracks. + +Note that Garmin introduced a while ago (around May 2018, for GDPR compatibility) a possibility to [download all of your Garmin Connect data](https://www.garmin.com/en-US/account/datamanagement/exportdata/) in one zip file. Depending on your needs this might be enough, but the script here offers additional features like getting GPX tracks instead of the original upload format or limiting the export to just a couple of activities. + +## Forks and Branches + +Before going into the details of this script itself, some meta information. + +There exist many [forks](https://help.github.com/articles/fork-a-repo/) of this script repository: + +- [pe-st](https://github.com/pe-st/garmin-connect-export) + This is my (**pe-st**) repository, the one you're looking at (or the source of the copy you're looking at). + It was [detached from **kjkjava**'s repo](https://github.com/pe-st/garmin-connect-export/issues/53) + (see below) in February 2021, after more than 5 years of inactivity of the upstream repo. +- [kjkjava](https://github.com/kjkjava/garmin-connect-export) + The original repo (mother repo) of my (**pe-st**) repo. It seems not maintained anymore (last commit in 2015, see also: [pr#42](https://github.com/kjkjava/garmin-connect-export/pull/42) and [issues#46](https://github.com/kjkjava/garmin-connect-export/issues/46)) +- [moderation](https://github.com/moderation/garmin-connect-export) + After some inactivity of the **@kjkjava** repo, **@moderation** made some corrections in his own fork to have a working script again. His fork is primarily designed for his use which is cycling, while mine (**pe-st**) is running. + In March 2018 I integrated **@moderation**'s work into my own repo, so logically **@moderation** is now the + father repo of my repo. In April 2018 **@moderation** migrated his script to Python 3. Unfortunately + **@moderation**'s script [didn't work for me for a couple of months](https://github.com/moderation/garmin-connect-export/issues/11), + probably because of different Garmin Connect REST endpoints (URLs). + +For the [branches](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) in **pe-st**'s repo see [BRANCH.md](BRANCH.md) + +## Description + +This script will backup your personal Garmin Connect data. All downloaded data will go into a directory called `YYYY-MM-DD_garmin_connect_export/` in the current working directory. Activity records and details will go into a CSV file called `activities.csv`. GPX files (or whatever format you specify) containing track data, activity title, and activity descriptions are saved as well, using the Activity ID. + +If there is no GPS track data (e.g., due to an indoor treadmill workout), a data file is still saved. If the GPX format is used, activity title and description data are saved. If the original format is used, Garmin may not provide a file at all and an empty file will be created. For activities where a GPX file was uploaded, Garmin may not have a TCX file available for download, so an empty file will be created. Since GPX is the only format Garmin should have for every activity, it is the default and preferred download format. + +If you have many activities, you may find that this script crashes with an "Operation timed out" message. Just run the script again and it will pick up where it left off. + +## Installation + +- If you're comfortable using Git, just clone the repo from github +- Otherwise get the latest `zip` (or `tar.gz`) from the [releases page](https://github.com/pe-st/garmin-connect-export/releases) + and unpack it where it suits you. + +## Usage + +You will need a little experience running things from the command line to use this script. That said, here are the usage details from the `--help` flag: + +``` +usage: gcexport.py [-h] [--version] [-v] [--username USERNAME] + [--password PASSWORD] [-c COUNT] [-e EXTERNAL] [-a ARGS] + [-f {gpx,tcx,original,json}] [-d DIRECTORY] [-s SUBDIR] + [-lp LOGPATH] [-u] [-ot] [--desc [DESC]] [-t TEMPLATE] + [-fp] [-sa START_ACTIVITY_NO] [-ex FILE] + +Garmin Connect Exporter + +optional arguments: + -h, --help show this help message and exit + --version print version and exit + -v, --verbosity increase output and log verbosity, save more intermediate files + --username USERNAME your Garmin Connect username or email address (otherwise, you will be prompted) + --password PASSWORD your Garmin Connect password (otherwise, you will be prompted) + -c COUNT, --count COUNT + number of recent activities to download, or 'all' (default: 1) + -e EXTERNAL, --external EXTERNAL + path to external program to pass CSV file too + -a ARGS, --args ARGS additional arguments to pass to external program + -f {gpx,tcx,original,json}, --format {gpx,tcx,original,json} + export format; can be 'gpx', 'tcx', 'original' or 'json' (default: 'gpx') + -d DIRECTORY, --directory DIRECTORY + the directory to export to (default: './YYYY-MM-DD_garmin_connect_export') + -s SUBDIR, --subdir SUBDIR + the subdirectory for activity files (tcx, gpx etc.), supported placeholders are {YYYY} and {MM} + (default: export directory) + -lp LOGPATH, --logpath LOGPATH + the directory to store logfiles (default: same as for --directory) + -u, --unzip if downloading ZIP files (format: 'original'), unzip the file and remove the ZIP file + -ot, --originaltime will set downloaded (and possibly unzipped) file time to the activity start time + --desc [DESC] append the activity's description to the file name of the download; limit size if number is given + -t TEMPLATE, --template TEMPLATE + template file with desired columns for CSV output + -fp, --fileprefix set the local time as activity file name prefix + -sa START_ACTIVITY_NO, --start_activity_no START_ACTIVITY_NO + give index for first activity to import, i.e. skipping the newest activities + -ex FILE, --exclude FILE + JSON file with Array of activity IDs to exclude from download. + Format example: {"ids": ["6176888711"]} +``` + +### Examples + +- `python gcexport.py --count all` + will download all of your data to a dated directory. + +- `python gcexport.py -c all -f gpx -ot --desc 20` + will export all of your data in GPX format, set the timestamp of the GPX files to the start time of the activity and append the 20 first characters of the activity's description to the file name. + +- `python gcexport.py -c all -e /Applications/LibreOffice.app/Contents/MacOS/soffice -a calc` + will download all of your data and then use LibreOffice to open the CSV file with the list of your activities (the path to LibreOffice is platform-specific; the example is for macOS). + +- `python gcexport.py -d ~/MyActivities -c 3 -f original -u --username bobbyjoe --password bestpasswordever1` + will download your three most recent activities in the FIT file format (or whatever they were uploaded as) into the `~/MyActivities` directory (unless they already exist). Using the `--password` flags is not recommended because your password will be stored in your command line history. Instead, omit it to be prompted (and note that nothing will be displayed when you type your password). Equally you might not want to have the username stored in your command line history; in this case avoid also to give the `--username` option, and you'll be prompted for it. Note also that depending on the age of your garmin account your username is the email address (I myself still can login both with username and email address, but I've had a report that for some users the email address is mandatory to login). + +Alternatively, you may run it with `./gcexport.py` if you set the file as executable (i.e., `chmod u+x gcexport.py`). + +### Notes on the Usage + +- The `-c COUNT` option might appear to count wrongly when exporting multi-sport activities; + they count as one activity, but the incrementing counter displayed on the console counts + also the individual parts of a multi-sport activity + + +### Python + +Of course, you must have Python installed to run this, any recent 3.x version should work +(see also [Python 3.x Versions](./CONTRIBUTING.md#python-3x-versions)). + +Most Mac and Linux users should already have Python. +Note that if you run into the [TLSV1 ALERT problem](https://github.com/pe-st/garmin-connect-export/issues/16) +or the [HTTP 403 Authentication Error](https://github.com/pe-st/garmin-connect-export/issues/59), +your Python installation might not be recent enough). +In this case you can install a more recent Python on your Mac using [Homebrew](https://docs.brew.sh/Homebrew-and-Python) +and/or [pyenv](https://github.com/pyenv/pyenv). + +Also, as stated above, you should have some basic command line experience. + + +## Data + +This tool is not guaranteed to get all of your data, or even download it correctly. I have only tested it with my account and it works fine, but different account settings or different data types could potentially cause problems. Also, because this is not an official feature of Garmin Connect, Garmin may very well make changes that break this utility (and they certainly have since I created this project). + +If you want to see all of the raw data that Garmin hands to this script, just choose the JSON export format (`-f json`); in this case only metadata is exported, no track data. + +The format of the CSV export file can be customized with template files (in Properties format, see the `--template` option); three examples are included: + +- `csv_header_default.properties` (the default) gives you my preferred selection of columns, mainly targeted at running and hiking +- `csv_header_all.properties` gives you all available columns, handy as starting point for your own selection +- `csv_header_moderation.properties` gives you the same output as **@moderation**'s fork, mainly targeted at cycling +- `csv_header_kjkjava.properties` gives you an output similar as **@kjkjava**'s original script, mainly targeted at running + +You can easily create a template file for your needs, just copy one of the examples and change the appearing columns, their order and/or their title. + +Some important columns explained: + +- `raw` (e.g. `durationRaw`) columns usually give you unformatted data as provided by the Garmin API, other columns (e.g. `duration`) often format the data more readable +- speed columns (e.g. `averageSpeedRaw` and `averageSpeedPace`): when there is `Pace` in the column name the value given is a speed (km/) or pace (minutes per kilometer) depending on the activity type (e.g. pace for running, hiking and walking activities, speed for other activities) +- The elevation is either uncorrected or corrected, with a flag telling which. The current API doesn't provide both sets of elevations + +## Garmin Connect API + +This script is for personal use only. It simulates a standard user session (i.e., in the browser), logging in using cookies and an authorization ticket. This makes the script pretty brittle. If you're looking for a more reliable option, particularly if you wish to use this for some production service, Garmin does offer a paid API service. + +More information about the API endpoints used in the script is available in [CONTRIBUTING.md](CONTRIBUTING.md) + +## History + +The original project was written in PHP (formerly in the `old` directory, now deleted), based on "Garmin Connect export to Dailymile" code at (link has been down for a while). It no longer works due to the way Garmin handles logins. It could be updated, but I (**kjkjava**) decided to rewrite everything in Python for the latest version. + +After 2015, when the original repo stopped being maintained, several forks from **kjkjava** started appearing (see +Forks and Branches section above). + +In 2021 this fork was [detached from the original repo](https://github.com/pe-st/garmin-connect-export/issues/53); +in what concerns Github, the repo isn't a fork anymore, but a new "original". +For the history of this fork see the [CHANGELOG](CHANGELOG.md) + + +## Contributions + +Contributions are welcome, see [CONTRIBUTING.md](CONTRIBUTING.md) + +Contributors as of 2021-12 (Hope I didn't forget anyone, +see also [Contributors](https://github.com/pe-st/garmin-connect-export/graphs/contributors)): + +- Kyle Krafka @kjkjava +- Jochem Wichers Hoeth @jowiho +- Andreas Loeffler @lefty01 +- @sclub +- Yohann Coppel @yohcop +- Tobias Ljunggren @tobiaslj +- @cdstrachan +- Michael Payne @moderation +- Chris McCarty @cmccarty +- Julien Rebetez @julienr +- Peter Steiner @pe-st +- @lindback +- @TheKiteRunning +- Jens Diemer @jedie +- Christian Moelders @chmoelders +- Christian Schulzendorff @chs8691 +- Josef K @jkall +- Thomas Th @telemaxx +- Bart Skowron @bxsx +- @reto +- Cristian @cristian5th +- @hannesweisbach + +## License + +[MIT](https://github.com/pe-st/garmin-connect-export/blob/master/LICENSE) © 2015 Kyle Krafka and contributors + +## Thank You + +Thanks for using this script and I hope you find it as useful as I do! :smile: diff --git a/garmin-connect-export/__pycache__/filtering.cpython-310.pyc b/garmin-connect-export/__pycache__/filtering.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd020e7e8b8ff182854fad0a3b5f2a3a5edb9e29 GIT binary patch literal 1965 zcmZWq-HIDG6qYoTi;}DsVG&)D$=p23DvDnzCBWQoA-_k!L zg#NZK>y5z6*YKzZ&@t3S3^S7BE_Ng75;uBXk9l36`Q3m8Ec^*|Ll&_Z#)y$4)T+(e z^>27S;*$2qMXEDVDB2hBmgSnutQb&z!fBoYq$Di5fnLFn0E&-%^wz`Z2u2;A#?wQL><{qb z3hx*^Kls`2PS6)fy_n$2^BBCB+4`eSp?%BsSVE#)S|*u3p%5eOOEIK2WScy`OAn+J zQqf74=kztFBgm%a%mM6=5}6F0-#$IkF3%3f?xdpx5Fx+(O`4AxuXafH&3*dZUWM~L zY%40{J(~3$5G{m;s~Z>W9_@&N@49`nttS@p(zf53UFm>hQhnTDc@d0t|Net_*u=a@ z1%o@6mRz&#j-Y2rp0O&Ztk`eW3=zus_JAhjZ{R4(1THWWX38Gk_+rEh^5|bsE&QyAhvH30%MGu|80hK zOmOpG5?;HI*TF5dfv*~_mx7_Y;GSOhC%0ZL)P(b9I_wA#a~ zx9x@1&3dfc^RNu7Sj*tkSS=Dn`|J97>{=^MnvhV(&_|5XO!MJ-Md>M>XiyXo%JJLM zH_7eIPHjDspsvRW$V#7NRk`A4Tu!MPrzuyeKh7^}6KAWUHLV2k9h5VR4&2RB3fr@S zaiA;%lGzex%iV`ERw+wQM@u;^g}bbSQ>bzu>vv_KhwVzXmC#uxfv-YN_i1}sG^rF2 z)TP0B8$6LduucZh8Pt)sRL+`r)MfBlLGP&6jBpFmO}t59d{2J7brIKNm^Wa}ht&os zdrfg_F Pis1|~_KA;UuW|E#mr@1u literal 0 HcmV?d00001 diff --git a/garmin-connect-export/csv_header_all.properties b/garmin-connect-export/csv_header_all.properties new file mode 100644 index 0000000..5a7b330 --- /dev/null +++ b/garmin-connect-export/csv_header_all.properties @@ -0,0 +1,92 @@ +# column template with all known columns +id=Activity ID +url=Activity URL +activityName=Activity Name +description=Description +startTimeIso=Start Time +startTime1123=Begin Timestamp +startTimeMillis=Begin Timestamp (ms) +startTimeRaw=Begin Timestamp (raw) +endTimeIso=End Time +endTime1123=End Timestamp +endTimeMillis=End Timestamp (ms) +locationName=Location Name +durationRaw=Duration (raw) +duration=Duration (h:m:s) +elapsedDurationRaw=Elapsed Duration (raw) +elapsedDuration=Elapsed Duration (h:m:s) +movingDurationRaw=Moving Duration (raw) +movingDuration=Moving Duration (h:m:s) +distanceRaw=Distance (km) +averageSpeedRaw=Average Speed (km/h) +averageSpeedPaceRaw=Average Speed or Pace (raw) +averageSpeedPace=Average Speed (km/h or min/km) +averageMovingSpeedRaw=Average Moving Speed (km/h) +averageMovingSpeedPaceRaw=Average Moving Speed or Pace (raw) +averageMovingSpeedPace=Average Moving Speed (km/h or min/km) +maxSpeedRaw=Max. Speed (km/h) +maxSpeedPaceRaw=Max. Speed or Pace (raw) +maxSpeedPace=Max. Speed (km/h or min/km) +elevationGain=Elevation Gain (m) +elevationGainUncorr=Elevation Gain (m, uncorr) +elevationGainCorr=Elevation Gain (m, corrected) +elevationLoss=Elevation Loss (m) +elevationLossUncorr=Elevation Loss (m, uncorr) +elevationLossCorr=Elevation Loss (m, corrected) +minElevation=Elevation Min. (m) +minElevationUncorr=Elevation Min. (m, uncorr) +minElevationCorr=Elevation Min. (m, corrected) +minElevation=Elevation Min. (m) +minElevationUncorr=Elevation Min. (m, uncorr) +maxElevationCorr=Elevation Max. (m, corrected) +elevationCorrected=Elevation Corrected +maxHRRaw=Max. Heart Rate (raw) +maxHR=Max. Heart Rate (bpm) +averageHRRaw=Average Heart Rate (raw) +averageHR=Average Heart Rate (bpm) +caloriesRaw=Calories (raw) +calories=Calories +vo2max=VO2max +aerobicEffect=Aerobic Training Effect +anaerobicEffect=Anaerobic Training Effect +hrZone1Low=Low Boundary HR Zone 1 +hrZone1Seconds=Seconds in HR Zone 1 +hrZone2Low=Low Boundary HR Zone 2 +hrZone2Seconds=Seconds in HR Zone 2 +hrZone3Low=Low Boundary HR Zone 3 +hrZone3Seconds=Seconds in HR Zone 3 +hrZone4Low=Low Boundary HR Zone 4 +hrZone4Seconds=Seconds in HR Zone 4 +hrZone5Low=Low Boundary HR Zone 5 +hrZone5Seconds=Seconds in HR Zone 5 +averageRunCadence=Avg. Run Cadence +maxRunCadence=Max. Run Cadence +strideLength=Stride Length +steps=Steps +averageCadence=Avg. Cadence (rpm) +maxCadence=Max. Cadence (rpm) +strokes=Strokes +averageTemperature=Avg. Temp (°C) +minTemperature=Min. Temp (°C) +maxTemperature=Max. Temp (°C) +device=Device +gear=Gear +activityTypeKey=Activity Type Key +activityType=Activity Type +activityParent=Activity Parent +eventTypeKey=Event Type Key +eventType=Event Type +privacy=Privacy +fileFormat=File Format +tz=Time Zone +tzOffset=Offset +locationName=Location Name +startLatitudeRaw=Begin Latitude (raw) +startLatitude=Begin Latitude (°DD) +startLongitudeRaw=Begin Longitude (raw) +startLongitude=Begin Longitude (°DD) +endLatitudeRaw=End Latitude (raw) +endLatitude=End Latitude (°DD) +endLongitudeRaw=End Longitude (raw) +endLongitude=End Longitude (°DD) +sampleCount=Sample count diff --git a/garmin-connect-export/csv_header_default.properties b/garmin-connect-export/csv_header_default.properties new file mode 100644 index 0000000..27acd07 --- /dev/null +++ b/garmin-connect-export/csv_header_default.properties @@ -0,0 +1,51 @@ +# default column template trying to fit multiple sports +startTimeIso=Start Time +endTimeIso=End Time +id=Activity ID +activityName=Activity Name +description=Description +locationName=Location Name +tz=Time Zone +tzOffset=Offset +duration=Duration (h:m:s) +elapsedDuration=Elapsed Duration (h:m:s) +movingDuration=Moving Duration (h:m:s) +activityParent=Activity Parent +activityType=Activity Type +eventType=Event Type +device=Device +gear=Gear +privacy=Privacy +fileFormat=File Format +distanceRaw=Distance (km) +averageSpeedRaw=Average Speed (km/h) +averageSpeedPace=Average Speed (km/h or min/km) +averageMovingSpeedRaw=Average Moving Speed (km/h) +averageMovingSpeedPace=Average Moving Speed (km/h or min/km) +maxSpeedRaw=Max. Speed (km/h) +maxSpeedPace=Max. Speed (km/h or min/km) +elevationGain=Elevation Gain (m) +elevationLoss=Elevation Loss (m) +minElevation=Elevation Min. (m) +maxElevation=Elevation Max. (m) +elevationCorrected=Elevation Corrected +startLatitude=Begin Latitude (°DD) +startLongitude=Begin Longitude (°DD) +endLatitude=End Latitude (°DD) +endLongitude=End Longitude (°DD) +maxHR=Max. Heart Rate (bpm) +averageHR=Average Heart Rate (bpm) +calories=Calories +vo2max=VO2max +aerobicEffect=Aerobic Training Effect +anaerobicEffect=Anaerobic Training Effect +averageRunCadence=Avg. Run Cadence +maxRunCadence=Max. Run Cadence +strideLength=Stride Length +steps=Steps +averageCadence=Avg. Cadence (rpm) +maxCadence=Max. Cadence (rpm) +strokes=Strokes +averageTemperature=Avg. Temp (°C) +minTemperature=Min. Temp (°C) +maxTemperature=Max. Temp (°C) diff --git a/garmin-connect-export/csv_header_kjkjava.properties b/garmin-connect-export/csv_header_kjkjava.properties new file mode 100644 index 0000000..bce94d6 --- /dev/null +++ b/garmin-connect-export/csv_header_kjkjava.properties @@ -0,0 +1,42 @@ +# column template mostly for running, matching the (old) output from https://github.com/kjkjava/garmin-connect-export +id=Activity ID +activityName=Activity Name +description=Description +startTime1123=Begin Timestamp +startTimeMillis=Begin Timestamp (ms) +endTime1123=End Timestamp +endTimeMillis=End Timestamp (ms) +device=Device +activityParent=Activity Parent +activityType=Activity Type +eventType=Event Type +tzOffset=Time Zone +maxElevationUnit=Max. Elevation (m) +maxElevation=Max. Elevation (Raw) +startLatitude=Begin latitude (°DD) +startLongitude=Begin longitude (°DD) +endLatitude=End latitude (°DD) +endLongitude=End longitude (°DD) +averageMovingSpeedPace=Average moving speed (km/h or min/km) +averageMovingSpeedPaceRaw=Average moving speed (Raw) +maxHR=Max. Heart Rate (bpm) +averageHR=Average Heart Rate (bpm) +maxSpeedPace=Max. speed (km/h or min/km) +maxSpeedPaceRaw=Max. speed (Raw) +calories=Calories +caloriesRaw=Calories (Raw) +elapsedDuration=Duration (h:m:s) +elapsedDurationRaw=Duration (Raw Seconds) +movingDuration=Moving Duration (h:m:s) +movingDurationRaw=Moving Duration (Raw Seconds) +averageSpeedPace=Average Speed (km/h or min/km) +averageSpeedPaceRaw=Average Speed (Raw) +distance=Distance (km) +distanceRaw=Distance (Raw) +minHR=Min. Heart Rate (bpm) +minElevationUnit=Min. Elevation (m) +minElevation=Min. Elevation (Raw) +elevationGainUnit=Elevation Gain (m) +elevationGain=Elevation Gain (Raw) +elevationLossUnit=Elevation Loss (m) +elevationLoss=Elevation Loss (Raw) diff --git a/garmin-connect-export/csv_header_moderation.properties b/garmin-connect-export/csv_header_moderation.properties new file mode 100644 index 0000000..d37e21b --- /dev/null +++ b/garmin-connect-export/csv_header_moderation.properties @@ -0,0 +1,43 @@ +# column template mostly for cycling, matching the output from https://github.com/moderation/garmin-connect-export +activityName=Activity name +description=Description +startTimeRaw=Begin timestamp +elapsedDuration=Duration (h:m:s) +movingDuration=Moving duration (h:m:s) +distanceRaw=Distance (km) +averageSpeedRaw=Average speed (km/h) +averageMovingSpeedRaw=Average moving speed (km/h) +maxSpeedRaw=Max. speed (km/h) +elevationLoss=Elevation loss uncorrected (m) +elevationGain=Elevation gain uncorrected (m) +minElevation=Elevation min. uncorrected (m) +maxElevation=Elevation max. uncorrected (m) +minHRRaw=Min. heart rate (bpm) +maxHRRaw=Max. heart rate (bpm) +averageHRRaw=Average heart rate (bpm) +caloriesRaw=Calories +averageCadence=Avg. cadence (rpm) +maxCadence=Max. cadence (rpm) +strokes=Strokes +averageTemperature=Avg. temp (°C) +minTemperature=Min. temp (°C) +maxTemperature=Max. temp (°C) +url=Map +# the following 3 columns are currently always empty +endTimeRaw=End timestamp +emptyStartTime=Begin timestamp (ms) +emptyEndTime=End timestamp (ms) +device=Device +activityTypeKey=Activity type +eventTypeKey=Event type +tz=Time zone +startLatitudeRaw=Begin latitude (°DD) +startLongitudeRaw=Begin longitude (°DD) +endLatitudeRaw=End latitude (°DD) +endLongitudeRaw=End longitude (°DD) +# the following 4 columns are currently always empty +emptyElevationGainCorr=Elevation gain corrected (m) +emptyElevationLossCorr=Elevation loss corrected (m) +emptyMaxElevationCorr=Elevation max. corrected (m) +emptyMinElevationCorr=Elevation min. corrected (m) +sampleCount=Sample count diff --git a/garmin-connect-export/filtering.py b/garmin-connect-export/filtering.py new file mode 100644 index 0000000..6cb9503 --- /dev/null +++ b/garmin-connect-export/filtering.py @@ -0,0 +1,77 @@ +""" +Helper functions for filtering the list of activities to download. +""" + +import json +import logging +import os + +DOWNLOADED_IDS_FILE_NAME = "downloaded_ids.json" +KEY_IDS = "ids" + + +def read_exclude(file): + """ + Returns list with ids from json file. Errors will be printed + :param file: String with file path to exclude + :return: List with IDs or, if file not found, None. + """ + + if not os.path.exists(file): + print("File not found:", file) + return None + + if not os.path.isfile(file): + print("Not a file:", file) + return None + + with open(file, 'r', encoding='utf-8') as json_file: + + try: + obj = json.load(json_file) + return obj['ids'] + + except json.JSONDecodeError: + print("No valid json in:", file) + return None + + +def update_download_stats(activity_id, directory): + """ + Add item to download_stats file, if not already there. Call this for every successful downloaded activity. + The statistic is independent of the downloaded file type. + :param activity_id: String with activity ID + :param directory: Download root directory + """ + file = os.path.join(directory, DOWNLOADED_IDS_FILE_NAME) + + # Very first time: touch the file + if not os.path.exists(file): + with open(file, 'w', encoding='utf-8') as read_obj: + read_obj.write(json.dumps("")) + + # read file + with open(file, 'r', encoding='utf-8') as read_obj: + data = read_obj.read() + + try: + obj = json.loads(data) + + except json.JSONDecodeError: + obj = "" + + # Sanitize wrong formats + obj = dict(obj) + + if KEY_IDS not in obj: + obj[KEY_IDS] = [] + + if activity_id in obj[KEY_IDS]: + logging.info("%s already in %s", activity_id, file) + return + + obj[KEY_IDS].append(activity_id) + obj[KEY_IDS].sort() + + with open(file, 'w', encoding='utf-8') as write_obj: + write_obj.write(json.dumps(obj)) diff --git a/garmin-connect-export/filtering_test.py b/garmin-connect-export/filtering_test.py new file mode 100644 index 0000000..1a41c6a --- /dev/null +++ b/garmin-connect-export/filtering_test.py @@ -0,0 +1,120 @@ +# Standard library imports +import json +import tempfile +import unittest +from os import path +from os import remove + +# Local application/library specific imports +import filtering +from filtering import DOWNLOADED_IDS_FILE_NAME +from filtering import KEY_IDS + +DIR = tempfile.gettempdir() + + +class TestExcludeFile(unittest.TestCase): + @property + def file_under_test(self): + return path.join(DIR, "exclude.json") + + @property + def non_existing_file(self): + return path.join(DIR, "non_existing_exclude.json") + + @property + def dir(self): + return DIR + + def setUp(self): + if path.exists(self.file_under_test): + remove(self.file_under_test) + + if path.exists(self.non_existing_file): + remove(self.non_existing_file) + + def test_read_exclude(self): + self.assertIsNone(filtering.read_exclude(self.non_existing_file)) + + with open(self.file_under_test, 'w') as f: + json.dump(dict(ids=["1001", "1002", "1010", "1003"]), f) + + ids = filtering.read_exclude(self.file_under_test) + + self.assertEqual(len(ids), 4) + + self.assertIn("1001", ids) + self.assertIn("1002", ids) + self.assertIn("1003", ids) + self.assertIn("1010", ids) + + +class TestDownloadStats(unittest.TestCase): + @property + def file_under_test(self): + return path.join(DIR, DOWNLOADED_IDS_FILE_NAME) + + @property + def dir(self): + return DIR + + def setUp(self): + if path.exists(self.file_under_test): + remove(self.file_under_test) + + def test_new_file(self): + filtering.update_download_stats('1000', self.dir) + + with open(self.file_under_test, 'r') as actual_file: + actual = json.load(actual_file) + + self.assertEqual(actual[KEY_IDS][0], '1000') + + def test_1000_items(self): + + r = range(1, 1000) + + for i in r: + filtering.update_download_stats(str(i), self.dir) + + with open(self.file_under_test, 'r') as actual_file: + actual = json.load(actual_file) + + self.assertEqual(len(actual[KEY_IDS]), len(r)) + + def test_new_item(self): + filtering.update_download_stats('1000', self.dir) + filtering.update_download_stats('1010', self.dir) + + with open(self.file_under_test, 'r') as actual_file: + actual = json.load(actual_file) + + self.assertEqual(actual[KEY_IDS][0], '1000') + self.assertEqual(actual[KEY_IDS][1], '1010') + + def test_corrupted_file(self): + with open(self.file_under_test, 'w') as corrupted_file: + corrupted_file.write("HUGO") + + filtering.update_download_stats('1000', self.dir) + + with open(self.file_under_test, 'r') as actual_file: + actual = json.load(actual_file) + + self.assertEqual(actual[KEY_IDS][0], '1000') + + def test_sort(self): + filtering.update_download_stats('1010', self.dir) + filtering.update_download_stats('1000', self.dir) + filtering.update_download_stats('1005', self.dir) + + with open(self.file_under_test, 'r') as actual_file: + actual = json.load(actual_file) + + self.assertEqual(actual[KEY_IDS][0], '1000') + self.assertEqual(actual[KEY_IDS][1], '1005') + self.assertEqual(actual[KEY_IDS][2], '1010') + + +if __name__ == '__main__': + unittest.main() diff --git a/garmin-connect-export/gcexport.py b/garmin-connect-export/gcexport.py new file mode 100644 index 0000000..eb5ad17 --- /dev/null +++ b/garmin-connect-export/gcexport.py @@ -0,0 +1,1345 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +File: gcexport.py +Original author: Kyle Krafka (https://github.com/kjkjava/) +Date: April 28, 2015 +Fork author: Michael P (https://github.com/moderation/) +Date: February 21, 2016 +Fork author: Peter Steiner (https://github.com/pe-st/) +Date: June 2017 +Date: March 2020 - Python3 support by Thomas Th. (https://github.com/telemaxx/) + +Description: Use this script to export your fitness data from Garmin Connect. + See README.md for more information, CHANGELOG.md for a history of the changes + +Activity & event types: + https://connect.garmin.com/modern/main/js/properties/event_types/event_types.properties + https://connect.garmin.com/modern/main/js/properties/activity_types/activity_types.properties +""" + +# Standard library imports +import argparse +import csv +import http.cookiejar +import io +import json +import logging +import os +import os.path +import re +import string +import sys +import unicodedata +import urllib.request +import zipfile +from datetime import datetime, timedelta, tzinfo +from getpass import getpass +from math import floor +from platform import python_version +from subprocess import call +from timeit import default_timer as timer +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request + +# Local application/library specific imports +from filtering import read_exclude, update_download_stats + +COOKIE_JAR = http.cookiejar.CookieJar() +OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(COOKIE_JAR), urllib.request.HTTPSHandler(debuglevel=0)) + +SCRIPT_VERSION = '4.0.0' + +# This version here should correspond to what is written in CONTRIBUTING.md#python-3x-versions +MINIMUM_PYTHON_VERSION = (3, 7) + +# this is almost the datetime format Garmin used in the activity-search-service +# JSON 'display' fields (Garmin didn't zero-pad the date and the hour, but %d and %H do) +ALMOST_RFC_1123 = "%a, %d %b %Y %H:%M" + +# used by sanitize_filename() +VALID_FILENAME_CHARS = f'-_.() {string.ascii_letters}{string.digits}' + +# map the numeric parentTypeId to its name for the CSV output +PARENT_TYPE_ID = { + 1: 'running', + 2: 'cycling', + 3: 'hiking', + 4: 'other', + 9: 'walking', + 17: 'any', + 26: 'swimming', + 29: 'fitness_equipment', + 71: 'motorcycling', + 83: 'transition', + 144: 'diving', + 149: 'yoga', + 165: 'winter_sports', +} + +# typeId values using pace instead of speed +USES_PACE = {1, 3, 9} # running, hiking, walking + +HR_ZONES_EMPTY = [None, None, None, None, None] + +# Maximum number of activities you can request at once. +# Used to be 100 and enforced by Garmin for older endpoints; for the current endpoint 'URL_GC_LIST' +# the limit is not known (I have less than 1000 activities and could get them all in one go) +LIMIT_MAXIMUM = 1000 + +MAX_TRIES = 3 + +CSV_TEMPLATE = os.path.join(os.path.dirname(os.path.realpath(__file__)), "csv_header_default.properties") + +WEBHOST = "https://connect.garmin.com" +REDIRECT = "https://connect.garmin.com/modern/" +BASE_URL = "https://connect.garmin.com/en-US/signin" +SSO = "https://sso.garmin.com/sso" +CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css" + +DATA = { + 'service': REDIRECT, + 'webhost': WEBHOST, + 'source': BASE_URL, + 'redirectAfterAccountLoginUrl': REDIRECT, + 'redirectAfterAccountCreationUrl': REDIRECT, + 'gauthHost': SSO, + 'locale': 'en_US', + 'id': 'gauth-widget', + 'cssUrl': CSS, + 'clientId': 'GarminConnect', + 'rememberMeShown': 'true', + 'rememberMeChecked': 'false', + 'createAccountShown': 'true', + 'openCreateAccount': 'false', + 'displayNameShown': 'false', + 'consumeServiceTicket': 'false', + 'initialFocus': 'true', + 'embedWidget': 'false', + 'generateExtraServiceTicket': 'true', + 'generateTwoExtraServiceTickets': 'false', + 'generateNoServiceTicket': 'false', + 'globalOptInShown': 'true', + 'globalOptInChecked': 'false', + 'mobile': 'false', + 'connectLegalTerms': 'true', + 'locationPromptShown': 'true', + 'showPassword': 'true', +} + +# URLs for various services. + +URL_GC_LOGIN = 'https://sso.garmin.com/sso/signin?' + urlencode(DATA) +URL_GC_POST_AUTH = 'https://connect.garmin.com/modern/activities?' +URL_GC_PROFILE = 'https://connect.garmin.com/modern/profile' +URL_GC_USERSTATS = 'https://connect.garmin.com/modern/proxy/userstats-service/statistics/' +URL_GC_LIST = 'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?' +URL_GC_ACTIVITY = 'https://connect.garmin.com/modern/proxy/activity-service/activity/' +URL_GC_DEVICE = 'https://connect.garmin.com/modern/proxy/device-service/deviceservice/app-info/' +URL_GC_GEAR = 'https://connect.garmin.com/modern/proxy/gear-service/gear/filterGear?activityId=' +URL_GC_ACT_PROPS = 'https://connect.garmin.com/modern/main/js/properties/activity_types/activity_types.properties' +URL_GC_EVT_PROPS = 'https://connect.garmin.com/modern/main/js/properties/event_types/event_types.properties' +URL_GC_GPX_ACTIVITY = 'https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/' +URL_GC_TCX_ACTIVITY = 'https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/' +URL_GC_ORIGINAL_ACTIVITY = 'http://connect.garmin.com/proxy/download-service/files/activity/' + + +def resolve_path(directory, subdir, time): + """ + Replace time variables and returns changed path. Supported place holders are {YYYY} and {MM} + :param directory: export root directory + :param subdir: subdirectory, can have place holders. + :param time: date-time-string + :return: Updated dictionary string + """ + ret = os.path.join(directory, subdir) + if re.compile(".*{YYYY}.*").match(ret): + ret = ret.replace("{YYYY}", time[0:4]) + if re.compile(".*{MM}.*").match(ret): + ret = ret.replace("{MM}", time[5:7]) + + return ret + + +def hhmmss_from_seconds(sec): + """Helper function that converts seconds to HH:MM:SS time format.""" + if isinstance(sec, (float, int)): + formatted_time = str(timedelta(seconds=int(sec))).zfill(8) + else: + formatted_time = "0.000" + return formatted_time + + +def kmh_from_mps(mps): + """Helper function that converts meters per second (mps) to km/h.""" + return str(mps * 3.6) + + +def sanitize_filename(name, max_length=0): + """ + Remove or replace characters that are unsafe for filename + """ + # inspired by https://stackoverflow.com/a/698714/3686 + cleaned_filename = unicodedata.normalize('NFKD', name) if name else '' + stripped_filename = ''.join(c for c in cleaned_filename if c in VALID_FILENAME_CHARS).replace(' ', '_') + return stripped_filename[:max_length] if max_length > 0 else stripped_filename + + +def write_to_file(filename, content, mode='w', file_time=None): + """ + Helper function that persists content to a file. + + :param filename: name of the file to write + :param content: content to write; can be 'bytes' or 'str'. + If it's 'bytes' and the mode 'w', it will be converted/decoded + :param mode: 'w' or 'wb' + :param file_time: if given use as timestamp for the file written (in seconds since 1970-01-01) + """ + if mode == 'w': + if isinstance(content, bytes): + content = content.decode('utf-8') + with io.open(filename, mode, encoding='utf-8') as text_file: + text_file.write(content) + elif mode == 'wb': + with io.open(filename, 'wb') as binary_file: + binary_file.write(content) + else: + raise Exception('Unsupported file mode: ', mode) + if file_time: + os.utime(filename, (file_time, file_time)) + + +def http_req(url, post=None, headers=None): + """ + Helper function that makes the HTTP requests. + + :param url: URL for the request + :param post: dictionary of POST parameters + :param headers: dictionary of headers + :return: response body (type 'bytes') + """ + request = Request(url) + # Tell Garmin we're some supported browser. + request.add_header( + 'User-Agent', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2816.0 Safari/537.36', + ) + request.add_header('nk', 'NT') # necessary since 2021-02-23 to avoid http error code 402 + if headers: + for header_key, header_value in headers.items(): + request.add_header(header_key, header_value) + if post: + post = urlencode(post) # Convert dictionary to POST parameter string. + post = post.encode("utf-8") + start_time = timer() + try: + response = OPENER.open(request, data=post) + except HTTPError as ex: + if hasattr(ex, 'code'): + logging.error('Server couldn\'t fulfill the request, url %s, code %s, error: %s', url, ex.code, ex) + logging.info('Headers returned:\n%s', ex.info()) + raise + except URLError as ex: + if hasattr(ex, 'reason'): + logging.error('Failed to reach url %s, error: %s', url, ex) + raise + logging.debug('Got %s in %s s from %s', response.getcode(), timer() - start_time, url) + logging.debug('Headers returned:\n%s', response.info()) + + # N.B. urllib2 will follow any 302 redirects. + # print(response.getcode()) + if response.getcode() == 204: + # 204 = no content, e.g. for activities without GPS coordinates there is no GPX download. + # Write an empty file to prevent redownloading it. + logging.info('Got 204 for %s, returning empty response', url) + return b'' + if response.getcode() != 200: + raise Exception(f'Bad return code ({response.getcode()}) for: {url}') + + return response.read() + + +def http_req_as_string(url, post=None, headers=None): + """Helper function that makes the HTTP requests, returning a string instead of bytes.""" + return http_req(url, post, headers).decode() + + +# idea stolen from https://stackoverflow.com/a/31852401/3686 +def load_properties(multiline, separator='=', comment_char='#', keys=None): + """ + Read a multiline string of properties (key/value pair separated by *separator*) into a dict + + :param multiline: input string of properties + :param separator: separator between key and value + :param comment_char: lines starting with this char are considered comments, not key/value pairs + :param keys: list to append the keys to + :return: + """ + props = {} + for line in multiline.splitlines(): + stripped_line = line.strip() + if stripped_line and not stripped_line.startswith(comment_char): + key_value = stripped_line.split(separator) + key = key_value[0].strip() + value = separator.join(key_value[1:]).strip().strip('"') + props[key] = value + if keys is not None: + keys.append(key) + return props + + +def value_if_found_else_key(some_dict, key): + """Lookup a value in some_dict and use the key itself as fallback""" + return some_dict.get(key, key) + + +def present(element, act): + """Return True if act[element] is valid and not None""" + if not act: + return False + if element not in act: + return False + return act[element] + + +def absent_or_null(element, act): + """Return False only if act[element] is valid and not None""" + if not act: + return True + if element not in act: + return True + if act[element]: + return False + return True + + +def from_activities_or_detail(element, act, detail, detail_container): + """Return detail[detail_container][element] if valid and act[element] (or None) otherwise""" + if absent_or_null(detail_container, detail) or absent_or_null(element, detail[detail_container]): + return None if absent_or_null(element, act) else act[element] + return detail[detail_container][element] + + +def trunc6(some_float): + """Return the given float as string formatted with six digit precision""" + return f'{floor(some_float * 1000000) / 1000000:12.6f}'.lstrip() + + +# A class building tzinfo objects for fixed-offset time zones. +# (copied from https://docs.python.org/2/library/datetime.html) +class FixedOffset(tzinfo): + """Fixed offset in minutes east from UTC.""" + + def __init__(self, offset, name): + super().__init__() + self.__offset = timedelta(minutes=offset) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return timedelta(0) + + +def offset_date_time(time_local, time_gmt): + """ + Build an 'aware' datetime from two 'naive' datetime objects (that is timestamps + as present in the activitylist-service.json), using the time difference as offset. + """ + local_dt = datetime_from_iso(time_local) + gmt_dt = datetime_from_iso(time_gmt) + offset = local_dt - gmt_dt + offset_tz = FixedOffset(offset.seconds // 60, "LCL") + return local_dt.replace(tzinfo=offset_tz) + + +def datetime_from_iso(iso_date_time): + """ + Call 'datetime.strptime' supporting different ISO time formats + (with or without 'T' between date and time, with or without microseconds, + but without offset) + :param iso_date_time: timestamp string in ISO format + :return: a 'naive` datetime + """ + pattern = re.compile(r"(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(\.\d+)?") + match = pattern.match(iso_date_time) + if not match: + raise Exception(f'Invalid ISO timestamp {iso_date_time}.') + micros = match.group(3) if match.group(3) else ".0" + iso_with_micros = f'{match.group(1)} {match.group(2)}{micros}' + return datetime.strptime(iso_with_micros, "%Y-%m-%d %H:%M:%S.%f") + + +def epoch_seconds_from_summary(summary): + """ + Determine the start time in epoch seconds (seconds since 1970-01-01) + + :param summary: summary dict + :return: epoch seconds as integer + """ + if present('beginTimestamp', summary): + return summary['beginTimestamp'] // 1000 + if present('startTimeLocal', summary) and present('startTimeGMT', summary): + date_time = offset_date_time(summary['startTimeLocal'], summary['startTimeGMT']) + return int(date_time.timestamp()) + logging.info('No timestamp found in activity %s', summary['activityId']) + return None + + +def pace_or_speed_raw(type_id, parent_type_id, mps): + """Convert speed (m/s) to speed (km/h) or pace (min/km) depending on type and parent type""" + kmh = 3.6 * mps + if (type_id in USES_PACE) or (parent_type_id in USES_PACE): + return 60 / kmh + return kmh + + +def pace_or_speed_formatted(type_id, parent_type_id, mps): + """ + Convert speed (m/s) to string: speed (km/h as x.x) or + pace (min/km as MM:SS), depending on type and parent type + """ + kmh = 3.6 * mps + if (type_id in USES_PACE) or (parent_type_id in USES_PACE): + # format seconds per kilometer as MM:SS, see https://stackoverflow.com/a/27751293 + div_mod = divmod(int(round(3600 / kmh)), 60) + return f'{div_mod[0]:02d}:{div_mod[1]:02d}' + return f'{round(kmh, 1):.1f}' + + +class CsvFilter: + """Collects, filters and writes CSV.""" + + def __init__(self, csv_file, csv_header_properties): + self.__csv_file = csv_file + with open(csv_header_properties, 'r', encoding='utf-8') as prop: + csv_header_props = prop.read() + self.__csv_columns = [] + self.__csv_headers = load_properties(csv_header_props, keys=self.__csv_columns) + self.__csv_field_names = [] + for column in self.__csv_columns: + self.__csv_field_names.append(self.__csv_headers[column]) + self.__writer = csv.DictWriter(self.__csv_file, fieldnames=self.__csv_field_names, quoting=csv.QUOTE_ALL) + self.__current_row = {} + + def write_header(self): + """Write the active column names as CSV header""" + self.__writer.writeheader() + + def write_row(self): + """Write the prepared CSV record""" + self.__writer.writerow(self.__current_row) + self.__current_row = {} + + def set_column(self, name, value): + """ + Store a column value (if the column is active) into + the record prepared for the next write_row call + """ + if value and name in self.__csv_columns: + self.__current_row[self.__csv_headers[name]] = value + + def is_column_active(self, name): + """Return True if the column is present in the header template""" + return name in self.__csv_columns + + +def parse_arguments(argv): + """ + Setup the argument parser and parse the command line arguments. + """ + current_date = datetime.now().strftime('%Y-%m-%d') + activities_directory = f'./{current_date}_garmin_connect_export' + + parser = argparse.ArgumentParser(description='Garmin Connect Exporter') + + # fmt: off + parser.add_argument('--version', action='version', version='%(prog)s ' + SCRIPT_VERSION, + help='print version and exit') + parser.add_argument('-v', '--verbosity', action='count', default=0, + help='increase output and log verbosity, save more intermediate files') + parser.add_argument('--username', + help='your Garmin Connect username or email address (otherwise, you will be prompted)') + parser.add_argument('--password', + help='your Garmin Connect password (otherwise, you will be prompted)') + parser.add_argument('-c', '--count', default='1', + help='number of recent activities to download, or \'all\' (default: 1)') + parser.add_argument('-e', '--external', + help='path to external program to pass CSV file too') + parser.add_argument('-a', '--args', + help='additional arguments to pass to external program') + parser.add_argument('-f', '--format', choices=['gpx', 'tcx', 'original', 'json'], default='gpx', + help="export format; can be 'gpx', 'tcx', 'original' or 'json' (default: 'gpx')") + parser.add_argument('-d', '--directory', default=activities_directory, + help='the directory to export to (default: \'./YYYY-MM-DD_garmin_connect_export\')') + parser.add_argument('-s', '--subdir', + help='the subdirectory for activity files (tcx, gpx etc.), supported placeholders are {YYYY} and {MM} (default: export directory)') + parser.add_argument('-lp', '--logpath', + help='the directory to store logfiles (default: same as for --directory)') + parser.add_argument('-u', '--unzip', action='store_true', + help='if downloading ZIP files (format: \'original\'), unzip the file and remove the ZIP file') + parser.add_argument('-ot', '--originaltime', action='store_true', + help='will set downloaded (and possibly unzipped) file time to the activity start time') + parser.add_argument('--desc', type=int, nargs='?', const=0, default=None, + help='append the activity\'s description to the file name of the download; limit size if number is given') + parser.add_argument('-t', '--template', default=CSV_TEMPLATE, + help='template file with desired columns for CSV output') + parser.add_argument('-fp', '--fileprefix', action='count', default=0, + help='set the local time as activity file name prefix') + parser.add_argument('-sa', '--start_activity_no', type=int, default=1, + help='give index for first activity to import, i.e. skipping the newest activities') + parser.add_argument('-ex', '--exclude', metavar='FILE', + help='JSON file with Array of activity IDs to exclude from download. Format example: {"ids": ["6176888711"]}') + # fmt: on + + return parser.parse_args(argv[1:]) + + +def login_to_garmin_connect(args): + """ + Perform all HTTP requests to login to Garmin Connect. + """ + username = args.username if args.username else input('Username: ') + password = args.password if args.password else getpass() + + logging.debug("Login params: %s", urlencode(DATA)) + + # Initially, we need to get a valid session cookie, so we pull the login page. + print('Connecting to Garmin Connect...', end='') + logging.info('Connecting to %s', URL_GC_LOGIN) + connect_response = http_req_as_string(URL_GC_LOGIN) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, 'connect_response.html'), connect_response, 'w') + for cookie in COOKIE_JAR: + logging.debug("Cookie %s : %s", cookie.name, cookie.value) + print(' Done.') + + # Now we'll actually login. + # Fields that are passed in a typical Garmin login. + post_data = { + 'username': username, + 'password': password, + 'embed': 'false', + 'rememberme': 'on', + } + + headers = {'referer': URL_GC_LOGIN} + + print('Requesting Login ticket...', end='') + logging.info('Requesting Login ticket') + login_response = http_req_as_string(f'{URL_GC_LOGIN}#', post_data, headers) + + for cookie in COOKIE_JAR: + logging.debug("Cookie %s : %s", cookie.name, cookie.value) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, 'login_response.html'), login_response, 'w') + + # extract the ticket from the login response + pattern = re.compile(r".*\?ticket=([-\w]+)\";.*", re.MULTILINE | re.DOTALL) + match = pattern.match(login_response) + if not match: + raise Exception( + 'Couldn\'t find ticket in the login response. Cannot log in. Did you enter the correct username and password?' + ) + login_ticket = match.group(1) + print(' Done. Ticket=', login_ticket, sep='') + + print("Authenticating...", end='') + logging.info('Authentication URL %s', f'{URL_GC_POST_AUTH}ticket={login_ticket}') + http_req(f'{URL_GC_POST_AUTH}ticket={login_ticket}') + print(' Done.') + + +def csv_write_record(csv_filter, extract, actvty, details, activity_type_name, event_type_name): + """ + Write out the given data as a CSV record + """ + + type_id = 4 if absent_or_null('activityType', actvty) else actvty['activityType']['typeId'] + parent_type_id = 4 if absent_or_null('activityType', actvty) else actvty['activityType']['parentTypeId'] + if present(parent_type_id, PARENT_TYPE_ID): + parent_type_key = PARENT_TYPE_ID[parent_type_id] + else: + parent_type_key = None + logging.warning("Unknown parentType %s, please tell script author", str(parent_type_id)) + + # get some values from detail if present, from a otherwise + start_latitude = from_activities_or_detail('startLatitude', actvty, details, 'summaryDTO') + start_longitude = from_activities_or_detail('startLongitude', actvty, details, 'summaryDTO') + end_latitude = from_activities_or_detail('endLatitude', actvty, details, 'summaryDTO') + end_longitude = from_activities_or_detail('endLongitude', actvty, details, 'summaryDTO') + + # fmt: off + csv_filter.set_column('id', str(actvty['activityId'])) + csv_filter.set_column('url', 'https://connect.garmin.com/modern/activity/' + str(actvty['activityId'])) + csv_filter.set_column('activityName', actvty['activityName'] if present('activityName', actvty) else None) + csv_filter.set_column('description', actvty['description'] if present('description', actvty) else None) + csv_filter.set_column('startTimeIso', extract['start_time_with_offset'].isoformat()) + csv_filter.set_column('startTime1123', extract['start_time_with_offset'].strftime(ALMOST_RFC_1123)) + csv_filter.set_column('startTimeMillis', str(actvty['beginTimestamp']) if present('beginTimestamp', actvty) else None) + csv_filter.set_column('startTimeRaw', details['summaryDTO']['startTimeLocal'] if present('startTimeLocal', details['summaryDTO']) else None) + csv_filter.set_column('endTimeIso', extract['end_time_with_offset'].isoformat() if extract['end_time_with_offset'] else None) + csv_filter.set_column('endTime1123', extract['end_time_with_offset'].strftime(ALMOST_RFC_1123) if extract['end_time_with_offset'] else None) + csv_filter.set_column('endTimeMillis', str(actvty['beginTimestamp'] + extract['elapsed_seconds'] * 1000) if present('beginTimestamp', actvty) else None) + csv_filter.set_column('durationRaw', str(round(actvty['duration'], 3)) if present('duration', actvty) else None) + csv_filter.set_column('duration', hhmmss_from_seconds(round(actvty['duration'])) if present('duration', actvty) else None) + csv_filter.set_column('elapsedDurationRaw', str(round(extract['elapsed_duration'], 3)) if extract['elapsed_duration'] else None) + csv_filter.set_column('elapsedDuration', hhmmss_from_seconds(round(extract['elapsed_duration'])) if extract['elapsed_duration'] else None) + csv_filter.set_column('movingDurationRaw', str(round(details['summaryDTO']['movingDuration'], 3)) if present('movingDuration', details['summaryDTO']) else None) + csv_filter.set_column('movingDuration', hhmmss_from_seconds(round(details['summaryDTO']['movingDuration'])) if present('movingDuration', details['summaryDTO']) else None) + csv_filter.set_column('distanceRaw', f"{actvty['distance'] / 1000:.5f}" if present('distance', actvty) else None) + csv_filter.set_column('averageSpeedRaw', kmh_from_mps(details['summaryDTO']['averageSpeed']) if present('averageSpeed', details['summaryDTO']) else None) + csv_filter.set_column('averageSpeedPaceRaw', trunc6(pace_or_speed_raw(type_id, parent_type_id, actvty['averageSpeed'])) if present('averageSpeed', actvty) else None) + csv_filter.set_column('averageSpeedPace', pace_or_speed_formatted(type_id, parent_type_id, actvty['averageSpeed']) if present('averageSpeed', actvty) else None) + csv_filter.set_column('averageMovingSpeedRaw', kmh_from_mps(details['summaryDTO']['averageMovingSpeed']) if present('averageMovingSpeed', details['summaryDTO']) else None) + csv_filter.set_column('averageMovingSpeedPaceRaw', trunc6(pace_or_speed_raw(type_id, parent_type_id, details['summaryDTO']['averageMovingSpeed'])) if present('averageMovingSpeed', details['summaryDTO']) else None) + csv_filter.set_column('averageMovingSpeedPace', pace_or_speed_formatted(type_id, parent_type_id, details['summaryDTO']['averageMovingSpeed']) if present('averageMovingSpeed', details['summaryDTO']) else None) + csv_filter.set_column('maxSpeedRaw', kmh_from_mps(details['summaryDTO']['maxSpeed']) if present('maxSpeed', details['summaryDTO']) else None) + csv_filter.set_column('maxSpeedPaceRaw', trunc6(pace_or_speed_raw(type_id, parent_type_id, details['summaryDTO']['maxSpeed'])) if present('maxSpeed', details['summaryDTO']) else None) + csv_filter.set_column('maxSpeedPace', pace_or_speed_formatted(type_id, parent_type_id, details['summaryDTO']['maxSpeed']) if present('maxSpeed', details['summaryDTO']) else None) + csv_filter.set_column('elevationLoss', str(round(details['summaryDTO']['elevationLoss'], 2)) if present('elevationLoss', details['summaryDTO']) else None) + csv_filter.set_column('elevationLossUncorr', str(round(details['summaryDTO']['elevationLoss'], 2)) if not actvty['elevationCorrected'] and present('elevationLoss', details['summaryDTO']) else None) + csv_filter.set_column('elevationLossCorr', str(round(details['summaryDTO']['elevationLoss'], 2)) if actvty['elevationCorrected'] and present('elevationLoss', details['summaryDTO']) else None) + csv_filter.set_column('elevationGain', str(round(details['summaryDTO']['elevationGain'], 2)) if present('elevationGain', details['summaryDTO']) else None) + csv_filter.set_column('elevationGainUncorr', str(round(details['summaryDTO']['elevationGain'], 2)) if not actvty['elevationCorrected'] and present('elevationGain', details['summaryDTO']) else None) + csv_filter.set_column('elevationGainCorr', str(round(details['summaryDTO']['elevationGain'], 2)) if actvty['elevationCorrected'] and present('elevationGain', details['summaryDTO']) else None) + csv_filter.set_column('minElevation', str(round(details['summaryDTO']['minElevation'], 2)) if present('minElevation', details['summaryDTO']) else None) + csv_filter.set_column('minElevationUncorr', str(round(details['summaryDTO']['minElevation'], 2)) if not actvty['elevationCorrected'] and present('minElevation', details['summaryDTO']) else None) + csv_filter.set_column('minElevationCorr', str(round(details['summaryDTO']['minElevation'], 2)) if actvty['elevationCorrected'] and present('minElevation', details['summaryDTO']) else None) + csv_filter.set_column('maxElevation', str(round(details['summaryDTO']['maxElevation'], 2)) if present('maxElevation', details['summaryDTO']) else None) + csv_filter.set_column('maxElevationUncorr', str(round(details['summaryDTO']['maxElevation'], 2)) if not actvty['elevationCorrected'] and present('maxElevation', details['summaryDTO']) else None) + csv_filter.set_column('maxElevationCorr', str(round(details['summaryDTO']['maxElevation'], 2)) if actvty['elevationCorrected'] and present('maxElevation', details['summaryDTO']) else None) + csv_filter.set_column('elevationCorrected', 'true' if actvty['elevationCorrected'] else 'false') + # csv_record += empty_record # no minimum heart rate in JSON + csv_filter.set_column('maxHRRaw', str(details['summaryDTO']['maxHR']) if present('maxHR', details['summaryDTO']) else None) + csv_filter.set_column('maxHR', f"{actvty['maxHR']:.0f}" if present('maxHR', actvty) else None) + csv_filter.set_column('averageHRRaw', str(details['summaryDTO']['averageHR']) if present('averageHR', details['summaryDTO']) else None) + csv_filter.set_column('averageHR', f"{actvty['averageHR']:.0f}" if present('averageHR', actvty) else None) + csv_filter.set_column('caloriesRaw', str(details['summaryDTO']['calories']) if present('calories', details['summaryDTO']) else None) + csv_filter.set_column('calories', f"{details['summaryDTO']['calories']:.0f}" if present('calories', details['summaryDTO']) else None) + csv_filter.set_column('vo2max', str(actvty['vO2MaxValue']) if present('vO2MaxValue', actvty) else None) + csv_filter.set_column('aerobicEffect', str(round(details['summaryDTO']['trainingEffect'], 2)) if present('trainingEffect', details['summaryDTO']) else None) + csv_filter.set_column('anaerobicEffect', str(round(details['summaryDTO']['anaerobicTrainingEffect'], 2)) if present('anaerobicTrainingEffect', details['summaryDTO']) else None) + csv_filter.set_column('hrZone1Low', str(extract['hrZones'][0]['zoneLowBoundary']) if present('zoneLowBoundary', extract['hrZones'][0]) else None) + csv_filter.set_column('hrZone1Seconds', f"{extract['hrZones'][0]['secsInZone']:.0f}" if present('secsInZone', extract['hrZones'][0]) else None) + csv_filter.set_column('hrZone2Low', str(extract['hrZones'][1]['zoneLowBoundary']) if present('zoneLowBoundary', extract['hrZones'][1]) else None) + csv_filter.set_column('hrZone2Seconds', f"{extract['hrZones'][1]['secsInZone']:.0f}" if present('secsInZone', extract['hrZones'][1]) else None) + csv_filter.set_column('hrZone3Low', str(extract['hrZones'][2]['zoneLowBoundary']) if present('zoneLowBoundary', extract['hrZones'][2]) else None) + csv_filter.set_column('hrZone3Seconds', f"{extract['hrZones'][2]['secsInZone']:.0f}" if present('secsInZone', extract['hrZones'][2]) else None) + csv_filter.set_column('hrZone4Low', str(extract['hrZones'][3]['zoneLowBoundary']) if present('zoneLowBoundary', extract['hrZones'][3]) else None) + csv_filter.set_column('hrZone4Seconds', f"{extract['hrZones'][3]['secsInZone']:.0f}" if present('secsInZone', extract['hrZones'][3]) else None) + csv_filter.set_column('hrZone5Low', str(extract['hrZones'][4]['zoneLowBoundary']) if present('zoneLowBoundary', extract['hrZones'][4]) else None) + csv_filter.set_column('hrZone5Seconds', f"{extract['hrZones'][4]['secsInZone']:.0f}" if present('secsInZone', extract['hrZones'][4]) else None) + csv_filter.set_column('averageRunCadence', str(round(details['summaryDTO']['averageRunCadence'], 2)) if present('averageRunCadence', details['summaryDTO']) else None) + csv_filter.set_column('maxRunCadence', str(details['summaryDTO']['maxRunCadence']) if present('maxRunCadence', details['summaryDTO']) else None) + csv_filter.set_column('strideLength', str(round(details['summaryDTO']['strideLength'], 2)) if present('strideLength', details['summaryDTO']) else None) + csv_filter.set_column('steps', str(actvty['steps']) if present('steps', actvty) else None) + csv_filter.set_column('averageCadence', str(actvty['averageBikingCadenceInRevPerMinute']) if present('averageBikingCadenceInRevPerMinute', actvty) else None) + csv_filter.set_column('maxCadence', str(actvty['maxBikingCadenceInRevPerMinute']) if present('maxBikingCadenceInRevPerMinute', actvty) else None) + csv_filter.set_column('strokes', str(actvty['strokes']) if present('strokes', actvty) else None) + csv_filter.set_column('averageTemperature', str(details['summaryDTO']['averageTemperature']) if present('averageTemperature', details['summaryDTO']) else None) + csv_filter.set_column('minTemperature', str(details['summaryDTO']['minTemperature']) if present('minTemperature', details['summaryDTO']) else None) + csv_filter.set_column('maxTemperature', str(details['summaryDTO']['maxTemperature']) if present('maxTemperature', details['summaryDTO']) else None) + csv_filter.set_column('device', extract['device'] if extract['device'] else None) + csv_filter.set_column('gear', extract['gear'] if extract['gear'] else None) + csv_filter.set_column('activityTypeKey', actvty['activityType']['typeKey'].title() if present('typeKey', actvty['activityType']) else None) + csv_filter.set_column('activityType', value_if_found_else_key(activity_type_name, 'activity_type_' + actvty['activityType']['typeKey']) if present('activityType', actvty) else None) + csv_filter.set_column('activityParent', value_if_found_else_key(activity_type_name, 'activity_type_' + parent_type_key) if parent_type_key else None) + csv_filter.set_column('eventTypeKey', actvty['eventType']['typeKey'].title() if present('typeKey', actvty['eventType']) else None) + csv_filter.set_column('eventType', value_if_found_else_key(event_type_name, actvty['eventType']['typeKey']) if present('eventType', actvty) else None) + csv_filter.set_column('privacy', details['accessControlRuleDTO']['typeKey'] if present('typeKey', details['accessControlRuleDTO']) else None) + csv_filter.set_column('fileFormat', details['metadataDTO']['fileFormat']['formatKey'] if present('fileFormat', details['metadataDTO']) and present('formatKey', details['metadataDTO']['fileFormat']) else None) + csv_filter.set_column('tz', details['timeZoneUnitDTO']['timeZone'] if present('timeZone', details['timeZoneUnitDTO']) else None) + csv_filter.set_column('tzOffset', extract['start_time_with_offset'].isoformat()[-6:]) + csv_filter.set_column('locationName', details['locationName'] if present('locationName', details) else None) + csv_filter.set_column('startLatitudeRaw', str(start_latitude) if start_latitude else None) + csv_filter.set_column('startLatitude', trunc6(start_latitude) if start_latitude else None) + csv_filter.set_column('startLongitudeRaw', str(start_longitude) if start_longitude else None) + csv_filter.set_column('startLongitude', trunc6(start_longitude) if start_longitude else None) + csv_filter.set_column('endLatitudeRaw', str(end_latitude) if end_latitude else None) + csv_filter.set_column('endLatitude', trunc6(end_latitude) if end_latitude else None) + csv_filter.set_column('endLongitudeRaw', str(end_longitude) if end_longitude else None) + csv_filter.set_column('endLongitude', trunc6(end_longitude) if end_longitude else None) + csv_filter.set_column('sampleCount', str(extract['samples']['metricsCount']) if present('metricsCount', extract['samples']) else None) + # fmt: on + + csv_filter.write_row() + + +def extract_device(device_dict, details, start_time_seconds, args, http_caller, file_writer): + """ + Try to get the device details (and cache them, as they're used for multiple activities) + + :param device_dict: cache (dict) of already known devices + :param details: dict with the details of an activity, should contain a device ID + :param start_time_seconds: if given use as timestamp for the file written (in seconds since 1970-01-01) + :param args: command-line arguments (for the file_writer callback) + :param http_caller: callback to perform the HTTP call for downloading the device details + :param file_writer: callback that saves the device details in a file + :return: string with the device name + """ + if not present('metadataDTO', details): + logging.warning("no metadataDTO") + return None + + metadata = details['metadataDTO'] + device_app_inst_id = ( + metadata['deviceApplicationInstallationId'] if present('deviceApplicationInstallationId', metadata) else None + ) + if device_app_inst_id: + if device_app_inst_id not in device_dict: + # observed from my stock of activities: + # details['metadataDTO']['deviceMetaDataDTO']['deviceId'] == null -> device unknown + # details['metadataDTO']['deviceMetaDataDTO']['deviceId'] == '0' -> device unknown + # details['metadataDTO']['deviceMetaDataDTO']['deviceId'] == 'someid' -> device known + device_dict[device_app_inst_id] = None + device_meta = metadata['deviceMetaDataDTO'] if present('deviceMetaDataDTO', metadata) else {} + device_id = device_meta['deviceId'] if present('deviceId', device_meta) else None + if 'deviceId' not in device_meta or device_id and device_id != '0': + device_json = http_caller(URL_GC_DEVICE + str(device_app_inst_id)) + file_writer(os.path.join(args.directory, f'device_{device_app_inst_id}.json'), device_json, 'w', start_time_seconds) + if not device_json: + logging.warning("Device Details %s are empty", device_app_inst_id) + device_dict[device_app_inst_id] = "device-id:" + str(device_app_inst_id) + else: + device_details = json.loads(device_json) + if present('productDisplayName', device_details): + device_dict[device_app_inst_id] = ( + device_details['productDisplayName'] + ' ' + device_details['versionString'] + ) + else: + logging.warning("Device details %s incomplete", device_app_inst_id) + return device_dict[device_app_inst_id] + return None + + +def load_zones(activity_id, start_time_seconds, args, http_caller, file_writer): + """ + Try to get the heart rate zones + + :param activity_id: ID of the activity (as string) + :param start_time_seconds: if given use as timestamp for the file written (in seconds since 1970-01-01) + :param args: command-line arguments (for the file_writer callback) + :param http_caller: callback to perform the HTTP call for downloading the device details + :param file_writer: callback that saves the device details in a file + :return: array with the heart rate zones + """ + zones = HR_ZONES_EMPTY + zones_json = http_caller(f'{URL_GC_ACTIVITY}{activity_id}/hrTimeInZones') + file_writer(os.path.join(args.directory, f'activity_{activity_id}_zones.json'), zones_json, 'w', start_time_seconds) + zones_raw = json.loads(zones_json) + if not zones_raw: + logging.warning("HR Zones %s are empty", activity_id) + else: + for raw_zone in zones_raw: + if present('zoneNumber', raw_zone): + index = raw_zone['zoneNumber'] - 1 + zones[index] = {} + zones[index]['secsInZone'] = raw_zone['secsInZone'] + zones[index]['zoneLowBoundary'] = raw_zone['zoneLowBoundary'] + return zones + + +def load_gear(activity_id, args): + """Retrieve the gear/equipment for an activity""" + try: + gear_json = http_req_as_string(URL_GC_GEAR + activity_id) + gear = json.loads(gear_json) + if gear: + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, f'activity_{activity_id}-gear.json'), gear_json, 'w') + gear_display_name = gear[0]['displayName'] if present('displayName', gear[0]) else None + gear_model = gear[0]['customMakeModel'] if present('customMakeModel', gear[0]) else None + logging.debug("Gear for %s = %s/%s", activity_id, gear_display_name, gear_model) + return gear_display_name if gear_display_name else gear_model + return None + except HTTPError as ex: + logging.info("Unable to get gear for %d, error: %s", activity_id, ex) + # logging.exception(ex) + return None + + +def export_data_file(activity_id, activity_details, args, file_time, append_desc, date_time): + """ + Write the data of the activity to a file, depending on the chosen data format + + The default filename is 'activity_' + activity_id, but this can be modified + by the '--fileprefix' option and the 'append_desc' parameter; the directory + to write the file into can be modified by the '--subdir' option. + + :param activity_id: ID of the activity (as string) + :param activity_details: details of the activity (for format 'json') + :param args: command-line arguments + :param file_time: if given the desired time stamp for the activity file (in seconds since 1970-01-01) + :param append_desc: suffix to the default filename + :param date_time: datetime in ISO format used for '--fileprefix' and '--subdir' options + :return: True if the file was written, False if the file existed already + """ + # Time dependent subdirectory for activity files, e.g. '{YYYY}' + if args.subdir is not None: + directory = resolve_path(args.directory, args.subdir, date_time) + # export activities to root directory + else: + directory = args.directory + + if not os.path.isdir(directory): + os.makedirs(directory) + + # timestamp as prefix for filename + if args.fileprefix > 0: + prefix = f'{date_time.replace("-", "").replace(":", "").replace(" ", "-")}-' + else: + prefix = "" + + original_basename = None + if args.format == 'gpx': + data_filename = os.path.join(directory, f'{prefix}activity_{activity_id}{append_desc}.gpx') + download_url = f'{URL_GC_GPX_ACTIVITY}{activity_id}?full=true' + file_mode = 'w' + elif args.format == 'tcx': + data_filename = os.path.join(directory, f'{prefix}activity_{activity_id}{append_desc}.tcx') + download_url = f'{URL_GC_TCX_ACTIVITY}{activity_id}?full=true' + file_mode = 'w' + elif args.format == 'original': + data_filename = os.path.join(directory, f'{prefix}activity_{activity_id}{append_desc}.zip') + # not all 'original' files are in FIT format, some are GPX or TCX... + original_basename = os.path.join(directory, f'{prefix}activity_{activity_id}{append_desc}') + download_url = URL_GC_ORIGINAL_ACTIVITY + activity_id + file_mode = 'wb' + elif args.format == 'json': + data_filename = os.path.join(directory, f'{prefix}activity_{activity_id}{append_desc}_summary.json') + file_mode = 'w' + else: + raise Exception('Unrecognized format.') + + if os.path.isfile(data_filename): + logging.debug('Data file for %s already exists', activity_id) + print('\tData file already exists; skipping...') + # Inform the main program that the file already exists + return False + + # Regardless of unzip setting, don't redownload if the ZIP or FIT/GPX/TCX original file exists. + if args.format == 'original' and ( + os.path.isfile(original_basename + '.fit') + or os.path.isfile(original_basename + '.gpx') + or os.path.isfile(original_basename + '.tcx') + ): + logging.debug('Original data file for %s already exists', activity_id) + print('\tOriginal data file already exists; skipping...') + # Inform the main program that the file already exists + return False + + if args.format != 'json': + # Download the data file from Garmin Connect. If the download fails (e.g., due to timeout), + # this script will die, but nothing will have been written to disk about this activity, so + # just running it again should pick up where it left off. + + try: + data = http_req(download_url) + except HTTPError as ex: + # Handle expected (though unfortunate) error codes; die on unexpected ones. + if ex.code == 500 and args.format == 'tcx': + # Garmin will give an internal server error (HTTP 500) when downloading TCX files + # if the original was a manual GPX upload. Writing an empty file prevents this file + # from being redownloaded, similar to the way GPX files are saved even when there + # are no tracks. One could be generated here, but that's a bit much. Use the GPX + # format if you want actual data in every file, as I believe Garmin provides a GPX + # file for every activity. + logging.info('Writing empty file since Garmin did not generate a TCX file for this activity...') + data = '' + elif ex.code == 404 and args.format == 'original': + # For manual activities (i.e., entered in online without a file upload), there is + # no original file. # Write an empty file to prevent redownloading it. + logging.info('Writing empty file since there was no original activity data...') + data = '' + else: + logging.info('Got %s for %s', ex.code, download_url) + raise Exception(f'Failed. Got an HTTP error {ex.code} for {download_url}') from ex + else: + data = activity_details + + # Persist file + write_to_file(data_filename, data, file_mode, file_time) + + # Success: Add activity ID to downloaded_ids.json + update_download_stats(activity_id, args.directory) + + if args.format == 'original': + # Even manual upload of a GPX file is zipped, but we'll validate the extension. + if args.unzip and data_filename[-3:].lower() == 'zip': + logging.debug('Unzipping and removing original file, size is %s', os.stat(data_filename).st_size) + if os.stat(data_filename).st_size > 0: + with open(data_filename, 'rb') as zip_file, zipfile.ZipFile(zip_file) as zip_obj: + for name in zip_obj.namelist(): + unzipped_name = zip_obj.extract(name, directory) + # prepend 'activity_' and append the description to the base name + name_base, name_ext = os.path.splitext(name) + # sometimes in 2020 Garmin added '_ACTIVITY' to the name in the ZIP. Remove it... + # note that 'new_name' should match 'original_basename' elsewhere in this script to + # avoid downloading the same files again + name_base = name_base.replace('_ACTIVITY', '') + new_name = os.path.join(directory, f'{prefix}activity_{name_base}{append_desc}{name_ext}') + logging.debug('renaming %s to %s', unzipped_name, new_name) + os.rename(unzipped_name, new_name) + if file_time: + os.utime(new_name, (file_time, file_time)) + else: + print('\tSkipping 0Kb zip file.') + os.remove(data_filename) + + # Inform the main program that the file is new + return True + + +def setup_logging(args): + """Setup logging""" + logpath = args.logpath if args.logpath else args.directory + if not os.path.isdir(logpath): + os.makedirs(logpath) + + logging.basicConfig( + filename=os.path.join(logpath, 'gcexport.log'), level=logging.DEBUG, format='%(asctime)s [%(levelname)-7.7s] %(message)s' + ) + + # set up logging to console + console = logging.StreamHandler() + console.setLevel(logging.WARN) + formatter = logging.Formatter('[%(levelname)s] %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + + +def logging_verbosity(verbosity): + """Adapt logging verbosity, separately for logfile and console output""" + logger = logging.getLogger() + for handler in logger.handlers: + if isinstance(handler, logging.FileHandler): + # this is the logfile handler + level = logging.DEBUG if verbosity > 0 else logging.INFO + handler.setLevel(level) + logging.info('New logfile level: %s', logging.getLevelName(level)) + elif isinstance(handler, logging.StreamHandler): + # this is the console handler + level = logging.DEBUG if verbosity > 1 else (logging.INFO if verbosity > 0 else logging.WARN) + handler.setLevel(level) + logging.debug('New console log level: %s', logging.getLevelName(level)) + + +def fetch_userstats(args): + """ + Http request for getting user statistic like total number of activities. The json will be saved as file + 'userstats.json' + :param args: command-line arguments (for args.directory etc) + :return: json with user statistics + """ + print('Getting display name...', end='') + logging.info('Profile page %s', URL_GC_PROFILE) + profile_page = http_req_as_string(URL_GC_PROFILE) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, 'profile.html'), profile_page, 'w') + + display_name = extract_display_name(profile_page) + print(' Done. displayName=', display_name, sep='') + + print('Fetching user stats...', end='') + logging.info('Userstats page %s', URL_GC_USERSTATS + display_name) + result = http_req_as_string(URL_GC_USERSTATS + display_name) + print(' Done.') + + # Persist JSON + write_to_file(os.path.join(args.directory, 'userstats.json'), result, 'w') + + return json.loads(result) + + +def extract_display_name(profile_page): + """ + Extract the display name from the profile page HTML document + :param profile_page: HTML document + :return: the display name + """ + # the display name should be in the HTML document as + # "displayName":"John.Doe" + pattern = re.compile(r".*\"displayName\":\"(.+?)\".*", re.MULTILINE | re.DOTALL) + match = pattern.match(profile_page) + if not match: + raise Exception('Did not find the display name in the profile page.') + display_name = match.group(1) + return display_name + + +def fetch_activity_list(args, total_to_download): + """ + Fetch the first 'total_to_download' activity summaries; as a side effect save them in json format. + :param args: command-line arguments (for args.directory etc) + :param total_to_download: number of activities to download + :return: List of activity summaries + """ + + # This while loop will download data from the server in multiple chunks, if necessary. + activities = [] + + total_downloaded = 0 + while total_downloaded < total_to_download: + # Maximum chunk size 'LIMIT_MAXIMUM' ... 400 return status if over maximum. So download + # maximum or whatever remains if less than maximum. + # As of 2018-03-06 I get return status 500 if over maximum + if total_to_download - total_downloaded > LIMIT_MAXIMUM: + num_to_download = LIMIT_MAXIMUM + else: + num_to_download = total_to_download - total_downloaded + + chunk = fetch_activity_chunk(args, num_to_download, total_downloaded) + activities.extend(chunk) + total_downloaded += num_to_download + + # it seems that parent multisport activities are not counted in userstats + if len(activities) != total_to_download: + logging.info('Expected %s activities, got %s.', total_to_download, len(activities)) + return activities + + +def annotate_activity_list(activities, start, exclude_list): + """ + Creates an action list with a tuple per activity summary + + The tuple per activity contains three values: + - index: the index of the activity summary in the activities argument + (the first gets index 0, the second index 1 etc) + - activity the activity summary from the activites argument + - action the action to take for this activity (d=download, s=skip, e=exclude) + + :param activities: List of activity summaries + :param start: One-based index of the first non-skipped activity + (i.e. with 1 no activity gets skipped, with 2 the first activity gets skipped etc) + :param exclude_list: List of activity ids that have to be skipped explicitly + :return: List of action tuples + """ + + action_list = [] + for index, activity in enumerate(activities): + if index < (start - 1): + action = 's' + elif str(activity['activityId']) in exclude_list: + action = 'e' + else: + action = 'd' + + action_list.append(dict(index=index, action=action, activity=activity)) + + return action_list + + +def fetch_activity_chunk(args, num_to_download, total_downloaded): + """ + Fetch a chunk of activity summaries; as a side effect save them in json format. + :param args: command-line arguments (for args.directory etc) + :param num_to_download: number of summaries to download in this chunk + :param total_downloaded: number of already downloaded summaries in previous chunks + :return: List of activity summaries + """ + + search_params = {'start': total_downloaded, 'limit': num_to_download} + # Query Garmin Connect + print('Querying list of activities ', total_downloaded + 1, '..', total_downloaded + num_to_download, '...', sep='', end='') + logging.info('Activity list URL %s', URL_GC_LIST + urlencode(search_params)) + result = http_req_as_string(URL_GC_LIST + urlencode(search_params)) + print(' Done.') + + # Persist JSON activities list + current_index = total_downloaded + 1 + activities_list_filename = f'activities-{current_index}-{total_downloaded+num_to_download}.json' + write_to_file(os.path.join(args.directory, activities_list_filename), result, 'w') + activity_summaries = json.loads(result) + fetch_multisports(activity_summaries, http_req_as_string, args) + return activity_summaries + + +def fetch_multisports(activity_summaries, http_caller, args): + """ + Search 'activity_summaries' for multisport activities and then + fetch the information for the activity parts (child activities) + and insert them into the 'activity_summaries' just after the multisport + activity + :param activity_summaries: list of activity summaries, will be modified in-place + :param http_caller: callback to perform the HTTP call for downloading the activity details + :param args: command-line arguments (for args.directory etc) + """ + for idx, child_summary in enumerate(activity_summaries): + type_key = None if absent_or_null('activityType', child_summary) else child_summary['activityType']['typeKey'] + if type_key == 'multi_sport': + _, details = fetch_details(child_summary['activityId'], http_caller) + + child_ids = ( + details['metadataDTO']['childIds'] if 'metadataDTO' in details and 'childIds' in details['metadataDTO'] else None + ) + # insert the children in reversed order always at the same index to get + # the correct order in activity_summaries + for child_id in reversed(child_ids): + child_string, child_details = fetch_details(child_id, http_caller) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, f'child_{child_id}.json'), child_string, 'w') + child_summary = {} + copy_details_to_summary(child_summary, child_details) + activity_summaries.insert(idx + 1, child_summary) + + +def fetch_details(activity_id, http_caller): + """ + Try to get the activity details for an activity + + :param activity_id: id of the activity to fetch + :param http_caller: callback to perform the HTTP call for downloading the activity details + :return details_as_string, details_as_json_dict: + """ + activity_details = None + details = None + tries = MAX_TRIES + while tries > 0: + activity_details = http_caller(f'{URL_GC_ACTIVITY}{activity_id}') + details = json.loads(activity_details) + # I observed a failure to get a complete JSON detail in about 5-10 calls out of 1000 + # retrying then statistically gets a better JSON ;-) + if details['summaryDTO']: + tries = 0 + else: + logging.info("Retrying activity details download %s", URL_GC_ACTIVITY + str(activity_id)) + tries -= 1 + if tries == 0: + raise Exception(f'Didn\'t get "summaryDTO" after {MAX_TRIES} tries for {activity_id}') + return activity_details, details + + +def copy_details_to_summary(summary, details): + """ + Add some activity properties from the 'details' dict to the 'summary' dict + + The choice of which properties are copied is determined by the properties + used by the 'csv_write_record' method. + + This particularly useful for childs of multisport activities, as I don't + know how to get these activity summaries otherwise + :param summary: summary dict, will be modified in-place + :param details: details dict + """ + # fmt: off + summary['activityId'] = details['activityId'] + summary['activityName'] = details['activityName'] + summary['description'] = details['description'] if present('description', details) else None + summary['activityType'] = {} + summary['activityType']['typeId'] = details['activityTypeDTO']['typeId'] if 'activityTypeDTO' in details and present('typeId', details['activityTypeDTO']) else None + summary['activityType']['typeKey'] = details['activityTypeDTO']['typeKey'] if 'activityTypeDTO' in details and present('typeKey', details['activityTypeDTO']) else None + summary['activityType']['parentTypeId'] = details['activityTypeDTO']['parentTypeId'] if 'activityTypeDTO' in details and present('parentTypeId', details['activityTypeDTO']) else None + summary['eventType'] = {} + summary['eventType']['typeKey'] = details['eventType']['typeKey'] if 'eventType' in details and present('typeKey', details['eventType']) else None + summary['startTimeLocal'] = details['summaryDTO']['startTimeLocal'] if 'summaryDTO' in details and 'startTimeLocal' in details['summaryDTO'] else None + summary['startTimeGMT'] = details['summaryDTO']['startTimeGMT'] if 'summaryDTO' in details and 'startTimeGMT' in details['summaryDTO'] else None + summary['duration'] = details['summaryDTO']['duration'] if 'summaryDTO' in details and 'duration' in details['summaryDTO'] else None + summary['distance'] = details['summaryDTO']['distance'] if 'summaryDTO' in details and 'distance' in details['summaryDTO'] else None + summary['averageSpeed'] = details['summaryDTO']['averageSpeed'] if 'summaryDTO' in details and 'averageSpeed' in details['summaryDTO'] else None + summary['maxHR'] = details['summaryDTO']['maxHR'] if 'summaryDTO' in details and 'maxHR' in details['summaryDTO'] else None + summary['averageHR'] = details['summaryDTO']['averageHR'] if 'summaryDTO' in details and 'averageHR' in details['summaryDTO'] else None + summary['elevationCorrected'] = details['metadataDTO']['elevationCorrected'] if 'metadataDTO' in details and 'elevationCorrected' in details['metadataDTO'] else None + # fmt: on + + +def process_activity_item(item, number_of_items, device_dict, activity_type_name, event_type_name, csv_filter, args): + """ + Process one activity item: download the data, parse it and write a line to the CSV file + + :param item: activity item tuple, see `annotate_activity_list()` + :param number_of_items: total number of items (for progress output) + :param device_dict: cache (dict) of already known devices + :param activity_type_name: lookup table for activity type descriptions + :param event_type_name: lookup table for event type descriptions + :param csv_filter: object encapsulating CSV file access + :param args: command-line arguments + """ + current_index = item['index'] + 1 + actvty = item['activity'] + action = item['action'] + + # Action: skipping + if action == 's': + # Display which entry we're skipping. + print('Skipping : Garmin Connect activity ', end='') + print(f"({current_index}/{number_of_items}) [{actvty['activityId']}]") + return + + # Action: excluding + if action == 'e': + # Display which entry we're skipping. + print('Excluding : Garmin Connect activity ', end='') + print(f"({current_index}/{number_of_items}) [{actvty['activityId']}]") + return + + # Action: download + # Display which entry we're working on. + print('Downloading: Garmin Connect activity ', end='') + print(f"({current_index}/{number_of_items}) [{actvty['activityId']}] {actvty['activityName']}") + + # Retrieve also the detail data from the activity (the one displayed on + # the https://connect.garmin.com/modern/activity/xxx page), because some + # data are missing from 'actvty' (or are even different, e.g. for my activities + # 86497297 or 86516281) + activity_details, details = fetch_details(actvty['activityId'], http_req_as_string) + + extract = {} + extract['start_time_with_offset'] = offset_date_time(actvty['startTimeLocal'], actvty['startTimeGMT']) + if 'summaryDTO' in details and 'elapsedDuration' in details['summaryDTO']: + elapsed_duration = details['summaryDTO']['elapsedDuration'] + else: + elapsed_duration = None + extract['elapsed_duration'] = elapsed_duration if elapsed_duration else actvty['duration'] + extract['elapsed_seconds'] = int(round(extract['elapsed_duration'])) + extract['end_time_with_offset'] = extract['start_time_with_offset'] + timedelta(seconds=extract['elapsed_seconds']) + + print('\t', extract['start_time_with_offset'].isoformat(), ', ', sep='', end='') + print(hhmmss_from_seconds(extract['elapsed_seconds']), ', ', sep='', end='') + if 'distance' in actvty and isinstance(actvty['distance'], float): + print(f"{actvty['distance'] / 1000:.3f} km") + else: + print('0.000 km') + + if args.desc is not None: + append_desc = '_' + sanitize_filename(actvty['activityName'], args.desc) + else: + append_desc = '' + + if args.originaltime: + start_time_seconds = epoch_seconds_from_summary(actvty) + else: + start_time_seconds = None + + extract['device'] = extract_device(device_dict, details, start_time_seconds, args, http_req_as_string, write_to_file) + + # try to get the JSON with all the samples (not all activities have it...), + # but only if it's really needed for the CSV output + extract['samples'] = None + if csv_filter.is_column_active('sampleCount'): + try: + # TODO implement retries here, I have observed temporary failures + activity_measurements = http_req_as_string(f"{URL_GC_ACTIVITY}{actvty['activityId']}/details") + write_to_file( + os.path.join(args.directory, f"activity_{actvty['activityId']}_samples.json"), + activity_measurements, + 'w', + start_time_seconds, + ) + samples = json.loads(activity_measurements) + extract['samples'] = samples + except HTTPError as ex: + logging.info("Unable to get samples for %d", actvty['activityId']) + logging.exception(ex) + + extract['gear'] = None + if csv_filter.is_column_active('gear'): + extract['gear'] = load_gear(str(actvty['activityId']), args) + + extract['hrZones'] = HR_ZONES_EMPTY + if csv_filter.is_column_active('hrZone1Low') or csv_filter.is_column_active('hrZone1Seconds'): + extract['hrZones'] = load_zones(str(actvty['activityId']), start_time_seconds, args, http_req_as_string, write_to_file) + + # Save the file and inform if it already existed. If the file already existed, do not apped the record to the csv + if export_data_file( + str(actvty['activityId']), activity_details, args, start_time_seconds, append_desc, actvty['startTimeLocal'] + ): + # Write stats to CSV. + csv_write_record(csv_filter, extract, actvty, details, activity_type_name, event_type_name) + + +def main(argv): + """ + Main entry point for gcexport.py + """ + args = parse_arguments(argv) + setup_logging(args) + logging.info("Starting %s version %s, using Python version %s", argv[0], SCRIPT_VERSION, python_version()) + logging_verbosity(args.verbosity) + + print('Welcome to Garmin Connect Exporter!') + + if sys.version_info < MINIMUM_PYTHON_VERSION: + logging.warning( + "Python version %s is older than %s.%s.x, results might be unexpected", + python_version(), + MINIMUM_PYTHON_VERSION[0], + MINIMUM_PYTHON_VERSION[1], + ) + + # Get filter list with IDs to exclude + if args.exclude is not None: + exclude_list = read_exclude(args.exclude) + if exclude_list is None: + sys.exit(1) + else: + exclude_list = [] + + # Create directory for data files. + if os.path.isdir(args.directory): + logging.warning( + 'Output directory %s already exists. Will skip already-downloaded files and append to the CSV file.', args.directory + ) + else: + os.mkdir(args.directory) + + login_to_garmin_connect(args) + + # Query the userstats (activities totals on the profile page). Needed for + # filtering and for downloading 'all' to know how many activities are available + userstats_json = fetch_userstats(args) + + if args.count == 'all': + total_to_download = int(userstats_json['userMetrics'][0]['totalActivities']) + else: + total_to_download = int(args.count) + + device_dict = {} + + # load some dictionaries with lookup data from REST services + activity_type_props = http_req_as_string(URL_GC_ACT_PROPS) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, 'activity_types.properties'), activity_type_props, 'w') + activity_type_name = load_properties(activity_type_props) + event_type_props = http_req_as_string(URL_GC_EVT_PROPS) + if args.verbosity > 0: + write_to_file(os.path.join(args.directory, 'event_types.properties'), activity_type_props, 'w') + event_type_name = load_properties(event_type_props) + + activities = fetch_activity_list(args, total_to_download) + action_list = annotate_activity_list(activities, args.start_activity_no, exclude_list) + + csv_filename = os.path.join(args.directory, 'activities.csv') + csv_existed = os.path.isfile(csv_filename) + + with open(csv_filename, mode='a', encoding='utf-8') as csv_file: + csv_filter = CsvFilter(csv_file, args.template) + + # Write header to CSV file + if not csv_existed: + csv_filter.write_header() + + # Process each activity. + for item in action_list: + process_activity_item(item, len(action_list), device_dict, activity_type_name, event_type_name, csv_filter, args) + + if args.external: + print('Open CSV output.') + print(csv_filename) + call([args.external, "--" + args.args, csv_filename]) + + print('Done!') + + +if __name__ == "__main__": + try: + main(sys.argv) + except KeyboardInterrupt: + print('Interrupted') + sys.exit(0) diff --git a/garmin-connect-export/gcexport_test.py b/garmin-connect-export/gcexport_test.py new file mode 100644 index 0000000..46a55ae --- /dev/null +++ b/garmin-connect-export/gcexport_test.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +""" +Tests for gcexport.py; Call them with this command line: + +py.test gcexport_test.py +""" + +from gcexport import * +from io import StringIO + + +def test_pace_or_speed_raw_cycling(): + # 10 m/s is 36 km/h + assert pace_or_speed_raw(2, 4, 10.0) == 36.0 + + +def test_pace_or_speed_raw_running(): + # 3.33 m/s is 12 km/h is 5 min/km + assert pace_or_speed_raw(1, 4, 10.0 / 3) == 5.0 + + +def test_pace_or_speed_formatted_cycling(): + # 10 m/s is 36 km/h + assert pace_or_speed_formatted(2, 4, 10.0) == '36.0' + + +def test_pace_or_speed_formatted_running(): + # 3.33 m/s is 12 km/h is 5 min/km + assert pace_or_speed_formatted(1, 4, 10.0 / 3) == '05:00' + + +def test_trunc6_more(): + assert trunc6(0.123456789) == '0.123456' + + +def test_trunc6_less(): + assert trunc6(0.123) == '0.123000' + + +def test_offset_date_time(): + assert offset_date_time("2018-03-08 12:23:22", "2018-03-08 11:23:22") == datetime( + 2018, 3, 8, 12, 23, 22, 0, FixedOffset(60, "LCL") + ) + assert offset_date_time("2018-03-08 12:23:22", "2018-03-08 12:23:22") == datetime( + 2018, 3, 8, 12, 23, 22, 0, FixedOffset(0, "LCL") + ) + + +def test_datetime_from_iso(): + assert datetime_from_iso("2018-03-08 12:23:22") == datetime(2018, 3, 8, 12, 23, 22, 0) + assert datetime_from_iso("2018-03-08 12:23:22.0") == datetime(2018, 3, 8, 12, 23, 22, 0) + assert datetime_from_iso("2018-03-08T12:23:22") == datetime(2018, 3, 8, 12, 23, 22, 0) + assert datetime_from_iso("2018-03-08T12:23:22.0") == datetime(2018, 3, 8, 12, 23, 22, 0) + + +def test_epoch_seconds_from_summary(): + # activity with a beginTimestamp + with open('json/activity_2541953812_overview.json') as json_timestamp: + summary = json.load(json_timestamp) + assert summary['beginTimestamp'] == 1520508202000 + assert epoch_seconds_from_summary(summary) == 1520508202 + + # activity with a startTimeLocal without fractions + with open('json/activity_multisport_overview.json') as json_timestamp: + summary = json.load(json_timestamp) + assert summary['beginTimestamp'] == None + assert summary['startTimeLocal'] == '2021-04-11 11:50:49' + assert epoch_seconds_from_summary(summary) == 1618134649 + + # activity with a startTimeLocal with fractions + summary['startTimeLocal'] = '2021-04-11 11:50:50.3' + assert epoch_seconds_from_summary(summary) == 1618134650 + + +def test_hhmmss_from_seconds(): + # check/document that no rounding happens in hhmmss_from_seconds and the caller must round itself: + # 2969.6 s are 49 minutes and 29.6 seconds + assert hhmmss_from_seconds(2969.6) == "00:49:29" + assert hhmmss_from_seconds(round(2969.6)) == "00:49:30" + + +def test_sanitize_filename(): + assert 'all_ascii' == sanitize_filename(u'all_ascii') + assert 'deja_funf' == sanitize_filename(u'déjà fünf') + assert 'deja_' == sanitize_filename(u'déjà fünf', 5) + assert '' == sanitize_filename(u'') + assert '' == sanitize_filename(None) + + with open('json/activity_emoji.json') as json_data: + details = json.load(json_data) + assert 'Biel__Pavillon' == sanitize_filename(details['activityName']) + + +def test_load_properties_keys(): + with open('csv_header_default.properties', 'r') as prop: + csv_header_props = prop.read() + csv_columns = [] + csv_headers = load_properties(csv_header_props, keys=csv_columns) + + assert csv_columns[0] == 'startTimeIso' + assert csv_headers['startTimeIso'] == "Start Time" + + +def test_csv_write_record(): + with open('json/activitylist-service.json') as json_data_1: + activities = json.load(json_data_1) + with open('json/activity_emoji.json') as json_data_2: + details = json.load(json_data_2) + with open('json/activity_types.properties', 'r') as prop_1: + activity_type_props = prop_1.read() + activity_type_name = load_properties(activity_type_props) + with open('json/event_types.properties', 'r') as prop_2: + event_type_props = prop_2.read() + event_type_name = load_properties(event_type_props) + + extract = {} + extract['start_time_with_offset'] = offset_date_time("2018-03-08 12:23:22", "2018-03-08 11:23:22") + extract['end_time_with_offset'] = offset_date_time("2018-03-08 12:23:22", "2018-03-08 12:23:22") + extract['elapsed_duration'] = 42.43 + extract['elapsed_seconds'] = 42 + extract['samples'] = None + extract['device'] = "some device" + extract['gear'] = "some gear" + extract['hrZones'] = HR_ZONES_EMPTY + extract['hrZones'][1] = json.loads('{ "secsInZone": 1689.269, "zoneLowBoundary": 138 }') + + csv_file = StringIO() + csv_filter = CsvFilter(csv_file, 'csv_header_all.properties') + csv_write_record(csv_filter, extract, activities[0], details, activity_type_name, event_type_name) + expected = '"Biel 🏛 Pavillon"' + assert csv_file.getvalue()[69 : 69 + len(expected)] == expected + + +def write_to_file_mock(filename, content, mode, file_time=None): + pass + + +def http_req_mock_device(url, post=None, headers=None): + with open('json/device_856399.json') as json_device: + return json_device.read() + + +def test_extract_device(): + args = parse_arguments([]) + + with open('json/activity_2541953812.json') as json_detail: + details = json.load(json_detail) + assert u'fēnix 5 10.0.0.0' == extract_device({}, details, None, args, http_req_mock_device, write_to_file_mock) + + with open('json/activity_154105348_gpx_device_null.json') as json_detail: + details = json.load(json_detail) + assert None == extract_device({}, details, None, args, http_req_mock_device, write_to_file_mock) + + with open('json/activity_995784118_gpx_device_0.json') as json_detail: + details = json.load(json_detail) + assert None == extract_device({}, details, None, args, http_req_mock_device, write_to_file_mock) + + +def http_req_mock_zones(url, post=None, headers=None): + with open('json/activity_2541953812_zones.json') as json_zones: + return json_zones.read() + + +def test_load_zones(): + args = parse_arguments([]) + + zones = load_zones('2541953812', None, args, http_req_mock_zones, write_to_file_mock) + assert 5 == len(zones) + assert 100 == zones[0]['zoneLowBoundary'] + assert 138 == zones[1]['zoneLowBoundary'] + assert 148 == zones[2]['zoneLowBoundary'] + assert 168 == zones[3]['zoneLowBoundary'] + assert 182 == zones[4]['zoneLowBoundary'] + assert 2462.848 == zones[0]['secsInZone'] + + +def test_extract_display_name(): + with open('html/profile_simple.html') as html: + profile_page = html.read() + assert 'John.Doe' == extract_display_name(profile_page) + + # some users reported (issue #65) to have an email address as display name + with open('html/profile_email.html') as html: + profile_page = html.read() + assert 'john.doe@email.org' == extract_display_name(profile_page) + + # some users reported to have a UUID as display name: + # https://github.com/moderation/garmin-connect-export/issues/31 + with open('html/profile_uuid.html') as html: + profile_page = html.read() + assert '36e29d65-715c-456b-9115-84f0b9a0c0ba' == extract_display_name(profile_page) + + +def test_resolve_path(): + assert resolve_path('root', 'sub/{YYYY}', '2018-03-08 12:23:22') == 'root/sub/2018' + assert resolve_path('root', 'sub/{MM}', '2018-03-08 12:23:22') == 'root/sub/03' + assert resolve_path('root', 'sub/{YYYY}/{MM}', '2018-03-08 12:23:22') == 'root/sub/2018/03' + assert resolve_path('root', 'sub/{yyyy}', '2018-03-08 12:23:22') == 'root/sub/{yyyy}' + assert resolve_path('root', 'sub/{YYYYMM}', '2018-03-08 12:23:22') == 'root/sub/{YYYYMM}' + assert resolve_path('root', 'sub/all', '2018-03-08 12:23:22') == 'root/sub/all' + + +mock_details_multi_counter = 0 + + +def http_req_mock_details_multi(url, post=None, headers=None): + + global mock_details_multi_counter + mock_details_multi_counter += 1 + + if mock_details_multi_counter == 1: + with open('json/activity_multisport_detail.json') as json_stream: + return json_stream.read() + elif mock_details_multi_counter >= 2 & mock_details_multi_counter <= 6: + with open('json/activity_multisport_child.json') as json_stream: + json_string = json_stream.read() + activity_id = url.split('/')[-1] + return json_string.replace('6588349076', activity_id) + else: + raise Exception('mock_details_multi_counter has invalid value ' + str(mock_details_multi_counter)) + + +def test_fetch_multisports(): + args = parse_arguments([]) + + with open('json/activities-list.json') as json_detail: + activity_summaries = json.load(json_detail) + + # assert state before fetch_multisports + assert activity_summaries[0]['activityId'] == 6609987243 + assert activity_summaries[1]['activityId'] == 6588349056 + assert activity_summaries[2]['activityId'] == 6585943400 + + global mock_details_multi_counter + mock_details_multi_counter = 0 + fetch_multisports(activity_summaries, http_req_mock_details_multi, args) + + # the entries 0/1/2 from before are now 0/1/7 + assert activity_summaries[0]['activityId'] == 6609987243 + assert activity_summaries[1]['activityId'] == 6588349056 + assert activity_summaries[7]['activityId'] == 6585943400 + + # at indexes 2..6 are now the five child activities + assert activity_summaries[2]['activityId'] == 6588349067 + assert activity_summaries[3]['activityId'] == 6588349072 + assert activity_summaries[4]['activityId'] == 6588349076 + assert activity_summaries[5]['activityId'] == 6588349079 + assert activity_summaries[6]['activityId'] == 6588349081 diff --git a/garmin-connect-export/html/profile_email.html b/garmin-connect-export/html/profile_email.html new file mode 100644 index 0000000..692dca6 --- /dev/null +++ b/garmin-connect-export/html/profile_email.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + Garmin Connect + + + + + + + + + + + + + + + + + + + + + + + + + + + Connect + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ + +
+ + + + +
+ + + + + + +
+ + + + + + + +
+ + +
+ + + +
+
+ + + + + + + diff --git a/garmin-connect-export/html/profile_simple.html b/garmin-connect-export/html/profile_simple.html new file mode 100644 index 0000000..29f17a1 --- /dev/null +++ b/garmin-connect-export/html/profile_simple.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + Garmin Connect + + + + + + + + + + + + + + + + + + + + + + + + + + + Connect + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ + +
+ + + + +
+ + + + + + +
+ + + + + + + +
+ + +
+ + + +
+
+ + + + + + + diff --git a/garmin-connect-export/html/profile_uuid.html b/garmin-connect-export/html/profile_uuid.html new file mode 100644 index 0000000..1ff582e --- /dev/null +++ b/garmin-connect-export/html/profile_uuid.html @@ -0,0 +1,135 @@ + + + + + + + + + + + + Garmin Connect + + + + + + + + + + + + + + + + + + + + + + + + + + + Connect + + + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ +
+ +
+ + +
+ + + + +
+ + + + + + +
+ + + + + + + +
+ + +
+ + + +
+
+ + + + + + + diff --git a/garmin-connect-export/json/README.md b/garmin-connect-export/json/README.md new file mode 100644 index 0000000..c2ae040 --- /dev/null +++ b/garmin-connect-export/json/README.md @@ -0,0 +1,5 @@ +JSON Examples +============= + +This directory contains some sample JSON output retrieved from Garmin Connect, +it allows to better understand the code ;-) diff --git a/garmin-connect-export/json/activities-list.json b/garmin-connect-export/json/activities-list.json new file mode 100644 index 0000000..9f03466 --- /dev/null +++ b/garmin-connect-export/json/activities-list.json @@ -0,0 +1,632 @@ +[ + { + "activityId": 6609987243, + "activityName": "Walk", + "description": null, + "startTimeLocal": "2021-04-15 11:44:05", + "startTimeGMT": "2021-04-15 09:44:05", + "activityType": { + "typeId": 9, + "typeKey": "walking", + "parentTypeId": 17, + "sortOrder": 27, + "isHidden": false, + "restricted": false + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 3385.43994140625, + "duration": 2702.589111328125, + "elapsedDuration": 2702589.111328125, + "movingDuration": 2335.0, + "elevationGain": 37.0, + "elevationLoss": 36.0, + "averageSpeed": 1.253000020980835, + "maxSpeed": 2.3610000610351562, + "startLatitude": 46.77777777777777, + "startLongitude": 7.1111111111111111, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/5979a7d6-2502-4869-8f57-86a28ab7eab3-2836200.png", + "calories": 225.0, + "averageHR": 78.0, + "maxHR": 104.0, + "averageRunningCadenceInStepsPerMinute": 68.53125, + "maxRunningCadenceInStepsPerMinute": 158.0, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": 3274, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "activityLikeProfileImageUrls": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_OUTDOOR_USER", + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 2, + "typeKey": "private" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": 1618479845000, + "sportTypeId": 11, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 0.5, + "anaerobicTrainingEffect": 0.0, + "strokes": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": 113.10710304272114, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": 43.0, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": null, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": 21.0, + "maxTemperature": null, + "minElevation": 62040.00244140625, + "maxElevation": 65540.00244140625, + "avgDoubleCadence": null, + "maxDoubleCadence": 158.0, + "summarizedExerciseSets": null, + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": null + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": 0.5, + "floorsClimbed": null, + "floorsDescended": null, + "manufacturer": "GARMIN", + "diveNumber": null, + "locationName": "Plasselb", + "bottomTime": null, + "lapCount": 3, + "endLatitude": 46.88888888888888, + "endLongitude": 7.222222222222222, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "flow": null, + "grit": null, + "jumpCount": null, + "caloriesEstimated": null, + "caloriesConsumed": null, + "waterEstimated": null, + "waterConsumed": null, + "maxAvgPower_1": null, + "maxAvgPower_2": null, + "maxAvgPower_5": null, + "maxAvgPower_10": null, + "maxAvgPower_20": null, + "maxAvgPower_30": null, + "maxAvgPower_60": null, + "maxAvgPower_120": null, + "maxAvgPower_300": null, + "maxAvgPower_600": null, + "maxAvgPower_1200": null, + "maxAvgPower_1800": null, + "maxAvgPower_3600": null, + "maxAvgPower_7200": null, + "maxAvgPower_18000": null, + "excludeFromPowerCurveReports": null, + "totalSets": null, + "activeSets": null, + "totalReps": null, + "minRespirationRate": null, + "maxRespirationRate": null, + "avgRespirationRate": null, + "trainingEffectLabel": null, + "activityTrainingLoad": null, + "avgFlow": null, + "avgGrit": null, + "minActivityLapDuration": 138.91900634765625, + "avgStress": null, + "startStress": null, + "endStress": null, + "differenceStress": null, + "maxStress": null, + "aerobicTrainingEffectMessage": "NO_AEROBIC_BENEFIT_18", + "anaerobicTrainingEffectMessage": "NO_ANAEROBIC_BENEFIT_0", + "splitSummaries": [], + "hasSplits": false, + "hasSeedFirstbeatProfile": null, + "favorite": false, + "decoDive": null, + "purposeful": false, + "pr": false, + "autoCalcCalories": false, + "parent": false, + "atpActivity": false, + "manualActivity": false, + "elevationCorrected": false + }, + { + "activityId": 6588349056, + "activityName": "Bike-Walk-Bike", + "description": null, + "startTimeLocal": "2021-04-11 11:50:49", + "startTimeGMT": "2021-04-11 09:50:49", + "activityType": { + "typeId": 89, + "typeKey": "multi_sport", + "parentTypeId": 17, + "sortOrder": 58, + "isHidden": false, + "restricted": false + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 38351.40899467468, + "duration": 10226.370854377747, + "elapsedDuration": 11183693.120002747, + "movingDuration": null, + "elevationGain": 991.0, + "elevationLoss": 994.0, + "averageSpeed": 3.7502462545896287, + "maxSpeed": null, + "startLatitude": 46.66666666666666, + "startLongitude": 7.111111111111111, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/5979a7d6-2502-4869-8f57-86a28ab7eab3-2836200.png", + "calories": 1139.0, + "averageHR": null, + "maxHR": 175.0, + "averageRunningCadenceInStepsPerMinute": null, + "maxRunningCadenceInStepsPerMinute": null, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": null, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "activityLikeProfileImageUrls": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_OUTDOOR_USER", + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 3, + "typeKey": "subscribers" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": null, + "sportTypeId": 18, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 3.4000000953674316, + "anaerobicTrainingEffect": 1.600000023841858, + "strokes": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": null, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": null, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": null, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": null, + "maxTemperature": null, + "minElevation": null, + "maxElevation": null, + "avgDoubleCadence": null, + "maxDoubleCadence": null, + "summarizedExerciseSets": null, + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": null + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": null, + "floorsClimbed": null, + "floorsDescended": null, + "manufacturer": "GARMIN", + "diveNumber": null, + "locationName": "Plasselb", + "bottomTime": null, + "lapCount": 5, + "endLatitude": 46.77777777777777, + "endLongitude": 7.222222222222222, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "flow": null, + "grit": null, + "jumpCount": null, + "caloriesEstimated": null, + "caloriesConsumed": null, + "waterEstimated": null, + "waterConsumed": null, + "maxAvgPower_1": null, + "maxAvgPower_2": null, + "maxAvgPower_5": null, + "maxAvgPower_10": null, + "maxAvgPower_20": null, + "maxAvgPower_30": null, + "maxAvgPower_60": null, + "maxAvgPower_120": null, + "maxAvgPower_300": null, + "maxAvgPower_600": null, + "maxAvgPower_1200": null, + "maxAvgPower_1800": null, + "maxAvgPower_3600": null, + "maxAvgPower_7200": null, + "maxAvgPower_18000": null, + "excludeFromPowerCurveReports": null, + "totalSets": null, + "activeSets": null, + "totalReps": null, + "minRespirationRate": null, + "maxRespirationRate": null, + "avgRespirationRate": null, + "trainingEffectLabel": null, + "activityTrainingLoad": null, + "avgFlow": null, + "avgGrit": null, + "minActivityLapDuration": null, + "avgStress": null, + "startStress": null, + "endStress": null, + "differenceStress": null, + "maxStress": null, + "aerobicTrainingEffectMessage": "IMPROVING_LACTATE_THRESHOLD_12", + "anaerobicTrainingEffectMessage": "MINOR_ANAEROBIC_BENEFIT_15", + "splitSummaries": [], + "hasSplits": false, + "hasSeedFirstbeatProfile": null, + "favorite": false, + "decoDive": null, + "purposeful": false, + "pr": false, + "autoCalcCalories": false, + "parent": true, + "atpActivity": false, + "manualActivity": false, + "elevationCorrected": false + }, + { + "activityId": 6585943400, + "activityName": "Run", + "description": null, + "startTimeLocal": "2021-04-11 07:48:33", + "startTimeGMT": "2021-04-11 05:48:33", + "activityType": { + "typeId": 1, + "typeKey": "running", + "parentTypeId": 17, + "sortOrder": 3, + "isHidden": false, + "restricted": false + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 7287.759765625, + "duration": 3459.342041015625, + "elapsedDuration": 4677150.87890625, + "movingDuration": 3108.290008544922, + "elevationGain": 187.0, + "elevationLoss": 175.0, + "averageSpeed": 2.1070001125335693, + "maxSpeed": 3.5179998874664307, + "startLatitude": 46.77777777777777, + "startLongitude": 7.111111111111111, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/5979a7d6-2502-4869-8f57-86a28ab7eab3-2836200.png", + "calories": 571.0, + "averageHR": 133.0, + "maxHR": 188.0, + "averageRunningCadenceInStepsPerMinute": 134.59375, + "maxRunningCadenceInStepsPerMinute": 241.0, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": 7862, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "activityLikeProfileImageUrls": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_OUTDOOR_USER", + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 3, + "typeKey": "subscribers" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": 1618120113000, + "sportTypeId": 1, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 2.9000000953674316, + "anaerobicTrainingEffect": 0.4000000059604645, + "strokes": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": 94.47436351638775, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": 43.0, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": null, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": 11.0, + "maxTemperature": null, + "minElevation": 56320.001220703125, + "maxElevation": 65459.99755859375, + "avgDoubleCadence": null, + "maxDoubleCadence": 241.0, + "summarizedExerciseSets": null, + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": null + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": 0.60003662109375, + "floorsClimbed": null, + "floorsDescended": null, + "manufacturer": "GARMIN", + "diveNumber": null, + "locationName": "Plasselb", + "bottomTime": null, + "lapCount": 8, + "endLatitude": 46.888888888888888, + "endLongitude": 7.222222222222222, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "flow": null, + "grit": null, + "jumpCount": null, + "caloriesEstimated": null, + "caloriesConsumed": null, + "waterEstimated": null, + "waterConsumed": null, + "maxAvgPower_1": null, + "maxAvgPower_2": null, + "maxAvgPower_5": null, + "maxAvgPower_10": null, + "maxAvgPower_20": null, + "maxAvgPower_30": null, + "maxAvgPower_60": null, + "maxAvgPower_120": null, + "maxAvgPower_300": null, + "maxAvgPower_600": null, + "maxAvgPower_1200": null, + "maxAvgPower_1800": null, + "maxAvgPower_3600": null, + "maxAvgPower_7200": null, + "maxAvgPower_18000": null, + "excludeFromPowerCurveReports": null, + "totalSets": null, + "activeSets": null, + "totalReps": null, + "minRespirationRate": null, + "maxRespirationRate": null, + "avgRespirationRate": null, + "trainingEffectLabel": null, + "activityTrainingLoad": null, + "avgFlow": null, + "avgGrit": null, + "minActivityLapDuration": 104.4000015258789, + "avgStress": null, + "startStress": null, + "endStress": null, + "differenceStress": null, + "maxStress": null, + "aerobicTrainingEffectMessage": "MAINTAINING_AEROBIC_BASE_7", + "anaerobicTrainingEffectMessage": "NO_ANAEROBIC_BENEFIT_0", + "splitSummaries": [], + "hasSplits": false, + "hasSeedFirstbeatProfile": null, + "favorite": false, + "decoDive": null, + "purposeful": false, + "pr": false, + "autoCalcCalories": false, + "parent": false, + "atpActivity": false, + "manualActivity": false, + "elevationCorrected": false + } +] diff --git a/garmin-connect-export/json/activity-search-service-1.2.json b/garmin-connect-export/json/activity-search-service-1.2.json new file mode 100644 index 0000000..9d1ff21 --- /dev/null +++ b/garmin-connect-export/json/activity-search-service-1.2.json @@ -0,0 +1,822 @@ +{"results": { + "activities": [ + { + "activity": { + "activityId": 2541953812, + "activityName": "Reckingen Augenstern-Ulrichen Langlauf", + "activityDescription": "", + "activityVideoUrl": "", + "locationName": "", + "isTitled": true, + "isElevationCorrected": false, + "isBarometricCapable": true, + "isSwimAlgorithmCapable": true, + "isVideoCapable": false, + "isActivityEdited": false, + "favorite": false, + "ispr": false, + "isAutoCalcCalories": false, + "isParent": false, + "parentId": 0, + "userId": 2836200, + "username": "eschep", + "displayname": "eschep", + "uploadDate": { + "@class": "org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl", + "display": "Thu, 8 Mar 2018 7:09", + "value": "2018-03-08", + "withDay": "Thu, 8 Mar 2018", + "abbr": "8 Mar 2018", + "millis": "1520489394000" + }, + "uploadApplication": { + "display": "Unknown", + "key": "unknown", + "version": "18.4.4.0" + }, + "device": { + "display": "fēnix 5", + "key": "fenix5", + "version": "8.0.0.0" + }, + "deviceId": "3946806421", + "deviceImageUrl": "https://static.garmincdn.com/com.garmin.connect/content/images/device-images/fenix-5.png", + "isDeviceReleased": true, + "externalId": "889442603", + "privacy": { + "display": "Subscriber", + "key": "subscribers", + "fieldNameDisplay": "Privacy" + }, + "numTrackpoints": -1, + "activityType": { + "display": "Cross Country Skiing", + "key": "cross_country_skiing", + "fieldNameDisplay": "Activity Type", + "parent": { + "display": "Other", + "key": "other", + "fieldNameDisplay": "Activity Type" + } + }, + "eventType": { + "display": "Uncategorized", + "key": "uncategorized", + "fieldNameDisplay": "Event Type" + }, + "activityTimeZone": { + "display": "(GMT+01:00) Central European Time", + "key": "Europe/Paris", + "fieldNameDisplay": "Time Zone", + "abbr": "Central European Time" + }, + "localizedSpeedLabel": "Speed", + "localizedPaceLabel": "Pace", + "activitySummary": { + "SumSampleCountDuration": { + "fieldDisplayName": "SumSampleCountDuration", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "SumSampleCountElevation": { + "fieldDisplayName": "SumSampleCountElevation", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "SumSampleCountDistance": { + "fieldDisplayName": "SumSampleCountDistance", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "GainUncorrectedElevation": { + "fieldDisplayName": "GainUncorrectedElevation", + "display": "10,600", + "displayUnit": "Centimeters", + "value": "10600.0", + "unitAbbr": "cm", + "withUnit": "10,600 Centimeters", + "withUnitAbbr": "10,600 cm", + "uom": "centimeter" + }, + "GainCorrectedElevation": { + "fieldDisplayName": "GainCorrectedElevation", + "display": "8,353", + "displayUnit": "Centimeters", + "value": "8353.0", + "unitAbbr": "cm", + "withUnit": "8,353 Centimeters", + "withUnitAbbr": "8,353 cm", + "uom": "centimeter" + }, + "SumSampleCountSpeed": { + "fieldDisplayName": "SumSampleCountSpeed", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "LossUncorrectedElevation": { + "fieldDisplayName": "LossUncorrectedElevation", + "display": "7,300", + "displayUnit": "Centimeters", + "value": "7300.0", + "unitAbbr": "cm", + "withUnit": "7,300 Centimeters", + "withUnitAbbr": "7,300 cm", + "uom": "centimeter" + }, + "SumSampleCountHeartRate": { + "fieldDisplayName": "SumSampleCountHeartRate", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "DirectFunctionalThresholdPower": { + "fieldDisplayName": "DirectFunctionalThresholdPower", + "display": "244", + "displayUnit": "Watts", + "value": "244.0", + "unitAbbr": "W", + "withUnit": "244 Watts", + "withUnitAbbr": "244 W", + "uom": "watt" + }, + "EndLongitude": { + "fieldDisplayName": "EndLongitude", + "display": "8.2481", + "displayUnit": "Decimal Degrees", + "value": "8.248067228123546", + "unitAbbr": "dd", + "withUnit": "8.2481 Decimal Degrees", + "withUnitAbbr": "8.2481 dd", + "uom": "dd" + }, + "WeightedMeanMovingSpeed": { + "fieldDisplayName": "Avg Moving Speed", + "display": "11.3", + "displayUnit": "Kilometers per Hour", + "value": "11.328197775200842", + "unitAbbr": "km/h", + "withUnit": "11.3 Kilometers per Hour", + "withUnitAbbr": "11.3 km/h", + "uom": "kph" + }, + "WeightedMeanMovingPace": { + "fieldDisplayName": "Avg Moving Pace", + "display": "05:18", + "displayUnit": "Minutes per Kilometer", + "value": "5.296517697753228", + "unitAbbr": "min/km", + "withUnit": "05:18 Minutes per Kilometer", + "withUnitAbbr": "05:18 min/km", + "uom": "minperkm" + }, + "MaxHeartRate": { + "fieldDisplayName": "Max HR", + "display": "146", + "displayUnit": "Beats per Minute", + "value": "146.0", + "unitAbbr": "bpm", + "withUnit": "146 Beats per Minute", + "withUnitAbbr": "146 bpm", + "uom": "bpm", + "percentMax": { + "fieldDisplayName": "Max HR", + "display": "79", + "displayUnit": "Percent of Max", + "value": "78.91891891891892", + "unitAbbr": "% of Max", + "withUnit": "79 Percent of Max", + "withUnitAbbr": "79 % of Max", + "uom": "bpm" + }, + "zones": { + "fieldDisplayName": "Max HR", + "display": "3.9", + "displayUnit": "Zones", + "value": "3.8947368421052633", + "unitAbbr": "z", + "withUnit": "3.9 Zones", + "withUnitAbbr": "3.9 z", + "uom": "bpm" + } + }, + "WeightedMeanHeartRate": { + "fieldDisplayName": "Avg HR", + "display": "110", + "displayUnit": "Beats per Minute", + "value": "110.0", + "unitAbbr": "bpm", + "withUnit": "110 Beats per Minute", + "withUnitAbbr": "110 bpm", + "uom": "bpm", + "percentMax": { + "fieldDisplayName": "Avg HR", + "display": "59", + "displayUnit": "Percent of Max", + "value": "59.45945945945946", + "unitAbbr": "% of Max", + "withUnit": "59 Percent of Max", + "withUnitAbbr": "59 % of Max", + "uom": "bpm" + }, + "zones": { + "fieldDisplayName": "Avg HR", + "display": "1.9", + "displayUnit": "Zones", + "value": "1.9473684210526314", + "unitAbbr": "z", + "withUnit": "1.9 Zones", + "withUnitAbbr": "1.9 z", + "uom": "bpm" + } + }, + "MinSpeed": { + "fieldDisplayName": "Min Speed", + "display": "0.3", + "displayUnit": "Kilometers per Hour", + "value": "0.30960001051425934", + "unitAbbr": "km/h", + "withUnit": "0.3 Kilometers per Hour", + "withUnitAbbr": "0.3 km/h", + "uom": "kph" + }, + "MinPace": { + "fieldDisplayName": "MinPace", + "display": "--:--", + "displayUnit": "Minutes per Kilometer", + "value": "-Infinity", + "unitAbbr": "min/km", + "withUnit": "--:-- Minutes per Kilometer", + "withUnitAbbr": "--:-- min/km", + "uom": "minperkm" + }, + "MaxSpeed": { + "fieldDisplayName": "Max Speed", + "display": "37.8", + "displayUnit": "Kilometers per Hour", + "value": "37.84320030212403", + "unitAbbr": "km/h", + "withUnit": "37.8 Kilometers per Hour", + "withUnitAbbr": "37.8 km/h", + "uom": "kph" + }, + "MaxPace": { + "fieldDisplayName": "Best Pace", + "display": "01:35", + "displayUnit": "Minutes per Kilometer", + "value": "1.5854895865303542", + "unitAbbr": "min/km", + "withUnit": "01:35 Minutes per Kilometer", + "withUnitAbbr": "01:35 min/km", + "uom": "minperkm" + }, + "SumEnergy": { + "fieldDisplayName": "Calories", + "display": "319", + "displayUnit": "Calories", + "value": "319.0915556488991", + "unitAbbr": "C", + "withUnit": "319 Calories", + "withUnitAbbr": "319 C", + "uom": "kilocalorie" + }, + "SumElapsedDuration": { + "fieldDisplayName": "Elapsed Time", + "display": "00:43:47", + "displayUnit": "Hours:Minutes:Seconds", + "value": "2627.38", + "unitAbbr": "h:m:s", + "withUnit": "00:43:47 Hours:Minutes:Seconds", + "withUnitAbbr": "00:43:47 h:m:s", + "uom": "second" + }, + "MaxRunCadence": { + "fieldDisplayName": "Max Run Cadence", + "display": "110", + "displayUnit": "Steps per Minute", + "value": "110.0", + "unitAbbr": "spm", + "withUnit": "110 Steps per Minute", + "withUnitAbbr": "110 spm", + "uom": "stepsPerMinute" + }, + "MaxDoubleCadence": { + "fieldDisplayName": "Max Run Cadence", + "display": "220", + "displayUnit": "Steps per Minute", + "value": "220.0", + "unitAbbr": "spm", + "withUnit": "220 Steps per Minute", + "withUnitAbbr": "220 spm", + "uom": "stepsPerMinute" + }, + "WeightedMeanRunCadence": { + "fieldDisplayName": "Avg Run Cadence", + "display": "17", + "displayUnit": "Steps per Minute", + "value": "17.0", + "unitAbbr": "spm", + "withUnit": "17 Steps per Minute", + "withUnitAbbr": "17 spm", + "uom": "stepsPerMinute" + }, + "WeightedMeanDoubleCadence": { + "fieldDisplayName": "Avg Run Cadence", + "display": "35", + "displayUnit": "Steps per Minute", + "value": "34.765625", + "unitAbbr": "spm", + "withUnit": "35 Steps per Minute", + "withUnitAbbr": "35 spm", + "uom": "stepsPerMinute" + }, + "BeginLatitude": { + "fieldDisplayName": "Beginning Latitude", + "display": "46.4668", + "displayUnit": "Decimal Degrees", + "value": "46.46675166673958", + "unitAbbr": "dd", + "withUnit": "46.4668 Decimal Degrees", + "withUnitAbbr": "46.4668 dd", + "uom": "dd" + }, + "SumMovingDuration": { + "fieldDisplayName": "Moving Time", + "display": "00:43:34", + "displayUnit": "Hours:Minutes:Seconds", + "value": "2614.0", + "unitAbbr": "h:m:s", + "withUnit": "00:43:34 Hours:Minutes:Seconds", + "withUnitAbbr": "00:43:34 h:m:s", + "uom": "second" + }, + "WeightedMeanSpeed": { + "fieldDisplayName": "Avg Speed", + "display": "11.3", + "displayUnit": "Kilometers per Hour", + "value": "11.271600151062014", + "unitAbbr": "km/h", + "withUnit": "11.3 Kilometers per Hour", + "withUnitAbbr": "11.3 km/h", + "uom": "kph" + }, + "WeightedMeanPace": { + "fieldDisplayName": "Avg Pace", + "display": "05:19", + "displayUnit": "Minutes per Kilometer", + "value": "5.323112885116562", + "unitAbbr": "min/km", + "withUnit": "05:19 Minutes per Kilometer", + "withUnitAbbr": "05:19 min/km", + "uom": "minperkm" + }, + "SumDuration": { + "fieldDisplayName": "Time", + "display": "00:43:47", + "displayUnit": "Hours:Minutes:Seconds", + "value": "2627.38", + "unitAbbr": "h:m:s", + "withUnit": "00:43:47 Hours:Minutes:Seconds", + "withUnitAbbr": "00:43:47 h:m:s", + "uom": "second" + }, + "SumDistance": { + "fieldDisplayName": "Distance", + "display": "8.23", + "displayUnit": "Kilometers", + "value": "8.22553", + "unitAbbr": "km", + "withUnit": "8.23 Kilometers", + "withUnitAbbr": "8.23 km", + "uom": "kilometer" + }, + "BeginLongitude": { + "fieldDisplayName": "Beginning Longitude", + "display": "8.2435", + "displayUnit": "Decimal Degrees", + "value": "8.243478052318096", + "unitAbbr": "dd", + "withUnit": "8.2435 Decimal Degrees", + "withUnitAbbr": "8.2435 dd", + "uom": "dd" + }, + "EndTrainingEffectIndices": { + "fieldDisplayName": "EndTrainingEffectIndices", + "display": "0.00", + "displayUnit": "", + "value": "0.0", + "unitAbbr": "", + "withUnit": "0.00 ", + "withUnitAbbr": "0.00 ", + "uom": "dimensionless" + }, + "SumSampleCountTimestamp": { + "fieldDisplayName": "SumSampleCountTimestamp", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "BeginTrainingEffectIndices": { + "fieldDisplayName": "BeginTrainingEffectIndices", + "display": "0.00", + "displayUnit": "", + "value": "0.0", + "unitAbbr": "", + "withUnit": "0.00 ", + "withUnitAbbr": "0.00 ", + "uom": "dimensionless" + }, + "MaxCorrectedElevation": { + "fieldDisplayName": "MaxCorrectedElevation", + "display": "135,848", + "displayUnit": "Centimeters", + "value": "135848.0", + "unitAbbr": "cm", + "withUnit": "135,848 Centimeters", + "withUnitAbbr": "135,848 cm", + "uom": "centimeter" + }, + "MinCorrectedElevation": { + "fieldDisplayName": "MinCorrectedElevation", + "display": "130,817", + "displayUnit": "Centimeters", + "value": "130817.0", + "unitAbbr": "cm", + "withUnit": "130,817 Centimeters", + "withUnitAbbr": "130,817 cm", + "uom": "centimeter" + }, + "MinHeartRate": { + "fieldDisplayName": "MinHeartRate", + "display": "83", + "displayUnit": "Beats per Minute", + "value": "83.0", + "unitAbbr": "bpm", + "withUnit": "83 Beats per Minute", + "withUnitAbbr": "83 bpm", + "uom": "bpm", + "percentMax": { + "fieldDisplayName": "MinHeartRate", + "display": "45", + "displayUnit": "Percent of Max", + "value": "44.86486486486487", + "unitAbbr": "% of Max", + "withUnit": "45 Percent of Max", + "withUnitAbbr": "45 % of Max", + "uom": "bpm" + }, + "zones": { + "fieldDisplayName": "MinHeartRate", + "display": "0.9", + "displayUnit": "Zones", + "value": "0.9021739130434783", + "unitAbbr": "z", + "withUnit": "0.9 Zones", + "withUnitAbbr": "0.9 z", + "uom": "bpm" + } + }, + "MaxUncorrectedElevation": { + "fieldDisplayName": "MaxUncorrectedElevation", + "display": "137,480", + "displayUnit": "Centimeters", + "value": "137480.0", + "unitAbbr": "cm", + "withUnit": "137,480 Centimeters", + "withUnitAbbr": "137,480 cm", + "uom": "centimeter" + }, + "MinUncorrectedElevation": { + "fieldDisplayName": "MinUncorrectedElevation", + "display": "132,600", + "displayUnit": "Centimeters", + "value": "132600.0", + "unitAbbr": "cm", + "withUnit": "132,600 Centimeters", + "withUnitAbbr": "132,600 cm", + "uom": "centimeter" + }, + "LossCorrectedElevation": { + "fieldDisplayName": "LossCorrectedElevation", + "display": "5,207", + "displayUnit": "Centimeters", + "value": "5207.0", + "unitAbbr": "cm", + "withUnit": "5,207 Centimeters", + "withUnitAbbr": "5,207 cm", + "uom": "centimeter" + }, + "GainElevation": { + "fieldDisplayName": "Elevation Gain", + "display": "106", + "displayUnit": "Meters", + "value": "106.0", + "unitAbbr": "m", + "withUnit": "106 Meters", + "withUnitAbbr": "106 m", + "uom": "meter" + }, + "SumStep": { + "fieldDisplayName": "Steps", + "display": "3,522", + "displayUnit": "Steps", + "value": "3522.0", + "unitAbbr": "s", + "withUnit": "3,522 Steps", + "withUnitAbbr": "3,522 s", + "uom": "step" + }, + "SumSampleCountLongitude": { + "fieldDisplayName": "SumSampleCountLongitude", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "BeginTimestamp": { + "fieldDisplayName": "Start", + "display": "Thu, 8 Mar 2018 12:23", + "displayUnit": "", + "value": "2018-03-08T11:23:22.000Z", + "unitAbbr": "Central European Time", + "withUnit": "Thu, 8 Mar 2018 12:23 ", + "withUnitAbbr": "Thu, 8 Mar 2018 12:23 Central European Time", + "uom": "Europe/Paris" + }, + "EndTimestamp": { + "fieldDisplayName": "EndTimestamp", + "display": "Thu, 8 Mar 2018 13:07", + "displayUnit": "", + "value": "2018-03-08T12:07:09.000Z", + "unitAbbr": "Central European Time", + "withUnit": "Thu, 8 Mar 2018 13:07 ", + "withUnitAbbr": "Thu, 8 Mar 2018 13:07 Central European Time", + "uom": "Europe/Paris" + }, + "SumSampleCountLatitude": { + "fieldDisplayName": "SumSampleCountLatitude", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "LossElevation": { + "fieldDisplayName": "Elevation Loss", + "display": "73", + "displayUnit": "Meters", + "value": "73.0", + "unitAbbr": "m", + "withUnit": "73 Meters", + "withUnitAbbr": "73 m", + "uom": "meter" + }, + "SumSampleCountAirTemperature": { + "fieldDisplayName": "SumSampleCountAirTemperature", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "SumAnaerobicTrainingEffect": { + "fieldDisplayName": "SumAnaerobicTrainingEffect", + "display": "0.10", + "displayUnit": "", + "value": "0.10000000149011612", + "unitAbbr": "", + "withUnit": "0.10 ", + "withUnitAbbr": "0.10 ", + "uom": "dimensionless" + }, + "SumSampleCountMovingDuration": { + "fieldDisplayName": "SumSampleCountMovingDuration", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "MaxVerticalSpeed": { + "fieldDisplayName": "MaxVerticalSpeed", + "display": "0.60", + "displayUnit": "Meters per Second", + "value": "0.5999755859375", + "unitAbbr": "mps", + "withUnit": "0.60 Meters per Second", + "withUnitAbbr": "0.60 mps", + "uom": "mps" + }, + "WeightedMeanStrideLength": { + "fieldDisplayName": "Avg Stride Length", + "display": "6.15", + "displayUnit": "Meters", + "value": "6.151658983791259", + "unitAbbr": "m", + "withUnit": "6.15 Meters", + "withUnitAbbr": "6.15 m", + "uom": "meter" + }, + "SumSampleCountElapsedDuration": { + "fieldDisplayName": "SumSampleCountElapsedDuration", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "MaxElevation": { + "fieldDisplayName": "Max Elevation", + "display": "1,375", + "displayUnit": "Meters", + "value": "1374.8", + "unitAbbr": "m", + "withUnit": "1,375 Meters", + "withUnitAbbr": "1,375 m", + "uom": "meter" + }, + "SumSampleCountMovingSpeed": { + "fieldDisplayName": "SumSampleCountMovingSpeed", + "display": "922", + "displayUnit": "Samples", + "value": "922.0", + "unitAbbr": "Samples", + "withUnit": "922 Samples", + "withUnitAbbr": "922 Samples", + "uom": "sampleCount" + }, + "MinAirTemperature": { + "fieldDisplayName": "Min Temperature", + "display": "21.0", + "displayUnit": "", + "value": "21.0", + "unitAbbr": "°C", + "withUnit": "21.0 ", + "withUnitAbbr": "21.0 °C", + "uom": "celcius" + }, + "MaxFractionalCadence": { + "fieldDisplayName": "Max Run Cadence", + "display": "0.00", + "displayUnit": "Revolutions per Minute", + "value": "0.0", + "unitAbbr": "rpm", + "withUnit": "0.00 Revolutions per Minute", + "withUnitAbbr": "0.00 rpm", + "uom": "rpm" + }, + "WeightedMeanFractionalCadence": { + "fieldDisplayName": "Avg Run Cadence", + "display": "0.38", + "displayUnit": "Revolutions per Minute", + "value": "0.3828125", + "unitAbbr": "rpm", + "withUnit": "0.38 Revolutions per Minute", + "withUnitAbbr": "0.38 rpm", + "uom": "rpm" + }, + "MaxAirTemperature": { + "fieldDisplayName": "Max Temperature", + "display": "28.0", + "displayUnit": "", + "value": "28.0", + "unitAbbr": "°C", + "withUnit": "28.0 ", + "withUnitAbbr": "28.0 °C", + "uom": "celcius" + }, + "EndLatitude": { + "fieldDisplayName": "EndLatitude", + "display": "46.4699", + "displayUnit": "Decimal Degrees", + "value": "46.469942070543766", + "unitAbbr": "dd", + "withUnit": "46.4699 Decimal Degrees", + "withUnitAbbr": "46.4699 dd", + "uom": "dd" + }, + "SumTrainingEffect": { + "fieldDisplayName": "Training Effect", + "display": "1.40", + "displayUnit": "", + "value": "1.399999976158142", + "unitAbbr": "", + "withUnit": "1.40 ", + "withUnitAbbr": "1.40 ", + "uom": "dimensionless" + }, + "WeightedMeanAirTemperature": { + "fieldDisplayName": "Avg Temperature", + "display": "24.8", + "displayUnit": "", + "value": "24.846973734297677", + "unitAbbr": "°C", + "withUnit": "24.8 ", + "withUnitAbbr": "24.8 °C", + "uom": "celcius" + }, + "SumSampleCountDoubleCadence": { + "fieldDisplayName": "SumSampleCountDoubleCadence", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "SumSampleCountFractionalCadence": { + "fieldDisplayName": "SumSampleCountFractionalCadence", + "display": "926", + "displayUnit": "Samples", + "value": "926.0", + "unitAbbr": "Samples", + "withUnit": "926 Samples", + "withUnitAbbr": "926 Samples", + "uom": "sampleCount" + }, + "MinElevation": { + "fieldDisplayName": "Min Elevation", + "display": "1,326", + "displayUnit": "Meters", + "value": "1326.0", + "unitAbbr": "m", + "withUnit": "1,326 Meters", + "withUnitAbbr": "1,326 m", + "uom": "meter" + } + }, + "totalLaps": {}, + "garminSwimAlgorithm": false, + "updatedDate": { + "@class": "sql-timestamp", + "$": "2018-03-08 11:35:20.0" + }, + "updatedDateFormatted": "2018-03-08 11:35:20.0 GMT", + "userRoles": [ + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_OUTDOOR_USER", + "ROLE_CONNECT_2_USER" + ] + } + } + ], + "totalFound": 861, + "currentPage": 1, + "totalPages": 87, + "query": { + "filters": { + "parent_id": "", + "parent_id_oper": "is", + "userId": "2836200", + "userId_oper": "=" + }, + "sortOrder": "DESC", + "sortField": "activitySummaryBeginTimestampGmt", + "activityStart": "0", + "activitiesPerPage": "10", + "explore": "false", + "ignoreUntitled": "false", + "ignoreNonGPS": "false" + } +}} diff --git a/garmin-connect-export/json/activity_154105348_gpx_device_null.json b/garmin-connect-export/json/activity_154105348_gpx_device_null.json new file mode 100644 index 0000000..eee33f9 --- /dev/null +++ b/garmin-connect-export/json/activity_154105348_gpx_device_null.json @@ -0,0 +1,94 @@ +{ + "activityId": 154105348, + "activityName": "Stechelberg-Lauterbrunnen", + "userProfileId": 2836200, + "isMultiSportParent": false, + "activityTypeDTO": { + "typeId": 81, + "typeKey": "cross_country_skiing", + "parentTypeId": 4, + "sortOrder": 39 + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 1, + "typeKey": "public" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 80, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 2, + "formatKey": "gpx" + }, + "associatedCourseId": null, + "lastUpdateDate": "2012-03-02T11:09:30.0", + "uploadedDate": "2012-03-02T10:49:45.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": false, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": { + "showElevation": true, + "showMovingSpeed": true, + "showSpeed": true, + "showTimestamp": true + }, + "childIds": [], + "sensors": null, + "activityImages": [], + "manufacturer": "", + "diveNumber": null, + "lapCount": 1, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": null, + "deviceTypePk": 19, + "deviceVersionPk": 80 + }, + "gcj02": false, + "favorite": false, + "autoCalcCalories": false, + "elevationCorrected": false, + "manualActivity": false, + "personalRecord": false + }, + "summaryDTO": { + "startTimeLocal": "2012-02-27T10:10:06.0", + "startTimeGMT": "2012-02-27T09:10:06.0", + "distance": 11610.0, + "duration": 4177.0, + "movingDuration": 3887.0, + "elapsedDuration": 4177.0, + "elevationGain": 137.0, + "elevationLoss": 138.0, + "maxElevation": 907.91, + "minElevation": 793.9, + "averageSpeed": 2.7777777777777777, + "averageMovingSpeed": 2.986575184492859, + "maxSpeed": 15.722222222222221 + } +} diff --git a/garmin-connect-export/json/activity_2541953812.json b/garmin-connect-export/json/activity_2541953812.json new file mode 100644 index 0000000..ae15a14 --- /dev/null +++ b/garmin-connect-export/json/activity_2541953812.json @@ -0,0 +1,148 @@ +{ + "activityId": 2541953812, + "activityName": "Reckingen Augenstern-Ulrichen Langlauf", + "userProfileId": 2836200, + "isMultiSportParent": false, + "activityTypeDTO": { + "typeId": 81, + "typeKey": "cross_country_skiing", + "parentTypeId": 4, + "sortOrder": 39 + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 3, + "typeKey": "subscribers" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 845288, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 7, + "formatKey": "fit" + }, + "associatedCourseId": null, + "lastUpdateDate": "2018-03-08T11:35:20.0", + "uploadedDate": "2018-03-08T06:09:54.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": true, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": { + "showAirTemperature": true, + "showDistance": true, + "showDuration": true, + "showElevation": true, + "showHeartRate": true, + "showMovingDuration": true, + "showMovingSpeed": true, + "showSpeed": true, + "showTimestamp": true + }, + "childIds": [], + "sensors": [ + { + "sku": "006-B2697-00", + "sourceType": "LOCAL", + "softwareVersion": 8.0 + }, + { + "sku": "006-B2697-00", + "sourceType": "LOCAL", + "localDeviceType": "BAROMETER", + "softwareVersion": 8.0 + }, + { + "sku": "006-B1621-00", + "sourceType": "LOCAL", + "localDeviceType": "GPS", + "softwareVersion": 4.3 + }, + { + "sourceType": "LOCAL", + "localDeviceType": "ACCELEROMETER" + }, + { + "sku": "006-B0000-00", + "sourceType": "LOCAL", + "localDeviceType": "BLUETOOTH_LOW_ENERGY_CHIPSET", + "softwareVersion": 606.72 + }, + { + "sourceType": "LOCAL", + "localDeviceType": "WHR", + "softwareVersion": 3.23 + } + ], + "activityImages": [], + "manufacturer": "GARMIN", + "diveNumber": null, + "lapCount": 9, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": "3946806421", + "deviceTypePk": 34236, + "deviceVersionPk": 845288 + }, + "gcj02": false, + "favorite": false, + "autoCalcCalories": false, + "elevationCorrected": false, + "manualActivity": false, + "personalRecord": false + }, + "summaryDTO": { + "startTimeLocal": "2018-03-08T12:23:22.0", + "startTimeGMT": "2018-03-08T11:23:22.0", + "startLatitude": 46.46675166673958, + "startLongitude": 8.243478052318096, + "distance": 8225.53, + "duration": 2627.38, + "movingDuration": 2614.0, + "elapsedDuration": 2627.38, + "elevationGain": 106.0, + "elevationLoss": 73.0, + "maxElevation": 1374.8, + "minElevation": 1326.0, + "averageSpeed": 3.13100004196167, + "averageMovingSpeed": 3.146721604222456, + "maxSpeed": 10.51200008392334, + "calories": 319.0915556488991, + "averageHR": 110.0, + "maxHR": 146.0, + "averageTemperature": 24.846973734297677, + "maxTemperature": 28.0, + "minTemperature": 21.0, + "trainingEffect": 1.399999976158142, + "anaerobicTrainingEffect": 0.10000000149011612, + "aerobicTrainingEffectMessage": "MINOR_AEROBIC_BENEFIT_0", + "anaerobicTrainingEffectMessage": "NO_ANAEROBIC_BENEFIT_0", + "endLatitude": 46.469942070543766, + "endLongitude": 8.248067228123546, + "maxVerticalSpeed": 0.5999755859375 + }, + "locationName": "Goms" +} diff --git a/garmin-connect-export/json/activity_2541953812_overview.json b/garmin-connect-export/json/activity_2541953812_overview.json new file mode 100644 index 0000000..261bd31 --- /dev/null +++ b/garmin-connect-export/json/activity_2541953812_overview.json @@ -0,0 +1,210 @@ +{ + "activityId": 2541953812, + "activityName": "Reckingen Augenstern-Ulrichen Langlauf", + "description": null, + "startTimeLocal": "2018-03-08 12:23:22", + "startTimeGMT": "2018-03-08 11:23:22", + "activityType": { + "typeId": 171, + "typeKey": "cross_country_skiing_ws", + "parentTypeId": 165, + "sortOrder": 101, + "isHidden": false, + "restricted": false + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 8225.5302734375, + "duration": 2627.3798828125, + "elapsedDuration": 2627379.8828125, + "movingDuration": 2614.0, + "elevationGain": 106.0, + "elevationLoss": 73.0, + "averageSpeed": 3.13100004196167, + "maxSpeed": 10.51200008392334, + "startLatitude": 46.46675166673958, + "startLongitude": 8.243478052318096, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/5979a7d6-2502-4869-8f57-86a28ab7eab3-2836200.png", + "calories": 319.0, + "averageHR": 110.0, + "maxHR": 146.0, + "averageRunningCadenceInStepsPerMinute": 34.765625, + "maxRunningCadenceInStepsPerMinute": 220.0, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": 3522, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "activityLikeProfileImageUrls": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_OUTDOOR_USER", + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 3, + "typeKey": "subscribers" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": 1520508202000, + "sportTypeId": 0, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 1.399999976158142, + "anaerobicTrainingEffect": 0.10000000149011612, + "strokes": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": 615.1658983791259, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": null, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": 244.0, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": 21.0, + "maxTemperature": null, + "minElevation": 132600.0, + "maxElevation": 137480.0048828125, + "avgDoubleCadence": null, + "maxDoubleCadence": 220.0, + "summarizedExerciseSets": null, + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": 0 + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": 0.5999755859375, + "floorsClimbed": null, + "floorsDescended": null, + "manufacturer": null, + "diveNumber": null, + "locationName": "Goms", + "bottomTime": null, + "lapCount": 9, + "endLatitude": 46.469942070543766, + "endLongitude": 8.248067228123546, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "flow": null, + "grit": null, + "jumpCount": null, + "caloriesEstimated": null, + "caloriesConsumed": null, + "waterEstimated": null, + "waterConsumed": null, + "maxAvgPower_1": null, + "maxAvgPower_2": null, + "maxAvgPower_5": null, + "maxAvgPower_10": null, + "maxAvgPower_20": null, + "maxAvgPower_30": null, + "maxAvgPower_60": null, + "maxAvgPower_120": null, + "maxAvgPower_300": null, + "maxAvgPower_600": null, + "maxAvgPower_1200": null, + "maxAvgPower_1800": null, + "maxAvgPower_3600": null, + "maxAvgPower_7200": null, + "maxAvgPower_18000": null, + "excludeFromPowerCurveReports": null, + "totalSets": null, + "activeSets": null, + "totalReps": null, + "minRespirationRate": null, + "maxRespirationRate": null, + "avgRespirationRate": null, + "trainingEffectLabel": null, + "activityTrainingLoad": null, + "avgFlow": null, + "avgGrit": null, + "minActivityLapDuration": null, + "avgStress": null, + "startStress": null, + "endStress": null, + "differenceStress": null, + "maxStress": null, + "aerobicTrainingEffectMessage": null, + "anaerobicTrainingEffectMessage": null, + "splitSummaries": [], + "hasSplits": null, + "hasSeedFirstbeatProfile": null, + "favorite": false, + "decoDive": null, + "purposeful": false, + "pr": false, + "autoCalcCalories": false, + "parent": false, + "atpActivity": null, + "manualActivity": false, + "elevationCorrected": false +} diff --git a/garmin-connect-export/json/activity_2541953812_samples.json b/garmin-connect-export/json/activity_2541953812_samples.json new file mode 100644 index 0000000..86c1daf --- /dev/null +++ b/garmin-connect-export/json/activity_2541953812_samples.json @@ -0,0 +1,5525 @@ +{ + "com.garmin.activity.details.json.ActivityDetails": { + "activityId": 2541953812, + "isDetailsAvailable": true, + "measurementCount": 18, + "metricsCount": 244, + "measurements": [ + { + "metricsIndex": 6, + "key": "directHeartRate", + "display": "Herzfrequenz", + "unit": "bpm", + "unitAbbr": "bpm", + "unitDisplay": "Schlag pro Minute" + }, + { + "metricsIndex": 9, + "key": "directUncorrectedElevation", + "display": "directUncorrectedElevation", + "unit": "centimeter", + "unitAbbr": "cm", + "unitDisplay": "Zentimeter" + }, + { + "metricsIndex": 11, + "key": "directPace", + "display": "Pace", + "unit": "minperkm", + "unitAbbr": "min/km", + "unitDisplay": "Minute pro Kilometer" + }, + { + "metricsIndex": 8, + "key": "directHeartRatePercentMax", + "display": "Herzfrequenz", + "unit": "percentMax", + "unitAbbr": "% max. Herzfrequenz", + "unitDisplay": "Prozentsatz von max." + }, + { + "metricsIndex": 0, + "key": "sumElapsedDuration", + "display": "Laufzeit", + "unit": "second", + "unitAbbr": "s", + "unitDisplay": "Sekunde" + }, + { + "metricsIndex": 15, + "key": "directFractionalCadence", + "display": "directFractionalCadence", + "unit": "rpm", + "unitAbbr": "1/min", + "unitDisplay": "Umdrehung pro Minute" + }, + { + "metricsIndex": 7, + "key": "directHeartRateZone", + "display": "Herzfrequenz", + "unit": "zones", + "unitAbbr": "z", + "unitDisplay": "Bereich" + }, + { + "metricsIndex": 12, + "key": "directElevation", + "display": "Höhe", + "unit": "meter", + "unitAbbr": "m", + "unitDisplay": "Meter" + }, + { + "metricsIndex": 14, + "key": "directLongitude", + "display": "Längengrad", + "unit": "dd", + "unitAbbr": "dd", + "unitDisplay": "Dezimalgrad" + }, + { + "metricsIndex": 16, + "key": "directLatitude", + "display": "Breitengrad", + "unit": "dd", + "unitAbbr": "dd", + "unitDisplay": "Dezimalgrad" + }, + { + "metricsIndex": 4, + "key": "sumDuration", + "display": "Zeit", + "unit": "second", + "unitAbbr": "s", + "unitDisplay": "Sekunde" + }, + { + "metricsIndex": 17, + "key": "directVerticalSpeed", + "display": "directVerticalSpeed", + "unit": "mps", + "unitAbbr": "mps", + "unitDisplay": "Meter pro Sekunde" + }, + { + "metricsIndex": 5, + "key": "directCorrectedElevation", + "display": "directCorrectedElevation", + "unit": "centimeter", + "unitAbbr": "cm", + "unitDisplay": "Zentimeter" + }, + { + "metricsIndex": 10, + "key": "directSpeed", + "display": "Geschwindigkeit", + "unit": "kph", + "unitAbbr": "km/h", + "unitDisplay": "Kilometer pro Stunde" + }, + { + "metricsIndex": 2, + "key": "directAirTemperature", + "display": "Temperatur", + "unit": "celcius", + "unitAbbr": "°C", + "unitDisplay": "°Celsius" + }, + { + "metricsIndex": 1, + "key": "sumDistance", + "display": "Distanz", + "unit": "kilometer", + "unitAbbr": "km", + "unitDisplay": "Kilometer" + }, + { + "metricsIndex": 3, + "key": "directTimestamp", + "display": "Uhrzeit", + "unit": "gmt", + "unitAbbr": "Greenwich Mean Time", + "unitDisplay": "(GMT) Greenwich Mean Time" + }, + { + "metricsIndex": 13, + "key": "sumMovingDuration", + "display": "sumMovingDuration", + "unit": "second", + "unitAbbr": "s", + "unitDisplay": "Sekunde" + } + ], + "metrics": [ + { + "metrics": [ + 0.0, + 0.0, + 28.0, + 1.520508202E+12, + 0.0, + 131600.0, + 84.0, + 0.8936170212765957, + 44.919786096256686, + 133059.99755859375, + 2.127600073814392, + 18.641135770848248, + 1330.5999755859375, + 0.0, + 8.243478052318096, + 0.0, + 46.46675166673958, + 0.0 + ] + }, + { + "metrics": [ + 1.0, + 0.0, + 28.0, + 1.520508203E+12, + 1.0, + 131600.0, + 84.0, + 0.8936170212765957, + 44.919786096256686, + 133019.9951171875, + 1.0943999648094178, + 18.641135770848248, + 1330.199951171875, + 1.0, + 8.24347821995616, + 0.0, + 46.46675108000636, + -0.4000000059604645 + ] + }, + { + "metrics": [ + 6.0, + 0.0006299999952316285, + 28.0, + 1.520508208E+12, + 6.0, + 131600.0, + 88.0, + 0.9361702127659575, + 47.05882352941177, + 133000.0, + 0.30960001051425934, + 18.641135770848248, + 1330.0, + 1.0, + 8.243482410907745, + 0.0, + 46.466747811064124, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 13.0, + 0.002819999933242798, + 28.0, + 1.520508215E+12, + 13.0, + 131580.0048828125, + 86.0, + 0.9148936170212766, + 45.98930481283423, + 132959.99755859375, + 0.5759999871253968, + 18.641135770848248, + 1329.5999755859375, + 1.0, + 8.243499174714088, + 0.0, + 46.46673289127648, + -0.05700000002980232 + ] + }, + { + "metrics": [ + 23.0, + 0.018700000762939453, + 28.0, + 1.520508225E+12, + 23.0, + 131559.99755859375, + 91.0, + 0.9680851063829787, + 48.663101604278076, + 132959.99755859375, + 11.120399951934814, + 5.3954893952857095, + 1329.5999755859375, + 10.0, + 8.243576036766171, + 0.0, + 46.4666142873466, + 0.0 + ] + }, + { + "metrics": [ + 42.0, + 0.08143000030517578, + 28.0, + 1.520508244E+12, + 42.0, + 131419.9951171875, + 94.0, + 1.0, + 50.26737967914438, + 133040.00244140625, + 11.58840036392212, + 5.177591222926377, + 1330.4000244140625, + 29.0, + 8.243544101715088, + 0.0, + 46.46605085581541, + -0.07999999821186066 + ] + }, + { + "metrics": [ + 61.0, + 0.15202000427246093, + 28.0, + 1.520508263E+12, + 61.0, + 131180.0048828125, + 94.0, + 1.0, + 50.26737967914438, + 133040.00244140625, + 12.830399608612062, + 4.676393708869879, + 1330.4000244140625, + 48.0, + 8.243520632386208, + 0.0, + 46.465416345745325, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 70.0, + 0.19061000061035155, + 28.0, + 1.520508272E+12, + 70.0, + 131019.9951171875, + 94.0, + 1.0, + 50.26737967914438, + 132900.0, + 16.22519989013672, + 3.69795136073941, + 1329.0, + 57.0, + 8.24348752386868, + 0.5, + 46.465070927515626, + -0.30000001192092896 + ] + }, + { + "metrics": [ + 78.0, + 0.22642999267578126, + 28.0, + 1.52050828E+12, + 78.0, + 130859.99755859375, + 94.0, + 1.0, + 50.26737967914438, + 132719.9951171875, + 15.552000617980958, + 3.858024538825508, + 1327.199951171875, + 65.0, + 8.243605121970177, + 0.0, + 46.464761048555374, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 81.0, + 0.2398699951171875, + 28.0, + 1.520508283E+12, + 81.0, + 130819.9951171875, + 93.0, + 0.9893617021276596, + 49.73262032085562, + 132619.9951171875, + 13.535999965667726, + 4.432624125604468, + 1326.199951171875, + 68.0, + 8.243715595453978, + 0.0, + 46.464670188724995, + -0.4000000059604645 + ] + }, + { + "metrics": [ + 82.0, + 0.2430800018310547, + 28.0, + 1.520508284E+12, + 82.0, + 130819.9951171875, + 93.0, + 0.9893617021276596, + 49.73262032085562, + 132619.9951171875, + 10.54800024032593, + 5.688282010329764, + 1326.199951171875, + 69.0, + 8.24375188909471, + 0.0, + 46.46465543657541, + 0.0 + ] + }, + { + "metrics": [ + 83.0, + 0.24547000122070312, + 28.0, + 1.520508285E+12, + 83.0, + 130819.9951171875, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 132600.0, + 8.701200199127198, + 6.89560045038597, + 1326.0, + 70.0, + 8.243782985955477, + 0.0, + 46.46465719677508, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 94.0, + 0.27754998779296874, + 28.0, + 1.520508296E+12, + 94.0, + 130859.99755859375, + 89.0, + 0.9468085106382979, + 47.593582887700535, + 132740.00244140625, + 11.152800178527833, + 5.379814849325131, + 1327.4000244140625, + 81.0, + 8.244181461632252, + 0.0, + 46.46474411711097, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 112.0, + 0.33533999633789063, + 28.0, + 1.520508314E+12, + 112.0, + 131100.0, + 90.0, + 0.9574468085106383, + 48.1283422459893, + 132659.99755859375, + 12.801599979400635, + 4.686914144212244, + 1326.5999755859375, + 99.0, + 8.244199315086007, + 0.0, + 46.46523320116103, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 129.0, + 0.3856300048828125, + 28.0, + 1.520508331E+12, + 129.0, + 131300.0, + 89.0, + 0.9468085106382979, + 47.593582887700535, + 132780.0048828125, + 8.564399814605714, + 7.005744863717841, + 1327.800048828125, + 116.0, + 8.244124967604876, + 0.0, + 46.465681632980704, + 0.0 + ] + }, + { + "metrics": [ + 141.0, + 0.41775, + 28.0, + 1.520508343E+12, + 141.0, + 131359.99755859375, + 83.0, + 0.8829787234042553, + 44.38502673796791, + 132919.9951171875, + 8.43120002746582, + 7.116424686467117, + 1329.199951171875, + 128.0, + 8.24434557929635, + 0.0, + 46.465912805870175, + 0.2669999897480011 + ] + }, + { + "metrics": [ + 150.0, + 0.448489990234375, + 28.0, + 1.520508352E+12, + 150.0, + 131580.0048828125, + 86.0, + 0.9148936170212766, + 45.98930481283423, + 132980.0048828125, + 8.262000274658204, + 7.262163884941554, + 1329.800048828125, + 137.0, + 8.24467146769166, + 0.0, + 46.46606435067952, + 0.0 + ] + }, + { + "metrics": [ + 163.0, + 0.4841600036621094, + 28.0, + 1.520508365E+12, + 163.0, + 131759.99755859375, + 83.0, + 0.8829787234042553, + 44.38502673796791, + 133159.99755859375, + 5.147999811172486, + 11.655012084845952, + 1331.5999755859375, + 150.0, + 8.245067847892642, + 0.0, + 46.46622268483043, + 0.2669999897480011 + ] + }, + { + "metrics": [ + 170.0, + 0.5011900024414062, + 28.0, + 1.520508372E+12, + 170.0, + 131840.00244140625, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 133259.99755859375, + 7.595999622344972, + 7.898894549112329, + 1332.5999755859375, + 157.0, + 8.245214531198144, + 0.0, + 46.46633776836097, + 0.0 + ] + }, + { + "metrics": [ + 174.0, + 0.5081900024414062, + 28.0, + 1.520508376E+12, + 174.0, + 131859.99755859375, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 133300.0, + 4.474800109863281, + 13.408420161550678, + 1333.0, + 161.0, + 8.245287369936705, + 0.0, + 46.466375486925244, + 0.0 + ] + }, + { + "metrics": [ + 181.0, + 0.5198099975585937, + 28.0, + 1.520508383E+12, + 181.0, + 131900.0, + 110.0, + 1.8888888888888888, + 58.8235294117647, + 133380.0048828125, + 4.607999897003174, + 13.020833626975811, + 1333.800048828125, + 168.0, + 8.245392311364412, + 0.0, + 46.46644748747349, + 0.0 + ] + }, + { + "metrics": [ + 200.0, + 0.5614600219726562, + 28.0, + 1.520508402E+12, + 200.0, + 131940.00244140625, + 113.0, + 2.0526315789473686, + 60.42780748663102, + 133559.99755859375, + 7.8732001304626476, + 7.620789389037703, + 1335.5999755859375, + 187.0, + 8.245751895010471, + 0.0, + 46.46672400645912, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 213.0, + 0.5946699829101563, + 28.0, + 1.520508415E+12, + 213.0, + 131940.00244140625, + 115.0, + 2.1578947368421053, + 61.49732620320856, + 133680.0048828125, + 4.557600116729737, + 13.164823256817987, + 1336.800048828125, + 200.0, + 8.245964040979743, + 0.0, + 46.46698493510485, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 227.0, + 0.6254000244140625, + 28.0, + 1.520508429E+12, + 227.0, + 132059.99755859375, + 117.0, + 2.263157894736842, + 62.5668449197861, + 133819.9951171875, + 5.180400037765503, + 11.582117128908102, + 1338.199951171875, + 214.0, + 8.246334185823798, + 0.0, + 46.467091888189316, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 236.0, + 0.6461500244140626, + 28.0, + 1.520508438E+12, + 236.0, + 132140.00244140625, + 122.0, + 2.526315789473684, + 65.24064171122994, + 133959.99755859375, + 7.696800041198731, + 7.7954474185164555, + 1339.5999755859375, + 223.0, + 8.246572986245155, + 0.0, + 46.46719112992287, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 247.0, + 0.6721500244140625, + 28.0, + 1.520508449E+12, + 247.0, + 132219.9951171875, + 118.0, + 2.3157894736842106, + 63.101604278074866, + 134000.0, + 9.442799663543703, + 6.354047755947323, + 1340.0, + 234.0, + 8.246816396713257, + 0.0, + 46.4673429261893, + -0.13300000131130219 + ] + }, + { + "metrics": [ + 262.0, + 0.7069600219726563, + 28.0, + 1.520508464E+12, + 262.0, + 132280.0048828125, + 114.0, + 2.1052631578947367, + 60.962566844919785, + 134040.00244140625, + 6.8616000652313245, + 8.744316113092674, + 1340.4000244140625, + 249.0, + 8.246881105005741, + 0.0, + 46.46765221841633, + 0.06700000166893005 + ] + }, + { + "metrics": [ + 269.0, + 0.7235, + 28.0, + 1.520508471E+12, + 269.0, + 132340.00244140625, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 134059.99755859375, + 9.1439998626709, + 6.561679889885128, + 1340.5999755859375, + 256.0, + 8.246948244050145, + 0.0, + 46.46779320202768, + 0.0 + ] + }, + { + "metrics": [ + 287.0, + 0.7724400024414062, + 28.0, + 1.520508489E+12, + 287.0, + 132400.0, + 109.0, + 1.8333333333333335, + 58.288770053475936, + 134219.9951171875, + 9.273599910736085, + 6.469979359637649, + 1342.199951171875, + 274.0, + 8.247048407793045, + 0.0, + 46.468224953860044, + 0.0 + ] + }, + { + "metrics": [ + 302.0, + 0.8194299926757812, + 28.0, + 1.520508504E+12, + 302.0, + 132259.99755859375, + 116.0, + 2.2105263157894735, + 62.032085561497325, + 134259.99755859375, + 2.9988000154495245, + 18.641135770848248, + 1342.5999755859375, + 289.0, + 8.247152091935277, + 0.0, + 46.468642204999924, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 320.0, + 0.8694099731445313, + 28.0, + 1.520508522E+12, + 320.0, + 132080.0048828125, + 120.0, + 2.4210526315789473, + 64.1711229946524, + 134219.9951171875, + 18.140400123596194, + 3.3075345418624353, + 1342.199951171875, + 307.0, + 8.247074475511909, + 0.0, + 46.46908602677286, + -0.17100000381469727 + ] + }, + { + "metrics": [ + 326.0, + 0.9067899780273437, + 28.0, + 1.520508528E+12, + 326.0, + 132019.9951171875, + 121.0, + 2.473684210526316, + 64.70588235294117, + 133940.00244140625, + 21.86639957427979, + 2.7439359556282246, + 1339.4000244140625, + 313.0, + 8.247117139399052, + 0.0, + 46.4694154355675, + -0.4000000059604645 + ] + }, + { + "metrics": [ + 335.0, + 0.9450800170898438, + 28.0, + 1.520508537E+12, + 335.0, + 132080.0048828125, + 126.0, + 2.736842105263158, + 67.37967914438502, + 133800.0, + 13.503599739074708, + 4.443259661968574, + 1338.0, + 322.0, + 8.247473035007715, + 0.0, + 46.46964618936181, + 0.0 + ] + }, + { + "metrics": [ + 346.0, + 0.9732100219726563, + 27.0, + 1.520508548E+12, + 346.0, + 132100.0, + 126.0, + 2.736842105263158, + 67.37967914438502, + 133959.99755859375, + 10.447199821472168, + 5.743165732188044, + 1339.5999755859375, + 333.0, + 8.247740836814046, + 0.0, + 46.46981575526297, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 355.0, + 1.0022100219726562, + 27.0, + 1.520508557E+12, + 355.0, + 132080.0048828125, + 126.0, + 2.736842105263158, + 67.37967914438502, + 134000.0, + 12.326400089263919, + 4.8676012118298, + 1340.0, + 342.0, + 8.248067228123546, + 0.0, + 46.469942070543766, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 365.0, + 1.0278299560546875, + 27.0, + 1.520508567E+12, + 365.0, + 132059.99755859375, + 126.0, + 2.736842105263158, + 67.37967914438502, + 134100.0, + 8.701200199127198, + 6.89560045038597, + 1341.0, + 352.0, + 8.248383142054081, + 0.0, + 46.46998079493642, + 0.06700000166893005 + ] + }, + { + "metrics": [ + 372.0, + 1.04406005859375, + 27.0, + 1.520508574E+12, + 372.0, + 132100.0, + 129.0, + 2.8947368421052633, + 68.98395721925134, + 134180.0048828125, + 8.330399608612062, + 7.202535632260826, + 1341.800048828125, + 359.0, + 8.248565951362252, + 0.0, + 46.46992010995746, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 379.0, + 1.0582900390625, + 27.0, + 1.520508581E+12, + 379.0, + 132140.00244140625, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 134259.99755859375, + 9.107999897003175, + 6.587615359080311, + 1342.5999755859375, + 366.0, + 8.248744988813996, + 0.0, + 46.46989689208567, + 0.0 + ] + }, + { + "metrics": [ + 390.0, + 1.096949951171875, + 27.0, + 1.520508592E+12, + 390.0, + 132300.0, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 134300.0, + 10.281599807739259, + 5.835667710664664, + 1343.0, + 377.0, + 8.249222924932837, + 0.0, + 46.469811564311385, + 0.0 + ] + }, + { + "metrics": [ + 397.0, + 1.114050048828125, + 27.0, + 1.520508599E+12, + 397.0, + 132359.99755859375, + 136.0, + 3.263157894736842, + 72.72727272727273, + 134319.9951171875, + 9.946800041198731, + 6.032090698866522, + 1343.199951171875, + 384.0, + 8.249437920749187, + 0.0, + 46.46977862343192, + 0.0 + ] + }, + { + "metrics": [ + 403.0, + 1.128, + 27.0, + 1.520508605E+12, + 403.0, + 132400.0, + 140.0, + 3.473684210526316, + 74.8663101604278, + 134400.0, + 7.430399608612062, + 8.07493582746992, + 1344.0, + 390.0, + 8.249599440023303, + 0.0, + 46.469786753878, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 408.0, + 1.1395699462890625, + 27.0, + 1.52050861E+12, + 408.0, + 132440.00244140625, + 136.0, + 3.263157894736842, + 72.72727272727273, + 134459.99755859375, + 12.463199615478516, + 4.814173074583813, + 1344.5999755859375, + 395.0, + 8.24973028153181, + 0.0, + 46.46978633478284, + 0.0 + ] + }, + { + "metrics": [ + 414.0, + 1.1586300048828124, + 27.0, + 1.520508616E+12, + 414.0, + 132500.0, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 134400.0, + 10.616399574279786, + 5.651633549792271, + 1344.0, + 401.0, + 8.249976290389895, + 0.0, + 46.46976646967232, + 0.0 + ] + }, + { + "metrics": [ + 421.0, + 1.1761199951171875, + 27.0, + 1.520508623E+12, + 421.0, + 132580.0048828125, + 125.0, + 2.6842105263157894, + 66.84491978609626, + 134500.0, + 5.547600030899048, + 10.815487720421755, + 1345.0, + 408.0, + 8.250175192952156, + 0.0, + 46.46974006667733, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 428.0, + 1.19252001953125, + 27.0, + 1.52050863E+12, + 428.0, + 132659.99755859375, + 120.0, + 2.4210526315789473, + 64.1711229946524, + 134540.00244140625, + 10.652400398254397, + 5.63253330412103, + 1345.4000244140625, + 415.0, + 8.250378118827939, + 0.0, + 46.46971735171974, + 0.0 + ] + }, + { + "metrics": [ + 434.0, + 1.21052001953125, + 27.0, + 1.520508636E+12, + 434.0, + 132759.99755859375, + 116.0, + 2.2105263157894735, + 62.032085561497325, + 134559.99755859375, + 13.068000411987306, + 4.591368083900722, + 1345.5999755859375, + 421.0, + 8.250580625608563, + 0.0, + 46.469643507152796, + 0.0 + ] + }, + { + "metrics": [ + 440.0, + 1.22927001953125, + 27.0, + 1.520508642E+12, + 440.0, + 132859.99755859375, + 113.0, + 2.0526315789473686, + 60.42780748663102, + 134600.0, + 10.85039978027344, + 5.52975016838393, + 1346.0, + 427.0, + 8.25077885761857, + 0.0, + 46.4695499651134, + 0.0 + ] + }, + { + "metrics": [ + 445.0, + 1.239510009765625, + 27.0, + 1.520508647E+12, + 445.0, + 132859.99755859375, + 111.0, + 1.9444444444444444, + 59.35828877005348, + 134700.0, + 4.636799955368042, + 12.939958719275298, + 1347.0, + 432.0, + 8.250894863158464, + 0.0, + 46.46950839087367, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 457.0, + 1.273719970703125, + 27.0, + 1.520508659E+12, + 457.0, + 132940.00244140625, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 134800.0, + 10.281599807739259, + 5.835667710664664, + 1348.0, + 444.0, + 8.251235336065292, + 0.0, + 46.46931284107268, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 463.0, + 1.292300048828125, + 27.0, + 1.520508665E+12, + 463.0, + 132959.99755859375, + 116.0, + 2.2105263157894735, + 62.032085561497325, + 134859.99755859375, + 7.257600116729737, + 8.267195635881341, + 1348.5999755859375, + 450.0, + 8.251416888087988, + 0.0, + 46.46920387633145, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 470.0, + 1.3063900146484375, + 27.0, + 1.520508672E+12, + 470.0, + 132980.0048828125, + 124.0, + 2.6315789473684212, + 66.31016042780749, + 134959.99755859375, + 5.317199993133546, + 11.284134523712105, + 1349.5999755859375, + 457.0, + 8.251546723768115, + 0.0, + 46.469114776700735, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 475.0, + 1.31372998046875, + 27.0, + 1.520508677E+12, + 475.0, + 133000.0, + 127.0, + 2.7894736842105265, + 67.9144385026738, + 135040.00244140625, + 5.788800144195558, + 10.36484219828562, + 1350.4000244140625, + 462.0, + 8.251604558899999, + 0.0, + 46.46906071342528, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 480.0, + 1.3213399658203124, + 27.0, + 1.520508682E+12, + 480.0, + 133000.0, + 129.0, + 2.8947368421052633, + 68.98395721925134, + 135159.99755859375, + 3.909599876403809, + 15.346839039495308, + 1351.5999755859375, + 467.0, + 8.251673122867942, + 0.0, + 46.46901184692979, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 494.0, + 1.3443399658203126, + 27.0, + 1.520508696E+12, + 494.0, + 133140.00244140625, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 135319.9951171875, + 7.664399814605714, + 7.828401631352871, + 1353.199951171875, + 481.0, + 8.251849142834544, + 0.0, + 46.468851836398244, + 0.06700000166893005 + ] + }, + { + "metrics": [ + 503.0, + 1.36, + 27.0, + 1.520508705E+12, + 503.0, + 133280.0048828125, + 127.0, + 2.7894736842105265, + 67.9144385026738, + 135459.99755859375, + 5.1335999965667725, + 11.687704544983355, + 1354.5999755859375, + 490.0, + 8.251982666552067, + 0.0, + 46.46874672733247, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 511.0, + 1.3739100341796875, + 26.0, + 1.520508713E+12, + 511.0, + 133400.0, + 125.0, + 2.6842105263157894, + 66.84491978609626, + 135540.00244140625, + 5.302800178527832, + 11.314776720222797, + 1355.4000244140625, + 498.0, + 8.252130607143044, + 0.0, + 46.46867472678423, + 0.0 + ] + }, + { + "metrics": [ + 519.0, + 1.390219970703125, + 26.0, + 1.520508721E+12, + 519.0, + 133519.9951171875, + 118.0, + 2.3157894736842106, + 63.101604278074866, + 135619.9951171875, + 7.343999862670898, + 8.16993479493053, + 1356.199951171875, + 506.0, + 8.252327414229512, + 0.0, + 46.468623764812946, + 0.0 + ] + }, + { + "metrics": [ + 529.0, + 1.41302001953125, + 26.0, + 1.520508731E+12, + 529.0, + 133659.99755859375, + 111.0, + 1.9444444444444444, + 59.35828877005348, + 135700.0, + 11.260800075531007, + 5.328218209146271, + 1357.0, + 516.0, + 8.25261608697474, + 0.5, + 46.468648575246334, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 540.0, + 1.4553599853515624, + 26.0, + 1.520508742E+12, + 540.0, + 133459.99755859375, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 135359.99755859375, + 18.881999588012697, + 3.1776295583700374, + 1353.5999755859375, + 527.0, + 8.252972653135657, + 0.0, + 46.46893221884966, + -1.0 + ] + }, + { + "metrics": [ + 547.0, + 1.507, + 26.0, + 1.520508749E+12, + 547.0, + 133119.9951171875, + 103.0, + 1.5, + 55.080213903743314, + 134940.00244140625, + 26.21160049438477, + 2.289062814949191, + 1349.4000244140625, + 534.0, + 8.253397112712264, + 0.0, + 46.469292640686035, + -0.6000000238418579 + ] + }, + { + "metrics": [ + 554.0, + 1.5580699462890626, + 26.0, + 1.520508756E+12, + 554.0, + 132700.0, + 100.0, + 1.3333333333333333, + 53.475935828877006, + 134619.9951171875, + 26.00999965667725, + 2.3068051058815335, + 1346.199951171875, + 541.0, + 8.253781674429774, + 0.0, + 46.46966261789203, + -0.6000000238418579 + ] + }, + { + "metrics": [ + 561.0, + 1.6158199462890626, + 26.0, + 1.520508763E+12, + 561.0, + 132280.0048828125, + 95.0, + 1.0555555555555556, + 50.80213903743316, + 134240.00244140625, + 27.532799148559572, + 2.179219035749186, + 1342.4000244140625, + 548.0, + 8.254116447642446, + 0.0, + 46.470126220956445, + -0.5 + ] + }, + { + "metrics": [ + 569.0, + 1.686699951171875, + 26.0, + 1.520508771E+12, + 569.0, + 131940.00244140625, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 133719.9951171875, + 32.78159980773926, + 1.830295054661575, + 1337.199951171875, + 556.0, + 8.25436145067215, + 0.0, + 46.47073868662119, + -0.6000000238418579 + ] + }, + { + "metrics": [ + 575.0, + 1.73825, + 26.0, + 1.520508777E+12, + 575.0, + 132059.99755859375, + 90.0, + 0.9574468085106383, + 48.1283422459893, + 133559.99755859375, + 22.39199924468994, + 2.679528493920812, + 1335.5999755859375, + 562.0, + 8.254565382376313, + 0.0, + 46.47117613814771, + 0.0 + ] + }, + { + "metrics": [ + 580.0, + 1.763739990234375, + 26.0, + 1.520508782E+12, + 580.0, + 132140.00244140625, + 88.0, + 0.9361702127659575, + 47.05882352941177, + 133740.00244140625, + 15.145199203491213, + 3.961651425368446, + 1337.4000244140625, + 567.0, + 8.25470888055861, + 0.0, + 46.4713822491467, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 588.0, + 1.7887900390625, + 26.0, + 1.52050879E+12, + 588.0, + 132240.00244140625, + 88.0, + 0.9361702127659575, + 47.05882352941177, + 133900.0, + 6.526799869537355, + 9.192866521316065, + 1339.0, + 575.0, + 8.25496000237763, + 0.5, + 46.47151987999678, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 592.0, + 1.7959000244140626, + 26.0, + 1.520508794E+12, + 592.0, + 132280.0048828125, + 103.0, + 1.5, + 55.080213903743314, + 133980.0048828125, + 4.679999828338623, + 12.82051329333055, + 1339.800048828125, + 579.0, + 8.255045246332884, + 0.5, + 46.4715445227921, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 598.0, + 1.8055999755859375, + 26.0, + 1.5205088E+12, + 598.0, + 132319.9951171875, + 115.0, + 2.1578947368421053, + 61.49732620320856, + 134159.99755859375, + 5.889600133895875, + 10.18744883318776, + 1341.5999755859375, + 585.0, + 8.25514867901802, + 0.0, + 46.47159028798342, + 0.4000000059604645 + ] + }, + { + "metrics": [ + 605.0, + 1.81522998046875, + 26.0, + 1.520508807E+12, + 605.0, + 132419.9951171875, + 123.0, + 2.5789473684210527, + 65.77540106951872, + 134300.0, + 5.666400003433228, + 10.588733583165055, + 1343.0, + 592.0, + 8.255281783640385, + 0.5, + 46.4716034475714, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 615.0, + 1.82772998046875, + 26.0, + 1.520508817E+12, + 615.0, + 132580.0048828125, + 128.0, + 2.8421052631578947, + 68.44919786096257, + 134500.0, + 4.971600151062012, + 12.068548996077864, + 1345.0, + 602.0, + 8.255450846627355, + 0.0, + 46.47162004373968, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 624.0, + 1.84097998046875, + 26.0, + 1.520508826E+12, + 624.0, + 132719.9951171875, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 134580.0048828125, + 4.856399917602539, + 12.35483095091152, + 1345.800048828125, + 611.0, + 8.255605744197965, + 0.5, + 46.471662037074566, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 629.0, + 1.8486199951171876, + 26.0, + 1.520508831E+12, + 629.0, + 132780.0048828125, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 134680.0048828125, + 5.385600185394288, + 11.140819583065154, + 1346.800048828125, + 616.0, + 8.255670117214322, + 0.0, + 46.471711825579405, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 640.0, + 1.86002001953125, + 26.0, + 1.520508842E+12, + 640.0, + 132900.0, + 124.0, + 2.6315789473684212, + 66.31016042780749, + 134819.9951171875, + 3.1211999416351324, + 18.641135770848248, + 1348.199951171875, + 627.0, + 8.25581512413919, + 0.0, + 46.47172515280545, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 646.0, + 1.8684300537109375, + 26.0, + 1.520508848E+12, + 646.0, + 132980.0048828125, + 119.0, + 2.3684210526315788, + 63.63636363636363, + 134940.00244140625, + 6.9012001991271985, + 8.69413990041736, + 1349.4000244140625, + 633.0, + 8.255898272618651, + 0.0, + 46.47175817750394, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 659.0, + 1.8865, + 26.0, + 1.520508861E+12, + 659.0, + 133219.9951171875, + 111.0, + 1.9444444444444444, + 59.35828877005348, + 135119.9951171875, + 7.470000171661378, + 8.032128331083506, + 1351.199951171875, + 646.0, + 8.256118632853031, + 0.5, + 46.47183001041412, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 672.0, + 1.9074300537109374, + 26.0, + 1.520508874E+12, + 672.0, + 133380.0048828125, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 135340.00244140625, + 6.069599962234498, + 9.885330233512006, + 1353.4000244140625, + 659.0, + 8.256283588707447, + 0.5, + 46.47198021411896, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 679.0, + 1.9171800537109376, + 26.0, + 1.520508881E+12, + 679.0, + 133459.99755859375, + 109.0, + 1.8333333333333335, + 58.288770053475936, + 135400.0, + 6.37199993133545, + 9.416195960225812, + 1354.0, + 666.0, + 8.256370928138494, + 0.0, + 46.47204827517271, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 687.0, + 1.928, + 26.0, + 1.520508889E+12, + 687.0, + 133540.00244140625, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 135500.0, + 8.114399814605715, + 7.39426222306689, + 1355.0, + 674.0, + 8.256447622552514, + 0.0, + 46.47213242948055, + 0.0 + ] + }, + { + "metrics": [ + 700.0, + 1.9541400146484376, + 26.0, + 1.520508902E+12, + 700.0, + 133619.9951171875, + 102.0, + 1.4444444444444444, + 54.54545454545455, + 135680.0048828125, + 4.658400106430054, + 12.879958492440606, + 1356.800048828125, + 687.0, + 8.256568238139153, + 0.0, + 46.472351951524615, + 0.11999999731779099 + ] + }, + { + "metrics": [ + 709.0, + 1.96781005859375, + 26.0, + 1.520508911E+12, + 709.0, + 133619.9951171875, + 103.0, + 1.5, + 55.080213903743314, + 135780.0048828125, + 6.065999794006348, + 9.89119717268774, + 1357.800048828125, + 696.0, + 8.256641160696745, + 0.0, + 46.47246066480875, + 0.0 + ] + }, + { + "metrics": [ + 713.0, + 1.9773399658203126, + 26.0, + 1.520508915E+12, + 713.0, + 133659.99755859375, + 114.0, + 2.1052631578947367, + 60.962566844919785, + 135819.9951171875, + 5.295599842071534, + 11.330161228445311, + 1358.199951171875, + 700.0, + 8.256713077425957, + 0.0, + 46.47252964787185, + 0.0 + ] + }, + { + "metrics": [ + 723.0, + 1.9963699951171876, + 26.0, + 1.520508925E+12, + 723.0, + 133619.9951171875, + 121.0, + 2.473684210526316, + 64.70588235294117, + 135759.99755859375, + 10.155600357055665, + 5.908070217662184, + 1357.5999755859375, + 710.0, + 8.256770158186555, + 0.0, + 46.472693933174014, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 728.0, + 2.0257900390625, + 26.0, + 1.52050893E+12, + 728.0, + 133340.00244140625, + 124.0, + 2.6315789473684212, + 66.31016042780749, + 135540.00244140625, + 27.03960056304932, + 2.2189676904470392, + 1355.4000244140625, + 715.0, + 8.256696313619614, + 0.0, + 46.47295117378235, + -0.800000011920929 + ] + }, + { + "metrics": [ + 733.0, + 2.07243994140625, + 26.0, + 1.520508935E+12, + 733.0, + 132880.0048828125, + 126.0, + 2.736842105263158, + 67.37967914438502, + 134980.0048828125, + 35.262000274658206, + 1.7015483961390667, + 1349.800048828125, + 720.0, + 8.256509229540825, + 0.0, + 46.473349230363965, + -1.2000000476837158 + ] + }, + { + "metrics": [ + 738.0, + 2.1262099609375, + 26.0, + 1.52050894E+12, + 738.0, + 132380.0048828125, + 121.0, + 2.473684210526316, + 64.70588235294117, + 134440.00244140625, + 37.16279983520508, + 1.6145177510323314, + 1344.4000244140625, + 725.0, + 8.256442425772548, + 0.0, + 46.47383068688214, + -0.8999999761581421 + ] + }, + { + "metrics": [ + 744.0, + 2.18493994140625, + 26.0, + 1.520508946E+12, + 744.0, + 132019.9951171875, + 114.0, + 2.1052631578947367, + 60.962566844919785, + 134059.99755859375, + 31.15799903869629, + 1.9256692298335252, + 1340.5999755859375, + 731.0, + 8.256424572318792, + 0.0, + 46.47435539402068, + -0.5 + ] + }, + { + "metrics": [ + 751.0, + 2.23839990234375, + 25.0, + 1.520508953E+12, + 751.0, + 132000.0, + 108.0, + 1.7777777777777777, + 57.75401069518717, + 133800.0, + 20.365200233459475, + 2.9462023119920824, + 1338.0, + 738.0, + 8.256626073271036, + 0.0, + 46.474810699000955, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 762.0, + 2.28293994140625, + 25.0, + 1.520508964E+12, + 762.0, + 131980.0048828125, + 101.0, + 1.3888888888888888, + 54.01069518716577, + 133900.0, + 16.160399436950684, + 3.71277951674946, + 1339.0, + 749.0, + 8.256886163726449, + 0.0, + 46.47516382858157, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 770.0, + 2.311669921875, + 25.0, + 1.520508972E+12, + 770.0, + 131959.99755859375, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 133780.0048828125, + 12.499199581146241, + 4.8003073814825585, + 1337.800048828125, + 757.0, + 8.25711096636951, + 0.5, + 46.475368682295084, + -0.2669999897480011 + ] + }, + { + "metrics": [ + 776.0, + 2.329800048828125, + 25.0, + 1.520508978E+12, + 776.0, + 131940.00244140625, + 116.0, + 2.2105263157894735, + 62.032085561497325, + 133819.9951171875, + 13.305600357055665, + 4.509379389272228, + 1338.199951171875, + 763.0, + 8.25725949369371, + 0.0, + 46.47549600340426, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 792.0, + 2.3763798828125, + 25.0, + 1.520508994E+12, + 792.0, + 131900.0, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 133819.9951171875, + 10.213199615478516, + 5.874750545467415, + 1338.199951171875, + 779.0, + 8.257571635767817, + 0.5, + 46.47585223428905, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 809.0, + 2.428, + 25.0, + 1.520509011E+12, + 809.0, + 131900.0, + 99.0, + 1.2777777777777777, + 52.94117647058823, + 133859.99755859375, + 8.86680021286011, + 6.766815375514836, + 1338.5999755859375, + 796.0, + 8.25791571289301, + 0.0, + 46.47625003941357, + 0.0 + ] + }, + { + "metrics": [ + 822.0, + 2.472239990234375, + 25.0, + 1.520509024E+12, + 822.0, + 131900.0, + 102.0, + 1.4444444444444444, + 54.54545454545455, + 133900.0, + 12.495599842071535, + 4.801690256596208, + 1339.0, + 809.0, + 8.258248642086983, + 0.0, + 46.476572155952454, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 836.0, + 2.51047998046875, + 25.0, + 1.520509038E+12, + 836.0, + 131940.00244140625, + 99.0, + 1.2777777777777777, + 52.94117647058823, + 133940.00244140625, + 7.757999897003174, + 7.733952153721646, + 1339.4000244140625, + 823.0, + 8.258528262376785, + 0.5, + 46.476848339661956, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 857.0, + 2.573580078125, + 25.0, + 1.520509059E+12, + 857.0, + 131980.0048828125, + 101.0, + 1.3888888888888888, + 54.01069518716577, + 133940.00244140625, + 11.015999794006348, + 5.446623196620353, + 1339.4000244140625, + 844.0, + 8.25891399756074, + 0.0, + 46.47721312008798, + 0.0 + ] + }, + { + "metrics": [ + 876.0, + 2.63614990234375, + 25.0, + 1.520509078E+12, + 876.0, + 132000.0, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 134000.0, + 10.314000034332278, + 5.817335642066864, + 1340.0, + 863.0, + 8.259681109338999, + 0.0, + 46.47737053222954, + 0.0 + ] + }, + { + "metrics": [ + 889.0, + 2.6809599609375, + 25.0, + 1.520509091E+12, + 889.0, + 132040.00244140625, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 134040.00244140625, + 10.781999588012695, + 5.56483048642548, + 1340.4000244140625, + 876.0, + 8.260224172845483, + 0.0, + 46.47750187665224, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 897.0, + 2.70993994140625, + 25.0, + 1.520509099E+12, + 897.0, + 132080.0048828125, + 97.0, + 1.1666666666666667, + 51.8716577540107, + 134000.0, + 13.705199718475342, + 4.3779004497918255, + 1340.0, + 884.0, + 8.2605738658458, + 0.0, + 46.477584186941385, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 911.0, + 2.75335009765625, + 25.0, + 1.520509113E+12, + 911.0, + 132140.00244140625, + 95.0, + 1.0555555555555556, + 50.80213903743316, + 134019.9951171875, + 10.378799629211427, + 5.781015353945971, + 1340.199951171875, + 898.0, + 8.26109865680337, + 0.0, + 46.47770748473704, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 923.0, + 2.794219970703125, + 25.0, + 1.520509125E+12, + 923.0, + 132159.99755859375, + 89.0, + 0.9468085106382979, + 47.593582887700535, + 134059.99755859375, + 11.253599739074708, + 5.331627337310409, + 1340.5999755859375, + 910.0, + 8.261578939855099, + 0.0, + 46.477837320417166, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 938.0, + 2.839679931640625, + 25.0, + 1.52050914E+12, + 938.0, + 132219.9951171875, + 83.0, + 0.8829787234042553, + 44.38502673796791, + 134080.0048828125, + 8.967599773406985, + 6.690753549230345, + 1340.800048828125, + 925.0, + 8.262104066088796, + 0.0, + 46.478018034249544, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 948.0, + 2.87356005859375, + 25.0, + 1.52050915E+12, + 948.0, + 132280.0048828125, + 84.0, + 0.8936170212765957, + 44.919786096256686, + 134100.0, + 12.160800075531007, + 4.93388589889963, + 1341.0, + 935.0, + 8.26251964084804, + 0.0, + 46.4781067147851, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 963.0, + 2.926219970703125, + 25.0, + 1.520509165E+12, + 963.0, + 132359.99755859375, + 84.0, + 0.8936170212765957, + 44.919786096256686, + 134140.00244140625, + 11.520000171661378, + 5.208333256764787, + 1341.4000244140625, + 950.0, + 8.263171082362533, + 0.0, + 46.478192964568734, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 973.0, + 2.95910009765625, + 25.0, + 1.520509175E+12, + 973.0, + 132419.9951171875, + 85.0, + 0.9042553191489362, + 45.45454545454545, + 134159.99755859375, + 10.749600219726563, + 5.581602923417949, + 1341.5999755859375, + 960.0, + 8.263557069003582, + 0.5, + 46.47827669978142, + 0.06700000166893005 + ] + }, + { + "metrics": [ + 986.0, + 3.003570068359375, + 25.0, + 1.520509188E+12, + 986.0, + 132519.9951171875, + 93.0, + 0.9893617021276596, + 49.73262032085562, + 134119.9951171875, + 14.882399368286135, + 4.031607977128868, + 1341.199951171875, + 973.0, + 8.264073561877012, + 0.0, + 46.47844249382615, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 995.0, + 3.03464990234375, + 25.0, + 1.520509197E+12, + 995.0, + 132559.99755859375, + 94.0, + 1.0, + 50.26737967914438, + 134140.00244140625, + 11.926799869537355, + 5.030687247905286, + 1341.4000244140625, + 982.0, + 8.264431888237596, + 0.0, + 46.47856310941279, + 0.0 + ] + }, + { + "metrics": [ + 1003.0, + 3.062989990234375, + 25.0, + 1.520509205E+12, + 1003.0, + 132580.0048828125, + 94.0, + 1.0, + 50.26737967914438, + 134259.99755859375, + 13.402800178527833, + 4.476676456620158, + 1342.5999755859375, + 990.0, + 8.264701617881656, + 0.0, + 46.478734435513616, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1018.0, + 3.112169921875, + 24.0, + 1.52050922E+12, + 1018.0, + 132800.0, + 93.0, + 0.9893617021276596, + 49.73262032085562, + 134240.00244140625, + 8.229600048065187, + 7.290755281127697, + 1342.4000244140625, + 1005.0, + 8.265227749943733, + 0.0, + 46.47896904498339, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 1024.0, + 3.127909912109375, + 24.0, + 1.520509226E+12, + 1024.0, + 132740.00244140625, + 104.0, + 1.5555555555555556, + 55.61497326203209, + 134259.99755859375, + 10.414799594879153, + 5.761032602250108, + 1342.5999755859375, + 1011.0, + 8.265301762148738, + 0.5, + 46.47909980267286, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1039.0, + 3.17, + 24.0, + 1.520509241E+12, + 1039.0, + 132780.0048828125, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 134280.0048828125, + 6.552000188827516, + 9.157508895422826, + 1342.800048828125, + 1026.0, + 8.26557015068829, + 0.0, + 46.47942493669689, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 1053.0, + 3.210159912109375, + 25.0, + 1.520509255E+12, + 1053.0, + 132840.00244140625, + 104.0, + 1.5555555555555556, + 55.61497326203209, + 134340.00244140625, + 9.270000171661378, + 6.472491790822346, + 1343.4000244140625, + 1040.0, + 8.265749104321003, + 0.0, + 46.47976356558502, + 0.0 + ] + }, + { + "metrics": [ + 1071.0, + 3.2625, + 25.0, + 1.520509273E+12, + 1071.0, + 132859.99755859375, + 101.0, + 1.3888888888888888, + 54.01069518716577, + 134400.0, + 9.306000137329104, + 6.447453162108, + 1344.0, + 1058.0, + 8.26610709540546, + 0.0, + 46.48016128689051, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 1079.0, + 3.285139892578125, + 25.0, + 1.520509281E+12, + 1079.0, + 132840.00244140625, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 134340.00244140625, + 10.818000411987306, + 5.546311492604, + 1343.4000244140625, + 1066.0, + 8.266247576102614, + 0.5, + 46.480329260230064, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 1092.0, + 3.32652001953125, + 25.0, + 1.520509294E+12, + 1092.0, + 132759.99755859375, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 134400.0, + 9.507600116729737, + 6.3107408047613385, + 1344.0, + 1079.0, + 8.266567848622799, + 0.0, + 46.48062187246978, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1097.0, + 3.3388798828125, + 25.0, + 1.520509299E+12, + 1097.0, + 132740.00244140625, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 134440.00244140625, + 6.75360016822815, + 8.88415045567339, + 1344.4000244140625, + 1084.0, + 8.266699109226465, + 0.0, + 46.48068616166711, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1112.0, + 3.393409912109375, + 25.0, + 1.520509314E+12, + 1112.0, + 132819.9951171875, + 95.0, + 1.0555555555555556, + 50.80213903743316, + 134440.00244140625, + 10.818000411987306, + 5.546311492604, + 1344.4000244140625, + 1099.0, + 8.267017118632793, + 0.0, + 46.48111950606108, + 0.0 + ] + }, + { + "metrics": [ + 1132.0, + 3.453830078125, + 25.0, + 1.520509334E+12, + 1132.0, + 132919.9951171875, + 94.0, + 1.0, + 50.26737967914438, + 134519.9951171875, + 9.33840036392212, + 6.425083276982147, + 1345.199951171875, + 1119.0, + 8.267366895452142, + 0.0, + 46.48160473443568, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1141.0, + 3.478610107421875, + 25.0, + 1.520509343E+12, + 1141.0, + 133000.0, + 94.0, + 1.0, + 50.26737967914438, + 134580.0048828125, + 9.709200096130372, + 6.17970578605267, + 1345.800048828125, + 1128.0, + 8.267546687275171, + 0.0, + 46.48178972303867, + 0.0 + ] + }, + { + "metrics": [ + 1151.0, + 3.50139990234375, + 25.0, + 1.520509353E+12, + 1151.0, + 133059.99755859375, + 94.0, + 1.0, + 50.26737967914438, + 134680.0048828125, + 7.6248001098632825, + 7.869058748751362, + 1346.800048828125, + 1138.0, + 8.267707116901875, + 0.0, + 46.48195954039693, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1164.0, + 3.5298701171875, + 25.0, + 1.520509366E+12, + 1164.0, + 133159.99755859375, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 134759.99755859375, + 7.462799835205079, + 8.039877973003573, + 1347.5999755859375, + 1151.0, + 8.267936361953616, + 0.0, + 46.482163555920124, + 0.0 + ] + }, + { + "metrics": [ + 1168.0, + 3.539159912109375, + 25.0, + 1.52050937E+12, + 1168.0, + 133200.0, + 110.0, + 1.8888888888888888, + 58.8235294117647, + 134780.0048828125, + 9.777600288391113, + 6.136475028871618, + 1347.800048828125, + 1155.0, + 8.26800299808383, + 0.0, + 46.48223337717354, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1179.0, + 3.568110107421875, + 25.0, + 1.520509381E+12, + 1179.0, + 133259.99755859375, + 117.0, + 2.263157894736842, + 62.5668449197861, + 134819.9951171875, + 7.664399814605714, + 7.828401631352871, + 1348.199951171875, + 1166.0, + 8.268159739673138, + 0.0, + 46.48246832191944, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 1191.0, + 3.5937099609375, + 25.0, + 1.520509393E+12, + 1191.0, + 133280.0048828125, + 123.0, + 2.5789473684210527, + 65.77540106951872, + 134959.99755859375, + 9.071999931335451, + 6.613756665137856, + 1349.5999755859375, + 1178.0, + 8.268114645034075, + 0.0, + 46.48269387893379, + 0.2669999897480011 + ] + }, + { + "metrics": [ + 1201.0, + 3.617489990234375, + 25.0, + 1.520509403E+12, + 1201.0, + 133380.0048828125, + 131.0, + 3.0, + 70.05347593582887, + 135019.9951171875, + 8.733600425720216, + 6.870018902548099, + 1350.199951171875, + 1188.0, + 8.26794482767582, + 0.0, + 46.48287149146199, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1214.0, + 3.651610107421875, + 25.0, + 1.520509416E+12, + 1214.0, + 133559.99755859375, + 132.0, + 3.0526315789473686, + 70.58823529411765, + 135180.0048828125, + 10.479600048065187, + 5.7254093416549425, + 1351.800048828125, + 1201.0, + 8.267677277326584, + 0.0, + 46.48311632685363, + 0.1599999964237213 + ] + }, + { + "metrics": [ + 1226.0, + 3.684419921875, + 25.0, + 1.520509428E+12, + 1226.0, + 133740.00244140625, + 132.0, + 3.0526315789473686, + 70.58823529411765, + 135319.9951171875, + 10.115999794006349, + 5.931198223980742, + 1353.199951171875, + 1213.0, + 8.267383491620421, + 0.0, + 46.48333081975579, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1239.0, + 3.721989990234375, + 25.0, + 1.520509441E+12, + 1239.0, + 133940.00244140625, + 133.0, + 3.1052631578947367, + 71.12299465240642, + 135359.99755859375, + 9.446400260925294, + 6.351625842088008, + 1353.5999755859375, + 1226.0, + 8.267530761659145, + 0.0, + 46.48361027240753, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 1254.0, + 3.784489990234375, + 25.0, + 1.520509456E+12, + 1254.0, + 133800.0, + 134.0, + 3.1578947368421053, + 71.65775401069519, + 135080.0048828125, + 15.584399986267092, + 3.8500038541664585, + 1350.800048828125, + 1241.0, + 8.268281444907188, + 0.0, + 46.4835237711668, + -0.14300000667572021 + ] + }, + { + "metrics": [ + 1265.0, + 3.83268994140625, + 25.0, + 1.520509467E+12, + 1265.0, + 133480.0048828125, + 136.0, + 3.263157894736842, + 72.72727272727273, + 134940.00244140625, + 17.334000205993654, + 3.4614052901218693, + 1349.4000244140625, + 1252.0, + 8.268905729055405, + 0.0, + 46.48351916112006, + 0.0 + ] + }, + { + "metrics": [ + 1274.0, + 3.8708798828125, + 25.0, + 1.520509476E+12, + 1274.0, + 133359.99755859375, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 134759.99755859375, + 12.628799629211427, + 4.751045370394126, + 1347.5999755859375, + 1261.0, + 8.269393891096115, + 0.0, + 46.483568865805864, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1283.0, + 3.905590087890625, + 25.0, + 1.520509485E+12, + 1283.0, + 133359.99755859375, + 124.0, + 2.6315789473684212, + 66.31016042780749, + 134680.0048828125, + 15.458400535583499, + 3.881384744423381, + 1346.800048828125, + 1270.0, + 8.26981583610177, + 0.0, + 46.483674393966794, + 0.0 + ] + }, + { + "metrics": [ + 1296.0, + 3.9471298828125, + 25.0, + 1.520509498E+12, + 1296.0, + 133359.99755859375, + 116.0, + 2.2105263157894735, + 62.032085561497325, + 134659.99755859375, + 9.990000343322755, + 6.006005800800955, + 1346.5999755859375, + 1283.0, + 8.270331071689725, + 0.0, + 46.48377296514809, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 1303.0, + 3.973840087890625, + 25.0, + 1.520509505E+12, + 1303.0, + 133280.0048828125, + 121.0, + 2.473684210526316, + 64.70588235294117, + 134719.9951171875, + 12.171600151062014, + 4.92950797490376, + 1347.199951171875, + 1290.0, + 8.270640196278691, + 0.0, + 46.483869859948754, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1310.0, + 4.00152001953125, + 24.0, + 1.520509512E+12, + 1310.0, + 133200.0, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 134740.00244140625, + 13.813199615478517, + 4.3436713927428094, + 1347.4000244140625, + 1297.0, + 8.270978908985853, + 0.0, + 46.48394102230668, + 0.0 + ] + }, + { + "metrics": [ + 1320.0, + 4.040280029296875, + 24.0, + 1.520509522E+12, + 1320.0, + 133219.9951171875, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 134740.00244140625, + 11.822399711608888, + 5.075111777271716, + 1347.4000244140625, + 1307.0, + 8.271472854539752, + 0.0, + 46.48401285521686, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 1327.0, + 4.0629599609375, + 24.0, + 1.520509529E+12, + 1327.0, + 133200.0, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 134759.99755859375, + 10.85039978027344, + 5.52975016838393, + 1347.5999755859375, + 1314.0, + 8.271765802055597, + 0.0, + 46.48397957906127, + 0.032999999821186066 + ] + }, + { + "metrics": [ + 1336.0, + 4.09708984375, + 24.0, + 1.520509538E+12, + 1336.0, + 133219.9951171875, + 137.0, + 3.3157894736842106, + 73.2620320855615, + 134780.0048828125, + 10.951200199127198, + 5.478851534170835, + 1347.800048828125, + 1323.0, + 8.272210713475943, + 0.5, + 46.48401914164424, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1347.0, + 4.13083984375, + 24.0, + 1.520509549E+12, + 1347.0, + 133259.99755859375, + 128.0, + 2.8421052631578947, + 68.44919786096257, + 134759.99755859375, + 11.41919975280762, + 5.25430864778838, + 1347.5999755859375, + 1334.0, + 8.272620504722, + 0.0, + 46.484121568500996, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 1358.0, + 4.161, + 24.0, + 1.52050956E+12, + 1358.0, + 133259.99755859375, + 121.0, + 2.473684210526316, + 64.70588235294117, + 134800.0, + 9.74160032272339, + 6.159152297805032, + 1348.0, + 1345.0, + 8.272955361753702, + 0.5, + 46.48424779996276, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1376.0, + 4.214669921875, + 24.0, + 1.520509578E+12, + 1376.0, + 133300.0, + 112.0, + 2.0, + 59.893048128342244, + 134880.0048828125, + 12.898799800872805, + 4.651595569995596, + 1348.800048828125, + 1363.0, + 8.27346540056169, + 0.0, + 46.48456698283553, + 0.11999999731779099 + ] + }, + { + "metrics": [ + 1388.0, + 4.255509765625, + 24.0, + 1.52050959E+12, + 1388.0, + 133419.9951171875, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 134840.00244140625, + 8.29800024032593, + 7.230657781909551, + 1348.4000244140625, + 1375.0, + 8.27393352985382, + 0.5, + 46.48470260202885, + 0.0 + ] + }, + { + "metrics": [ + 1399.0, + 4.28614990234375, + 24.0, + 1.520509601E+12, + 1399.0, + 133500.0, + 100.0, + 1.3333333333333333, + 53.475935828877006, + 134859.99755859375, + 8.46360025405884, + 7.089181697023813, + 1348.5999755859375, + 1386.0, + 8.274297891184688, + 0.0, + 46.48479270748794, + -0.06700000166893005 + ] + }, + { + "metrics": [ + 1410.0, + 4.314990234375, + 24.0, + 1.520509612E+12, + 1410.0, + 133419.9951171875, + 102.0, + 1.4444444444444444, + 54.54545454545455, + 134919.9951171875, + 5.039999914169313, + 11.904762109879748, + 1349.199951171875, + 1397.0, + 8.274556724354625, + 0.0, + 46.48460553959012, + 0.0 + ] + }, + { + "metrics": [ + 1419.0, + 4.35781005859375, + 24.0, + 1.520509621E+12, + 1419.0, + 133459.99755859375, + 100.0, + 1.3333333333333333, + 53.475935828877006, + 134919.9951171875, + 10.616399574279786, + 5.651633549792271, + 1349.199951171875, + 1406.0, + 8.274985626339912, + 0.0, + 46.48437009193003, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1433.0, + 4.39927978515625, + 24.0, + 1.520509635E+12, + 1433.0, + 133500.0, + 99.0, + 1.2777777777777777, + 52.94117647058823, + 134919.9951171875, + 8.229600048065187, + 7.290755281127697, + 1349.199951171875, + 1420.0, + 8.275409080088139, + 0.5, + 46.484576957300305, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 1446.0, + 4.443759765625, + 24.0, + 1.520509648E+12, + 1446.0, + 133480.0048828125, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 134940.00244140625, + 9.608399677276612, + 6.244536241960982, + 1349.4000244140625, + 1433.0, + 8.275806633755565, + 0.0, + 46.48482330143452, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1461.0, + 4.48160009765625, + 24.0, + 1.520509663E+12, + 1461.0, + 133519.9951171875, + 94.0, + 1.0, + 50.26737967914438, + 134980.0048828125, + 11.455199718475342, + 5.23779606524275, + 1349.800048828125, + 1448.0, + 8.276222543790936, + 0.0, + 46.48504910990596, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1475.0, + 4.52506005859375, + 24.0, + 1.520509677E+12, + 1475.0, + 133559.99755859375, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135019.9951171875, + 9.709200096130372, + 6.17970578605267, + 1350.199951171875, + 1462.0, + 8.276610290631652, + 0.0, + 46.48532881401479, + 0.0 + ] + }, + { + "metrics": [ + 1490.0, + 4.5580400390625, + 24.0, + 1.520509692E+12, + 1490.0, + 133540.00244140625, + 97.0, + 1.1666666666666667, + 51.8716577540107, + 135259.99755859375, + 7.5239996910095215, + 7.974481987777646, + 1352.5999755859375, + 1477.0, + 8.276823777705431, + 0.5, + 46.48558471351862, + 0.13300000131130219 + ] + }, + { + "metrics": [ + 1505.0, + 4.59008984375, + 24.0, + 1.520509707E+12, + 1505.0, + 133600.0, + 97.0, + 1.1666666666666667, + 51.8716577540107, + 135300.0, + 8.402400398254397, + 7.140816572424357, + 1353.0, + 1492.0, + 8.276957804337144, + 0.0, + 46.485856119543314, + 0.0 + ] + }, + { + "metrics": [ + 1525.0, + 4.64618994140625, + 24.0, + 1.520509727E+12, + 1525.0, + 133759.99755859375, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135380.0048828125, + 5.518799972534181, + 10.871928736429377, + 1353.800048828125, + 1512.0, + 8.27706660144031, + 0.0, + 46.48635140620172, + 0.11999999731779099 + ] + }, + { + "metrics": [ + 1539.0, + 4.67631982421875, + 24.0, + 1.520509741E+12, + 1539.0, + 133840.00244140625, + 99.0, + 1.2777777777777777, + 52.94117647058823, + 135519.9951171875, + 11.48759994506836, + 5.223023111782203, + 1355.199951171875, + 1526.0, + 8.277171207591891, + 0.0, + 46.486610071733594, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 1550.0, + 4.71002001953125, + 24.0, + 1.520509752E+12, + 1550.0, + 133959.99755859375, + 106.0, + 1.6666666666666665, + 56.68449197860963, + 135580.0048828125, + 9.17280035018921, + 6.541077721239445, + 1355.800048828125, + 1537.0, + 8.277368266135454, + 0.0, + 46.486878879368305, + 0.0 + ] + }, + { + "metrics": [ + 1556.0, + 4.72581982421875, + 24.0, + 1.520509758E+12, + 1556.0, + 134000.0, + 120.0, + 2.4210526315789473, + 64.1711229946524, + 135619.9951171875, + 8.43120002746582, + 7.116424686467117, + 1356.199951171875, + 1543.0, + 8.277447139844298, + 0.0, + 46.48700955323875, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1565.0, + 4.75231005859375, + 24.0, + 1.520509767E+12, + 1565.0, + 134059.99755859375, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 135640.00244140625, + 13.842000102996828, + 4.334633692063754, + 1356.4000244140625, + 1552.0, + 8.27757672406733, + 0.5, + 46.48723100312054, + 0.0 + ] + }, + { + "metrics": [ + 1577.0, + 4.7939599609375, + 24.0, + 1.520509779E+12, + 1577.0, + 134180.0048828125, + 134.0, + 3.1578947368421053, + 71.65775401069519, + 135540.00244140625, + 10.249199581146241, + 5.854115683567339, + 1355.4000244140625, + 1564.0, + 8.277889117598534, + 0.0, + 46.487535517662764, + 0.0 + ] + }, + { + "metrics": [ + 1593.0, + 4.83414013671875, + 24.0, + 1.520509795E+12, + 1593.0, + 134259.99755859375, + 141.0, + 3.526315789473684, + 75.40106951871658, + 135619.9951171875, + 8.330399608612062, + 7.202535632260826, + 1356.199951171875, + 1580.0, + 8.278231769800186, + 0.0, + 46.487807258963585, + 0.11999999731779099 + ] + }, + { + "metrics": [ + 1608.0, + 4.88391015625, + 24.0, + 1.52050981E+12, + 1608.0, + 134280.0048828125, + 145.0, + 3.736842105263158, + 77.54010695187166, + 135640.00244140625, + 10.177199649810792, + 5.895531391399548, + 1356.4000244140625, + 1595.0, + 8.278642063960433, + 0.0, + 46.48814622312784, + 0.0 + ] + }, + { + "metrics": [ + 1614.0, + 4.89958984375, + 24.0, + 1.520509816E+12, + 1614.0, + 134280.0048828125, + 146.0, + 3.7894736842105265, + 78.07486631016043, + 135700.0, + 9.169199752807618, + 6.543646297336685, + 1357.0, + 1601.0, + 8.278759242966771, + 0.0, + 46.488260217010975, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1619.0, + 4.91702978515625, + 24.0, + 1.520509821E+12, + 1619.0, + 134240.00244140625, + 138.0, + 3.3684210526315788, + 73.79679144385027, + 135800.0, + 7.995599842071534, + 7.504127419720263, + 1358.0, + 1606.0, + 8.278828728944063, + 0.0, + 46.488406816497445, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1634.0, + 4.96435986328125, + 24.0, + 1.520509836E+12, + 1634.0, + 134300.0, + 138.0, + 3.3684210526315788, + 73.79679144385027, + 135900.0, + 5.248799800872803, + 11.431184706649097, + 1359.0, + 1621.0, + 8.279280262067914, + 0.5, + 46.48867880925536, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1648.0, + 5.0056201171875, + 24.0, + 1.52050985E+12, + 1648.0, + 134240.00244140625, + 137.0, + 3.3157894736842106, + 73.2620320855615, + 135900.0, + 8.938800144195557, + 6.712310270295194, + 1359.0, + 1635.0, + 8.279715618118644, + 0.5, + 46.48888726718724, + -0.13300000131130219 + ] + }, + { + "metrics": [ + 1657.0, + 5.02335009765625, + 24.0, + 1.520509859E+12, + 1657.0, + 134200.0, + 129.0, + 2.8947368421052633, + 68.98395721925134, + 136059.99755859375, + 7.6932003021240245, + 7.79909500022176, + 1360.5999755859375, + 1644.0, + 8.279883591458201, + 0.0, + 46.488998495042324, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1668.0, + 5.05489013671875, + 24.0, + 1.52050987E+12, + 1668.0, + 134259.99755859375, + 122.0, + 2.526315789473684, + 65.24064171122994, + 136180.0048828125, + 5.7815998077392585, + 10.377750450953716, + 1361.800048828125, + 1655.0, + 8.280180981382728, + 0.0, + 46.48919178172946, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1684.0, + 5.10197998046875, + 24.0, + 1.520509886E+12, + 1684.0, + 134200.0, + 114.0, + 2.1052631578947367, + 60.962566844919785, + 136180.0048828125, + 8.837999725341799, + 6.788866471669819, + 1361.800048828125, + 1671.0, + 8.280625892803073, + 0.0, + 46.48947802372277, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1696.0, + 5.1564501953125, + 24.0, + 1.520509898E+12, + 1696.0, + 134219.9951171875, + 110.0, + 1.8888888888888888, + 58.8235294117647, + 135940.00244140625, + 19.88640060424805, + 3.017137249019466, + 1359.4000244140625, + 1683.0, + 8.28110315836966, + 0.0, + 46.48982746526599, + -0.3330000042915344 + ] + }, + { + "metrics": [ + 1704.0, + 5.1978701171875, + 24.0, + 1.520509906E+12, + 1704.0, + 134140.00244140625, + 108.0, + 1.7777777777777777, + 57.75401069518717, + 135719.9951171875, + 15.688799285888674, + 3.824384448972277, + 1357.199951171875, + 1691.0, + 8.281362159177661, + 0.5, + 46.49015041999519, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1723.0, + 5.2647900390625, + 24.0, + 1.520509925E+12, + 1723.0, + 134040.00244140625, + 104.0, + 1.5555555555555556, + 55.61497326203209, + 135659.99755859375, + 13.31279983520508, + 4.506940745351913, + 1356.5999755859375, + 1710.0, + 8.281977726146579, + 0.0, + 46.49056817404926, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 1739.0, + 5.31975, + 24.0, + 1.520509941E+12, + 1739.0, + 133959.99755859375, + 102.0, + 1.4444444444444444, + 54.54545454545455, + 135619.9951171875, + 16.42680072784424, + 3.6525675940231648, + 1356.199951171875, + 1726.0, + 8.282459015026689, + 0.0, + 46.49093136191368, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 1757.0, + 5.39356982421875, + 24.0, + 1.520509959E+12, + 1757.0, + 133900.0, + 100.0, + 1.3333333333333333, + 53.475935828877006, + 135580.0048828125, + 11.354400157928467, + 5.284295002594535, + 1355.800048828125, + 1744.0, + 8.282963605597615, + 0.0, + 46.491459254175425, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 1779.0, + 5.46075, + 24.0, + 1.520509981E+12, + 1779.0, + 133940.00244140625, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 135619.9951171875, + 11.18520040512085, + 5.364231112437698, + 1356.199951171875, + 1766.0, + 8.28374957665801, + 0.0, + 46.491777431219816, + 0.07999999821186066 + ] + }, + { + "metrics": [ + 1796.0, + 5.51110986328125, + 24.0, + 1.520509998E+12, + 1796.0, + 134000.0, + 97.0, + 1.1666666666666667, + 51.8716577540107, + 135719.9951171875, + 7.056000137329103, + 8.5034011967454, + 1357.199951171875, + 1783.0, + 8.284325245767832, + 0.5, + 46.4919856376946, + 0.0 + ] + }, + { + "metrics": [ + 1807.0, + 5.54008984375, + 24.0, + 1.520510009E+12, + 1807.0, + 134000.0, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 135600.0, + 9.507600116729737, + 6.3107408047613385, + 1356.0, + 1794.0, + 8.284692876040936, + 0.0, + 46.49205503985286, + 0.0 + ] + }, + { + "metrics": [ + 1814.0, + 5.5581298828125, + 24.0, + 1.520510016E+12, + 1814.0, + 134000.0, + 115.0, + 2.1578947368421053, + 61.49732620320856, + 135640.00244140625, + 6.98759994506836, + 8.586639258640762, + 1356.4000244140625, + 1801.0, + 8.284911308437586, + 0.0, + 46.49211706593633, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1825.0, + 5.5922998046875, + 24.0, + 1.520510027E+12, + 1825.0, + 134000.0, + 123.0, + 2.5789473684210527, + 65.77540106951872, + 135659.99755859375, + 12.632400226593019, + 4.749691185820046, + 1356.5999755859375, + 1812.0, + 8.2852853089571, + 0.0, + 46.49227791465819, + -0.06700000166893005 + ] + }, + { + "metrics": [ + 1842.0, + 5.657919921875, + 24.0, + 1.520510044E+12, + 1842.0, + 134000.0, + 133.0, + 3.1052631578947367, + 71.12299465240642, + 135640.00244140625, + 11.455199718475342, + 5.23779606524275, + 1356.4000244140625, + 1829.0, + 8.285949910059571, + 0.5, + 46.49264403618872, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 1851.0, + 5.6872001953125, + 24.0, + 1.520510053E+12, + 1851.0, + 133980.0048828125, + 139.0, + 3.4210526315789473, + 74.33155080213903, + 135580.0048828125, + 11.217599773406985, + 5.348737807016352, + 1355.800048828125, + 1838.0, + 8.286147806793451, + 0.0, + 46.4928680844605, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 1868.0, + 5.7438798828125, + 24.0, + 1.52051007E+12, + 1868.0, + 133940.00244140625, + 133.0, + 3.1052631578947367, + 71.12299465240642, + 135580.0048828125, + 12.765600013732913, + 4.700131599568646, + 1355.800048828125, + 1855.0, + 8.286550305783749, + 0.5, + 46.49329296313226, + 0.0 + ] + }, + { + "metrics": [ + 1878.0, + 5.78072021484375, + 25.0, + 1.52051008E+12, + 1878.0, + 133940.00244140625, + 126.0, + 2.736842105263158, + 67.37967914438502, + 135640.00244140625, + 13.168799972534181, + 4.556223812127182, + 1356.4000244140625, + 1865.0, + 8.286830009892583, + 0.0, + 46.49355808272958, + 0.0 + ] + }, + { + "metrics": [ + 1898.0, + 5.85377978515625, + 25.0, + 1.5205101E+12, + 1898.0, + 133980.0048828125, + 115.0, + 2.1578947368421053, + 61.49732620320856, + 135640.00244140625, + 11.620799732208253, + 5.163155840789837, + 1356.4000244140625, + 1885.0, + 8.28761505894363, + 0.0, + 46.493913140147924, + 0.032999999821186066 + ] + }, + { + "metrics": [ + 1914.0, + 5.903740234375, + 25.0, + 1.520510116E+12, + 1914.0, + 134000.0, + 111.0, + 1.9444444444444444, + 59.35828877005348, + 135640.00244140625, + 13.672800350189211, + 4.388274418939329, + 1356.4000244140625, + 1901.0, + 8.288095090538263, + 0.0, + 46.49421161971986, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 1934.0, + 5.9630498046875, + 25.0, + 1.520510136E+12, + 1934.0, + 134000.0, + 107.0, + 1.7222222222222223, + 57.219251336898395, + 135680.0048828125, + 9.370799732208253, + 6.402868669338306, + 1356.800048828125, + 1921.0, + 8.288601525127888, + 0.0, + 46.49460732936859, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 1946.0, + 6.0022900390625, + 25.0, + 1.520510148E+12, + 1946.0, + 134000.0, + 105.0, + 1.6111111111111112, + 56.149732620320854, + 135640.00244140625, + 9.439199924468994, + 6.35647093949812, + 1356.4000244140625, + 1933.0, + 8.288997150957584, + 0.0, + 46.494825426489115, + -0.13300000131130219 + ] + }, + { + "metrics": [ + 1958.0, + 6.04597998046875, + 25.0, + 1.52051016E+12, + 1958.0, + 134000.0, + 103.0, + 1.5, + 55.080213903743314, + 135640.00244140625, + 13.636800384521486, + 4.399859081321106, + 1356.4000244140625, + 1945.0, + 8.289461676031351, + 0.0, + 46.495038997381926, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 1979.0, + 6.1181201171875, + 25.0, + 1.520510181E+12, + 1979.0, + 134019.9951171875, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 135680.0048828125, + 12.427199649810794, + 4.828119101869706, + 1356.800048828125, + 1966.0, + 8.29035778529942, + 0.0, + 46.49521225132048, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 1997.0, + 6.18752001953125, + 24.0, + 1.520510199E+12, + 1997.0, + 133980.0048828125, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135680.0048828125, + 12.059999656677247, + 4.9751245207357755, + 1356.800048828125, + 1984.0, + 8.291259091347456, + 0.0, + 46.49525768123567, + 0.0 + ] + }, + { + "metrics": [ + 2005.0, + 6.20935986328125, + 24.0, + 1.520510207E+12, + 2005.0, + 133940.00244140625, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135700.0, + 6.552000188827516, + 9.157508895422826, + 1357.0, + 1992.0, + 8.291534604504704, + 0.0, + 46.49530059657991, + 0.0 + ] + }, + { + "metrics": [ + 2010.0, + 6.223080078125, + 24.0, + 1.520510212E+12, + 2010.0, + 133940.00244140625, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135740.00244140625, + 12.092399883270266, + 4.961794233666511, + 1357.4000244140625, + 1997.0, + 8.291604174301028, + 0.0, + 46.49540335871279, + 0.0 + ] + }, + { + "metrics": [ + 2032.0, + 6.287240234375, + 24.0, + 1.520510234E+12, + 2032.0, + 133980.0048828125, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 135700.0, + 11.58840036392212, + 5.177591222926377, + 1357.0, + 2019.0, + 8.291626302525401, + 0.0, + 46.49594868533313, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 2048.0, + 6.3525, + 24.0, + 1.52051025E+12, + 2048.0, + 133980.0048828125, + 91.0, + 0.9680851063829787, + 48.663101604278076, + 135719.9951171875, + 13.939199924468994, + 4.304407737683386, + 1357.199951171875, + 2035.0, + 8.292154697701335, + 0.0, + 46.49640541523695, + 0.0 + ] + }, + { + "metrics": [ + 2071.0, + 6.433509765625, + 24.0, + 1.520510273E+12, + 2071.0, + 134000.0, + 94.0, + 1.0, + 50.26737967914438, + 135780.0048828125, + 14.108399677276614, + 4.252785672682473, + 1357.800048828125, + 2058.0, + 8.292984422296286, + 0.5, + 46.496839514002204, + 0.032999999821186066 + ] + }, + { + "metrics": [ + 2091.0, + 6.50647021484375, + 24.0, + 1.520510293E+12, + 2091.0, + 133980.0048828125, + 94.0, + 1.0, + 50.26737967914438, + 135700.0, + 12.427199649810794, + 4.828119101869706, + 1357.0, + 2078.0, + 8.293810039758682, + 0.5, + 46.497138161212206, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 2112.0, + 6.58114990234375, + 24.0, + 1.520510314E+12, + 2112.0, + 134059.99755859375, + 94.0, + 1.0, + 50.26737967914438, + 135740.00244140625, + 11.790000343322756, + 5.089058376998343, + 1357.4000244140625, + 2099.0, + 8.29473054036498, + 0.0, + 46.49729423224926, + -0.03999999910593033 + ] + }, + { + "metrics": [ + 2130.0, + 6.643919921875, + 24.0, + 1.520510332E+12, + 2130.0, + 134159.99755859375, + 97.0, + 1.1666666666666667, + 51.8716577540107, + 135759.99755859375, + 13.471200370788576, + 4.453946074627924, + 1357.5999755859375, + 2117.0, + 8.29553185030818, + 0.0, + 46.497371681034565, + 0.0 + ] + }, + { + "metrics": [ + 2150.0, + 6.70125, + 24.0, + 1.520510352E+12, + 2150.0, + 134240.00244140625, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 135780.0048828125, + 11.11680021286011, + 5.397236512588483, + 1357.800048828125, + 2137.0, + 8.295840807259083, + 0.0, + 46.49780393578112, + -0.032999999821186066 + ] + }, + { + "metrics": [ + 2168.0, + 6.77127978515625, + 24.0, + 1.52051037E+12, + 2168.0, + 134300.0, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 135800.0, + 12.326400089263919, + 4.8676012118298, + 1358.0, + 2155.0, + 8.296402646228671, + 0.0, + 46.49827357381582, + 0.0 + ] + }, + { + "metrics": [ + 2185.0, + 6.829990234375, + 24.0, + 1.520510387E+12, + 2185.0, + 134300.0, + 95.0, + 1.0555555555555556, + 50.80213903743316, + 135859.99755859375, + 11.084399986267092, + 5.4130128907596635, + 1358.5999755859375, + 2172.0, + 8.296981835737824, + 0.0, + 46.4986115321517, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 2201.0, + 6.8803701171875, + 24.0, + 1.520510403E+12, + 2201.0, + 134300.0, + 99.0, + 1.2777777777777777, + 52.94117647058823, + 135800.0, + 11.253599739074708, + 5.331627337310409, + 1358.0, + 2188.0, + 8.297524563968182, + 0.0, + 46.49885896593332, + -0.07999999821186066 + ] + }, + { + "metrics": [ + 2219.0, + 6.93868017578125, + 23.0, + 1.520510421E+12, + 2219.0, + 134340.00244140625, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 135919.9951171875, + 11.959200096130372, + 5.017057957865774, + 1359.199951171875, + 2206.0, + 8.298107357695699, + 0.0, + 46.49918083101511, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 2233.0, + 6.98152978515625, + 23.0, + 1.520510435E+12, + 2233.0, + 134400.0, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 136019.9951171875, + 10.076400089263917, + 5.954507510666245, + 1360.199951171875, + 2220.0, + 8.298572385683656, + 0.5, + 46.499282754957676, + 0.2669999897480011 + ] + }, + { + "metrics": [ + 2243.0, + 7.00702001953125, + 23.0, + 1.520510445E+12, + 2243.0, + 134540.00244140625, + 98.0, + 1.2222222222222223, + 52.406417112299465, + 136119.9951171875, + 8.031599807739259, + 7.4704917386675485, + 1361.199951171875, + 2230.0, + 8.298870110884309, + 0.5, + 46.49917487986386, + 0.0 + ] + }, + { + "metrics": [ + 2249.0, + 7.02268017578125, + 23.0, + 1.520510451E+12, + 2249.0, + 134640.00244140625, + 113.0, + 2.0526315789473686, + 60.42780748663102, + 136259.99755859375, + 6.652800178527833, + 9.018758778544456, + 1362.5999755859375, + 2236.0, + 8.29906515777111, + 0.0, + 46.499130707234144, + 0.30000001192092896 + ] + }, + { + "metrics": [ + 2255.0, + 7.0378701171875, + 23.0, + 1.520510457E+12, + 2255.0, + 134740.00244140625, + 123.0, + 2.5789473684210527, + 65.77540106951872, + 136400.0, + 8.76960039138794, + 6.841816882662309, + 1364.0, + 2242.0, + 8.29926305450499, + 0.5, + 46.499098939821124, + 0.4000000059604645 + ] + }, + { + "metrics": [ + 2262.0, + 7.06031005859375, + 23.0, + 1.520510464E+12, + 2262.0, + 134700.0, + 129.0, + 2.8947368421052633, + 68.98395721925134, + 136419.9951171875, + 13.571999931335451, + 4.420866513082583, + 1364.199951171875, + 2249.0, + 8.299514260143042, + 0.5, + 46.49919834919274, + 0.0 + ] + }, + { + "metrics": [ + 2274.0, + 7.09408984375, + 23.0, + 1.520510476E+12, + 2274.0, + 134659.99755859375, + 134.0, + 3.1578947368421053, + 71.65775401069519, + 136459.99755859375, + 12.736800384521485, + 4.710759233136414, + 1364.5999755859375, + 2261.0, + 8.299881890416145, + 0.0, + 46.49936070665717, + 0.2669999897480011 + ] + }, + { + "metrics": [ + 2282.0, + 7.13116015625, + 23.0, + 1.520510484E+12, + 2282.0, + 134600.0, + 127.0, + 2.7894736842105265, + 67.9144385026738, + 136240.00244140625, + 15.825599670410156, + 3.7913255270942265, + 1362.4000244140625, + 2269.0, + 8.300314396619797, + 0.0, + 46.499506551772356, + -0.4000000059604645 + ] + }, + { + "metrics": [ + 2294.0, + 7.16341015625, + 23.0, + 1.520510496E+12, + 2294.0, + 134519.9951171875, + 117.0, + 2.263157894736842, + 62.5668449197861, + 136340.00244140625, + 8.913600254058839, + 6.731286831567165, + 1363.4000244140625, + 2281.0, + 8.300698539242148, + 0.5, + 46.499619372189045, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 2303.0, + 7.1977001953125, + 23.0, + 1.520510505E+12, + 2303.0, + 134400.0, + 113.0, + 2.0526315789473686, + 60.42780748663102, + 136219.9951171875, + 17.39880065917969, + 3.448513560636935, + 1362.199951171875, + 2290.0, + 8.301081759855151, + 0.0, + 46.49977250955999, + -0.05000000074505806 + ] + }, + { + "metrics": [ + 2310.0, + 7.23227978515625, + 23.0, + 1.520510512E+12, + 2310.0, + 134359.99755859375, + 110.0, + 1.8888888888888888, + 58.8235294117647, + 136140.00244140625, + 17.39880065917969, + 3.448513560636935, + 1361.4000244140625, + 2297.0, + 8.301345119252801, + 0.0, + 46.50001642294228, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 2319.0, + 7.27122021484375, + 23.0, + 1.520510521E+12, + 2319.0, + 134319.9951171875, + 107.0, + 1.7222222222222223, + 57.219251336898395, + 136000.0, + 11.991600322723391, + 5.003502318060374, + 1360.0, + 2306.0, + 8.301563886925578, + 0.5, + 46.500331331044436, + -0.10000000149011612 + ] + }, + { + "metrics": [ + 2335.0, + 7.33089013671875, + 23.0, + 1.520510537E+12, + 2335.0, + 134259.99755859375, + 104.0, + 1.5555555555555556, + 55.61497326203209, + 135959.99755859375, + 11.22839984893799, + 5.343593104913783, + 1359.5999755859375, + 2322.0, + 8.302184399217367, + 0.5, + 46.500637689605355, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 2349.0, + 7.37841015625, + 23.0, + 1.520510551E+12, + 2349.0, + 134200.0, + 101.0, + 1.3888888888888888, + 54.01069518716577, + 135980.0048828125, + 9.684000205993653, + 6.195786734377041, + 1359.800048828125, + 2336.0, + 8.302672812715173, + 0.0, + 46.50089057162404, + 0.0 + ] + }, + { + "metrics": [ + 2367.0, + 7.43566015625, + 23.0, + 1.520510569E+12, + 2367.0, + 134259.99755859375, + 95.0, + 1.0555555555555556, + 50.80213903743316, + 136000.0, + 11.656799697875979, + 5.147210346501261, + 1360.0, + 2354.0, + 8.303332049399614, + 0.0, + 46.50111151859164, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 2386.0, + 7.4944501953125, + 23.0, + 1.520510588E+12, + 2386.0, + 134500.0, + 92.0, + 0.9787234042553191, + 49.19786096256684, + 136080.0048828125, + 10.85039978027344, + 5.52975016838393, + 1360.800048828125, + 2373.0, + 8.30407996661961, + 0.0, + 46.501089222729206, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 2397.0, + 7.5285498046875, + 22.0, + 1.520510599E+12, + 2397.0, + 134659.99755859375, + 90.0, + 0.9574468085106383, + 48.1283422459893, + 136180.0048828125, + 10.314000034332278, + 5.817335642066864, + 1361.800048828125, + 2384.0, + 8.304515406489372, + 0.5, + 46.50105603039265, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 2408.0, + 7.55306982421875, + 22.0, + 1.52051061E+12, + 2408.0, + 134759.99755859375, + 96.0, + 1.1111111111111112, + 51.336898395721924, + 136459.99755859375, + 5.342399883270264, + 11.230907704960483, + 1364.5999755859375, + 2395.0, + 8.304834757000208, + 0.0, + 46.50105653330684, + 0.4000000059604645 + ] + }, + { + "metrics": [ + 2414.0, + 7.56647998046875, + 22.0, + 1.520510616E+12, + 2414.0, + 134859.99755859375, + 100.0, + 1.3333333333333333, + 53.475935828877006, + 136559.99755859375, + 7.009200096130371, + 8.560177936013657, + 1365.5999755859375, + 2401.0, + 8.305014548823237, + 0.5, + 46.50103717111051, + 0.30000001192092896 + ] + }, + { + "metrics": [ + 2418.0, + 7.575, + 22.0, + 1.52051062E+12, + 2418.0, + 134919.9951171875, + 113.0, + 2.0526315789473686, + 60.42780748663102, + 136619.9951171875, + 5.9364001750946045, + 10.1071353416709, + 1366.199951171875, + 2405.0, + 8.305125525221229, + 0.0, + 46.50101504288614, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2425.0, + 7.58883984375, + 22.0, + 1.520510627E+12, + 2425.0, + 135019.9951171875, + 125.0, + 2.6842105263157894, + 66.84491978609626, + 136740.00244140625, + 5.702399969100952, + 10.521885581003833, + 1367.4000244140625, + 2412.0, + 8.305307244881988, + 0.0, + 46.50101118721068, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 2433.0, + 7.6009599609375, + 22.0, + 1.520510635E+12, + 2433.0, + 135080.0048828125, + 131.0, + 3.0, + 70.05347593582887, + 136819.9951171875, + 4.8671999931335455, + 12.32741619342654, + 1368.199951171875, + 2420.0, + 8.305439678952098, + 0.0, + 46.50106466375291, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2435.0, + 7.60485009765625, + 22.0, + 1.520510637E+12, + 2435.0, + 135119.9951171875, + 132.0, + 3.0526315789473686, + 70.58823529411765, + 136900.0, + 7.4771996498107915, + 8.024394535662598, + 1369.0, + 2422.0, + 8.305491982027888, + 0.0, + 46.501058880239725, + 0.6000000238418579 + ] + }, + { + "metrics": [ + 2437.0, + 7.607490234375, + 22.0, + 1.520510639E+12, + 2437.0, + 135140.00244140625, + 133.0, + 3.1052631578947367, + 71.12299465240642, + 136959.99755859375, + 3.93480019569397, + 15.248550632294041, + 1369.5999755859375, + 2424.0, + 8.305526599287987, + 0.5, + 46.50106826797128, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2449.0, + 7.62931982421875, + 22.0, + 1.520510651E+12, + 2449.0, + 135419.9951171875, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 137140.00244140625, + 5.579999828338624, + 10.752688504985898, + 1371.4000244140625, + 2436.0, + 8.305810410529375, + 0.0, + 46.50110313668847, + 0.05000000074505806 + ] + }, + { + "metrics": [ + 2459.0, + 7.6512900390625, + 22.0, + 1.520510661E+12, + 2459.0, + 135619.9951171875, + 141.0, + 3.526315789473684, + 75.40106951871658, + 137300.0, + 8.355600357055664, + 7.180812562598761, + 1373.0, + 2446.0, + 8.30609162338078, + 0.0, + 46.50113674812019, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2473.0, + 7.68518994140625, + 22.0, + 1.520510675E+12, + 2473.0, + 135719.9951171875, + 141.0, + 3.526315789473684, + 75.40106951871658, + 137400.0, + 10.526400089263916, + 5.699954353169151, + 1374.0, + 2460.0, + 8.306491021066904, + 0.0, + 46.501252837479115, + 0.1599999964237213 + ] + }, + { + "metrics": [ + 2485.0, + 7.71881982421875, + 22.0, + 1.520510687E+12, + 2485.0, + 135840.00244140625, + 140.0, + 3.473684210526316, + 74.8663101604278, + 137440.00244140625, + 10.324800109863283, + 5.8112505204514315, + 1374.4000244140625, + 2472.0, + 8.306851107627153, + 0.5, + 46.50141963735223, + 0.07999999821186066 + ] + }, + { + "metrics": [ + 2490.0, + 7.731990234375, + 22.0, + 1.520510692E+12, + 2490.0, + 135740.00244140625, + 139.0, + 3.4210526315789473, + 74.33155080213903, + 137459.99755859375, + 8.647199821472169, + 6.938662370564385, + 1374.5999755859375, + 2477.0, + 8.306976584717631, + 0.5, + 46.50149876251817, + 0.03999999910593033 + ] + }, + { + "metrics": [ + 2495.0, + 7.74618017578125, + 22.0, + 1.520510697E+12, + 2495.0, + 135700.0, + 135.0, + 3.2105263157894735, + 72.19251336898395, + 137480.0048828125, + 12.39480028152466, + 4.840739556040634, + 1374.800048828125, + 2482.0, + 8.307142965495586, + 0.0, + 46.50154913775623, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 2500.0, + 7.769490234375, + 22.0, + 1.520510702E+12, + 2500.0, + 135659.99755859375, + 131.0, + 3.0, + 70.05347593582887, + 137359.99755859375, + 18.27360076904297, + 3.2834251317148775, + 1373.5999755859375, + 2487.0, + 8.307429207488894, + 0.0, + 46.501617869362235, + -0.30000001192092896 + ] + }, + { + "metrics": [ + 2508.0, + 7.82539013671875, + 22.0, + 1.52051071E+12, + 2508.0, + 135319.9951171875, + 125.0, + 2.6842105263157894, + 66.84491978609626, + 137019.9951171875, + 29.02320098876953, + 2.067311597890835, + 1370.199951171875, + 2495.0, + 8.308077044785023, + 0.0, + 46.5018474496901, + -0.6000000238418579 + ] + }, + { + "metrics": [ + 2515.0, + 7.8799599609375, + 22.0, + 1.520510717E+12, + 2515.0, + 134859.99755859375, + 118.0, + 2.3157894736842106, + 63.101604278074866, + 136640.00244140625, + 26.287200164794925, + 2.2824796720783858, + 1366.4000244140625, + 2502.0, + 8.308646343648434, + 0.0, + 46.502135284245014, + -0.5 + ] + }, + { + "metrics": [ + 2521.0, + 7.9227099609375, + 21.0, + 1.520510723E+12, + 2521.0, + 134719.9951171875, + 110.0, + 1.8888888888888888, + 58.8235294117647, + 136340.00244140625, + 22.95359973907471, + 2.613969080843556, + 1363.4000244140625, + 2508.0, + 8.308982206508517, + 0.0, + 46.50243946351111, + -0.4000000059604645 + ] + }, + { + "metrics": [ + 2532.0, + 7.9782900390625, + 21.0, + 1.520510734E+12, + 2532.0, + 134740.00244140625, + 103.0, + 1.5, + 55.080213903743314, + 136159.99755859375, + 13.968000411987305, + 4.295532520210132, + 1361.5999755859375, + 2519.0, + 8.309395015239716, + 0.5, + 46.502848165109754, + -0.16699999570846558 + ] + }, + { + "metrics": [ + 2539.0, + 8.00168017578125, + 21.0, + 1.520510741E+12, + 2539.0, + 134719.9951171875, + 114.0, + 2.1052631578947367, + 60.962566844919785, + 136159.99755859375, + 9.446400260925294, + 6.351625842088008, + 1361.5999755859375, + 2526.0, + 8.309613112360239, + 0.0, + 46.50299409404397, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2547.0, + 8.025830078125, + 21.0, + 1.520510749E+12, + 2547.0, + 134700.0, + 127.0, + 2.7894736842105265, + 67.9144385026738, + 136200.0, + 8.29800024032593, + 7.230657781909551, + 1362.0, + 2534.0, + 8.309820229187608, + 0.0, + 46.50315544568002, + 0.06700000166893005 + ] + }, + { + "metrics": [ + 2556.0, + 8.05377978515625, + 21.0, + 1.520510758E+12, + 2556.0, + 134680.0048828125, + 138.0, + 3.3684210526315788, + 73.79679144385027, + 136219.9951171875, + 4.939199924468994, + 12.147716417543174, + 1362.199951171875, + 2543.0, + 8.310019383206964, + 0.5, + 46.50336239486933, + 0.0 + ] + }, + { + "metrics": [ + 2568.0, + 8.081990234375, + 21.0, + 1.52051077E+12, + 2568.0, + 134659.99755859375, + 140.0, + 3.473684210526316, + 74.8663101604278, + 136280.0048828125, + 7.592399883270264, + 7.902639604666907, + 1362.800048828125, + 2555.0, + 8.31023370847106, + 0.0, + 46.50356691330671, + 0.15000000596046448 + ] + }, + { + "metrics": [ + 2583.0, + 8.1234501953125, + 21.0, + 1.520510785E+12, + 2583.0, + 134659.99755859375, + 140.0, + 3.473684210526316, + 74.8663101604278, + 136340.00244140625, + 8.028000068664552, + 7.473841492129052, + 1363.4000244140625, + 2570.0, + 8.310562027618289, + 0.0, + 46.50385835207999, + 0.10000000149011612 + ] + }, + { + "metrics": [ + 2594.0, + 8.1570498046875, + 21.0, + 1.520510796E+12, + 2594.0, + 134700.0, + 140.0, + 3.473684210526316, + 74.8663101604278, + 136319.9951171875, + 10.717199993133546, + 5.59847721890435, + 1363.199951171875, + 2581.0, + 8.31085472367704, + 0.5, + 46.50407929904759, + -0.20000000298023224 + ] + }, + { + "metrics": [ + 2603.0, + 8.1825400390625, + 21.0, + 1.520510805E+12, + 2603.0, + 134740.00244140625, + 138.0, + 3.3684210526315788, + 73.79679144385027, + 136340.00244140625, + 7.7255996704101575, + 7.766387409615099, + 1363.4000244140625, + 2590.0, + 8.311040299013257, + 0.5, + 46.50426160544157, + 0.20000000298023224 + ] + }, + { + "metrics": [ + 2606.0, + 8.1887900390625, + 21.0, + 1.520510808E+12, + 2606.0, + 134740.00244140625, + 138.0, + 3.3684210526315788, + 73.79679144385027, + 136340.00244140625, + 3.697199821472168, + 16.228498028031638, + 1363.4000244140625, + 2593.0, + 8.311029067263007, + 0.5, + 46.504317596554756, + 0.0 + ] + }, + { + "metrics": [ + 2623.0, + 8.22376953125, + 21.0, + 1.520510825E+12, + 2623.0, + 134780.0048828125, + 136.0, + 3.263157894736842, + 72.72727272727273, + 136180.0048828125, + 3.830400037765503, + 15.664160249695882, + 1361.800048828125, + 2610.0, + 8.310985062271357, + 0.0, + 46.50462948717177, + -0.13300000131130219 + ] + }, + { + "metrics": [ + 2625.0, + 8.2250302734375, + 21.0, + 1.520510827E+12, + 2625.0, + 134780.0048828125, + 132.0, + 3.0526315789473686, + 70.58823529411765, + 136180.0048828125, + 1.6128000497817996, + 18.641135770848248, + 1361.800048828125, + 2612.0, + 8.310986068099737, + 0.0, + 46.50464029982686, + 0.0 + ] + }, + { + "metrics": [ + 2627.0, + 8.2255302734375, + 21.0, + 1.520510829E+12, + 2627.0, + 134780.0048828125, + 130.0, + 2.9473684210526314, + 69.5187165775401, + 136180.0048828125, + 1.5083999991416934, + 18.641135770848248, + 1361.800048828125, + 2614.0, + 8.310995120555162, + 0.0, + 46.50464407168329, + 0.0 + ] + } + ], + "detailsAvailable": true + } +} diff --git a/garmin-connect-export/json/activity_2541953812_zones.json b/garmin-connect-export/json/activity_2541953812_zones.json new file mode 100644 index 0000000..baed48f --- /dev/null +++ b/garmin-connect-export/json/activity_2541953812_zones.json @@ -0,0 +1,27 @@ +[ + { + "zoneNumber": 3, + "secsInZone": 567.662, + "zoneLowBoundary": 148 + }, + { + "zoneNumber": 1, + "secsInZone": 2462.848, + "zoneLowBoundary": 100 + }, + { + "zoneNumber": 2, + "secsInZone": 1689.269, + "zoneLowBoundary": 138 + }, + { + "zoneNumber": 4, + "secsInZone": 0.0, + "zoneLowBoundary": 168 + }, + { + "zoneNumber": 5, + "secsInZone": 0.0, + "zoneLowBoundary": 182 + } +] \ No newline at end of file diff --git a/garmin-connect-export/json/activity_995784118_gpx_device_0.json b/garmin-connect-export/json/activity_995784118_gpx_device_0.json new file mode 100644 index 0000000..ae8b3f8 --- /dev/null +++ b/garmin-connect-export/json/activity_995784118_gpx_device_0.json @@ -0,0 +1,100 @@ +{ + "activityId": 995784118, + "activityName": "Cournillens (Runkeeper)", + "userProfileId": 2836200, + "isMultiSportParent": false, + "activityTypeDTO": { + "typeId": 12, + "typeKey": "uncategorized", + "parentTypeId": 17, + "sortOrder": 2 + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 4, + "typeKey": "groups" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 756265, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 2, + "formatKey": "gpx" + }, + "associatedCourseId": null, + "lastUpdateDate": "2016-02-06T17:42:12.0", + "uploadedDate": "2015-12-28T23:18:11.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": false, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": { + "showDistance": true, + "showDuration": true, + "showElevation": true, + "showHeartRate": true, + "showMovingDuration": true, + "showMovingSpeed": true, + "showSpeed": true, + "showTimestamp": true + }, + "childIds": [], + "sensors": null, + "activityImages": [], + "manufacturer": "", + "diveNumber": null, + "lapCount": 1, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": "0", + "deviceTypePk": 19, + "deviceVersionPk": 756265 + }, + "gcj02": false, + "favorite": false, + "autoCalcCalories": false, + "elevationCorrected": true, + "personalRecord": false, + "manualActivity": false + }, + "summaryDTO": { + "startTimeLocal": "2015-12-27T13:05:44.0", + "startTimeGMT": "2015-12-27T12:05:44.0", + "distance": 9943.03, + "duration": 3620.0, + "movingDuration": 3543.0, + "elapsedDuration": 3620.0, + "elevationGain": 104.05, + "elevationLoss": 108.5, + "maxElevation": 646.57, + "minElevation": 570.99, + "averageSpeed": 2.746692120858901, + "averageMovingSpeed": 2.806385965991877, + "maxSpeed": 7.874701976886471, + "averageHR": 149.0, + "maxHR": 182.0 + } +} diff --git a/garmin-connect-export/json/activity_emoji.json b/garmin-connect-export/json/activity_emoji.json new file mode 100644 index 0000000..d5d96ab --- /dev/null +++ b/garmin-connect-export/json/activity_emoji.json @@ -0,0 +1,153 @@ +{ + "activityId": 2958378924, + "activityName": "Biel 🏛 Pavillon", + "userProfileId": 2836200, + "isMultiSportParent": false, + "activityTypeDTO": { + "typeId": 1, + "typeKey": "running", + "parentTypeId": 17, + "sortOrder": 3 + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 3, + "typeKey": "subscribers" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 856399, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 7, + "formatKey": "fit" + }, + "associatedCourseId": null, + "lastUpdateDate": "2018-08-24T05:54:31.0", + "uploadedDate": "2018-08-24T05:38:55.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": true, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": { + "showAirTemperature": true, + "showDistance": true, + "showDuration": true, + "showElevation": true, + "showHeartRate": true, + "showMovingDuration": true, + "showMovingSpeed": true, + "showRunCadence": true, + "showSpeed": true, + "showTimestamp": true + }, + "childIds": [], + "sensors": [ + { + "sku": "006-B2697-00", + "sourceType": "LOCAL", + "softwareVersion": 10.0 + }, + { + "sku": "006-B2697-00", + "sourceType": "LOCAL", + "localDeviceType": "BAROMETER", + "softwareVersion": 10.0 + }, + { + "sku": "006-B1621-00", + "sourceType": "LOCAL", + "localDeviceType": "GPS", + "softwareVersion": 4.4 + }, + { + "sourceType": "LOCAL", + "localDeviceType": "ACCELEROMETER", + "softwareVersion": 0.0 + }, + { + "sku": "006-B0000-00", + "sourceType": "LOCAL", + "localDeviceType": "BLUETOOTH_LOW_ENERGY_CHIPSET", + "softwareVersion": 606.72 + }, + { + "sourceType": "LOCAL", + "localDeviceType": "WHR", + "softwareVersion": 3.31 + } + ], + "activityImages": [], + "manufacturer": "GARMIN", + "diveNumber": null, + "lapCount": 5, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": "3946806421", + "deviceTypePk": 34236, + "deviceVersionPk": 856399 + }, + "gcj02": false, + "autoCalcCalories": false, + "elevationCorrected": false, + "favorite": false, + "manualActivity": false, + "personalRecord": false + }, + "summaryDTO": { + "startTimeLocal": "2018-08-24T12:01:50.0", + "startTimeGMT": "2018-08-24T10:01:50.0", + "startLatitude": 47.13532981462777, + "startLongitude": 7.243065973743796, + "distance": 4146.45, + "duration": 2147.244, + "movingDuration": 1844.0, + "elapsedDuration": 2147.244, + "elevationGain": 101.0, + "elevationLoss": 99.0, + "maxElevation": 545.6, + "minElevation": 444.2, + "averageSpeed": 1.9309999942779543, + "averageMovingSpeed": 2.2486172425772777, + "maxSpeed": 3.6579999923706055, + "calories": 357.0388685495535, + "averageHR": 132.0, + "maxHR": 160.0, + "averageRunCadence": 124.03125, + "maxRunCadence": 187.0, + "averageTemperature": 26.007921714818266, + "maxTemperature": 29.0, + "minTemperature": 24.0, + "strideLength": 92.05550090389445, + "trainingEffect": 2.4000000953674316, + "anaerobicTrainingEffect": 0.10000000149011612, + "aerobicTrainingEffectMessage": "MAINTAINING_AEROBIC_FITNESS_1", + "anaerobicTrainingEffectMessage": "NO_ANAEROBIC_BENEFIT_0", + "endLatitude": 47.133489567786455, + "endLongitude": 7.231966909021139, + "maxVerticalSpeed": 0.600006103515625 + }, + "locationName": "Biel/Bienne" +} diff --git a/garmin-connect-export/json/activity_multisport_child.json b/garmin-connect-export/json/activity_multisport_child.json new file mode 100644 index 0000000..19a75ea --- /dev/null +++ b/garmin-connect-export/json/activity_multisport_child.json @@ -0,0 +1,118 @@ +{ + "activityId": 6588349076, + "activityUUID": { + "uuid": "925e94a1-1044-463d-98ec-5dfc631b34ee" + }, + "activityName": "Bike-Walk-Bike - Gehen", + "userProfileId": 2836200, + "parentId": 6588349056, + "isMultiSportParent": false, + "activityTypeDTO": { + "typeId": 9, + "typeKey": "walking", + "parentTypeId": 17, + "sortOrder": 27, + "isHidden": false, + "restricted": false + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 3, + "typeKey": "subscribers" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 880836, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 7, + "formatKey": "fit" + }, + "associatedCourseId": null, + "lastUpdateDate": "2021-04-11T13:48:04.0", + "uploadedDate": "2021-04-11T12:59:56.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": true, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": {}, + "childIds": [], + "childActivityTypes": [], + "sensors": [], + "activityImages": [], + "manufacturer": "GARMIN", + "diveNumber": null, + "lapCount": 3, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": "3946806421", + "deviceTypePk": 34236, + "deviceVersionPk": 880836 + }, + "hasIntensityIntervals": false, + "hasSplits": false, + "manualActivity": false, + "autoCalcCalories": false, + "favorite": false, + "trimmed": false, + "personalRecord": false, + "gcj02": false, + "elevationCorrected": false + }, + "summaryDTO": { + "startTimeLocal": "2021-04-11T12:36:16.0", + "startTimeGMT": "2021-04-11T10:36:16.0", + "startLatitude": 46.70255539007485, + "startLongitude": 7.202448779717088, + "distance": 4036.26, + "duration": 4375.461, + "movingDuration": 4020.0, + "elapsedDuration": 5332.783, + "elevationGain": 162.0, + "elevationLoss": 160.0, + "maxElevation": 1457.8, + "minElevation": 1362.2, + "averageSpeed": 0.921999990940094, + "averageMovingSpeed": 1.004044778548663, + "maxSpeed": 1.5399999618530273, + "calories": 385.0, + "averageHR": 125.0, + "maxHR": 152.0, + "averageRunCadence": 67.65625, + "maxRunCadence": 176.0, + "averageTemperature": 21.299496217983616, + "maxTemperature": 24.0, + "minTemperature": 17.0, + "trainingEffect": 3.4000000953674316, + "anaerobicTrainingEffect": 1.600000023841858, + "aerobicTrainingEffectMessage": "IMPROVING_LACTATE_THRESHOLD_12", + "anaerobicTrainingEffectMessage": "MINOR_ANAEROBIC_BENEFIT_15", + "endLatitude": 46.69704956933856, + "endLongitude": 7.196000078693032, + "maxVerticalSpeed": 0.5999755859375, + "minActivityLapDuration": 663.623 + }, + "locationName": "Plasselb" +} \ No newline at end of file diff --git a/garmin-connect-export/json/activity_multisport_detail.json b/garmin-connect-export/json/activity_multisport_detail.json new file mode 100644 index 0000000..5884567 --- /dev/null +++ b/garmin-connect-export/json/activity_multisport_detail.json @@ -0,0 +1,112 @@ +{ + "activityId": 6588349056, + "activityUUID": { + "uuid": "35ac0d3f-e43a-40c1-88de-c9c664998afd" + }, + "activityName": "Bike-Walk-Bike", + "userProfileId": 2836200, + "isMultiSportParent": true, + "activityTypeDTO": { + "typeId": 89, + "typeKey": "multi_sport", + "parentTypeId": 17, + "sortOrder": 58, + "isHidden": false, + "restricted": false + }, + "eventTypeDTO": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "accessControlRuleDTO": { + "typeId": 3, + "typeKey": "subscribers" + }, + "timeZoneUnitDTO": { + "unitId": 124, + "unitKey": "Europe/Paris", + "factor": 0.0, + "timeZone": "Europe/Paris" + }, + "metadataDTO": { + "isOriginal": true, + "deviceApplicationInstallationId": 880836, + "agentApplicationInstallationId": null, + "agentString": null, + "fileFormat": { + "formatId": 7, + "formatKey": "fit" + }, + "associatedCourseId": null, + "lastUpdateDate": "2021-04-11T13:48:04.0", + "uploadedDate": "2021-04-11T12:59:56.0", + "videoUrl": null, + "hasPolyline": true, + "hasChartData": true, + "hasHrTimeInZones": false, + "hasPowerTimeInZones": false, + "userInfoDto": { + "userProfilePk": 2836200, + "displayname": "eschep", + "fullname": "Peter Steiner", + "profileImageUrlLarge": null, + "profileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "profileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "userPro": false + }, + "chartAvailability": {}, + "childIds": [ + 6588349067, + 6588349072, + 6588349076, + 6588349079, + 6588349081 + ], + "childActivityTypes": [ + "cycling", + "transition", + "walking", + "transition", + "cycling" + ], + "sensors": null, + "activityImages": [], + "manufacturer": "GARMIN", + "diveNumber": null, + "lapCount": 5, + "associatedWorkoutId": null, + "isAtpActivity": null, + "deviceMetaDataDTO": { + "deviceId": "3946806421", + "deviceTypePk": 34236, + "deviceVersionPk": 880836 + }, + "hasIntensityIntervals": false, + "hasSplits": false, + "manualActivity": false, + "autoCalcCalories": false, + "favorite": false, + "personalRecord": false, + "trimmed": false, + "gcj02": false, + "elevationCorrected": false + }, + "summaryDTO": { + "startTimeLocal": "2021-04-11T11:50:49.0", + "startTimeGMT": "2021-04-11T09:50:49.0", + "distance": 38351.41, + "duration": 10226.371, + "elapsedDuration": 11183.693, + "elevationGain": 991.0, + "elevationLoss": 994.0, + "averageSpeed": 3.7502462545896287, + "calories": 1139.0, + "maxHR": 175.0, + "trainingEffect": 3.4000000953674316, + "anaerobicTrainingEffect": 1.600000023841858, + "aerobicTrainingEffectMessage": "IMPROVING_LACTATE_THRESHOLD_12", + "anaerobicTrainingEffectMessage": "MINOR_ANAEROBIC_BENEFIT_15" + }, + "locationName": "Plasselb" +} diff --git a/garmin-connect-export/json/activity_multisport_overview.json b/garmin-connect-export/json/activity_multisport_overview.json new file mode 100644 index 0000000..29a09b9 --- /dev/null +++ b/garmin-connect-export/json/activity_multisport_overview.json @@ -0,0 +1,210 @@ +{ + "activityId": 6588349056, + "activityName": "Bike-Walk-Bike", + "description": null, + "startTimeLocal": "2021-04-11 11:50:49", + "startTimeGMT": "2021-04-11 09:50:49", + "activityType": { + "typeId": 89, + "typeKey": "multi_sport", + "parentTypeId": 17, + "sortOrder": 58, + "isHidden": false, + "restricted": false + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 38351.40899467468, + "duration": 10226.370854377747, + "elapsedDuration": 11183693.120002747, + "movingDuration": null, + "elevationGain": 991.0, + "elevationLoss": 994.0, + "averageSpeed": 3.7502462545896287, + "maxSpeed": null, + "startLatitude": 46.66666666666666, + "startLongitude": 7.111111111111111, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/c957e6a9-adb2-4cdb-8105-bf0739323504-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/1a8b37ca-2cd1-47bf-a191-9326dd26949f-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/5979a7d6-2502-4869-8f57-86a28ab7eab3-2836200.png", + "calories": 1139.0, + "averageHR": null, + "maxHR": 175.0, + "averageRunningCadenceInStepsPerMinute": null, + "maxRunningCadenceInStepsPerMinute": null, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": null, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "activityLikeProfileImageUrls": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_OUTDOOR_USER", + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 3, + "typeKey": "subscribers" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": null, + "sportTypeId": 18, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 3.4000000953674316, + "anaerobicTrainingEffect": 1.600000023841858, + "strokes": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": null, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": null, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": null, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": null, + "maxTemperature": null, + "minElevation": null, + "maxElevation": null, + "avgDoubleCadence": null, + "maxDoubleCadence": null, + "summarizedExerciseSets": null, + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": null + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": null, + "floorsClimbed": null, + "floorsDescended": null, + "manufacturer": "GARMIN", + "diveNumber": null, + "locationName": "Plasselb", + "bottomTime": null, + "lapCount": 5, + "endLatitude": 46.77777777777777, + "endLongitude": 7.222222222222222, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "flow": null, + "grit": null, + "jumpCount": null, + "caloriesEstimated": null, + "caloriesConsumed": null, + "waterEstimated": null, + "waterConsumed": null, + "maxAvgPower_1": null, + "maxAvgPower_2": null, + "maxAvgPower_5": null, + "maxAvgPower_10": null, + "maxAvgPower_20": null, + "maxAvgPower_30": null, + "maxAvgPower_60": null, + "maxAvgPower_120": null, + "maxAvgPower_300": null, + "maxAvgPower_600": null, + "maxAvgPower_1200": null, + "maxAvgPower_1800": null, + "maxAvgPower_3600": null, + "maxAvgPower_7200": null, + "maxAvgPower_18000": null, + "excludeFromPowerCurveReports": null, + "totalSets": null, + "activeSets": null, + "totalReps": null, + "minRespirationRate": null, + "maxRespirationRate": null, + "avgRespirationRate": null, + "trainingEffectLabel": null, + "activityTrainingLoad": null, + "avgFlow": null, + "avgGrit": null, + "minActivityLapDuration": null, + "avgStress": null, + "startStress": null, + "endStress": null, + "differenceStress": null, + "maxStress": null, + "aerobicTrainingEffectMessage": "IMPROVING_LACTATE_THRESHOLD_12", + "anaerobicTrainingEffectMessage": "MINOR_ANAEROBIC_BENEFIT_15", + "splitSummaries": [], + "hasSplits": false, + "hasSeedFirstbeatProfile": null, + "favorite": false, + "decoDive": null, + "purposeful": false, + "pr": false, + "autoCalcCalories": false, + "parent": true, + "atpActivity": false, + "manualActivity": false, + "elevationCorrected": false +} diff --git a/garmin-connect-export/json/activity_types.properties b/garmin-connect-export/json/activity_types.properties new file mode 100644 index 0000000..225c98a --- /dev/null +++ b/garmin-connect-export/json/activity_types.properties @@ -0,0 +1,123 @@ +activity_type_all=All Activities +activity_type_any=Any Activity Type +activity_type_atv=ATV +activity_type_backcountry_skiing_snowboarding=Backcountry Skiing/Snowboarding +activity_type_breathwork=Breathwork +activity_type_bikeToRunTransition=Bike to Run Transition +activity_type_bmx=BMX +activity_type_boating=Boating +activity_type_casual_walking=Casual Walking +activity_type_cross_country_skiing=Cross Country Classic Skiing +cross_country_skiing=Cross Country Skiing +activity_type_cycling=Cycling +activity_type_cyclocross=Cyclocross +activity_type_downhill_biking=Downhill Biking +activity_type_driving_general=Driving +activity_type_elliptical=Elliptical +activity_type_fitness_equipment=Gym & Fitness Equipment +activity_type_flying=Flying +activity_type_golf=Golf +activity_type_gravel_cycling=Gravel/Unpaved Cycling +activity_type_hang_gliding=Hang Gliding +activity_type_hiking=Hiking +activity_type_horseback_riding=Horseback Riding +activity_type_hunting_fishing=Hunting/Fishing +activity_type_indoor_cardio=Cardio +activity_type_indoor_cycling=Indoor Cycling +activity_type_indoor_rowing=Indoor Rowing +activity_type_inline_skating=Inline Skating +activity_type_lap_swimming=Pool Swimming +activity_type_motocross=Motocross +activity_type_motorcycling=Motorcycling +activity_type_mountain_biking=Mountain Biking +activity_type_mountaineering=Mountaineering +activity_type_multi_sport=Multisport +activity_type_open_water_swimming=Open Water Swimming +open_water_swimming=Swim +activity_type_other=Other +activity_type_paddling=Paddling +activity_type_rc_drone=RC/Drone +activity_type_recumbent_cycling=Recumbent Cycling +activity_type_resort_skiing_snowboarding=Resort Skiing/Snowboarding +activity_type_road_biking=Road Cycling +activity_type_road_cycling=Road Cycling +activity_type_rock_climbing=Rock Climbing +activity_type_rowing=Rowing +activity_type_runToBikeTransition=Run to Bike Transition +activity_type_running=Running +activity_type_sailing=Sailing +activity_type_skate_skiing=Cross Country Skate Skiing +activity_type_skating=Skating +activity_type_sky_diving=Sky Diving +activity_type_snow_shoe=Snowshoeing +activity_type_snowmobiling=Snowmobiling +activity_type_speed_walking=Speed Walking +activity_type_stair_climbing=Stair Stepper +activity_type_stand_up_paddleboarding=Stand Up Paddleboarding +activity_type_step_tracking_and_walking=Step Tracking and Walking +activity_type_steps=Steps +activity_type_street_running=Street Running +activity_type_strength_training=Strength Training +activity_type_surfing=Surfing +activity_type_swimToBikeTransition=Swim to Bike Transition +activity_type_swimming=Swimming +activity_type_tennis=Tennis +activity_type_track_cycling=Track Cycling +activity_type_track_running=Track Running +activity_type_trail_running=Trail Running +activity_type_transition=Transition +activity_type_treadmill_running=Treadmill Running +activity_type_treadmill=Treadmill +activity_type_indoor_track=Indoor Track +activity_type_indoor_bike=Bike Indoor +activity_type_indoor_walk=Walk Indoor +activity_type_triathlon=Triathlon +activity_type_uncategorized=Uncategorized +activity_type_wakeboarding=Wakeboarding +activity_type_walking=Walking +activity_type_whitewater_rafting_kayaking=Whitewater Kayaking/Rafting +activity_type_wind_kite_surfing=Wind/Kite Surfing +activity_type_wingsuit_flying=Wingsuit Flying +activity_type_yoga=Yoga +activity_type_toe_to_toe=Toe-to-Toe™ +activity_type_toe_to_toe_no_tm=Toe-to-Toe +activity_type_floor_climbing=Floor Climbing +activity_type_emergency=Emergency +activity_type_safety=Safety +activity_type_safety_event=Safety Event +activity_type_incident_detected=Incident Detected +select_one=Select one... +any_activity_type=Any Activity Type +uncategorized=Uncategorized +calories_burned=Calories Burned +activity_type_cardio_training=Cardio Training +activity_type_diving=Diving +activity_type_multi_gas_diving=Multi-Gas Dive +activity_type_single_gas_diving=Single-Gas Dive +activity_type_apnea_diving=Apnea +activity_type_gauge_diving=Gauge Dive +activity_type_apnea_hunting=Apnea Hunt +activity_type_ccr_diving=CCR Dive +activity_type_obstacle_run=Obstacle Running +activity_type_virtual_run=Virtual Running +activity_type_virtual_ride=Virtual Cycling +activity_type_stop_watch=Stopwatch +activity_type_indoor_running=Indoor Running +activity_type_indoor_walking=Indoor Walking +activity_type_pilates=Pilates +activity_type_auto_racing=Auto Racing +activity_type_winter_sports=Winter Sports +activity_type_backcountry_skiing_snowboarding_ws=Backcountry Skiing/Snowboarding +activity_type_cross_country_skiing_ws=Cross Country Classic Skiing +activity_type_resort_skiing_snowboarding_ws=Resort Skiing/Snowboarding +activity_type_skate_skiing_ws=Cross Country Skate Skiing +activity_type_skating_ws=Skating +activity_type_snow_shoe_ws=Snowshoeing +activity_type_snowmobiling_ws=Snowmobiling +activity_type_indoor_climbing=Indoor Climbing +activity_type_bouldering=Bouldering +activity_type_offshore_grinding=Offshore Grinding +activity_type_onshore_grinding=Onshore Grinding +activity_type_ultra_run=Ultra Running +activity_type_hiit=HIIT +activity_type_e_sport=Esports diff --git a/garmin-connect-export/json/activitylist-service.json b/garmin-connect-export/json/activitylist-service.json new file mode 100644 index 0000000..c753678 --- /dev/null +++ b/garmin-connect-export/json/activitylist-service.json @@ -0,0 +1,165 @@ +[ + { + "activityId": 2541953812, + "activityName": "Biel 🏛 Pavillon", + "description": null, + "startTimeLocal": "2018-03-08 12:23:22", + "startTimeGMT": "2018-03-08 11:23:22", + "activityType": { + "typeId": 81, + "typeKey": "cross_country_skiing", + "parentTypeId": 4, + "sortOrder": 39 + }, + "eventType": { + "typeId": 9, + "typeKey": "uncategorized", + "sortOrder": 10 + }, + "comments": null, + "parentId": null, + "distance": 8225.5302734375, + "duration": 2627.3798828125, + "elapsedDuration": 2627379.8828125, + "movingDuration": 2614.0, + "elevationGain": 106.0, + "elevationLoss": 73.0, + "averageSpeed": 3.13100004196167, + "maxSpeed": 10.51200008392334, + "startLatitude": 46.46675166673958, + "startLongitude": 8.243478052318096, + "hasPolyline": true, + "ownerId": 2836200, + "ownerDisplayName": "eschep", + "ownerFullName": "Peter Steiner", + "ownerProfileImageUrlSmall": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/773876c5-f1fe-4f94-9acd-991eeeb69c24-2836200.png", + "ownerProfileImageUrlMedium": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/92900e93-d959-4a73-bab0-6d18a21b6f76-2836200.png", + "ownerProfileImageUrlLarge": "https://s3.amazonaws.com/garmin-connect-prod/profile_images/eea51d41-c67c-437c-a4c9-dc76f637fe68-2836200.png", + "ownerProfilePk": null, + "calories": 319.0, + "averageHR": 110.0, + "maxHR": 146.0, + "averageRunningCadenceInStepsPerMinute": 34.765625, + "maxRunningCadenceInStepsPerMinute": 220.0, + "averageBikingCadenceInRevPerMinute": null, + "maxBikingCadenceInRevPerMinute": null, + "averageSwimCadenceInStrokesPerMinute": null, + "maxSwimCadenceInStrokesPerMinute": null, + "averageSwolf": null, + "activeLengths": null, + "steps": 3522, + "conversationUuid": null, + "conversationPk": null, + "numberOfActivityLikes": null, + "numberOfActivityComments": null, + "likedByUser": null, + "commentedByUser": null, + "activityLikeDisplayNames": null, + "activityLikeFullNames": null, + "requestorRelationship": null, + "userRoles": [ + "ROLE_CONNECTUSER", + "ROLE_FITNESS_USER", + "ROLE_WELLNESS_USER", + "ROLE_OUTDOOR_USER", + "ROLE_CONNECT_2_USER" + ], + "privacy": { + "typeId": 3, + "typeKey": "subscribers" + }, + "userPro": false, + "courseId": null, + "poolLength": null, + "unitOfPoolLength": null, + "hasVideo": false, + "videoUrl": null, + "timeZoneId": 124, + "beginTimestamp": 1520508202000, + "sportTypeId": 0, + "avgPower": null, + "maxPower": null, + "aerobicTrainingEffect": 1.399999976158142, + "anaerobicTrainingEffect": 0.10000000149011612, + "strokes": null, + "avgEfficiency": null, + "minEfficiency": null, + "normPower": null, + "leftBalance": null, + "rightBalance": null, + "avgLeftBalance": null, + "max20MinPower": null, + "avgVerticalOscillation": null, + "avgGroundContactTime": null, + "avgStrideLength": 615.1658983791259, + "avgFractionalCadence": null, + "maxFractionalCadence": null, + "trainingStressScore": null, + "intensityFactor": null, + "vO2MaxValue": null, + "avgVerticalRatio": null, + "avgGroundContactBalance": null, + "lactateThresholdBpm": null, + "lactateThresholdSpeed": null, + "maxFtp": null, + "avgStrokeDistance": null, + "avgStrokeCadence": null, + "maxStrokeCadence": null, + "workoutId": null, + "avgStrokes": null, + "minStrokes": null, + "deviceId": 3946806421, + "minTemperature": 21.0, + "maxTemperature": null, + "minElevation": 132600.0, + "maxElevation": 137480.0048828125, + "avgDoubleCadence": null, + "maxDoubleCadence": 220.0, + "summarizedExerciseSets": [], + "maxDepth": null, + "avgDepth": null, + "surfaceInterval": null, + "startN2": null, + "endN2": null, + "startCns": null, + "endCns": null, + "summarizedDiveInfo": { + "weight": null, + "weightUnit": null, + "visibility": null, + "visibilityUnit": null, + "surfaceCondition": null, + "current": null, + "waterType": null, + "waterDensity": null, + "summarizedDiveGases": [], + "totalSurfaceTime": 0 + }, + "activityLikeAuthors": null, + "avgVerticalSpeed": null, + "maxVerticalSpeed": 0.5999755859375, + "floorsClimbed": null, + "floorsDescended": null, + "diveNumber": null, + "locationName": "Goms", + "bottomTime": null, + "lapCount": 9, + "endLatitude": 46.469942070543766, + "endLongitude": 8.248067228123546, + "minAirSpeed": null, + "maxAirSpeed": null, + "avgAirSpeed": null, + "avgWindYawAngle": null, + "minCda": null, + "maxCda": null, + "avgCda": null, + "avgWattsPerCda": null, + "favorite": false, + "decoDive": null, + "pr": false, + "autoCalcCalories": false, + "parent": false, + "elevationCorrected": false, + "purposeful": false + } +] diff --git a/garmin-connect-export/json/device_856399.json b/garmin-connect-export/json/device_856399.json new file mode 100644 index 0000000..722817c --- /dev/null +++ b/garmin-connect-export/json/device_856399.json @@ -0,0 +1,35 @@ +{ + "applicationKey" : "fenix5", + "partNumber" : "006-B2697-00", + "productDisplayName" : "fēnix 5", + "productSku" : "010-01688-00", + "imageUrl" : "https://static.garmincdn.com/com.garmin.connect/content/images/device-images/fenix-5.png", + "deviceEmbedVideoLink" : null, + "deviceVideoPageLink" : null, + "wifi" : false, + "bluetoothClassicDevice" : false, + "bluetoothLowEnergyDevice" : true, + "wellness" : false, + "wasp" : false, + "primary" : true, + "minGCMiOSVersion" : 200, + "minGCMAndroidVersion" : 1400, + "minGCMWindowsVersion" : 0, + "hybrid" : true, + "appSupport" : false, + "weightScale" : false, + "hasOpticalHeartRate" : true, + "hasSecondaryUsers" : false, + "unitId" : null, + "highlighted" : false, + "displayOrder" : null, + "bestInClassVideoLink" : null, + "deviceSettingsFile" : null, + "deviceCategories" : [ ], + "versionMajor" : "10", + "versionMinor" : "0", + "buildMajor" : "0", + "buildMinor" : "0", + "versionString" : "10.0.0.0", + "released" : true +} \ No newline at end of file diff --git a/garmin-connect-export/json/device_99280678.json b/garmin-connect-export/json/device_99280678.json new file mode 100644 index 0000000..e992dbc --- /dev/null +++ b/garmin-connect-export/json/device_99280678.json @@ -0,0 +1,35 @@ +{ + "applicationKey" : "fenix5", + "partNumber" : "006-B2697-00", + "productDisplayName" : "fēnix 5", + "productSku" : "010-01688-00", + "imageUrl" : "https://static.garmincdn.com/com.garmin.connect/content/images/device-images/fenix-5.png", + "deviceEmbedVideoLink" : null, + "deviceVideoPageLink" : null, + "wifi" : false, + "bluetoothClassicDevice" : false, + "bluetoothLowEnergyDevice" : true, + "wellness" : false, + "wasp" : false, + "primary" : true, + "minGCMiOSVersion" : 200, + "minGCMAndroidVersion" : 1400, + "minGCMWindowsVersion" : 0, + "hybrid" : true, + "appSupport" : false, + "weightScale" : false, + "hasOpticalHeartRate" : true, + "hasSecondaryUsers" : false, + "unitId" : 3946806421, + "highlighted" : false, + "displayOrder" : null, + "bestInClassVideoLink" : null, + "deviceSettingsFile" : null, + "deviceCategories" : [ ], + "versionMajor" : "8", + "versionMinor" : "0", + "buildMajor" : "0", + "buildMinor" : "0", + "versionString" : "8.0.0.0", + "released" : true +} \ No newline at end of file diff --git a/garmin-connect-export/json/event_types.properties b/garmin-connect-export/json/event_types.properties new file mode 100644 index 0000000..fb06c61 --- /dev/null +++ b/garmin-connect-export/json/event_types.properties @@ -0,0 +1,7 @@ +all=Any Event Type +geocaching=Geocaching +race=Race +specialEvent=Special Event +training=Training +transportation=Transportation +uncategorized=Uncategorized diff --git a/garmin-connect-export/json/userstats.json b/garmin-connect-export/json/userstats.json new file mode 100644 index 0000000..56ec079 --- /dev/null +++ b/garmin-connect-export/json/userstats.json @@ -0,0 +1,26 @@ +{ + "userProfileId": 2836200, + "statisticsStartDate": null, + "statisticsEndDate": null, + "userMetrics": [ + { + "activityType": { + "typeId": 17, + "typeKey": "all", + "parentTypeId": 0, + "sortOrder": 1 + }, + "parentActivityType": { + "typeId": 17, + "typeKey": "all", + "parentTypeId": 0, + "sortOrder": 1 + }, + "totalActivities": 1028, + "totalDistance": 123.9, + "totalDuration": 123.724, + "totalCalories": 123.0, + "totalElevationGain": 123.51 + } + ] +}