Skip to content

Commit

Permalink
### Version 5.1.2 (build 2308.2601)
Browse files Browse the repository at this point in the history
#### Solved Issues
- Conversion failed for open water swimming activities in conjunction with distance normalization. Closes #74.
  • Loading branch information
CTHRU committed Aug 26, 2023
1 parent 12f5b48 commit 72b62a6
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 39 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
All notable changes to this project are documented in this file.

## Release Notes
### Version 5.1.2 (build 2308.2601)
#### Solved Issues
- Conversion failed for open water swimming activities in conjunction with distance normalization. Closes #74.

### Version 5.1.1 (build 2207.2501)
#### New features and changes
- Added support for conversion of activities with dummy location (start) records.
Expand Down
59 changes: 32 additions & 27 deletions Hitrava.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Hitrava.py
# Original Work Copyright (c) 2019 Ari Cooper-Davis / Christoph Vanthuyne - github.com/aricooperdavis/Huawei-TCX-Converter
# Modified Work Copyright (c) 2019-2020 Christoph Vanthuyne - https://github.com/CTHRU/Hitrava
# Modified Work Copyright (c) 2019-2023 Christoph Vanthuyne - https://github.com/CTHRU/Hitrava
# Released under the Non-Profit Open Software License version 3.0


Expand Down Expand Up @@ -51,16 +51,16 @@
PROGRAM_NAME = 'Hitrava'
PROGRAM_MAJOR_VERSION = '5'
PROGRAM_MINOR_VERSION = '1'
PROGRAM_PATCH_VERSION = '1'
PROGRAM_MAJOR_BUILD = '2207'
PROGRAM_MINOR_BUILD = '2501'
PROGRAM_PATCH_VERSION = '2'
PROGRAM_MAJOR_BUILD = '2308'
PROGRAM_MINOR_BUILD = '2601'

OUTPUT_DIR = './output'
GPS_TIMEOUT = dts_delta(seconds=10)


class HiActivity:
"""" This class represents all the data contained in a HiTrack file."""
""" This class represents all the data contained in a HiTrack file."""

TYPE_WALK = 'Walk'
TYPE_RUN = 'Run'
Expand Down Expand Up @@ -239,7 +239,7 @@ def _add_segment_stop(self, segment_stop: datetime, segment_distance: int = -1):

# TODO Verify if something useful can be done with the (optional) altitude data in the tp=lbs records
def add_location_data(self, data: []):
""""Add location data from a tp=lbs record in the HiTrack file.
""" Add location data from a tp=lbs record in the HiTrack file.
Information:
- When tracking an activity with a mobile phone only, the HiTrack files seem to contain altitude
information in the alt data tag (in ft). This seems not to be the case when an activity is started from a
Expand Down Expand Up @@ -599,8 +599,8 @@ def _add_data_detail(self, data: dict):
self.stop = data['t']

def get_segments(self) -> list:
"""" Returns the segment list.
- For swimming activities, the segments were identified during parsing of the SWOLF data.
""" Returns the segment list.
- For pool swimming activities, the segments were identified during parsing of the SWOLF data.
- For walking, running and cycling activities, the segments must be calculated once based on the parsed
location data. Because the location data is not (always) in chronological order (e.g. loops in the track),
for these activities
Expand All @@ -614,7 +614,7 @@ def _reset_segments(self):
self._current_segment = None

def _detect_activity_type(self) -> str:
""""Auto-detection of the activity type. Only valid when called after all data has been parsed."""
""" Auto-detection of the activity type. Only valid when called after all data has been parsed."""
logging.getLogger(PROGRAM_NAME).debug('Detecting activity type for activity %s with parameters %s',
self.activity_id, self.activity_params)

Expand Down Expand Up @@ -673,7 +673,7 @@ def _detect_activity_type(self) -> str:
return self._activity_type

