Exemple #1
0
    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
Exemple #2
0
 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
Exemple #3
0
    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
Exemple #4
0
 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])))
Exemple #5
0
    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
Exemple #6
0
    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
Exemple #7
0
    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
Exemple #8
0
    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)
Exemple #16
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
Exemple #17
0
    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
Exemple #18
0
 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)
Exemple #20
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)
Exemple #25
0
    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
Exemple #26
0
 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"]])
Exemple #27
0
 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))
Exemple #28
0
    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
Exemple #29
0
    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
Exemple #30
0
    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