# All the functionality needed to connect to Strava # Python import oauth2 as oauth import cgi import requests import requests.auth import json from django.utils import timezone from datetime import datetime import numpy as np from dateutil import parser import time import math from math import sin,cos,atan2,sqrt import os,sys import gzip # Django from django.shortcuts import render_to_response from django.http import HttpResponseRedirect, HttpResponse,JsonResponse from django.conf import settings from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required # Project # from .models import Profile from rowingdata import rowingdata import pandas as pd from rowers.models import Rower,Workout from rowers.models import checkworkoutuser import stravalib from stravalib.exc import ActivityUploadFailed,TimeoutExceeded from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET # Exponentially weighted moving average # Used for data smoothing of the jagged data obtained by Strava # See bitbucket issue 72 def ewmovingaverage(interval,window_size): # Experimental code using Exponential Weighted moving average try: intervaldf = pd.DataFrame({'v':interval}) idf_ewma1 = intervaldf.ewm(span=window_size) idf_ewma2 = intervaldf[::-1].ewm(span=window_size) i_ewma1 = idf_ewma1.mean().ix[:,'v'] i_ewma2 = idf_ewma2.mean().ix[:,'v'] interval2 = np.vstack((i_ewma1,i_ewma2[::-1])) interval2 = np.mean( interval2, axis=0) # average except ValueError: interval2 = interval return interval2 from utils import geo_distance # Custom exception handler, returns a 401 HTTP message # with exception details in the json data def custom_exception_handler(exc,message): response = { "errors": [ { "code": str(exc), "detail": message, } ] } res = HttpResponse(message) res.status_code = 401 res.json = json.dumps(response) return res # Custom error class - to raise a NoTokenError class StravaNoTokenError(Exception): def __init__(self,value): self.value=value def __str__(self): return repr(self.value) # Exchange access code for long-lived access token def get_token(code): client_auth = requests.auth.HTTPBasicAuth(STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET) post_data = {"grant_type": "authorization_code", "code": code, "redirect_uri": STRAVA_REDIRECT_URI, "client_secret": STRAVA_CLIENT_SECRET, "client_id":STRAVA_CLIENT_ID, } headers = {'user-agent': 'sanderroosendaal'} response = requests.post("https://www.strava.com/oauth/token", data=post_data, headers=headers) try: token_json = response.json() thetoken = token_json['access_token'] except KeyError: thetoken = 0 return [thetoken] # Make authorization URL including random string 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()) params = {"client_id": STRAVA_CLIENT_ID, "response_type": "code", "redirect_uri": STRAVA_REDIRECT_URI, "scope":"write"} import urllib url = "https://www.strava.com/oauth/authorize" +urllib.urlencode(params) return HttpResponseRedirect(url) # Get list of workouts available on Strava def get_strava_workout_list(user): r = Rower.objects.get(user=user) if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) else: # ready to fetch. Hurray authorizationstring = str('Bearer ' + r.stravatoken) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json'} url = "https://www.strava.com/api/v3/athlete/activities" s = requests.get(url,headers=headers) return s # Get a Strava workout summary data and stroke data by ID def get_strava_workout(user,stravaid): r = Rower.objects.get(user=user) if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" return custom_exception_handler(401,s) else: # ready to fetch. Hurray fetchresolution = 'high' series_type = 'time' authorizationstring = str('Bearer ' + r.stravatoken) headers = {'Authorization': authorizationstring, 'user-agent': 'sanderroosendaal', 'Content-Type': 'application/json', 'resolution': 'medium',} url = "https://www.strava.com/api/v3/activities/"+str(stravaid) workoutsummary = requests.get(url,headers=headers).json() workoutsummary['timezone'] = "Etc/UTC" startdatetime = workoutsummary['start_date'] url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/cadence?resolution="+fetchresolution+"&series_type="+series_type spmjson = requests.get(url,headers=headers) url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/heartrate?resolution="+fetchresolution+"&series_type="+series_type hrjson = requests.get(url,headers=headers) url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/time?resolution="+fetchresolution+"&series_type="+series_type timejson = requests.get(url,headers=headers) url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/velocity_smooth?resolution="+fetchresolution+"&series_type="+series_type velojson = requests.get(url,headers=headers) url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/distance?resolution="+fetchresolution+"&series_type="+series_type distancejson = requests.get(url,headers=headers) url = "https://www.strava.com/api/v3/activities/"+str(stravaid)+"/streams/latlng?resolution="+fetchresolution+"&series_type="+series_type latlongjson = requests.get(url,headers=headers) try: t = np.array(timejson.json()[0]['data']) nr_rows = len(t) d = np.array(distancejson.json()[1]['data']) if nr_rows == 0: return (0,"Error: Time data had zero length") except IndexError: return (0,"Error: No Distance information in the Strava data") except KeyError: return (0,"something went wrong with the Strava import") try: spm = np.array(spmjson.json()[1]['data']) except: spm = np.zeros(nr_rows) try: hr = np.array(hrjson.json()[1]['data']) except IndexError: hr = np.zeros(nr_rows) except KeyError: hr = np.zeros(nr_rows) try: velo = np.array(velojson.json()[1]['data']) except IndexError: velo = np.zeros(nr_rows) except KeyError: velo = np.zeros(nr_rows) dt = np.diff(t).mean() wsize = round(5./dt) velo2 = ewmovingaverage(velo,wsize) coords = np.array(latlongjson.json()[0]['data']) try: lat = coords[:,0] lon = coords[:,1] except IndexError: lat = np.zeros(len(t)) lon = np.zeros(len(t)) except KeyError: lat = np.zeros(len(t)) lon = np.zeros(len(t)) strokelength = velo*60./(spm) strokelength[np.isinf(strokelength)] = 0.0 pace = 500./(1.0*velo2) pace[np.isinf(pace)] = 0.0 df = pd.DataFrame({'t':10*t, 'd':10*d, 'p':10*pace, 'spm':spm, 'hr':hr, 'lat':lat, 'lon':lon, 'strokelength':strokelength, }) # startdatetime = datetime.datetime.strptime(startdatetime,"%Y-%m-%d-%H:%M:%S") return [workoutsummary,df] # Generate Workout data for Strava (a TCX file) def createstravaworkoutdata(w,dozip=True): filename = w.csvfilename row = rowingdata(filename) tcxfilename = filename[:-4]+'.tcx' try: newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' row.exporttotcx(tcxfilename,notes=newnotes) if dozip: gzfilename = tcxfilename+'.gz' with file(tcxfilename,'rb') as inF: s = inF.read() with gzip.GzipFile(gzfilename,'wb') as outF: outF.write(s) try: os.remove(tcxfilename) except WindowError: pass return gzfilename,"" else: return tcxfilename,"" # Upload the TCX file to Strava and set the workout activity type # to rowing on Strava def handle_stravaexport(f2,workoutname,stravatoken,description=''): # w = Workout.objects.get(id=workoutid) client = stravalib.Client(access_token=stravatoken) act = client.upload_activity(f2,'tcx.gz',name=workoutname) try: res = act.wait(poll_interval=5.0,timeout=30) message = 'Workout successfully synchronized to Strava' except: res = 0 message = 'Strava upload timed out' # description doesn't work yet. Have to wait for stravalib to update if res: act = client.update_activity(res.id,activity_type='Rowing',description=description,device_name='Rowsandall.com') else: message = 'Strava activity update timed out.' return (0,message) return (res.id,message) def workout_strava_upload(user,w): message = "Uploading to Strava" stravaid=-1 r = Rower.objects.get(user=user) res = -1 if (r.stravatoken == '') or (r.stravatoken is None): s = "Token doesn't exist. Need to authorize" raise StravaNoTokenError else: if (checkworkoutuser(user,w)): try: tcxfile,tcxmesg = createstravaworkoutdata(w) if tcxfile: with open(tcxfile,'rb') as f: res,mes = handle_stravaexport(f,w.name, r.stravatoken, description=w.notes+'\n from '+w.workoutsource+' via rowsandall.com') if res==0: message = mes w.uploadedtostrava = -1 stravaid = -1 w.save() try: os.remove(tcxfile) except WindowsError: pass return message,stravaid w.uploadedtostrava = res w.save() try: os.remove(tcxfile) except WindowsError: pass message = mes stravaid = res return message,stravaid else: message = "Strava TCX data error "+tcxmesg w.uploadedtostrava = -1 stravaid = -1 w.save() return message, stravaid except ActivityUploadFailed as e: message = "Strava Upload error: %s" % e w.uploadedtostrava = -1 stravaid = -1 w.save() os.remove(tcxfile) return message,stravaid return message,stravaid return message,stravaid