def DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] now = datetime.now() prev = now - timedelta(6 * 365 / 12) period = [] aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year, now.month) period.append(aperiod) if exhaustive: for _ in range(20): now = prev prev = now - timedelta(6 * 365 / 12) aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year, now.month) period.append(aperiod) for dateInterval in period: headers = self._getAuthHeaders(svcRecord) resp = requests.get(DECATHLONCOACH_API_BASE_URL + "/users/" + str(svcRecord.ExternalID) + "/activities.xml?date=" + dateInterval, headers=headers) if resp.status_code == 400: logger.info(resp.content) raise APIException( "No authorization to retrieve activity list", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) if resp.status_code == 401: logger.info(resp.content) raise APIException( "No authorization to retrieve activity list", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) if resp.status_code == 403: logger.info(resp.content) raise APIException( "No authorization to retrieve activity list", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) root = xml.fromstring(resp.content) logger.info("\t\t nb activity : " + str(len(root.findall('.//ID')))) for ride in root.iter('ACTIVITY'): activity = UploadedActivity() activity.TZ = pytz.timezone("UTC") startdate = ride.find('.//STARTDATE').text + ride.find( './/TIMEZONE').text datebase = parse(startdate) activity.StartTime = datebase #pytz.utc.localize(datebase) activity.ServiceData = { "ActivityID": ride.find('ID').text, "Manual": ride.find('MANUAL').text } logger.info("\t\t DecathlonCoach Activity ID : " + ride.find('ID').text) if ride.find('SPORTID' ).text not in self._reverseActivityTypeMappings: exclusions.append( APIExcludeActivity("Unsupported activity type %s" % ride.find('SPORTID').text, activity_id=ride.find('ID').text, user_exception=UserException( UserExceptionType.Other))) logger.info( "\t\tDecathlonCoach Unknown activity, sport id " + ride.find('SPORTID').text + " is not mapped") continue activity.Type = self._reverseActivityTypeMappings[ride.find( 'SPORTID').text] for val in ride.iter('VALUE'): if val.get('id') == self._unitMap["duration"]: activity.EndTime = activity.StartTime + timedelta( 0, int(val.text)) if val.get('id') == self._unitMap["distance"]: activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=int(val.text)) if val.get('id') == self._unitMap["kcal"]: activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=int(val.text)) if val.get('id') == self._unitMap["speedaverage"]: meterperhour = int(val.text) meterpersecond = meterperhour / 3600 activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.MetersPerSecond, avg=meterpersecond, max=None) if ride.find('LIBELLE' ).text == "" or ride.find('LIBELLE').text is None: txtdate = startdate.split(' ') activity.Name = "Sport DecathlonCoach " + txtdate[0] else: activity.Name = ride.find('LIBELLE').text activity.Private = False activity.Stationary = ride.find('MANUAL').text activity.GPS = ride.find('ABOUT').find('TRACK').text activity.AdjustTZ() activity.CalculateUID() activities.append(activity) return activities, exclusions
def UploadActivity(self, serviceRecord, activity): activityData = {} # Props to the SportTracks API people for seamlessly supprting activities with or without TZ data. activityData["start_time"] = activity.StartTime.isoformat() if activity.Name: activityData["name"] = activity.Name if activity.Notes: activityData["notes"] = activity.Notes activityData[ "sharing"] = "public" if not activity.Private else "private" activityData["type"] = self._reverseActivityMappings[activity.Type] def _resolveDuration(obj): if obj.Stats.TimerTime.Value is not None: return obj.Stats.TimerTime.asUnits( ActivityStatisticUnit.Seconds).Value if obj.Stats.MovingTime.Value is not None: return obj.Stats.MovingTime.asUnits( ActivityStatisticUnit.Seconds).Value return (obj.EndTime - obj.StartTime).total_seconds() def _mapStat(dict, key, val, naturalValue=False): if val is not None: if naturalValue: val = round(val) dict[key] = val _mapStat(activityData, "clock_duration", (activity.EndTime - activity.StartTime).total_seconds()) _mapStat( activityData, "duration", _resolveDuration(activity) ) # This has to be set, otherwise all time shows up as "stopped" :( _mapStat( activityData, "total_distance", activity.Stats.Distance.asUnits( ActivityStatisticUnit.Meters).Value) _mapStat(activityData, "calories", activity.Stats.Energy.asUnits( ActivityStatisticUnit.Kilojoules).Value, naturalValue=True) _mapStat(activityData, "elevation_gain", activity.Stats.Elevation.Gain) _mapStat(activityData, "elevation_loss", activity.Stats.Elevation.Loss) _mapStat(activityData, "max_speed", activity.Stats.Speed.Max) _mapStat(activityData, "avg_heartrate", activity.Stats.HR.Average) _mapStat(activityData, "max_heartrate", activity.Stats.HR.Max) _mapStat(activityData, "avg_cadence", activity.Stats.Cadence.Average) _mapStat(activityData, "max_cadence", activity.Stats.Cadence.Max) _mapStat(activityData, "avg_power", activity.Stats.Power.Average) _mapStat(activityData, "max_power", activity.Stats.Power.Max) activityData["laps"] = [] lapNum = 0 for lap in activity.Laps: lapNum += 1 lapinfo = { "number": lapNum, "start_time": lap.StartTime.isoformat(), "type": "REST" if lap.Intensity == LapIntensity.Rest else "ACTIVE" } _mapStat(lapinfo, "clock_duration", (lap.EndTime - lap.StartTime).total_seconds()) # Required too. _mapStat(lapinfo, "duration", _resolveDuration( lap)) # This field is required for laps to be created. _mapStat( lapinfo, "distance", lap.Stats.Distance.asUnits( ActivityStatisticUnit.Meters).Value) # Probably required. _mapStat(lapinfo, "calories", lap.Stats.Energy.asUnits( ActivityStatisticUnit.Kilojoules).Value, naturalValue=True) _mapStat(lapinfo, "elevation_gain", lap.Stats.Elevation.Gain) _mapStat(lapinfo, "elevation_loss", lap.Stats.Elevation.Loss) _mapStat(lapinfo, "max_speed", lap.Stats.Speed.Max) _mapStat(lapinfo, "avg_heartrate", lap.Stats.HR.Average) _mapStat(lapinfo, "max_heartrate", lap.Stats.HR.Max) activityData["laps"].append(lapinfo) if not activity.Stationary: timer_stops = [] timer_stopped_at = None def stream_append(stream, wp, data): stream += [ round((wp.Timestamp - activity.StartTime).total_seconds()), data ] location_stream = [] distance_stream = [] elevation_stream = [] heartrate_stream = [] power_stream = [] cadence_stream = [] for lap in activity.Laps: for wp in lap.Waypoints: if wp.Location and wp.Location.Latitude and wp.Location.Longitude: stream_append( location_stream, wp, [wp.Location.Latitude, wp.Location.Longitude]) if wp.HR: stream_append(heartrate_stream, wp, round(wp.HR)) if wp.Distance: stream_append(distance_stream, wp, wp.Distance) if wp.Cadence or wp.RunCadence: stream_append( cadence_stream, wp, round(wp.Cadence) if wp.Cadence else round(wp.RunCadence)) if wp.Power: stream_append(power_stream, wp, wp.Power) if wp.Location and wp.Location.Altitude: stream_append(elevation_stream, wp, wp.Location.Altitude) if wp.Type == WaypointType.Pause and not timer_stopped_at: timer_stopped_at = wp.Timestamp if wp.Type != WaypointType.Pause and timer_stopped_at: timer_stops.append([timer_stopped_at, wp.Timestamp]) timer_stopped_at = None activityData["elevation"] = elevation_stream activityData["heartrate"] = heartrate_stream activityData["power"] = power_stream activityData["cadence"] = cadence_stream activityData["distance"] = distance_stream activityData["location"] = location_stream activityData["timer_stops"] = [[y.isoformat() for y in x] for x in timer_stops] headers = self._getAuthHeaders(serviceRecord) headers.update({"Content-Type": "application/json"}) upload_resp = requests.post(self.OpenFitEndpoint + "/fitnessActivities.json", data=json.dumps(activityData), headers=headers) if upload_resp.status_code != 200: if upload_resp.status_code == 401: raise APIException("ST.mobi trial expired", block=True, user_exception=UserException( UserExceptionType.AccountExpired, intervention_required=True)) raise APIException("Unable to upload activity %s" % upload_resp.text) return upload_resp.json()["uris"][0]
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData["Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] return activity activityID = activity.ServiceData["ActivityID"] streamdata = requests.get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "auth") raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata["message"] == "Record Not Found": self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "missing") raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasMovingData = "moving" in ridedata and len(ridedata["moving"]) > 0 moving = True if "error" in ridedata: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "data") raise APIException("Strava error " + ridedata["error"]) hasLocation = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): latlng = ridedata["latlng"][idx] waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None else: # strava only returns 0 as invalid coords, so no need to check for null (update: ??) hasLocation = True if hasAltitude: waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif hasMovingData and not moving and ridedata["moving"][idx] is True: waypoint.Type = WaypointType.Resume moving = True elif hasMovingData and ridedata["moving"][idx] is False: waypoint.Type = WaypointType.Pause moving = False if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] lap.Waypoints.append(waypoint) if not hasLocation: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "faulty") raise APIExcludeActivity("No waypoints with location", activityId=activityID, userException=UserException(UserExceptionType.Corrupt)) self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), None) return activity
def DownloadActivityList(self, serviceRecord, exhaustive=False): """ GET List of Activities as JSON File URL: http://app.velohero.com/export/workouts/json Parameters: user = username pass = password date_from = YYYY-MM-DD date_to = YYYY-MM-DD """ activities = [] exclusions = [] discoveredWorkoutIds = [] params = self._add_auth_params({}, record=serviceRecord) limitDateFormat = "%Y-%m-%d" if exhaustive: listEnd = datetime.now() + timedelta( days=1.5) # Who knows which TZ it's in listStart = datetime(day=1, month=1, year=1980) # The beginning of time else: listEnd = datetime.now() + timedelta( days=1.5) # Who knows which TZ it's in listStart = listEnd - timedelta(days=20) # Doesn't really matter params.update({ "date_from": listStart.strftime(limitDateFormat), "date_to": listEnd.strftime(limitDateFormat) }) logger.debug("Requesting %s to %s" % (listStart, listEnd)) res = requests.get(self._urlRoot + "/export/workouts/json", params=params) if res.status_code != 200: if res.status_code == 403: raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to retrieve activity list") res.raise_for_status() try: res = res.json() except ValueError: raise APIException("Could not decode activity list") if "workouts" not in res: raise APIException("No activities") for workout in res["workouts"]: workoutId = int(workout["id"]) if workoutId in discoveredWorkoutIds: continue # There's the possibility of query overlap discoveredWorkoutIds.append(workoutId) if workout["file"] is not "1": logger.debug("Skip workout with ID: " + str(workoutId) + " (no file)") continue # Skip activity without samples (no PWX export) activity = UploadedActivity() logger.debug("Workout ID: " + str(workoutId)) # Duration (dur_time) duration = self._durationToSeconds(workout["dur_time"]) activity.Stats.TimerTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=duration) # Start time (date_ymd, start_time) startTimeStr = workout["date_ymd"] + " " + workout["start_time"] activity.StartTime = self._parseDateTime(startTimeStr) # End time (date_ymd, start_time) + dur_time activity.EndTime = self._parseDateTime(startTimeStr) + timedelta( seconds=duration) # Sport (sport_id) if workout["sport_id"] in self._reverseActivityMappings: activity.Type = self._reverseActivityMappings[ workout["sport_id"]] else: activity.Type = ActivityType.Other # Distance (dist_km) activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Kilometers, value=float(workout["dist_km"])) # Workout is hidden activity.Private = workout["hide"] == "1" activity.ServiceData = {"workoutId": workoutId} activity.CalculateUID() activities.append(activity) return activities, exclusions
def _get_session(self, record=None, email=None, password=None, skip_cache=False): from tapiriik.auth.credential_storage import CredentialStore cached = self._sessionCache.Get(record.ExternalID if record else email) if cached and not skip_cache: return cached if record: # longing for C style overloads... password = CredentialStore.Decrypt( record.ExtendedAuthorization["Password"]) email = CredentialStore.Decrypt( record.ExtendedAuthorization["Email"]) session = requests.Session() self._rate_limit() mPreResp = session.get(self._urlRoot + "/api/tapiriikProfile", allow_redirects=False) # New site gets this redirect, old one does not if mPreResp.status_code == 403: data = { "_username": email, "_password": password, "_remember_me": "true", } preResp = session.post(self._urlRoot + "/api/login", data=data) if preResp.status_code != 200: raise APIException("Login error %s %s" % (preResp.status_code, preResp.text)) try: preResp = preResp.json() except ValueError: raise APIException("Parse error %s %s" % (preResp.status_code, preResp.text), block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) if "success" not in preResp and "error" not in preResp: raise APIException("Login error", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) success = True error = "" if "success" in preResp: success = ["success"] if "error" in preResp: error = preResp["error"] if not success: logger.debug("Login error %s" % (error)) raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) # Double check self._rate_limit() mRedeemResp1 = session.get(self._urlRoot + "/api/tapiriikProfile", allow_redirects=False) if mRedeemResp1.status_code != 200: raise APIException( "Motivato redeem error %s %s" % (mRedeemResp1.status_code, mRedeemResp1.text)) else: logger.debug("code %s" % mPreResp.status_code) raise APIException("Unknown Motivato prestart response %s %s" % (mPreResp.status_code, mPreResp.text)) self._sessionCache.Set(record.ExternalID if record else email, session) session.headers.update(self._obligatory_headers) return session
def _get_session(self, record=None, email=None, password=None, skip_cache=False): from tapiriik.auth.credential_storage import CredentialStore cached = self._sessionCache.Get(record.ExternalID if record else email) if cached and not skip_cache: logger.debug("Using cached credential") return cached if record: # longing for C style overloads... password = CredentialStore.Decrypt(record.ExtendedAuthorization["Password"]) email = CredentialStore.Decrypt(record.ExtendedAuthorization["Email"]) session = requests.Session() # JSIG CAS, cool I guess. # Not quite OAuth though, so I'll continue to collect raw credentials. # Commented stuff left in case this ever breaks because of missing parameters... data = { "username": email, "password": password, "_eventId": "submit", "embed": "true", # "displayNameRequired": "false" } params = { "service": "https://connect.garmin.com/post-auth/login", # "redirectAfterAccountLoginUrl": "http://connect.garmin.com/post-auth/login", # "redirectAfterAccountCreationUrl": "http://connect.garmin.com/post-auth/login", # "webhost": "olaxpw-connect00.garmin.com", "clientId": "GarminConnect", # "gauthHost": "https://sso.garmin.com/sso", # "rememberMeShown": "true", # "rememberMeChecked": "false", "consumeServiceTicket": "false", # "id": "gauth-widget", # "embedWidget": "false", # "cssUrl": "https://static.garmincdn.com/com.garmin.connect/ui/src-css/gauth-custom.css", # "source": "http://connect.garmin.com/en-US/signin", # "createAccountShown": "true", # "openCreateAccount": "false", # "usernameShown": "true", # "displayNameShown": "false", # "initialFocus": "true", # "locale": "en" } # I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do... preResp = session.get("https://sso.garmin.com/sso/login", params=params) if preResp.status_code != 200: raise APIException("SSO prestart error %s %s" % (preResp.status_code, preResp.text)) data["lt"] = re.search("name=\"lt\"\s+value=\"([^\"]+)\"", preResp.text).groups(1)[0] ssoResp = session.post("https://sso.garmin.com/sso/login", params=params, data=data, allow_redirects=False) if ssoResp.status_code != 200 or "temporarily unavailable" in ssoResp.text: raise APIException("SSO error %s %s" % (ssoResp.status_code, ssoResp.text)) if "renewPassword" in ssoResp.text: raise APIException("Reset password", block=True, user_exception=UserException(UserExceptionType.RenewPassword, intervention_required=True)) ticket_match = re.search("ticket=([^']+)'", ssoResp.text) if not ticket_match: raise APIException("Invalid login", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) ticket = ticket_match.groups(1)[0] # ...AND WE'RE NOT DONE YET! self._rate_limit() gcRedeemResp = session.get("https://connect.garmin.com/post-auth/login", params={"ticket": ticket}, allow_redirects=False) if gcRedeemResp.status_code != 302: raise APIException("GC redeem-start error %s %s" % (gcRedeemResp.status_code, gcRedeemResp.text)) # There are 6 redirects that need to be followed to get the correct cookie # ... :( expected_redirect_count = 6 current_redirect_count = 1 while True: self._rate_limit() gcRedeemResp = session.get(gcRedeemResp.headers["location"], allow_redirects=False) if current_redirect_count >= expected_redirect_count and gcRedeemResp.status_code != 200: raise APIException("GC redeem %d/%d error %s %s" % (current_redirect_count, expected_redirect_count, gcRedeemResp.status_code, gcRedeemResp.text)) if gcRedeemResp.status_code == 200 or gcRedeemResp.status_code == 404: break current_redirect_count += 1 if current_redirect_count > expected_redirect_count: break self._sessionCache.Set(record.ExternalID if record else email, session) session.headers.update(self._obligatory_headers) return session
def _downloadActivitySummary(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] summary_resp = self._request_with_reauth(serviceRecord, lambda session: session.get("https://connect.garmin.com/modern/proxy/activity-service/activity/" + str(activityID))) try: summary_data = summary_resp.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (summary_resp.status_code, summary_resp.text)) stat_map = {} def mapStat(gcKey, statKey, type, units): stat_map[gcKey] = { "key": statKey, "attr": type, "units": units } def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]) if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update(ActivityStatistic(stat["units"], **({stat["attr"]: value}))) mapStat("movingDuration", "MovingTime", "value", ActivityStatisticUnit.Seconds) mapStat("duration", "TimerTime", "value", ActivityStatisticUnit.Seconds) mapStat("distance", "Distance", "value", ActivityStatisticUnit.Meters) mapStat("maxSpeed", "Speed", "max", ActivityStatisticUnit.MetersPerSecond) mapStat("averageSpeed", "Speed", "avg", ActivityStatisticUnit.MetersPerSecond) mapStat("calories", "Energy", "value", ActivityStatisticUnit.Kilocalories) mapStat("maxHR", "HR", "max", ActivityStatisticUnit.BeatsPerMinute) mapStat("averageHR", "HR", "avg", ActivityStatisticUnit.BeatsPerMinute) mapStat("minElevation", "Elevation", "min", ActivityStatisticUnit.Meters) mapStat("maxElevation", "Elevation", "max", ActivityStatisticUnit.Meters) mapStat("elevationGain", "Elevation", "gain", ActivityStatisticUnit.Meters) mapStat("elevationLoss", "Elevation", "loss", ActivityStatisticUnit.Meters) mapStat("averageBikeCadence", "Cadence", "avg", ActivityStatisticUnit.RevolutionsPerMinute) mapStat("averageCadence", "Cadence", "avg", ActivityStatisticUnit.StepsPerMinute) applyStats(summary_data["summaryDTO"], activity.Stats) laps_resp = self._request_with_reauth(serviceRecord, lambda session: session.get("https://connect.garmin.com/modern/proxy/activity-service/activity/%s/splits" % str(activityID))) try: laps_data = laps_resp.json() except ValueError: raise APIException("Failure downloading activity laps summary %s:%s" % (laps_resp.status_code, laps_resp.text)) for lap_data in laps_data["lapDTOs"]: lap = Lap() if "startTimeGMT" in lap_data: lap.StartTime = pytz.utc.localize(datetime.strptime(lap_data["startTimeGMT"], "%Y-%m-%dT%H:%M:%S.0")) elapsed_duration = None if "elapsedDuration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["elapsedDuration"]))) elif "duration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["duration"]))) if lap.StartTime and elapsed_duration: # Always recalculate end time based on duration, if we have the start time lap.EndTime = lap.StartTime + elapsed_duration if not lap.StartTime and lap.EndTime and elapsed_duration: # Sometimes calculate start time based on duration lap.StartTime = lap.EndTime - elapsed_duration if not lap.StartTime or not lap.EndTime: # Garmin Connect is weird. raise APIExcludeActivity("Activity lap has no BeginTimestamp or EndTimestamp", user_exception=UserException(UserExceptionType.Corrupt)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
def UploadActivity(self, svcRecord, activity): logging.info("UPLOAD To Fitbit Activity tz " + str(activity.TZ) + " dt tz " + str( activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) logger.info("Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) # Check if we're currently uploading item if self.LastUpload is not None: while (datetime.now() - self.LastUpload).total_seconds() < 5: time.sleep(1) logger.debug("Inter-upload cooldown") # Get activity source source_svc = None if hasattr(activity, "ServiceDataCollection"): source_svc = str(list(activity.ServiceDataCollection.keys())[0]) upload_id = None userID = svcRecord.ExternalID activity_id = activity.ServiceData["ActivityID"] activity_date = activity.StartTime.strftime("%Y-%m-%d") activity_time = activity.StartTime.strftime("%H:%M:%S") durationDelta = activity.EndTime - activity.StartTime duration = durationDelta.total_seconds() * 1000 distance = 0 if activity.Stats.Distance: distance = activity.Stats.Distance.asUnits(ActivityStatisticUnit.Meters).Value calories = 0 if activity.Stats.Energy and activity.Stats.Energy.asUnits(ActivityStatisticUnit.Kilocalories).Value is not None: calories = activity.Stats.Energy.asUnits(ActivityStatisticUnit.Kilocalories).Value parameters = { 'manualCalories': int(calories), 'startTime': activity_time, 'date': activity_date, 'durationMillis': int(duration), 'distance': distance, 'distanceUnit': 'Meter', } # If activity type is "other" set name into parameters, else set activity type # uri parameters doesn't accept both on same post if activity.Type != 20: activity_name = activity.StartTime.strftime("%d/%m/%Y") if activity.Name: activity_name = activity.Name parameters['activityName'] = activity_name else: parameters['activityId'] = self._activityTypeMappings[activity.Type] activity_upload_uri = 'https://api.fitbit.com/1/user/' + userID + '/activities.json' resp = self._requestWithAuth(lambda session: session.post( activity_upload_uri, data=parameters, headers={ 'Authorization': 'Bearer ' + svcRecord.Authorization.get('AccessToken') }), svcRecord) self.LastUpload = datetime.now() if resp.status_code != 201 and resp.status_code != 200: if resp.status_code == 401: raise APIException( "No authorization to upload activity " + activity.UID + " response " + resp.text + " status " + str( resp.status_code), block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) raise APIException( "Unable to upload activity " + activity.UID + " response " + resp.text + " status " + str( resp.status_code)) resp_json = resp.json() upload_id = resp_json['activityLog']['activityId'] return upload_id
def _get_session(self, record=None, email=None, password=None, skip_cache=False): from tapiriik.auth.credential_storage import CredentialStore cached = self._sessionCache.Get(record.ExternalID if record else email) if cached and not skip_cache: return cached if record: # longing for C style overloads... password = CredentialStore.Decrypt( record.ExtendedAuthorization["Password"]) email = CredentialStore.Decrypt( record.ExtendedAuthorization["Email"]) session = requests.Session() self._rate_limit() gcPreResp = session.get("http://connect.garmin.com/", allow_redirects=False) # New site gets this redirect, old one does not if gcPreResp.status_code == 200: self._rate_limit() gcPreResp = session.get("https://connect.garmin.com/signin", allow_redirects=False) req_count = int( re.search("j_id(\d+)", gcPreResp.text).groups(1)[0]) params = { "login": "******", "login:loginUsernameField": email, "login:password": password, "login:signInButton": "Sign In" } auth_retries = 3 # Did I mention Garmin Connect is silly? for retries in range(auth_retries): params["javax.faces.ViewState"] = "j_id%d" % req_count req_count += 1 self._rate_limit() resp = session.post("https://connect.garmin.com/signin", data=params, allow_redirects=False) if resp.status_code >= 500 and resp.status_code < 600: raise APIException("Remote API failure") if resp.status_code != 302: # yep if "errorMessage" in resp.text: if retries < auth_retries - 1: time.sleep(1) continue else: raise APIException( "Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) else: raise APIException("Mystery login error %s" % resp.text) break elif gcPreResp.status_code == 302: # JSIG CAS, cool I guess. # Not quite OAuth though, so I'll continue to collect raw credentials. # Commented stuff left in case this ever breaks because of missing parameters... data = { "username": email, "password": password, "_eventId": "submit", "embed": "true", # "displayNameRequired": "false" } params = { "service": "http://connect.garmin.com/post-auth/login", # "redirectAfterAccountLoginUrl": "http://connect.garmin.com/post-auth/login", # "redirectAfterAccountCreationUrl": "http://connect.garmin.com/post-auth/login", # "webhost": "olaxpw-connect00.garmin.com", "clientId": "GarminConnect", # "gauthHost": "https://sso.garmin.com/sso", # "rememberMeShown": "true", # "rememberMeChecked": "false", "consumeServiceTicket": "false", # "id": "gauth-widget", # "embedWidget": "false", # "cssUrl": "https://static.garmincdn.com/com.garmin.connect/ui/src-css/gauth-custom.css", # "source": "http://connect.garmin.com/en-US/signin", # "createAccountShown": "true", # "openCreateAccount": "false", # "usernameShown": "true", # "displayNameShown": "false", # "initialFocus": "true", # "locale": "en" } # I may never understand what motivates people to mangle a perfectly good protocol like HTTP in the ways they do... preResp = session.get("https://sso.garmin.com/sso/login", params=params) if preResp.status_code != 200: raise APIException("SSO prestart error %s %s" % (preResp.status_code, preResp.text)) data["lt"] = re.search("name=\"lt\"\s+value=\"([^\"]+)\"", preResp.text).groups(1)[0] ssoResp = session.post("https://sso.garmin.com/sso/login", params=params, data=data, allow_redirects=False) if ssoResp.status_code != 200: raise APIException("SSO error %s %s" % (ssoResp.status_code, ssoResp.text)) ticket_match = re.search("ticket=([^']+)'", ssoResp.text) if not ticket_match: raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) ticket = ticket_match.groups(1)[0] # ...AND WE'RE NOT DONE YET! self._rate_limit() gcRedeemResp1 = session.get( "http://connect.garmin.com/post-auth/login", params={"ticket": ticket}, allow_redirects=False) if gcRedeemResp1.status_code != 302: raise APIException( "GC redeem 1 error %s %s" % (gcRedeemResp1.status_code, gcRedeemResp1.text)) self._rate_limit() gcRedeemResp2 = session.get(gcRedeemResp1.headers["location"], allow_redirects=False) if gcRedeemResp2.status_code != 302: raise APIException( "GC redeem 2 error %s %s" % (gcRedeemResp2.status_code, gcRedeemResp2.text)) else: raise APIException("Unknown GC prestart response %s %s" % (gcPreResp.status_code, gcPreResp.text)) self._sessionCache.Set(record.ExternalID if record else email, session) return session
def Authorize(self, email, password): from tapiriik.auth.credential_storage import CredentialStore session = self._get_session(email=email, password=password, skip_cache=True) self._rate_limit() try: dashboard = session.get("http://connect.garmin.com/modern") userdata_json_str = re.search(r"VIEWER_SOCIAL_PROFILE\s*=\s*JSON\.parse\((.+)\);$", dashboard.text, re.MULTILINE).group(1) userdata = json.loads(json.loads(userdata_json_str)) username = userdata["displayName"] except Exception as e: raise APIException("Unable to retrieve username: %s" % e, block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) return (username, {}, {"Email": CredentialStore.Encrypt(email), "Password": CredentialStore.Encrypt(password)})
def DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] before = earliestDate = None # define low parameter limit = 20 offset = 0 sort = "desc" # get user Fitbit ID userID = svcRecord.ExternalID # get service Tapiriik ID service_id = svcRecord._id # get user "start sync from date" info # then prepare afterDate var (this var determine the date since we download activities) user = db.users.find_one({'ConnectedServices': {'$elemMatch': {'ID': service_id, 'Service': 'fitbit'}}}) afterDateObj = datetime.now() - timedelta(days=1) if user['Config']['sync_skip_before'] is not None: afterDateObj = user['Config']['sync_skip_before'] else: if exhaustive: afterDateObj = datetime.now() - timedelta(days=3650) # throw back to 10 years afterDate = afterDateObj.strftime("%Y-%m-%d") logging.info("\t Download Fitbit activities since : " + afterDate) # prepare parameters to set in fitbit request uri uri_parameters = { 'limit': limit, 'offset': offset, 'sort': sort, 'afterDate': afterDate, 'token': svcRecord.Authorization.get('AccessToken') } # set base fitbit request uri activities_uri_origin = 'https://api.fitbit.com/1/user/' + userID + '/activities/list.json' # first execute offset = 0, # offset will be set to -1 if fitbit response don't give next pagination info # offset will be incremented by 1 if fitbit response give next pagination info index_total = 0 while offset > -1: # prepare uri parameters uri_parameters['offset'] = offset # build fitbit uri with new parameters activities_uri = activities_uri_origin + "?" + urlencode(uri_parameters) # execute fitbit request using "request with auth" function (it refreshes token if needed) logging.info("\t\t downloading offset : " + str(offset)) resp = self._requestWithAuth(lambda session: session.get( activities_uri, headers={ 'Authorization': 'Bearer ' + svcRecord.Authorization.get('AccessToken') }), svcRecord) # check if request has error if resp.status_code != 204 and resp.status_code != 200: raise APIException("Unable to find Fitbit activities") # get request data data = {} try: data = resp.json() except ValueError: raise APIException("Failed parsing fitbit list response %s - %s" % (resp.status_code, resp.text)) # if request return activities infos if data['activities']: ftbt_activities = data['activities'] logging.info("\t\t nb activity : " + str(len(ftbt_activities))) # for every activities in this request pagination # (Fitbit give 20 activities MAXIMUM, use limit parameter) for ftbt_activity in ftbt_activities: index_total = index_total +1 activity = UploadedActivity() #parse date start to get timezone and date parsedDate = ftbt_activity["startTime"][0:19] + ftbt_activity["startTime"][23:] activity.StartTime = datetime.strptime(parsedDate, "%Y-%m-%dT%H:%M:%S%z") activity.TZ = pytz.utc logger.debug("\tActivity s/t %s: %s" % (activity.StartTime, ftbt_activity["activityName"])) activity.EndTime = activity.StartTime + timedelta(0, (ftbt_activity["duration"]/1000)) activity.ServiceData = {"ActivityID": ftbt_activity["logId"], "Manual": ftbt_activity["logType"]} # check if activity type ID exists if ftbt_activity["activityTypeId"] not in self._reverseActivityTypeMappings: exclusions.append(APIExcludeActivity("Unsupported activity type %s" % ftbt_activity["activityTypeId"], activity_id=ftbt_activity["logId"], user_exception=UserException(UserExceptionType.Other))) logger.info("\t\tUnknown activity") continue activity.Type = self._reverseActivityTypeMappings[ftbt_activity["activityTypeId"]] activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=ftbt_activity["distance"]) if "speed" in ftbt_activity: activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.KilometersPerHour, avg=ftbt_activity["speed"], max=ftbt_activity["speed"] ) activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=ftbt_activity["calories"]) # Todo: find fitbit data name #activity.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=ride[ # "moving_time"] if "moving_time" in ride and ride[ # "moving_time"] > 0 else None) # They don't let you manually enter this, and I think it returns 0 for those activities. # Todo: find fitbit data name #if "average_watts" in ride: # activity.Stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts, # avg=ride["average_watts"]) if "averageHeartRate" in ftbt_activity: activity.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=ftbt_activity["averageHeartRate"])) # Todo: find fitbit data name #if "max_heartrate" in ride: # activity.Stats.HR.update( # ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=ride["max_heartrate"])) # Todo: find fitbit data name #if "average_cadence" in ride: # activity.Stats.Cadence.update(ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, # avg=ride["average_cadence"])) # Todo: find fitbit data name #if "average_temp" in ride: # activity.Stats.Temperature.update( # ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, avg=ride["average_temp"])) if "calories" in ftbt_activity: activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=ftbt_activity["calories"]) activity.Name = ftbt_activity["activityName"] activity.Private = False if ftbt_activity['logType'] is 'manual': activity.Stationary = True else: activity.Stationary = False # Todo: find fitbit data #activity.GPS = ("start_latlng" in ride) and (ride["start_latlng"] is not None) activity.AdjustTZ() activity.CalculateUID() activities.append(activity) logging.info("\t\t Fitbit Activity ID : " + str(ftbt_activity["logId"])) if not exhaustive: break # get next info for while condition and prepare offset for next request if 'next' not in data['pagination'] or not data['pagination']['next']: next = None offset = -1 else: next = data['pagination']['next'] offset = offset + 1 logging.info("\t\t total Fitbit activities downloaded : " + str(index_total)) return activities, exclusions
def DownloadActivityList(self, serviceRecord, exhaustive=False): def mapStatTriple(act, stats_obj, key, units): if "%s_max" % key in act and act["%s_max" % key]: stats_obj.update( ActivityStatistic(units, max=float(act["%s_max" % key]))) if "%s_min" % key in act and act["%s_min" % key]: stats_obj.update( ActivityStatistic(units, min=float(act["%s_min" % key]))) if "%s_avg" % key in act and act["%s_avg" % key]: stats_obj.update( ActivityStatistic(units, avg=float(act["%s_avg" % key]))) # http://ridewithgps.com/users/1/trips.json?limit=200&order_by=created_at&order_dir=asc # offset also supported activities = [] exclusions = [] # They don't actually support paging right now, for whatever reason params = self._add_auth_params({}, record=serviceRecord) res = requests.get( "https://ridewithgps.com/users/{}/trips.json".format( serviceRecord.ExternalID), params=params) res = res.json() # Apparently some API users are seeing this new result format - I'm not if type(res) is dict: res = res.get("results", []) if res == []: return [], [] # No activities for act in res: if "distance" not in act: exclusions.append( APIExcludeActivity("No distance", activity_id=act["id"], user_exception=UserException( UserExceptionType.Corrupt))) continue if "duration" not in act or not act["duration"]: exclusions.append( APIExcludeActivity("No duration", activity_id=act["id"], user_exception=UserException( UserExceptionType.Corrupt))) continue activity = UploadedActivity() logger.debug("Name " + act["name"] + ":") if len(act["name"].strip()): activity.Name = act["name"] if len(act["description"].strip()): activity.Notes = act["description"] activity.GPS = act["is_gps"] activity.Stationary = not activity.GPS # I think # 0 = public, 1 = private, 2 = friends activity.Private = act["visibility"] == 1 activity.StartTime = dateutil.parser.parse(act["departed_at"]) try: activity.TZ = pytz.timezone(act["time_zone"]) except pytz.exceptions.UnknownTimeZoneError: # Sometimes the time_zone returned isn't quite what we'd like it # So, just pull the offset from the datetime if isinstance(activity.StartTime.tzinfo, tzutc): activity.TZ = pytz.utc # The dateutil tzutc doesn't have an _offset value. else: activity.TZ = pytz.FixedOffset( activity.StartTime.tzinfo.utcoffset( activity.StartTime).total_seconds() / 60) activity.StartTime = activity.StartTime.replace( tzinfo=activity.TZ) # Overwrite dateutil's sillyness activity.EndTime = activity.StartTime + timedelta( seconds=self._duration_to_seconds(act["duration"])) logger.debug("Activity s/t " + str(activity.StartTime)) activity.AdjustTZ() activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, float(act["distance"])) mapStatTriple(act, activity.Stats.Power, "watts", ActivityStatisticUnit.Watts) mapStatTriple(act, activity.Stats.Speed, "speed", ActivityStatisticUnit.KilometersPerHour) mapStatTriple(act, activity.Stats.Cadence, "cad", ActivityStatisticUnit.RevolutionsPerMinute) mapStatTriple(act, activity.Stats.HR, "hr", ActivityStatisticUnit.BeatsPerMinute) if "elevation_gain" in act and act["elevation_gain"]: activity.Stats.Elevation.update( ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(act["elevation_gain"]))) if "elevation_loss" in act and act["elevation_loss"]: activity.Stats.Elevation.update( ActivityStatistic(ActivityStatisticUnit.Meters, loss=float(act["elevation_loss"]))) # Activity type is not implemented yet in RWGPS results; we will assume cycling, though perhaps "OTHER" wouuld be correct activity.Type = ActivityType.Cycling activity.CalculateUID() activity.ServiceData = {"ActivityID": act["id"]} activities.append(activity) return activities, exclusions
def Authorize(self, username, password): if not re.match(r"[^@]+@[^@]+\.[^@]+", username): raise APIException("Not a email address. Please, enter your email.", user_exception=UserException(UserExceptionType.NotAValidEmail)) if username != password: raise APIException("Inputs doesn't match. Please enter same email in both inputs.", user_exception=UserException(UserExceptionType.EmailsDoNotMatch)) self._ensure_user_root_exists(username) return (username, {})
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] logger.info("\t\t DC LOADING : " + str(activityID)) headers = self._getAuthHeaders(svcRecord) resp = requests.get(DECATHLONCOACH_API_BASE_URL + "/activity/" + activityID + "/fullactivity.xml", headers=headers) if resp.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: root = xml.fromstring(resp.content) except: raise APIException( "Stream data returned from DecathlonCoach is not XML") lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] lap.Waypoints = [] activity.GPS = False #work on date startdate = root.find('.//STARTDATE').text timezone = root.find('.//TIMEZONE').text datebase = parse(startdate + timezone) for pt in root.iter('LOCATION'): wp = Waypoint() delta = int(pt.get('elapsed_time')) formatedDate = datebase + timedelta(seconds=delta) wp.Timestamp = formatedDate #self._parseDate(formatedDate.isoformat()) wp.Location = Location() wp.Location.Latitude = float(pt.find('LATITUDE').text[:8]) wp.Location.Longitude = float(pt.find('LONGITUDE').text[:8]) activity.GPS = True wp.Location.Altitude = int(pt.find('ELEVATION').text[:8]) #get the HR value in the Datastream node and measures collection for hr in root.iter('MEASURE'): if pt.get('elapsed_time') == hr.get('elapsed_time'): for measureValue in hr.iter('VALUE'): if measureValue.get('id') == "1": wp.HR = int(measureValue.text) break break lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def _getActivity(self, serviceRecord, dbcl, path, base_activity=None): try: metadata, file = dbcl.files_download(path) except dropbox.exceptions.DropboxException as e: self._raiseDbException(e) try: if path.lower().endswith(".tcx"): act = TCXIO.Parse(file.content, base_activity) else: act = GPXIO.Parse(file.content, base_activity) except ValueError as e: raise APIExcludeActivity("Invalid GPX/TCX " + str(e), activity_id=path, user_exception=UserException(UserExceptionType.Corrupt)) except lxml.etree.XMLSyntaxError as e: raise APIExcludeActivity("LXML parse error " + str(e), activity_id=path, user_exception=UserException(UserExceptionType.Corrupt)) return act, metadata.rev
def DownloadActivityList(self, serviceRecord, exhaustive=False): #http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?&start=0&limit=50 session = self._get_session(record=serviceRecord) page = 1 pageSz = 100 activities = [] exclusions = [] while True: logger.debug("Req with " + str({ "start": (page - 1) * pageSz, "limit": pageSz })) self._rate_limit() retried_auth = False while True: res = session.get( "http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities", params={ "start": (page - 1) * pageSz, "limit": pageSz }) # It's 10 PM and I have no clue why it's throwing these errors, maybe we just need to log in again? if res.status_code == 403 and not retried_auth: retried_auth = True session = self._get_session(serviceRecord, skip_cache=True) else: break try: res = res.json()["results"] except ValueError: res_txt = res.text # So it can capture in the log message raise APIException("Parse failure in GC list resp: %s" % res.status_code) if "activities" not in res: break # No activities on this page - empty account. for act in res["activities"]: act = act["activity"] if "sumDistance" not in act: exclusions.append( APIExcludeActivity("No distance", activityId=act["activityId"], userException=UserException( UserExceptionType.Corrupt))) continue activity = UploadedActivity() # Don't really know why sumSampleCountTimestamp doesn't appear in swim activities - they're definitely timestamped... activity.Stationary = "sumSampleCountSpeed" not in act and "sumSampleCountTimestamp" not in act activity.GPS = "endLatitude" in act activity.Private = act["privacy"]["key"] == "private" try: activity.TZ = pytz.timezone(act["activityTimeZone"]["key"]) except pytz.exceptions.UnknownTimeZoneError: activity.TZ = pytz.FixedOffset( float(act["activityTimeZone"]["offset"]) * 60) logger.debug("Name " + act["activityName"]["value"] + ":") if len(act["activityName"]["value"].strip( )) and act["activityName"][ "value"] != "Untitled": # This doesn't work for internationalized accounts, oh well. activity.Name = act["activityName"]["value"] if len(act["activityDescription"]["value"].strip()): activity.Notes = act["activityDescription"]["value"] # beginTimestamp/endTimestamp is in UTC activity.StartTime = pytz.utc.localize( datetime.utcfromtimestamp( float(act["beginTimestamp"]["millis"]) / 1000)) if "sumElapsedDuration" in act: activity.EndTime = activity.StartTime + timedelta( 0, round(float(act["sumElapsedDuration"]["value"]))) elif "sumDuration" in act: activity.EndTime = activity.StartTime + timedelta( minutes=float(act["sumDuration"] ["minutesSeconds"].split(":")[0]), seconds=float(act["sumDuration"] ["minutesSeconds"].split(":")[1])) else: activity.EndTime = pytz.utc.localize( datetime.utcfromtimestamp( float(act["endTimestamp"]["millis"]) / 1000)) logger.debug("Activity s/t " + str(activity.StartTime) + " on page " + str(page)) activity.AdjustTZ() # TODO: fix the distance stats to account for the fact that this incorrectly reported km instead of meters for the longest time. activity.Stats.Distance = ActivityStatistic( self._unitMap[act["sumDistance"]["uom"]], value=float(act["sumDistance"]["value"])) activity.Type = self._resolveActivityType( act["activityType"]["key"]) activity.CalculateUID() activity.ServiceData = {"ActivityID": int(act["activityId"])} activities.append(activity) logger.debug("Finished page " + str(page) + " of " + str(res["search"]["totalPages"])) if not exhaustive or int(res["search"]["totalPages"]) == page: break else: page += 1 return activities, exclusions
def DownloadActivity(self, serviceRecord, activity): # activity might not be populated at this point, still possible to bail out if not activity.ServiceData["Tagged"]: if not (hasattr(serviceRecord, "Config") and "UploadUntagged" in serviceRecord.Config and serviceRecord.Config["UploadUntagged"]): raise APIExcludeActivity("Activity untagged", permanent=False, activity_id=activity.ServiceData["Path"], user_exception=UserException(UserExceptionType.Untagged)) path = activity.ServiceData["Path"] dbcl = self._getClient(serviceRecord) activity, rev = self._getActivity(serviceRecord, dbcl, path, base_activity=activity) # Dropbox doesn't support stationary activities yet. if activity.CountTotalWaypoints() <= 1: raise APIExcludeActivity("Too few waypoints", activity_id=path, user_exception=UserException(UserExceptionType.Corrupt)) return activity
def _downloadActivitySummary(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] session = self._get_session(record=serviceRecord) self._rate_limit() res = session.get( "http://connect.garmin.com/proxy/activity-service-1.3/json/activity/" + str(activityID)) try: raw_data = res.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (res.status_code, res.text)) stat_map = {} def mapStat(gcKey, statKey, type): stat_map[gcKey] = {"key": statKey, "attr": type} def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]["value"]) units = self._unitMap[gc_dict[gc_key]["uom"]] if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update( ActivityStatistic(units, **({ stat["attr"]: value }))) mapStat("SumMovingDuration", "MovingTime", "value") mapStat("SumDuration", "TimerTime", "value") mapStat("SumDistance", "Distance", "value") mapStat("MinSpeed", "Speed", "min") mapStat("MaxSpeed", "Speed", "max") mapStat("WeightedMeanSpeed", "Speed", "avg") mapStat("MinAirTemperature", "Temperature", "min") mapStat("MaxAirTemperature", "Temperature", "max") mapStat("WeightedMeanAirTemperature", "Temperature", "avg") mapStat("SumEnergy", "Energy", "value") mapStat("MaxHeartRate", "HR", "max") mapStat("WeightedMeanHeartRate", "HR", "avg") mapStat("MaxDoubleCadence", "RunCadence", "max") mapStat("WeightedMeanDoubleCadence", "RunCadence", "avg") mapStat("MaxBikeCadence", "Cadence", "max") mapStat("WeightedMeanBikeCadence", "Cadence", "avg") mapStat("MinPower", "Power", "min") mapStat("MaxPower", "Power", "max") mapStat("WeightedMeanPower", "Power", "avg") mapStat("MinElevation", "Elevation", "min") mapStat("MaxElevation", "Elevation", "max") mapStat("GainElevation", "Elevation", "gain") mapStat("LossElevation", "Elevation", "loss") applyStats(raw_data["activity"]["activitySummary"], activity.Stats) for lap_data in raw_data["activity"]["totalLaps"]["lapSummaryList"]: lap = Lap() if "BeginTimestamp" in lap_data: lap.StartTime = pytz.utc.localize( datetime.utcfromtimestamp( float(lap_data["BeginTimestamp"]["value"]) / 1000)) if "EndTimestamp" in lap_data: lap.EndTime = pytz.utc.localize( datetime.utcfromtimestamp( float(lap_data["EndTimestamp"]["value"]) / 1000)) elapsed_duration = None if "SumElapsedDuration" in lap_data: elapsed_duration = timedelta(seconds=round( float(lap_data["SumElapsedDuration"]["value"]))) elif "SumDuration" in lap_data: elapsed_duration = timedelta( seconds=round(float(lap_data["SumDuration"]["value"]))) if lap.StartTime and elapsed_duration: # Always recalculate end time based on duration, if we have the start time lap.EndTime = lap.StartTime + elapsed_duration if not lap.StartTime and lap.EndTime and elapsed_duration: # Sometimes calculate start time based on duration lap.StartTime = lap.EndTime - elapsed_duration if not lap.StartTime or not lap.EndTime: # Garmin Connect is weird. raise APIExcludeActivity( "Activity lap has no BeginTimestamp or EndTimestamp", userException=UserException(UserExceptionType.Corrupt)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
def Authorize(self, email, password): from tapiriik.auth.credential_storage import CredentialStore session = self._get_session(email=email, password=password, skip_cache=True) # TODO: http://connect.garmin.com/proxy/userprofile-service/socialProfile/ has the proper immutable user ID, not that anyone ever changes this one... self._rate_limit() username = session.get("http://connect.garmin.com/user/username").json()["username"] if not len(username): raise APIException("Unable to retrieve username", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) return (username, {}, {"Email": CredentialStore.Encrypt(email), "Password": CredentialStore.Encrypt(password)})
def DownloadActivityList(self, serviceRecord, exhaustive=False): oauthSession = self._oauthSession(serviceRecord) activities = [] exclusions = [] page_url = "https://api.endomondo.com/api/1/workouts" while True: resp = oauthSession.get(page_url) try: respList = resp.json()["data"] except ValueError: self._rateLimitBailout(resp) raise APIException("Error decoding activity list resp %s %s" % (resp.status_code, resp.text)) for actInfo in respList: activity = UploadedActivity() activity.StartTime = self._parseDate(actInfo["start_time"]) logger.debug("Activity s/t %s" % activity.StartTime) if "is_tracking" in actInfo and actInfo["is_tracking"]: exclusions.append( APIExcludeActivity( "Not complete", activity_id=actInfo["id"], permanent=False, user_exception=UserException( UserExceptionType.LiveTracking))) continue if "end_time" in actInfo: activity.EndTime = self._parseDate(actInfo["end_time"]) if actInfo["sport"] in self._activityMappings: activity.Type = self._activityMappings[actInfo["sport"]] # "duration" is timer time if "duration_total" in actInfo: activity.Stats.TimerTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=float(actInfo["duration_total"])) if "distance_total" in actInfo: activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Kilometers, value=float(actInfo["distance_total"])) if "calories_total" in actInfo: activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=float(actInfo["calories_total"])) activity.Stats.Elevation = ActivityStatistic( ActivityStatisticUnit.Meters) if "altitude_max" in actInfo: activity.Stats.Elevation.Max = float( actInfo["altitude_max"]) if "altitude_min" in actInfo: activity.Stats.Elevation.Min = float( actInfo["altitude_min"]) if "total_ascent" in actInfo: activity.Stats.Elevation.Gain = float( actInfo["total_ascent"]) if "total_descent" in actInfo: activity.Stats.Elevation.Loss = float( actInfo["total_descent"]) activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.KilometersPerHour) if "speed_max" in actInfo: activity.Stats.Speed.Max = float(actInfo["speed_max"]) if "heart_rate_avg" in actInfo: activity.Stats.HR = ActivityStatistic( ActivityStatisticUnit.BeatsPerMinute, avg=float(actInfo["heart_rate_avg"])) if "heart_rate_max" in actInfo: activity.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=float( actInfo["heart_rate_max"]))) if "cadence_avg" in actInfo: activity.Stats.Cadence = ActivityStatistic( ActivityStatisticUnit.RevolutionsPerMinute, avg=int(actInfo["cadence_avg"])) if "cadence_max" in actInfo: activity.Stats.Cadence.update( ActivityStatistic( ActivityStatisticUnit.RevolutionsPerMinute, max=int(actInfo["cadence_max"]))) if "title" in actInfo: activity.Name = actInfo["title"] activity.ServiceData = {"WorkoutID": int(actInfo["id"])} activity.CalculateUID() activities.append(activity) paging = resp.json()["paging"] if "next" not in paging or not paging["next"] or not exhaustive: break else: page_url = paging["next"] return activities, exclusions
def DownloadActivityList(self, svcRecord, exhaustive=False): ns = { "tpw": "http://www.trainingpeaks.com/TPWebServices/", "xsi": "http://www.w3.org/2001/XMLSchema-instance" } activities = [] exclusions = [] reqData = self._authData(svcRecord) limitDateFormat = "%d %B %Y" if exhaustive: listEnd = datetime.now() + timedelta( days=1.5) # Who knows which TZ it's in listStart = datetime(day=1, month=1, year=1980) # The beginning of time else: listEnd = datetime.now() + timedelta( days=1.5) # Who knows which TZ it's in listStart = listEnd - timedelta(days=20) # Doesn't really matter lastActivityDay = None discoveredWorkoutIds = [] while True: reqData.update({ "startDate": listStart.strftime(limitDateFormat), "endDate": listEnd.strftime(limitDateFormat) }) print("Requesting %s to %s" % (listStart, listEnd)) resp = requests.post( "https://www.trainingpeaks.com/tpwebservices/service.asmx/GetWorkoutsForAthlete", data=reqData) xresp = etree.XML(resp.content) for xworkout in xresp: activity = UploadedActivity() workoutId = xworkout.find("tpw:WorkoutId", namespaces=ns).text workoutDayEl = xworkout.find("tpw:WorkoutDay", namespaces=ns) startTimeEl = xworkout.find("tpw:StartTime", namespaces=ns) workoutDay = dateutil.parser.parse(workoutDayEl.text) startTime = dateutil.parser.parse( startTimeEl.text ) if startTimeEl is not None and startTimeEl.text else None if lastActivityDay is None or workoutDay.replace( tzinfo=None) > lastActivityDay: lastActivityDay = workoutDay.replace(tzinfo=None) if startTime is None: continue # Planned but not executed yet. activity.StartTime = startTime endTimeEl = xworkout.find("tpw:TimeTotalInSeconds", namespaces=ns) if not endTimeEl.text: exclusions.append( APIExcludeActivity("Activity has no duration", activityId=workoutId, userException=UserException( UserExceptionType.Corrupt))) continue activity.EndTime = activity.StartTime + timedelta( seconds=float(endTimeEl.text)) distEl = xworkout.find("tpw:DistanceInMeters", namespaces=ns) if distEl.text: activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=float(distEl.text)) # PWX is damn near comprehensive, no need to fill in any of the other statisitcs here, really if workoutId in discoveredWorkoutIds: continue # There's the possibility of query overlap, if there are multiple activities on a single day that fall across the query return limit discoveredWorkoutIds.append(workoutId) workoutTypeEl = xworkout.find("tpw:WorkoutTypeDescription", namespaces=ns) if workoutTypeEl.text: if workoutTypeEl.text == "Day Off": continue # TrainingPeaks has some weird activity types... if workoutTypeEl.text not in self._workoutTypeMappings: exclusions.append( APIExcludeActivity("Activity type %s unknown" % workoutTypeEl.text, activityId=workoutId, userException=UserException( UserExceptionType.Corrupt))) continue activity.Type = self._workoutTypeMappings[ workoutTypeEl.text] activity.ServiceData = {"WorkoutID": workoutId} activity.CalculateUID() activities.append(activity) if not exhaustive: break # Since TP only lets us query by date range, to get full activity history we need to query successively smaller ranges if len(xresp): if listStart == lastActivityDay: break # This wouldn't work if you had more than #MaxQueryReturn activities on that day - but that number is probably 50+ listStart = lastActivityDay else: break # We're done return activities, exclusions
def _rateLimitBailout(self, response): if response.status_code == 503 and "user_refused" in response.text: raise APIException("Endomondo user token rate limit reached", user_exception=UserException( UserExceptionType.RateLimited))
def UploadActivity(self, serviceRecord, activity): """ POST a Multipart-Encoded File URL: http://app.velohero.com/upload/file Parameters: user = username pass = password view = json file = multipart-encodes file (fit, tcx, pwx, gpx, srm, hrm...) Maximum file size per file is 16 MB. """ has_location = has_distance = has_speed = False for lap in activity.Laps: for wp in lap.Waypoints: if wp.Location and wp.Location.Latitude and wp.Location.Longitude: has_location = True if wp.Distance: has_distance = True if wp.Speed: has_speed = True if has_location and has_distance and has_speed: format = "fit" data = FITIO.Dump(activity) elif has_location and has_distance: format = "tcx" data = TCXIO.Dump(activity) elif has_location: format = "gpx" data = GPXIO.Dump(activity) else: format = "fit" data = FITIO.Dump(activity) # Upload files = { "file": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + "." + format, data) } params = self._add_auth_params({"view": "json"}, record=serviceRecord) res = requests.post(self._urlRoot + "/upload/file", files=files, params=params) if res.status_code != 200: if res.status_code == 403: raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to upload activity") res.raise_for_status() try: res = res.json() except ValueError: raise APIException("Could not decode activity list") if "error" in res: raise APIException(res["error"]) # Set date, start time, comment and sport if "id" in res: workoutId = res["id"] params = self._add_auth_params( { "workout_date": activity.StartTime.strftime("%Y-%m-%d"), "workout_start_time": activity.StartTime.strftime("%H:%M:%S"), "workout_comment": activity.Notes, "sport_id": self._activityMappings[activity.Type], "workout_hide": "yes" if activity.Private else "no" }, record=serviceRecord) res = requests.get(self._urlRoot + "/workouts/change/{}".format(workoutId), params=params) if res.status_code != 200: if res.status_code == 403: raise APIException( "No authorization to change activity with workout ID: {}" .format(workoutId), block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException( "Unable to change activity with workout ID: {}".format( workoutId)) return workoutId
def DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] before = earliestDate = None while True: if before is not None and before < 0: break # Caused by activities that "happened" before the epoch. We generally don't care about those activities... logger.debug("Req with before=" + str(before) + "/" + str(earliestDate)) self._globalRateLimit() resp = requests.get("https://www.strava.com/api/v3/athletes/" + str(svcRecord.ExternalID) + "/activities", headers=self._apiHeaders(svcRecord), params={"before": before}) if resp.status_code == 401: raise APIException( "No authorization to retrieve activity list", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) earliestDate = None reqdata = resp.json() if not len(reqdata): break # No more activities to see for ride in reqdata: activity = UploadedActivity() activity.TZ = pytz.timezone( re.sub("^\([^\)]+\)\s*", "", ride["timezone"]) ) # Comes back as "(GMT -13:37) The Stuff/We Want"" activity.StartTime = pytz.utc.localize( datetime.strptime(ride["start_date"], "%Y-%m-%dT%H:%M:%SZ")) logger.debug("\tActivity s/t %s: %s" % (activity.StartTime, ride["name"])) if not earliestDate or activity.StartTime < earliestDate: earliestDate = activity.StartTime before = calendar.timegm( activity.StartTime.astimezone(pytz.utc).timetuple()) activity.EndTime = activity.StartTime + timedelta( 0, ride["elapsed_time"]) activity.ServiceData = { "ActivityID": ride["id"], "Manual": ride["manual"] } if ride["type"] not in self._reverseActivityTypeMappings: exclusions.append( APIExcludeActivity("Unsupported activity type %s" % ride["type"], activityId=ride["id"], userException=UserException( UserExceptionType.Other))) logger.debug("\t\tUnknown activity") continue activity.Type = self._reverseActivityTypeMappings[ride["type"]] activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=ride["distance"]) if "max_speed" in ride or "average_speed" in ride: activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.MetersPerSecond, avg=ride["average_speed"] if "average_speed" in ride else None, max=ride["max_speed"] if "max_speed" in ride else None) activity.Stats.MovingTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=ride["moving_time"] if "moving_time" in ride and ride["moving_time"] > 0 else None ) # They don't let you manually enter this, and I think it returns 0 for those activities. # Strava doesn't handle "timer time" to the best of my knowledge - although they say they do look at the FIT total_timer_time field, so...? if "average_watts" in ride: activity.Stats.Power = ActivityStatistic( ActivityStatisticUnit.Watts, avg=ride["average_watts"]) if "average_heartrate" in ride: activity.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=ride["average_heartrate"])) if "max_heartrate" in ride: activity.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=ride["max_heartrate"])) if "average_cadence" in ride: activity.Stats.Cadence.update( ActivityStatistic( ActivityStatisticUnit.RevolutionsPerMinute, avg=ride["average_cadence"])) if "average_temp" in ride: activity.Stats.Temperature.update( ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, avg=ride["average_temp"])) if "calories" in ride: activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=ride["calories"]) activity.Name = ride["name"] activity.Private = ride["private"] activity.Stationary = ride["manual"] activity.GPS = ("start_latlng" in ride) and (ride["start_latlng"] is not None) activity.AdjustTZ() activity.CalculateUID() activities.append(activity) if not exhaustive or not earliestDate: break return activities, exclusions
def DownloadActivityList(self, serviceRecord, exhaustive=False): headers = self._getAuthHeaders(serviceRecord) activities = [] exclusions = [] pageUri = self.OpenFitEndpoint + "/fitnessActivities.json" activity_tz_cache_raw = cachedb.sporttracks_meta_cache.find_one( {"ExternalID": serviceRecord.ExternalID}) activity_tz_cache_raw = activity_tz_cache_raw if activity_tz_cache_raw else { "Activities": [] } activity_tz_cache = dict([(x["ActivityURI"], x["TZ"]) for x in activity_tz_cache_raw["Activities"] ]) while True: logger.debug("Req against " + pageUri) res = requests.get(pageUri, headers=headers) try: res = res.json() except ValueError: raise APIException( "Could not decode activity list response %s %s" % (res.status_code, res.text)) for act in res["items"]: activity = UploadedActivity() activity.ServiceData = {"ActivityURI": act["uri"]} if len(act["name"].strip()): activity.Name = act["name"] # Longstanding ST.mobi bug causes it to return negative partial-hour timezones as "-2:-30" instead of "-2:30" fixed_start_time = re.sub(r":-(\d\d)", r":\1", act["start_time"]) activity.StartTime = dateutil.parser.parse(fixed_start_time) if isinstance(activity.StartTime.tzinfo, tzutc): activity.TZ = pytz.utc # The dateutil tzutc doesn't have an _offset value. else: activity.TZ = pytz.FixedOffset( activity.StartTime.tzinfo.utcoffset( activity.StartTime).total_seconds() / 60 ) # Convert the dateutil lame timezones into pytz awesome timezones. activity.StartTime = activity.StartTime.replace( tzinfo=activity.TZ) activity.EndTime = activity.StartTime + timedelta( seconds=float(act["duration"])) activity.Stats.TimerTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=float(act["duration"] )) # OpenFit says this excludes paused times. # Sometimes activities get returned with a UTC timezone even when they are clearly not in UTC. if activity.TZ == pytz.utc: if act["uri"] in activity_tz_cache: activity.TZ = pytz.FixedOffset( activity_tz_cache[act["uri"]]) else: # So, we get the first location in the activity and calculate the TZ from that. try: firstLocation = self._downloadActivity( serviceRecord, activity, returnFirstLocation=True) except APIExcludeActivity: pass else: try: activity.CalculateTZ(firstLocation, recalculate=True) except: # We tried! pass else: activity.AdjustTZ() finally: activity_tz_cache[ act["uri"]] = activity.StartTime.utcoffset( ).total_seconds() / 60 logger.debug("Activity s/t " + str(activity.StartTime)) activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=float(act["total_distance"])) types = [x.strip().lower() for x in act["type"].split(":")] types.reverse( ) # The incoming format is like "walking: hiking" and we want the most specific first activity.Type = None for type_key in types: if type_key in self._activityMappings: activity.Type = self._activityMappings[type_key] break if not activity.Type: exclusions.append( APIExcludeActivity("Unknown activity type %s" % act["type"], activity_id=act["uri"], user_exception=UserException( UserExceptionType.Other))) continue activity.CalculateUID() activities.append(activity) if not exhaustive or "next" not in res or not len(res["next"]): break else: pageUri = res["next"] logger.debug("Writing back meta cache") cachedb.sporttracks_meta_cache.update( {"ExternalID": serviceRecord.ExternalID}, { "ExternalID": serviceRecord.ExternalID, "Activities": [{ "ActivityURI": k, "TZ": v } for k, v in activity_tz_cache.items()] }, upsert=True) return activities, exclusions
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData[ "Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [ Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats) ] return activity activityID = activity.ServiceData["ActivityID"] self._globalRateLimit() streamdata = requests.get( "https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata[ "message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap( stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime ) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) if "latlng" in ridedata: latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if hasAltitude: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] lap.Waypoints.append(waypoint) return activity
def _safe_call(self, serviceRecord, method, endpoint, params={}, files=None, retry_count=3): resp = None for i in range(0, retry_count): try: session = self._get_session(serviceRecord) if method == "get": resp = session.get(endpoint, params=self._with_auth(serviceRecord, params)) elif method == "post": resp = session.post(endpoint, data=self._with_auth(serviceRecord, params), files=files) else: raise APIException("Unsupported method: {}".format(method), user_exception=UserException(UserExceptionType.Other)) if resp.status_code == 200: # For some reason aerobia api always return 200 instead of 401 if "/users/sign_up" not in resp.text and "неверный ключ аутентификации" not in resp.text.lower(): break # most likely token or session expired. self._refresh_token(serviceRecord) except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout) as ex: # Aerobia sometimes answer like # "Remote end closed connection without response" # "Failed to establish a new connection: [WinError 10060]" # wait a bit and retry time.sleep(.2) if resp is None: raise APIException("Connectivity issues") return resp
def _raiseDbException(self, e): if type(e) is dropbox.exceptions.AuthError: raise APIException("Authorization error - %s" % e, block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) if hasattr(e, "error") and getattr(e.error, "insufficient_space", None): raise APIException("Dropbox quota error", block=True, user_exception=UserException(UserExceptionType.AccountFull, intervention_required=True)) raise APIException("API failure - %s" % e)
def UploadActivity(self, serviceRecord, activity): logger.info("Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) if self.LastUpload is not None: while (datetime.now() - self.LastUpload).total_seconds() < 5: time.sleep(1) logger.debug("Inter-upload cooldown") source_svc = None if hasattr(activity, "ServiceDataCollection"): source_svc = str(list(activity.ServiceDataCollection.keys())[0]) upload_id = None if activity.CountTotalWaypoints(): req = { "data_type": "fit", "activity_name": activity.Name, "description": activity.Notes, # Paul Mach said so. "activity_type": self._activityTypeMappings[activity.Type], "private": 1 if activity.Private else 0} if "fit" in activity.PrerenderedFormats: logger.debug("Using prerendered FIT") fitData = activity.PrerenderedFormats["fit"] else: # TODO: put the fit back into PrerenderedFormats once there's more RAM to go around and there's a possibility of it actually being used. fitData = FITIO.Dump(activity) files = {"file":("tap-sync-" + activity.UID + "-" + str(os.getpid()) + ("-" + source_svc if source_svc else "") + ".fit", fitData)} response = requests.post("http://www.strava.com/api/v3/uploads", data=req, files=files, headers=self._apiHeaders(serviceRecord)) if response.status_code != 201: if response.status_code == 401: raise APIException("No authorization to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code), block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) if "duplicate of activity" in response.text: logger.debug("Duplicate") self.LastUpload = datetime.now() return # Fine by me. The majority of these cases were caused by a dumb optimization that meant existing activities on services were never flagged as such if tapiriik didn't have to synchronize them elsewhere. raise APIException("Unable to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code)) upload_id = response.json()["id"] while not response.json()["activity_id"]: time.sleep(5) response = requests.get("http://www.strava.com/api/v3/uploads/%s" % upload_id, headers=self._apiHeaders(serviceRecord)) logger.debug("Waiting for upload - status %s id %s" % (response.json()["status"], response.json()["activity_id"])) if response.json()["error"]: error = response.json()["error"] if "duplicate of activity" in error: self.LastUpload = datetime.now() logger.debug("Duplicate") return # I guess we're done here? raise APIException("Strava failed while processing activity - last status %s" % response.text) upload_id = response.json()["activity_id"] else: localUploadTS = activity.StartTime.strftime("%Y-%m-%d %H:%M:%S") req = { "name": activity.Name if activity.Name else activity.StartTime.strftime("%d/%m/%Y"), # This is required "description": activity.Notes, "type": self._activityTypeMappings[activity.Type], "private": 1 if activity.Private else 0, "start_date_local": localUploadTS, "distance": activity.Stats.Distance.asUnits(ActivityStatisticUnit.Meters).Value, "elapsed_time": round((activity.EndTime - activity.StartTime).total_seconds()) } headers = self._apiHeaders(serviceRecord) response = requests.post("https://www.strava.com/api/v3/activities", data=req, headers=headers) # FFR this method returns the same dict as the activity listing, as REST services are wont to do. if response.status_code != 201: if response.status_code == 401: raise APIException("No authorization to upload activity " + activity.UID + " response " + response.text + " status " + str(response.status_code), block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to upload stationary activity " + activity.UID + " response " + response.text + " status " + str(response.status_code)) upload_id = response.json()["id"] self.LastUpload = datetime.now() return upload_id
def Authorize(self, email, password): from tapiriik.auth.credential_storage import CredentialStore cookies = self._get_cookies(email=email, password=password) username = requests.get("http://connect.garmin.com/user/username", cookies=cookies).json()["username"] if not len(username): raise APIException("Unable to retrieve username", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) return (username, {}, {"Email": CredentialStore.Encrypt(email), "Password": CredentialStore.Encrypt(password)})