from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals # The interactions with the Concept2 logbook API # All C2 related functions should be defined here # (There is still some stuff defined directly in views.py. Need to # move that here.) from rowers.imports import * import datetime from requests import Request, Session import rowers.mytypes as mytypes from rowers.mytypes import otwtypes from iso8601 import ParseError import numpy import json from rowsandall_app.settings import ( C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET ) from rowers.tasks import handle_c2_import_stroke_data import django_rq queue = django_rq.get_queue('default') queuelow = django_rq.get_queue('low') queuehigh = django_rq.get_queue('low') from rowers.utils import myqueue oauth_data = { 'client_id': C2_CLIENT_ID, 'client_secret': C2_CLIENT_SECRET, 'redirect_uri': C2_REDIRECT_URI, 'autorization_uri': "https://log.concept2.com/oauth/authorize", 'content_type': 'application/x-www-form-urlencoded', 'tokenname': 'c2token', 'refreshtokenname': 'c2refreshtoken', 'expirydatename': 'tokenexpirydate', 'bearer_auth': True, 'base_url': "https://log.concept2.com/oauth/access_token", 'scope':'write', } # Checks if user has Concept2 tokens, resets tokens if they are # expired. def c2_open(user): r = Rower.objects.get(user=user) if (r.c2token == '') or (r.c2token is None): s = "Token doesn't exist. Need to authorize" raise NoTokenError("User has no token") else: if (timezone.now()>r.tokenexpirydate): res = rower_c2_token_refresh(user) if res == None: raise NoTokenError("User has no token") if res[0] != None: thetoken = res[0] else: raise NoTokenError("User has no token") else: thetoken = r.c2token return thetoken def add_stroke_data(user,c2id,workoutid,startdatetime,csvfilename, workouttype='rower'): r = Rower.objects.get(user=user) if (r.c2token == '') or (r.c2token is None): return custom_exception_handler(401,s) s = "Token doesn't exist. Need to authorize" elif (timezone.now()>r.tokenexpirydate): s = "Token expired. Needs to refresh." return custom_exception_handler(401,s) else: starttimeunix = arrow.get(startdatetime).timestamp job = myqueue(queue, handle_c2_import_stroke_data, r.c2token, c2id, workoutid, starttimeunix, csvfilename,workouttype=workouttype) return 1 def get_c2_workouts(rower): if not isprorower(rower): return 0 try: thetoken = c2_open(rower.user) except NoTokenError: return 0 res = get_c2_workout_list(rower.user,page=1) if (res.status_code != 200): return 0 else: c2ids = [item['id'] for item in res.json()['data']] alldata = {} for item in res.json()['data']: alldata[item['id']] = item knownc2ids = [ w.uploadedtoc2 for w in Workout.objects.filter(user=rower) ] tombstones = [ t.uploadedtoc2 for t in TombStone.objects.filter(user=rower) ] knownc2ids = uniqify(knownc2ids+tombstones) newids = [c2id for c2id in c2ids if not c2id in knownc2ids] for c2id in newids: workoutid = create_async_workout(alldata, rower.user,c2id) return 1 # get workout metrics, then relay stroke data to an asynchronous task def create_async_workout(alldata,user,c2id): data = alldata[c2id] splitdata = None distance = data['distance'] c2id = data['id'] workouttype = data['type'] verified = data['verified'] startdatetime = iso8601.parse_date(data['date']) weightclass = data['weight_class'] weightcategory = 'hwt' if weightclass == "L": weightcategory = 'lwt' # Create CSV file name and save data to CSV file csvfilename ='media/Import_'+str(c2id)+'.csv.gz' totaltime = data['time']/10. duration = dataprep.totaltime_sec_to_string(totaltime) try: timezone_str = tz(data['timezone']) except: timezone_str = 'UTC' workoutdate = startdatetime.astimezone( pytz.timezone(timezone_str) ).strftime('%Y-%m-%d') starttime = startdatetime.astimezone( pytz.timezone(timezone_str) ).strftime('%H:%M:%S') r = Rower.objects.get(user=user) w = Workout( user=r, workouttype = workouttype, name = 'C2 Import Workout from {startdatetime}'.format(startdatetime=startdatetime), date = workoutdate, starttime = starttime, startdatetime = startdatetime, timezone = timezone_str, duration = duration, distance=distance, weightcategory = weightcategory, uploadedtoc2 = c2id, csvfilename = csvfilename, notes = 'imported from Concept2 log' ) w.save() # Check if workout has stroke data, and get the stroke data result = add_stroke_data(user,c2id,w.id,startdatetime,csvfilename, workouttype = w.workouttype) return w.id # convert datetime object to seconds def makeseconds(t): seconds = t.hour*3600.+t.minute*60.+t.second+0.1*int(t.microsecond/1.e5) return seconds # convert our weight class code to Concept2 weight class code def c2wc(weightclass): if (weightclass=="lwt"): res = "L" else: res = "H" return res # Concept2 logbook sends over split data for each interval # We use it here to generate a custom summary # Some users complained about small differences def summaryfromsplitdata(splitdata,data,filename,sep='|'): totaldist = data['distance'] totaltime = data['time']/10. try: spm = data['stroke_rate'] except KeyError: spm = 0 try: resttime = data['rest_time']/10. except KeyError: resttime = 0 try: restdistance = data['rest_distance'] except KeyError: restdistance = 0 try: avghr = data['heart_rate']['average'] except KeyError: avghr = 0 try: maxhr = data['heart_rate']['max'] except KeyError: maxhr = 0 try: avgpace = 500.*totaltime/totaldist except (ZeroDivisionError,OverflowError): avgpace = 0. try: restpace = 500.*resttime/restdistance except (ZeroDivisionError,OverflowError): restpace = 0. velo = totaldist/totaltime avgpower = 2.8*velo**(3.0) try: restvelo = restdistance/resttime except (ZeroDivisionError,OverflowError): restvelo = 0 restpower = 2.8*restvelo**(3.0) try: avgdps = totaldist/data['stroke_count'] except (ZeroDivisionError,OverflowError,KeyError): avgdps = 0 from rowingdata import summarystring,workstring,interval_string sums = summarystring(totaldist,totaltime,avgpace,spm,avghr,maxhr, avgdps,avgpower,readFile=filename, separator=sep) sums += workstring(totaldist,totaltime,avgpace,spm,avghr,maxhr, avgdps,avgpower,separator=sep,symbol='W') sums += workstring(restdistance,resttime,restpace,0,0,0,0,restpower, separator=sep, symbol='R') sums += '\nWorkout Details\n' sums += '#-{sep}SDist{sep}-Split-{sep}-SPace-{sep}-Pwr-{sep}SPM-{sep}AvgHR{sep}MaxHR{sep}DPS-\n'.format( sep=sep ) intervalnr=0 sa = [] results = [] try: timebased = data['workout_type'] in ['FixedTimeSplits','FixedTimeInterval'] except KeyError: timebased = False for interval in splitdata: idist = interval['distance'] itime = interval['time']/10. ipace = 500.*itime/idist try: ispm = interval['stroke_rate'] except KeyError: ispm = 0 try: irest_time = interval['rest_time']/10. except KeyError: irest_time = 0 try: iavghr = interval['heart_rate']['average'] except KeyError: iavghr = 0 try: imaxhr = interval['heart_rate']['average'] except KeyError: imaxhr = 0 # create interval values iarr = [idist,'meters','work'] resarr = [itime] if timebased: iarr = [itime,'seconds','work'] resarr = [idist] if irest_time > 0: iarr += [irest_time,'seconds','rest'] try: resarr += [interval['rest_distance']] except KeyError: resarr += [np.nan] sa += iarr results += resarr if itime != 0: ivelo = idist/itime ipower = 2.8*ivelo**(3.0) else: ivelo = 0 ipower = 0 sums += interval_string(intervalnr,idist,itime,ipace,ispm, iavghr,imaxhr,0,ipower,separator=sep) intervalnr+=1 return sums,sa,results # Not used now. Could be used to add workout split data to Concept2 # logbook but needs to be reviewed. def createc2workoutdata_as_splits(w): filename = w.csvfilename row = rowingdata(csvfile=filename) # resize per minute df = row.df.groupby(lambda x:x/60).mean() averagehr = int(df[' HRCur (bpm)'].mean()) maxhr = int(df[' HRCur (bpm)'].max()) # adding diff, trying to see if this is valid t = 10*df.loc[:,' ElapsedTime (sec)'].diff().values t[0] = t[1] d = df.loc[:,' Horizontal (meters)'].diff().values d[0] = d[1] p = 10*df.loc[:,' Stroke500mPace (sec/500m)'].values t = t.astype(int) d = d.astype(int) p = p.astype(int) spm = df[' Cadence (stokes/min)'].astype(int) spm[0] = spm[1] hr = df[' HRCur (bpm)'].astype(int) split_data = [] for i in range(len(t)): thisrecord = {"time":t[i],"distance":d[i],"stroke_rate":spm[i], "heart_rate":{ "average:":hr[i] } } split_data.append(thisrecord) try: durationstr = datetime.datetime.strptime(str(w.duration),"%H:%M:%S.%f") except ValueError: durationstr = datetime.datetime.strptime(str(w.duration),"%H:%M:%S") try: newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' wtype = w.workouttype if wtype in otwtypes: wtype = 'water' data = { "type": wtype, "date": w.startdatetime.isoformat(), "distance": int(w.distance), "time": int(10*makeseconds(durationstr)), "timezone": w.timezone, "weight_class": c2wc(w.weightcategory), "comments": newnotes, "heart_rate": { "average": averagehr, "max": maxhr, }, "splits": split_data, } return data # Create the Data object for the stroke data to be sent to Concept2 logbook # API def createc2workoutdata(w): filename = w.csvfilename try: row = rowingdata(filename) except IOError: return 0 try: averagehr = int(row.df[' HRCur (bpm)'].mean()) maxhr = int(row.df[' HRCur (bpm)'].max()) except ValueError: averagehr = 0 maxhr = 0 # adding diff, trying to see if this is valid t = 10*row.df.loc[:,'TimeStamp (sec)'].values-10*row.df.loc[:,'TimeStamp (sec)'].iloc[0] try: t[0] = t[1] except IndexError: pass d = 10*row.df.loc[:,' Horizontal (meters)'].values try: d[0] = d[1] except IndexError: pass p = abs(10*row.df.loc[:,' Stroke500mPace (sec/500m)'].values) p = np.clip(p,0,3600) if w.workouttype == 'bike': p = 2.0*p t = t.astype(int) d = d.astype(int) p = p.astype(int) spm = row.df[' Cadence (stokes/min)'].astype(int) try: spm[0] = spm[1] except IndexError: pass try: hr = row.df[' HRCur (bpm)'].astype(int) except ValueError: hr = 0*d stroke_data = [] t = t.tolist() d = d.tolist() p = p.tolist() spm = spm.tolist() hr = hr.tolist() for i in range(len(t)): thisrecord = {"t":t[i], "d":d[i], "p":p[i], "spm":spm[i], "hr":hr[i]} stroke_data.append(thisrecord) try: durationstr = datetime.datetime.strptime(str(w.duration),"%H:%M:%S.%f") except ValueError: durationstr = datetime.datetime.strptime(str(w.duration),"%H:%M:%S") workouttype = w.workouttype if workouttype in otwtypes: workouttype = 'water' try: startdatetime = w.startdatetime.isoformat() except AttributeError: startdate = datetime.datetime.combine(w.date,datetime.time()) data = { "type": mytypes.c2mapping[workouttype], "date": w.startdatetime.isoformat(), "timezone": w.timezone, "distance": int(w.distance), "time": int(10*makeseconds(durationstr)), "weight_class": c2wc(w.weightcategory), "comments": w.notes, "heart_rate": { "average": averagehr, "max": maxhr, }, "stroke_data": stroke_data, } return data # Refresh Concept2 authorization token def do_refresh_token(refreshtoken): scope = "results:write,user:read" client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) post_data = {"grant_type": "refresh_token", "client_secret": C2_CLIENT_SECRET, "client_id":C2_CLIENT_ID, "refresh_token": refreshtoken, } headers = {'user-agent': 'sanderroosendaal'} url = "https://log.concept2.com/oauth/access_token" s = Session() req = Request('POST',url, data=post_data, headers=headers) prepped = req.prepare() prepped.body+="&scope=" prepped.body+=scope response = s.send(prepped) token_json = response.json() try: thetoken = token_json['access_token'] expires_in = token_json['expires_in'] refresh_token = token_json['refresh_token'] except: with open("media/c2errors.log","a") as errorlog: errorstring = str(sys.exc_info()[0]) timestr = time.strftime("%Y%m%d-%H%M%S") errorlog.write(timestr+errorstring+"\r\n") errorlog.write(str(token_json)+"\r\n") thetoken = None expires_in = None refresh_token = None return [thetoken,expires_in,refresh_token] # Exchange authorization code for authorization token def get_token(code): messg='' scope = "user:read,results:write" client_auth = requests.auth.HTTPBasicAuth(C2_CLIENT_ID, C2_CLIENT_SECRET) post_data = {"grant_type": "authorization_code", "code": code, "redirect_uri": C2_REDIRECT_URI, "client_secret": C2_CLIENT_SECRET, "client_id":C2_CLIENT_ID, } headers = {'user-agent': 'sanderroosendaal'} url = "https://log.concept2.com/oauth/access_token" s = Session() req = Request('POST',url, data=post_data, headers=headers) prepped = req.prepare() prepped.body+="&scope=" prepped.body+=scope response = s.send(prepped) token_json = response.json() try: status_code = response.status_code # status_code = token_json['status_code'] except AttributeError: # except KeyError: return (0,response.text) try: status_code = token_json.status_code except AttributeError: return (0,'Attribute Error on c2_get_token') if status_code == 200: thetoken = token_json['access_token'] expires_in = token_json['expires_in'] refresh_token = token_json['refresh_token'] else: return (0,token_json['message']) return (thetoken,expires_in,refresh_token,messg) # Make URL for authorization and load it def make_authorization_url(request): # Generate a random string for the state parameter # Save it for use later to prevent xsrf attacks from uuid import uuid4 state = str(uuid4()) scope = "user:read,results:write" params = {"client_id": C2_CLIENT_ID, "response_type": "code", "redirect_uri": C2_REDIRECT_URI} url = "https://log.concept2.com/oauth/authorize?"+ urllib.parse.urlencode(params) url += "&scope="+scope return HttpResponseRedirect(url) # Get workout from C2 ID def get_workout(user,c2id): r = Rower.objects.get(user=user) if (r.c2token == '') or (r.c2token is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) ,0 elif (timezone.now()>r.tokenexpirydate): s = "Token expired. Needs to refresh." return custom_exception_handler(401,s),0 else: # ready to fetch. Hurray authorizationstring = str('Bearer ' + r.c2token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} url = "https://log.concept2.com/api/users/me/results/"+str(c2id) s = requests.get(url,headers=headers) data = s.json()['data'] splitdata = None if 'workout' in data: if 'splits' in data['workout']: splitdata = data['workout']['splits'] elif 'intervals' in data['workout']: splitdata = data['workout']['intervals'] else: splitdata = None # Check if workout has stroke data, and get the stroke data if data['stroke_data']: res2 = get_c2_workout_strokes(user,c2id) if res2.status_code == 200: strokedata = pd.DataFrame.from_dict(res2.json()['data']) else: strokedata = pd.DataFrame() else: strokedata = pd.DataFrame() return data,strokedata # Get stroke data belonging to C2 ID def get_c2_workout_strokes(user,c2id): r = Rower.objects.get(user=user) if (r.c2token == '') or (r.c2token is None): return custom_exception_handler(401,s) s = "Token doesn't exist. Need to authorize" elif (timezone.now()>r.tokenexpirydate): s = "Token expired. Needs to refresh." return custom_exception_handler(401,s) else: # ready to fetch. Hurray authorizationstring = str('Bearer ' + r.c2token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} url = "https://log.concept2.com/api/users/me/results/"+str(c2id)+"/strokes" s = requests.get(url,headers=headers) return s # Get list of C2 workouts. We load only the first page, # assuming that users don't want to import their old workouts def get_c2_workout_list(user,page=1): r = Rower.objects.get(user=user) if (r.c2token == '') or (r.c2token is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) elif (timezone.now()>r.tokenexpirydate): s = "Token expired. Needs to refresh." return custom_exception_handler(401,s) else: # ready to fetch. Hurray authorizationstring = str('Bearer ' + r.c2token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} url = "https://log.concept2.com/api/users/me/results" url += "?page={page}".format(page=page) s = requests.get(url,headers=headers) return s # Get username, having access token. # Handy for checking if the API access is working def get_username(access_token): authorizationstring = str('Bearer ' + access_token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} import urllib url = "https://log.concept2.com/api/users/me" response = requests.get(url,headers=headers) me_json = response.json() try: res = me_json['data']['username'] id = me_json['data']['id'] except KeyError: res = None return res # Get user id, having access token # Handy for checking if the API access is working def get_userid(access_token): authorizationstring = str('Bearer ' + access_token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} import urllib url = "https://log.concept2.com/api/users/me" try: response = requests.get(url,headers=headers) except: return 0 me_json = response.json() try: res = me_json['data']['id'] except KeyError: res = 0 return res # For debugging purposes def process_callback(request): # need error handling code = request.GET['code'] access_token = get_token(code) username,id = get_username(access_token) return HttpResponse("got a user name: %s" % username) def default(o): if isinstance(o, numpy.int64): return int(o) raise TypeError # Uploading workout def workout_c2_upload(user,w): message = 'trying C2 upload' try: if mytypes.c2mapping[w.workouttype] is None: return "This workout type cannot be uploaded to Concept2",0 except KeyError: return "This workout type cannot be uploaded to Concept2",0 thetoken = c2_open(user) r = Rower.objects.get(user=user) # ready to upload. Hurray if (checkworkoutuser(user,w)): c2userid = get_userid(r.c2token) if not c2userid: raise NoTokenError("User has no token") data = createc2workoutdata(w) if data == 0: return "Error: No data file. Contact info@rowsandall.com if the problem persists",0 authorizationstring = str('Bearer ' + r.c2token) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} import urllib url = "https://log.concept2.com/api/users/%s/results" % (c2userid) response = requests.post(url,headers=headers,data=json.dumps(data,default=default)) if (response.status_code == 409 ): message = "Concept2 Duplicate error" w.uploadedtoc2 = -1 c2id = -1 w.save() elif (response.status_code == 201 or response.status_code == 200): # s= json.loads(response.text) s = response.json() c2id = s['data']['id'] w.uploadedtoc2 = c2id w.save() message = "Upload to Concept2 was successful" else: message = "Something went wrong in workout_c2_upload_view. Response code 200/201 but C2 sync failed: "+response.text c2id = 0 return message,c2id # This is token refresh. Looks for tokens in our database, then refreshes def rower_c2_token_refresh(user): r = Rower.objects.get(user=user) res = do_refresh_token(r.c2refreshtoken) if res[0]: access_token = res[0] expires_in = res[1] refresh_token = res[2] expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) r = Rower.objects.get(user=user) r.c2token = access_token r.tokenexpirydate = expirydatetime r.c2refreshtoken = refresh_token r.save() return r.c2token else: return None # Create workout data from Strava or Concept2 # data and create the associated Workout object and save it def add_workout_from_data(user,importid,data,strokedata, source='c2',splitdata=None, workoutsource='concept2'): try: workouttype = mytypes.c2mappinginv[data['type']] except KeyError: workouttype = 'rower' if workouttype not in [x[0] for x in Workout.workouttypes]: workouttype = 'other' try: comments = data['comments'] except: comments = ' ' try: thetimezone = tz(data['timezone']) except: thetimezone = 'UTC' r = Rower.objects.get(user=user) try: rowdatetime = iso8601.parse_date(data['date_utc']) except KeyError: rowdatetime = iso8601.parse_date(data['start_date']) except ParseError: rowdatetime = iso8601.parse_date(data['date']) try: c2intervaltype = data['workout_type'] except KeyError: c2intervaltype = '' try: title = data['name'] except KeyError: title = "" try: t = data['comments'].split('\n', 1)[0] title += t[:20] except: title = 'Imported' starttimeunix = arrow.get(rowdatetime).timestamp res = make_cumvalues(0.1*strokedata['t']) cum_time = res[0] lapidx = res[1] unixtime = cum_time+starttimeunix # unixtime[0] = starttimeunix seconds = 0.1*strokedata.loc[:,'t'] nr_rows = len(unixtime) try: latcoord = strokedata.loc[:,'lat'] loncoord = strokedata.loc[:,'lon'] except: latcoord = np.zeros(nr_rows) loncoord = np.zeros(nr_rows) try: strokelength = strokedata.loc[:,'strokelength'] except: strokelength = np.zeros(nr_rows) dist2 = 0.1*strokedata.loc[:,'d'] try: spm = strokedata.loc[:,'spm'] except KeyError: spm = 0*dist2 try: hr = strokedata.loc[:,'hr'] except KeyError: hr = 0*spm pace = strokedata.loc[:,'p']/10. pace = np.clip(pace,0,1e4) pace = pace.replace(0,300) velo = 500./pace power = 2.8*velo**3 if workouttype == 'bike': velo = 1000./pace pace = 500./velo # save csv # Create data frame with all necessary data to write to csv df = pd.DataFrame({'TimeStamp (sec)':unixtime, ' Horizontal (meters)': dist2, ' Cadence (stokes/min)':spm, ' HRCur (bpm)':hr, ' longitude':loncoord, ' latitude':latcoord, ' Stroke500mPace (sec/500m)':pace, ' Power (watts)':power, ' DragFactor':np.zeros(nr_rows), ' DriveLength (meters)':np.zeros(nr_rows), ' StrokeDistance (meters)':strokelength, ' DriveTime (ms)':np.zeros(nr_rows), ' StrokeRecoveryTime (ms)':np.zeros(nr_rows), ' AverageDriveForce (lbs)':np.zeros(nr_rows), ' PeakDriveForce (lbs)':np.zeros(nr_rows), ' lapIdx':lapidx, ' ElapsedTime (sec)':seconds }) df.sort_values(by='TimeStamp (sec)',ascending=True) timestr = strftime("%Y%m%d-%H%M%S") # Create CSV file name and save data to CSV file csvfilename ='media/{code}_{importid}.csv'.format( importid=importid, code = uuid4().hex[:16] ) res = df.to_csv(csvfilename+'.gz',index_label='index', compression='gzip') # with Concept2 if source=='c2': try: totaldist = data['distance'] totaltime = data['time']/10. except KeyError: totaldist = 0 totaltime = 0 else: totaldist = 0 totaltime = 0 id,message = dataprep.save_workout_database( csvfilename,r, workouttype=workouttype, title=title,notes=comments, workoutsource=workoutsource, dosummary=True ) return id,message