Private
Public Access
1
0

runkeeper functionality complete (not fully tested)

This commit is contained in:
Sander Roosendaal
2017-03-23 20:07:10 +01:00
parent d89219a2df
commit 08c0838d85
4 changed files with 375 additions and 40 deletions

View File

@@ -35,6 +35,14 @@ from rowsandall_app.settings import (
RUNKEEPER_CLIENT_ID, RUNKEEPER_CLIENT_SECRET,RUNKEEPER_REDIRECT_URI, 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 # Exponentially weighted moving average
# Used for data smoothing of the jagged data obtained by Strava # Used for data smoothing of the jagged data obtained by Strava
# See bitbucket issue 72 # See bitbucket issue 72
@@ -149,40 +157,106 @@ def get_runkeeper_workout(user,runkeeperid):
return s return s
# Generate Workout data for Runkeeper (a TCX file) # Create Workout Data for upload to SportTracks
def createrunkeeperworkoutdata(w): def createrunkeeperworkoutdata(w):
filename = w.csvfilename filename = w.csvfilename
try: try:
row = rowingdata(filename) row = rowingdata(filename)
tcxfilename = filename[:-4]+'.tcx'
row.exporttotcx(tcxfilename,notes=w.notes)
except: 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 d = row.df.ix[:,'cum_dist'].values
# to rowing on Runkeeper d[0] = d[1]
def handle_runkeeperexport(f2,workoutname,runkeepertoken,description=''): t = t.astype(int)
# w = Workout.objects.get(id=workoutid) d = d.astype(int)
client = runkeeperlib.Client(access_token=runkeepertoken) spm = row.df[' Cadence (stokes/min)'].astype(int)
spm[0] = spm[1]
act = client.upload_activity(f2,'tcx',name=workoutname) hr = row.df[' HRCur (bpm)'].astype(int)
haslatlon=1
try: try:
res = act.wait(poll_interval=5.0,timeout=30) lat = row.df[' latitude'].values
message = 'Workout successfully synchronized to Runkeeper' lon = row.df[' longitude'].values
except: if not lat.std() and not lon.std():
res = 0 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")
if haslatlon:
data = {
# description doesn't work yet. Have to wait for runkeeperlib to update "type": "Rowing",
if res: "start_time": start_time,
act = client.update_activity(res.id,activity_type='Rowing',description=description) "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: else:
message = 'Runkeeper upload timed out.' data = {
return (0,message) "type": "Rowing",
"start_time": start_time,
return (res.id,message) "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)

View File

@@ -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 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 # Custom exception handler, returns a 401 HTTP message
# with exception details in the json data # with exception details in the json data
def custom_exception_handler(exc,message): def custom_exception_handler(exc,message):

View File

@@ -219,6 +219,7 @@ urlpatterns = [
url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view), url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view),
url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view), url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view),
url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_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'^multi-compare$',views.multi_compare_view),
url(r'^me/teams/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view), url(r'^me/teams/c/(?P<message>\w+.*)/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),
url(r'^me/teams/s/(?P<successmessage>\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P<successmessage>\w+.*)$',views.rower_teams_view),

View File

@@ -1,4 +1,5 @@
import time import time
import timestring
import zipfile import zipfile
import operator import operator
import warnings import warnings
@@ -46,6 +47,8 @@ import datetime
import iso8601 import iso8601
import c2stuff import c2stuff
from c2stuff import C2NoTokenError from c2stuff import C2NoTokenError
from runkeeperstuff import RunKeeperNoTokenError
from sporttracksstuff import SportTracksNoTokenError
from iso8601 import ParseError from iso8601 import ParseError
import stravastuff import stravastuff
import sporttracksstuff import sporttracksstuff
@@ -189,8 +192,28 @@ def get_time(second):
# get the workout ID from the SportTracks URI # get the workout ID from the SportTracks URI
def getidfromsturi(uri): def getidfromsturi(uri,length=8):
return uri[len(uri)-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 # Splits SportTracks data which is one long sequence of
# [t,[lat,lon],t2,[lat2,lon2] ...] # [t,[lat,lon],t2,[lat2,lon2] ...]
@@ -485,6 +508,166 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
return id,message 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 # Create workout from SportTracks Data, which are slightly different
# than Strava or Concept2 data # than Strava or Concept2 data
def add_workout_from_stdata(user,importid,data): def add_workout_from_stdata(user,importid,data):
@@ -515,19 +698,15 @@ def add_workout_from_stdata(user,importid,data):
except: except:
rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S") rowdatetime = datetime.datetime.strptime(data['date'],"%Y-%m-%d %H:%M:%S")
rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc) rowdatetime = thetimezone.localize(rowdatetime).astimezone(utc)
starttimeunix = mktime(rowdatetime.utctimetuple())
# try:
# c2intervaltype = data['workout_type']
# except:
# c2intervaltype = ''
try: try:
title = data['name'] title = data['name']
except: except:
title = "Imported data" title = "Imported data"
starttimeunix = mktime(rowdatetime.utctimetuple())
res = splitstdata(data['distance']) res = splitstdata(data['distance'])
@@ -683,7 +862,18 @@ def sporttracks_open(user):
thetoken = r.sporttrackstoken thetoken = r.sporttrackstoken
return thetoken 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 # Export workout to TCX and send to user's email address
@login_required() @login_required()
def workout_tcxemail_view(request,id=0): def workout_tcxemail_view(request,id=0):
@@ -965,6 +1155,70 @@ def workout_c2_upload_view(request,id=0):
return HttpResponseRedirect(url) 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 # Upload workout to SportTracks
@login_required() @login_required()
def workout_sporttracks_upload_view(request,id=0): def workout_sporttracks_upload_view(request,id=0):
@@ -4487,7 +4741,7 @@ def workout_runkeeperimport_view(request,message=""):
else: else:
workouts = [] workouts = []
for item in res.json()['items']: for item in res.json()['items']:
d = int(float(item['total_distance'])) d = int(float(item['total_distance']))
i = getidfromsturi(item['uri'],length=9) i = getidfromsturi(item['uri'],length=9)
ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) ttot = str(datetime.timedelta(seconds=int(float(item['duration']))))
s = item['start_time'] s = item['start_time']
@@ -4662,12 +4916,9 @@ def workout_getstravaworkout_view(request,stravaid):
# Imports a workout from Runkeeper # Imports a workout from Runkeeper
@login_required() @login_required()
def workout_getrunkeeperworkout_view(request,runkeeperid): def workout_getrunkeeperworkout_view(request,runkeeperid):
res = runkeeperstuff.get_runkeeper_workout(request.user,runkeeperid)
res = runkeeperstuff.get_runkeeper_workout(request.user,runkeeperid) res = runkeeperstuff.get_runkeeper_workout(request.user,runkeeperid)
data = res.json() data = res.json()
return HttpResponse(data)
id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data) id,message = add_workout_from_runkeeperdata(request.user,runkeeperid,data)
w = Workout.objects.get(id=id) w = Workout.objects.get(id=id)
w.uploadedtorunkeeper=runkeeperid w.uploadedtorunkeeper=runkeeperid