def _calc_segments_and_distances(self):
"""" Perform the following detailed data calculations for walk, run, or cycle activities:
""" Perform the following detailed data calculations for walk, run, or cycle activities:
- segment list
- segment start, stop, duration and cumulative distance
- detailed track point cumulative distances
Expand Down Expand Up @@ -802,7 +802,7 @@ def get_segment_data(self, segment: dict) -> list:
segment_data_dict = {k: v for k, v in self.data_dict.items()
if segment['start'] <= k <= segment['stop']}
else:
# E.g for swimming activities, the last segment is not closed due to no stop record nor valid record that
# E.g. for swimming activities, the last segment is not closed due to no stop record nor valid record that
# indicates the end of the activity. Return all remaining data starting from the start timestamp
segment_data_dict = {k: v for k, v in self.data_dict.items()
if segment['start'] <= k}
Expand Down Expand Up @@ -841,15 +841,15 @@ def get_swim_data(self) -> Optional[list]:
return None

def _calc_pool_swim_data(self) -> list:
"""" Calculates the real swim (lap) data based on the raw parsed pool swim data
""" Calculates the real swim (lap) data based on the raw parsed pool swim data
The following calculation steps on the raw parsed data is applied.
1. Starting point is the raw parsed data per lap (segment). The data consists of multiple data records
with a 5 second time interval containing the same SWOLF and stroke frequency (in strokes/minute) values.
with a 5-second time interval containing the same SWOLF and stroke frequency (in strokes/minute) values.
2. Calculate the number of strokes in the lap.
Number of strokes = stroke frequency x (last - first lqp timestamp) / 60
3. Calculate the lap time: lap time = SWOLF - number of strokes
:return
:return:
A list of lap data dictionaries containing the following data:
'lap' : lap number in the activity
'start' : Start timestamp of the lap
Expand Down Expand Up @@ -919,7 +919,7 @@ def _calc_pool_swim_data(self) -> list:
return swim_data

def _get_open_water_swim_data(self) -> list:
"""" Calculates the real swim (lap) data based on the raw parsed open water swim data"""
""" Calculates the real swim (lap) data based on the raw parsed open water swim data """
logging.getLogger(PROGRAM_NAME).info('Calculating swim data for activity %s', self.activity_id)

swim_data = []
Expand Down Expand Up @@ -1248,20 +1248,20 @@ def parse(self, from_date: datetime.date = datetime.date(1970, 1, 1)) -> list:
# data {list}
# 00 {dict}
# motionPathData {list}
# 0 {dict)
# 0 {dict}
# sportType {int}
# attribute {str} 'HW_EXT_TRACK_DETAIL@is<HiTrack File Data>&&HW_EXT_TRACK_SIMPLIFY@is<Other Data>
# 1 {dict)
# 1 {dict}
# sportType {int}
# attribute {str} 'HW_EXT_TRACK_DETAIL@is<HiTrack File Data>&&HW_EXT_TRACK_SIMPLIFY@is<Other Data>
# recordDay {int} 'YYYYMMDD'
#
# JSON data structure AS OF 07/2020
# data {list}
# 0 {dict)
# 0 {dict}
# sportType {int}
# attribute {str} 'HW_EXT_TRACK_DETAIL@is<HiTrack File Data>&&HW_EXT_TRACK_SIMPLIFY@is<Other Data>
# 1 {dict)
# 1 {dict}
# sportType {int}
# attribute {str} 'HW_EXT_TRACK_DETAIL@is<HiTrack File Data>&&HW_EXT_TRACK_SIMPLIFY@is<Other Data>
n = -1
Expand Down Expand Up @@ -1299,7 +1299,7 @@ def parse(self, from_date: datetime.date = datetime.date(1970, 1, 1)) -> list:
logging.getLogger(PROGRAM_NAME).error('Error parsing JSON file <%s>\n%s', self.json_file.name, e)
raise Exception('Error parsing JSON file <%s>', self.json_file.name)

def _parse_activity(self, activity_dict: dict) -> HiActivity:
def _parse_activity(self, activity_dict: dict) -> Optional[HiActivity]:
# Create a HiTrack file from the HiTrack data
hitrack_data = activity_dict['attribute']
# Strip prefix and suffix from raw HiTrack data
Expand Down Expand Up @@ -1430,6 +1430,11 @@ def _parse_activity(self, activity_dict: dict) -> HiActivity:
activity_start,
sport_type)

