diff --git a/README.md b/README.md index 1373b29..1bd8bbf 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,10 @@ Get weight from Nokia Health and update to Garmin Connect or Smashrun. 2. Satisfy the following requirements: - Python 3.X - - Python libraries: arrow, requests, requests-oauthlib + - Set up a virtual env with requirements: + - `python3 -m venv venv` + - `source venv/bin/activate` + - `pip install -r requirements.txt` 3. [Register](https://account.withings.com/partner/add_oauth2) an application with Nokia Health and obtain a consumer key and secret. 1. logo: the requirements are quite strict, [feel free to use this one](https://github.com/magnific0/nokia-weight-sync/blob/master/logo256w.png) @@ -60,7 +63,6 @@ nokia-weight-sync includes components the following open-source projects: * ```fit.py``` from [ikasamah/withings-garmin](https://github.com/ikasamah/withings-garmin), MIT License (c) 2013 Masayuki Hamasaki, adapted for Python 3. * ```garmin.py``` from [jaroslawhartman/withings-garmin-v2](https://github.com/jaroslawhartman/withings-garmin-v2), MIT License (c) 2013 Masayuki Hamasaki, adapted for Python 3. -* ```nokia.py``` from [python-nokia](https://github.com/orcasgit/python-nokia), MIT License (c) 2012 Maxime Bouroumeau-Fuseau, 2017 ORCAS, unmodified. * ```sessioncache.py``` from [cpfair/tapiriik](https://github.com/cpfair/tapiriik/blob/187d1b97ce73cc35b5e2194eb4631ceff20499e3/tapiriik/services/sessioncache.py), Apache License 2.0, unmodified. * ```smashrun.py``` from [campbellr/smashrun-client](https://github.com/campbellr/smashrun-client), Apache License 2.0, several fixes. diff --git a/nokia.py b/nokia.py deleted file mode 100644 index 7ea299a..0000000 --- a/nokia.py +++ /dev/null @@ -1,281 +0,0 @@ -# -*- coding: utf-8 -*- -# -""" -Python library for the Nokia Health API -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Nokia Health API - - -Uses Oauth 2.0 to authentify. You need to obtain a consumer key -and consumer secret from Nokia by creating an application -here: - -Usage: - -auth = NokiaAuth(CLIENT_ID, CONSUMER_SECRET, CALLBACK_URL) -authorize_url = auth.get_authorize_url() -print("Go to %s allow the app and copy the url you are redirected to." % authorize_url) -authorization_response = raw_input('Please enter your full authorization response url: ') -creds = auth.get_credentials(authorization_response) - -client = NokiaApi(creds) -measures = client.get_measures(limit=1) -print("Your last measured weight: %skg" % measures[0].weight) - -creds = client.get_credentials() - -""" - -from __future__ import unicode_literals - -__title__ = 'nokia' -__version__ = '0.4.0' -__author__ = 'Maxime Bouroumeau-Fuseau, and ORCAS' -__license__ = 'MIT' -__copyright__ = 'Copyright 2012-2017 Maxime Bouroumeau-Fuseau, and ORCAS' - -__all__ = [str('NokiaCredentials'), str('NokiaAuth'), str('NokiaApi'), - str('NokiaMeasures'), str('NokiaMeasureGroup')] - -import arrow -import datetime -import json - -from arrow.parser import ParserError -from requests_oauthlib import OAuth2Session -from oauthlib.oauth2 import WebApplicationClient - -class NokiaCredentials(object): - def __init__(self, access_token=None, token_expiry=None, token_type=None, - refresh_token=None, user_id=None, - client_id=None, consumer_secret=None): - self.access_token = access_token - self.token_expiry = token_expiry - self.token_type = token_type - self.refresh_token = refresh_token - self.user_id = user_id - self.client_id = client_id - self.consumer_secret = consumer_secret - - -class NokiaAuth(object): - URL = 'https://account.withings.com' - - def __init__(self, client_id, consumer_secret, callback_uri, - scope='user.metrics'): - self.client_id = client_id - self.consumer_secret = consumer_secret - self.callback_uri = callback_uri - self.scope = scope - - def _oauth(self): - return OAuth2Session(self.client_id, - redirect_uri=self.callback_uri, - scope=self.scope) - - def get_authorize_url(self): - return self._oauth().authorization_url( - '%s/oauth2_user/authorize2'%self.URL - )[0] - - def get_credentials(self, code): - tokens = self._oauth().fetch_token( - '%s/oauth2/token' % self.URL, - code=code, - timeout=2, - client_secret=self.consumer_secret) - - return NokiaCredentials( - access_token=tokens['access_token'], - token_expiry=str(ts()+int(tokens['expires_in'])), - token_type=tokens['token_type'], - refresh_token=tokens['refresh_token'], - user_id=tokens['userid'], - client_id=self.client_id, - consumer_secret=self.consumer_secret, - ) - - -def is_date(key): - return 'date' in key - - -def is_date_class(val): - return isinstance(val, (datetime.date, datetime.datetime, arrow.Arrow, )) - - -# Calculate seconds since 1970-01-01 (timestamp) in a way that works in -# Python 2 and Python3 -# https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp -def ts(): - return int(( - datetime.datetime.utcnow() - datetime.datetime(1970, 1, 1) - ).total_seconds()) - - -class NokiaApi(object): - URL = 'https://wbsapi.withings.net' - - def __init__(self, credentials): - self.credentials = credentials - self.token = { - 'access_token': credentials.access_token, - 'refresh_token': credentials.refresh_token, - 'token_type': credentials.token_type, - 'expires_in': str(int(credentials.token_expiry) - ts()), - } - oauth_client = WebApplicationClient(credentials.client_id, - token=self.token, default_token_placement='query') - self.client = OAuth2Session( - credentials.client_id, - token=self.token, - client=oauth_client, - auto_refresh_url='{}/oauth2/token'.format(NokiaAuth.URL), - auto_refresh_kwargs={ - 'client_id': credentials.client_id, - 'client_secret': credentials.consumer_secret, - }, - token_updater=self.set_token - ) - - def get_credentials(self): - return self.credentials - - def set_token(self, token): - self.token = token - self.credentials.token_expiry = str( - ts() + int(self.token['expires_in']) - ) - self.credentials.access_token = self.token['access_token'] - self.credentials.refresh_token = self.token['refresh_token'] - - def request(self, service, action, params=None, method='GET', - version=None): - params = params or {} - params['userid'] = self.credentials.user_id - params['action'] = action - for key, val in params.items(): - if is_date(key) and is_date_class(val): - params[key] = arrow.get(val).timestamp - url_parts = filter(None, [self.URL, version, service]) - r = self.client.request(method, '/'.join(url_parts), params=params,timeout=10) - response = json.loads(r.content.decode()) - if response['status'] != 0: - raise Exception("Error code %s" % response['status']) - return response.get('body', None) - - def get_user(self): - return self.request('user', 'getbyuserid') - - def get_activities(self, **kwargs): - r = self.request('measure', 'getactivity', params=kwargs, version='v2') - activities = r['activities'] if 'activities' in r else [r] - return [NokiaActivity(act) for act in activities] - - def get_measures(self, **kwargs): - r = self.request('measure', 'getmeas', kwargs) - return NokiaMeasures(r) - - def get_sleep(self, **kwargs): - r = self.request('sleep', 'get', params=kwargs, version='v2') - return NokiaSleep(r) - - def subscribe(self, callback_url, comment, **kwargs): - params = {'callbackurl': callback_url, 'comment': comment} - params.update(kwargs) - self.request('notify', 'subscribe', params) - - def unsubscribe(self, callback_url, **kwargs): - params = {'callbackurl': callback_url} - params.update(kwargs) - self.request('notify', 'revoke', params) - - def is_subscribed(self, callback_url, appli=1): - params = {'callbackurl': callback_url, 'appli': appli} - try: - self.request('notify', 'get', params) - return True - except: - return False - - def list_subscriptions(self, appli=1): - r = self.request('notify', 'list', {'appli': appli}) - return r['profiles'] - - -class NokiaObject(object): - def __init__(self, data): - self.set_attributes(data) - - def set_attributes(self, data): - self.data = data - for key, val in data.items(): - try: - setattr(self, key, arrow.get(val) if is_date(key) else val) - except ParserError: - setattr(self, key, val) - - -class NokiaActivity(NokiaObject): - pass - - -class NokiaMeasures(list, NokiaObject): - def __init__(self, data): - super(NokiaMeasures, self).__init__( - [NokiaMeasureGroup(g) for g in data['measuregrps']]) - self.set_attributes(data) - - -class NokiaMeasureGroup(NokiaObject): - MEASURE_TYPES = ( - ('weight', 1), - ('height', 4), - ('fat_free_mass', 5), - ('fat_ratio', 6), - ('fat_mass_weight', 8), - ('diastolic_blood_pressure', 9), - ('systolic_blood_pressure', 10), - ('heart_pulse', 11), - ('temperature', 12), - ('spo2', 54), - ('body_temperature', 71), - ('skin_temperature', 72), - ('muscle_mass', 76), - ('hydration', 77), - ('bone_mass', 88), - ('pulse_wave_velocity', 91) - ) - - def __init__(self, data): - super(NokiaMeasureGroup, self).__init__(data) - for n, t in self.MEASURE_TYPES: - self.__setattr__(n, self.get_measure(t)) - - def is_ambiguous(self): - return self.attrib == 1 or self.attrib == 4 - - def is_measure(self): - return self.category == 1 - - def is_target(self): - return self.category == 2 - - def get_measure(self, measure_type): - for m in self.measures: - if m['type'] == measure_type: - return m['value'] * pow(10, m['unit']) - return None - - -class NokiaSleepSeries(NokiaObject): - def __init__(self, data): - super(NokiaSleepSeries, self).__init__(data) - self.timedelta = self.enddate - self.startdate - - -class NokiaSleep(NokiaObject): - def __init__(self, data): - super(NokiaSleep, self).__init__(data) - self.series = [NokiaSleepSeries(series) for series in self.series] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ab58911 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +nokia