def RevokeAuthorization(self, serviceRecord): resp = requests.post("https://runkeeper.com/apps/de-authorize", data={"access_token": serviceRecord.Authorization["Token"]}) if resp.status_code != 204 and resp.status_code != 200: raise APIException("Unable to deauthorize RK auth token, status " + str(resp.status_code) + " resp " + resp.text) pass
def UploadActivity(self, serviceRecord, activity): session = self._oauthSession(serviceRecord) device_id = self._deviceId(serviceRecord) if not serviceRecord.GetConfiguration()["DeviceRegistered"]: device_info = { "name": "tapiriik", "vendor": "tapiriik", "model": "tapiriik", "os": "tapiriik", "os_version": "1", "app_variant": "tapiriik", "app_version": "1" } device_add_resp = session.post( "https://api.endomondo.com/api/1/device/%s" % device_id, data=json.dumps(device_info)) if device_add_resp.status_code != 200: self._rateLimitBailout(device_add_resp) raise APIException( "Could not add device %s %s" % (device_add_resp.status_code, device_add_resp.text)) serviceRecord.SetConfiguration({"DeviceRegistered": True}) activity_id = "tap-" + activity.UID + "-" + str(os.getpid()) sport = self._getSport(activity) upload_data = { "device_id": device_id, "sport": sport, "start_time": self._formatDate(activity.StartTime), "end_time": self._formatDate(activity.EndTime), "points": [] } if activity.Name: upload_data["title"] = activity.Name if activity.Notes: upload_data["notes"] = activity.Notes if activity.Stats.Distance.Value is not None: upload_data["distance_total"] = activity.Stats.Distance.asUnits( ActivityStatisticUnit.Kilometers).Value if activity.Stats.TimerTime.Value is not None: upload_data["duration_total"] = activity.Stats.TimerTime.asUnits( ActivityStatisticUnit.Seconds).Value elif activity.Stats.MovingTime.Value is not None: upload_data["duration_total"] = activity.Stats.MovingTime.asUnits( ActivityStatisticUnit.Seconds).Value else: upload_data["duration_total"] = ( activity.EndTime - activity.StartTime).total_seconds() if activity.Stats.Energy.Value is not None: upload_data["calories_total"] = activity.Stats.Energy.asUnits( ActivityStatisticUnit.Kilocalories).Value elev_stats = activity.Stats.Elevation.asUnits( ActivityStatisticUnit.Meters) if elev_stats.Max is not None: upload_data["altitude_max"] = elev_stats.Max if elev_stats.Min is not None: upload_data["altitude_min"] = elev_stats.Min if elev_stats.Gain is not None: upload_data["total_ascent"] = elev_stats.Gain if elev_stats.Loss is not None: upload_data["total_descent"] = elev_stats.Loss speed_stats = activity.Stats.Speed.asUnits( ActivityStatisticUnit.KilometersPerHour) if speed_stats.Max is not None: upload_data["speed_max"] = speed_stats.Max hr_stats = activity.Stats.HR.asUnits( ActivityStatisticUnit.BeatsPerMinute) if hr_stats.Average is not None: upload_data["heart_rate_avg"] = hr_stats.Average if hr_stats.Max is not None: upload_data["heart_rate_max"] = hr_stats.Max if activity.Stats.Cadence.Average is not None: upload_data["cadence_avg"] = activity.Stats.Cadence.asUnits( ActivityStatisticUnit.RevolutionsPerMinute).Average elif activity.Stats.RunCadence.Average is not None: upload_data["cadence_avg"] = activity.Stats.RunCadence.asUnits( ActivityStatisticUnit.StepsPerMinute).Average if activity.Stats.Cadence.Max is not None: upload_data["cadence_max"] = activity.Stats.Cadence.asUnits( ActivityStatisticUnit.RevolutionsPerMinute).Max elif activity.Stats.RunCadence.Max is not None: upload_data["cadence_max"] = activity.Stats.RunCadence.asUnits( ActivityStatisticUnit.StepsPerMinute).Max if activity.Stats.Power.Average is not None: upload_data["power_avg"] = activity.Stats.Power.asUnits( ActivityStatisticUnit.Watts).Average if activity.Stats.Power.Max is not None: upload_data["power_max"] = activity.Stats.Power.asUnits( ActivityStatisticUnit.Watts).Max for wp in activity.GetFlatWaypoints(): pt = { "time": self._formatDate(wp.Timestamp), } if wp.Location: if wp.Location.Latitude is not None and wp.Location.Longitude is not None: pt["lat"] = wp.Location.Latitude pt["lng"] = wp.Location.Longitude if wp.Location.Altitude is not None: pt["alt"] = wp.Location.Altitude if wp.HR is not None: pt["hr"] = round(wp.HR) if wp.Cadence is not None: pt["cad"] = round(wp.Cadence) elif wp.RunCadence is not None: pt["cad"] = round(wp.RunCadence) if wp.Power is not None: pt["pow"] = round(wp.Power) if wp.Type == WaypointType.Pause: pt["inst"] = "pause" elif wp.Type == WaypointType.Resume: pt["inst"] = "resume" upload_data["points"].append(pt) if len(upload_data["points"]): upload_data["points"][0]["inst"] = "start" upload_data["points"][-1]["inst"] = "stop" upload_resp = session.post( "https://api.endomondo.com/api/1/workouts/%s" % activity_id, data=json.dumps(upload_data)) if upload_resp.status_code != 200: self._rateLimitBailout(upload_resp) raise APIException("Could not upload activity %s %s" % (upload_resp.status_code, upload_resp.text)) return upload_resp.json()["id"]
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(self.ApiEndpoint + "/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): #/proxy/upload-service-1.1/json/upload/.fit fit_file = FITIO.Dump(activity) files = { "data": ("tap-sync-" + str(os.getpid()) + "-" + activity.UID + ".fit", fit_file) } res = self._request_with_reauth( serviceRecord, lambda session: session.post( "https://connect.garmin.com/proxy/upload-service-1.1/json/upload/.fit", files=files)) res = res.json()["detailedImportResult"] if len(res["successes"]) == 0: if len(res["failures"]) and len( res["failures"][0]["messages"]) and res["failures"][0][ "messages"][0]["content"] == "Duplicate activity": logger.debug("Duplicate") return # ...cool? raise APIException("Unable to upload activity %s" % res) if len(res["successes"]) > 1: raise APIException( "Uploaded succeeded, resulting in too many activities") actid = res["successes"][0]["internalId"] name = activity.Name # Capture in logs notes = activity.Notes # Update activity metadata not included in the FIT file. metadata_object = {} if activity.Name and activity.Name.strip(): metadata_object["activityName"] = activity.Name if activity.Notes and activity.Notes.strip(): metadata_object["description"] = activity.Notes if activity.Type not in [ ActivityType.Running, ActivityType.Cycling, ActivityType.Other ]: # Set the legit activity type - whatever it is, it's not supported by the FIT schema acttype = [ k for k, v in self._reverseActivityMappings.items() if v == activity.Type ] if len(acttype) == 0: raise APIWarning( "GarminConnect does not support activity type " + activity.Type) else: acttype = acttype[0] metadata_object["activityTypeDTO"] = {"typeKey": acttype} if activity.Private: metadata_object["accessControlRuleDTO"] = {"typeKey": "private"} if metadata_object: metadata_object["activityId"] = actid encoding_headers = { "Content-Type": "application/json; charset=UTF-8" } # GC really, really needs this part, otherwise it throws obscure errors like "Invalid signature for signature method HMAC-SHA1" res = self._request_with_reauth( serviceRecord, lambda session: session. put("https://connect.garmin.com/proxy/activity-service/activity/" + str(actid), data=json.dumps(metadata_object), headers=encoding_headers)) if res.status_code != 204: raise APIWarning("Unable to set activity metadata - %d %s" % (res.status_code, res.text)) return actid
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 "power_avg" in actInfo: activity.Stats.Power = ActivityStatistic( ActivityStatisticUnit.Watts, avg=int(actInfo["power_avg"])) if "power_max" in actInfo: activity.Stats.Power.update( ActivityStatistic(ActivityStatisticUnit.Watts, max=int(actInfo["power_max"]))) if "title" in actInfo: activity.Name = actInfo["title"] activity.ServiceData = { "WorkoutID": int(actInfo["id"]), "Sport": actInfo["sport"] } 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 _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)) 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"] res = self._request_with_reauth( serviceRecord, lambda session: session. get("https://connect.garmin.com/modern/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", 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 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)) 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 try: reqdata = resp.json() except ValueError: raise APIException("Failed parsing strava list response %s - %s" % (resp.status_code, resp.text)) 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"], activity_id=ride["id"], user_exception=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 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, drop_pauses=True) files = {"file":("tap-sync-" + activity.UID + "-" + str(os.getpid()) + ("-" + source_svc if source_svc else "") + ".fit", fitData)} response = requests.post("https://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"] upload_poll_wait = 8 # The mode of processing times while not response.json()["activity_id"]: time.sleep(upload_poll_wait) response = requests.get("https://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 _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 RevokeAuthorization(self, serviceRecord): resp = requests.post("https://www.strava.com/oauth/deauthorize", headers=self._apiHeaders(serviceRecord)) if resp.status_code != 204 and resp.status_code != 200: raise APIException("Unable to deauthorize Strava auth token, status " + str(resp.status_code) + " resp " + resp.text) pass
def DownloadActivityList(self, serviceRecord, exhaustive=False): logger.debug("Checking motivato premium state") self._applyPaymentState(serviceRecord) logger.debug("Motivato DownloadActivityList") session = self._get_session(record=serviceRecord) activities = [] exclusions = [] self._rate_limit() retried_auth = False #headers = {'X-App-With-Tracks': "true"} headers = {} res = session.post(self._urlRoot + "/api/workouts/sync", headers=headers) if res.status_code == 403 and not retried_auth: retried_auth = True session = self._get_session(serviceRecord, skip_cache=True) try: respList = res.json() except ValueError: res_txt = res.text # So it can capture in the log message raise APIException("Parse failure in Motivato list resp: %s" % res.status_code) for actInfo in respList: if "duration" in actInfo: duration = self._durationToSeconds(actInfo["duration"]) else: continue activity = UploadedActivity() if "time_start" in actInfo["metas"]: startTimeStr = actInfo["training_at"] + " " + actInfo["metas"][ "time_start"] else: startTimeStr = actInfo["training_at"] + " 00:00:00" activity.StartTime = self._parseDateTime(startTimeStr) activity.EndTime = self._parseDateTime(startTimeStr) + timedelta( seconds=duration) activity.Type = self._reverseActivityMappings[ actInfo["discipline_id"]] activity.Stats.TimerTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=duration) if "distance" in actInfo: activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Kilometers, value=float(actInfo["distance"])) #activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.KilometersPerSecond, value=1.0/float(actInfo["metas"]["pace"])) activity.ServiceData = {"WorkoutID": int(actInfo["id"])} activity.CalculateUID() logger.debug("Generated UID %s" % activity.UID) activities.append(activity) return activities, exclusions
def UploadActivity(self, serviceRecord, activity): logger.debug("Motivato UploadActivity") session = self._get_session(record=serviceRecord) dic = dict(training_at=activity.StartTime.strftime("%Y-%m-%d"), distance=activity.Stats.Distance.asUnits( ActivityStatisticUnit.Kilometers).Value, duration="", user_comment=activity.Notes, updated_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), created_at=activity.StartTime.strftime("%Y-%m-%d %H:%M:%S"), discipline_id=self._activityMappings[activity.Type], source_id=8, metas=dict( distance=activity.Stats.Distance.asUnits( ActivityStatisticUnit.Kilometers).Value, duration="", time_start=activity.StartTime.strftime("%H:%M:%S")), track={}) if activity.Stats.TimerTime.Value is not None: secs = activity.Stats.TimerTime.asUnits( ActivityStatisticUnit.Seconds).Value elif activity.Stats.MovingTime.Value is not None: secs = activity.Stats.MovingTime.asUnits( ActivityStatisticUnit.Seconds).Value else: secs = (activity.EndTime - activity.StartTime).total_seconds() dic["metas"]["duration"] = str(timedelta(seconds=secs)) dic["duration"] = str(timedelta(seconds=secs)) pace = str(timedelta(seconds=secs / activity.Stats.Distance.Value)) meta_hr_avg = activity.Stats.HR.Average meta_hr_max = activity.Stats.HR.Max if pace: dic["metas"]["pace"] = pace if meta_hr_avg: dic["metas"]["meta_hr_avg"] = meta_hr_avg if meta_hr_max: dic["metas"]["meta_hr_max"] = meta_hr_max if len(activity.Laps) > 0: dic["track"] = dict(name=activity.Name, mtime=secs, points=[]) for tk in activity.Laps: for wpt in tk.Waypoints: pt = dict( lat=wpt.Location.Latitude, lon=wpt.Location.Longitude, ele=wpt.Location.Altitude, bpm=wpt.HR, moment=wpt.Timestamp.strftime('%Y-%m-%d %H:%M:%S')) if wpt.Speed and wpt.Speed != None and wpt.Speed != 0: pt["pace"] = (1000.0 / wpt.Speed) dic["track"]["points"].append(pt) toSend = json.dumps(dic) try: res = session.post(self._urlRoot + "/api/workout", data=toSend) except APIWarning as e: raise APIException(str(e)) if res.status_code != 201: raise APIException("Activity didn't upload: %s, %s" % (res.status_code, res.text)) try: retJson = res.json() except ValueError: raise APIException("Activity upload parse error for %s, %s" % (res.status_code, res.text)) return retJson["id"]
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 = self._requestWithAuth( lambda session: session. get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng,distance,velocity_smooth" ), 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 hasDistance = "distance" in ridedata and len(ridedata["distance"]) > 0 hasVelocity = "velocity_smooth" in ridedata and len( ridedata["velocity_smooth"]) > 0 if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) inPause = False 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]) # When pausing, Strava sends this format: # idx = 100 ; time = 1000; moving = true # idx = 101 ; time = 1001; moving = true => convert to Pause # idx = 102 ; time = 2001; moving = false => convert to Resume: (2001-1001) seconds pause # idx = 103 ; time = 2002; moving = true if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif idx < waypointCt - 2 and ridedata["moving"][idx + 1] and inPause: waypoint.Type = WaypointType.Resume inPause = False elif idx < waypointCt - 2 and not ridedata["moving"][ idx + 1] and not inPause: waypoint.Type = WaypointType.Pause inPause = True 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] if hasVelocity: waypoint.Speed = ridedata["velocity_smooth"][idx] if hasDistance: waypoint.Distance = ridedata["distance"][idx] lap.Waypoints.append(waypoint) return activity
def DownloadActivity(self, svcRecord, activity): # thanks to Cosmo Catalano for the API reference code activityID = [ x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == svcRecord ][0] streamdata = requests.get( "https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,watts_calc,temp,resting,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)) streamdata = streamdata.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"] activity.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 hasRestingData = "resting" in ridedata and len(ridedata["resting"]) > 0 moving = True if "error" in ridedata: 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 hasRestingData and not moving and ridedata["resting"][ idx] is False: waypoint.Type = WaypointType.Resume moving = True elif hasRestingData and ridedata["resting"][idx] is True: 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] activity.Waypoints.append(waypoint) if not hasLocation: raise APIExcludeActivity("No waypoints with location", activityId=activityID) return activity
def raise_api_exception(): raise APIException( "Token expired or revoked", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True))
def UploadActivity(self, serviceRecord, activity): logger.info("Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) req = { "id": 0, "data_type": "tcx", "external_id": "tap-sync-" + str(os.getpid()) + "-" + activity.UID + "-" + activity.UploadedTo[0]["Connection"].Service.ID, "activity_name": activity.Name, "activity_type": self._activityTypeMappings[activity.Type], "private": activity.Private } if "tcx" in activity.PrerenderedFormats: logger.debug("Using prerendered TCX") tcxData = activity.PrerenderedFormats["tcx"] else: activity.EnsureTZ() tcxData = TCXIO.Dump(activity) # TODO: put the tcx back into PrerenderedFormats once there's more RAM to go around and there's a possibility of it actually being used. files = {"file": (req["external_id"] + ".tcx", tcxData)} 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)) 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(1) 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: logger.debug("Duplicate") return # I guess we're done here? raise APIException( "Strava failed while processing activity - last status %s" % response.text)
def DownloadActivityList(self, serviceRecord, exhaustive=False): activities = [] session = self._get_session(record=serviceRecord) session.headers.update({"Accept": "application/json"}) workouts_resp = session.get( "https://api.trainerroad.com/api/careerworkouts") if workouts_resp.status_code != 200: if workouts_resp.status_code == 401: raise APIException("Invalid login", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException("Workout listing error") cached_record = cachedb.trainerroad_meta.find_one( {"ExternalID": serviceRecord.ExternalID}) if not cached_record: cached_workout_meta = {} else: cached_workout_meta = cached_record["Workouts"] workouts = workouts_resp.json() for workout in workouts: # Un/f their API doesn't provide the start/end times in the list response # So we need to pull the extra data, if it's not already cached workout_id = str(workout["Id"]) # Mongo doesn't do non-string keys if workout_id not in cached_workout_meta: meta_resp = session.get( "https://api.trainerroad.com/api/careerworkouts?guid=%s" % workout["Guid"]) # We don't need everything full_meta = meta_resp.json() meta = { key: full_meta[key] for key in [ "WorkoutDate", "WorkoutName", "WorkoutNotes", "TotalMinutes", "TotalKM", "AvgWatts", "Kj" ] } cached_workout_meta[workout_id] = meta else: meta = cached_workout_meta[workout_id] activity = UploadedActivity() activity.ServiceData = {"ID": int(workout_id)} activity.Name = meta["WorkoutName"] activity.Notes = meta["WorkoutNotes"] activity.Type = ActivityType.Cycling # Everything's in UTC activity.StartTime = dateutil.parser.parse( meta["WorkoutDate"]).replace(tzinfo=pytz.utc) activity.EndTime = activity.StartTime + timedelta( minutes=meta["TotalMinutes"]) activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Kilometers, value=meta["TotalKM"]) activity.Stats.Power = ActivityStatistic( ActivityStatisticUnit.Watts, avg=meta["AvgWatts"]) activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilojoules, value=meta["Kj"]) activity.Stationary = False activity.GPS = False activity.CalculateUID() activities.append(activity) cachedb.trainerroad_meta.update( {"ExternalID": serviceRecord.ExternalID}, { "ExternalID": serviceRecord.ExternalID, "Workouts": cached_workout_meta }, upsert=True) return activities, []
def DownloadActivityList(self, serviceRecord, exhaustive=False): #http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?&start=0&limit=50 page = 1 pageSz = 100 activities = [] exclusions = [] while True: logger.debug("Req with " + str({ "start": (page - 1) * pageSz, "limit": pageSz })) res = self._request_with_reauth( serviceRecord, lambda session: session.get( "https://connect.garmin.com/modern/proxy/activity-search-service-1.0/json/activities", params={ "start": (page - 1) * pageSz, "limit": pageSz })) 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 - %s" % (res.status_code, res.text)) if "activities" not in res: break # No activities on this page - empty account. for act in res["activities"]: act = act["activity"] 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() if "sumDistance" in act and float( act["sumDistance"]["value"]) != 0: activity.Stats.Distance = ActivityStatistic( self._unitMap[act["sumDistance"]["uom"]], value=float(act["sumDistance"]["value"])) if "device" in act and act["device"]["key"] != "unknown": devId = DeviceIdentifier.FindMatchingIdentifierOfType( DeviceIdentifierType.GC, {"Key": act["device"]["key"]}) ver_split = act["device"]["key"].split(".") ver_maj = None ver_min = None if len(ver_split) == 4: # 2.90.0.0 ver_maj = int(ver_split[0]) ver_min = int(ver_split[1]) activity.Device = Device(devId, verMaj=ver_maj, verMin=ver_min) 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 DownloadActivityList(self, serviceRecord, exhaustive=False): #https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?limit=20&start=0 page = 1 pageSz = 100 activities = [] exclusions = [] while True: logger.debug("Req with " + str({ "start": (page - 1) * pageSz, "limit": pageSz })) res = self._request_with_reauth( lambda session: session.get( "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities", params={ "start": (page - 1) * pageSz, "limit": pageSz }), serviceRecord) try: res = res.json() except ValueError: res_txt = res.text # So it can capture in the log message raise APIException("Parse failure in GC list resp: %s - %s" % (res.status_code, res_txt)) for act in res: activity = UploadedActivity() # stationary activities have movingDuration = None while non-gps static activities have 0.0 activity.Stationary = act["movingDuration"] is None activity.GPS = act["hasPolyline"] activity.Private = act["privacy"]["typeKey"] == "private" activity_name = act["activityName"] logger.debug("Name " + activity_name if activity_name is not None else "Untitled" + ":") if activity_name is not None and len( activity_name.strip() ) and activity_name != "Untitled": # This doesn't work for internationalized accounts, oh well. activity.Name = activity_name activity_description = act["description"] if activity_description is not None and len( activity_description.strip()): activity.Notes = activity_description activity.StartTime = pytz.utc.localize( datetime.strptime(act["startTimeGMT"], "%Y-%m-%d %H:%M:%S")) if act["elapsedDuration"] is not None: activity.EndTime = activity.StartTime + timedelta( 0, float(act["elapsedDuration"]) / 1000) elif act["duration"] is not None: activity.EndTime = activity.StartTime + timedelta( 0, float(act["duration"])) else: # somehow duration is not defined. Set 1 second then. activity.EndTime = activity.StartTime + timedelta(0, 1) logger.debug("Activity s/t " + str(activity.StartTime) + " on page " + str(page)) if "distance" in act and act["distance"] and float( act["distance"]) != 0: activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=float(act["distance"])) activity.Type = self._resolveActivityType( act["activityType"]["typeKey"]) activity.CalculateUID() activity.ServiceData = {"ActivityID": int(act["activityId"])} activities.append(activity) logger.debug("Finished page " + str(page)) if not exhaustive or len(res) == 0: break else: page += 1 return activities, exclusions
def DownloadActivity(self, serviceRecord, activity): # First, download the summary stats and lap stats self._downloadActivitySummary(serviceRecord, activity) if len(activity.Laps) == 1: activity.Stats = activity.Laps[ 0].Stats # They must be identical to pass the verification if activity.Stationary: # Nothing else to download return activity # https://connect.garmin.com/proxy/activity-service-1.3/json/activityDetails/#### activityID = activity.ServiceData["ActivityID"] res = self._request_with_reauth( serviceRecord, lambda session: session. get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/" + str(activityID) + "?maxSize=999999999")) try: raw_data = res.json( )["com.garmin.activity.details.json.ActivityDetails"] except ValueError: raise APIException("Activity data parse error for %s: %s" % (res.status_code, res.text)) if "measurements" not in raw_data: activity.Stationary = True # We were wrong, oh well return activity attrs_map = {} def _map_attr(gc_key, wp_key, units, in_location=False, is_timestamp=False): attrs_map[gc_key] = { "key": wp_key, "to_units": units, "in_location": in_location, # Blegh "is_timestamp": is_timestamp # See above } _map_attr("directSpeed", "Speed", ActivityStatisticUnit.MetersPerSecond) _map_attr("sumDistance", "Distance", ActivityStatisticUnit.Meters) _map_attr("directHeartRate", "HR", ActivityStatisticUnit.BeatsPerMinute) _map_attr("directBikeCadence", "Cadence", ActivityStatisticUnit.RevolutionsPerMinute) _map_attr("directDoubleCadence", "RunCadence", ActivityStatisticUnit.StepsPerMinute) # 2*x mystery solved _map_attr("directAirTemperature", "Temp", ActivityStatisticUnit.DegreesCelcius) _map_attr("directPower", "Power", ActivityStatisticUnit.Watts) _map_attr("directElevation", "Altitude", ActivityStatisticUnit.Meters, in_location=True) _map_attr("directLatitude", "Latitude", None, in_location=True) _map_attr("directLongitude", "Longitude", None, in_location=True) _map_attr("directTimestamp", "Timestamp", None, is_timestamp=True) # Figure out which metrics we'll be seeing in this activity attrs_indexed = {} for measurement in raw_data["measurements"]: key = measurement["key"] if key in attrs_map: if attrs_map[key]["to_units"]: attrs_map[key]["from_units"] = self._unitMap[ measurement["unit"]] if attrs_map[key]["to_units"] == attrs_map[key][ "from_units"]: attrs_map[key]["to_units"] = attrs_map[key][ "from_units"] = None attrs_indexed[measurement["metricsIndex"]] = attrs_map[key] # Process the data frames frame_idx = 0 active_lap_idx = 0 for frame in raw_data["metrics"]: wp = Waypoint() for idx, attr in attrs_indexed.items(): value = frame["metrics"][idx] target_obj = wp if attr["in_location"]: if not wp.Location: wp.Location = Location() target_obj = wp.Location # Handle units if attr["is_timestamp"]: value = pytz.utc.localize( datetime.utcfromtimestamp(value / 1000)) elif attr["to_units"]: value = ActivityStatistic.convertValue( value, attr["from_units"], attr["to_units"]) # Write the value (can't use __dict__ because __slots__) setattr(target_obj, attr["key"], value) # Fix up lat/lng being zero (which appear to represent missing coords) if wp.Location and wp.Location.Latitude == 0 and wp.Location.Longitude == 0: wp.Location.Latitude = None wp.Location.Longitude = None # Please visit a physician before complaining about this if wp.HR == 0: wp.HR = None # Bump the active lap if required while (active_lap_idx < len(activity.Laps) - 1 and # Not the last lap activity.Laps[active_lap_idx + 1].StartTime <= wp.Timestamp ): active_lap_idx += 1 activity.Laps[active_lap_idx].Waypoints.append(wp) frame_idx += 1 return activity
def _downloadActivitySummary(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] summary_resp = self._request_with_reauth( lambda session: session. get("https://connect.garmin.com/modern/proxy/activity-service/activity/" + str(activityID)), serviceRecord) 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( lambda session: session. get("https://connect.garmin.com/modern/proxy/activity-service/activity/%s/splits" % str(activityID)), serviceRecord) 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, serviceRecord, activity): metrics = { "data": [], "metricTypes": [], "intervalUnit": "SEC", "intervalValue": 10 if activity.Type == ActivityType.Running else 5 # What a joke. } act = [{ "deviceName": "tapiriik", "deviceType": "BIKE" if activity.Type == ActivityType.Cycling else "WATCH", # ??? nike+ is weird "startTime": calendar.timegm( activity.StartTime.astimezone(pytz.utc).timetuple()) * 1000, "timeZoneName": str(activity.TZ), "activityType": [ k for k, v in self._reverseActivityMappings.items() if v == activity.Type ][0], "metrics": metrics }] wps = activity.GetFlatWaypoints() wpidx = 0 full_metrics = [] max_metrics = set() while True: wp = wps[wpidx] my_metrics = {} if wp.Location and wp.Location.Latitude is not None and wp.Location.Longitude is not None: elev = wp.Location.Altitude if wp.Location.Altitude else 0 # They always require this field, it's meh my_metrics.update({ "latitude": wp.Location.Latitude, "longitude": wp.Location.Longitude, "elevation": elev }) if wp.Distance is not None: my_metrics["distance"] = wp.Distance / 1000 # m -> km if wp.HR is not None: my_metrics["heartrate"] = round(wp.HR) if wp.Speed is not None: my_metrics["speed"] = wp.Speed if wp.Calories is not None: my_metrics["calories"] = round(wp.Calories) if wp.Power is not None: my_metrics["watts"] = round(wp.Power) max_metrics |= my_metrics.keys() full_metrics.append(my_metrics) # Skip to next wp skip_delta = 0 while (wpidx + skip_delta < len(wps) - 1) and ( wps[wpidx + skip_delta].Timestamp - wps[wpidx].Timestamp ).total_seconds() < metrics["intervalValue"]: skip_delta += 1 if skip_delta == 0: break # We're done wpidx += skip_delta if wpidx == 0 and len(wps) > 0: raise Exception("Activity had waypoints, none were used") max_metrics = sorted(list(max_metrics)) metrics["metricTypes"] = max_metrics # Passing null metric values makes Nike+ sad # So we hold the last value until a new one is available frame_hold = {x: 0 for x in max_metrics} # Blegh, close enough for metric_frame in full_metrics: frame_hold.update(metric_frame) metrics["data"].append([frame_hold[x] for x in max_metrics]) headers = {"Content-Type": "application/json"} session = self._get_session(serviceRecord) upload_resp = session.post("https://api.nike.com/me/sport/activities", params=self._with_auth(session), data=json.dumps(act), headers=headers) if upload_resp.status_code != 201: error_codes = [x["code"] for x in upload_resp.json()["errors"]] if 320 in error_codes: # Invalid combination of metric types and blah blah blah raise APIException("Not enough data, have keys %s" % max_metrics, user_exception=UserException( UserExceptionType.InsufficientData)) raise APIException("Could not upload activity %s - %s" % (upload_resp.status_code, upload_resp.text)) return upload_resp.json()[0]["activityId"]
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] logging.info("\t\t DC LOADING : " + str(activityID)) headers = self._getAuthHeaders(svcRecord) self._rate_limit() resp = requests.get(DECATHLON_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 Decathlon is not XML") activity.GPS = False activity.Stationary = True #work on date startdate = root.find('.//STARTDATE').text timezone = root.find('.//TIMEZONE').text datebase = parse(startdate + timezone) ridedata = {} ridedataindex = [] for pt in root.iter('LOCATION'): delta = int(pt.get('elapsed_time')) ridedataindex.append(delta) ridedata[delta] = {} if activityID == 'eu2132ac60d9a40a1d9a': logging.info('========time : ' + str(delta)) logging.info('========lat : ' + str(float(pt.find('LATITUDE').text[:8]))) ridedata[delta]['LATITUDE'] = float(pt.find('LATITUDE').text[:8]) ridedata[delta]['LONGITUDE'] = float(pt.find('LONGITUDE').text[:8]) ridedata[delta]['ELEVATION'] = int(pt.find('ELEVATION').text[:8]) if len(ridedata) > 0: activity.GPS = True activity.Stationary = False for measure in root.iter('MEASURE'): delta = int(measure.get('elapsed_time')) if delta not in ridedataindex: ridedataindex.append(delta) ridedata[delta] = {} for measureValue in measure.iter('VALUE'): if measureValue.get('id') == "1": ridedata[delta]['HR'] = int(measureValue.text) if measureValue.get('id') == "6": ridedata[delta]['SPEED'] = int(measureValue.text) if measureValue.get('id') == "5": ridedata[delta]['DISTANCE'] = int(measureValue.text) if measureValue.get('id') == "20": ridedata[delta]['LAP'] = int(measureValue.text) ridedataindex.sort() if len(ridedata) == 0: lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] else: lapWaypoints = [] startTimeLap = activity.StartTime for elapsedTime in ridedataindex: rd = ridedata[elapsedTime] wp = Waypoint() delta = elapsedTime formatedDate = datebase + timedelta(seconds=delta) wp.Timestamp = formatedDate #self._parseDate(formatedDate.isoformat()) if 'LATITUDE' in rd: wp.Location = Location() wp.Location.Latitude = rd['LATITUDE'] wp.Location.Longitude = rd['LONGITUDE'] wp.Location.Altitude = rd['ELEVATION'] if 'HR' in rd: wp.HR = rd['HR'] if 'SPEED' in rd: wp.Speed = rd['SPEED'] / 3600 if 'DISTANCE' in rd: wp.Distance = rd['DISTANCE'] lapWaypoints.append(wp) if "LAP" in rd: #build the lap lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=formatedDate) lap.Waypoints = lapWaypoints activity.Laps.append(lap) # re init a new lap startTimeLap = formatedDate lapWaypoints = [] #build last lap if len(lapWaypoints) > 0: lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=formatedDate) lap.Waypoints = lapWaypoints activity.Laps.append(lap) return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get( "https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: self._rateLimitBailout(resp) res_txt = resp.text raise APIException( "Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False old_location = None in_pause = False for pt in resp["points"]: wp = Waypoint() if "time" not in pt: # Manually-entered activities with a course attached to them have date-less waypoints # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK # So, ignore the points entirely continue wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if wp.Location == old_location: # We have seen the point with the same coordinates # before. This causes other services (e.g Strava) to # interpret this as if we were standing for a while, # which causes us having wrong activity time when # importing. We mark the point as paused in hopes this # fixes the issue. in_pause = True wp.Type = WaypointType.Pause elif in_pause: in_pause = False wp.Type = WaypointType.Resume old_location = wp.Location if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] if "pow" in pt: wp.Power = pt["pow"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def UploadActivity(self, svcRecord, activity): logging.info("UPLOAD To Decathlon Activity tz " + str(activity.TZ) + " dt tz " + str(activity.StartTime.tzinfo) + " starttime " + str(activity.StartTime)) #XML build root = etree.Element("ACTIVITY") header = etree.SubElement(root, "HEADER") etree.SubElement(header, "NAME").text = activity.Name etree.SubElement(header, "DATE").text = str(activity.StartTime).replace( " ", "T") duration = int((activity.EndTime - activity.StartTime).total_seconds()) etree.SubElement(header, "DURATION").text = str(duration) etree.SubElement( header, "SPORTID").text = self._activityTypeMappings[activity.Type] etree.SubElement(header, "LDID").text = str(svcRecord.ExternalID) etree.SubElement(header, "MANUAL", attrib=None).text = "true" summary = etree.SubElement(root, "SUMMARY") dataSummaryDuration = etree.SubElement(summary, "VALUE") dataSummaryDuration.text = str( int((activity.EndTime - activity.StartTime).total_seconds())) dataSummaryDuration.attrib["id"] = self._unitMap["duration"] if activity.Stats.Distance.Value is not None and activity.Stats.Distance.Value > 0: dataSummaryDistance = etree.SubElement(summary, "VALUE") dataSummaryDistance.text = str((int( activity.Stats.Distance.asUnits( ActivityStatisticUnit.Meters).Value))) dataSummaryDistance.attrib["id"] = self._unitMap["distance"] if activity.Stats.Energy.Value is not None: dataSummaryKcal = etree.SubElement(summary, "VALUE") dataSummaryKcal.text = str((int( activity.Stats.Energy.asUnits( ActivityStatisticUnit.Kilocalories).Value))) dataSummaryKcal.attrib["id"] = self._unitMap["kcal"] if activity.Stats.HR.Average is not None and activity.Stats.HR.Average > 0: dataSummaryHR = etree.SubElement(summary, "VALUE") dataSummaryHR.text = str(int(activity.Stats.HR.Average)) dataSummaryHR.attrib["id"] = self._unitMap["hravg"] #Speed average, We accept meter/hour if activity.Stats.Speed.Average is not None and activity.Stats.Speed.Average > 0: dataSummarySpeedAvg = etree.SubElement(summary, "VALUE") speed_kmh = activity.Stats.Speed.asUnits( ActivityStatisticUnit.KilometersPerHour).Average speed_mh = 1000 * speed_kmh dataSummarySpeedAvg.text = str((int(speed_mh))) dataSummarySpeedAvg.attrib["id"] = self._unitMap["speedaverage"] datameasure = etree.SubElement(root, "DATA") if len(activity.Laps) > 1: addLap = True else: addLap = False for lap in activity.Laps: for wp in lap.Waypoints: if wp.HR is not None or wp.Speed is not None or wp.Distance is not None or wp.Calories is not None: oneMeasureLocation = etree.SubElement( datameasure, "MEASURE") oneMeasureLocation.attrib["elapsed_time"] = str( duration - int((activity.EndTime - wp.Timestamp).total_seconds())) if wp.HR is not None: measureHR = etree.SubElement(oneMeasureLocation, "VALUE") measureHR.text = str(int(wp.HR)) measureHR.attrib["id"] = self._unitMap["hrcurrent"] if wp.Speed is not None: measureSpeed = etree.SubElement( oneMeasureLocation, "VALUE") measureSpeed.text = str(int(wp.Speed * 3600)) measureSpeed.attrib["id"] = self._unitMap[ "speedcurrent"] if wp.Calories is not None: measureKcaletree = etree.SubElement( oneMeasureLocation, "VALUE") measureKcaletree.text = str(int(wp.Calories)) measureKcaletree.attrib["id"] = self._unitMap["kcal"] if wp.Distance is not None: measureDistance = etree.SubElement( oneMeasureLocation, "VALUE") measureDistance.text = str(int(wp.Distance)) measureDistance.attrib["id"] = self._unitMap[ "distance"] if addLap and oneMeasureLocation is not None: measureLap = etree.SubElement(oneMeasureLocation, "VALUE") measureLap.text = "1" measureLap.attrib[ "id"] = "20" #add a lap here this elapsed time if len(activity.GetFlatWaypoints()) > 0: if activity.GetFlatWaypoints()[0].Location is not None: if activity.GetFlatWaypoints( )[0].Location.Latitude is not None: track = etree.SubElement(root, "TRACK") tracksummary = etree.SubElement(track, "SUMMARY") etree.SubElement(tracksummary, "LIBELLE").text = "" tracksummarylocation = etree.SubElement( tracksummary, "LOCATION") tracksummarylocation.attrib["elapsed_time"] = "0" etree.SubElement(tracksummarylocation, "LATITUDE").text = str( activity.GetFlatWaypoints() [0].Location.Latitude)[:8] etree.SubElement(tracksummarylocation, "LONGITUDE").text = str( activity.GetFlatWaypoints() [0].Location.Longitude)[:8] etree.SubElement(tracksummarylocation, "ELEVATION").text = "0" etree.SubElement(tracksummary, "DISTANCE").text = str( int( activity.Stats.Distance.asUnits( ActivityStatisticUnit.Meters).Value)) etree.SubElement(tracksummary, "DURATION").text = str( int((activity.EndTime - activity.StartTime).total_seconds())) etree.SubElement( tracksummary, "SPORTID").text = self._activityTypeMappings[ activity.Type] etree.SubElement(tracksummary, "LDID").text = str(svcRecord.ExternalID) for wp in activity.GetFlatWaypoints(): if wp.Location is None or wp.Location.Latitude is None or wp.Location.Longitude is None: continue # drop the point #oneLocation = etree.SubElement(track, "LOCATION") oneLocation = etree.SubElement(track, "LOCATION") oneLocation.attrib["elapsed_time"] = str( duration - int((activity.EndTime - wp.Timestamp).total_seconds())) etree.SubElement(oneLocation, "LATITUDE").text = str( wp.Location.Latitude)[:8] etree.SubElement(oneLocation, "LONGITUDE").text = str( wp.Location.Longitude)[:8] if wp.Location.Altitude is not None: etree.SubElement(oneLocation, "ELEVATION").text = str( int(wp.Location.Altitude)) else: etree.SubElement(oneLocation, "ELEVATION").text = "0" activityXML = etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="UTF-8") headers = self._getAuthHeaders(svcRecord) self._rate_limit() upload_resp = requests.post(DECATHLON_API_BASE_URL + "/activity/import.xml", data=activityXML, headers=headers) if upload_resp.status_code != 200: raise APIException("Could not upload activity %s %s" % (upload_resp.status_code, upload_resp.text)) upload_id = None try: root = xml.fromstring(upload_resp.content) upload_id = root.find('.//ID').text except: raise APIException("Stream data returned is not XML") return upload_id
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 DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] before = earliestDate = None while True: logger.debug("Req with before=" + str(before) + "/" + str(earliestDate)) 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 " + str(activity.StartTime)) if not earliestDate or activity.StartTime < earliestDate: earliestDate = activity.StartTime before = calendar.timegm( activity.StartTime.astimezone(pytz.utc).timetuple()) if ride["start_latlng"] is None or ride[ "end_latlng"] is None or ride[ "distance"] is None or ride["distance"] == 0: exclusions.append( APIExcludeActivity("No path", activityId=ride["id"])) logger.debug("\t\tNo pts") continue # stationary activity - no syncing for now activity.EndTime = activity.StartTime + timedelta( 0, ride["elapsed_time"]) activity.UploadedTo = [{ "Connection": svcRecord, "ActivityID": ride["id"] }] actType = [ k for k, v in self._reverseActivityTypeMappings.items() if v == ride["type"] ] if not len(actType): exclusions.append( APIExcludeActivity("Unsupported activity type %s" % ride["type"], activityId=ride["id"])) logger.debug("\t\tUnknown activity") continue activity.Type = actType[0] activity.Distance = ride["distance"] activity.Name = ride["name"] activity.Private = ride["private"] activity.AdjustTZ() activity.CalculateUID() activities.append(activity) if not exhaustive or not earliestDate: break return activities, exclusions
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] logger.info("\t\t DC LOADING : " + str(activityID)) headers = self._getAuthHeaders(svcRecord) resp = requests.get(self.ApiEndpoint + "/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 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]