# For open water swimming activities, the SWOLF based segment data can not be used.
# Replace it by the raw GPS data (done in
if hi_activity.get_activity_type() == HiActivity.TYPE_OPEN_WATER_SWIM:
hi_activity.get_swim_data()

# Start date and time (in UTC)
hi_activity.start = activity_start

Expand Down Expand Up @@ -1499,7 +1504,7 @@ class TcxActivity:
(HiActivity.TYPE_INDOOR_CYCLE, 'biking'), # Not recognized by Strava TCX upload, change activity type after upload manually to Virtual Ride.
(HiActivity.TYPE_CROSS_TRAINER, 'elliptical'), # Not recognized by Strava TCX upload, change activity type after upload manually to Elliptical.
(HiActivity.TYPE_OTHER, _SPORT_OTHER),
(HiActivity.TYPE_CROSSFIT, 'crossfit'), # Not recognzied by Strava TCX upload, chnage activity type after upload manually to Crossfit.
(HiActivity.TYPE_CROSSFIT, 'crossfit'), # Not recognized by Strava TCX upload, change activity type after upload manually to Crossfit.
(HiActivity.TYPE_UNKNOWN, _SPORT_OTHER),
(HiActivity.TYPE_CROSS_COUNTRY_RUN, 'running')]

Expand Down Expand Up @@ -1543,7 +1548,7 @@ def _get_sport(self):
return sport

