diff --git a/rowers/integrations/__init__.py b/rowers/integrations/__init__.py index 6677bbaa..343df09b 100644 --- a/rowers/integrations/__init__.py +++ b/rowers/integrations/__init__.py @@ -1,3 +1,4 @@ from .c2 import C2Integration from .strava import StravaIntegration from .nk import NKIntegration +from .sporttracks import SportTracksIntegration diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 69774d33..45397cfa 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -56,7 +56,7 @@ def c2wc(weightclass): class C2Integration(SyncIntegration): def __init__(self, *args, **kwargs): - super(C2Integration, self).__init__(self, *args, **kwargs) + super(C2Integration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': C2_CLIENT_ID, 'client_secret': C2_CLIENT_SECRET, diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py index 03fb2a3a..8434ce1e 100644 --- a/rowers/integrations/integrations.py +++ b/rowers/integrations/integrations.py @@ -26,7 +26,7 @@ class SyncIntegration(metaclass=ABCMeta): rower = Rower() def __init__(self, *args, **kwargs): - user = args[1] + user = args[0] self.user = user self.rower = user.rower diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 41bea80f..42b1d17a 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -1,8 +1,6 @@ from .integrations import SyncIntegration, NoTokenError from rowers.models import User, Rower, Workout, TombStone -from rowingdata import rowingdata - from rowers import mytypes from rowers.nkimportutils import * from rowers.tasks import handle_nk_async_workout @@ -31,7 +29,7 @@ queuehigh = django_rq.get_queue('low') class NKIntegration(SyncIntegration): def __init__(self, *args, **kwargs): - super(NKIntegration, self).__init__(self, *args, **kwargs) + super(NKIntegration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': NK_CLIENT_ID, 'client_secret': NK_CLIENT_SECRET, @@ -310,7 +308,3 @@ class NKIntegration(SyncIntegration): return r.nktoken -# just as a quick test during development -u = User.objects.get(id=1) - -nk_integration_1 = NKIntegration(u) diff --git a/rowers/integrations/sporttracks.py b/rowers/integrations/sporttracks.py new file mode 100644 index 00000000..e6dcbccb --- /dev/null +++ b/rowers/integrations/sporttracks.py @@ -0,0 +1,143 @@ +from .integrations import SyncIntegration, NoTokenError +from rowers.models import User, Rower, Workout, TombStone + +from rowers.tasks import handle_sporttracks_sync +from rowers.rower_rules import is_workout_user +import rowers.mytypes as mytypes +from rowsandall_app.settings import ( + SPORTTRACKS_CLIENT_SECRET, SPORTTRACKS_CLIENT_ID, + SPORTTRACKS_REDIRECT_URI +) + +import re +import numpy + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('high') + +from rowers.utils import myqueue, dologging + +class SportTracksIntegration(SyncIntegration): + def __init__(self, *args, **kwargs): + super(SportTracksIntegration, self).__init__(*args, **kwargs) + + self.oauth_data = { + 'client_id': SPORTTRACKS_CLIENT_ID, + 'client_secret': SPORTTRACKS_CLIENT_SECRET, + 'redirect_uri': SPORTTRACKS_REDIRECT_URI, + 'autorization_uri': "https://api.sporttracks.mobi/oauth2/authorize", + 'content_type': 'application/json', + 'tokenname': 'sporttrackstoken', + 'refreshtokenname': 'sporttracksrefreshtoken', + 'expirydatename': 'sporttrackstokenexpirydate', + 'bearer_auth': False, + 'base_url': "https://api.sporttracks.mobi/oauth2/token", + 'scope': 'write', + } + + + def open(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).open(*args, **kwargs) + + def createworkoutdata(self, w, *args, **kwargs): + return None + + def workout_export(self, workout, *args, **kwargs) -> str: + pass + + def get_workouts(self, *args, **kwargs) -> int: + pass + + + + def get_workout(self, id) -> int: + _ = self.open() + + r = self.rower + + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities/" + \ + str(sporttracksid) + s = requests.get(url, headers=headers) + + data = s.json() + + strokedata = pd.DataFrame.from_dict({ + key: pd.Series(value, dtype='object') for key, value in data.items() + }) + + id = myqueue( + queue, + handle_sporttracks_workout_from_data, + self.user, + sporttracksid, data, + strokedata, + 'sporttracks', + 'sporttracks' + ) + return id + + + def get_workout_list(self, *args, **kwargs) -> list: + _ = self.open() + r = self.rower + + authorizationstring = str('Bearer ' + r.sporttrackstoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities" + res = requests.get(url, headers=headers) + + if (res.status_code != 200): + s = "Token doesn't exist. Need to authorize" + raise NoTokenError(s) + + + workouts = [] + + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + for item in res.json()['items']: + d = int(float(item['total_distance'])) + i = int(getidfromuri(item['uri'])) + if i in knownstids: # pragma: no cover + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id', 'distance', 'duration', + 'starttime', 'rowtype', 'source', 'name', 'new'] + values = [i, d, ttot, s, r, None, n, nnn] + res = dict(zip(keys, values)) + workouts.append(res) + + return workouts + + def make_authorization_url(self, *args, **kwargs) -> str: # pragma: no cover + return super(SportTracksIntegration, self).make_authorization_url(*args, **kwargs) + + def get_token(self, code, *args, **kwargs) -> (str, int, str): + return "" + + + + def token_refresh(self, *args, **kwargs) -> str: + return super(SportTracksIntegration, self).token_refresh(*args, **kwargs) + + + +# just as a quick test during development +u = User.objects.get(id=1) + +nk_integration_1 = SportTracksIntegration(u) + diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index 079bab33..efd83ccc 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -79,7 +79,7 @@ def strava_push_delete(id): # pragma: no cover class StravaIntegration(SyncIntegration): def __init__(self, *args, **kwargs): - super(StravaIntegration, self).__init__(self, *args, **kwargs) + super(StravaIntegration, self).__init__(*args, **kwargs) self.oauth_data = { 'client_id': STRAVA_CLIENT_ID, 'client_secret': STRAVA_CLIENT_SECRET, diff --git a/rowers/sporttracksstuff.py b/rowers/sporttracksstuff.py index 4b4f2dd8..e21cae76 100644 --- a/rowers/sporttracksstuff.py +++ b/rowers/sporttracksstuff.py @@ -1,4 +1,4 @@ -from rowers.tasks import handle_sporttracks_sync +from rowers.tasks import handle_sporttracks_sync, handle_sporttracks_workout_from_data from rowers.rower_rules import is_workout_user import rowers.mytypes as mytypes from rowsandall_app.settings import ( @@ -127,12 +127,14 @@ def get_workout(user, sporttracksid, do_async=False): key: pd.Series(value, dtype='object') for key, value in data.items() }) - id, message = add_workout_from_data( + id= myqueue( + queue, + handle_sporttracks_workout_from_data, user, sporttracksid, data, strokedata, - source='sporttracks', - workoutsource='sporttracks') + 'sporttracks', + 'sporttracks') return id @@ -346,165 +348,3 @@ def workout_sporttracks_upload(user, w, asynchron=False): # pragma: no cover # Create workout from SportTracks Data, which are slightly different # than Strava or Concept2 data - -def add_workout_from_data(user, importid, data, strokedata, source='sporttracks', - workoutsource='sporttracks'): - try: - workouttype = data['type'] - except KeyError: # pragma: no cover - workouttype = 'other' - - if workouttype not in [x[0] for x in Workout.workouttypes]: - workouttype = 'other' - try: - comments = data['comments'] - except: - comments = '' - - r = Rower.objects.get(user=user) - try: - rowdatetime = iso8601.parse_date(data['start_time']) - except iso8601.ParseError: # pragma: no cover - 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 = arrow.get(rowdatetime).timestamp() - - try: - title = data['name'] - except: # pragma: no cover - title = "Imported data" - - try: - res = splitstdata(data['distance']) - distance = res[1] - times_distance = res[0] - except KeyError: # pragma: no cover - try: - res = splitstdata(data['heartrate']) - times_distance = res[0] - distance = 0*times_distance - except KeyError: - return (0, "No distance or heart rate data in the workout") - - try: - locs = data['location'] - - res = splitstdata(locs) - times_location = res[0] - latlong = res[1] - latcoord = [] - loncoord = [] - - for coord in latlong: - lat = coord[0] - lon = coord[1] - latcoord.append(lat) - loncoord.append(lon) - except: - times_location = times_distance - latcoord = np.zeros(len(times_distance)) - loncoord = np.zeros(len(times_distance)) - if workouttype in mytypes.otwtypes: # pragma: no cover - workouttype = 'rower' - - try: - res = splitstdata(data['cadence']) - times_spm = res[0] - spm = res[1] - except KeyError: # pragma: no cover - times_spm = times_distance - spm = 0*times_distance - - try: - res = splitstdata(data['heartrate']) - 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 = ewmovingaverage(velo, wsize) - - df[' Stroke500mPace (sec/500m)'] = 500./velo2 - - df = df.fillna(0) - - df.sort_values(by='TimeStamp (sec)', ascending=True) - -# csvfilename ='media/Import_'+str(importid)+'.csv' - csvfilename = 'media/{code}_{importid}.csv'.format( - importid=importid, - code=uuid4().hex[:16] - ) - - 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, - dosmooth=r.dosmooth, - workoutsource='sporttracks') - - return (id, message) diff --git a/rowers/tasks.py b/rowers/tasks.py index 73e8e773..7e52311a 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -447,6 +447,174 @@ def handle_c2_sync(workoutid, url, headers, data, debug=False, **kwargs): return 1 +def splitstdata(lijst): + t = [] + latlong = [] + while len(lijst) >= 2: + t.append(lijst[0]) + latlong.append(lijst[1]) + lijst = lijst[2:] + + return [np.array(t), np.array(latlong)] + +@app.task +def handle_sporttracks_workout_from_data(user, importid, data, strokedata, source, + workoutsource, debug=False, **kwargs): + + try: + workouttype = data['type'] + except KeyError: # pragma: no cover + workouttype = 'other' + + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'other' + try: + comments = data['comments'] + except: + comments = '' + + r = Rower.objects.get(user=user) + rowdatetime = iso8601.parse_date(data['start_time']) + starttimeunix = arrow.get(rowdatetime).timestamp() + + try: + title = data['name'] + except: # pragma: no cover + title = "Imported data" + + try: + res = splitstdata(data['distance']) + distance = res[1] + times_distance = res[0] + except KeyError: # pragma: no cover + try: + res = splitstdata(data['heartrate']) + times_distance = res[0] + distance = 0*times_distance + except KeyError: + return (0, "No distance or heart rate data in the workout") + + try: + locs = data['location'] + + res = splitstdata(locs) + times_location = res[0] + latlong = res[1] + latcoord = [] + loncoord = [] + + for coord in latlong: + lat = coord[0] + lon = coord[1] + latcoord.append(lat) + loncoord.append(lon) + except: + times_location = times_distance + latcoord = np.zeros(len(times_distance)) + loncoord = np.zeros(len(times_distance)) + if workouttype in mytypes.otwtypes: # pragma: no cover + workouttype = 'rower' + + try: + res = splitstdata(data['cadence']) + times_spm = res[0] + spm = res[1] + except KeyError: # pragma: no cover + times_spm = times_distance + spm = 0*times_distance + + try: + res = splitstdata(data['heartrate']) + 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 = ewmovingaverage(velo, wsize) + + df[' Stroke500mPace (sec/500m)'] = 500./velo2 + + df = df.fillna(0) + + df.sort_values(by='TimeStamp (sec)', ascending=True) + + + csvfilename = 'media/{code}_{importid}.csv'.format( + importid=importid, + code=uuid4().hex[:16] + ) + + res = df.to_csv(csvfilename+'.gz', index_label='index', + compression='gzip') + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': user.id, + 'file': csvfilename+'.gz', + 'title': '', + 'workouttype': workouttype, + 'boattype': '1x', + 'sporttracksid': importid, + 'title':title, + } + session = requests.session() + newHeaders = {'Content-type': 'application/json', 'Accept': 'text/plain'} + session.headers.update(newHeaders) + _ = session.post(UPLOAD_SERVICE_URL, json=uploadoptions) + + return 1 + @app.task def handle_sporttracks_sync(workoutid, url, headers, data, debug=False, **kwargs): diff --git a/rowers/uploads.py b/rowers/uploads.py index 7d699303..d0eb510f 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -236,6 +236,10 @@ def do_sync(w, options, quick=False): f.write(str(e)) do_st_export = w.user.sporttracks_auto_export + if options['sporttracksid'] != 0 and options['sporttracksid'] != '': + w.uploadedtost = options['sporttracksid'] + w.save() + do_st_export = False try: # pragma: no cover upload_to_st = options['upload_to_SportTracks'] or do_st_export do_st_export = upload_to_st diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 9bafbd6e..b110322e 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5209,6 +5209,7 @@ def workout_upload_api(request): # sync related IDs + sporttracksid = post_data.get('sporttracksid','') c2id = post_data.get('c2id', '') workoutid = post_data.get('id','') startdatetime = post_data.get('startdatetime', '')