Skip to content
This repository has been archived by the owner on Nov 14, 2020. It is now read-only.

Commit

Permalink
Fix for Issue #7
Browse files Browse the repository at this point in the history
  • Loading branch information
Jarek Hartman committed Apr 11, 2018
1 parent f004dab commit a9ba742
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 34 deletions.
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# withings-garmin-v2

**NOTE: Withings is a legacy name of Nokia Health Body / Body Cardio Scales. Feel free to use this script with Nokia products as well **

## References

* Based on withings-garmin by Masayuki Hamasaki, improved to support SSO authorization in Garmin Connect 2.

* SSO authorization derived from https://github.com/cpfair/tapiriik

## Pre-requisites

* Python 2.5 - 2.7
* 'Requests: HTTP for Humans' (http://docs.python-requests.org/en/latest/)

```
$ sudo easy_install requests
```

* simplejson

```
$ sudo easy_install simplejson
```

## Usage

```
Usage: $python sync.py [options]
Options:
-h, --help show this help message and exit
--withings-username=<user>, --wu=<user>
username to login Withings Web Service.
--withings-password=<pass>, --wp=<pass>
password to login Withings Web Service.
--withings-shortname=<name>, --ws=<name>
your shortname used in Withings.
--garmin-username=<user>, --gu=<user>
username to login Garmin Connect.
--garmin-password=<pass>, --gp=<pass>
password to login Garmin Connect.
-f <date>, --fromdate=<date>
-t <date>, --todate=<date>
--no-upload Won't upload to Garmin Connect and output binary-
string to stdout.
-v, --verbose Run verbosely
```

## Tips

* Export to a file
```
$ python sync.py --no-upload > weight.fit
```

* You can hardcode your usernames and passwords in the script (`sync.py`):

```
WITHINGS_USERNMAE = ''
WITHINGS_PASSWORD = ''
WITHINGS_SHORTNAME = ''
GARMIN_USERNAME = ''
GARMIN_PASSWORD = ''
```
File renamed without changes.
56 changes: 31 additions & 25 deletions garmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import requests
import re
import sys
import json

class LoginSucceeded(Exception):
pass
Expand All @@ -20,24 +21,24 @@ class LoginFailed(Exception):
class GarminConnect(object):
LOGIN_URL = 'https://connect.garmin.com/signin'
UPLOAD_URL = 'https://connect.garmin.com/modern/proxy/upload-service/upload/.fit'

_sessionCache = SessionCache(lifetime=timedelta(minutes=30), freshen_on_get=True)

def create_opener(self, cookie):
this = self
class _HTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
if req.get_full_url() == this.LOGIN_URL:
raise LoginSucceeded
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
return urllib2.build_opener(_HTTPRedirectHandler, urllib2.HTTPCookieProcessor(cookie))
return urllib2.build_opener(_HTTPRedirectHandler, urllib2.HTTPCookieProcessor(cookie))

##############################################
# From https://github.com/cpfair/tapiriik

def _get_session(self, record=None, email=None, password=None):
session = requests.Session()

# JSIG CAS, cool I guess.
# Not quite OAuth though, so I'll continue to collect raw credentials.
# Commented stuff left in case this ever breaks because of missing parameters...
Expand All @@ -49,9 +50,9 @@ def _get_session(self, record=None, email=None, password=None):
# "displayNameRequired": "false"
}
params = {
"service": "https://connect.garmin.com/post-auth/login",
"redirectAfterAccountLoginUrl": "http://connect.garmin.com/post-auth/login",
"redirectAfterAccountCreationUrl": "http://connect.garmin.com/post-auth/login",
"service": "https://connect.garmin.com/modern",
"redirectAfterAccountLoginUrl": "http://connect.garmin.com/modern",
"redirectAfterAccountCreationUrl": "http://connect.garmin.com/modern",
# "webhost": "olaxpw-connect00.garmin.com",
"clientId": "GarminConnect",
"gauthHost": "https://sso.garmin.com/sso",
Expand All @@ -69,12 +70,12 @@ def _get_session(self, record=None, email=None, password=None):
# "initialFocus": "true",
# "locale": "en"
}

