Beispiel #1
0
 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)
Beispiel #2
0
    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)
Beispiel #3
0
    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)
Beispiel #4
0
    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)
Beispiel #5
0
    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)
Beispiel #6
0
    def test_unitconv_distance_nonmetric(self):
        stat = ActivityStatistic(ActivityStatisticUnit.Miles, value=1)
        self.assertEqual(stat.asUnits(ActivityStatisticUnit.Feet).Value, 5280)

        stat = ActivityStatistic(ActivityStatisticUnit.Feet, value=5280/2)
        self.assertEqual(stat.asUnits(ActivityStatisticUnit.Miles).Value, 0.5)
    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, []
Beispiel #8
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)
Beispiel #9
0
    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)
Beispiel #10
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []

        now = datetime.now()
        prev = now - timedelta(6 * 365 / 12)

        period = []

        aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year,
                                     now.month)
        period.append(aperiod)

        if exhaustive:
            for _ in range(20):
                now = prev
                prev = now - timedelta(6 * 365 / 12)
                aperiod = "%s%02d-%s%02d" % (prev.year, prev.month, now.year,
                                             now.month)
                period.append(aperiod)

        for dateInterval in period:
            headers = self._getAuthHeaders(svcRecord)
            resp = requests.get(DECATHLONCOACH_API_BASE_URL + "/users/" +
                                str(svcRecord.ExternalID) +
                                "/activities.xml?date=" + dateInterval,
                                headers=headers)
            if resp.status_code == 400:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))
            if resp.status_code == 401:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))
            if resp.status_code == 403:
                logger.info(resp.content)
                raise APIException(
                    "No authorization to retrieve activity list",
                    block=True,
                    user_exception=UserException(
                        UserExceptionType.Authorization,
                        intervention_required=True))

            root = xml.fromstring(resp.content)

            logger.info("\t\t nb activity : " +
                        str(len(root.findall('.//ID'))))

            for ride in root.iter('ACTIVITY'):

                activity = UploadedActivity()
                activity.TZ = pytz.timezone("UTC")

                startdate = ride.find('.//STARTDATE').text + ride.find(
                    './/TIMEZONE').text
                datebase = parse(startdate)

                activity.StartTime = datebase  #pytz.utc.localize(datebase)

                activity.ServiceData = {
                    "ActivityID": ride.find('ID').text,
                    "Manual": ride.find('MANUAL').text
                }

                logger.info("\t\t DecathlonCoach Activity ID : " +
                            ride.find('ID').text)

                if ride.find('SPORTID'
                             ).text not in self._reverseActivityTypeMappings:
                    exclusions.append(
                        APIExcludeActivity("Unsupported activity type %s" %
                                           ride.find('SPORTID').text,
                                           activity_id=ride.find('ID').text,
                                           user_exception=UserException(
                                               UserExceptionType.Other)))
                    logger.info(
                        "\t\tDecathlonCoach Unknown activity, sport id " +
                        ride.find('SPORTID').text + " is not mapped")
                    continue

                activity.Type = self._reverseActivityTypeMappings[ride.find(
                    'SPORTID').text]

                for val in ride.iter('VALUE'):
                    if val.get('id') == self._unitMap["duration"]:
                        activity.EndTime = activity.StartTime + timedelta(
                            0, int(val.text))
                    if val.get('id') == self._unitMap["distance"]:
                        activity.Stats.Distance = ActivityStatistic(
                            ActivityStatisticUnit.Meters, value=int(val.text))
                    if val.get('id') == self._unitMap["kcal"]:
                        activity.Stats.Energy = ActivityStatistic(
                            ActivityStatisticUnit.Kilocalories,
                            value=int(val.text))
                    if val.get('id') == self._unitMap["speedaverage"]:
                        meterperhour = int(val.text)
                        meterpersecond = meterperhour / 3600
                        activity.Stats.Speed = ActivityStatistic(
                            ActivityStatisticUnit.MetersPerSecond,
                            avg=meterpersecond,
                            max=None)

                if ride.find('LIBELLE'
                             ).text == "" or ride.find('LIBELLE').text is None:
                    txtdate = startdate.split(' ')
                    activity.Name = "Sport DecathlonCoach " + txtdate[0]
                else:
                    activity.Name = ride.find('LIBELLE').text

                activity.Private = False
                activity.Stationary = ride.find('MANUAL').text
                activity.GPS = ride.find('ABOUT').find('TRACK').text
                activity.AdjustTZ()
                activity.CalculateUID()
                activities.append(activity)

        return activities, exclusions
    def 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 DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []

        url = self.SingletrackerDomain + "getRidesByUserId"
        extID = svcRecord.ExternalID

        payload = {"userId": extID}
        headers = {
            'content-type': "application/json",
            'cache-control': "no-cache",
        }
        response = requests.post(url,
                                 data=json.dumps(payload),
                                 headers=headers)
        try:
            reqdata = response.json()
        except ValueError:
            raise APIException(
                "Failed parsing Singletracker list response %s - %s" %
                (resp.status_code, resp.text))

        for ride in reqdata:
            activity = UploadedActivity()
            activity.StartTime = datetime.strptime(
                datetime.utcfromtimestamp(
                    ride["startTime"]).strftime('%Y-%m-%d %H:%M:%S'),
                "%Y-%m-%d %H:%M:%S")
            if "stopTime" in ride:
                activity.EndTime = datetime.strptime(
                    datetime.utcfromtimestamp(
                        ride["stopTime"]).strftime('%Y-%m-%d %H:%M:%S'),
                    "%Y-%m-%d %H:%M:%S")
            activity.ServiceData = {
                "ActivityID": ride["rideId"],
                "Manual": "False"
            }

            activity.Name = ride["trackName"]

            logger.debug("\tActivity s/t %s: %s" %
                         (activity.StartTime, activity.Name))
            activity.Type = ActivityType.MountainBiking
            if "totalDistance" in ride:
                activity.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Meters, value=ride["totalDistance"])

            if "avgSpeed" in ride:
                activity.Stats.Speed = ActivityStatistic(
                    ActivityStatisticUnit.MetersPerSecond,
                    avg=ride["avgSpeed"])
            activity.Notes = None

            activity.GPS = True

            activity.Private = False
            activity.Stationary = False  # True = no sensor data

            activity.CalculateUID()
            activities.append(activity)

        return activities, exclusions
    def test_stat_coalesce_multi_mixed2(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)
        stat5.coalesceWith(stat2)
        stat3.coalesceWith(stat5)
        stat4.coalesceWith(stat3)
        stat1.coalesceWith(stat4)

        self.assertEqual(stat1.Value, 3)
Beispiel #14
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        oauthSession = self._oauthSession(serviceRecord)

        activities = []
        exclusions = []

        page_url = "https://api.endomondo.com/api/1/workouts"

        while True:
            resp = oauthSession.get(page_url)
            try:
                respList = resp.json()["data"]
            except ValueError:
                self._rateLimitBailout(resp)
                raise APIException("Error decoding activity list resp %s %s" %
                                   (resp.status_code, resp.text))
            for actInfo in respList:
                activity = UploadedActivity()
                activity.StartTime = self._parseDate(actInfo["start_time"])
                logger.debug("Activity s/t %s" % activity.StartTime)
                if "is_tracking" in actInfo and actInfo["is_tracking"]:
                    exclusions.append(
                        APIExcludeActivity(
                            "Not complete",
                            activity_id=actInfo["id"],
                            permanent=False,
                            user_exception=UserException(
                                UserExceptionType.LiveTracking)))
                    continue

                if "end_time" in actInfo:
                    activity.EndTime = self._parseDate(actInfo["end_time"])

                if actInfo["sport"] in self._activityMappings:
                    activity.Type = self._activityMappings[actInfo["sport"]]

                # "duration" is timer time
                if "duration_total" in actInfo:
                    activity.Stats.TimerTime = ActivityStatistic(
                        ActivityStatisticUnit.Seconds,
                        value=float(actInfo["duration_total"]))

                if "distance_total" in actInfo:
                    activity.Stats.Distance = ActivityStatistic(
                        ActivityStatisticUnit.Kilometers,
                        value=float(actInfo["distance_total"]))

                if "calories_total" in actInfo:
                    activity.Stats.Energy = ActivityStatistic(
                        ActivityStatisticUnit.Kilocalories,
                        value=float(actInfo["calories_total"]))

                activity.Stats.Elevation = ActivityStatistic(
                    ActivityStatisticUnit.Meters)

                if "altitude_max" in actInfo:
                    activity.Stats.Elevation.Max = float(
                        actInfo["altitude_max"])

                if "altitude_min" in actInfo:
                    activity.Stats.Elevation.Min = float(
                        actInfo["altitude_min"])

                if "total_ascent" in actInfo:
                    activity.Stats.Elevation.Gain = float(
                        actInfo["total_ascent"])

                if "total_descent" in actInfo:
                    activity.Stats.Elevation.Loss = float(
                        actInfo["total_descent"])

                activity.Stats.Speed = ActivityStatistic(
                    ActivityStatisticUnit.KilometersPerHour)
                if "speed_max" in actInfo:
                    activity.Stats.Speed.Max = float(actInfo["speed_max"])

                if "heart_rate_avg" in actInfo:
                    activity.Stats.HR = ActivityStatistic(
                        ActivityStatisticUnit.BeatsPerMinute,
                        avg=float(actInfo["heart_rate_avg"]))

                if "heart_rate_max" in actInfo:
                    activity.Stats.HR.update(
                        ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                          max=float(
                                              actInfo["heart_rate_max"])))

                if "cadence_avg" in actInfo:
                    activity.Stats.Cadence = ActivityStatistic(
                        ActivityStatisticUnit.RevolutionsPerMinute,
                        avg=int(actInfo["cadence_avg"]))

                if "cadence_max" in actInfo:
                    activity.Stats.Cadence.update(
                        ActivityStatistic(
                            ActivityStatisticUnit.RevolutionsPerMinute,
                            max=int(actInfo["cadence_max"])))

                if "title" in actInfo:
                    activity.Name = actInfo["title"]

                activity.ServiceData = {"WorkoutID": int(actInfo["id"])}

                activity.CalculateUID()
                activities.append(activity)

            paging = resp.json()["paging"]
            if "next" not in paging or not paging["next"] or not exhaustive:
                break
            else:
                page_url = paging["next"]

        return activities, exclusions
