Changeset 53bb3e5 in OpenWorkouts-current for ow/models


Ignore:
Timestamp:
Jan 9, 2019, 12:31:33 PM (5 years ago)
Author:
borja <borja@…>
Branches:
current, feature/docs, master
Children:
119412d
Parents:
e3d7b13
Message:

(#13) - fit files parsing + (#26) - generate .gpx from .fit

  • Added fitparse as a new dependency IMPORTANT: please install it in your existing envs:

pip install python-fitparse

  • Added new attribute to workouts to store attached fit files as Blob objects. IMPORTANT: please update any workouts you have in your db, adding the fit_file attribute to them (None by default)
  • Added new module with the code needed to interact with .fit files (parse, gather data, transform to gpx)
  • Added code to "load" a workout from a fit file
  • Added tools and helpers to transform values (meters->kilometers, meters-per-second->kms-per-hour, semicircles-to-degrees, etc)
  • Refactored some imports
File:
1 edited

Legend:

Unmodified
Added
Removed
  • ow/models/workout.py

    re3d7b13 r53bb3e5  
    77from repoze.folder import Folder
    88from pyramid.security import Allow, Everyone
    9 from ow.utilities import GPXMinidomParser
     9
     10from ow.utilities import (
     11    GPXMinidomParser,
     12    copy_blob,
     13    create_blob,
     14)
     15
     16from ow.fit import Fit
    1017
    1118
     
    5663        self.tracking_file = kw.get('tracking_file', None)  # Blob
    5764        self.tracking_filetype = ''  # unicode string
     65        # attr to store ANT fit files. For now this file is used to
     66        # generate a gpx-encoded tracking file we then use through
     67        # the whole app
     68        self.fit_file = kw.get('fit_file', None)  # Blob
    5869
    5970    @property
     
    188199        return None
    189200
     201    @property
     202    def tracking_file_path(self):
     203        """
     204        Get the path to the blob file attached as a tracking file.
     205
     206        First check if the file was not committed to the db yet (new workout
     207        not saved yet) and use the path to the temporary file on the fs.
     208        If none is found there, go for the real blob file in the blobs
     209        directory
     210        """
     211        path = None
     212        if self.tracking_file:
     213            path = self.tracking_file._p_blob_uncommitted
     214            if path is None:
     215                path = self.tracking_file._p_blob_committed
     216        return path
     217
     218    @property
     219    def fit_file_path(self):
     220        """
     221        Get the path to the blob file attached as a fit file.
     222
     223        First check if the file was not committed to the db yet (new workout
     224        not saved yet) and use the path to the temporary file on the fs.
     225        If none is found there, go for the real blob file in the blobs
     226        directory
     227        """
     228        path = None
     229        if self.fit_file:
     230            path = self.fit_file._p_blob_uncommitted
     231            if path is None:
     232                path = self.fit_file._p_blob_committed
     233        return path
     234
    190235    def load_from_file(self):
    191236        """
     
    195240        if self.tracking_filetype == 'gpx':
    196241            self.load_from_gpx()
     242        elif self.tracking_filetype == 'fit':
     243            self.load_from_fit()
    197244
    198245    def load_from_gpx(self):
     
    283330        return parser.tracks
    284331
     332    def load_from_fit(self):
     333        """
     334        Try to load data from an ANT-compatible .fit file (if any has been
     335        added to this workout).
     336
     337        "Load data" means:
     338
     339        1. Copy over the uploaded fit file to self.fit_file, so we can keep
     340           that copy around for future use
     341
     342        2. generate a gpx object from the fit file
     343
     344        3. save the gpx object as the tracking_file, which then will be used
     345           by the current code to display and gather data to be displayed/shown
     346           to the user.
     347
     348        4. Grab some basic info from the fit file and store it in the Workout
     349        """
     350        # backup the fit file
     351        self.fit_file = copy_blob(self.tracking_file)
     352
     353        # create an instance of our Fit class
     354        fit = Fit(self.fit_file_path)
     355        fit.load()
     356
     357        # fit -> gpx and store that as the main tracking file
     358        self.tracking_file = create_blob(fit.gpx, 'gpx')
     359        self.tracking_filetype = 'gpx'
     360
     361        # grab the needed data from the fit file, update the workout
     362        self.start = fit.data['start']
     363        # ensure this datetime start object is timezone-aware
     364        self.start = self.start.replace(tzinfo=timezone.utc)
     365        # duration comes in seconds, store a timedelta
     366        self.duration = timedelta(seconds=fit.data['duration'])
     367        # distance comes in meters
     368        self.distance = Decimal(fit.data['distance']) / Decimal(1000.00)
     369        self.uphill = Decimal(fit.data['uphill'])
     370        self.downhill = Decimal(fit.data['downhill'])
     371        # If the user did not provide us with a title, build one from the
     372        # info in the fit file
     373        if not self.title:
     374            self.title = fit.name
     375
     376        if fit.data['avg_hr']:
     377            self.hr_avg = Decimal(fit.data['avg_hr'])
     378            self.hr_min = Decimal(fit.data['min_hr'])
     379            self.hr_max = Decimal(fit.data['max_hr'])
     380
     381        if fit.data['avg_cad']:
     382            self.cad_avg = Decimal(fit.data['avg_cad'])
     383            self.cad_min = Decimal(fit.data['min_cad'])
     384            self.cad_max = Decimal(fit.data['max_cad'])
     385
     386        if fit.data['avg_atemp']:
     387            self.atemp_avg = Decimal(fit.data['avg_atemp'])
     388            self.atemp_min = Decimal(fit.data['min_atemp'])
     389            self.atemp_max = Decimal(fit.data['max_atemp'])
     390
     391        return True
     392
    285393    @property
    286394    def has_tracking_file(self):
     
    290398    def has_gpx(self):
    291399        return self.has_tracking_file and self.tracking_filetype == 'gpx'
     400
     401    @property
     402    def has_fit(self):
     403        return self.fit_file is not None
Note: See TracChangeset for help on using the changeset viewer.