# I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do...
preResp = session.get("https://sso.garmin.com/sso/login", params=params)
if preResp.status_code != 200:
raise APIException("SSO prestart error %s %s" % (preResp.status_code, preResp.text))

ssoResp = session.post("https://sso.garmin.com/sso/login", params=params, data=data, allow_redirects=False)
if ssoResp.status_code != 200 or "temporarily unavailable" in ssoResp.text:
raise APIException("SSO error %s %s" % (ssoResp.status_code, ssoResp.text))
Expand All @@ -90,8 +91,8 @@ def _get_session(self, record=None, email=None, password=None):
# self.print_cookies(cookies=session.cookies)

# ...AND WE'RE NOT DONE YET!
gcRedeemResp = session.get("https://connect.garmin.com/post-auth/login", allow_redirects=False)

gcRedeemResp = session.get("https://connect.garmin.com/modern", allow_redirects=False)
if gcRedeemResp.status_code != 302:
raise APIException("GC redeem-start error %s %s" % (gcRedeemResp.status_code, gcRedeemResp.text))

Expand Down Expand Up @@ -119,35 +120,41 @@ def _get_session(self, record=None, email=None, password=None):
break

self._sessionCache.Set(record.ExternalID if record else email, session.cookies)

# self.print_cookies(session.cookies)

return session
return session

def print_cookies(self, cookies):
print "Cookies"

for key, value in cookies.items():
print "Key: " + key + ", " + value

def login(self, username, password):

session = self._get_session(email=username, password=password)
res = session.get("https://connect.garmin.com/user/username")
GCusername = res.json()["username"]

sys.stderr.write('Garmin Connect User Name: ' + GCusername + '\n')

if not len(GCusername):
raise APIException("Unable to retrieve username", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))

try:
dashboard = session.get("http://connect.garmin.com/modern")
userdata_json_str = re.search(r"VIEWER_SOCIAL_PROFILE\s*=\s*JSON\.parse\((.+)\);$", dashboard.text, re.MULTILINE).group(1)
userdata = json.loads(json.loads(userdata_json_str))
username = userdata["displayName"]

sys.stderr.write('Garmin Connect User Name: ' + username + '\n')

except Exception as e:
sys.stderr.write('Unable to retrieve username!\n')

return (session)


def upload_file(self, f, session):
files = {"data": ("withings.fit", f)}

res = session.post(self.UPLOAD_URL,
files=files,
headers={"nk": "NT"})
headers={"nk": "NT"})

try:
resp = res.json()["detailedImportResult"]
Expand All @@ -159,4 +166,3 @@ def upload_file(self, f, session):
raise APIException("Bad response during GC upload: %s %s" % (res.status_code, res.text))

return (res.status_code == 200 or res.status_code == 201 or res.status_code == 204)

17 changes: 8 additions & 9 deletions sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ def verbose_print(s):
if not user:
print 'could not find user: %s' % withings_shortname
return
# if not user.ispublic:
# print 'user %s has not opened withings data' % withings_shortname
# return
if not user.ispublic:
print 'user %s has not opened withings data' % withings_shortname
return
startdate = int(time.mktime(fromdate.timetuple()))
enddate = int(time.mktime(todate.timetuple())) + 86399
groups = user.get_measure_groups(startdate=startdate, enddate=enddate)
Expand All @@ -92,16 +92,16 @@ def verbose_print(s):
for group in groups:
# get extra physical measurements

from measurements import Measurements
from measurements import Measurements
measurements = Measurements()

dt = group.get_datetime()
weight = group.get_weight()
fat_ratio = group.get_fat_ratio()
fit.write_device_info(timestamp=dt)
fit.write_weight_scale(timestamp=dt,
weight=weight,
percent_fat=fat_ratio,
fit.write_weight_scale(timestamp=dt,
weight=weight,
percent_fat=fat_ratio,
percent_hydration=measurements.getPercentHydration()
)
verbose_print('appending weight scale record... %s %skg %s%%\n' % (dt, weight, fat_ratio))
Expand All @@ -124,4 +124,3 @@ def verbose_print(s):

if __name__ == '__main__':
main()

0 comments on commit a9ba742

Please sign in to comment.