2
# -*- coding: iso-8859-1 -*-
4
#Copyright (C) Fiz Vazquez vud1@sindominio.net
6
#This program is free software; you can redistribute it and/or
7
#modify it under the terms of the GNU General Public License
8
#as published by the Free Software Foundation; either version 2
9
#of the License, or (at your option) any later version.
11
#This program is distributed in the hope that it will be useful,
12
#but WITHOUT ANY WARRANTY; without even the implied warranty of
13
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
#GNU General Public License for more details.
16
#You should have received a copy of the GNU General Public License
17
#along with this program; if not, write to the Free Software
18
#Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
26
from lxml import etree
27
from pytrainer.lib.xmlUtils import XMLParser
28
import dateutil.parser
29
from datetime import date, timedelta, datetime
30
from dateutil.tz import * # for tzutc()
33
class garmintools_full():
34
""" Plugin to import from a Garmin device using garmintools
35
Checks each activity to see if any entries are in the database with the same start time
36
Creates GPX files for each activity not in the database
38
Note: using lxml see http://codespeak.net/lxml
40
def __init__(self, parent = None, validate=False):
42
self.confdir = self.parent.conf.getValue("confdir")
43
self.tmpdir = self.parent.conf.getValue("tmpdir")
44
# Tell garmintools where to save retrieved data from GPS device
45
os.environ['GARMIN_SAVE_RUNS']=self.tmpdir
46
self.data_path = os.path.dirname(__file__)
47
self.validate = validate
48
self.sport = self.getConfValue("Force_sport_to")
49
self.deltaDays = self.getConfValue("Not_older_days")
50
if self.deltaDays is None:
51
logging.info("Delta days not set, retrieving complete history, defaulting to 0")
53
#so far hardcoded to False - dg 20100104
54
#self.legacyComp = self.getConfValue("Legacy_comparison")
55
self.maxGap = self.getConfValue("Max_gap_seconds")
56
if self.maxGap is None:
57
logging.info("No gap defined, strict comparison")
60
def getConfValue(self, confVar):
61
info = XMLParser(self.data_path+"/conf.xml")
62
code = info.getValue("pytrainer-plugin","plugincode")
63
plugindir = self.parent.conf.getValue("plugindir")
64
if not os.path.isfile(plugindir+"/"+code+"/conf.xml"):
67
info = XMLParser(plugindir+"/"+code+"/conf.xml")
68
value = info.getValue("pytrainer-plugin",confVar)
74
if self.checkLoadedModule():
75
numError = self.getDeviceInfo()
77
#TODO Remove Zenity below
78
outgps = commands.getstatusoutput("garmin_save_runs | zenity --progress --pulsate --text='Loading Data' auto-close")
80
# now we should have a lot of gmn (binary) files under $GARMIN_SAVE_RUNS
81
foundFiles = self.searchFiles(self.tmpdir, "gmn")
82
logging.info("Retrieved "+str(len(foundFiles))+" entries from GPS device")
83
# Trying to minimize number of files to dump
84
if int(self.deltaDays) > 0:
85
selectedFiles = self.discardOld(foundFiles)
87
logging.info("Retrieving complete history from GPS device")
88
selectedFiles = foundFiles
89
if len(selectedFiles) > 0:
90
logging.info("Dumping "+str(len(selectedFiles))+" binary files found")
91
dumpFiles = self.dumpBinaries(selectedFiles)
92
self.listStringDBUTC = self.parent.parent.ddbb.select("records","date_time_utc")
94
logging.info("Starting import. Comparison will be made with "+str(self.maxGap)+" seconds interval")
96
logging.info("Starting import. Comparison will be strict")
97
importFiles = self.importEntries(dumpFiles)
99
logging.info("No new entries to add")
101
logging.error("Error when retrieving data from GPS device")
103
#TODO Remove Zenity below
105
os.popen("zenity --error --text='No Garmin device found\nCheck your configuration'");
107
os.popen("zenity --error --text='Can not find garmintools binaries\nCheck your configuration'")
108
else: #No garmin device found
109
#TODO Remove Zenity below
110
os.popen("zenity --error --text='Can not handle Garmin device (wrong module loaded)\nCheck your configuration'");
111
logging.info("Entries to import: "+str(len(importFiles)))
115
def discardOld(self, listEntries):
118
logging.info("Discarding entries older than "+str(self.deltaDays)+" days")
119
limit = datetime.now() - timedelta(days = int(self.deltaDays))
120
for entry in listEntries:
121
filename = os.path.split(entry)[1].rstrip(".gmn")
122
filenameDateTime = datetime.strptime(filename,"%Y%m%dT%H%M%S")
123
logging.debug("Entry time: "+str(filenameDateTime)+" | limit: "+str(limit))
124
if filenameDateTime < limit:
125
logging.debug("Discarding old entry: "+str(filenameDateTime))
127
tempList.append(entry)
131
def importEntries(self, entries):
132
# modified from garmintools plugin written by jb
134
logging.debug("Selected files: "+str(entries))
136
for filename in entries:
137
if self.valid_input_file(filename):
138
#Garmin dump files are not valid xml - need to load into a xmltree
139
#read file into string
140
with open(filename, 'r') as f:
142
fileString = StringIO.StringIO("<root>"+xmlString+"</root>")
144
tree = etree.parse(fileString)
145
#if not self.inDatabase(tree, filename):
146
if not self.entryExists(tree, filename):
147
sport = self.getSport(tree)
148
gpxfile = "%s/garmintools-%d.gpx" % (self.tmpdir, len(importfiles))
149
self.createGPXfile(gpxfile, tree)
150
importfiles.append((gpxfile, sport))
152
logging.debug("%s already present. Skipping import." % (filename,) )
154
logging.error("File %s failed validation" % (filename))
158
def valid_input_file(self, filename):
159
""" Function to validate input file if requested"""
160
if not self.validate: #not asked to validate
161
logging.debug("Not validating %s" % (filename) )
164
logging.debug("Cannot validate garmintools dump files yet")
166
'''xslfile = os.path.realpath(self.parent.parent.data_path)+ "/schemas/GarminTrainingCenterDatabase_v2.xsd"
167
from lib.xmlValidation import xmlValidator
168
validator = xmlValidator()
169
return validator.validateXSL(filename, xslfile)'''
171
def entryExists(self, tree, filename):
173
stringStartDatetime = self.detailsFromFile(tree) # this time is localtime! (with timezone offset)
175
if stringStartDatetime is not None:
176
startDatetime = dateutil.parser.parse(stringStartDatetime)
177
# converting to utc for proper comparison with date_time_utc
178
stringStartUTC = startDatetime.astimezone(tzutc()).strftime("%Y-%m-%dT%H:%M:%SZ")
179
if self.checkDupe(stringStartUTC, self.listStringDBUTC, int(self.maxGap)):
182
logging.info("Marking "+str(filename)+" | "+str(stringStartUTC)+" to import")
185
logging.debug("Not able to find start time, please check "+str(filename))
186
exists = True # workaround for old/not correct entries (will crash at some point during import process otherwise)
190
def checkDupe(self, stringStartUTC, listStringStartUTC, gap):
191
""" Checks if there is any startUTC in DB between provided startUTC plus a defined gap:
192
Check for same day (as baselined to UTC)
193
startDatetime + delta (~ 3 mins) >= listDatetime[x]
198
returns: True if any coincidence is found. False otherwise"""
202
# Retrieve date from 2010-01-14T11:34:49Z
203
stringStartDate = stringStartUTC[0:10]
204
for entry in listStringStartUTC:
205
#logging.debug("start: "+str(startDatetime)+" | entry: "+str(entry)+" | gap: "+str(datetimePlusDelta))
206
if entry[0] is not None:
207
if str(entry[0]).startswith(stringStartDate):
208
deltaGap = timedelta(seconds=gap)
209
datetimeStartUTC = datetime.strptime(stringStartUTC,"%Y-%m-%dT%H:%M:%SZ")
210
datetimeStartUTCDB = datetime.strptime(entry[0],"%Y-%m-%dT%H:%M:%SZ")
211
datetimePlusDelta = datetimeStartUTC + deltaGap
212
if datetimeStartUTC <= datetimeStartUTCDB and datetimeStartUTCDB <= datetimePlusDelta:
214
logging.debug("Found: "+str(stringStartUTC)+" <= "+str(entry[0])+" <= "+str(datetimePlusDelta))
217
if (stringStartUTC,) in listStringStartUTC: # strange way to store results from DB
222
def getSport(self, tree):
223
#return sport from file or overide if present
226
root = tree.getroot()
227
sportElement = root.find(".//run")
229
sport = sportElement.get("sport")
230
sport = sport.capitalize()
235
def detailsFromFile(self, tree):
236
root = tree.getroot()
238
pointElement = root.find(".//point")
239
if pointElement is not None:
240
stringStartDatetime = pointElement.get("time")
241
return stringStartDatetime
244
def createGPXfile(self, gpxfile, tree):
245
""" Function to transform a Garmintools dump file to a valid GPX+ file
247
xslt_doc = etree.parse(self.data_path+"/translate.xsl")
248
transform = etree.XSLT(xslt_doc)
249
result_tree = transform(tree)
250
result_tree.write(gpxfile, xml_declaration=True)
252
def dumpBinaries(self, listFiles):
255
for filename in listFiles:
256
outdump = filename.replace('.gmn', '.dump')
257
logging.debug("outdump: "+str(outdump))
258
result = commands.getstatusoutput("garmin_dump %s > %s" %(filename,outdump))
260
dumpFiles.append(outdump)
262
logging.error("Error when creating dump of "+str(filename)+": "+str(result))
266
def searchFiles(self, rootPath, extension):
269
logging.debug("rootPath: "+str(rootPath))
270
result = commands.getstatusoutput("find %s -name *.%s" %(rootPath,extension))
272
foundFiles = result[1].splitlines()
273
#logging.debug("Found files: "+str(foundFiles))
274
logging.info ("Found files: "+str(len(foundFiles)))
276
logging.error("Not able to locate files from GPS: "+str(result))
280
def getDeviceInfo(self):
282
result = commands.getstatusoutput('garmin_get_info')
283
logging.debug("Returns "+str(result))
286
if result[1] != "garmin unit could not be opened!":
288
#ToDo: review, always get "lxml.etree.XMLSyntaxError: PCDATA invalid Char value 28, line 6, column 29" error
289
xmlString = result[1].rstrip()
290
logging.debug("xmlString: "+str(xmlString))
291
prueba = etree.XMLID(xmlString)
292
logging.debug("Prueba: "+str(prueba))
293
tree = etree.fromstring(xmlString)
294
description = self.getProductDesc(tree)
295
if description is not None:
296
logging.info("Found "+str(description))
300
logging.error("Not able to identify GPS device. Continuing anyway...")
303
logging.error(result[1])
306
logging.error("Can not find garmintools binaries, please check your installation")
311
def getProductDesc(self, tree):
312
root = tree.getroot()
313
pointProduct = root.find(".//garmin_product")
314
if pointProduct is not None:
315
desc = pointProduct.get("product_description")
319
def checkLoadedModule(self):
321
outmod = commands.getstatusoutput('/sbin/lsmod | grep garmin_gps')
322
if outmod[0]==256: #there is no garmin_gps module loaded
329
def createUserdirBackup(self):
331
result = commands.getstatusoutput('tar -cvzf '+os.environ['HOME']+'/pytrainer_`date +%Y%m%d_%H%M`.tar.gz '+self.confdir)
333
raise Exception, "Copying current user directory does not work, error #"+str(result)
335
logging.info('User directory backup successfully created')