Beispiel #15
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        ns = {
            "tpw": "http://www.trainingpeaks.com/TPWebServices/",
            "xsi": "http://www.w3.org/2001/XMLSchema-instance"
        }
        activities = []
        exclusions = []

        reqData = self._authData(svcRecord)

        limitDateFormat = "%d %B %Y"

        if exhaustive:
            listEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            listStart = datetime(day=1, month=1,
                                 year=1980)  # The beginning of time
        else:
            listEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            listStart = listEnd - timedelta(days=20)  # Doesn't really matter

        lastActivityDay = None
        discoveredWorkoutIds = []
        while True:
            reqData.update({
                "startDate": listStart.strftime(limitDateFormat),
                "endDate": listEnd.strftime(limitDateFormat)
            })
            print("Requesting %s to %s" % (listStart, listEnd))
            resp = requests.post(
                "https://www.trainingpeaks.com/tpwebservices/service.asmx/GetWorkoutsForAthlete",
                data=reqData)
            xresp = etree.XML(resp.content)
            for xworkout in xresp:
                activity = UploadedActivity()

                workoutId = xworkout.find("tpw:WorkoutId", namespaces=ns).text

                workoutDayEl = xworkout.find("tpw:WorkoutDay", namespaces=ns)
                startTimeEl = xworkout.find("tpw:StartTime", namespaces=ns)

                workoutDay = dateutil.parser.parse(workoutDayEl.text)
                startTime = dateutil.parser.parse(
                    startTimeEl.text
                ) if startTimeEl is not None and startTimeEl.text else None

                if lastActivityDay is None or workoutDay.replace(
                        tzinfo=None) > lastActivityDay:
                    lastActivityDay = workoutDay.replace(tzinfo=None)

                if startTime is None:
                    continue  # Planned but not executed yet.
                activity.StartTime = startTime

                endTimeEl = xworkout.find("tpw:TimeTotalInSeconds",
                                          namespaces=ns)
                if not endTimeEl.text:
                    exclusions.append(
                        APIExcludeActivity("Activity has no duration",
                                           activityId=workoutId,
                                           userException=UserException(
                                               UserExceptionType.Corrupt)))
                    continue

                activity.EndTime = activity.StartTime + timedelta(
                    seconds=float(endTimeEl.text))

                distEl = xworkout.find("tpw:DistanceInMeters", namespaces=ns)
                if distEl.text:
                    activity.Stats.Distance = ActivityStatistic(
                        ActivityStatisticUnit.Meters, value=float(distEl.text))
                # PWX is damn near comprehensive, no need to fill in any of the other statisitcs here, really

                if workoutId in discoveredWorkoutIds:
                    continue  # There's the possibility of query overlap, if there are multiple activities on a single day that fall across the query return limit
                discoveredWorkoutIds.append(workoutId)

                workoutTypeEl = xworkout.find("tpw:WorkoutTypeDescription",
                                              namespaces=ns)
                if workoutTypeEl.text:
                    if workoutTypeEl.text == "Day Off":
                        continue  # TrainingPeaks has some weird activity types...
                    if workoutTypeEl.text not in self._workoutTypeMappings:
                        exclusions.append(
                            APIExcludeActivity("Activity type %s unknown" %
                                               workoutTypeEl.text,
                                               activityId=workoutId,
                                               userException=UserException(
                                                   UserExceptionType.Corrupt)))
                        continue
                    activity.Type = self._workoutTypeMappings[
                        workoutTypeEl.text]

                activity.ServiceData = {"WorkoutID": workoutId}
                activity.CalculateUID()
                activities.append(activity)

            if not exhaustive:
                break

            # Since TP only lets us query by date range, to get full activity history we need to query successively smaller ranges
            if len(xresp):
                if listStart == lastActivityDay:
                    break  # This wouldn't work if you had more than #MaxQueryReturn activities on that day - but that number is probably 50+
                listStart = lastActivityDay
            else:
                break  # We're done

        return activities, exclusions
Beispiel #16
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        #http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?&start=0&limit=50
        session = self._get_session(record=serviceRecord)
        page = 1
        pageSz = 100
        activities = []
        exclusions = []
        while True:
            logger.debug("Req with " + str({
                "start": (page - 1) * pageSz,
                "limit": pageSz
            }))
            self._rate_limit()

            retried_auth = False
            while True:
                res = session.get(
                    "http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities",
                    params={
                        "start": (page - 1) * pageSz,
                        "limit": pageSz
                    })
                # It's 10 PM and I have no clue why it's throwing these errors, maybe we just need to log in again?
                if res.status_code == 403 and not retried_auth:
                    retried_auth = True
                    session = self._get_session(serviceRecord, skip_cache=True)
                else:
                    break
            try:
                res = res.json()["results"]
            except ValueError:
                res_txt = res.text  # So it can capture in the log message
                raise APIException("Parse failure in GC list resp: %s" %
                                   res.status_code)
            if "activities" not in res:
                break  # No activities on this page - empty account.
            for act in res["activities"]:
                act = act["activity"]
                if "sumDistance" not in act:
                    exclusions.append(
                        APIExcludeActivity("No distance",
                                           activityId=act["activityId"],
                                           userException=UserException(
                                               UserExceptionType.Corrupt)))
                    continue
                activity = UploadedActivity()

                # Don't really know why sumSampleCountTimestamp doesn't appear in swim activities - they're definitely timestamped...
                activity.Stationary = "sumSampleCountSpeed" not in act and "sumSampleCountTimestamp" not in act
                activity.GPS = "endLatitude" in act

                activity.Private = act["privacy"]["key"] == "private"

                try:
                    activity.TZ = pytz.timezone(act["activityTimeZone"]["key"])
                except pytz.exceptions.UnknownTimeZoneError:
                    activity.TZ = pytz.FixedOffset(
                        float(act["activityTimeZone"]["offset"]) * 60)

                logger.debug("Name " + act["activityName"]["value"] + ":")
                if len(act["activityName"]["value"].strip(
                )) and act["activityName"][
                        "value"] != "Untitled":  # This doesn't work for internationalized accounts, oh well.
                    activity.Name = act["activityName"]["value"]

                if len(act["activityDescription"]["value"].strip()):
                    activity.Notes = act["activityDescription"]["value"]

                # beginTimestamp/endTimestamp is in UTC
                activity.StartTime = pytz.utc.localize(
                    datetime.utcfromtimestamp(
                        float(act["beginTimestamp"]["millis"]) / 1000))
                if "sumElapsedDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        0, round(float(act["sumElapsedDuration"]["value"])))
                elif "sumDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        minutes=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[0]),
                        seconds=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[1]))
                else:
                    activity.EndTime = pytz.utc.localize(
                        datetime.utcfromtimestamp(
                            float(act["endTimestamp"]["millis"]) / 1000))
                logger.debug("Activity s/t " + str(activity.StartTime) +
                             " on page " + str(page))
                activity.AdjustTZ()

                # TODO: fix the distance stats to account for the fact that this incorrectly reported km instead of meters for the longest time.
                activity.Stats.Distance = ActivityStatistic(
                    self._unitMap[act["sumDistance"]["uom"]],
                    value=float(act["sumDistance"]["value"]))

                activity.Type = self._resolveActivityType(
                    act["activityType"]["key"])

                activity.CalculateUID()

                activity.ServiceData = {"ActivityID": int(act["activityId"])}

                activities.append(activity)
            logger.debug("Finished page " + str(page) + " of " +
                         str(res["search"]["totalPages"]))
            if not exhaustive or int(res["search"]["totalPages"]) == page:
                break
            else:
                page += 1
        return activities, exclusions
