diff --git a/CHANGELOG.md b/CHANGELOG.md index 687327f..5048e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Hitrava.py b/Hitrava.py index fc5011f..d7801c2 100644 --- a/Hitrava.py +++ b/Hitrava.py @@ -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 @@ -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' @@ -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 @@ -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 @@ -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) @@ -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 @@ -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} @@ -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 @@ -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 = [] @@ -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&&HW_EXT_TRACK_SIMPLIFY@is - # 1 {dict) + # 1 {dict} # sportType {int} # attribute {str} 'HW_EXT_TRACK_DETAIL@is&&HW_EXT_TRACK_SIMPLIFY@is # 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&&HW_EXT_TRACK_SIMPLIFY@is - # 1 {dict) + # 1 {dict} # sportType {int} # attribute {str} 'HW_EXT_TRACK_DETAIL@is&&HW_EXT_TRACK_SIMPLIFY@is n = -1 @@ -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 @@ -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 @@ -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')] @@ -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 @@ -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) @@ -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: @@ -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') diff --git a/LICENSE.md b/LICENSE.md index dd3ee89..228ec6c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -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: diff --git a/README.md b/README.md index 2f51725..009ab14 100644 --- a/README.md +++ b/README.md @@ -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). @@ -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 @@ -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. @@ -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. @@ -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 ``` @@ -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.