adding simple export to Garmin
This commit is contained in:
@@ -66,8 +66,8 @@ payload2 = {
|
|||||||
"exerciseCategory": None
|
"exerciseCategory": None
|
||||||
}]}
|
}]}
|
||||||
|
|
||||||
payload = {'workoutName': '4x1000m',
|
payload = {'workoutName': '4x1000m row',
|
||||||
'sport': 'GENERIC',
|
'sport': 'CARDIO_TRAINING',
|
||||||
'description': 'Uploaded from Rowsandall.com',
|
'description': 'Uploaded from Rowsandall.com',
|
||||||
'estimatedDurationInSecs': 2700,
|
'estimatedDurationInSecs': 2700,
|
||||||
'estimatedDistanceInMeters': 8768,
|
'estimatedDistanceInMeters': 8768,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ from rowers.mytypes import otwtypes
|
|||||||
from rowers.rower_rules import is_workout_user,ispromember
|
from rowers.rower_rules import is_workout_user,ispromember
|
||||||
from iso8601 import ParseError
|
from iso8601 import ParseError
|
||||||
|
|
||||||
|
from rowers.plannedsessions import ps_dict_get_description
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
@@ -131,6 +133,7 @@ def garmin_processcallback(redirect_response,resource_owner_key,resource_owner_s
|
|||||||
verifier = oauth_response.get('oauth_verifier')
|
verifier = oauth_response.get('oauth_verifier')
|
||||||
token = oauth_response.get('oauth_token')
|
token = oauth_response.get('oauth_token')
|
||||||
access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token'
|
access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token'
|
||||||
|
print(token,access_token_url)
|
||||||
|
|
||||||
# Using OAuth1Session
|
# Using OAuth1Session
|
||||||
garmin = OAuth1Session(oauth_data['client_id'],
|
garmin = OAuth1Session(oauth_data['client_id'],
|
||||||
@@ -191,6 +194,8 @@ def get_garmin_workout_list(user): # pragma: no cover
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def garmin_can_export_session(user): # pragma: no cover
|
def garmin_can_export_session(user): # pragma: no cover
|
||||||
|
if user.rower.rowerplan not in ['coach','plan']:
|
||||||
|
return False
|
||||||
result = get_garmin_permissions(user)
|
result = get_garmin_permissions(user)
|
||||||
if 'WORKOUT_IMPORT' in result:
|
if 'WORKOUT_IMPORT' in result:
|
||||||
return True
|
return True
|
||||||
@@ -254,7 +259,6 @@ def step_to_garmin(step,order=0):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
targetType = None
|
targetType = None
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
targetValue = step['dict']['targetValue']
|
targetValue = step['dict']['targetValue']
|
||||||
if targetValue == 0:
|
if targetValue == 0:
|
||||||
@@ -283,6 +287,13 @@ def step_to_garmin(step,order=0):
|
|||||||
if targetValueLow is not None and targetValueLow > 1000:
|
if targetValueLow is not None and targetValueLow > 1000:
|
||||||
targetValueLow -= 1000
|
targetValueLow -= 1000
|
||||||
|
|
||||||
|
# temp - throw away target other than power
|
||||||
|
if targetType.lower() != 'power':
|
||||||
|
targetType = None
|
||||||
|
targetValue = None
|
||||||
|
targetValueLow = None
|
||||||
|
targetValueHigh = None
|
||||||
|
|
||||||
if targetValue is None and targetValueLow is None and targetValueHigh is None:
|
if targetValue is None and targetValueLow is None and targetValueHigh is None:
|
||||||
targetType = None
|
targetType = None
|
||||||
|
|
||||||
@@ -333,8 +344,8 @@ def step_to_garmin(step,order=0):
|
|||||||
def ps_to_garmin(ps,r):
|
def ps_to_garmin(ps,r):
|
||||||
payload = {
|
payload = {
|
||||||
'workoutName': ps.name,
|
'workoutName': ps.name,
|
||||||
'sport': 'GENERIC',
|
'sport':r.garminactivity,
|
||||||
'description':'Uploaded from Rowsandall.com',
|
'description':ps_dict_get_description(ps.steps),
|
||||||
'estimatedDurationInSecs':60*ps.approximate_duration,
|
'estimatedDurationInSecs':60*ps.approximate_duration,
|
||||||
'estimatedDistanceInMeters': ps.approximate_distance,
|
'estimatedDistanceInMeters': ps.approximate_distance,
|
||||||
'workoutProvider': 'Rowsandall.com',
|
'workoutProvider': 'Rowsandall.com',
|
||||||
@@ -390,12 +401,13 @@ def get_garmin_permissions(user): # pragma: no cover
|
|||||||
|
|
||||||
def garmin_session_create(ps,user): # pragma: no cover
|
def garmin_session_create(ps,user): # pragma: no cover
|
||||||
if not ps.steps:
|
if not ps.steps:
|
||||||
print('aap')
|
|
||||||
return 0
|
return 0
|
||||||
if not garmin_can_export_session(user):
|
if not garmin_can_export_session(user):
|
||||||
print('noot')
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
if ps.garmin_schedule_id != 0:
|
||||||
|
return ps.garmin_schedule_id
|
||||||
|
|
||||||
r = Rower.objects.get(user=user)
|
r = Rower.objects.get(user=user)
|
||||||
if (r.garmintoken == '') or (r.garmintoken is None):
|
if (r.garmintoken == '') or (r.garmintoken is None):
|
||||||
s = "Token doesn't exist. Need to authorize"
|
s = "Token doesn't exist. Need to authorize"
|
||||||
@@ -414,7 +426,7 @@ def garmin_session_create(ps,user): # pragma: no cover
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = requests.post(url,auth=garminheaders,json=payload)
|
response = requests.post(url,auth=garminheaders,json=payload)
|
||||||
print(response.status_code,response.text)
|
|
||||||
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@@ -432,7 +444,6 @@ def garmin_session_create(ps,user): # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
response = requests.post(url,auth=garminheaders,json=payload)
|
response = requests.post(url,auth=garminheaders,json=payload)
|
||||||
print(response.status_code,response.text)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -837,6 +837,15 @@ class Rower(models.Model):
|
|||||||
('None','None'),
|
('None','None'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
garminsports = (
|
||||||
|
('GENERIC','Custom'),
|
||||||
|
('RUNNING','Running'),
|
||||||
|
('CYCLING','Cycling'),
|
||||||
|
('LAP_SWIMMING','Lap Swimming'),
|
||||||
|
('STRENGTH_TRAINING','Strength Training'),
|
||||||
|
('CARDIO_TRAINING','Cardio Training'),
|
||||||
|
)
|
||||||
|
|
||||||
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
||||||
|
|
||||||
#billing details
|
#billing details
|
||||||
@@ -1015,6 +1024,10 @@ class Rower(models.Model):
|
|||||||
garminrefreshtoken = models.CharField(default='',max_length=1000,
|
garminrefreshtoken = models.CharField(default='',max_length=1000,
|
||||||
blank=True,null=True)
|
blank=True,null=True)
|
||||||
|
|
||||||
|
garminactivity = models.CharField(default='RUNNING',max_length=200,
|
||||||
|
verbose_name='Garmin Activity for Structured Workouts',
|
||||||
|
choices=garminsports)
|
||||||
|
|
||||||
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
||||||
stravatokenexpirydate = models.DateTimeField(blank=True,null=True)
|
stravatokenexpirydate = models.DateTimeField(blank=True,null=True)
|
||||||
stravarefreshtoken = models.CharField(default='',max_length=1000,
|
stravarefreshtoken = models.CharField(default='',max_length=1000,
|
||||||
@@ -3853,6 +3866,7 @@ class RowerExportForm(ModelForm):
|
|||||||
model = Rower
|
model = Rower
|
||||||
fields = [
|
fields = [
|
||||||
'stravaexportas',
|
'stravaexportas',
|
||||||
|
'garminactivity',
|
||||||
'polar_auto_import',
|
'polar_auto_import',
|
||||||
'c2_auto_export',
|
'c2_auto_export',
|
||||||
'c2_auto_import',
|
'c2_auto_import',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ from rowers.tasks import (
|
|||||||
from rowers.utils import totaltime_sec_to_string
|
from rowers.utils import totaltime_sec_to_string
|
||||||
|
|
||||||
def ps_dict_get_description(d,short=False): # pragma: no cover
|
def ps_dict_get_description(d,short=False): # pragma: no cover
|
||||||
sdict,totalmeters,totalseconds,totalrscore = ps_dict_order(d,short=short)
|
sdict,totalmeters,totalseconds,totalrscore = ps_dict_order(d,short=short,html=False)
|
||||||
s = ''
|
s = ''
|
||||||
for item in sdict:
|
for item in sdict:
|
||||||
s += item['string']+'\n'
|
s += item['string']+'\n'
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
<i class="fas fa-pencil-alt fa-fw"></i> Edit Session</a>
|
<i class="fas fa-pencil-alt fa-fw"></i> Edit Session</a>
|
||||||
/
|
/
|
||||||
<a href="/rowers/sessions/{{ psdict.id.1 }}/maketemplate/?next={{ request.path|urlencode }}"><i class="fas fa-books fa-fw"></i> Save to Library</a>
|
<a href="/rowers/sessions/{{ psdict.id.1 }}/maketemplate/?next={{ request.path|urlencode }}"><i class="fas fa-books fa-fw"></i> Save to Library</a>
|
||||||
|
{% if psdict.garmin_schedule_id.1 %}
|
||||||
|
<i class="fas fa-watch-fitness fa-fw"></i>Exported to Garmin
|
||||||
|
{% else %}
|
||||||
|
<a href="/rowers/sessions/{{ psdict.id.1 }}/togarmin/?next={{ request.path|urlencode }}"><i class="fas fa-watch-fitness fa-fw"></i> Export to Garmin</a>
|
||||||
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1>Session {{ psdict.name.1 }}</h1>
|
<h1>Session {{ psdict.name.1 }}</h1>
|
||||||
|
|||||||
@@ -63,7 +63,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
|
Garmin Connnect has no manual sync, so connecting your account to your Garmin account will
|
||||||
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction).
|
automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you
|
||||||
|
want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity"
|
||||||
|
to a activity type that is supported by your watch. Not all watches support "Custom" activities, so
|
||||||
|
you may have to set your activity to Run or Ride while rowing.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
|
||||||
|
|||||||
@@ -769,6 +769,8 @@ urlpatterns = [
|
|||||||
name='plannedsession_templateedit_view'),
|
name='plannedsession_templateedit_view'),
|
||||||
re_path(r'^sessions/(?P<id>\d+)/maketemplate/$',views.plannedsession_totemplate_view,
|
re_path(r'^sessions/(?P<id>\d+)/maketemplate/$',views.plannedsession_totemplate_view,
|
||||||
name='plannedsession_totemplate_view'),
|
name='plannedsession_totemplate_view'),
|
||||||
|
re_path(r'^sessions/(?P<id>\d+)/togarmin/$',views.plannedsession_togarmin_view,
|
||||||
|
name='plannedsession_togarmin_view'),
|
||||||
re_path(r'^sessions/(?P<id>\d+)/compare/$',
|
re_path(r'^sessions/(?P<id>\d+)/compare/$',
|
||||||
views.plannedsession_compare_view,
|
views.plannedsession_compare_view,
|
||||||
name='plannedsession_compare_view'),
|
name='plannedsession_compare_view'),
|
||||||
|
|||||||
@@ -775,7 +775,7 @@ def ps_dict_order_dict(d,short=False):
|
|||||||
|
|
||||||
return sdict2
|
return sdict2
|
||||||
|
|
||||||
def ps_dict_order(d,short=False,rower=None):
|
def ps_dict_order(d,short=False,rower=None,html=True):
|
||||||
sdict = collections.OrderedDict({})
|
sdict = collections.OrderedDict({})
|
||||||
steps = d['steps']
|
steps = d['steps']
|
||||||
|
|
||||||
@@ -836,7 +836,10 @@ def ps_dict_order(d,short=False,rower=None):
|
|||||||
holduntil.append(item['repeatID'])
|
holduntil.append(item['repeatID'])
|
||||||
multiplier.append(item['repeatValue'])
|
multiplier.append(item['repeatValue'])
|
||||||
factor *= item['repeatValue']
|
factor *= item['repeatValue']
|
||||||
|
if html:
|
||||||
spaces += ' '
|
spaces += ' '
|
||||||
|
else:
|
||||||
|
spaces += ' '
|
||||||
if item['type'] == 'Step':
|
if item['type'] == 'Step':
|
||||||
item['string'] = spaces+item['string']
|
item['string'] = spaces+item['string']
|
||||||
sdict3.append(item)
|
sdict3.append(item)
|
||||||
@@ -847,7 +850,10 @@ def ps_dict_order(d,short=False,rower=None):
|
|||||||
if item['stepID'] == holduntil[-1]:
|
if item['stepID'] == holduntil[-1]:
|
||||||
sdict3.append(hold.pop())
|
sdict3.append(hold.pop())
|
||||||
factor /= multiplier.pop()
|
factor /= multiplier.pop()
|
||||||
|
if html:
|
||||||
spaces = spaces[:-18]
|
spaces = spaces[:-18]
|
||||||
|
else:
|
||||||
|
spaces = spaces[:-3]
|
||||||
holduntil.pop()
|
holduntil.pop()
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
prevstep = sdict3.pop()
|
prevstep = sdict3.pop()
|
||||||
@@ -861,7 +867,10 @@ def ps_dict_order(d,short=False,rower=None):
|
|||||||
factor /= multiplier.pop()
|
factor /= multiplier.pop()
|
||||||
sdict3.append(prevstep)
|
sdict3.append(prevstep)
|
||||||
holduntil.pop()
|
holduntil.pop()
|
||||||
|
if html:
|
||||||
spaces = spaces[:-18]
|
spaces = spaces[:-18]
|
||||||
|
else:
|
||||||
|
spaces = spaces[:-3]
|
||||||
|
|
||||||
sdict = list(reversed(sdict3))
|
sdict = list(reversed(sdict3))
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from rowingdata import trainingparser
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
|
import rowers.garmin_stuff as gs
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required('plannedsession.view_session',fn=get_session_by_pk,raise_exception=True)
|
@permission_required('plannedsession.view_session',fn=get_session_by_pk,raise_exception=True)
|
||||||
@@ -1979,6 +1980,37 @@ def plannedsession_templateedit_view(request,id=0):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True)
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
|
||||||
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def plannedsession_togarmin_view(request,id=0):
|
||||||
|
|
||||||
|
r = getrequestplanrower(request)
|
||||||
|
|
||||||
|
startdate, enddate = get_dates_timeperiod(request)
|
||||||
|
|
||||||
|
ps = get_object_or_404(PlannedSession,pk=id)
|
||||||
|
|
||||||
|
result = gs.garmin_session_create(ps,r.user)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
messages.error(request,'You failed to export your session to Garmin Connect')
|
||||||
|
else:
|
||||||
|
messages.info(request,'Session is now on Garmin Connect. Sync your Garmin watch')
|
||||||
|
|
||||||
|
url = reverse(plannedsession_view,kwargs={'userid':r.user.id,
|
||||||
|
'id':ps.id,})
|
||||||
|
|
||||||
|
startdatestring = startdate.strftime('%Y-%m-%d')
|
||||||
|
enddatestring = enddate.strftime('%Y-%m-%d')
|
||||||
|
url += '?when='+startdatestring+'/'+enddatestring
|
||||||
|
|
||||||
|
next = request.GET.get('next', url)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(next)
|
||||||
|
|
||||||
|
|
||||||
@permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True)
|
@permission_required('plannedsession.change_session',fn=get_session_by_pk,raise_exception=True)
|
||||||
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
|
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
|
||||||
message="This functionality requires a Coach or Self-Coach plan",
|
message="This functionality requires a Coach or Self-Coach plan",
|
||||||
@@ -2385,6 +2417,7 @@ def plannedsession_view(request,id=0,userid=0):
|
|||||||
steps = ps_dict_get_description_html(d,short=False)
|
steps = ps_dict_get_description_html(d,short=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return render(request,'plannedsessionview.html',
|
return render(request,'plannedsessionview.html',
|
||||||
{
|
{
|
||||||
'psdict': psdict,
|
'psdict': psdict,
|
||||||
|
|||||||
Reference in New Issue
Block a user