Beispiel #17
0
    def DownloadActivityList(self, svcRecord, exhaustive=False):
        activities = []
        exclusions = []
        before = earliestDate = None

        # define low parameter
        limit = 20
        offset = 0
        sort = "desc"
        # get user Fitbit ID
        userID = svcRecord.ExternalID
        # get service Tapiriik ID
        service_id = svcRecord._id
        # get user "start sync from date" info
        # then prepare afterDate var (this var determine the date since we download activities)
        user = db.users.find_one({'ConnectedServices': {'$elemMatch': {'ID': service_id, 'Service': 'fitbit'}}})
        afterDateObj = datetime.now() - timedelta(days=1)

        if user['Config']['sync_skip_before'] is not None:
            afterDateObj = user['Config']['sync_skip_before']
        else:
            if exhaustive:
                afterDateObj = datetime.now() - timedelta(days=3650) # throw back to 10 years

        afterDate = afterDateObj.strftime("%Y-%m-%d")
        logging.info("\t Download Fitbit activities since : " + afterDate)

        # prepare parameters to set in fitbit request uri
        uri_parameters = {
            'limit': limit,
            'offset': offset,
            'sort': sort,
            'afterDate': afterDate,
            'token': svcRecord.Authorization.get('AccessToken')
        }
        # set base fitbit request uri
        activities_uri_origin = 'https://api.fitbit.com/1/user/' + userID + '/activities/list.json'

        # first execute offset = 0,
        # offset will be set to -1 if fitbit response don't give next pagination info
        # offset will be incremented by 1 if fitbit response give next pagination info
        index_total = 0
        while offset > -1:

            # prepare uri parameters
            uri_parameters['offset'] = offset
            # build fitbit uri with new parameters
            activities_uri = activities_uri_origin + "?" + urlencode(uri_parameters)
            # execute fitbit request using "request with auth" function (it refreshes token if needed)
            logging.info("\t\t downloading offset : " + str(offset))
            resp = self._requestWithAuth(lambda session: session.get(
                activities_uri,
                headers={
                    'Authorization': 'Bearer ' + svcRecord.Authorization.get('AccessToken')
                }), svcRecord)

            # check if request has error
            if resp.status_code != 204 and resp.status_code != 200:
                raise APIException("Unable to find Fitbit activities")

            # get request data
            data = {}
            try:
                data = resp.json()
            except ValueError:
                raise APIException("Failed parsing fitbit list response %s - %s" % (resp.status_code, resp.text))

            # if request return activities infos
            if data['activities']:
                ftbt_activities = data['activities']
                logging.info("\t\t nb activity : " + str(len(ftbt_activities)))

                # for every activities in this request pagination
                # (Fitbit give 20 activities MAXIMUM, use limit parameter)
                for ftbt_activity in ftbt_activities:
                    index_total = index_total +1
                    activity = UploadedActivity()

                    #parse date start to get timezone and date
                    parsedDate = ftbt_activity["startTime"][0:19] + ftbt_activity["startTime"][23:]
                    activity.StartTime = datetime.strptime(parsedDate, "%Y-%m-%dT%H:%M:%S%z")
                    activity.TZ = pytz.utc

                    logger.debug("\tActivity s/t %s: %s" % (activity.StartTime, ftbt_activity["activityName"]))

                    activity.EndTime = activity.StartTime + timedelta(0, (ftbt_activity["duration"]/1000))
                    activity.ServiceData = {"ActivityID": ftbt_activity["logId"], "Manual": ftbt_activity["logType"]}

                    # check if activity type ID exists
                    if ftbt_activity["activityTypeId"] not in self._reverseActivityTypeMappings:
                        exclusions.append(APIExcludeActivity("Unsupported activity type %s" % ftbt_activity["activityTypeId"],
                                                             activity_id=ftbt_activity["logId"],
                                                             user_exception=UserException(UserExceptionType.Other)))
                        logger.info("\t\tUnknown activity")
                        continue

                    activity.Type = self._reverseActivityTypeMappings[ftbt_activity["activityTypeId"]]

                    activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Kilometers,
                                                                value=ftbt_activity["distance"])

                    if "speed" in ftbt_activity:
                        activity.Stats.Speed = ActivityStatistic(
                            ActivityStatisticUnit.KilometersPerHour,
                            avg=ftbt_activity["speed"],
                            max=ftbt_activity["speed"]
                        )
                    activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=ftbt_activity["calories"])
                    # Todo: find fitbit data name
                    #activity.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=ride[
                    #    "moving_time"] if "moving_time" in ride and ride[
                    #    "moving_time"] > 0 else None)  # They don't let you manually enter this, and I think it returns 0 for those activities.
                    # Todo: find fitbit data name
                    #if "average_watts" in ride:
                    #    activity.Stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts,
                    #                                             avg=ride["average_watts"])

                    if "averageHeartRate" in ftbt_activity:
                        activity.Stats.HR.update(
                            ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=ftbt_activity["averageHeartRate"]))
                    # Todo: find fitbit data name
                    #if "max_heartrate" in ride:
                    #    activity.Stats.HR.update(
                    #        ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=ride["max_heartrate"]))
                    # Todo: find fitbit data name
                    #if "average_cadence" in ride:
                    #    activity.Stats.Cadence.update(ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute,
                    #                                                    avg=ride["average_cadence"]))
                    # Todo: find fitbit data name
                    #if "average_temp" in ride:
                    #    activity.Stats.Temperature.update(
                    #        ActivityStatistic(ActivityStatisticUnit.DegreesCelcius, avg=ride["average_temp"]))

                    if "calories" in ftbt_activity:
                        activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories,
                                                                  value=ftbt_activity["calories"])
                    activity.Name = ftbt_activity["activityName"]


                    activity.Private = False
                    if ftbt_activity['logType'] is 'manual':
                        activity.Stationary = True
                    else:
                        activity.Stationary = False


                    # Todo: find fitbit data
                    #activity.GPS = ("start_latlng" in ride) and (ride["start_latlng"] is not None)
                    activity.AdjustTZ()
                    activity.CalculateUID()
                    activities.append(activity)
                    logging.info("\t\t Fitbit Activity ID : " + str(ftbt_activity["logId"]))

                if not exhaustive:
                    break
            # get next info for while condition and prepare offset for next request
            if 'next' not in data['pagination'] or not data['pagination']['next']:
                next = None
                offset = -1
            else:
                next = data['pagination']['next']
                offset = offset + 1

        logging.info("\t\t total Fitbit activities downloaded : " + str(index_total))
        return activities, exclusions
