From 90913631a66b5ee3ac95e71f162ad435c1b06230 Mon Sep 17 00:00:00 2001 From: Markus Birth Date: Mon, 24 Jan 2022 11:32:40 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 2 ++ Pipfile | 12 +++++++ Pipfile.lock | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 30 ++++++++++++++++ auth.py.example | 6 ++++ fetch.py | 61 +++++++++++++++++++++++++++++++++ php/gcserve.php | 35 +++++++++++++++++++ spo2_empty.db3 | Bin 0 -> 12288 bytes 8 files changed, 235 insertions(+) create mode 100644 .gitignore create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 auth.py.example create mode 100755 fetch.py create mode 100644 php/gcserve.php create mode 100644 spo2_empty.db3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7502e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/auth.py +/spo2.db3 diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..b4c2085 --- /dev/null +++ b/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +garminconnect = "*" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..7921167 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,89 @@ +{ + "_meta": { + "hash": { + "sha256": "9a50d04f5b70c0f051d0e240e3cc06c311ecb455e788a312b32cbf4f85fd4e79" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd", + "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455" + ], + "markers": "python_version >= '3'", + "version": "==2.0.10" + }, + "cloudscraper": { + "hashes": [ + "sha256:674fd739f9412188aae8d6614e3e6316939fc0670ef5646abd3d316f1a59d3c2", + "sha256:dda29028c5628b5ba3e4dc43816ed38fd46bd945ef938c420f185586a6d8dff2" + ], + "version": "==1.2.58" + }, + "garminconnect": { + "hashes": [ + "sha256:cddd59eca3009ae6b0c2fd216b8049759680b6f88390cea49afce1ed68ce1bed" + ], + "index": "pypi", + "version": "==0.1.44" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "pyparsing": { + "hashes": [ + "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", + "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.7" + }, + "requests": { + "hashes": [ + "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.27.1" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "urllib3": { + "hashes": [ + "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", + "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.8" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a08ac3 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +Garmin SpO2 to Apple Health +=========================== + +For some reason, Garmin Connect on iOS doesn't sync Blood Oxygen (SpO2) data into Apple Health +despite syncing various other metrics perfectly fine already. + +There's a [feature request](https://forums.garmin.com/apps-software/mobile-apps-web/f/garmin-connect-mobile-ios/254977/request-for-spo2-vo2-max-and-respiration-data-to-be-shared-to-apple-health-app) +in their forums from February 2021, but apart from "We've added this to our feature request list" +nothing came of it in the past 11 months. And I found similar requests as old as [over 4 years (June 2018)](https://forums.garmin.com/outdoor-recreation/outdoor-recreation/f/fenix-5-plus-series/147239/spo2-measurements). + +Until Garmin finds the 5 minutes to implement this feature, I've resorted to use the wonderful +[garminconnect](https://github.com/cyberjunky/python-garminconnect) Python-library to download +my SpO2 data, a PHP script to serve the data from the last 2 days as JSON and an iOS Shortcut to +sync that to Apple Health. + +While it's not the most elegant solution, it works. + + +Usage +----- + +1. Copy `auth.py.example` to `auth.py` and edit it to specify your Garmin Connect credentials +2. Copy `spo2_empty.db3` to `spo2.db3` +3. Run `pipenv install` to install dependencies +4. Make a cronjob running `pipenv run ./fetch.py` regularly +5. Copy the script from the `php` folder to a webserver and edit the file to point to the SQLite + database file. +6. Install the [SpO2 Inserter Shortcut](https://www.icloud.com/shortcuts/7f6f94eb536e4fb1857993bfbc181ccb) + on your iPhone and point it to the PHP script. +7. Optional: Create an Automation in Shortcuts to run this automatically. diff --git a/auth.py.example b/auth.py.example new file mode 100644 index 0000000..dc8a325 --- /dev/null +++ b/auth.py.example @@ -0,0 +1,6 @@ +# Your Garmin Connect credentials +USERNAME = "changeme@changeme.com" +PASSWORD = "changeme" + +# Number of days to fetch (default: last 3 days) +DAYS_TO_FETCH = 3 diff --git a/fetch.py b/fetch.py new file mode 100755 index 0000000..426d3b7 --- /dev/null +++ b/fetch.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import datetime +import sqlite3 +from auth import USERNAME, PASSWORD, DAYS_TO_FETCH +from pprint import pprint +from time import sleep +from garminconnect import ( + Garmin, + GarminConnectConnectionError +) + +today = datetime.date.today() +startfrom = today - datetime.timedelta(days=DAYS_TO_FETCH) + +print(f"Logging into Garmin Connect using {USERNAME}.") +api = Garmin(USERNAME, PASSWORD) + +tries = 0 +while True: + try: + api.login() + break + except GarminConnectConnectionError as e: + tries += 1 + wait = tries + if wait > 2: + wait = 2 + print(f"Error during login! Retry #{tries} in {wait} sec.", flush=True) + sleep(wait) + +db = sqlite3.connect("spo2.db3") + +reqday = startfrom +while reqday <= today: + print("Querying data for: {}".format(reqday.isoformat())) + sleep = api.get_sleep_data(reqday.isoformat()) + #pprint(sleep) + + if not "wellnessEpochSPO2DataDTOList" in sleep: + print("No SpO2 data for {}. Skipping day.".format(reqday.isoformat())) + reqday += datetime.timedelta(days=1) + continue + + spo2 = sleep["wellnessEpochSPO2DataDTOList"] + #pprint(spo2) + + print("Got {} records.".format(len(spo2))) + + for rec in spo2: + ts = datetime.datetime.fromisoformat(rec["epochTimestamp"][:-2]) # strip hundreds of seconds + sql = "INSERT OR IGNORE INTO spo2 VALUES (?, ?, ?)" + db.execute(sql, [ts.timestamp(), rec["spo2Reading"], rec["readingConfidence"]]) + + reqday += datetime.timedelta(days=1) + +db.commit() +db.close() + +api.logout() diff --git a/php/gcserve.php b/php/gcserve.php new file mode 100644 index 0000000..594daa6 --- /dev/null +++ b/php/gcserve.php @@ -0,0 +1,35 @@ +prepare('SELECT * FROM spo2 WHERE timestamp_utc >= ?'); +$pq->execute(array($servefrom)); +$res = $pq->fetchAll(); + +#header('Content-Type: text/plain'); +header('Content-Type: application/json'); + +$result = array(); +foreach ($res as $row) { + $dto = new DateTime('@' . $row['timestamp_utc'], new DateTimeZone('UTC')); + $result[] = array( + 'unixtime' => intval($row['timestamp_utc']), + #'datestr' => $dto->format('Y.m.d \A\D \a\t H:i:s \U\T\C'), + 'datestr' => $dto->format('c'), + 'spo2_value' => intval($row['spo2_percent']), + 'spo2_confidence' => intval($row['spo2_confidence']), + ); +} + +#var_dump($result); +echo json_encode($result); diff --git a/spo2_empty.db3 b/spo2_empty.db3 new file mode 100644 index 0000000000000000000000000000000000000000..bf7b9d941867411dc4fcdcd16ac6fc4ab06e7439 GIT binary patch literal 12288 zcmeI#&r8EF6bJCMif)C`UCLmm(2E<0;5>SNq(U|0PQ{LB1z zJem}`gTk)D_y*of+Po6->FK@9y-aYrDmQr|=nd%;O(S=d5<)sMPGq#>R4!DsLu(fY z<_@`t{<`~3#k7TQa|!M8u{)#VwbW~ zeG^xFlk!6RE!8aFL2?GKUfB*y_009U<00Izz00bZafnyag)Wy*MAM5YM Zy&wPq2tWV=5P$##AOHafKmY=bzz>AeOM?Ia literal 0 HcmV?d00001