From cbf7c4ab0bc05324bfcdd2317992144d16e133d8 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Sun, 16 May 2021 15:33:03 +0200
Subject: [PATCH] adding simple export to Garmin
---
garminscript.py | 4 +--
rowers/garmin_stuff.py | 25 +++++++++++-----
rowers/models.py | 14 +++++++++
rowers/plannedsessions.py | 2 +-
rowers/templates/plannedsessionview.html | 5 ++++
rowers/templates/rower_exportsettings.html | 5 +++-
rowers/urls.py | 2 ++
rowers/utils.py | 17 ++++++++---
rowers/views/planviews.py | 33 ++++++++++++++++++++++
9 files changed, 92 insertions(+), 15 deletions(-)
diff --git a/garminscript.py b/garminscript.py
index 9cb803af..f29698ec 100644
--- a/garminscript.py
+++ b/garminscript.py
@@ -66,8 +66,8 @@ payload2 = {
"exerciseCategory": None
}]}
-payload = {'workoutName': '4x1000m',
- 'sport': 'GENERIC',
+payload = {'workoutName': '4x1000m row',
+ 'sport': 'CARDIO_TRAINING',
'description': 'Uploaded from Rowsandall.com',
'estimatedDurationInSecs': 2700,
'estimatedDistanceInMeters': 8768,
diff --git a/rowers/garmin_stuff.py b/rowers/garmin_stuff.py
index 1636e48e..9b5bf773 100644
--- a/rowers/garmin_stuff.py
+++ b/rowers/garmin_stuff.py
@@ -11,6 +11,8 @@ from rowers.mytypes import otwtypes
from rowers.rower_rules import is_workout_user,ispromember
from iso8601 import ParseError
+from rowers.plannedsessions import ps_dict_get_description
+
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')
token = oauth_response.get('oauth_token')
access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token'
+ print(token,access_token_url)
# Using OAuth1Session
garmin = OAuth1Session(oauth_data['client_id'],
@@ -191,6 +194,8 @@ def get_garmin_workout_list(user): # pragma: no cover
return result
def garmin_can_export_session(user): # pragma: no cover
+ if user.rower.rowerplan not in ['coach','plan']:
+ return False
result = get_garmin_permissions(user)
if 'WORKOUT_IMPORT' in result:
return True
@@ -254,7 +259,6 @@ def step_to_garmin(step,order=0):
except KeyError:
targetType = None
-
try:
targetValue = step['dict']['targetValue']
if targetValue == 0:
@@ -283,6 +287,13 @@ def step_to_garmin(step,order=0):
if targetValueLow is not None and 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:
targetType = None
@@ -333,8 +344,8 @@ def step_to_garmin(step,order=0):
def ps_to_garmin(ps,r):
payload = {
'workoutName': ps.name,
- 'sport': 'GENERIC',
- 'description':'Uploaded from Rowsandall.com',
+ 'sport':r.garminactivity,
+ 'description':ps_dict_get_description(ps.steps),
'estimatedDurationInSecs':60*ps.approximate_duration,
'estimatedDistanceInMeters': ps.approximate_distance,
'workoutProvider': 'Rowsandall.com',
@@ -390,12 +401,13 @@ def get_garmin_permissions(user): # pragma: no cover
def garmin_session_create(ps,user): # pragma: no cover
if not ps.steps:
- print('aap')
return 0
if not garmin_can_export_session(user):
- print('noot')
return 0
+ if ps.garmin_schedule_id != 0:
+ return ps.garmin_schedule_id
+
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.garmintoken is None):
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)
- print(response.status_code,response.text)
+
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)
- print(response.status_code,response.text)
if response.status_code != 200:
return 0
diff --git a/rowers/models.py b/rowers/models.py
index 7d612fef..d5449391 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -837,6 +837,15 @@ class Rower(models.Model):
('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)
#billing details
@@ -1015,6 +1024,10 @@ class Rower(models.Model):
garminrefreshtoken = models.CharField(default='',max_length=1000,
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)
stravatokenexpirydate = models.DateTimeField(blank=True,null=True)
stravarefreshtoken = models.CharField(default='',max_length=1000,
@@ -3853,6 +3866,7 @@ class RowerExportForm(ModelForm):
model = Rower
fields = [
'stravaexportas',
+ 'garminactivity',
'polar_auto_import',
'c2_auto_export',
'c2_auto_import',
diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py
index 687ac4a4..60e82e56 100644
--- a/rowers/plannedsessions.py
+++ b/rowers/plannedsessions.py
@@ -86,7 +86,7 @@ from rowers.tasks import (
from rowers.utils import totaltime_sec_to_string
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 = ''
for item in sdict:
s += item['string']+'\n'
diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html
index 21d57028..2389ffcc 100644
--- a/rowers/templates/plannedsessionview.html
+++ b/rowers/templates/plannedsessionview.html
@@ -18,6 +18,11 @@
Edit Session
/
Save to Library
+ {% if psdict.garmin_schedule_id.1 %}
+ Exported to Garmin
+ {% else %}
+ Export to Garmin
+ {% endif %}
{% endif %}
Session {{ psdict.name.1 }}
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index 79332c87..e631af88 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -63,7 +63,10 @@
{% endif %}
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.
Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete
diff --git a/rowers/urls.py b/rowers/urls.py
index 0377375f..7f002f21 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -769,6 +769,8 @@ urlpatterns = [
name='plannedsession_templateedit_view'),
re_path(r'^sessions/(?P\d+)/maketemplate/$',views.plannedsession_totemplate_view,
name='plannedsession_totemplate_view'),
+ re_path(r'^sessions/(?P\d+)/togarmin/$',views.plannedsession_togarmin_view,
+ name='plannedsession_togarmin_view'),
re_path(r'^sessions/(?P\d+)/compare/$',
views.plannedsession_compare_view,
name='plannedsession_compare_view'),
diff --git a/rowers/utils.py b/rowers/utils.py
index defdd0af..0c1a35d4 100644
--- a/rowers/utils.py
+++ b/rowers/utils.py
@@ -775,7 +775,7 @@ def ps_dict_order_dict(d,short=False):
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({})
steps = d['steps']
@@ -836,7 +836,10 @@ def ps_dict_order(d,short=False,rower=None):
holduntil.append(item['repeatID'])
multiplier.append(item['repeatValue'])
factor *= item['repeatValue']
- spaces += ' '
+ if html:
+ spaces += ' '
+ else:
+ spaces += ' '
if item['type'] == 'Step':
item['string'] = spaces+item['string']
sdict3.append(item)
@@ -847,7 +850,10 @@ def ps_dict_order(d,short=False,rower=None):
if item['stepID'] == holduntil[-1]:
sdict3.append(hold.pop())
factor /= multiplier.pop()
- spaces = spaces[:-18]
+ if html:
+ spaces = spaces[:-18]
+ else:
+ spaces = spaces[:-3]
holduntil.pop()
else: # pragma: no cover
prevstep = sdict3.pop()
@@ -861,7 +867,10 @@ def ps_dict_order(d,short=False,rower=None):
factor /= multiplier.pop()
sdict3.append(prevstep)
holduntil.pop()
- spaces = spaces[:-18]
+ if html:
+ spaces = spaces[:-18]
+ else:
+ spaces = spaces[:-3]
sdict = list(reversed(sdict3))
diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py
index dc117e6b..e9a75884 100644
--- a/rowers/views/planviews.py
+++ b/rowers/views/planviews.py
@@ -8,6 +8,7 @@ from rowingdata import trainingparser
import json
from taggit.models import Tag
+import rowers.garmin_stuff as gs
@login_required
@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)
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
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)
+
return render(request,'plannedsessionview.html',
{
'psdict': psdict,