Beispiel #18
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        def mapStatTriple(act, stats_obj, key, units):
            if "%s_max" % key in act and act["%s_max" % key]:
                stats_obj.update(
                    ActivityStatistic(units, max=float(act["%s_max" % key])))
            if "%s_min" % key in act and act["%s_min" % key]:
                stats_obj.update(
                    ActivityStatistic(units, min=float(act["%s_min" % key])))
            if "%s_avg" % key in act and act["%s_avg" % key]:
                stats_obj.update(
                    ActivityStatistic(units, avg=float(act["%s_avg" % key])))

        # http://ridewithgps.com/users/1/trips.json?limit=200&order_by=created_at&order_dir=asc
        # offset also supported
        activities = []
        exclusions = []
        # They don't actually support paging right now, for whatever reason
        params = self._add_auth_params({}, record=serviceRecord)

        res = requests.get(
            "https://ridewithgps.com/users/{}/trips.json".format(
                serviceRecord.ExternalID),
            params=params)
        res = res.json()

        # Apparently some API users are seeing this new result format - I'm not
        if type(res) is dict:
            res = res.get("results", [])

        if res == []:
            return [], []  # No activities
        for act in res:
            if "distance" not in act:
                exclusions.append(
                    APIExcludeActivity("No distance",
                                       activity_id=act["id"],
                                       user_exception=UserException(
                                           UserExceptionType.Corrupt)))
                continue
            if "duration" not in act or not act["duration"]:
                exclusions.append(
                    APIExcludeActivity("No duration",
                                       activity_id=act["id"],
                                       user_exception=UserException(
                                           UserExceptionType.Corrupt)))
                continue
            activity = UploadedActivity()

            logger.debug("Name " + act["name"] + ":")
            if len(act["name"].strip()):
                activity.Name = act["name"]

            if len(act["description"].strip()):
                activity.Notes = act["description"]

            activity.GPS = act["is_gps"]
            activity.Stationary = not activity.GPS  # I think

            # 0 = public, 1 = private, 2 = friends
            activity.Private = act["visibility"] == 1

            activity.StartTime = dateutil.parser.parse(act["departed_at"])

            try:
                activity.TZ = pytz.timezone(act["time_zone"])
            except pytz.exceptions.UnknownTimeZoneError:
                # Sometimes the time_zone returned isn't quite what we'd like it
                # So, just pull the offset from the datetime
                if isinstance(activity.StartTime.tzinfo, tzutc):
                    activity.TZ = pytz.utc  # The dateutil tzutc doesn't have an _offset value.
                else:
                    activity.TZ = pytz.FixedOffset(
                        activity.StartTime.tzinfo.utcoffset(
                            activity.StartTime).total_seconds() / 60)

            activity.StartTime = activity.StartTime.replace(
                tzinfo=activity.TZ)  # Overwrite dateutil's sillyness

            activity.EndTime = activity.StartTime + timedelta(
                seconds=self._duration_to_seconds(act["duration"]))
            logger.debug("Activity s/t " + str(activity.StartTime))
            activity.AdjustTZ()

            activity.Stats.Distance = ActivityStatistic(
                ActivityStatisticUnit.Meters, float(act["distance"]))

            mapStatTriple(act, activity.Stats.Power, "watts",
                          ActivityStatisticUnit.Watts)
            mapStatTriple(act, activity.Stats.Speed, "speed",
                          ActivityStatisticUnit.KilometersPerHour)
            mapStatTriple(act, activity.Stats.Cadence, "cad",
                          ActivityStatisticUnit.RevolutionsPerMinute)
            mapStatTriple(act, activity.Stats.HR, "hr",
                          ActivityStatisticUnit.BeatsPerMinute)

            if "elevation_gain" in act and act["elevation_gain"]:
                activity.Stats.Elevation.update(
                    ActivityStatistic(ActivityStatisticUnit.Meters,
                                      gain=float(act["elevation_gain"])))

            if "elevation_loss" in act and act["elevation_loss"]:
                activity.Stats.Elevation.update(
                    ActivityStatistic(ActivityStatisticUnit.Meters,
                                      loss=float(act["elevation_loss"])))

            # Activity type is not implemented yet in RWGPS results; we will assume cycling, though perhaps "OTHER" wouuld be correct
            activity.Type = ActivityType.Cycling

            activity.CalculateUID()
            activity.ServiceData = {"ActivityID": act["id"]}
            activities.append(activity)
        return activities, exclusions
Beispiel #19
0
    def DownloadActivityList(self, svcRecord, exhaustive_start_time=None):
        activities = []
        exclusions = []

        headers = self._apiHeaders(svcRecord)

        limitDateFormat = "%Y-%m-%d"

        if exhaustive_start_time:
            totalListEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            totalListStart = exhaustive_start_time - timedelta(days=1.5)
        else:
            totalListEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            totalListStart = totalListEnd - timedelta(
                days=20)  # Doesn't really matter

        listStep = timedelta(days=45)
        listEnd = totalListEnd
        listStart = max(totalListStart, totalListEnd - listStep)

        while True:
            logger.debug("Requesting %s to %s" % (listStart, listEnd))
            resp = requests.get(TRAININGPEAKS_API_BASE_URL +
                                "/v1/workouts/%s/%s" %
                                (listStart.strftime(limitDateFormat),
                                 listEnd.strftime(limitDateFormat)),
                                headers=headers)

            for act in resp.json():
                if not act.get("completed", True):
                    continue
                activity = UploadedActivity()
                activity.StartTime = dateutil.parser.parse(
                    act["StartTime"]).replace(tzinfo=None)
                logger.debug("Activity s/t " + str(activity.StartTime))
                activity.EndTime = activity.StartTime + timedelta(
                    hours=act["TotalTime"])
                activity.Name = act.get("Title", None)
                activity.Notes = act.get("Description", None)
                activity.Type = self._workoutTypeMappings.get(
                    act.get("WorkoutType", "").lower(), ActivityType.Other)

                activity.Stats.Cadence = ActivityStatistic(
                    ActivityStatisticUnit.RevolutionsPerMinute,
                    avg=act.get("CadenceAverage", None),
                    max=act.get("CadenceMaximum", None))
                activity.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Meters,
                    value=act.get("Distance", None))
                activity.Stats.Elevation = ActivityStatistic(
                    ActivityStatisticUnit.Meters,
                    avg=act.get("ElevationAverage", None),
                    min=act.get("ElevationMinimum", None),
                    max=act.get("ElevationMaximum", None),
                    gain=act.get("ElevationGain", None),
                    loss=act.get("ElevationLoss", None))
                activity.Stats.Energy = ActivityStatistic(
                    ActivityStatisticUnit.Kilojoules,
                    value=act.get("Energy", None))
                activity.Stats.HR = ActivityStatistic(
                    ActivityStatisticUnit.BeatsPerMinute,
                    avg=act.get("HeartRateAverage", None),
                    min=act.get("HeartRateMinimum", None),
                    max=act.get("HeartRateMaximum", None))
                activity.Stats.Power = ActivityStatistic(
                    ActivityStatisticUnit.Watts,
                    avg=act.get("PowerAverage", None),
                    max=act.get("PowerMaximum", None))
                activity.Stats.Temperature = ActivityStatistic(
                    ActivityStatisticUnit.DegreesCelcius,
                    avg=act.get("TemperatureAverage", None),
                    min=act.get("TemperatureMinimum", None),
                    max=act.get("TemperatureMaximum", None))
                activity.Stats.Speed = ActivityStatistic(
                    ActivityStatisticUnit.MetersPerSecond,
                    avg=act.get("VelocityAverage", None),
                    max=act.get("VelocityMaximum", None))
                activity.CalculateUID()
                activities.append(activity)

            if not exhaustive_start_time:
                break

            listStart -= listStep
            listEnd -= listStep
            if listEnd < totalListStart:
                break

        return activities, exclusions
    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)
Beispiel #21
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        """
        GET List of Activities as JSON File

        URL: http://app.velohero.com/export/workouts/json
        Parameters:
        user      = username
        pass      = password
        date_from = YYYY-MM-DD
        date_to   = YYYY-MM-DD
        """
        activities = []
        exclusions = []
        discoveredWorkoutIds = []

        params = self._add_auth_params({}, record=serviceRecord)

        limitDateFormat = "%Y-%m-%d"

        if exhaustive:
            listEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            listStart = datetime(day=1, month=1,
                                 year=1980)  # The beginning of time
        else:
            listEnd = datetime.now() + timedelta(
                days=1.5)  # Who knows which TZ it's in
            listStart = listEnd - timedelta(days=20)  # Doesn't really matter

        params.update({
            "date_from": listStart.strftime(limitDateFormat),
            "date_to": listEnd.strftime(limitDateFormat)
        })
        logger.debug("Requesting %s to %s" % (listStart, listEnd))
        res = requests.get(self._urlRoot + "/export/workouts/json",
                           params=params)

        if res.status_code != 200:
            if res.status_code == 403:
                raise APIException("Invalid login",
                                   block=True,
                                   user_exception=UserException(
                                       UserExceptionType.Authorization,
                                       intervention_required=True))
            raise APIException("Unable to retrieve activity list")

        res.raise_for_status()
        try:
            res = res.json()
        except ValueError:
            raise APIException("Could not decode activity list")
        if "workouts" not in res:
            raise APIException("No activities")
        for workout in res["workouts"]:
            workoutId = int(workout["id"])
            if workoutId in discoveredWorkoutIds:
                continue  # There's the possibility of query overlap
            discoveredWorkoutIds.append(workoutId)
            if workout["file"] is not "1":
                logger.debug("Skip workout with ID: " + str(workoutId) +
                             " (no file)")
                continue  # Skip activity without samples (no PWX export)

            activity = UploadedActivity()

            logger.debug("Workout ID: " + str(workoutId))
            # Duration (dur_time)
            duration = self._durationToSeconds(workout["dur_time"])
            activity.Stats.TimerTime = ActivityStatistic(
                ActivityStatisticUnit.Seconds, value=duration)
            # Start time (date_ymd, start_time)
            startTimeStr = workout["date_ymd"] + " " + workout["start_time"]
            activity.StartTime = self._parseDateTime(startTimeStr)
            # End time (date_ymd, start_time) + dur_time
            activity.EndTime = self._parseDateTime(startTimeStr) + timedelta(
                seconds=duration)
            # Sport (sport_id)
            if workout["sport_id"] in self._reverseActivityMappings:
                activity.Type = self._reverseActivityMappings[
                    workout["sport_id"]]
            else:
                activity.Type = ActivityType.Other
            # Distance (dist_km)
            activity.Stats.Distance = ActivityStatistic(
                ActivityStatisticUnit.Kilometers,
                value=float(workout["dist_km"]))
            # Workout is hidden
            activity.Private = workout["hide"] == "1"

            activity.ServiceData = {"workoutId": workoutId}
            activity.CalculateUID()
            activities.append(activity)

        return activities, exclusions
