Private
Public Access
1
0
Files
rowsandall/rowers/garmin_stuff.py
Sander Roosendaal 2c1e6c5909 bug fixes
2021-09-14 19:46:59 +02:00

599 lines
19 KiB
Python

from rowers.imports import *
import datetime
import requests
from requests import Session, Request
from requests_oauthlib import OAuth1,OAuth1Session
from requests_oauthlib.oauth1_session import TokenRequestDenied
from requests import Request, Session
import rowers.mytypes as mytypes
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
import numpy
import json
from json.decoder import JSONDecodeError
from uuid import uuid4
import logging
from rowsandall_app.settings import (
GARMIN_CLIENT_KEY, GARMIN_REDIRECT_URI, GARMIN_CLIENT_SECRET
)
from pytz import timezone as tz, utc
# You must initialize logging, otherwise you'll not see debug output.
#logging.basicConfig()
#logging.getLogger().setLevel(logging.DEBUG)
#requests_log = logging.getLogger("requests.packages.urllib3")
#requests_log.setLevel(logging.DEBUG)
#requests_log.propagate = True
from rowers.tasks import handle_get_garmin_file
import django_rq
queue = django_rq.get_queue('default')
queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low')
from rowers.utils import myqueue
from rowers.models import C2WorldClassAgePerformance,Rower,Workout,TombStone
from django.core.exceptions import PermissionDenied
from rowers.utils import custom_exception_handler,NoTokenError
from rowingdata import rowingdata
oauth_data = {
'client_id': GARMIN_CLIENT_KEY,
'client_secret': GARMIN_CLIENT_SECRET,
'redirect_uri': GARMIN_REDIRECT_URI,
'authorization_uri': "https://connectapi.garmin.com/oauth-service/oauth/request_token",
'content_type': 'application/x-www-form-urlencoded',
'tokenname': 'garmintoken',
'refreshtokenname': 'garminrefreshtoken',
'expirydatename': 'garmintokenexpirydate',
'bearer_auth': True,
'base_url': "https://connect.garmin.com/oauthConfirm",
'scope':'write',
'headers': 'Authorization: OAuth oauth_version="1.0"'
}
columns = {
'startTimeInSeconds':'TimeStamp (sec)',
'latitudeInDegree':' latitude',
'longitudeInDegree':' longitude',
'heartRate':' HRCur (bpm)',
'speedMetersPerSecond':' AverageBoatSpeed (m/s)',
'totalDistanceInMeters':' Horizontal (meters)',
'clockDurationInSeconds':' ElapsedTime (sec)',
'powerInWatts':' Power (watts)',
'bikeCadenceInRPM':' Cadence (stokes/min)',
}
targettypes = {
"Speed": "SPEED",
"HeartRate": "HEART_RATE",
"Open": "OPEN",
"Cadence": "CADENCE",
"Power": "POWER",
"Grade": "GRADE",
"Resistance": "RESISTANCE",
"Power3s": "POWER_3S",
"Power10s": "POWER_10S",
"Power30s": "POWER_30S",
"PowerLap": "POWER_LAP",
"SwimStroke": "SWIM_STROKE",
"SpeedLap": "SPEED_LAP",
"HeartRateLap": "HEART_RATE_LAP"
}
repeattypes = {
"RepeatUntilStepsCmplt": "REPEAT_UNTIL_STEPS_CMPLT",
"RepeatUntilTime": "REPEAT_UNTIL_TIME",
"RepeatUntilDistance": "REPEAT_UNTIL_TIME",
"RepeatUntilCalories": "REPEAT_UNTIL_CALORIES" ,
"RepeatUntilHrLessThan": "REPEAT_UNTIL_HR_LESS_THAN" ,
"RepeatUntilHrGreaterThan": "REPEAT_UNTIL_HR_GREATER_THAN",
"RepeatUntilPowerLessThan": "REPEAT_UNTIL_POWER_LESS_THAN",
"RepeatUntilPowerGreaterThan": "REPEAT_UNTIL_POWER_GREATER_THAN",
"RepeatUntilPowerLapLessThan": "REPEAT_UNTIL_POWER_LAP_LESS_THAN",
"RepeatUntilPowerLapGreaterThan": "REPEAT_UNTIL_POWER_LAP_GREATER_THAN",
}
def garmin_authorize(): # pragma: no cover
redirect_uri = oauth_data['redirect_uri']
client_secret = oauth_data['client_secret']
client_id = oauth_data['client_id']
base_uri = oauth_data['base_url']
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
)
fetch_response = garmin.fetch_request_token(oauth_data['authorization_uri'])
resource_owner_key = fetch_response.get('oauth_token')
resource_owner_secret = fetch_response.get('oauth_token_secret')
authorization_url = garmin.authorization_url(base_uri)
return authorization_url,resource_owner_key,resource_owner_secret
def garmin_processcallback(redirect_response,resource_owner_key,resource_owner_secret): # pragma: no cover
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
)
oauth_response = garmin.parse_authorization_response(redirect_response)
verifier = oauth_response.get('oauth_verifier')
token = oauth_response.get('oauth_token')
access_token_url = 'https://connectapi.garmin.com/oauth-service/oauth/access_token'
# Using OAuth1Session
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
resource_owner_key=resource_owner_key,
resource_owner_secret=resource_owner_secret,
verifier=verifier,)
try:
oauth_tokens = garmin.fetch_access_token(access_token_url)
garmintoken = oauth_tokens.get('oauth_token')
garminrefreshtoken = oauth_tokens.get('oauth_token_secret')
except TokenRequestDenied:
garmintoken = ''
garminrefreshtoken = ''
return garmintoken,garminrefreshtoken
def garmin_open(user): # pragma: no cover
r = Rower.objects.get(user=user)
token = Rower.garmintoken
if (token == '') or (token is None):
raise NoTokenError("User has no garmin token")
return token
def get_garmin_file(r,callbackURL,starttime,fileType): # pragma: no cover
job = myqueue(
queue,
handle_get_garmin_file,
oauth_data['client_id'],
oauth_data['client_secret'],
r.garmintoken,
r.garminrefreshtoken,
r.user.id,
callbackURL,
fileType,
)
return job.id
def get_garmin_workout_list(user): # pragma: no cover
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.garmintoken is None):
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
garmin = OAuth1Session(oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
resource_owner_key=r.garmintoken,
resource_owner_secret=r.garminrefreshtoken,
)
url = 'https://healthapi.garmin.com/wellness-api/rest/activities?uploadStartTimeInSeconds=1593113760&uploadEndTimeInSeconds=1593279360'
result = garmin.get(url)
return result
def garmin_can_export_session(user):
if user.rower.rowerplan not in ['coach','plan']:
return False # pragma: no cover
result = get_garmin_permissions(user)
if 'WORKOUT_IMPORT' in result:
return True
return False # pragma: no cover
from rowers import utils
def step_to_garmin(step,order=0):
durationtype = step['dict']['durationType']
durationvalue = step['dict']['durationValue']
durationvaluetype = None
try:
intensity = step['dict']['intensity'].upper()
if intensity.lower() == 'active':
intensity = 'INTERVAL'
except KeyError:
intensity = None
#durationvaluetype = ''
if durationtype == 'Time':
durationtype = 'TIME'
durationvalue = int(durationvalue/1000.)
elif durationtype == 'Distance':
durationtype = 'DISTANCE'
durationvalue = int(durationvalue/100)
durationvaluetype = 'METER'
elif durationtype == 'HrLessThan': # pragma: no cover
durationtype = 'HR_LESS_THAN'
if durationvalue <= 100:
durationvaluetype = 'PERCENT'
else:
durationvaluetype = None
durationvalue -= 100
elif durationtype == 'HrGreaterThan': # pragma: no cover
durationtype = 'HR_GREATER_THAN'
if durationvalue <= 100:
durationvaluetype = 'PERCENT'
else:
durationvaluetype = None
durationvalue -= 100
elif durationtype == 'PowerLessThan': # pragma: no cover
durationtype = 'POWER_LESS_THAN'
if durationvalue <= 1000:
durationvaluetype = 'PERCENT'
else:
durationvaluetype = None
durationvalue -= 1000
elif durationtype == 'PowerGreaterThan': # pragma: no cover
durationtype = 'POWER_GREATER_THAN'
if durationvalue <= 1000:
durationvaluetype = 'PERCENT'
else:
durationvaluetype = None
durationvalue -= 1000
elif durationtype == 'Reps': # pragma: no cover
durationtype = 'REPS'
try:
targetType = step['dict']['targetType']
targetType = targettypes[targetType]
except KeyError:
targetType = None
try:
targetValue = step['dict']['targetValue']
if targetValue == 0: # pragma: no cover
targetValue = None
except KeyError:
targetValue = None
if targetType is not None and targetType.lower() == "power":
targetType = 'POWER'
if targetValue is not None and targetValue <= 1000:
targetValueType = 'PERCENT' # pragma: no cover
else:
targetValueType = None
targetValue -= 1000
try:
targetValueLow = step['dict']['targetValueLow']
if targetValueLow == 0 and targetValue is not None and targetValue > 0: # pragma: no cover
targetValueLow = targetValue
targetValue = None
elif targetValueLow == 0: # pragma: no cover
targetValueLow = None
elif targetValueLow <= 1000 and targetType == 'POWER': # pragma: no cover
targetValueType = 'PERCENT'
elif targetValueLow > 1000 and targetType == 'POWER': # pragma: no cover
targetValueLow -= 1000
except KeyError:
targetValueLow = None
try:
targetValueHigh = step['dict']['targetValueHigh']
if targetValue is not None and targetValue > 0 and targetValueHigh == 0: # pragma: no cover
targetValueHigh = targetValue
targetValue = 0
elif targetValueHigh <= 1000 and targetType == 'POWER': # pragma: no cover
targetValueType = 'PERCENT'
elif targetValueHigh > 1000 and targetType == 'POWER': # pragma: no cover
targetValueHigh -= 1000
elif targetValueHigh == 0: # pragma: no cover
targetValueHigh = None
except KeyError:
targetValueHigh = None
if targetValue is None and targetValueLow is None and targetValueHigh is None:
targetType = None
steptype = 'WorkoutRepeatStep'
if step['type'].lower() == 'step':
steptype = 'WorkoutStep'
repeattype = None
if steptype == 'WorkoutRepeatStep':
repeattype = repeattypes[step['dict']['durationType']]
durationtype = 'REPS'
durationvalue = None
durationvaluetype = None
targetType = None
targetValue = None
out = {
'type': steptype,
'stepOrder':order,
'repeatType':repeattype,
'repeatValue':step['repeatValue'],
'intensity':intensity,
'description':step['dict']['wkt_step_name'],
'durationType':durationtype,
'durationValue':durationvalue,
'durationValueType':durationvaluetype,
'targetType':targetType,
'targetValue':targetValue,
'targetValueLow':targetValueLow,
'targetValueHigh':targetValueHigh,
}
try:
steps = step['steps']
lijst = []
order += 1
for s in steps:
sout, order = step_to_garmin(s, order)
lijst.append(sout)
out['steps'] = lijst
order -= 1
except KeyError:
pass
order += 1
return out, order
def ps_to_garmin(ps,r):
payload = {
'workoutName': ps.name,
'sport':r.garminactivity,
'description':ps_dict_get_description(ps.steps),
'estimatedDurationInSecs':60*ps.approximate_duration,
'estimatedDistanceInMeters': ps.approximate_distance,
'workoutProvider': 'Rowsandall.com',
'workoutSourceId': 'Rowsandall.com',
}
steps = []
steplist = utils.ps_dict_order_dict(ps.steps)
while steplist:
step, steplist = utils.peel(steplist)
steps.append(step)
steps = list(reversed(steps))
lijst = []
i = 0
for step in steps:
gstep, i = step_to_garmin(step,i)
lijst.append(gstep)
payload['steps'] = lijst
return payload
def get_garmin_permissions(user):
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.garmintoken is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
garminheaders = OAuth1(
client_key = oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
resource_owner_key=r.garmintoken,
resource_owner_secret=r.garminrefreshtoken,
signature_method='HMAC-SHA1',
)
url = 'https://apis.garmin.com/userPermissions'
result = requests.get(url,auth=garminheaders)
if result.status_code == 200:
return result.json()
return [] # pragma: no cover
def garmin_session_create(ps,user):
if not ps.steps:
return 0 # pragma: no cover
if not garmin_can_export_session(user):
return 0 # pragma: no cover
if ps.garmin_schedule_id != 0:
return ps.garmin_schedule_id # pragma: no cover
r = Rower.objects.get(user=user)
if (r.garmintoken == '') or (r.garmintoken is None): # pragma: no cover
s = "Token doesn't exist. Need to authorize"
return custom_exception_handler(401,s)
payload = ps_to_garmin(ps,r)
url = 'https://apis.garmin.com/training-api/workout'
garminheaders = OAuth1(
client_key = oauth_data['client_id'],
client_secret=oauth_data['client_secret'],
resource_owner_key=r.garmintoken,
resource_owner_secret=r.garminrefreshtoken,
signature_method='HMAC-SHA1',
)
response = requests.post(url,auth=garminheaders,json=payload)
if response.status_code != 200: # pragma: no cover
return 0
garmin_workout_id = response.json()['workoutId']
url = 'https://apis.garmin.com/training-api/schedule'
payload = {
'workoutId': garmin_workout_id,
'date': ps.preferreddate.strftime('%Y-%m-%d')
}
response = requests.post(url,auth=garminheaders,json=payload)
if response.status_code != 200: # pragma: no cover
return 0
ps.garmin_schedule_id = response.json()
ps.garmin_workout_id = garmin_workout_id
ps.save()
return garmin_workout_id
def garmin_getworkout(garminid,r,activity):
starttime = activity['startTimeInSeconds']
startdatetime = arrow.get(starttime)
try:
offset = activity['startTimeOffsetInSeconds']
except KeyError: # pragma: no cover
offset = 0
durationseconds = activity['durationInSeconds']
duration = dataprep.totaltime_sec_to_string(durationseconds)
activitytype = activity['activityType']
name = ''
date = startdatetime.date()
try:
distance = activity['distanceInMeters']
except KeyError:
distance = 0
try:
averagehr = activity['averageHeartRateInBeatsPerMinute']
maxhr = activity['maxHeartRateInBeatsPerMinute']
except KeyError: # pragma: no cover
averagehr = 0
maxhr = 0
try:
w = Workout.objects.get(uploadedtogarmin=garminid)
except Workout.DoesNotExist:
newcsvfile='media/garmin{code}_{importid}.csv'.format(
code=uuid4().hex[:16],
importid=garminid,
)
w = Workout(user=r,csvfilename=newcsvfile)
utc_offset = datetime.timedelta(seconds=offset)
now = datetime.datetime.now(pytz.utc)
zones = [tz.zone for tz in map(pytz.timezone, pytz.all_timezones_set)
if now.astimezone(tz).utcoffset() == utc_offset]
if r.defaulttimezone in zones: # pragma: no cover
thetimezone = r.defaulttimezone
elif len(zones):
thetimezone = zones[0]
else: # pragma: no cover
thetimezone = utc
startdatetime = datetime.datetime(
year=startdatetime.year,
month=startdatetime.month,
day=startdatetime.day,
hour=startdatetime.hour,
minute=startdatetime.minute,
second=startdatetime.second,
).astimezone(pytz.timezone(thetimezone))
w.startdatetime = startdatetime
w.starttime = w.startdatetime.time()
try:
w.duration = datetime.datetime.strptime(duration,"%H:%M:%S.%f").time()
except ValueError: # pragma: no cover
w.duration = datetime.datetime.strptime(duration,"%H:%M:%S")
try:
w.workouttype = mytypes.garminmappinginv[activitytype]
except KeyError: # pragma: no cover
w.workouttype = 'other'
w.name = name
w.date = date
w.distance = distance
w.uploadedtogarmin = garminid
w.save()
return w
def garmin_workouts_from_details(data):
activities = data['activityDetails']
for activity in activities:
try: # pragma: no cover
garmintoken = activity['userAccessToken']
except KeyError: # pragma: no cover
return 0
except TypeError: # pragma: no cover
return 0
try:
r = Rower.objects.get(garmintoken=garmintoken)
garminid = activity['summaryId'][:-7]
summary = activity['summary']
w = garmin_getworkout(garminid,r,summary)
samples = activity['samples']
df = pd.DataFrame(samples)
df.rename(columns=columns,inplace=True)
try:
pace = 500./df[' AverageBoatSpeed (m/s)']
except KeyError:
pace = 0
df[' AverageBoatSpeed (m/s)'] = 0
df[' Stroke500mPace (sec/500m)'] = pace
try:
spm = df[' Cadence (stokes/min)']
except KeyError:
df[' Cadence (stokes/min)'] = 0
df['cum_dist'] = df[' Horizontal (meters)']
try:
power = df[' Power (watts)']
except KeyError:
df[' Power (watts)'] = 0
df[' AverageDriveForce (lbs)'] = 0
df[' DriveLength (meters)'] = 0
df[' PeakDriveForce (lbs)'] = 1
df[' DriveTime (ms)'] = 0
rowdata = rowingdata(df=df)
rowdata.write_csv(w.csvfilename,gzip=True)
data = dataprep.dataprep(rowdata.df,id=w.id)
summary = rowdata.allstats()
w.summary=summary
w.uploadedtogarmin = garminid
w.save()
trimp,hrtss = dataprep.workout_trimp(w)
rscore,normp = dataprep.workout_rscore(w)
except Rower.DoesNotExist: # pragma: no cover
pass
return 1
def garmin_workouts_from_summaries(activities):
for activity in activities:
garmintoken = activity['userAccessToken']
try:
r = Rower.objects.get(garmintoken=garmintoken)
id = activity['summaryId']
w = garmin_getworkout(id,r,activity)
except Rower.DoesNotExist: # pragma: no cover
pass
return 1