from rowers.models import C2WorldClassAgePerformance, Rower, Workout, TombStone from rowers import utils from rowers.utils import custom_exception_handler, NoTokenError from rowingdata import rowingdata from django.core.exceptions import PermissionDenied from rowers.utils import myqueue import datetime import requests from requests import Session, Request from requests_oauthlib import OAuth1, OAuth1Session from requests_oauthlib.oauth1_session import TokenRequestDenied 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 arrow from rowers import dataprep import pytz 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 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') 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 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 = r.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 != 'pro': return False # pragma: no cover result = get_garmin_permissions(user) if 'WORKOUT_IMPORT' in result: return True return False # pragma: no cover 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: 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 = [ttz.zone for ttz in map( pytz.timezone, pytz.all_timezones_set) if now.astimezone(ttz).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: _ = df[' Cadence (stokes/min)'] except KeyError: df[' Cadence (stokes/min)'] = 0 df['cum_dist'] = df[' Horizontal (meters)'] try: _ = 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'] _ = garmin_getworkout(id, r, activity) except Rower.DoesNotExist: # pragma: no cover pass return 1