Beispiel #22
0
    def _populate_sbr_activity(self, api_sbr_activity, usersettings):
        # Example JSON feed (unimportant fields have been removed)
        # [{
        #    "EventId": 63128401,                   #  Internal ID
        #    "EventType": 3,                        #  Swim (3), bike (1), or run (2)
        #    "EventDate": "4/22/2016",
        #    "EventTime": "7:44 AM",                #  User's time, time zone not specified
        #    "Planned": false,                      #  Training plan or actual data
        #    "TotalMinutes": 34.97,
        #    "TotalKilometers": 1.55448,
        #    "AverageHeartRate": 125,
        #    "MinimumHeartRate": 100,
        #    "MaximumHeartRate": 150,
        #    "MemberId": 999999,
        #    "MemberUsername": "******",
        #    "HasDeviceUpload": true,
        #    "DeviceUploadFile": "http://beginnertriathlete.com/discussion/storage/workouts/555555/abcd-123.fit",
        #    "RouteName": "",                       #  Might contain a description of the event
        #    "Comments": "",                        #  User supplied notes
        # }, ... ]

        activity = UploadedActivity()
        workout_id = api_sbr_activity["EventId"]
        eventType = api_sbr_activity["EventType"]
        eventDate = api_sbr_activity["EventDate"]
        eventTime = api_sbr_activity["EventTime"]
        totalMinutes = api_sbr_activity["TotalMinutes"]
        totalKms = api_sbr_activity["TotalKilometers"]
        averageHr = api_sbr_activity["AverageHeartRate"]
        minimumHr = api_sbr_activity["MinimumHeartRate"]
        maximumHr = api_sbr_activity["MaximumHeartRate"]
        deviceUploadFile = api_sbr_activity["DeviceUploadFile"]
        comments = api_sbr_activity["Comments"]

        # Basic SBR data does not include GPS or sensor data. If this event originated from a device upload,
        # DownloadActivity will find it.
        activity.Stationary = True

        # Same as above- The data might be there, but it's not supplied in the basic activity feed.
        activity.GPS = False

        activity.Notes = comments
        activity.Private = usersettings["Privacy"]
        activity.Type = self._workoutTypeMappings[str(eventType)]

        # Get the user's timezone from their profile. (Activity.TZ should be mentioned in the object hierarchy docs?)
        # Question: I believe if DownloadActivity finds device data, it will overwrite this. Which is OK with me.
        # The device data will most likely be more accurate.
        try:
            activity.TZ = pytz.timezone(usersettings["TimeZone"])
        except pytz.exceptions.UnknownTimeZoneError:
            activity.TZ = pytz.timezone(self._serverDefaultTimezone)

        # activity.StartTime and EndTime aren't mentioned in the object hierarchy docs, but I see them
        # set in all the other providers.
        activity.StartTime = dateutil.parser.parse(
            eventDate + " " + eventTime,
            dayfirst=False).replace(tzinfo=activity.TZ)
        activity.EndTime = activity.StartTime + timedelta(minutes=totalMinutes)

        # We can calculate some metrics from the supplied data. Would love to see some non-source code documentation
        # on each statistic and what it expects as input.
        activity.Stats.Distance = ActivityStatistic(
            ActivityStatisticUnit.Kilometers, value=totalKms)
        activity.Stats.HR = ActivityStatistic(
            ActivityStatisticUnit.BeatsPerMinute,
            avg=float(averageHr),
            min=float(minimumHr),
            max=float(maximumHr))
        activity.Stats.MovingTime = ActivityStatistic(
            ActivityStatisticUnit.Seconds, value=float(totalMinutes * 60))
        activity.Stats.TimerTime = ActivityStatistic(
            ActivityStatisticUnit.Seconds, value=float(totalMinutes * 60))
        # While BT does support laps, the current API doesn't report on them - a limitation that may need to be
        # corrected in a future update. For now, treat manual entries as a single lap. As more and more people upload
        # workouts using devices anyway, this probably matters much less than it once did.
        lap = Lap(stats=activity.Stats,
                  startTime=activity.StartTime,
                  endTime=activity.EndTime)
        activity.Laps = [lap]

        # Not 100% positive how this is utilized, but it is common for all providers. Detects duplicate downloads?
        activity.CalculateUID()

        # If a device file is attached, we'll get more details about this event in DownloadActivity
        activity.ServiceData = {
            "ID": int(workout_id),
            "DeviceUploadFile": deviceUploadFile
        }

        return activity
 def test_stat_coalesce(self):
     stat1 = ActivityStatistic(ActivityStatisticUnit.Meters, value=1)
     stat2 = ActivityStatistic(ActivityStatisticUnit.Meters, value=2)
     stat1.coalesceWith(stat2)
     self.assertEqual(stat1.Value, 1.5)
Beispiel #24
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        headers = self._getAuthHeaders(serviceRecord)
        activities = []
        exclusions = []
        pageUri = self.OpenFitEndpoint + "/fitnessActivities.json"

        activity_tz_cache_raw = cachedb.sporttracks_meta_cache.find_one(
            {"ExternalID": serviceRecord.ExternalID})
        activity_tz_cache_raw = activity_tz_cache_raw if activity_tz_cache_raw else {
            "Activities": []
        }
        activity_tz_cache = dict([(x["ActivityURI"], x["TZ"])
                                  for x in activity_tz_cache_raw["Activities"]
                                  ])

        while True:
            logger.debug("Req against " + pageUri)
            res = requests.get(pageUri, headers=headers)
            try:
                res = res.json()
            except ValueError:
                raise APIException(
                    "Could not decode activity list response %s %s" %
                    (res.status_code, res.text))
            for act in res["items"]:
                activity = UploadedActivity()
                activity.ServiceData = {"ActivityURI": act["uri"]}

                if len(act["name"].strip()):
                    activity.Name = act["name"]
                    # Longstanding ST.mobi bug causes it to return negative partial-hour timezones as "-2:-30" instead of "-2:30"
                fixed_start_time = re.sub(r":-(\d\d)", r":\1",
                                          act["start_time"])
                activity.StartTime = dateutil.parser.parse(fixed_start_time)
                if isinstance(activity.StartTime.tzinfo, tzutc):
                    activity.TZ = pytz.utc  # The dateutil tzutc doesn't have an _offset value.
                else:
                    activity.TZ = pytz.FixedOffset(
                        activity.StartTime.tzinfo.utcoffset(
                            activity.StartTime).total_seconds() / 60
                    )  # Convert the dateutil lame timezones into pytz awesome timezones.

                activity.StartTime = activity.StartTime.replace(
                    tzinfo=activity.TZ)
                activity.EndTime = activity.StartTime + timedelta(
                    seconds=float(act["duration"]))
                activity.Stats.TimerTime = ActivityStatistic(
                    ActivityStatisticUnit.Seconds,
                    value=float(act["duration"]
                                ))  # OpenFit says this excludes paused times.

                # Sometimes activities get returned with a UTC timezone even when they are clearly not in UTC.
                if activity.TZ == pytz.utc:
                    if act["uri"] in activity_tz_cache:
                        activity.TZ = pytz.FixedOffset(
                            activity_tz_cache[act["uri"]])
                    else:
                        # So, we get the first location in the activity and calculate the TZ from that.
                        try:
                            firstLocation = self._downloadActivity(
                                serviceRecord,
                                activity,
                                returnFirstLocation=True)
                        except APIExcludeActivity:
                            pass
                        else:
                            try:
                                activity.CalculateTZ(firstLocation,
                                                     recalculate=True)
                            except:
                                # We tried!
                                pass
                            else:
                                activity.AdjustTZ()
                            finally:
                                activity_tz_cache[
                                    act["uri"]] = activity.StartTime.utcoffset(
                                    ).total_seconds() / 60

                logger.debug("Activity s/t " + str(activity.StartTime))
                activity.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Meters,
                    value=float(act["total_distance"]))

                types = [x.strip().lower() for x in act["type"].split(":")]
                types.reverse(
                )  # The incoming format is like "walking: hiking" and we want the most specific first
                activity.Type = None
                for type_key in types:
                    if type_key in self._activityMappings:
                        activity.Type = self._activityMappings[type_key]
                        break
                if not activity.Type:
                    exclusions.append(
                        APIExcludeActivity("Unknown activity type %s" %
                                           act["type"],
                                           activity_id=act["uri"],
                                           user_exception=UserException(
                                               UserExceptionType.Other)))
                    continue

                activity.CalculateUID()
                activities.append(activity)
            if not exhaustive or "next" not in res or not len(res["next"]):
                break
            else:
                pageUri = res["next"]
        logger.debug("Writing back meta cache")
        cachedb.sporttracks_meta_cache.update(
            {"ExternalID": serviceRecord.ExternalID}, {
                "ExternalID":
                serviceRecord.ExternalID,
                "Activities": [{
                    "ActivityURI": k,
                    "TZ": v
                } for k, v in activity_tz_cache.items()]
            },
            upsert=True)
        return activities, exclusions
    def 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)
