diff --git a/rowers/runkeeperstuff.py b/rowers/runkeeperstuff.py index 48b82a31..c29f8f7e 100644 --- a/rowers/runkeeperstuff.py +++ b/rowers/runkeeperstuff.py @@ -35,6 +35,14 @@ from rowsandall_app.settings import ( RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET,RUNKEEPER_REDIRECT_URI, ) +# Custom error class - to raise a NoTokenError +class RunKeeperNoTokenError(Exception): + def __init__(self,value): + self.value=value + + def __str__(self): + return repr(self.value) + # Exponentially weighted moving average # Used for data smoothing of the jagged data obtained by Strava # See bitbucket issue 72 @@ -149,40 +157,106 @@ def get_runkeeper_workout(user,runkeeperid): return s -# Generate Workout data for Runkeeper (a TCX file) +# Create Workout Data for upload to SportTracks def createrunkeeperworkoutdata(w): filename = w.csvfilename try: row = rowingdata(filename) - tcxfilename = filename[:-4]+'.tcx' - row.exporttotcx(tcxfilename,notes=w.notes) except: - tcxfilename = 0 + return 0 + + averagehr = int(row.df[' HRCur (bpm)'].mean()) + maxhr = int(row.df[' HRCur (bpm)'].max()) + duration = w.duration.hour*3600 + duration += w.duration.minute*60 + duration += w.duration.second + duration += +1.0e-6*w.duration.microsecond - return tcxfilename + # adding diff, trying to see if this is valid + #t = row.df.ix[:,'TimeStamp (sec)'].values-10*row.df.ix[0,'TimeStamp (sec)'] + t = row.df.ix[:,'TimeStamp (sec)'].values-row.df.ix[0,'TimeStamp (sec)'] + t[0] = t[1] -# Upload the TCX file to Runkeeper and set the workout activity type -# to rowing on Runkeeper -def handle_runkeeperexport(f2,workoutname,runkeepertoken,description=''): - # w = Workout.objects.get(id=workoutid) - client = runkeeperlib.Client(access_token=runkeepertoken) - - act = client.upload_activity(f2,'tcx',name=workoutname) + d = row.df.ix[:,'cum_dist'].values + d[0] = d[1] + t = t.astype(int) + d = d.astype(int) + spm = row.df[' Cadence (stokes/min)'].astype(int) + spm[0] = spm[1] + hr = row.df[' HRCur (bpm)'].astype(int) + + haslatlon=1 + try: - res = act.wait(poll_interval=5.0,timeout=30) - message = 'Workout successfully synchronized to Runkeeper' - except: - res = 0 + lat = row.df[' latitude'].values + lon = row.df[' longitude'].values + if not lat.std() and not lon.std(): + haslatlon = 0 + except KeyError: + haslatlon = 0 + + # path data + if haslatlon: + locdata = [] + for e in zip(t,lat,lon): + point = {'timestamp':e[0], + 'latitude':e[1], + 'longitude':e[2],} + locdata.append(point) + + hrdata = [] + for e in zip(t,hr): + point = {'timestamp':e[0], + 'heart_rate':e[1] + } + hrdata.append(point) + + distancedata = [] + for e in zip(t,d): + point = {'timestamp':e[0], + 'distance':e[1] + } + distancedata.append(point) + + start_time = w.startdatetime.strftime("%a, %d %b %Y %H:%M:%S") - - - # description doesn't work yet. Have to wait for runkeeperlib to update - if res: - act = client.update_activity(res.id,activity_type='Rowing',description=description) + if haslatlon: + data = { + "type": "Rowing", + "start_time": start_time, + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "average_heart_rate": averagehr, + "path": locdata, + "distance": distancedata, + "heartrate": hrdata, + "post_to_twitter":"false", + "post_to_facebook":"false", + } else: - message = 'Runkeeper upload timed out.' - return (0,message) - - return (res.id,message) + data = { + "type": "Rowing", + "start_time": start_time, + "total_distance": int(w.distance), + "duration": duration, + "notes": w.notes, + "avg_heartrate": averagehr, + "distance": distancedata, + "heartrate": hrdata, + "post_to_twitter":"false", + "post_to_facebook":"false", + } + return data + +# Obtain Runkeeper Workout ID from the response returned on successful +# upload +def getidfromresponse(response): + uri = response.headers["Location"] + id = uri[len(uri)-9:] + + return int(id) + + diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py index 15447944..0e15aca6 100644 --- a/rowers/sporttracksstuff.py +++ b/rowers/sporttracksstuff.py @@ -33,6 +33,15 @@ from rowers.models import Rower,Workout from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI +# Custom error class - to raise a NoTokenError +class SportTracksNoTokenError(Exception): + def __init__(self,value): + self.value=value + + def __str__(self): + return repr(self.value) + + # Custom exception handler, returns a 401 HTTP message # with exception details in the json data def custom_exception_handler(exc,message): diff --git a/rowers/urls.py b/rowers/urls.py index 512e1356..ae58dcad 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -219,6 +219,7 @@ urlpatterns = [ url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view), url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view), url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), + url(r'^workout/(\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view), url(r'^multi-compare$',views.multi_compare_view), url(r'^me/teams/c/(?P\w+.*)/s/(?P\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P\w+.*)$',views.rower_teams_view), diff --git a/rowers/views.py b/rowers/views.py index 603a4e44..a69ecc10 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1,4 +1,5 @@ import time +import timestring import zipfile import operator import warnings @@ -46,6 +47,8 @@ import datetime import iso8601 import c2stuff from c2stuff import C2NoTokenError +from runkeeperstuff import RunKeeperNoTokenError +from sporttracksstuff import SportTracksNoTokenError from iso8601 import ParseError import stravastuff import sporttracksstuff @@ -189,8 +192,28 @@ def get_time(second): # get the workout ID from the SportTracks URI -def getidfromsturi(uri): - return uri[len(uri)-8:] +def getidfromsturi(uri,length=8): + return uri[len(uri)-length:] + +def splitrunkeeperlatlongdata(lijst,tname,latname,lonname): + t = [] + lat = [] + lon = [] + for d in lijst: + t.append(d[tname]) + lat.append(d[latname]) + lon.append(d[lonname]) + + return [np.array(t),np.array(lat),np.array(lon)] + +def splitrunkeeperdata(lijst,xname,yname): + x = [] + y = [] + for d in lijst: + x.append(d[xname]) + y.append(d[yname]) + + return [np.array(x),np.array(y)] # Splits SportTracks data which is one long sequence of # [t,[lat,lon],t2,[lat2,lon2] ...] @@ -485,6 +508,166 @@ def add_workout_from_strokedata(user,importid,data,strokedata, return id,message +# Create workout from RunKeeper Data +def add_workout_from_runkeeperdata(user,importid,data): + # To Do - add utcoffset to time + workouttype = data['type'] + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'water' + try: + comments = data['notes'] + except: + comments = '' + + try: + utcoffset = tz(data['utcoffset']) + except: + utcoffset = 0 + + r = Rower.objects.get(user=user) + + try: + rowdatetime = iso8601.parse_date(data['start_time']) + except iso8601.ParseError: + try: + rowdatetime = datetime.datetime.strptime(data['start_time'],"%Y-%m-%d %H:%M:%S") + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + except: + try: + rowdatetime = dateutil.parser.parse(data['start_time']) + #rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + except: + rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S") + rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) + starttimeunix = mktime(rowdatetime.utctimetuple()) + startimeunix += utcoffset*3600 + + + try: + title = data['name'] + except: + title = "Imported data" + + + + res = splitrunkeeperdata(data['distance'],'timestamp','distance') + + distance = res[1] + times_distance = res[0] + + try: + l = data['path'] + + res = splitrunkeeperlatlongdata(l,'timestamp','latitude','longitude') + times_location = res[0] + latcoord = res[1] + loncoord = res[2] + + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype == 'water': + workouttype = 'rower' + + try: + res = splitrunkeeperdata(data['cadence'],'timestamp','cadence') + times_spm = res[0] + spm = res[1] + except KeyError: + times_spm = times_distance + spm = 0*times_distance + + try: + res = splitrunkeeperdata(data['heart_rate'],'timestamp','heart_rate') + hr = res[1] + times_hr = res[0] + except KeyError: + times_hr = times_distance + hr = 0*times_distance + + + # create data series and remove duplicates + distseries = pd.Series(distance,index=times_distance) + distseries = distseries.groupby(distseries.index).first() + latseries = pd.Series(latcoord,index=times_location) + latseries = latseries.groupby(latseries.index).first() + lonseries = pd.Series(loncoord,index=times_location) + lonseries = lonseries.groupby(lonseries.index).first() + spmseries = pd.Series(spm,index=times_spm) + spmseries = spmseries.groupby(spmseries.index).first() + hrseries = pd.Series(hr,index=times_hr) + hrseries = hrseries.groupby(hrseries.index).first() + + + # Create dicts and big dataframe + d = { + ' Horizontal (meters)': distseries, + ' latitude': latseries, + ' longitude': lonseries, + ' Cadence (stokes/min)': spmseries, + ' HRCur (bpm)' : hrseries, + } + + + + df = pd.DataFrame(d) + + df = df.groupby(level=0).last() + + cum_time = df.index.values + df[' ElapsedTime (sec)'] = cum_time + + velo = df[' Horizontal (meters)'].diff()/df[' ElapsedTime (sec)'].diff() + + df[' Power (watts)'] = 0.0*velo + + nr_rows = len(velo.values) + + df[' DriveLength (meters)'] = np.zeros(nr_rows) + df[' StrokeDistance (meters)'] = np.zeros(nr_rows) + df[' DriveTime (ms)'] = np.zeros(nr_rows) + df[' StrokeRecoveryTime (ms)'] = np.zeros(nr_rows) + df[' AverageDriveForce (lbs)'] = np.zeros(nr_rows) + df[' PeakDriveForce (lbs)'] = np.zeros(nr_rows) + df[' lapIdx'] = np.zeros(nr_rows) + + + + unixtime = cum_time+starttimeunix + unixtime[0] = starttimeunix + + df['TimeStamp (sec)'] = unixtime + + + dt = np.diff(cum_time).mean() + wsize = round(5./dt) + + velo2 = stravastuff.ewmovingaverage(velo,wsize) + + df[' Stroke500mPace (sec/500m)'] = 500./velo2 + + + df = df.fillna(0) + + df.sort_values(by='TimeStamp (sec)',ascending=True) + + timestr = strftime("%Y%m%d-%H%M%S") + + csvfilename ='media/Import_'+str(importid)+'.csv' + + res = df.to_csv(csvfilename+'.gz',index_label='index', + compression='gzip') + + id,message = dataprep.save_workout_database(csvfilename,r, + workouttype=workouttype, + title=title, + notes=comments) + + return (id,message) + + + # Create workout from SportTracks Data, which are slightly different # than Strava or Concept2 data def add_workout_from_stdata(user,importid,data): @@ -515,19 +698,15 @@ def add_workout_from_stdata(user,importid,data): except: rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S") rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) - -# try: -# c2intervaltype = data['workout_type'] - -# except: -# c2intervaltype = '' + starttimeunix = mktime(rowdatetime.utctimetuple()) + try: title = data['name'] except: title = "Imported data" - starttimeunix = mktime(rowdatetime.utctimetuple()) + res = splitstdata(data['distance']) @@ -683,7 +862,18 @@ def sporttracks_open(user): thetoken = r.sporttrackstoken return thetoken - + +# Checks if user has SportTracks token, renews them if they are expired +def runkeeper_open(user): + r = Rower.objects.get(user=user) + if (r.runkeepertoken == '') or (r.runkeepertoken is None): + s = "Token doesn't exist. Need to authorize" + raise RunKeeperNoTokenError("User has no token") + else: + thetoken = r.runkeepertoken + + return thetoken + # Export workout to TCX and send to user's email address @login_required() def workout_tcxemail_view(request,id=0): @@ -965,6 +1155,70 @@ def workout_c2_upload_view(request,id=0): return HttpResponseRedirect(url) +# Upload workout to RunKeeper +@login_required() +def workout_runkeeper_upload_view(request,id=0): + message = "" + try: + thetoken = runkeeper_open(request.user) + except RunKeeperNoTokenError: + return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") + + # ready to upload. Hurray + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + if (checkworkoutuser(request.user,w)): + data = runkeeperstuff.createrunkeeperworkoutdata(w) + if not data: + message = "Data error" + url = reverse(workout_export_view, + kwargs = { + 'message':str(message), + 'id':str(w.id), + }) + return HttpResponseRedirect(url) + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json', + 'Content-Length':'nnn'} + + import urllib + url = "https://api.runkeeper.com/fitnessActivities" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + w.uploadedtorunkeeper = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + runkeeperid = runkeeperstuff.getidfromresponse(response) + w.uploadedtorunkeeper = runkeeperid + w.save() + url = "/rowers/workout/"+str(w.id)+"/export" + return HttpResponseRedirect(url) + else: + s = response + print dir(s) + print s.text + message = "Something went wrong in workout_runkeeper_upload_view: %s" % s.reason + + else: + message = "You are not authorized to upload this workout" + + url = reverse(workout_export_view, + kwargs = { + 'message':str(message), + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + # Upload workout to SportTracks @login_required() def workout_sporttracks_upload_view(request,id=0): @@ -4487,7 +4741,7 @@ def workout_runkeeperimport_view(request,message=""): workouts = [] for item in res.json()['items']: d = int(float(item['total_distance'])) - i = getidfromsturi(item['uri']) + i = getidfromsturi(item['uri'],length=9) ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) s = item['start_time'] r = item['type'] @@ -4662,12 +4916,9 @@ def workout_getstravaworkout_view(request,stravaid): @login_required() def workout_getrunkeeperworkout_view(request,runkeeperid): res = runkeeperstuff.get_runkeeper_workout(request.user,runkeeperid) - print res.json() data = res.json() - return HttpResponse(data) - - id,message = add_workout_from_stdata(request.user,runkeeperid,data) + id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data) w = Workout.objects.get(id=id) w.uploadedtorunkeeper=runkeeperid w.save()