Private
Public Access
1
0

API v2 (version 1)

This commit is contained in:
Sander Roosendaal
2020-07-25 12:26:30 +02:00
parent 37ed947fb7
commit 7fd1b2ba42
4 changed files with 353 additions and 42 deletions

View File

@@ -10,91 +10,91 @@
<p>On this page, a work in progress, I will collect useful information
for developers of rowing data apps and hardware.</p>
<p>I presume you have an app (smartphone app, dedicated hardware, web site)
where your users (customers) generate, collect or store their rowing
related workout data. You can now offer your users easy ways to get
their data on this site.</p>
</li>
<li class="grid_2">
<li class="grid_4">
<p>There are three ways to allow your users to get data to Rowsandall.com.</p>
<h2>File based export from your app</h2>
<p>Enable export of TCX, FIT or CSV formatted files from your app.
The users
upload the file to Rowsandall.com.</p>
<ul class="contentli">
<p><ul class="contentli">
<li>Advantages
<ul class="contentli">
<p><ul class="contentli">
<li>User sees immediate results</li>
</ul>
</ul></p>
</li>
<li>Disadvantages
<ul class="contentli">
<p><ul class="contentli">
<li>It is a multi-step process: Download from your
app, store, upload.</li>
</ul>
</ul></p>
</li>
</ul>
</ul></p>
<h2>Email from your app</h2>
<p>Similar as above, generate TCX, FIT or CSV formatted files and
email them
to <em>workouts@rowsandall.com</em> directly from your app. The From: field
should be the email address of the registered user.</p>
<ul class="contentli">
<p><ul class="contentli">
<li>Advantages
<ul class="contentli">
<p><ul class="contentli">
<li>It's a simple process, which can be automated.</li>
</ul>
</ul></p>
</li>
<li>Disadvantages
<ul class="contentli">
<p><ul class="contentli">
<li>It may take up to five minutes for the workout to show up
on the site.</li>
</ul>
</ul></p>
</li>
</ul>
</ul></p>
<h2>Using the REST API</h2>
<p>We are building a REST API which will allow you to post and
receive stroke
data from the site directly.</p>
<p>The REST API is a work in progress. We are open to improvement
suggestions (provided they don't break existing apps). Please send
email to <a href="mailto:info@rowsandall.com">info@rowsandall.com</a>
with questions and/or suggestions. We
will get back to you as soon as possible.</p>
<ul class="contentli">
<p><ul class="contentli">
<li>Advantages
<ul class="contentli">
<p><ul class="contentli">
<li>Once it is set up, this is a one-click operation.</li>
<li>You can read a user's workout data from the site and use
them in your app.</li>
<li>This is not limited to workout data. You could make a full mobile
version of our site.</li>
</ul>
</ul></p>
</li>
<li>Disadvantages
<ul class="contentli">
<p><ul class="contentli">
<li>The API is not stable and not fully tested yet.</li>
<li>You need to register your app with us. We can revoke your
permissions if you misuse them.</li>
<li>The user user must grant permissions to your app.</li>
<li>You need to manage authorization tokens.</li>
</ul>
</ul></p>
</li>
</ul>
</ul></p>
</li>
<li class="grid_2">
<li class="grid_4">
<h2>Quick Links</h2>
<h3>Accepted file formats</h3>
@@ -103,20 +103,20 @@
<p>However, some rowing related parameters are not supported by TCX and FIT. Therefore, we are supporting the CSV format that is documented in the following link.</p>
<ul class="contentli"><li><a href="http://rowingdata.readthedocs.io/en/latest/#csv-file-standard">Our standard rowing CSV file</a></li></ul>
<p><ul class="contentli"><li><a href="http://rowingdata.readthedocs.io/en/latest/#csv-file-standard">Our standard rowing CSV file</a></li></ul></p>
<p>Using this standard will guarantee that your user's data are accepted
without complaints.</p>
<h2>API related documentation</h2>
<h3>Registering an app</h3>
<p>We have disabled the self service app link for security reasons.
We will replace it with a secure self service app link soon. If you
need to register an app, please send email to info@rowsandall.com</p>
<h3>Authentication</h3>
<p>Standard <a href="https://oauth.net/2/">Oauth2</a> authentication.
@@ -128,14 +128,15 @@
<p>The redirect URI for user authentication has to be <em>https</em>.
Developers of iOS or Android apps should contact me directly if
this doesn't work for them. I can add exceptions.</p>
<p>
<ul class="contentli">
<li>Authorization URL: <b>https://{{ request.get_host }}/rowers/o/authorize</b></li>
<li>Access Token request: <b>https://{{ request.get_host }}/rowers/o/token/</b></li>
<li>Access Token refresh: <b>https://{{ request.get_host }}/rowers/o/token/</b></li>
<li>Handy utility for testing: <b><a href="http://django-oauth-toolkit.herokuapp.com/consumer/">http://django-oauth-toolkit.herokuapp.com/consumer/</a></b></li>
</ul>
</p>
<h3>API documentation</h3>
@@ -146,12 +147,14 @@
<p>The workout summary data and the stroke data are obtained and sent
separately.</p>
<p>
<ul class="contentli">
<li><a href="/rowers/api-docs/">API documentation</a>
(But refer to the below for stroke data.)</li>
<li><a href="/rowers/api-docs#/workouts">Try out the workout summary API</a></li>
<li><a href="/rowers/api-docs#!/workouts/strokedata_list">GET stroke data</a></li>
</ul>
</p>
<h3>POST stroke data - API</h3>
<p>You can only post stroke data to an existing workout with
workout number {id}. If the workout already has stroke data, you
@@ -159,9 +162,13 @@
future to enable updating stroke data. Stroke data for workout {id} are
posted to:</p>
<h4>API v1</h4>
<p>
<ul class="contentli">
<li><b>https://{{ request.get_host }}/rowers/api/workouts/{id}/strokedata</b></li>
</ul>
</p>
<p>The payload is application/json data and looks as follows:</p>
@@ -176,14 +183,48 @@
}
</pre></p>
<p>Mandatory data fields are:</p>
<h4>API v2</h4>
<p>
<ul class="contentli">
<li><b>https://{{ request.get_host }}/rowers/api/v2/workouts/{id}/strokedata</b></li>
</ul>
</p>
<p>The payload is application/json data and looks as follows:</p>
<p><pre>
{'data':
[
{"distance":5, "power": 112, "hr": 132, "pace": 145800, "spm": 11, "time": 0},
{"distance":12, "power": 221, "hr": 131, "pace": 116400, "spm": 41, "time": 2200},
{"distance":19, "power": 511, "hr": 131, "pace": 88100, "spm": 56, "time": 4599},
{"distance":27, "power": 673, "hr": 132, "pace": 80400, "spm": 59, "time": 7000},
{"distance":35, "power": 744, "hr": 133, "pace": 77700, "spm": 55, "time": 9599},
{"distance":43, "power": 754, "hr": 136, "pace": 77400, "spm": 48, "time": 12000},
{"distance":51, "power": 754, "hr": 139, "pace": 77400, "spm": 48, "time": 14400},
{"distance":59, "power": 749, "hr": 142, "pace": 77600, "spm": 48, "time": 16799},
{"distance":67, "power": 729, "hr": 145, "pace": 78300, "spm": 48, "time": 19400},
{"distance":75, "power": 729, "hr": 147, "pace": 78300, "spm": 48, "time": 21799},
{"distance":82, "power": 726, "hr": 150, "pace": 78400, "spm": 48, "time": 24200},
{"distance":90, "power": 709, "hr": 152, "pace": 79000, "spm": 48, "time": 26599},
{"distance":100, "power": 707, "hr": 153, "pace": 79100, "spm": 49, "time": 29000},
]
}
</pre></p>
<p>For both v1 and v2, mandatory data fields are:</p>
<p>
<ul class="contentli">
<li><b>time</b>: Time (milliseconds since workout start)</li>
<li><b>distance</b>: Distance (meters)</li>
<li><b>pace</b>: Pace (milliseconds per 500m)</li>
<li><b>spm</b> Stroke rate (strokes per minute)</li>
</ul>
</p>
<p>Optional data fiels are:</p>
<p>
<ul class="contentli">
<li><b>power</b>: Power (Watt)</li>
<li><b>drivelength</b>: Drive length (meters)</li>
@@ -201,13 +242,13 @@
<li><b>slip</b>: Slip as defined per Empower oarlock (degrees)</li>
</ul>
<p>Consistency checks will be done and the stroke data will be
</p>
<p>For both API versions, consistency checks will be done and the stroke data will be
refused if the mandatory data fields don't pass the checks.
All mandatory data fields
must have the same number of records. If an optional data field
fails a test, its values are silently replaced by zeros.</p>
</li>
</ul>
@@ -216,4 +257,3 @@
{% block sidebar %}
{% include 'menu_help.html' %}
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "newbase.html" %}
{% block main %}
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<h1>Stroke Data for workout</h1>
<form enctype="application/json" action="/rowers/api/v2/workouts/{{ id }}/strokedata/" method="post">
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
<input class="button green" type="submit" value="POST">
</form>
{% endblock %}
{% block sidebar %}
{% include 'menu_workout.html' %}
{% endblock %}