Beispiel #26
0
    def _downloadActivity(self,
                          serviceRecord,
                          activity,
                          returnFirstLocation=False):
        activityURI = activity.ServiceData["ActivityURI"]
        headers = self._getAuthHeaders(serviceRecord)
        activityData = requests.get(activityURI, headers=headers)
        activityData = activityData.json()

        if "clock_duration" in activityData:
            activity.EndTime = activity.StartTime + timedelta(
                seconds=float(activityData["clock_duration"]))

        activity.Private = "sharing" in activityData and activityData[
            "sharing"] != "public"

        activity.GPS = False  # Gets set back if there is GPS data

        if "notes" in activityData:
            activity.Notes = activityData["notes"]

        activity.Stats.Energy = ActivityStatistic(
            ActivityStatisticUnit.Kilojoules,
            value=float(activityData["calories"]))

        activity.Stats.Elevation = ActivityStatistic(
            ActivityStatisticUnit.Meters,
            gain=float(activityData["elevation_gain"])
            if "elevation_gain" in activityData else None,
            loss=float(activityData["elevation_loss"])
            if "elevation_loss" in activityData else None)

        activity.Stats.HR = ActivityStatistic(
            ActivityStatisticUnit.BeatsPerMinute,
            avg=activityData["avg_heartrate"]
            if "avg_heartrate" in activityData else None,
            max=activityData["max_heartrate"]
            if "max_heartrate" in activityData else None)
        activity.Stats.Cadence = ActivityStatistic(
            ActivityStatisticUnit.RevolutionsPerMinute,
            avg=activityData["avg_cadence"]
            if "avg_cadence" in activityData else None,
            max=activityData["max_cadence"]
            if "max_cadence" in activityData else None)
        activity.Stats.Power = ActivityStatistic(
            ActivityStatisticUnit.Watts,
            avg=activityData["avg_power"]
            if "avg_power" in activityData else None,
            max=activityData["max_power"]
            if "max_power" in activityData else None)

        laps_info = []
        laps_starts = []
        if "laps" in activityData:
            laps_info = activityData["laps"]
            for lap in activityData["laps"]:
                laps_starts.append(dateutil.parser.parse(lap["start_time"]))
        lap = None
        for lapinfo in laps_info:
            lap = Lap()
            activity.Laps.append(lap)
            lap.StartTime = dateutil.parser.parse(lapinfo["start_time"])
            lap.EndTime = lap.StartTime + timedelta(
                seconds=lapinfo["clock_duration"])
            if "type" in lapinfo:
                lap.Intensity = LapIntensity.Active if lapinfo[
                    "type"] == "ACTIVE" else LapIntensity.Rest
            if "distance" in lapinfo:
                lap.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Meters,
                    value=float(lapinfo["distance"]))
            if "duration" in lapinfo:
                lap.Stats.TimerTime = ActivityStatistic(
                    ActivityStatisticUnit.Seconds, value=lapinfo["duration"])
            if "calories" in lapinfo:
                lap.Stats.Energy = ActivityStatistic(
                    ActivityStatisticUnit.Kilojoules,
                    value=lapinfo["calories"])
            if "elevation_gain" in lapinfo:
                lap.Stats.Elevation.update(
                    ActivityStatistic(ActivityStatisticUnit.Meters,
                                      gain=float(lapinfo["elevation_gain"])))
            if "elevation_loss" in lapinfo:
                lap.Stats.Elevation.update(
                    ActivityStatistic(ActivityStatisticUnit.Meters,
                                      loss=float(lapinfo["elevation_loss"])))
            if "max_speed" in lapinfo:
                lap.Stats.Speed.update(
                    ActivityStatistic(ActivityStatisticUnit.MetersPerSecond,
                                      max=float(lapinfo["max_speed"])))
            if "max_speed" in lapinfo:
                lap.Stats.Speed.update(
                    ActivityStatistic(ActivityStatisticUnit.MetersPerSecond,
                                      max=float(lapinfo["max_speed"])))
            if "avg_speed" in lapinfo:
                lap.Stats.Speed.update(
                    ActivityStatistic(ActivityStatisticUnit.MetersPerSecond,
                                      avg=float(lapinfo["avg_speed"])))
            if "max_heartrate" in lapinfo:
                lap.Stats.HR.update(
                    ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                      max=float(lapinfo["max_heartrate"])))
            if "avg_heartrate" in lapinfo:
                lap.Stats.HR.update(
                    ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                      avg=float(lapinfo["avg_heartrate"])))
        if lap is None:  # No explicit laps => make one that encompasses the entire activity
            lap = Lap()
            activity.Laps.append(lap)
            lap.Stats = activity.Stats
            lap.StartTime = activity.StartTime
            lap.EndTime = activity.EndTime
        elif len(activity.Laps) == 1:
            activity.Stats.update(
                activity.Laps[0].Stats
            )  # Lap stats have a bit more info generally.
            activity.Laps[0].Stats = activity.Stats

        timerStops = []
        if "timer_stops" in activityData:
            for stop in activityData["timer_stops"]:
                timerStops.append([
                    dateutil.parser.parse(stop[0]),
                    dateutil.parser.parse(stop[1])
                ])

        def isInTimerStop(timestamp):
            for stop in timerStops:
                if timestamp >= stop[0] and timestamp < stop[1]:
                    return True
                if timestamp >= stop[1]:
                    return False
            return False

        # Collate the individual streams into our waypoints.
        # Global sample rate is variable - will pick the next nearest stream datapoint.
        # Resampling happens on a lookbehind basis - new values will only appear their timestamp has been reached/passed

        wasInPause = False
        currentLapIdx = 0
        lap = activity.Laps[currentLapIdx]

        streams = []
        for stream in [
                "location", "elevation", "heartrate", "power", "cadence",
                "distance"
        ]:
            if stream in activityData:
                streams.append(stream)
        stream_indices = dict([(stream, -1) for stream in streams
                               ])  # -1 meaning the stream has yet to start
        stream_lengths = dict([(stream, len(activityData[stream]) / 2)
                               for stream in streams])
        # Data comes as "stream":[timestamp,value,timestamp,value,...]
        stream_values = {}
        for stream in streams:
            values = []
            for x in range(0, int(len(activityData[stream]) / 2)):
                values.append((activityData[stream][x * 2],
                               activityData[stream][x * 2 + 1]))
            stream_values[stream] = values

        currentOffset = 0

        def streamVal(stream):
            nonlocal stream_values, stream_indices
            return stream_values[stream][stream_indices[stream]][1]

        def hasStreamData(stream):
            nonlocal stream_indices, streams
            return stream in streams and stream_indices[stream] >= 0

        while True:
            advance_stream = None
            advance_offset = None
            for stream in streams:
                if stream_indices[stream] + 1 == stream_lengths[stream]:
                    continue  # We're at the end - can't advance
                if advance_offset is None or stream_values[stream][
                        stream_indices[stream] +
                        1][0] - currentOffset < advance_offset:
                    advance_offset = stream_values[stream][
                        stream_indices[stream] + 1][0] - currentOffset
                    advance_stream = stream
            if not advance_stream:
                break  # We've hit the end of every stream, stop
            # Advance streams sharing the current timestamp
            for stream in streams:
                if stream == advance_stream:
                    continue  # For clarity, we increment this later
                if stream_indices[stream] + 1 == stream_lengths[stream]:
                    continue  # We're at the end - can't advance
                if stream_values[stream][
                        stream_indices[stream] +
                        1][0] == stream_values[advance_stream][
                            stream_indices[advance_stream] + 1][0]:
                    stream_indices[stream] += 1
            stream_indices[
                advance_stream] += 1  # Advance the key stream for this waypoint
            currentOffset = stream_values[advance_stream][stream_indices[
                advance_stream]][0]  # Update the current time offset

            waypoint = Waypoint(activity.StartTime +
                                timedelta(seconds=currentOffset))

            if hasStreamData("location"):
                waypoint.Location = Location(
                    streamVal("location")[0],
                    streamVal("location")[1], None)
                activity.GPS = True
                if returnFirstLocation:
                    return waypoint.Location

            if hasStreamData("elevation"):
                if not waypoint.Location:
                    waypoint.Location = Location(None, None, None)
                waypoint.Location.Altitude = streamVal("elevation")

            if hasStreamData("heartrate"):
                waypoint.HR = streamVal("heartrate")

            if hasStreamData("power"):
                waypoint.Power = streamVal("power")

            if hasStreamData("cadence"):
                waypoint.Cadence = streamVal("cadence")

            if hasStreamData("distance"):
                waypoint.Distance = streamVal("distance")

            inPause = isInTimerStop(waypoint.Timestamp)
            waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause
            if wasInPause and not inPause:
                waypoint.Type = WaypointType.Resume
            wasInPause = inPause

            # We only care if it's possible to start a new lap, i.e. there are more left
            if currentLapIdx + 1 < len(laps_starts):
                if laps_starts[currentLapIdx + 1] < waypoint.Timestamp:
                    # A new lap has started
                    currentLapIdx += 1
                    lap = activity.Laps[currentLapIdx]

            lap.Waypoints.append(waypoint)

        if returnFirstLocation:
            return None  # I guess there were no waypoints?
        if activity.CountTotalWaypoints():
            activity.GetFlatWaypoints()[0].Type = WaypointType.Start
            activity.GetFlatWaypoints()[-1].Type = WaypointType.End
            activity.Stationary = False
        else:
            activity.Stationary = True

        return activity
    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)