def generate_xml(self) -> xml_et.Element:
""""Generates the TCX XML content."""
""" Generates the TCX XML content."""
logging.getLogger(PROGRAM_NAME).debug('Generating TCX XML data for activity %s', self.hi_activity.activity_id)
try:
# * TrainingCenterDatabase
Expand Down Expand Up @@ -1920,16 +1925,16 @@ def _convert_hitrack_timestamp(hitrack_timestamp: float, timestamp_ref: datetime


def _get_tz_aware_datetime(naive_datetime: dts, time_zone: tz):
""""All datetimes in the HiActivity are represented as UTC datetimes and are parsed as time zone unaware
""" All datetimes in the HiActivity are represented as UTC datetimes and are parsed as time zone unaware
(naive) datetime objects. This method returns the equivalent time zone aware datetime representation using the
time zone information of the HiTrack activity (if any). If the Hitrack activity has no time zone information,
UTC (GMT) time zone is assumed.
:param naive_datetime:
:param time_zone:
:type naive_datetime: datetime.datetime
:param time_zone:
:type time_zone: datetime.timezone
:return
:return:
The (time zone) aware datetime corresponding to the naive datetime in the naive_datetime parameter
"""
utc_datetime = dts.replace(naive_datetime, tzinfo=tz.utc)
Expand All @@ -1942,7 +1947,7 @@ def _get_tz_aware_datetime(naive_datetime: dts, time_zone: tz):


def _init_logging(level: str = 'INFO'):
""""
"""
Initializes the Python logging.getLogger(PROGRAM_NAME). A program specific Logger is created.
Parameters:
Expand Down Expand Up @@ -2039,7 +2044,7 @@ def pool_length_type(arg):
tcx_group.add_argument('--tcx_use_raw_distance_data',
help='In JSON or ZIP mode, when using this option the converted TCX files will use the raw \
distance data as calculated from the raw HiTrack data. When not specified (default), all \
distances in the TCX files will be normalized to match the original Huawei distance.' ,
distances in the TCX files will be normalized to match the original Huawei distance.',
action='store_true')

output_group = parser.add_argument_group('OUTPUT options')
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Non-Profit Open Software License 3.0 (NPOSL-3.0)

Copyright (c) 2019-2022 Christoph Vanthuyne
Copyright (c) 2019-2023 Christoph Vanthuyne

This Non-Profit Open Software License ("Non-Profit OSL") version 3.0 (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work:

Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

----------
## Introduction
Hitrava converts health activities registered using a Honor or Huawei activity tracker or smart watch in the
Hitrava converts health activities registered using a Honor or Huawei activity tracker or smartwatch in the
[`Huawei Health`](https://play.google.com/store/apps/details?id=com.huawei.health) app into a file format that can be
directly uploaded to [`Strava`](https://strava.com).

Expand Down Expand Up @@ -49,15 +49,14 @@ learn more on [https://cthru.hopto.org](https://cthru.hopto.org/hitrava-web).
- Crossfit
- Conversion contains generic activity information such as GPS track, distance, duration, calorie consumption (as
available during recording of the activity).
- When available and depending on the activity type, conversion includes health data from your Huawei or Honor smart
watch / fitness band:
- When available and depending on the activity type, conversion includes health data from your Huawei or Honor smartwatch / fitness band:
- Heart rate
- Cadence
- Conversion is done using the centralized data from Huawei Health. In principle, any recent Huawei or Honor smart watch
- Conversion is done using the centralized data from Huawei Health. In principle, any recent Huawei or Honor smartwatch
or fitness band should be supported, if you see the data in Huawei Health, e.g.
- Huawei smart watches: e.g. Huawei Watch GT2
- Huawei smartwatches: e.g. Huawei Watch GT2
- Huawei fitness bands: e.g. Huawei Band 4, Huawei Band 4 Pro
- Honor smart watches: e.g. Honor MagicWatch 2
- Honor smartwatches: e.g. Honor MagicWatch 2
- Honor fitness bands: e.g. Honor Band 4, Honor Band 5

## Installation
Expand Down Expand Up @@ -96,7 +95,7 @@ above. Your Hitrava installation folder should now contain at least the followin
> Run_Hitrava_Decrypt.cmd
## How to convert your health activities and import them in Strava
All users can use conversion from a **[ZIP](#Encrypted-ZIP-conversion-procedure)** file or a **[JSON](#JSON-file-conversion-example)** file.
All users can use conversion from a **[ZIP](#Windows-Users---Encrypted-ZIP-conversion-procedure)** file or a **[JSON](#JSON-file-conversion-example)** file.
For users with rooted phones, legacy **[file](#single-file-conversion-examples)** and
**[tar](#tar-file-conversion-examples)** options are still available.

Expand Down Expand Up @@ -128,7 +127,7 @@ step 3 below.

#### Step 3 - Convert the data with Hitrava

>**Tip**: If you're on Windows and you're not familiar with the Command Prompt or just want to do a quick
>**Tip**: If you're on Windows, and you're not familiar with the Command Prompt or just want to do a quick
> conversion with default arguments, you can use the _Run_Hitrava_Decrypt.cmd_ batch file.
>- Open the _Run_Hitrava_Decrypt.cmd_ file with a text editor and change the password 123456 to the password you
>provided in step 2 above.
Expand Down Expand Up @@ -329,12 +328,12 @@ python Hitrava.py --file HiTrack_12345678901212345678912
```
The next example converts extracted file HiTrack_12345678901212345678912 to HiTrack_12345678901212345678912.tcx in
the _./my_output_dir_ directory. The program logging level is set to display debug messages. The converted file is
validated against the TCX XSD schema (requires installed xmlschema library and an internet connection).
validated against the TCX XSD schema (requires xmlschema library and an internet connection).
```
python Hitrava.py --file HiTrack_12345678901212345678912 --output_dir my_output_dir --validate_xml --log_level DEBUG
```
The following example converts an extracted file HiTrack_12345678901212345678912 to HiTrack_12345678901212345678912.tcx
in the _./output_ directory and forces the sport to walking.
in the _./output_ directory and forces the sport to 'walking'.
```
python Hitrava.py --file HiTrack_12345678901212345678912 --sport Walk
```
Expand Down Expand Up @@ -363,7 +362,7 @@ For a full changelog of all versions, please look in [`CHANGELOG.md`](./CHANGELO
## Copyright and License
[![nposl3.0][shield nposl3.0]][tldrlegal nposl3.0]

Copyright (c) 2019-2022 Christoph Vanthuyne
Copyright (c) 2019-2023 Christoph Vanthuyne

Licensed under the Non-Profit Open Software License version 3.0 from Hitrava version 3.1.1 onward.

Expand Down

0 comments on commit 72b62a6

Please sign in to comment.