From e961dad57de7dd48393cb462d9ae05a9e627f40c Mon Sep 17 00:00:00 2001 From: Christian-T-A Date: Thu, 20 Jul 2017 11:30:41 +0200 Subject: [PATCH 1/3] Setio integrates with tapiriik --- tapiriik/local_settings.py.example | 3 + tapiriik/services/Setio/__init__.py | 1 + tapiriik/services/Setio/setio.py | 286 +++++++++++++++++++ tapiriik/services/service.py | 6 +- tapiriik/web/static/img/services/Setio.png | Bin 0 -> 2262 bytes tapiriik/web/static/img/services/Setio_l.png | Bin 0 -> 4753 bytes 6 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 tapiriik/services/Setio/__init__.py create mode 100644 tapiriik/services/Setio/setio.py create mode 100644 tapiriik/web/static/img/services/Setio.png create mode 100644 tapiriik/web/static/img/services/Setio_l.png diff --git a/tapiriik/local_settings.py.example b/tapiriik/local_settings.py.example index a9c373981..0bad4bcc8 100644 --- a/tapiriik/local_settings.py.example +++ b/tapiriik/local_settings.py.example @@ -38,6 +38,9 @@ RUNKEEPER_CLIENT_SECRET="####" RWGPS_APIKEY = "####" +SETIO_CLIENT_ID = "####" +SETIO_CLIENT_SECRET = "####" + # See http://api.smashrun.com for info. # For now, you need to email hi@smashrun.com for access SMASHRUN_CLIENT_ID = "####" diff --git a/tapiriik/services/Setio/__init__.py b/tapiriik/services/Setio/__init__.py new file mode 100644 index 000000000..773fa14c7 --- /dev/null +++ b/tapiriik/services/Setio/__init__.py @@ -0,0 +1 @@ +from .setio import * diff --git a/tapiriik/services/Setio/setio.py b/tapiriik/services/Setio/setio.py new file mode 100644 index 000000000..e215fdda6 --- /dev/null +++ b/tapiriik/services/Setio/setio.py @@ -0,0 +1,286 @@ +# +# Created by Christian Toft Andersen 2017 for SETIO @ +# +from tapiriik.settings import WEB_ROOT, SETIO_CLIENT_SECRET, SETIO_CLIENT_ID +from tapiriik.services.service_base import ServiceAuthenticationType, ServiceBase +from tapiriik.services.service_record import ServiceRecord +from tapiriik.database import cachedb +from tapiriik.services.interchange import UploadedActivity, ActivityType, ActivityStatistic, ActivityStatisticUnit, Waypoint, WaypointType, Location, Lap +from tapiriik.services.api import APIException, UserException, UserExceptionType, APIExcludeActivity +from tapiriik.services.fit import FITIO +from datetime import datetime, timedelta + +from django.core.urlresolvers import reverse +from datetime import datetime, timedelta +from urllib.parse import urlencode +import calendar +import requests +import os +import logging +import pytz +import re +import time +import json +import dateutil.parser + +logger = logging.getLogger(__name__) + +class SetioService(ServiceBase): + ID = "setio" + DisplayName = "Setio" + DisplayAbbreviation = "SET" + AuthenticationType = ServiceAuthenticationType.OAuth + AuthenticationNoFrame = True # They don't prevent the iframe, it just looks really ugly. + PartialSyncRequiresTrigger = True + LastUpload = None + + SupportsHR = SupportsCadence = SupportsTemp = SupportsPower = True + + SupportsActivityDeletion = True + + # For mapping common->Setio; no ambiguity in Setio activity type + _activityTypeMappings = { + ActivityType.Cycling: "Ride", + ActivityType.MountainBiking: "Ride", + ActivityType.Hiking: "Hike", + ActivityType.Running: "Run", + ActivityType.Walking: "Walk", + ActivityType.Snowboarding: "Snowboard", + ActivityType.Skating: "IceSkate", + ActivityType.CrossCountrySkiing: "NordicSki", + ActivityType.DownhillSkiing: "AlpineSki", + ActivityType.Swimming: "Swim", + ActivityType.Gym: "Workout", + ActivityType.Rowing: "Rowing", + ActivityType.Elliptical: "Elliptical", + ActivityType.RollerSkiing: "RollerSki", + ActivityType.StrengthTraining: "WeightTraining", + } + + # For mapping Setio->common + _reverseActivityTypeMappings = { + "Ride": ActivityType.Cycling, + "VirtualRide": ActivityType.Cycling, + "EBikeRide": ActivityType.Cycling, + "MountainBiking": ActivityType.MountainBiking, + "Run": ActivityType.Running, + "Hike": ActivityType.Hiking, + "Walk": ActivityType.Walking, + "AlpineSki": ActivityType.DownhillSkiing, + "CrossCountrySkiing": ActivityType.CrossCountrySkiing, + "NordicSki": ActivityType.CrossCountrySkiing, + "BackcountrySki": ActivityType.DownhillSkiing, + "Snowboard": ActivityType.Snowboarding, + "Swim": ActivityType.Swimming, + "IceSkate": ActivityType.Skating, + "Workout": ActivityType.Gym, + "Rowing": ActivityType.Rowing, + "Kayaking": ActivityType.Rowing, + "Canoeing": ActivityType.Rowing, + "StandUpPaddling": ActivityType.Rowing, + "Elliptical": ActivityType.Elliptical, + "RollerSki": ActivityType.RollerSkiing, + "WeightTraining": ActivityType.StrengthTraining, + } + + SupportedActivities = list(_activityTypeMappings.keys()) + + def WebInit(self): + params = {'scope':'write,view_private', + 'client_id':SETIO_CLIENT_ID, + 'response_type':'code', + 'redirect_uri':WEB_ROOT + reverse("oauth_return", kwargs={"service": "setio"})} + self.UserAuthorizationURL = \ + "https://setio.run/oauth/authorize?" + urlencode(params) + + def _apiHeaders(self, serviceRecord): + return {"Authorization": "access_token " + serviceRecord.Authorization["OAuthToken"]} + + def RetrieveAuthorizationToken(self, req, level): + code = req.GET.get("code") + authorizationData = {"OAuthToken": code} + return (code, authorizationData) + + def RevokeAuthorization(self, serviceRecord): + # you can't revoke the tokens setio distributes :\ + pass + + + def DownloadActivityList(self, svcRecord, exhaustive=False): + activities = [] + exclusions = [] + + url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunsByUserId" + + extID = svcRecord.ExternalID + + payload = "{\"userId\": \"" + extID + "\"}" + headers = { + 'content-type': "application/json", + 'cache-control': "no-cache", + } + response = requests.request("POST", url, data=payload, headers=headers) + try: + reqdata = response.json() + except ValueError: + raise APIException("Failed parsing Setio list response %s - %s" % (resp.status_code, resp.text)) + + + for ride in reqdata: + activity = UploadedActivity() + + activity.StartTime = datetime.strptime(datetime.utcfromtimestamp(ride["startTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") + if "stopTimeStamp" in ride: + activity.EndTime = datetime.strptime(datetime.utcfromtimestamp(ride["stopTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") + activity.ServiceData = {"ActivityID": ride["runId"], "Manual": "False"} + + # if ride["type"] not in self._reverseActivityTypeMappings: + if "Run" not in self._reverseActivityTypeMappings: + exclusions.append( + APIExcludeActivity("Unsupported activity type %s" % "Run", activity_id=ride["runId"], + user_exception=UserException(UserExceptionType.Other))) + logger.debug("\t\tUnknown activity") + continue + + activity.Name = ride["programName"] + + activity.Type = ActivityType.Running + if "totalDistance" in ride: + activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=ride["totalDistance"]) + + if "averageCadence" in ride: + activity.Stats.Cadence.update(ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=ride["averageCadence"])) + + if "averageSpeed" in ride: + activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=ride["averageSpeed"]) + + #get comment + url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunComment" + payload = "{\"userId\": \"" + extID + "\",\"runId\":\"" + activity.ServiceData["ActivityID"] + "\"}" + headers = { + 'content-type': "application/json", + 'cache-control': "no-cache", + } + streamdata = requests.request("POST", url, data=payload, headers=headers) + if streamdata.status_code == 500: + raise APIException("Internal server error") + + if streamdata.status_code == 403: + raise APIException("No authorization to download activity", block=True, + user_exception=UserException(UserExceptionType.Authorization, + intervention_required=True)) + + if streamdata.status_code != 204: # "Record Not Found": + try: + commentdata = streamdata.json() + except: + raise APIException("Stream data returned is not JSON") + + if "comment" in commentdata: + activity.Notes = commentdata["comment"] + else: + activity.Notes = "" + else: + activity.Notes = "" + + activity.GPS = True + + activity.Private = False + activity.Stationary = False #True = no sensor data + + activity.CalculateUID() + activities.append(activity) + + return activities, exclusions + + + def SubscribeToPartialSyncTrigger(self, serviceRecord): + # There is no per-user webhook subscription with Setio. + serviceRecord.SetPartialSyncTriggerSubscriptionState(True) + + def UnsubscribeFromPartialSyncTrigger(self, serviceRecord): + # # As above. + serviceRecord.SetPartialSyncTriggerSubscriptionState(False) + + def DownloadActivity(self, svcRecord, activity): + # if activity.ServiceData["Manual"]: + # # Maybe implement later + # activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] + # return activity + activityID = activity.ServiceData["ActivityID"] + + extID = svcRecord.ExternalID + + url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunData" + payload = "{\"userId\": \"" + extID + "\",\"runId\":\"" + str(activityID) + "\"}" + headers = { + 'content-type': "application/json", + 'cache-control': "no-cache", + } + streamdata = requests.request("POST", url, data=payload, headers=headers) + if streamdata.status_code == 500: + raise APIException("Internal server error") + + if streamdata.status_code == 403: + raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) + + if streamdata.status_code == 204: #"Record Not Found": + raise APIException("Could not find rundata") + + try: + streamdata = streamdata.json() + except: + raise APIException("Stream data returned is not JSON") + + ridedata = {} + + lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Setio doesn't support laps, but we need somewhere to put the waypoints. + activity.Laps = [lap] + lap.Waypoints = [] + + wayPointExist = False + + countWayPoints = 0 + for stream in streamdata: + waypoint = Waypoint(dateutil.parser.parse(stream["time"], ignoretz=True)) + + if "latitude" in stream: + if "longitude" in stream: + latitude = stream["latitude"] + longitude = stream["longitude"] + waypoint.Location = Location(latitude, longitude, None) + if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: + waypoint.Location.Longitude = None + waypoint.Location.Latitude = None + + if "cadence" in stream: + waypoint.Cadence = stream["cadence"] + if waypoint.Cadence > 0: + waypoint.Cadence = waypoint.Cadence / 2 + + if "elevation" in stream: + if not waypoint.Location: + waypoint.Location = Location(None, None, None) + waypoint.Location.Altitude = stream["elevation"] + + if "distance" in stream: + waypoint.Distance = stream["distance"] + if "speed" in stream: + waypoint.Speed = stream["speed"] + waypoint.Type = WaypointType.Regular + lap.Waypoints.append(waypoint) + countWayPoints = countWayPoints + 1 + + if countWayPoints < 60: + lap.Waypoints = [] + return activity + + + def UploadActivity(self, serviceRecord, activity): + pass + + def DeleteCachedData(self, serviceRecord): + pass + + def DeleteActivity(self, serviceRecord, uploadId): + pass diff --git a/tapiriik/services/service.py b/tapiriik/services/service.py index 7c6e7df32..2856d6c11 100644 --- a/tapiriik/services/service.py +++ b/tapiriik/services/service.py @@ -41,7 +41,8 @@ def List(): VeloHero, TrainerRoad, Smashrun, - BeginnerTriathlete + BeginnerTriathlete, + Setio ] + PRIVATE_SERVICES def PreferredDownloadPriorityList(): @@ -64,7 +65,8 @@ def PreferredDownloadPriorityList(): BeginnerTriathlete, # No temperature Motivato, NikePlus, - Pulsstory + Pulsstory, + Setio ] + PRIVATE_SERVICES def WebInit(): diff --git a/tapiriik/web/static/img/services/Setio.png b/tapiriik/web/static/img/services/Setio.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d14a0ef256fbb34052d816a99ac8366c77e66d GIT binary patch literal 2262 zcmV;{2r2i8P)Px-kV!;ARCodHoOx^%RUF4#KoO`nRq#T@qY{lM8VCk~fHBGuh#JM1s*rdkcmy#X z5D6Obs0s)P>K{sCkf2TQ0AfuP@Rm~)NkJf@oKmzXhltkCx82$9&i-b0W_K&KzF+d) zH}7}7x4-w>H*em|Y*CRT7l9^-KyehGb#-;6=yglrRLU)&8mucWF5W}wsF_88F>6nk zdI61>#;+~d1%3uA8Mn=pu?fG&V{|p>3F^RdumFFxl-UUfoxl`O3&LnC!q-8U>|Pr+ z(0?h+X^yfIuLbOf90w6~D?I;UCB9mLk>Epc0EE#xginE!!+fU7 z&>aT0fxp3e@I4Uy1e9#SghDB(0Q*23+K=*NaCETuMhSW@v7$#eYhvMQ0H}`cGO4L? zeiF249G%P~11M(qd^qHC7(>dt4#EKr}(8R*c zIG};ehZbXeQu-)0)Zn)U8^FgvXR=iKoJw2p6i5Yj^ZJFqK6N1{u>^ej3~oM!|WvwpaEA%dHo=&T2avglrle5RpuQG&N3MfN7g zl|UB*Yrsk%>Y_DNeR+E(zBI(T+cDdYgXj-bZx zfcnqC{DSaZVl@UVb|Pasr_ns^=*f_wN)utF z&WYm(DG!1tJOgjgHO2X96I0yxex_ANPirn~cIpVfk7eU{xkrEj)VN(~c=!^YPJfYz zanpMFK0{fjNWcH_@RoshfEMst><$>0+(Nfl9>hN4L8B|rV(XJn~YAYX3iDCry( zHUiyncY@(^9?9ZewHacQRSEZHL!v9axgHth~Ly`8WbEvn{PZPMxVUIv*Ko4RkR>IE;!=JfX>gkbCdxDZmI$mN0t)*Te zxvVMI^Y6;T69IGcnStJ&V|Qu{JF^W#&t!RtRfW`Yt{H13$yE+YOb?FfB49+Ie(_GG z|4!meMLx;UIRoA-FcY0GD3^eb!ROfRqbwwyNE<_9INV4dbY=KSX&oS{5hQ#6oF)7={u2JUsGt~>qpgN(xvFn0INtY%{-3&#}P+y->2i%Z%M`HbWm8rRV2x10!MB$mD!$#np{1~d!l%20Q)wf3pMOHP@M zjz(dgo4K-uspZO37w_h`B)L{15Ofo=nDRo<7xV{u&bb4yNJ*<3qQfj<0B=8P58e+FG!_^ZJTa6Nk3ogE2+Hdu#}jF$)bbNrSYfmn&<4+F`y56ppoF_`C< zC+ar^@EyAaM!cWJg^wQKS89-1EZ zsy3j_(8xq`X<={8T4zUg;uNDszzT`Kb#@+uF}`^}HgmBfvU#Vlg(F<+MfQr=T|cdS<;9GtD6+pBmB;G^%ZbIHdcE ze)^R*;y!%l_u=a45zu2!){_jwbNCToo9;SUOMRIl$&7m7t+5sv21=^gbkx%^R2o_6 zY3pK8BPV&JtqMm4i~v7WJ*oMD;j$OJ#|^&8V$8ZDm|_Su5m^2WRh#7{O^_2kX)v7* zM0qx68oKJJpO*U>u3nG`=xW@ehZwI&Z|PuM3ECS@suF8vLlliwzd?$V<#gs{Ol~*K z?t-U3gU~NwKG=eOo%GiQ5aSnd{~*YB39tkogYlR8 zHw8KOJT0;01;=kuoCnn5Tsk`!yE6Q|PwD7ihybyEmS(iBv-T0Q^+1WF7fuJ_M`xLW zP)1_OD-G0wdpOi%Gg>6>0b!&)l`7C1yBmPsQ*-n$Mu3=qM04kZn}Cl00AaKq;nSci zwvT{pOQ0}c8M3f7Qc{`Ko_5Z4_0bh{1jm9cK);anbgG8ZQ8S7FW7eMTXkY78PzrRl kx(n!bP|p8BifttN2d|VHEO)qi?*IS*07*qoM6N<$f&}&~NdN!< literal 0 HcmV?d00001 diff --git a/tapiriik/web/static/img/services/Setio_l.png b/tapiriik/web/static/img/services/Setio_l.png new file mode 100644 index 0000000000000000000000000000000000000000..b171710aef817bcdc914c965958a6bfa47a7405b GIT binary patch literal 4753 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 literal 0 HcmV?d00001 From 19adf8a58219189e4892548cd57af3a627b91a23 Mon Sep 17 00:00:00 2001 From: Christian-T-A Date: Wed, 9 Aug 2017 14:39:50 +0200 Subject: [PATCH 2/3] Fixing bugs requested by Collin --- tapiriik/services/Setio/setio.py | 92 ++++++++++++-------------------- 1 file changed, 35 insertions(+), 57 deletions(-) diff --git a/tapiriik/services/Setio/setio.py b/tapiriik/services/Setio/setio.py index e215fdda6..6954a5b02 100644 --- a/tapiriik/services/Setio/setio.py +++ b/tapiriik/services/Setio/setio.py @@ -3,25 +3,15 @@ # from tapiriik.settings import WEB_ROOT, SETIO_CLIENT_SECRET, SETIO_CLIENT_ID from tapiriik.services.service_base import ServiceAuthenticationType, ServiceBase -from tapiriik.services.service_record import ServiceRecord -from tapiriik.database import cachedb from tapiriik.services.interchange import UploadedActivity, ActivityType, ActivityStatistic, ActivityStatisticUnit, Waypoint, WaypointType, Location, Lap -from tapiriik.services.api import APIException, UserException, UserExceptionType, APIExcludeActivity -from tapiriik.services.fit import FITIO -from datetime import datetime, timedelta from django.core.urlresolvers import reverse -from datetime import datetime, timedelta +from datetime import datetime from urllib.parse import urlencode -import calendar import requests -import os import logging -import pytz -import re -import time -import json import dateutil.parser +import json logger = logging.getLogger(__name__) @@ -31,8 +21,9 @@ class SetioService(ServiceBase): DisplayAbbreviation = "SET" AuthenticationType = ServiceAuthenticationType.OAuth AuthenticationNoFrame = True # They don't prevent the iframe, it just looks really ugly. - PartialSyncRequiresTrigger = True + PartialSyncRequiresTrigger = False LastUpload = None + SetioDomain = "https://us-central1-project-2489250248063150762.cloudfunctions.net/" SupportsHR = SupportsCadence = SupportsTemp = SupportsPower = True @@ -110,16 +101,19 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] - url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunsByUserId" - + url = self.SetioDomain + "getRunsByUserId" extID = svcRecord.ExternalID - payload = "{\"userId\": \"" + extID + "\"}" + #payload = "{\"userId\": \"" + extID + "\"}" + payload = {} + payload["userId"] = extID + payload=json.dumps(payload, ensure_ascii=False) + headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - response = requests.request("POST", url, data=payload, headers=headers) + response = requests.post( url, data=payload, headers=headers) try: reqdata = response.json() except ValueError: @@ -128,22 +122,14 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): for ride in reqdata: activity = UploadedActivity() - activity.StartTime = datetime.strptime(datetime.utcfromtimestamp(ride["startTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") if "stopTimeStamp" in ride: activity.EndTime = datetime.strptime(datetime.utcfromtimestamp(ride["stopTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") activity.ServiceData = {"ActivityID": ride["runId"], "Manual": "False"} - # if ride["type"] not in self._reverseActivityTypeMappings: - if "Run" not in self._reverseActivityTypeMappings: - exclusions.append( - APIExcludeActivity("Unsupported activity type %s" % "Run", activity_id=ride["runId"], - user_exception=UserException(UserExceptionType.Other))) - logger.debug("\t\tUnknown activity") - continue - activity.Name = ride["programName"] + logger.debug("\tActivity s/t %s: %s" % (activity.StartTime, activity.Name)) activity.Type = ActivityType.Running if "totalDistance" in ride: activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=ride["totalDistance"]) @@ -155,13 +141,17 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=ride["averageSpeed"]) #get comment - url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunComment" - payload = "{\"userId\": \"" + extID + "\",\"runId\":\"" + activity.ServiceData["ActivityID"] + "\"}" + url = self.SetioDomain + "getRunComment" + payload = {} + payload["userId"] = extID + payload["runId"] = activity.ServiceData["ActivityID"] + payload=json.dumps(payload, ensure_ascii=False) + headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - streamdata = requests.request("POST", url, data=payload, headers=headers) + streamdata = requests.post(url, data=payload, headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") @@ -170,7 +160,7 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) - if streamdata.status_code != 204: # "Record Not Found": + if streamdata.status_code == 200: # Ok try: commentdata = streamdata.json() except: @@ -179,9 +169,9 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): if "comment" in commentdata: activity.Notes = commentdata["comment"] else: - activity.Notes = "" + activity.Notes = None else: - activity.Notes = "" + activity.Notes = None activity.GPS = True @@ -193,15 +183,6 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): return activities, exclusions - - def SubscribeToPartialSyncTrigger(self, serviceRecord): - # There is no per-user webhook subscription with Setio. - serviceRecord.SetPartialSyncTriggerSubscriptionState(True) - - def UnsubscribeFromPartialSyncTrigger(self, serviceRecord): - # # As above. - serviceRecord.SetPartialSyncTriggerSubscriptionState(False) - def DownloadActivity(self, svcRecord, activity): # if activity.ServiceData["Manual"]: # # Maybe implement later @@ -211,26 +192,28 @@ def DownloadActivity(self, svcRecord, activity): extID = svcRecord.ExternalID - url = "https://us-central1-project-2489250248063150762.cloudfunctions.net/getRunData" - payload = "{\"userId\": \"" + extID + "\",\"runId\":\"" + str(activityID) + "\"}" + url = self.SetioDomain + "getRunData" + payload = {} + payload["userId"] = extID + payload["runId"] = activityID + payload=json.dumps(payload, ensure_ascii=False) + headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - streamdata = requests.request("POST", url, data=payload, headers=headers) + streamdata = requests.post( url, data=payload, headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") if streamdata.status_code == 403: raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) - if streamdata.status_code == 204: #"Record Not Found": - raise APIException("Could not find rundata") - - try: - streamdata = streamdata.json() - except: - raise APIException("Stream data returned is not JSON") + if streamdata.status_code == 200: #Ok + try: + streamdata = streamdata.json() + except: + raise APIException("Stream data returned is not JSON") ridedata = {} @@ -240,7 +223,6 @@ def DownloadActivity(self, svcRecord, activity): wayPointExist = False - countWayPoints = 0 for stream in streamdata: waypoint = Waypoint(dateutil.parser.parse(stream["time"], ignoretz=True)) @@ -255,8 +237,7 @@ def DownloadActivity(self, svcRecord, activity): if "cadence" in stream: waypoint.Cadence = stream["cadence"] - if waypoint.Cadence > 0: - waypoint.Cadence = waypoint.Cadence / 2 + waypoint.Cadence = waypoint.Cadence / 2 if "elevation" in stream: if not waypoint.Location: @@ -269,10 +250,7 @@ def DownloadActivity(self, svcRecord, activity): waypoint.Speed = stream["speed"] waypoint.Type = WaypointType.Regular lap.Waypoints.append(waypoint) - countWayPoints = countWayPoints + 1 - if countWayPoints < 60: - lap.Waypoints = [] return activity From ac9dfefda878838cb6b417c4bd28cf88f3f932f1 Mon Sep 17 00:00:00 2001 From: Christian-T-A Date: Fri, 11 Aug 2017 10:10:26 +0200 Subject: [PATCH 3/3] some json enconding done the right way --- tapiriik/services/Setio/setio.py | 73 ++++++++++++++------------------ 1 file changed, 31 insertions(+), 42 deletions(-) diff --git a/tapiriik/services/Setio/setio.py b/tapiriik/services/Setio/setio.py index 6954a5b02..463b44fb5 100644 --- a/tapiriik/services/Setio/setio.py +++ b/tapiriik/services/Setio/setio.py @@ -3,7 +3,8 @@ # from tapiriik.settings import WEB_ROOT, SETIO_CLIENT_SECRET, SETIO_CLIENT_ID from tapiriik.services.service_base import ServiceAuthenticationType, ServiceBase -from tapiriik.services.interchange import UploadedActivity, ActivityType, ActivityStatistic, ActivityStatisticUnit, Waypoint, WaypointType, Location, Lap +from tapiriik.services.interchange import UploadedActivity, ActivityType, ActivityStatistic, ActivityStatisticUnit, \ + Waypoint, WaypointType, Location, Lap from django.core.urlresolvers import reverse from datetime import datetime @@ -15,6 +16,7 @@ logger = logging.getLogger(__name__) + class SetioService(ServiceBase): ID = "setio" DisplayName = "Setio" @@ -77,10 +79,10 @@ class SetioService(ServiceBase): SupportedActivities = list(_activityTypeMappings.keys()) def WebInit(self): - params = {'scope':'write,view_private', - 'client_id':SETIO_CLIENT_ID, - 'response_type':'code', - 'redirect_uri':WEB_ROOT + reverse("oauth_return", kwargs={"service": "setio"})} + params = {'scope': 'write,view_private', + 'client_id': SETIO_CLIENT_ID, + 'response_type': 'code', + 'redirect_uri': WEB_ROOT + reverse("oauth_return", kwargs={"service": "setio"})} self.UserAuthorizationURL = \ "https://setio.run/oauth/authorize?" + urlencode(params) @@ -96,7 +98,6 @@ def RevokeAuthorization(self, serviceRecord): # you can't revoke the tokens setio distributes :\ pass - def DownloadActivityList(self, svcRecord, exhaustive=False): activities = [] exclusions = [] @@ -104,27 +105,24 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): url = self.SetioDomain + "getRunsByUserId" extID = svcRecord.ExternalID - #payload = "{\"userId\": \"" + extID + "\"}" - payload = {} - payload["userId"] = extID - payload=json.dumps(payload, ensure_ascii=False) - + payload = {"userId": extID} headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - response = requests.post( url, data=payload, headers=headers) + response = requests.post(url, data=json.dumps(payload), headers=headers) try: reqdata = response.json() except ValueError: raise APIException("Failed parsing Setio list response %s - %s" % (resp.status_code, resp.text)) - for ride in reqdata: activity = UploadedActivity() - activity.StartTime = datetime.strptime(datetime.utcfromtimestamp(ride["startTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") + activity.StartTime = datetime.strptime( + datetime.utcfromtimestamp(ride["startTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") if "stopTimeStamp" in ride: - activity.EndTime = datetime.strptime(datetime.utcfromtimestamp(ride["stopTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") + activity.EndTime = datetime.strptime( + datetime.utcfromtimestamp(ride["stopTimeStamp"]).strftime('%Y-%m-%d %H:%M:%S'), "%Y-%m-%d %H:%M:%S") activity.ServiceData = {"ActivityID": ride["runId"], "Manual": "False"} activity.Name = ride["programName"] @@ -135,23 +133,21 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): activity.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=ride["totalDistance"]) if "averageCadence" in ride: - activity.Stats.Cadence.update(ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=ride["averageCadence"])) + activity.Stats.Cadence.update( + ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=ride["averageCadence"])) if "averageSpeed" in ride: - activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=ride["averageSpeed"]) + activity.Stats.Speed = ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, + avg=ride["averageSpeed"]) - #get comment + # get comment url = self.SetioDomain + "getRunComment" - payload = {} - payload["userId"] = extID - payload["runId"] = activity.ServiceData["ActivityID"] - payload=json.dumps(payload, ensure_ascii=False) - + payload = { "userId": extID, "runId": activity.ServiceData["ActivityID"]} headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - streamdata = requests.post(url, data=payload, headers=headers) + streamdata = requests.post(url, data=json.dumps(payload), headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") @@ -160,7 +156,7 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) - if streamdata.status_code == 200: # Ok + if streamdata.status_code == 200: # Ok try: commentdata = streamdata.json() except: @@ -176,7 +172,7 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): activity.GPS = True activity.Private = False - activity.Stationary = False #True = no sensor data + activity.Stationary = False # True = no sensor data activity.CalculateUID() activities.append(activity) @@ -184,32 +180,25 @@ def DownloadActivityList(self, svcRecord, exhaustive=False): return activities, exclusions def DownloadActivity(self, svcRecord, activity): - # if activity.ServiceData["Manual"]: - # # Maybe implement later - # activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] - # return activity - activityID = activity.ServiceData["ActivityID"] + activityID = activity.ServiceData["ActivityID"] extID = svcRecord.ExternalID - url = self.SetioDomain + "getRunData" - payload = {} - payload["userId"] = extID - payload["runId"] = activityID - payload=json.dumps(payload, ensure_ascii=False) - + payload = {"userId": extID, "runId": activityID} headers = { 'content-type': "application/json", 'cache-control': "no-cache", } - streamdata = requests.post( url, data=payload, headers=headers) + streamdata = requests.post(url, data=json.dumps(payload), headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") if streamdata.status_code == 403: - raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) + raise APIException("No authorization to download activity", block=True, + user_exception=UserException(UserExceptionType.Authorization, + intervention_required=True)) - if streamdata.status_code == 200: #Ok + if streamdata.status_code == 200: # Ok try: streamdata = streamdata.json() except: @@ -217,7 +206,8 @@ def DownloadActivity(self, svcRecord, activity): ridedata = {} - lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Setio doesn't support laps, but we need somewhere to put the waypoints. + lap = Lap(stats=activity.Stats, startTime=activity.StartTime, + endTime=activity.EndTime) # Setio doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] @@ -253,7 +243,6 @@ def DownloadActivity(self, svcRecord, activity): return activity - def UploadActivity(self, serviceRecord, activity): pass