Beispiel #28
0
    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 test_unitconv_distance_nonmetric(self):
        stat = ActivityStatistic(ActivityStatisticUnit.Miles, value=1)
        self.assertEqual(stat.asUnits(ActivityStatisticUnit.Feet).Value, 5280)

        stat = ActivityStatistic(ActivityStatisticUnit.Feet, value=5280 / 2)
        self.assertEqual(stat.asUnits(ActivityStatisticUnit.Miles).Value, 0.5)
Beispiel #30
0
    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
Beispiel #31
0
    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)
Beispiel #32
0
    def create_random_activity(svc=None,
                               actType=ActivityType.Other,
                               tz=False,
                               record=None):
        ''' 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(pytz.all_timezones[random.randint(
                0,
                len(pytz.all_timezones) - 1)])
            act.TZ = tz
        elif tz is not False:
            act.TZ = tz

        if len(act.Waypoints) > 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(random.randint(2000, 2020),
                                 random.randint(1, 12), random.randint(1, 28),
                                 random.randint(0, 23), random.randint(0, 59),
                                 random.randint(0, 59))
        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
        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 (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

            if waypointTime > act.EndTime:
                wp.Timestamp = act.EndTime
                wp.Type = WaypointType.End
            act.Waypoints.append(wp)
        if len(act.Waypoints) == 0:
            raise ValueError("No waypoints populated")
        return act
Beispiel #33
0
    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)
Beispiel #34
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        logger.debug("DownloadActivityList")
        allItems = []
        headers = self._apiHeaders(serviceRecord)
        nextRequest = '/v7.1/workout/?user=' + str(serviceRecord.ExternalID)
        while True:
            response = requests.get("https://api.mapmyfitness.com" +
                                    nextRequest,
                                    headers=headers)
            if response.status_code != 200:
                if response.status_code == 401 or response.status_code == 403:
                    raise APIException(
                        "No authorization to retrieve activity list",
                        block=True,
                        user_exception=UserException(
                            UserExceptionType.Authorization,
                            intervention_required=True))
                raise APIException(
                    "Unable to retrieve activity list " + str(response),
                    serviceRecord)
            data = response.json()
            allItems += data["_embedded"]["workouts"]
            nextLink = data["_links"].get("next")
            if not exhaustive or not nextLink:
                break
            nextRequest = nextLink[0]["href"]

        activities = []
        exclusions = []
        for act in allItems:
            # TODO catch exception and add to exclusions
            activity = UploadedActivity()
            activityID = act["_links"]["self"][0]["id"]
            activity.StartTime = datetime.strptime(act["start_datetime"],
                                                   "%Y-%m-%dT%H:%M:%S%z")
            activity.Notes = act["notes"] if "notes" in act else None

            # aggregate
            aggregates = act["aggregates"]
            elapsed_time_total = aggregates[
                "elapsed_time_total"] if "elapsed_time_total" in aggregates else "0"
            activity.EndTime = activity.StartTime + timedelta(
                0, round(float(elapsed_time_total)))
            activity.Stats.TimerTime = ActivityStatistic(
                ActivityStatisticUnit.Seconds, value=float(elapsed_time_total))
            activity.Stats.MovingTime = ActivityStatistic(
                ActivityStatisticUnit.Seconds, value=float(elapsed_time_total))
            if "active_time_total" in aggregates:
                activity.Stats.MovingTime = ActivityStatistic(
                    ActivityStatisticUnit.Seconds,
                    value=float(aggregates["active_time_total"]))

            if "distance_total" in aggregates:
                activity.Stats.Distance = ActivityStatistic(
                    ActivityStatisticUnit.Meters,
                    value=float(aggregates["distance_total"]))

            if "speed_min" in aggregates:
                activity.Stats.Speed.Min = float(aggregates["speed_min"])
            if "speed_max" in aggregates:
                activity.Stats.Speed.Max = float(aggregates["speed_max"])
            if "speed_avg" in aggregates:
                activity.Stats.Speed.Average = float(aggregates["speed_avg"])

            if "heartrate_min" in aggregates:
                activity.Stats.HR.update(
                    ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                      min=float(aggregates["heartrate_min"])))
            if "heartrate_max" in aggregates:
                activity.Stats.HR.update(
                    ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute,
                                      max=float(aggregates["heartrate_max"])))
            if "heartrate_avg" in aggregates:
                activity.Stats.HR = ActivityStatistic(
                    ActivityStatisticUnit.BeatsPerMinute,
                    avg=float(aggregates["heartrate_avg"]))

            if "cadence_min" in aggregates:
                activity.Stats.Cadence.update(
                    ActivityStatistic(
                        ActivityStatisticUnit.RevolutionsPerMinute,
                        min=int(aggregates["cadence_min"])))
            if "cadence_max" in aggregates:
                activity.Stats.Cadence.update(
                    ActivityStatistic(
                        ActivityStatisticUnit.RevolutionsPerMinute,
                        max=int(aggregates["cadence_max"])))
            if "cadence_avg" in aggregates:
                activity.Stats.Cadence = ActivityStatistic(
                    ActivityStatisticUnit.RevolutionsPerMinute,
                    avg=int(aggregates["cadence_avg"]))

            if "power_min" in aggregates:
                activity.Stats.Power.update(
                    ActivityStatistic(ActivityStatisticUnit.Watts,
                                      min=int(aggregates["power_min"])))
            if "power_max" in aggregates:
                activity.Stats.Power.update(
                    ActivityStatistic(ActivityStatisticUnit.Watts,
                                      max=int(aggregates["power_max"])))
            if "power_avg" in aggregates:
                activity.Stats.Power = ActivityStatistic(
                    ActivityStatisticUnit.Watts,
                    avg=int(aggregates["power_avg"]))

            activityTypeLink = act["_links"].get("activity_type")
            activityTypeID = activityTypeLink[0][
                "id"] if activityTypeLink is not None else None

            privacyLink = act["_links"].get("privacy")
            privacyID = privacyLink[0][
                "id"] if privacyLink is not None else None
            activity.Private = privacyID == "0"

            activity.Type = self._resolveActivityType(activityTypeID, headers)

            activity.ServiceData = {
                "ActivityID": activityID,
                "activityTypeID": activityTypeID,
                "privacyID": privacyID
            }
            activity.CalculateUID()
            activities.append(activity)
        return activities, exclusions
Beispiel #35
0
    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)
Beispiel #36
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))
            resp = self._requestWithAuth(
                lambda session: session.get(
                    "https://www.strava.com/api/v3/athletes/" + str(
                        svcRecord.ExternalID) + "/activities",
                    params={"before": before}), svcRecord)
            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
Beispiel #37
0
    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)
Beispiel #38
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
Beispiel #39
0
 def test_unitconv_noop(self):
     stat = ActivityStatistic(ActivityStatisticUnit.KilometersPerHour, value=100)
     self.assertEqual(stat.asUnits(ActivityStatisticUnit.KilometersPerHour).Value, 100)
Beispiel #40
0
    def DownloadActivityList(self, serviceRecord, exhaustive=False):
        #http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities?&start=0&limit=50
        cookies = self._get_cookies(record=serviceRecord)
        page = 1
        pageSz = 100
        activities = []
        exclusions = []
        while True:
            logger.debug("Req with " + str({
                "start": (page - 1) * pageSz,
                "limit": pageSz
            }))
            self._rate_limit()
            res = requests.get(
                "http://connect.garmin.com/proxy/activity-search-service-1.0/json/activities",
                params={
                    "start": (page - 1) * pageSz,
                    "limit": pageSz
                },
                cookies=cookies)
            try:
                res = res.json()["results"]
            except ValueError:
                res_txt = res.text  # So it can capture in the log message
                raise APIException("Parse failure in GC list resp: %s" %
                                   res.status_code)
            if "activities" not in res:
                break  # No activities on this page - empty account.
            for act in res["activities"]:
                act = act["activity"]
                if "sumDistance" not in act:
                    exclusions.append(
                        APIExcludeActivity("No distance",
                                           activityId=act["activityId"],
                                           userException=UserException(
                                               UserExceptionType.Corrupt)))
                    continue
                activity = UploadedActivity()

                if "sumSampleCountSpeed" not in act and "sumSampleCountTimestamp" not in act:  # Don't really know why sumSampleCountTimestamp doesn't appear in swim activities - they're definitely timestamped...
                    activity.Stationary = True
                else:
                    activity.Stationary = False

                try:
                    activity.TZ = pytz.timezone(act["activityTimeZone"]["key"])
                except pytz.exceptions.UnknownTimeZoneError:
                    activity.TZ = pytz.FixedOffset(
                        float(act["activityTimeZone"]["offset"]) * 60)

                logger.debug("Name " + act["activityName"]["value"] + ":")
                if len(act["activityName"]["value"].strip(
                )) and act["activityName"][
                        "value"] != "Untitled":  # This doesn't work for internationalized accounts, oh well.
                    activity.Name = act["activityName"]["value"]

                if len(act["activityDescription"]["value"].strip()):
                    activity.Notes = act["activityDescription"]["value"]
                # beginTimestamp/endTimestamp is in UTC
                activity.StartTime = pytz.utc.localize(
                    datetime.utcfromtimestamp(
                        float(act["beginTimestamp"]["millis"]) / 1000))
                if "sumElapsedDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        0, round(float(act["sumElapsedDuration"]["value"])))
                elif "sumDuration" in act:
                    activity.EndTime = activity.StartTime + timedelta(
                        minutes=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[0]),
                        seconds=float(act["sumDuration"]
                                      ["minutesSeconds"].split(":")[1]))
                else:
                    activity.EndTime = pytz.utc.localize(
                        datetime.utcfromtimestamp(
                            float(act["endTimestamp"]["millis"]) / 1000))
                logger.debug("Activity s/t " + str(activity.StartTime) +
                             " on page " + str(page))
                activity.AdjustTZ()
                # TODO: fix the distance stats to account for the fact that this incorrectly reported km instead of meters for the longest time.
                activity.Stats.Distance = ActivityStatistic(
                    self._unitMap[act["sumDistance"]["uom"]],
                    value=float(act["sumDistance"]["value"]))

                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"]])

                if "sumMovingDuration" in act:
                    activity.Stats.MovingTime = ActivityStatistic(
                        ActivityStatisticUnit.Time,
                        value=timedelta(
                            seconds=float(act["sumMovingDuration"]["value"])))

                if "sumDuration" in act:
                    activity.Stats.TimerTime = ActivityStatistic(
                        ActivityStatisticUnit.Time,
                        value=timedelta(
                            minutes=float(act["sumDuration"]
                                          ["minutesSeconds"].split(":")[0]),
                            seconds=float(act["sumDuration"]
                                          ["minutesSeconds"].split(":")[1])))

                mapStat(
                    "minSpeed", "Speed", "min", useSourceUnits=True
                )  # We need to suppress conversion here, so we can fix the pace-speed issue below
                mapStat("maxSpeed", "Speed", "max", useSourceUnits=True)
                mapStat("weightedMeanSpeed",
                        "Speed",
                        "avg",
                        useSourceUnits=True)
                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("maxRunCadence", "RunCadence", "max")
                mapStat("weightedMeanRunCadence", "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")

                # 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

                # To get it to match what the user sees in GC.
                if activity.Stats.RunCadence.Max is not None:
                    activity.Stats.RunCadence.Max *= 2
                if activity.Stats.RunCadence.Average is not None:
                    activity.Stats.RunCadence.Average *= 2

                # GC incorrectly reports pace measurements as kph/mph when they are in fact in min/km or min/mi
                if "minSpeed" in act:
                    if ":" in act["minSpeed"][
                            "withUnitAbbr"] and activity.Stats.Speed.Min:
                        activity.Stats.Speed.Min = 60 / activity.Stats.Speed.Min
                if "maxSpeed" in act:
                    if ":" in act["maxSpeed"][
                            "withUnitAbbr"] and activity.Stats.Speed.Max:
                        activity.Stats.Speed.Max = 60 / activity.Stats.Speed.Max
                if "weightedMeanSpeed" in act:
                    if ":" in act["weightedMeanSpeed"][
                            "withUnitAbbr"] and activity.Stats.Speed.Average:
                        activity.Stats.Speed.Average = 60 / activity.Stats.Speed.Average

                # Similarly, they do weird stuff with HR at times - %-of-max and zones
                # ...and we can't just fix these, so we have to calculate it after the fact (blegh)
                recalcHR = False
                if "maxHeartRate" in act:
                    if "%" in act["maxHeartRate"]["withUnitAbbr"] or "z" in act[
                            "maxHeartRate"]["withUnitAbbr"]:
                        activity.Stats.HR.Max = None
                        recalcHR = True
                if "weightedMeanHeartRate" in act:
                    if "%" in act["weightedMeanHeartRate"][
                            "withUnitAbbr"] or "z" in act[
                                "weightedMeanHeartRate"]["withUnitAbbr"]:
                        activity.Stats.HR.Average = None
                        recalcHR = True

                activity.Type = self._resolveActivityType(
                    act["activityType"]["key"])

                activity.CalculateUID()
                activity.ServiceData = {
                    "ActivityID": act["activityId"],
                    "RecalcHR": recalcHR
                }

                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
Beispiel #41
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)
                else:
                    activity.EndTime = activity.StartTime + timedelta(
                        0, float(act["duration"]))

                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
Beispiel #42
0
    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/modern/proxy/activity-service/activity/###/details
        activityID = activity.ServiceData["ActivityID"]
        res = self._request_with_reauth(
            lambda session: session.
            get("https://connect.garmin.com/modern/proxy/activity-service/activity/{}/details?maxSize=999999999"
                .format(activityID)), serviceRecord)
        try:
            raw_data = res.json()
        except ValueError:
            raise APIException("Activity data parse error for %s: %s" %
                               (res.status_code, res.text))

        if "metricDescriptors" 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["metricDescriptors"]:
            key = measurement["key"]
            if key in attrs_map:
                if attrs_map[key]["to_units"]:
                    attrs_map[key]["from_units"] = self._unitMap[
                        measurement["unit"]["key"]]
                    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["activityDetailMetrics"]:
            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
Beispiel #43
0
    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"]
        session = self._get_session(record=serviceRecord)
        self._rate_limit()
        res = session.get("http://connect.garmin.com/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 = {}
        attr_count = len(raw_data["measurements"])
        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
            # 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
Beispiel #44
0
    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)