def _create_activity(self, activity_data): activity = UploadedActivity() activity.GPS = not activity_data["has-route"] if "detailed-sport-info" in activity_data and activity_data["detailed-sport-info"] in self._reverse_activity_type_mappings: activity.Type = self._reverse_activity_type_mappings[activity_data["detailed-sport-info"]] else: activity.Type = ActivityType.Other activity.StartTime = pytz.utc.localize(isodate.parse_datetime(activity_data["start-time"])) activity.EndTime = activity.StartTime + isodate.parse_duration(activity_data["duration"]) distance = activity_data["distance"] if "distance" in activity_data else None activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=float(distance) if distance else None) hr_data = activity_data["heart-rate"] if "heart-rate" in activity_data else None avg_hr = hr_data["average"] if "average" in hr_data else None max_hr = hr_data["maximum"] if "maximum" in hr_data else None activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(avg_hr) if avg_hr else None, max=float(max_hr) if max_hr else None)) calories = activity_data["calories"] if "calories" in activity_data else None activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=int(calories) if calories else None) activity.ServiceData = {"ActivityID": activity_data["id"]} logger.debug("\tActivity s/t {}: {}".format(activity.StartTime, activity.Type)) activity.CalculateUID() return activity
def _populateActivity(self, rawRecord): ''' Populate the 1st level of the activity object with all details required for UID from RK API data ''' activity = UploadedActivity() # can stay local + naive here, recipient services can calculate TZ as required activity.StartTime = datetime.strptime(rawRecord["start_time"], "%a, %d %b %Y %H:%M:%S") activity.Stats.MovingTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=float(rawRecord["duration"])) # P. sure this is moving time activity.EndTime = activity.StartTime + timedelta( seconds=float(rawRecord["duration"]) ) # this is inaccurate with pauses - excluded from hash activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=rawRecord["total_distance"]) # I'm fairly sure this is how the RK calculation works. I remember I removed something exactly like this from ST.mobi, but I trust them more than I trust myself to get the speed right. if (activity.EndTime - activity.StartTime).total_seconds() > 0: activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.KilometersPerHour, avg=activity.Stats.Distance.asUnits( ActivityStatisticUnit.Kilometers).Value / ((activity.EndTime - activity.StartTime).total_seconds() / 60 / 60)) activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=rawRecord["total_calories"] if "total_calories" in rawRecord else None) if rawRecord["type"] in self._activityMappings: activity.Type = self._activityMappings[rawRecord["type"]] activity.GPS = rawRecord["has_path"] activity.CalculateUID() return activity
def _create_activity(self, data): activity = UploadedActivity() activity.Name = data.get("name") activity.StartTime = pytz.utc.localize(datetime.strptime(data.get("start_at"), "%Y-%m-%dT%H:%M:%SZ")) activity.EndTime = activity.StartTime + timedelta(0, float(data.get("duration"))) sport_id = data.get("sport_id") activity.Type = self._reverseActivityMappings.get(int(sport_id), ActivityType.Other) if sport_id else ActivityType.Other distance = data.get("distance") activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=float(distance) if distance else None) activity.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=float(data.get("total_time_in_seconds"))) avg_speed = data.get("average_speed") max_speed = data.get("max_speed") activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.KilometersPerHour, avg=float(avg_speed) if avg_speed else None, max=float(max_speed) if max_speed else None) avg_hr = data.get("average_heart_rate") max_hr = data.get("maximum_heart_rate") activity.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(avg_hr) if avg_hr else None, max=float(max_hr) if max_hr else None)) calories = data.get("calories") activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=int(calories) if calories else None) activity.ServiceData = {"ActivityID": data.get("id")} logger.debug("\tActivity s/t {}: {}".format(activity.StartTime, activity.Type)) activity.CalculateUID() return activity
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])))
def DownloadActivity(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] if AGGRESSIVE_CACHE: ridedata = cachedb.rk_activity_cache.find_one({"uri": activityID}) if not AGGRESSIVE_CACHE or ridedata is None: response = requests.get("https://api.runkeeper.com" + activityID, headers=self._apiHeaders(serviceRecord)) if response.status_code != 200: if response.status_code == 401 or response.status_code == 403: raise APIException("No authorization to download activity" + activityID, block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to download activity " + activityID + " response " + str(response) + " " + response.text) ridedata = response.json() ridedata["Owner"] = serviceRecord.ExternalID if AGGRESSIVE_CACHE: cachedb.rk_activity_cache.insert(ridedata) if "is_live" in ridedata and ridedata["is_live"] is True: raise APIExcludeActivity("Not complete", activity_id=activityID, permanent=False, user_exception=UserException(UserExceptionType.LiveTracking)) if "userID" in ridedata and int(ridedata["userID"]) != int(serviceRecord.ExternalID): raise APIExcludeActivity("Not the user's own activity", activity_id=activityID, user_exception=UserException(UserExceptionType.Other)) self._populateActivityWaypoints(ridedata, activity) if "climb" in ridedata: activity.Stats.Elevation = ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(ridedata["climb"])) if "average_heart_rate" in ridedata: activity.Stats.HR = ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(ridedata["average_heart_rate"])) activity.Stationary = activity.CountTotalWaypoints() <= 1 # This could cause confusion, since when I upload activities to RK I populate the notes field with the activity name. My response is to... well... not sure. activity.Notes = ridedata["notes"] if "notes" in ridedata else None activity.Private = ridedata["share"] == "Just Me" return activity
def _populateActivity(self, rawRecord): ''' Populate the 1st level of the activity object with all details required for UID from pulsstory API data ''' activity = UploadedActivity() # can stay local + naive here, recipient services can calculate TZ as required activity.Name = rawRecord["Name"] if "Name" in rawRecord else None activity.StartTime = datetime.strptime(rawRecord["StartTime"], "%Y-%m-%d %H:%M:%S") activity.Stats.MovingTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=float(rawRecord["Duration"])) activity.EndTime = activity.StartTime + timedelta( seconds=float(rawRecord["Duration"])) activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=rawRecord["Distance"]) if (activity.EndTime - activity.StartTime).total_seconds() > 0: activity.Stats.Speed = ActivityStatistic( ActivityStatisticUnit.KilometersPerHour, avg=activity.Stats.Distance.asUnits( ActivityStatisticUnit.Kilometers).Value / ((activity.EndTime - activity.StartTime).total_seconds() / 60 / 60)) activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=rawRecord["Energy"] if "Energy" in rawRecord else None) if rawRecord["Type"] in self._activityMappings: activity.Type = self._activityMappings[rawRecord["Type"]] activity.GPS = rawRecord["HasPath"] if "HasPath" in rawRecord else False activity.Stationary = rawRecord[ "HasPoints"] if "HasPoints" in rawRecord else True activity.Notes = rawRecord["Notes"] if "Notes" in rawRecord else None activity.Private = rawRecord[ "Private"] if "Private" in rawRecord else True activity.CalculateUID() return activity
def DownloadActivityList(self, serviceRecord, exhaustive=False): activities = [] exclusions = [] for act in self._getActivities(serviceRecord, exhaustive=exhaustive): activity = UploadedActivity() activity.StartTime = dateutil.parser.parse(act['startDateTimeLocal']) activity.EndTime = activity.StartTime + timedelta(seconds=act['duration']) _type = self._activityMappings.get(act['activityType']) if not _type: exclusions.append(APIExcludeActivity("Unsupported activity type %s" % act['activityType'], activity_id=act["activityId"], user_exception=UserException(UserExceptionType.Other))) activity.ServiceData = {"ActivityID": act['activityId']} activity.Type = _type activity.Notes = act['notes'] activity.GPS = bool(act.get('startLatitude')) activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=act['distance']) activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=act['calories']) if 'heartRateMin' in act: activity.Stats.HR = ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, min=act['heartRateMin'], max=act['heartRateMax'], avg=act['heartRateAverage']) activity.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=act['duration']) if 'temperature' in act: activity.Stats.Temperature = ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, avg=act['temperature']) activity.CalculateUID() logger.debug("\tActivity s/t %s", activity.StartTime) activities.append(activity) return activities, exclusions
def DownloadActivityList(self, serviceRecord, exhaustive=False): session = self._get_session(serviceRecord) list_params = self._with_auth(session, {"count": 20, "offset": 1}) activities = [] exclusions = [] while True: list_resp = session.get("https://api.nike.com/me/sport/activities", params=list_params) list_resp = list_resp.json() for act in list_resp["data"]: activity = UploadedActivity() activity.ServiceData = {"ID": act["activityId"]} if act["status"] != "COMPLETE": exclusions.append( APIExcludeActivity( "Not complete", activity_id=act["activityId"], permanent=False, user_exception=UserException( UserExceptionType.LiveTracking))) continue activity.StartTime = dateutil.parser.parse( act["startTime"]).replace(tzinfo=pytz.utc) activity.EndTime = activity.StartTime + self._durationToTimespan( act["metricSummary"]["duration"]) tz_name = act["activityTimeZone"] # They say these are all IANA standard names - they aren't if tz_name in self._timezones: tz_name = self._timezones[tz_name] activity.TZ = pytz.timezone(tz_name) if act["activityType"] in self._activityMappings: activity.Type = self._activityMappings[act["activityType"]] activity.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Kilometers, value=float(act["metricSummary"]["distance"])) activity.Stats.Strides = ActivityStatistic( ActivityStatisticUnit.Strides, value=int(act["metricSummary"]["steps"])) activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilocalories, value=float(act["metricSummary"]["calories"])) activity.CalculateUID() activities.append(activity) if len(list_resp["data"]) == 0 or not exhaustive: break list_params["offset"] += list_params["count"] return activities, exclusions
def test_unitconv_distance_metric(self): stat = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=1) self.assertEqual( stat.asUnits(ActivityStatisticUnit.Meters).Value, 1000) stat = ActivityStatistic(ActivityStatisticUnit.Meters, value=250) self.assertEqual( stat.asUnits(ActivityStatisticUnit.Kilometers).Value, 0.25)
def test_unitconv_impossible(self): stat = ActivityStatistic(ActivityStatisticUnit.KilometersPerHour, value=100) self.assertRaises(ValueError, stat.asUnits, ActivityStatisticUnit.Meters) stat = ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, value=100) self.assertRaises(ValueError, stat.asUnits, ActivityStatisticUnit.Miles)
def test_stat_coalesce_missing(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2) stat1.coalesceWith(stat2) self.assertEqual(stat1.Value, 2) stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=1) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None) stat1.coalesceWith(stat2) self.assertEqual(stat1.Value, 1)
def test_stat_coalesce_multi(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=1) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2) stat3 = ActivityStatistic(ActivityStatisticUnit.Meters, value=3) stat4 = ActivityStatistic(ActivityStatisticUnit.Meters, value=4) stat5 = ActivityStatistic(ActivityStatisticUnit.Meters, value=5) stat1.coalesceWith(stat2) stat1.coalesceWith(stat3) stat1.coalesceWith(stat4) stat1.coalesceWith(stat5) self.assertEqual(stat1.Value, 3)
def test_unitconv_velocity_metric(self): stat = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, value=100) self.assertEqual( stat.asUnits(ActivityStatisticUnit.KilometersPerHour).Value, 360) stat = ActivityStatistic(ActivityStatisticUnit.KilometersPerHour, value=50) self.assertAlmostEqual(stat.asUnits( ActivityStatisticUnit.MetersPerSecond).Value, 13.89, places=2)
def test_stat_coalesce_multi_missingmixed(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=1) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2) stat3 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None) stat4 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None) stat5 = ActivityStatistic(ActivityStatisticUnit.Meters, value=5) stat5.coalesceWith(stat2) stat3.coalesceWith(stat5) stat4.coalesceWith(stat3) stat1.coalesceWith(stat4) self.assertAlmostEqual(stat1.Value, 8 / 3)
def test_unitconv_velocity_cross(self): stat = ActivityStatistic(ActivityStatisticUnit.KilometersPerHour, value=100) self.assertAlmostEqual(stat.asUnits( ActivityStatisticUnit.MilesPerHour).Value, 62, places=0) stat = ActivityStatistic(ActivityStatisticUnit.MilesPerHour, value=60) self.assertAlmostEqual(stat.asUnits( ActivityStatisticUnit.KilometersPerHour).Value, 96.5, places=0)
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): #http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/#####?full=true activityID = activity.ServiceData["ActivityID"] cookies = self._get_cookies(record=serviceRecord) self._rate_limit() res = requests.get( "http://connect.garmin.com/proxy/activity-service-1.1/tcx/activity/" + str(activityID) + "?full=true", cookies=cookies) try: TCXIO.Parse(res.content, activity) except ValueError as e: raise APIExcludeActivity("TCX parse error " + str(e), userException=UserException( UserExceptionType.Corrupt)) if activity.ServiceData["RecalcHR"]: logger.debug("Recalculating HR") avgHR, maxHR = ActivityStatisticCalculator.CalculateAverageMaxHR( activity) activity.Stats.HR.coalesceWith( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=maxHR, avg=avgHR)) if len(activity.Laps) == 1: activity.Laps[0].Stats.update( activity.Stats ) # I trust Garmin Connect's stats more than whatever shows up in the TCX activity.Stats = activity.Laps[ 0].Stats # They must be identical to pass the verification if activity.Stats.Temperature.Min is not None or activity.Stats.Temperature.Max is not None or activity.Stats.Temperature.Average is not None: logger.debug("Retrieving additional temperature data") # TCX doesn't have temperature, for whatever reason... self._rate_limit() res = requests.get( "http://connect.garmin.com/proxy/activity-service-1.1/gpx/activity/" + str(activityID) + "?full=true", cookies=cookies) try: temp_act = GPXIO.Parse(res.content, suppress_validity_errors=True) except ValueError as e: pass else: logger.debug("Merging additional temperature data") full_waypoints = activity.GetFlatWaypoints() temp_waypoints = temp_act.GetFlatWaypoints() merge_idx = 0 for x in range(len(temp_waypoints)): while full_waypoints[merge_idx].Timestamp < temp_waypoints[ x].Timestamp and merge_idx < len( full_waypoints) - 1: merge_idx += 1 full_waypoints[merge_idx].Temp = temp_waypoints[x].Temp return activity
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})))
def test_unitconv_distance_cross(self): stat = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=1) self.assertAlmostEqual(stat.asUnits(ActivityStatisticUnit.Miles).Value, 0.6214, places=4) stat = ActivityStatistic(ActivityStatisticUnit.Miles, value=1) self.assertAlmostEqual(stat.asUnits( ActivityStatisticUnit.Kilometers).Value, 1.609, places=3) stat = ActivityStatistic(ActivityStatisticUnit.Miles, value=1) self.assertAlmostEqual(stat.asUnits( ActivityStatisticUnit.Meters).Value, 1609, places=0)
def _populateActivity(self, rawRecord): ''' Populate the 1st level of the activity object with all details required for UID from API data ''' activity = UploadedActivity() activity.StartTime = dateutil.parser.parse(rawRecord["start"]) activity.EndTime = activity.StartTime + timedelta(seconds=rawRecord["duration"]) activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=rawRecord["distance"]) activity.GPS = rawRecord["hasGps"] activity.Stationary = not rawRecord["hasGps"] activity.CalculateUID() return activity
def test_unitconv_temp(self): stat = ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, value=0) self.assertEqual( stat.asUnits(ActivityStatisticUnit.DegreesFahrenheit).Value, 32) stat = ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, value=-40) self.assertEqual( stat.asUnits(ActivityStatisticUnit.DegreesFahrenheit).Value, -40) stat = ActivityStatistic(ActivityStatisticUnit.DegreesFahrenheit, value=-40) self.assertEqual( stat.asUnits(ActivityStatisticUnit.DegreesCelcius).Value, -40) stat = ActivityStatistic(ActivityStatisticUnit.DegreesFahrenheit, value=32) self.assertEqual( stat.asUnits(ActivityStatisticUnit.DegreesCelcius).Value, 0)
def test_stat_sum(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, min=None) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2, max=2) stat3 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, gain=3) stat4 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, gain=4) stat5 = ActivityStatistic(ActivityStatisticUnit.Meters, value=5, max=3) stat5.sumWith(stat2) stat3.sumWith(stat5) stat4.sumWith(stat3) stat1.sumWith(stat4) self.assertEqual(stat1.Value, 7) self.assertEqual(stat1.Max, 3) self.assertEqual(stat1.Gain, 7)
def test_stat_update(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, min=None) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2, max=2) stat3 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, gain=3) stat4 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, gain=4) stat5 = ActivityStatistic(ActivityStatisticUnit.Meters, value=5, max=3) stat5.update(stat2) stat3.update(stat5) stat4.update(stat3) stat1.update(stat4) self.assertEqual(stat1.Value, 2) self.assertEqual(stat1.Max, 2) self.assertEqual(stat1.Gain, 3)
def test_stat_coalesce_multi_missingmixed_multivalued(self): stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, min=None) stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2, max=2) stat3 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, gain=3) stat4 = ActivityStatistic(ActivityStatisticUnit.Meters, value=None, loss=4) stat5 = ActivityStatistic(ActivityStatisticUnit.Meters, value=5, min=3) stat5.coalesceWith(stat2) stat3.coalesceWith(stat5) stat4.coalesceWith(stat3) stat1.coalesceWith(stat4) self.assertAlmostEqual(stat1.Value, 7 / 2) self.assertEqual(stat1.Min, 3) self.assertEqual(stat1.Max, 2) self.assertEqual(stat1.Gain, 3) self.assertEqual(stat1.Loss, 4)
def DownloadActivity(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] response = requests.post(self.URLBase + activityID, data=self._apiData(serviceRecord)) if response.status_code != 200: if response.status_code == 401 or response.status_code == 403: raise APIException("No authorization to download activity" + activityID, block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) raise APIException("Unable to download activity " + activityID + " response " + str(response) + " " + response.text) ridedata = response.json() ridedata["Owner"] = serviceRecord.ExternalID if "UserID" in ridedata and int(ridedata["UserID"]) != int( serviceRecord.ExternalID): raise APIExcludeActivity("Not the user's own activity", activity_id=activityID, user_exception=UserException( UserExceptionType.Other)) self._populateActivityWaypoints(ridedata, activity) if "Climb" in ridedata: activity.Stats.Elevation = ActivityStatistic( ActivityStatisticUnit.Meters, gain=float(ridedata["Climb"])) if "AvgHr" in ridedata: activity.Stats.HR = ActivityStatistic( ActivityStatisticUnit.BeatsPerMinute, avg=float(ridedata["AvgHr"])) activity.Stationary = activity.CountTotalWaypoints() <= 1 return activity
def mapStat(gcKey, statKey, type, useSourceUnits=False): nonlocal activity, act if gcKey in act: value = float(act[gcKey]["value"]) if math.isinf(value): return # GC returns the minimum speed as "-Infinity" instead of 0 some times :S activity.Stats.__dict__[statKey].update( ActivityStatistic(self._unitMap[act[gcKey]["uom"]], **({ type: value }))) if useSourceUnits: activity.Stats.__dict__[ statKey] = activity.Stats.__dict__[ statKey].asUnits( self._unitMap[act[gcKey]["uom"]])
def _mapStat(name, statKey, type): nonlocal activity _unitMap = { "mi": ActivityStatisticUnit.Miles, "km": ActivityStatisticUnit.Kilometers, "kcal": ActivityStatisticUnit.Kilocalories, "ft": ActivityStatisticUnit.Feet, "m": ActivityStatisticUnit.Meters, "rpm": ActivityStatisticUnit.RevolutionsPerMinute, "avg-hr": ActivityStatisticUnit.BeatsPerMinute, "max-hr": ActivityStatisticUnit.BeatsPerMinute, } statValue = _findStat(name) if statValue: statUnit = statValue.split( " ")[1] if " " in statValue else None unit = _unitMap[statUnit] if statUnit else _unitMap[name] statValue = statValue.split(" ")[0] valData = {type: float(statValue)} activity.Stats.__dict__[statKey].update( ActivityStatistic(unit, **valData))
def create_random_activity(svc=None, actType=ActivityType.Other, tz=False, record=None, withPauses=True, withLaps=True): ''' creates completely random activity with valid waypoints and data ''' act = TestTools.create_blank_activity(svc, actType, record=record) if tz is True: tz = pytz.timezone("America/Atikokan") act.TZ = tz elif tz is not False: act.TZ = tz if act.CountTotalWaypoints() > 0: raise ValueError("Waypoint list already populated") # this is entirely random in case the testing account already has events in it (API doesn't support delete, etc) act.StartTime = datetime(2011, 12, 13, 14, 15, 16) if tz is not False: if hasattr(tz, "localize"): act.StartTime = tz.localize(act.StartTime) else: act.StartTime = act.StartTime.replace(tzinfo=tz) act.EndTime = act.StartTime + timedelta( 0, random.randint(60 * 5, 60 * 60) ) # don't really need to upload 1000s of pts to test this... act.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=random.random() * 10000) act.Name = str(random.random()) paused = False waypointTime = act.StartTime backToBackPauses = False act.Laps = [] lap = Lap(startTime=act.StartTime) while waypointTime < act.EndTime: wp = Waypoint() if waypointTime == act.StartTime: wp.Type = WaypointType.Start wp.Timestamp = waypointTime wp.Location = Location( random.random() * 180 - 90, random.random() * 180 - 90, random.random() * 1000) # this is gonna be one intense activity if not (wp.HR == wp.Cadence == wp.Calories == wp.Power == wp.Temp == None): raise ValueError("Waypoint did not initialize cleanly") if svc.SupportsHR: wp.HR = float(random.randint(90, 180)) if svc.SupportsPower: wp.Power = float(random.randint(0, 1000)) if svc.SupportsCalories: wp.Calories = float(random.randint(0, 500)) if svc.SupportsCadence: wp.Cadence = float(random.randint(0, 100)) if svc.SupportsTemp: wp.Temp = float(random.randint(0, 100)) if withPauses and (random.randint(40, 50) == 42 or backToBackPauses ) and not paused: # pause quite often wp.Type = WaypointType.Pause paused = True elif paused: paused = False wp.Type = WaypointType.Resume backToBackPauses = not backToBackPauses waypointTime += timedelta(0, int(random.random() + 9.5)) # 10ish seconds lap.Waypoints.append(wp) if waypointTime > act.EndTime: wp.Timestamp = act.EndTime wp.Type = WaypointType.End elif withLaps and wp.Timestamp < act.EndTime and random.randint( 40, 60) == 42: # occasionally start new laps lap.EndTime = wp.Timestamp act.Laps.append(lap) lap = Lap(startTime=waypointTime) # Final lap lap.EndTime = act.EndTime act.Laps.append(lap) if act.CountTotalWaypoints() == 0: raise ValueError("No waypoints populated") act.CalculateUID() act.EnsureTZ() return act
def DownloadActivity(self, serviceRecord, activity): session = self._get_session(serviceRecord) act_id = activity.ServiceData["ID"] activityDetails = session.get( "https://api.nike.com/me/sport/activities/%s" % act_id, params=self._with_auth(session)) activityDetails = activityDetails.json() streams = { metric["metricType"].lower(): self._nikeStream(metric) for metric in activityDetails["metrics"] } activity.GPS = activityDetails["isGpsActivity"] if activity.GPS: activityGps = session.get( "https://api.nike.com/me/sport/activities/%s/gps" % act_id, params=self._with_auth(session)) activityGps = activityGps.json() streams["gps"] = self._nikeStream(activityGps, "waypoints") activity.Stats.Elevation.update( ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(activityGps["elevationGain"]), loss=float(activityGps["elevationLoss"]), max=float(activityGps["elevationMax"]), min=float(activityGps["elevationMin"]))) lap = Lap(startTime=activity.StartTime, endTime=activity.EndTime) lap.Stats = activity.Stats activity.Laps = [lap] # I thought I wrote StreamSampler to be generator-friendly - nope. streams = {k: list(v) for k, v in streams.items()} # The docs are unclear on which of these are actually stream metrics, oh well def stream_waypoint(offset, speed=None, distance=None, heartrate=None, calories=None, steps=None, watts=None, gps=None, **kwargs): wp = Waypoint() wp.Timestamp = activity.StartTime + timedelta(seconds=offset) wp.Speed = float(speed) if speed else None wp.Distance = float(distance) / 1000 if distance else None wp.HR = float(heartrate) if heartrate else None wp.Calories = float(calories) if calories else None wp.Power = float(watts) if watts else None if gps: wp.Location = Location(lat=float(gps["latitude"]), lon=float(gps["longitude"]), alt=float(gps["elevation"])) lap.Waypoints.append(wp) StreamSampler.SampleWithCallback(stream_waypoint, streams) activity.Stationary = len(lap.Waypoints) == 0 return activity
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"], 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