#!/usr/bin/env python3 """ __author__ = "Praveen Kumar Pendyala" __email__ = "mail@pkp.io" """ import time import argparse import logging import dateutil.tz import dateutil.parser import configparser import json from datetime import timedelta, date, datetime import fitbit from fitbit.exceptions import HTTPTooManyRequests from apiclient.discovery import build from oauth2client.file import Storage from oauth2client.client import OAuth2Credentials from googleapiclient.errors import HttpError from random import randint from app import DATE_FORMAT class Remote: """Methods for remote api calls and synchronization from Fitbit to Google Fit""" FITBIT_API_URL = 'https://api.fitbit.com/1' GFIT_MAX_POINTS_PER_UPDATE = 2000 # Max number of data points that can be sent in a single update request def __init__(self, fitbitClient, googleClient, convertor, helper): """ Intialize a remote object. fitbitClient -- authenticated fitbit client googleClient -- authenticated google client convertor -- a convertor object for type conversions helper -- a helper object for fitbit credentials update """ self.fitbitClient = fitbitClient self.googleClient = googleClient self.convertor = convertor self.helper = helper ########################### Remote data read/write methods ############################ def ReadFromFitbit(self, api_call, *args, **kwargs): """Peforms a read request from Fitbit API. The request will be paused if API rate limiting has been reached! api_call -- api method to call args -- arguments to pass for the method """ # res_url,date_stamp,detail_level try: resp = api_call(*args,**kwargs) except HTTPTooManyRequests as e: # retry between 5-10 minutes after the hour seconds_till_retry = e.retry_after_secs + randint(300,600) print('') print('-------------------- Fitbit API rate limit reached -------------------') retry_time = datetime.now()+timedelta(seconds=seconds_till_retry) print('Will retry at {}'.format(retry_time.strftime('%H:%M:%S'))) print('') time.sleep(seconds_till_retry) resp = self.ReadFromFitbit(api_call,*args,**kwargs) return resp def WriteToGoogleFit(self, dataSourceId, data_points): """Write data to google fit dataSourceId -- data source id for google fit data_point -- google data points """ # max and min timestamps of any data point we will be adding to googlefit - required by gfit API. if len(data_points) == 0: return minLogNs = min([point['startTimeNanos'] for point in data_points]) maxLogNs = max([point['endTimeNanos'] for point in data_points]) datasetId = '%s-%s' % (minLogNs, maxLogNs) if len(data_points) < self.GFIT_MAX_POINTS_PER_UPDATE: try: self.googleClient.users().dataSources().datasets().patch( userId='me', dataSourceId=dataSourceId, datasetId=datasetId, body=dict( dataSourceId=dataSourceId, maxEndTimeNs=maxLogNs, minStartTimeNs=minLogNs, point=data_points) ).execute() except BrokenPipeError as e: # Re-create the googleClient since the last one is broken self.googleClient = self.helper.GetGoogleClient() self.WriteToGoogleFit(dataSourceId, data_points) else: half = int(len(data_points)/2) self.WriteToGoogleFit(dataSourceId, data_points[:half]) self.WriteToGoogleFit(dataSourceId, data_points[half:]) def WriteSessionToGoogleFit(self, session_data): """Write data to google fit session_data -- a session data """ try: self.googleClient.users().sessions().update( userId='me', sessionId=session_data['id'], body=session_data).execute() except BrokenPipeError as e: # Re-create the googleClient since the last one is broken self.googleClient = self.helper.GetGoogleClient() self.WriteSessionToGoogleFit(session_data) def CreateGoogleFitDataSource(self, dataType): try: self.googleClient.users().dataSources().get( userId='me', dataSourceId=self.convertor.GetDataSourceId(dataType)).execute() except HttpError as error: if not 'DataSourceId not found' in str(error): raise error # Data source doesn't already exist so, create it! self.googleClient.users().dataSources().create( userId='me', body=self.convertor.GetDataSource(dataType)).execute() ########################################### Sync methods ######################################## def SyncFitbitToGoogleFit(self, dataType, date_stamp): """ Sync Fitbit data to Google fit for a given day. dataType -- fitbit data type to sync date_stamp -- timestamp in yyyy-mm-dd format of the day to sync """ # Persist current credentials. Incase the request fails. self.helper.UpdateFitbitCredentials(self.fitbitClient) if dataType in ('steps','distance','heart_rate','calories'): return self.SyncFitbitIntradayToGoogleFit(dataType, date_stamp) elif dataType in ('weight','body_fat'): return self.SyncFitbitLogToGoogleFit(dataType, date_stamp) elif dataType in ('sleep'): return self.SyncFitbitSleepToGoogleFit(date_stamp) else: raise ValueError("Unexpected data type given!") def SyncFitbitIntradayToGoogleFit(self, dataType, date_stamp): """ Sync Fitbit data of a particular intraday type to Google fit for a given day. dataType -- fitbit data type to sync date_stamp -- timestamp in yyyy-mm-dd format of the day to sync """ if dataType == 'steps': res_path,detail_level,resp_id = 'activities/steps','1min','activities-steps-intraday' elif dataType == 'distance': res_path,detail_level,resp_id = 'activities/distance','1min','activities-distance-intraday' elif dataType == 'heart_rate': res_path,detail_level,resp_id = 'activities/heart','1sec','activities-heart-intraday' elif dataType == 'calories': res_path,detail_level,resp_id = 'activities/calories','1min','activities-calories-intraday' else: raise ValueError("Unexpected data type given!") dataSourceId = self.convertor.GetDataSourceId(dataType) # Get intraday data from fitbit interday_raw = self.ReadFromFitbit(self.fitbitClient.intraday_time_series, res_path, base_date=date_stamp, detail_level=detail_level) try: intraday_data = interday_raw[resp_id]['dataset'] except KeyError as e: print('') print('Uh oh! Looks like you didn\'t set your "OAuth 2.0 Application Type" to "Personal" during Fitbit setup.') print('For more information, refer https://github.com/praveendath92/fitbit-googlefit/issues/2') print('') exit() # convert all fitbit data points to google fit data points googlePoints = [self.convertor.ConvertFibitPoint(date_stamp,point,dataType) for point in intraday_data] # Write a day of fitbit data to Google fit self.WriteToGoogleFit(dataSourceId, googlePoints) print("synced {} - {} data points".format(dataType,len(googlePoints)) ) def SyncFitbitLogToGoogleFit(self, dataType, date_stamp): """ Sync Fitbit logs of a particular type to Google Fit for a given day. dataType -- fitbit data type to sync date_stamp -- timestamp in yyyy-mm-dd format of the day to sync """ if dataType == 'weight': callMethod,resp_id = self.fitbitClient.get_bodyweight,'weight' elif dataType == 'body_fat': callMethod,resp_id = self.fitbitClient.get_bodyfat,'fat' else: raise ValueError("Unexpected data type given!") dataSourceId = self.convertor.GetDataSourceId(dataType) # Get intraday distance for date_stamp from fitbit fitbitLogs = self.ReadFromFitbit(callMethod,base_date=date_stamp,end_date=date_stamp)[resp_id] # convert all fitbit data points to google fit data points googlePoints = [self.convertor.ConvertFibitPoint(date_stamp,point,dataType) for point in fitbitLogs] # Write a day of fitbit data to Google fit self.WriteToGoogleFit(dataSourceId, googlePoints) print("synced {} - {} logs".format(dataType,len(googlePoints)) ) def SyncFitbitSleepToGoogleFit(self, date_stamp): """ Sync sleep data for a given day from Fitbit to Google fit. date_stamp -- timestamp in yyyy-mm-dd format of the start day """ dataSourceId = self.convertor.GetDataSourceId('sleep') date_obj = self.convertor.parseHumanReadableDate(date_stamp) # Get sleep data for a given date fitbitSleeps = self.ReadFromFitbit(self.fitbitClient.get_sleep,date_obj)['sleep'] # Iterate over each sleep log for that day sleep_count = 0 for fit_sleep in fitbitSleeps: minute_points = fit_sleep['minuteData'] sleep_count += 1 # save first time stamp for comparison start_time = minute_points[0]['dateTime'] # convert to date, add 1 day, convert back to string next_date_stamp = (datetime.strptime(date_stamp, DATE_FORMAT) + timedelta(1)).strftime(DATE_FORMAT) # convert all fitbit data points to google fit data points googlePoints = [self.convertor.ConvertFibitPoint((date_stamp if start_time <= point['dateTime'] else \ next_date_stamp),point,'sleep') for point in minute_points] # 1. Write a fit session about sleep google_session = self.convertor.ConvertGFitSleepSession(googlePoints, fit_sleep['logId']) self.WriteSessionToGoogleFit(google_session) # 2. create activity segment data points for the activity self.WriteToGoogleFit(dataSourceId, googlePoints) print("synced sleep - {} logs".format(sleep_count)) def SyncFitbitActivitiesToGoogleFit(self, start_date='', callurl=None): """ Sync activities data starting from a given day from Fitbit to Google fit. start_date -- timestamp in yyyy-mm-dd format of the start day callurl -- url to fetch activities from """ # Fitbit activities list endpoint is in beta stage. It may break in the future and not directly supported # by the python client library. dataSourceId = self.convertor.GetDataSourceId('activity') if not callurl: callurl = '{}/user/-/activities/list.json?afterDate={}&sort=asc&offset=0&limit=20'.format(self.FITBIT_API_URL,start_date) activities_raw = self.ReadFromFitbit(self.fitbitClient.make_request, callurl) activities = activities_raw['activities'] startTimeMillis,endTimeMillis = [],[] for activity in activities: # 1. write a fit session about the activity google_session = self.convertor.ConvertFitbitActivityLog(activity) self.WriteSessionToGoogleFit(google_session) # 2. create activity segment data points for the activity activity_segment = dict( dataTypeName='com.google.activity.segment', startTimeNanos=self.convertor.nano(google_session['startTimeMillis']), endTimeNanos=self.convertor.nano(google_session['endTimeMillis']), value=[dict(intVal=google_session['activityType'])] ) self.WriteToGoogleFit(dataSourceId, [activity_segment]) # Just for user output startTimeMillis.append(google_session['startTimeMillis']) endTimeMillis.append(google_session['endTimeMillis']) if len(startTimeMillis) > 0: print("Synced {} exercises between : {} -- {}".format(len(activities), datetime.fromtimestamp(min(startTimeMillis)/1000).strftime('%Y-%m-%d'), datetime.fromtimestamp(max(endTimeMillis)/1000).strftime('%Y-%m-%d')) ) else: print("No Fitbit exercises logged since {}".format(start_date)) return if activities_raw['pagination']['next'] != '': self.SyncFitbitActivitiesToGoogleFit(callurl=activities_raw['pagination']['next'])