Private
Public Access
1
0
Files
rowsandall/rowers/integrations/intervals.py
2024-12-28 15:35:57 +01:00

704 lines
23 KiB
Python

from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids
from rowers.models import Rower, User, Workout, TombStone, PlannedSession
from rowingdata import rowingdata
from rowingdata import FITParser as FP
from rowingdata.otherparsers import FitSummaryData
from rowers.rower_rules import user_is_not_basic, user_is_coachee
from rowers import mytypes
import shutil
from rowers.rower_rules import is_workout_user, ispromember
from rowers.utils import myqueue, dologging, custom_exception_handler
from rowers.tasks import handle_intervals_getworkout
import urllib
import gzip
import requests
import arrow
import datetime
import os
from uuid import uuid4
from django.utils import timezone
from datetime import timedelta
import rowers.dataprep as dataprep
from rowers.opaque import encoder
from rowsandall_app.settings import (
INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL,
UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL
)
import django_rq
queue = django_rq.get_queue('default', default_timeout=3600)
queuelow = django_rq.get_queue('low', default_timeout=3600)
queuehigh = django_rq.get_queue('high', default_timeout=3600)
def seconds_to_duration(seconds):
hours = seconds // 3600
minutes = (seconds % 3600) // 60
remaining_seconds = seconds % 60
# Format as "H:MM:SS" or "MM:SS" if no hours
if hours > 0:
return f"{int(hours)}:{int(minutes):02}:{int(remaining_seconds):02}"
else:
return f"{int(minutes)}:{int(remaining_seconds):02}"
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
intervals_token_url = 'https://intervals.icu/api/oauth/token'
webhookverification = 'JA9Vt6RNH10'
class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
super(IntervalsIntegration, self).__init__(*args, **kwargs)
self.oauth_data = {
'client_id': INTERVALS_CLIENT_ID,
'client_secret': INTERVALS_CLIENT_SECRET,
'redirect_uri': INTERVALS_REDIRECT_URI,
'authorization_uri': intervals_authorize_url,
'content_type': 'application/json',
'tokenname': 'intervals_token',
'expirydatename': 'intervals_exp',
'refreshtokenname': 'intervals_r',
'bearer_auth': True,
'base_url': 'https://intervals.icu/api/v1/',
'grant_type': 'refresh_token',
'headers': headers,
'scope': 'ACTIVITY:WRITE, LIBRARY:READ, CALENDAR:WRITE',
}
def get_token(self, code, *args, **kwargs):
post_data = {
'client_id': str(self.oauth_data['client_id']),
'client_secret': self.oauth_data['client_secret'],
'code': code,
}
response = requests.post(
intervals_token_url,
data=post_data,
)
if response.status_code not in [200, 201]:
dologging('intervals.icu.log',response.text)
return [0,"Failed to get token. ",0]
token_json = response.json()
access_token = token_json['access_token']
athlete = token_json['athlete']
return [access_token, athlete, '']
def get_name(self):
return 'Intervals'
def get_shortname(self):
return 'intervals'
def open(self, *args, **kwargs):
# dologging('intervals.icu.log', "Getting token for user {id}".format(id=self.rower.id))
token = super(IntervalsIntegration, self).open(*args, **kwargs)
return token
def createworkoutdata(self, w, *args, **kwargs) -> str:
dozip = kwargs.get('dozip', True)
# resample if wanted by user, not tested
if w.user.intervals_resample_to_1s:
datadf, id, msgs = dataprep.resample(
w.id, w.user, w, overwrite=False
)
w_resampled = Workout.objects.get(id=id)
filename = w_resampled.csvfilename
else:
w_resampled = None
filename = w.csvfilename
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
data = dataprep.read_df_sql(w.id)
try:
datalength = len(data)
except AttributeError:
datalength = 0
if datalength == 0:
data.rename(columns=columndict, inplace=True)
_ = data.to_csv(w.csvfilename+'.gz', index_label='index', compression='gzip')
try:
row = rowingdata(csvfile=filename)
except IOError: # pragma: no cover
return '' # pragma: no cover
else:
return ''
tcxfilename = w.csvfilename[:-4] + '.tcx'
try:
newnotes = w.notes + '\n from'+w.workoutsource+' via rowsandall.com'
except TypeError:
newnotes = 'from'+w.workoutsource+' via rowsandall.com'
if w.user.intervals_resample_to_1s and w_resampled:
w_resampled.delete()
row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype])
if dozip:
gzfilename = tcxfilename + '.gz'
try:
with open(tcxfilename, 'rb') as inF:
s = inF.read()
with gzip.GzipFile(gzfilename, 'wb') as outF:
outF.write(s)
try:
os.remove(tcxfilename)
except WindowsError: # pragma: no cover
pass
except FileNotFoundError:
return ''
return gzfilename
return tcxfilename
def workout_export(self, workout, *args, **kwargs) -> str:
token = self.open()
dologging('intervals.icu.log', "Exporting workout {id}".format(id=workout.id))
filename = self.createworkoutdata(workout)
if not filename:
return 0
params = {
'name': workout.name,
'description': workout.notes,
'external_id': encoder.encode_hex(workout.id),
}
authorizationstring = str('Bearer ' + token)
# headers with authorization string and content type multipart/form-data
headers = {
'Authorization': authorizationstring,
}
url = "https://intervals.icu/api/v1/athlete/{athleteid}/activities".format(athleteid=0)
with open(filename, 'rb') as f:
files = {'file': f}
response = requests.post(url, params=params, headers=headers, files=files)
if response.status_code not in [200, 201]:
dologging('intervals.icu.log', response.reason)
return 0
id = response.json()['id']
# set workout type to workouttype
url = "https://intervals.icu/api/v1/activity/{activityid}".format(activityid=id)
thetype = mytypes.intervalsmapping[workout.workouttype]
response = requests.put(url, headers=headers, json={'type': thetype})
if response.status_code not in [200, 201]:
return 0
workout.uploadedtointervals = id
workout.save()
os.remove(filename)
dologging('intervals.icu.log', "Exported workout {id}".format(id=workout.id))
return id
def get_workout_list(self, *args, **kwargs) -> int:
url = self.oauth_data['base_url'] + 'athlete/0/activities?'
startdate = timezone.now() - timedelta(days=30)
enddate = timezone.now() + timedelta(days=1)
startdatestring = kwargs.get("startdate","")
enddatestring = kwargs.get("enddate","")
try:
startdate = arrow.get(startdatestring).datetime
except:
pass
try:
enddate = arrow.get(enddatestring).datetime
except:
pass
url += 'oldest=' + startdate.strftime('%Y-%m-%d') + '&newest=' + enddate.strftime('%Y-%m-%d')
headers = {
'accept': '*/*',
'authorization': 'Bearer ' + self.open(),
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return []
data = response.json()
known_interval_ids = get_known_ids(self.rower, 'intervalsid')
workouts = []
for item in data:
try:
i = item['id']
r = item['type']
d = item['distance']
ttot = seconds_to_duration(item['moving_time'])
s = item['start_date']
s2 = ''
c = item['name']
if i in known_interval_ids:
nnn = ''
else:
nnn = 'NEW'
keys = ['id','distance','duration','starttime',
'rowtype','source','name','new']
values = [i, d, ttot, s, r, s2, c, nnn]
ress = dict(zip(keys, values))
workouts.append(ress)
except KeyError:
dologging('intervals.icu.log', item)
return workouts
def update_workout(self, id, *args, **kwargs) -> int:
_ = self.open()
r = self.rower
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
url = self.oauth_data['base_url'] + 'activity/' + str(id)
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
data = response.json()
ws = Workout.objects.filter(uploadedtointervals=id)
for w in ws:
try:
w.name = data['name']
except KeyError:
pass
try:
w.notes = data['description']
except KeyError:
pass
try:
w.workouttype = mytypes.intervalsmappinginv[data['type']]
except KeyError:
pass
w.save()
# we stop here now
return 1
# get fit file (not used)
url = self.oauth_data['base_url'] + 'activity/' + str(id) + '/fit-file'
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
try:
fit_data = response.content
fit_filename = 'media/intervals_' + str(id) + '.fit'
with open(fit_filename, 'wb') as f:
f.write(fit_data)
except:
return 0
try:
row = FP(fit_filename)
rowdata = rowingdata(df=row.df)
rowsummary = FitSummaryData(fit_filename)
except Exception as e:
dologging('intervals.icu.log', e)
return 0
for w in ws:
# copy fit_file to random file name using shutil
temp_filename = 'media/' + str(uuid4()) + '.fit'
try:
shutil.copy(fit_filename, temp_filename)
uploadoptions = {
'secret': UPLOAD_SERVICE_SECRET,
'user': self.rower.user.id,
'boattype': '1x',
'workouttype': w.workouttype,
'file': temp_filename,
'intervalsid': id,
'id': w.id,
}
url = UPLOAD_SERVICE_URL
response = requests.post(url, data=uploadoptions)
except FileNotFoundError:
return 0
except Exception as e:
dologging('intervals.icu.log', e)
# remove fit_file
try:
os.remove(fit_filename)
except:
pass
return 1
def get_workout(self, id, *args, **kwargs) -> int:
_ = self.open()
r = self.rower
# check if workout with this id already exists
known_interval_ids = get_known_ids(r, 'intervalsid')
if id in known_interval_ids:
return self.update_workout(id)
record = create_or_update_syncrecord(r, None, intervalsid=id)
_ = myqueue(queuehigh,
handle_intervals_getworkout,
self.rower,
self.rower.intervals_token,
id)
return 1
def get_workouts(self, *args, **kwargs):
startdate = timezone.now() - timedelta(days=7)
enddate = timezone.now() + timedelta(days=1)
startdatestring = kwargs.get(startdate,"")
enddatestring = kwargs.get(enddate,"")
try:
startdate = arrow.get(startdatestring).datetime
except:
pass
try:
enddate = arrow.get(enddatestring).datetime
except:
pass
count = 0
workouts = self.get_workout_list(startdate=startdate, enddate=enddate)
for workout in workouts:
if workout['new'] == 'NEW':
self.get_workout(workout['id'])
count +=1
return count
def make_authorization_url(self, *args, **kwargs):
return super(IntervalsIntegration, self).make_authorization_url(*args, **kwargs)
def token_refresh(self, *args, **kwargs):
return super(IntervalsIntegration, self).token_refresh(*args, **kwargs)
def get_plannedsessions_list(self, *args, **kwargs):
_ = self.open()
r = self.rower
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
# first get the folders - we need the folder id for the next call
oldest = (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d')
newest = (timezone.now() + timedelta(days=30)).strftime('%Y-%m-%d')
url = self.oauth_data['base_url'] + 'athlete/0/events' #'?category=WORKOUT'
url += '?oldest=' + oldest + '&newest=' + newest
response = requests.get(url, headers=headers)
if response.status_code != 200:
return []
data = response.json()
return data
def update_plannedsession(self, ps, data, *args, **kwargs):
_ = self.open()
r = self.rower
if data['category'] == 'WORKOUT':
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + '/downloadfit'
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
else:
filename = 'planned_' + str(ps.intervals_icu_id) + '.fit'
filename2 = 'media/planned_' + str(ps.intervals_icu_id) + '.fit'
with open(filename2, 'wb') as f:
f.write(response.content)
data['fitfile'] = filename
return data
def get_plannedsession(self, id, *args, **kwargs):
_ = self.open()
r = self.rower
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(id)
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
data = response.json()
# get file from athlete/0/events/{id}/downloadfit
if data['category'] == 'WORKOUT':
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(id) + '/downloadfit'
response = requests.get(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
filename = 'planned_' + str(id) + '.fit'
filename2 = 'media/planned_' + str(id) + '.fit'
with open(filename2, 'wb') as f:
f.write(response.content)
data['fitfile'] = filename
return data
def plannedsession_create(self, ps, *args, **kwargs):
_ = self.open()
r = self.rower
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
stepstext = ps.steps_intervals()
category = 'WORKOUT'
startdate = ps.preferreddate.strftime('%Y-%m-%dT%H:%M:%S')
enddate = ps.preferreddate.strftime('%Y-%m-%d') + 'T23:59:59'
if ps.sessiontype == 'cycletarget':
category = 'TARGET'
startdate = ps.startdate.strftime('%Y-%m-%dT%H:%M:%S')
enddate = ps.enddate.strftime('%Y-%m-%d') + 'T23:59:59'
data = {
"start_date_local": startdate,
"type": mytypes.intervalsmapping[ps.sessionsport],
"category": category,
"end_date_local": enddate,
"name": ps.name,
"description": stepstext,
"indoor": ps.sessionsport in mytypes.ergtypes,
'external_id': ps.id,
}
if ps.sessiontype == 'cycletarget':
if ps.sessionmode == 'time':
data['time_target'] = ps.sessionvalue*60
elif ps.sessionmode == 'distance':
data['distance_target'] = ps.sessionvalue
elif ps.sessionmode == 'rScore':
data['load_target'] = ps.sessionvalue
elif ps.sessionmode == 'Trimp':
data['load_target'] = ps.sessionvalue/2.
url = self.oauth_data['base_url'] + 'athlete/0/events'
response = requests.post(url, headers=headers, json=data)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
data = response.json()
id = data['id']
ps.intervals_icu_id = id
ps.save()
return id
def plannedsession_delete(self, ps, *args, **kwargs):
_ = self.open()
r = self.rower
headers = {
'Authorization': 'Bearer ' + r.intervals_token,
}
url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id)
response = requests.delete(url, headers=headers)
if response.status_code != 200:
dologging('intervals.icu.log', response.text)
return 0
ps.intervals_icu_id = None
ps.save()
return 1
def update_calendar(self, r, event, *args, **kwargs):
try:
records = event["events"]
except KeyError:
records = []
for record in records:
id = record['id']
data = {}
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if pss.count() > 0:
ps = pss[0]
data = self.update_plannedsession(ps, record)
else:
data = self.get_plannedsession(id)
ps = PlannedSession(
manager=r.user,
intervals_icu_id=id,
)
ps.save()
ps.rower.add(r)
except PlannedSession.DoesNotExist:
continue
# got data
if data:
ps.name = data['name']
try:
ps.comment = data['description']
except KeyError:
pass
ps.startdate = arrow.get(data['start_date_local']).datetime
ps.enddate = arrow.get(data['end_date_local']).datetime
ps.preferreddate = arrow.get(data['start_date_local']).datetime
try:
ps.sessionsport = mytypes.intervalsmappinginv[data['type']]
except KeyError:
pass
ps.sessiontype = 'session'
ps.save()
try:
timetarget = data['time_target']
except KeyError:
timetarget = None
if timetarget is None:
try:
timetarget = data['moving_time']
except KeyError:
timetarget = None
if timetarget is None:
timetarget = 3600
timetarget = int(timetarget)/60.
ps.sessionvalue = timetarget
ps.save()
if data['category'].lower() == 'workout':
ps.fitfile = data['fitfile']
ps.save()
ps.update_steps()
if data['category'].lower() == 'target':
ps.sessiontype = 'cycletarget'
ps.sessionvalue = int(data['time_target'])/60.
ps.enddate = ps.startdate + datetime.timedelta(days=6)
ps.save()
try:
deleted_records = event["deleted_events"]
except KeyError:
deleted_records = []
for record in deleted_records:
id = record['id']
try:
pss = PlannedSession.objects.filter(intervals_icu_id=id)
if r.intervals_delete_plannedsession and pss.count() > 0:
for ps in pss:
ps.delete()
except PlannedSession.DoesNotExist:
continue
return 1
def import_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_import:
return 0
if user_is_not_basic(self.rower.user) or user_is_coachee(self.rower.user):
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
result = self.get_workout(id)
except KeyError:
pass
return 1
return 0
def delete_activities(self, event, *args, **kwargs):
if not self.rower.intervals_auto_delete:
return 0
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
try:
ws = Workout.objects.filter(uploadedtointervals=id)
for w in ws:
if w.user == self.rower:
w.delete()
except Workout.DoesNotExist:
pass
except KeyError:
pass
return 1
def update_activities(self, event, *args, **kwargs):
try:
record = event["activity"]
except KeyError:
records = []
try:
id = record['id']
result = self.update_workout(id)
except KeyError:
pass
return 1