View File

@@ -210,6 +210,7 @@ urlpatterns = [
re_path(r'^api-docs/$', views.schema_view,name='schema_view'),
re_path(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
re_path(r'^api/workouts/(?P<id>\b[0-9A-Fa-f]+\b)/strokedata/$',views.strokedatajson,name='strokedatajson'),
re_path(r'^api/v2/workouts/(?P<id>\b[0-9A-Fa-f]+\b)/strokedata/$',views.strokedatajson_v2,name='strokedatajson_v2'),
re_path(r'^500v/$',views.error500_view,name='error500_view'),
path('502/', TemplateView.as_view(template_name='502.html'),name='502'),
path('500/', TemplateView.as_view(template_name='500.html'),name='500'),
@@ -738,6 +739,7 @@ urlpatterns = [
views.TrainingTargetUpdate.as_view()),
name='trainingtarget_update_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/test\_strokedata/$',views.strokedataform),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/v2/test\_strokedata/$',views.strokedataform_v2),
re_path(r'^sessions/teamcreate/user/(?P<userid>\d+)/$',views.plannedsession_teamcreate_view,
name='plannedsession_teamcreate_view'),
re_path(r'^sessions/teamcreate/team/(?P<teamid>\d+)/user/(?P<userid>\d+)/$',

View File

@@ -40,10 +40,256 @@ def strokedataform(request,id=0):
'workout':w,
})
@login_required()
def strokedataform_v2(request,id=0):
try:
id=int(id)
except ValueError:
id = 0
try:
w = Workout.objects.get(id=id)
except Workout.DoesNotExist:
raise Http404("Workout doesn't exist")
if request.method == 'GET':
form = StrokeDataForm()
return render(request, 'strokedata_form_v2.html',
{
'form':form,
'teams':get_my_teams(request.user),
'id':id,
'workout':w,
})
elif request.method == 'POST':
form = StrokeDataForm()
return render(request, 'strokedata_form_v2.html',
{
'form':form,
'teams':get_my_teams(request.user),
'id':id,
'workout':w,
})
# Process the POSTed stroke data according to the API definition
# Return the GET stroke data according to the API definition
from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer
@csrf_exempt
@login_required()
@api_view(["GET","POST"])
def strokedatajson_v2(request,id):
"""
POST: Add Stroke data to workout
GET: Get stroke data of workout
This v2 API works on stroke based data dict:
{"data": [{"hr": 110, "p": 3600, "spm": 53, "d": 6, "t": 12}, {"hr": 111, "p": 3600, "spm": 53, "d": 6, "t": 12}, {"hr": 111, "p": 3600, "spm": 64, "d": 6, "t": 22}, {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 55}, {"hr": 110, "p": 3600, "spm": 16, "d": 14, "t": 82}, {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 109}, {"hr": 107, "p": 3600, "spm": 12, "d": 22, "t": 133}, {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3577, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3411, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 2649, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3099, "spm": 12, "d": 32, "t": 157}, {"hr": 108, "p": 3600, "spm": 12, "d": 32, "t": 157}, {"hr": 100, "p": 3600, "spm": 44, "d": 115, "t": 292}, {"hr": 99, "p": 3600, "spm": 27, "d": 129, "t": 305}, {"hr": 97, "p": 3600, "spm": 34, "d": 161, "t": 330}, {"hr": 96, "p": 3600, "spm": 25, "d": 177, "t": 344}, {"hr": 96, "p": 3494, "spm": 43, "d": 196, "t": 357}, {"hr": 98, "p": 2927, "spm": 26, "d": 235, "t": 377}, {"hr": 102, "p": 2718, "spm": 27, "d": 380, "t": 455}, {"hr": 102, "p": 2753, "spm": 9, "d": 398, "t": 472}, {"hr": 102, "p": 2864, "spm": 61, "d": 406, "t": 477}, {"hr": 101, "p": 2780, "spm": 15, "d": 484, "t": 515}, {"hr": 101, "p": 2365, "spm": 16, "d": 583, "t": 554}, {"hr": 103, "p": 1965, "spm": 16, "d": 681, "t": 592}, {"hr": 104, "
"""
row = get_object_or_404(Workout,pk=id)
if row.user != request.user.rower:
raise PermissionDenied("You have no access to this workout")
try:
id = int(id)
except ValueError:
return HttpResponse("Not a valid workout number",stauts=400)
if request.method == 'GET':
columns = ['spm','time','hr','pace','power','distance']
datadf = dataprep.getsmallrowdata_db(columns,ids=[id])
with open('media/apilog.log','a') as logfile:
logfile.write(str(timezone.now())+": ")
logfile.write(request.user.username+"(GET) \n")
data = datadf.to_json(orient='records')
return JSONResponse(data)
if request.method == 'POST':
checkdata, r = dataprep.getrowdata_db(id=row.id)
if not checkdata.empty:
return HttpResponse("Duplicate Error",400)
df = pd.DataFrame()
try:
df = pd.read_json(request.POST['data'],orient='split')
except KeyError:
try:
df = pd.read_json(request.POST['strokedata'],orient='split')
except:
return HttpResponse("No JSON object could be decoded",400)
df.index = df.index.astype(int)
df.sort_index(inplace=True)
#time, pace, distance,spm
try:
time = df['time']/1.e3
except KeyError:
try:
time = df['t']/10.
except KeyError:
return HttpResponse("Missing time",400)
try:
spm = df['spm']
except KeyError:
return HttpResponse("Missing spm",400)
try:
distance = df['distance']
except KeyError:
try:
distance = df['d']/10.
except KeyError:
return HttpResponse("Missing distance",400)
try:
pace = df['pace']/1.e3
except KeyError:
try:
pace = df['p']/10.
except KeyError:
return HttpResponse("Missing pace",400)
try:
power = df['power']
except KeyError:
power = 0*time
try:
drivelength = df['drivelength']
except KeyError:
drivelength = 0*time
try:
drivespeed = df['drivespeed']
except KeyError:
drivespeed = 0*time
try:
dragfactor = df['dragfactor']
except KeyError:
dragfactor = 0*time
try:
drivetime = df['drivetime']
except KeyError:
drivetime = 0*time
try:
strokerecoverytime = df['strokerecoverytime']
except KeyError:
strokerecoverytime = 0*time
try:
averagedriveforce = df['averagedriveforce']
except KeyError:
averagedriveforce = 0*time
try:
peakdriveforce = df['peakdriveforce']
except KeyError:
peakdriveforce = 0*time
try:
wash = df['wash']
except KeyError:
wash = 0*time
try:
catch = df['catch']
except KeyError:
catch = 0*time
try:
finish = df['finish']
except KeyError:
finish = 0*time
try:
peakforceangle = df['peakforceangle']
except KeyError:
peakforceangle = 0*time
try:
driveenergy = df['driveenergy']
except KeyError:
driveenergy = 60.*power/spm
try:
slip = df['slip']
except KeyError:
slip = 0*time
try:
lapidx = df['lapidx']
except KeyError:
lapidx = 0*time
try:
hr = df['hr']
except KeyError:
hr = 0*df['time']
starttime = totimestamp(row.startdatetime)+time[0]
unixtime = starttime+time
with open('media/apilog.log','a') as logfile:
logfile.write(str(starttime)+": ")
logfile.write(request.user.username+"(POST) \r\n")
data = pd.DataFrame({'TimeStamp (sec)':unixtime,
' Horizontal (meters)': distance,
' Cadence (stokes/min)':spm,
' HRCur (bpm)':hr,
' DragFactor':dragfactor,
' Stroke500mPace (sec/500m)':pace,
' Power (watts)':power,
' DriveLength (meters)':drivelength,
' DriveTime (ms)':drivetime,
' StrokeRecoveryTime (ms)':strokerecoverytime,
' AverageDriveForce (lbs)':averagedriveforce,
' PeakDriveForce (lbs)':peakdriveforce,
' lapIdx':lapidx,
' ElapsedTime (sec)':time,
'catch':catch,
'slip':slip,
'finish':finish,
'wash':wash,
'driveenergy':driveenergy,
'peakforceangle':peakforceangle,
})
r = getrower(request.user)
timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S")
csvfilename ='media/Import_'+timestr+'.csv'
res = data.to_csv(csvfilename+'.gz',index_label='index',
compression='gzip')
row.csvfilename = csvfilename
row.save()
powerperc = 100*np.array([r.pw_ut2,
r.pw_ut1,
r.pw_at,
r.pw_tr,r.pw_an])/r.ftp
ftp = float(r.ftp)
if row.workouttype in mytypes.otwtypes:
ftp = ftp*(100.-r.otwslack)/100.
rr = rrower(hrmax=r.max,hrut2=r.ut2,
hrut1=r.ut1,hrat=r.at,
hrtr=r.tr,hran=r.an,ftp=ftp,
powerperc=powerperc,powerzones=r.powerzones)
rowdata = rdata(row.csvfilename,rower=rr).df
datadf = dataprep.dataprep(rowdata,id=row.id,bands=True,barchart=True,otwpower=True,empower=True)
return(HttpResponse(encoder.encode_hex(row.id),status=201))
return HttpResponseNotAllowed("Method not supported")
@csrf_exempt
@login_required()
@api_view(['GET','POST'])
@@ -182,7 +428,7 @@ def strokedatajson(request,id):
# mangling
#
return HttpResponse(row.id,status=201)
return HttpResponse(encoder.encode_hex(row.id),status=201)
#Method not supported
return HttpResponseNotAllowed("Method not supported")