Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Garmin Connect

Based on python-ant-downloader code
  • Loading branch information...
commit e8ab6bb08af6d17612c84f9944b49801f703ad8f 1 parent 7460c5a
@mlt authored
View
27 src/garmin/antd/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012, Braiden Kindt.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
+''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
View
4 src/garmin/antd/README.org
@@ -0,0 +1,4 @@
+* Notice
+
+This code is a part of [[https://github.com/braiden/python-ant-downloader][another project]]. Original copyright apply. See license details in [[./LICENSE][LICENSE]].
+
View
0  src/garmin/antd/__init__.py
No changes.
View
189 src/garmin/antd/connect.py
@@ -0,0 +1,189 @@
+# Copyright (c) 2012, Braiden Kindt.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials
+# provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
+# ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+import logging
+import os
+import sys
+import urllib
+import urllib2
+import cookielib
+import json
+import glob
+
+import antd.plugin as plugin
+
+_log = logging.getLogger("antd.connect")
+
+class GarminConnect(plugin.Plugin):
+
+ username = None
+ password = None
+
+ logged_in = False
+ login_invalid = False
+
+ def __init__(self):
+ import poster.streaminghttp
+ cookies = cookielib.CookieJar()
+ cookie_handler = urllib2.HTTPCookieProcessor(cookies)
+ self.opener = urllib2.build_opener(
+ cookie_handler,
+ poster.streaminghttp.StreamingHTTPHandler,
+ poster.streaminghttp.StreamingHTTPRedirectHandler,
+ poster.streaminghttp.StreamingHTTPSHandler)
+ # sign in started failing on or around Jul-19-2012
+ # add headers to exactly match firefox, seems to work again
+ # no idea why. garmin does accept our login without these
+ # headers by for some reason json is not parsed ?!
+ self.opener.addheaders = [
+ ('User-Agent', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1'),
+ ('Referer', 'https://connect.garmin.com/signin'),
+ ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
+ ('Accept-Language', 'en-us,en;q=0.5'),
+ ('Accept-Encoding', 'gzip, deflate'),
+ ]
+
+
+ def data_available(self, device_sn, format, files):
+ if format not in ("tcx"): return files
+ result = []
+ try:
+ for file in files:
+ self.login()
+ self.upload(format, file)
+ result.append(file)
+ except Exception:
+ _log.warning("Failed to uplaod to Garmin Connect.", exc_info=True)
+ finally:
+ return result
+
+ def login(self):
+ if self.logged_in: return
+ if self.login_invalid: raise InvalidLogin()
+ # get session cookies
+ _log.debug("Fetching cookies from Garmin Connect.")
+ self.opener.open("http://connect.garmin.com/signin")
+ # build the login string
+ login_dict = {
+ "login": "login",
+ "login:loginUsernameField": self.username,
+ "login:password": self.password,
+ "login:signInButton": "Sign In",
+ "javax.faces.ViewState": "j_id1",
+ }
+ login_str = urllib.urlencode(login_dict)
+ # post login credentials
+ _log.debug("Posting login credentials to Garmin Connect. username=%s", self.username)
+ self.opener.open("https://connect.garmin.com/signin", login_str)
+ # verify we're logged in
+ _log.debug("Checking if login was successful.")
+ reply = self.opener.open("http://connect.garmin.com/user/username")
+ username = json.loads(reply.read())["username"]
+ if username == "":
+ self.login_invalid = True
+ raise InvalidLogin()
+ elif username != self.username:
+ _log.warning("Username mismatch, probably OK, if upload fails check user/pass. %s != %s" % (username, self.username))
+ self.logged_in = True
+
+ def upload(self, format, file_name):
+ import poster.encode
+ with open(file_name) as file:
+ upload_dict = {
+ "responseContentType": "text/html",
+ "data": file,
+ }
+ data, headers = poster.encode.multipart_encode(upload_dict)
+ _log.info("Uploading %s to Garmin Connect.", file_name)
+ request = urllib2.Request("http://connect.garmin.com/proxy/upload-service-1.1/json/upload/.%s" % format, data, headers)
+ self.opener.open(request)
+
+class StravaConnect(plugin.Plugin):
+
+ server = None
+ smtp_server = None
+ smtp_port = None
+ smtp_username = None
+ smtp_password = None
+
+ logged_in = False
+
+ def __init__(self):
+ from smtplib import SMTP
+ self.server = SMTP()
+ pass
+
+ def data_available(self, device_sn, format, files):
+ if format not in ("tcx"): return files
+ result = []
+ try:
+ for file in files:
+ self.login()
+ self.upload(format, file)
+ result.append(file)
+ self.logout()
+ except Exception:
+ _log.warning("Failed to upload to Strava.", exc_info=True)
+ finally:
+ return result
+
+ def logout(self):
+ self.server.close()
+
+ def login(self):
+ if self.logged_in: return
+ self.server.connect(self.smtp_server, self.smtp_port)
+ self.server.ehlo()
+ self.server.starttls()
+ self.server.ehlo()
+ self.server.login(self.smtp_username, self.smtp_password)
+ self.logged_in = True
+
+ def upload(self, format, file_name):
+ from email.mime.base import MIMEBase
+ from email.mime.multipart import MIMEMultipart
+ import datetime
+ from email import encoders
+ outer = MIMEMultipart()
+ outer['Subject'] = 'Garmin Data Upload from %s' % datetime.date.today()
+ outer['To' ] = 'upload@strava.com'
+ outer['From' ] = self.smtp_username
+ outer.preamble = 'You will not see this in a MIME-aware mail reader.\n'
+ with open(file_name, 'rb') as fp:
+ msg = MIMEBase('application', 'octet-stream')
+ msg.set_payload(fp.read())
+ encoders.encode_base64(msg)
+ msg.add_header('Content-Disposition', 'attachment', filename=file_name)
+ outer.attach(msg)
+ self.server.sendmail(self.smtp_username, 'upload@strava.com', outer.as_string())
+
+class InvalidLogin(Exception): pass
+
+
+# vim: ts=4 sts=4 et
View
127 src/garmin/antd/plugin.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2012, Braiden Kindt.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials
+# provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
+# ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+
+import logging
+import dbm
+import os
+
+_log = logging.getLogger("antd.plugin")
+_plugins = []
+
+class Plugin(object):
+ """
+ A plugin receives notifications when new data
+ is available, it can consume the data or transform it.
+ TCX file generation, and garmin connect upload are
+ both implementations of plugin. You can implement
+ your own to produce new file formats or upload somewhere.
+ """
+
+ def data_available(self, device_sn, format, files):
+ """
+ Notification that data is available, this could
+ be raw packet data from device, or higher level
+ data generated by other plugins, e.g. TCX.
+ Return: files which were sucessfullly processed.
+ """
+ pass
+
+
+class PluginQueue(object):
+ """
+ File based queue representing unprocessed
+ files which were not handled the a plugin.
+ """
+
+ def __init__(self, plugin):
+ try: self.queue_file_name = plugin.cache
+ except AttributeError: self.queue_file_name = None
+ self.queue = []
+
+ def load_queue(self):
+ if self.queue_file_name and os.path.isfile(self.queue_file_name):
+ with open(self.queue_file_name, "r") as file:
+ lines = file.read().splitlines()
+ self.queue = []
+ for line in lines:
+ device_sn, format, file = line.split(",")
+ if os.path.isfile(file):
+ self.queue.append((int(device_sn), format, file))
+ else:
+ _log.warning("File pending processing, but disappeared. %s", file)
+
+ def save_queue(self):
+ if self.queue_file_name and self.queue:
+ with open(self.queue_file_name, "w") as file:
+ file.writelines("%d,%s,%s\n" % e for e in self.queue)
+ elif self.queue_file_name and os.path.isfile(self.queue_file_name):
+ os.unlink(self.queue_file_name)
+
+ def add_to_queue(self, device_sn, format, files):
+ for file in files:
+ self.queue.append((device_sn, format, file))
+
+
+def register_plugins(*plugins):
+ _plugins.extend(p for p in plugins if p is not None)
+ for plugin in plugins:
+ try: plugin and recover_and_publish_data(plugin)
+ except Exception: _log.warning("Plugin failed. %s", plugin, exc_info=True)
+
+def recover_and_publish_data(plugin):
+ q = PluginQueue(plugin)
+ q.load_queue()
+ if q.queue:
+ try:
+ _log.debug("Attempting to reprocess failed files.")
+ for device_sn, format, file in list(q.queue):
+ if plugin.data_available(device_sn, format, [file]):
+ q.queue.remove((device_sn, format, file))
+ except Exception:
+ _log.warning("Plugin failed. %s", plugin, exc_info=True)
+ finally:
+ q.save_queue()
+
+def publish_data(device_sn, format, files):
+ for plugin in _plugins:
+ try:
+ processed = plugin.data_available(device_sn, format, files)
+ not_processed = [f for f in files if f not in processed]
+ except Exception:
+ processed = []
+ not_processed = files
+ _log.warning("Plugin failed. %s", plugin, exc_info=True)
+ finally:
+ q = PluginQueue(plugin)
+ q.load_queue()
+ q.add_to_queue(device_sn, format, not_processed)
+ q.save_queue()
+
+
+# vim: ts=4 sts=4 et
View
33 src/garmin/tcx2garmin.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
# Use https://github.com/chmouel/python-garmin-upload
# Don't forget patch https://github.com/chmouel/python-garmin-upload/issues/1
# Create ~/.schwinn810.yaml with the following 3 lines
@@ -7,27 +7,36 @@
# password: your_password
import argparse
-from UploadGarmin import UploadGarmin
+#from UploadGarmin import UploadGarmin
+from antd import connect # requires python-poster
from yaml import load, dump
-import os
+import os, logging
+
+logging.basicConfig()
parser = argparse.ArgumentParser(description='Uploads TCX to Garmin Connect')
-parser.add_argument(dest='tcx', help='TCX file to upload')
+parser.add_argument(nargs='+', dest='tcx', help='TCX files to upload')
args = parser.parse_args()
stream = file(os.path.expanduser('~/.schwinn810.yaml'), 'r')
cfg = load(stream)
cfg = cfg['garmin']
-g = UploadGarmin()
+client = connect.GarminConnect()
+client.username = cfg["username"]
+client.password = cfg["password"]
+
+client.data_available(None, "tcx", args.tcx)
+#client.upload("tcx", name)
+
+# g = UploadGarmin()
-print("Using %s and %s to connect" % (cfg["username"], cfg["password"]))
-g.login(cfg["username"], cfg["password"])
+# print("Using %s and %s to connect" % (cfg["username"], cfg["password"]))
+# g.login(cfg["username"], cfg["password"])
-print("Uploading %s" % args.tcx)
-wId = g.upload_ctx(args.tcx)
+# print("Uploading %s" % args.tcx)
+# wId = g.upload_ctx(args.tcx)
-name = os.path.splitext(os.path.basename(args.tcx))
-print("Renaming it to %s" % name[0])
-g.name_workout(wId, name[0])
+# print("Renaming it to %s" % name[0])
+# g.name_workout(wId, name[0])
Please sign in to comment.
Something went wrong with that request. Please try again.