GarminDb 3.6.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. garmindb/GarminConnectConfig.json.example +51 -0
  2. garmindb/__init__.py +39 -0
  3. garmindb/activities_fit_data.py +29 -0
  4. garmindb/activity_fit_file_processor.py +424 -0
  5. garmindb/activity_fit_plugin_base.py +45 -0
  6. garmindb/analyze.py +201 -0
  7. garmindb/checkup.py +118 -0
  8. garmindb/copy.py +64 -0
  9. garmindb/download.py +321 -0
  10. garmindb/export_activities.py +52 -0
  11. garmindb/fit_data.py +59 -0
  12. garmindb/fit_file_processor.py +250 -0
  13. garmindb/fitbitdb/__init__.py +11 -0
  14. garmindb/fitbitdb/analyze.py +84 -0
  15. garmindb/fitbitdb/fitbit_db.py +157 -0
  16. garmindb/fitbitdb/import_csv.py +73 -0
  17. garmindb/garmin_connect_config_manager.py +268 -0
  18. garmindb/garmin_connect_enums.py +346 -0
  19. garmindb/garmin_json_data.py +326 -0
  20. garmindb/garmin_tcx_data.py +148 -0
  21. garmindb/garmindb/__init__.py +14 -0
  22. garmindb/garmindb/activities_db.py +646 -0
  23. garmindb/garmindb/garmin_db.py +476 -0
  24. garmindb/garmindb/garmin_summary_db.py +121 -0
  25. garmindb/garmindb/monitoring_db.py +292 -0
  26. garmindb/import_monitoring.py +483 -0
  27. garmindb/monitoring_fit_file_processor.py +111 -0
  28. garmindb/monitoring_fit_plugin_base.py +26 -0
  29. garmindb/mshealthdb/__init__.py +11 -0
  30. garmindb/mshealthdb/analyze.py +90 -0
  31. garmindb/mshealthdb/import_csv.py +129 -0
  32. garmindb/mshealthdb/mshealth_db.py +195 -0
  33. garmindb/open_with_basecamp.py +17 -0
  34. garmindb/open_with_google_earth.py +17 -0
  35. garmindb/plugin_base.py +29 -0
  36. garmindb/plugin_manager.py +34 -0
  37. garmindb/sleep_fit_file_processor.py +45 -0
  38. garmindb/statistics.py +28 -0
  39. garmindb/summarydb/__init__.py +10 -0
  40. garmindb/summarydb/summary_base.py +223 -0
  41. garmindb/summarydb/summary_db.py +86 -0
  42. garmindb/tcx.py +128 -0
  43. garmindb/version.py +32 -0
  44. garmindb/version_info.py +17 -0
  45. garmindb-3.6.5.data/scripts/fitbit.py +69 -0
  46. garmindb-3.6.5.data/scripts/garmindb_bug_report.py +61 -0
  47. garmindb-3.6.5.data/scripts/garmindb_checkup.py +47 -0
  48. garmindb-3.6.5.data/scripts/garmindb_cli.py +374 -0
  49. garmindb-3.6.5.data/scripts/mshealth.py +71 -0
  50. garmindb-3.6.5.dist-info/METADATA +113 -0
  51. garmindb-3.6.5.dist-info/RECORD +54 -0
  52. garmindb-3.6.5.dist-info/WHEEL +5 -0
  53. garmindb-3.6.5.dist-info/licenses/LICENSE +339 -0
  54. garmindb-3.6.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,51 @@
1
+ {
2
+ "db": {
3
+ "type" : "sqlite"
4
+ },
5
+ "garmin": {
6
+ "domain" : "garmin.com"
7
+ },
8
+ "credentials": {
9
+ "user" : "",
10
+ "secure_password" : false,
11
+ "password" : "",
12
+ "password_file" : null
13
+ },
14
+ "data": {
15
+ "weight_start_date" : "12/31/2019",
16
+ "sleep_start_date" : "12/31/2019",
17
+ "rhr_start_date" : "12/31/2019",
18
+ "monitoring_start_date" : "12/31/2019",
19
+ "download_latest_activities" : 25,
20
+ "download_all_activities" : 1000
21
+ },
22
+ "directories": {
23
+ "relative_to_home" : true,
24
+ "base_dir" : "HealthData",
25
+ "mount_dir" : "/Volumes/GARMIN"
26
+ },
27
+ "enabled_stats": {
28
+ "monitoring" : true,
29
+ "steps" : true,
30
+ "itime" : true,
31
+ "sleep" : true,
32
+ "rhr" : true,
33
+ "weight" : true,
34
+ "activities" : true
35
+ },
36
+ "course_views": {
37
+ "steps" : []
38
+ },
39
+ "modes": {
40
+ },
41
+ "activities": {
42
+ "display" : []
43
+ },
44
+ "settings": {
45
+ "metric" : false,
46
+ "default_display_activities" : ["walking", "running", "cycling"]
47
+ },
48
+ "checkup": {
49
+ "look_back_days" : 90
50
+ }
51
+ }
garmindb/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """A database and database objects for storing health data from Garmin Connect."""
2
+
3
+ __author__ = "Tom Goetz"
4
+ __copyright__ = "Copyright Tom Goetz"
5
+ __license__ = "GPL"
6
+
7
+ # flake8: noqa
8
+
9
+ from .version_info import version_string
10
+
11
+ __version__ = version_string()
12
+
13
+ from .activity_fit_plugin_base import ActivityFitPluginBase
14
+ from .monitoring_fit_plugin_base import MonitoringFitPluginBase
15
+ from .activity_fit_file_processor import ActivityFitFileProcessor
16
+ from .fit_data import FitData
17
+ from .fit_file_processor import FitFileProcessor
18
+ from .garmin_connect_config_manager import GarminConnectConfigManager
19
+ from .statistics import Statistics
20
+ from .tcx import Tcx
21
+ from .monitoring_fit_file_processor import MonitoringFitFileProcessor
22
+ from .sleep_fit_file_processor import SleepFitFileProcessor
23
+ from .export_activities import ActivityExporter
24
+ from .open_with_basecamp import OpenWithBaseCamp
25
+ from .open_with_google_earth import OpenWithGoogleEarth
26
+
27
+ from .checkup import Checkup
28
+
29
+ from .copy import Copy
30
+ from .download import Download
31
+ from .analyze import Analyze
32
+ from .plugin_manager import PluginManager
33
+ from .version import format_version, log_version, python_version_check
34
+
35
+ from .import_monitoring import GarminMonitoringFitData, GarminSleepFitData, GarminSummaryData, GarminUserSettings, GarminSocialProfile, GarminPersonalInformation, GarminWeightData, \
36
+ GarminSleepData, GarminRhrData, GarminSettingsFitData, GarminHydrationData
37
+ from .activities_fit_data import GarminActivitiesFitData
38
+ from .garmin_tcx_data import GarminTcxData
39
+ from .garmin_json_data import GarminJsonSummaryData, GarminJsonDetailsData
@@ -0,0 +1,29 @@
1
+ """Class for importing Garmin activity from FIT files."""
2
+
3
+ __author__ = "Tom Goetz"
4
+ __copyright__ = "Copyright Tom Goetz"
5
+ __license__ = "GPL"
6
+
7
+
8
+ import fitfile
9
+
10
+ from .fit_data import FitData
11
+
12
+
13
+ class GarminActivitiesFitData(FitData):
14
+ """Class for importing Garmin activity data from FIT files."""
15
+
16
+ def __init__(self, input_dir, latest, measurement_system, debug):
17
+ """
18
+ Return an instance of GarminActivitiesFitData.
19
+
20
+ Parameters:
21
+ ----------
22
+ db_params (dict): configuration data for accessing the database
23
+ input_dir (string): directory (full path) to check for data files
24
+ latest (Boolean): check for latest files only
25
+ measurement_system (enum): which measurement system to use when importing the files
26
+ debug (Boolean): enable debug logging
27
+
28
+ """
29
+ super().__init__(input_dir, debug, latest, False, [fitfile.FileType.activity], measurement_system)
@@ -0,0 +1,424 @@
1
+ """Class that takes a parsed activity FIT file object and imports it into a database."""
2
+
3
+ __author__ = "Tom Goetz"
4
+ __copyright__ = "Copyright Tom Goetz"
5
+ __license__ = "GPL"
6
+
7
+ import logging
8
+ import sys
9
+
10
+ import fitfile
11
+
12
+ from .garmindb import File, ActivitiesDb, Activities, ActivityRecords, ActivityLaps, ActivitySplits, ActivitiesDevices, StepsActivities, \
13
+ CycleActivities, ClimbingActivities, PaddleActivities
14
+ from .fit_file_processor import FitFileProcessor
15
+
16
+
17
+ logger = logging.getLogger(__file__)
18
+ logger.addHandler(logging.StreamHandler(stream=sys.stdout))
19
+ root_logger = logging.getLogger()
20
+
21
+
22
+ class ActivityFitFileProcessor(FitFileProcessor):
23
+ """Class that takes a parsed activity FIT file object and imports it into a database."""
24
+
25
+ def write_file(self, fit_file):
26
+ """Given a Fit File object, write all of its messages to the DB."""
27
+ self.activity_fit_file_plugins = [plugin for plugin in self.plugin_manager.get_file_processors('ActivityFit', fit_file).values()]
28
+ if len(self.activity_fit_file_plugins):
29
+ root_logger.info("Loaded %d activity plugins %r for file %s", len(self.activity_fit_file_plugins), self.activity_fit_file_plugins, fit_file)
30
+ # Create the db after setting up the plugins so that plugin tables are handled properly
31
+ self.garmin_act_db = ActivitiesDb(self.db_params, self.debug - 1)
32
+ with self.garmin_db.managed_session() as self.garmin_db_session, self.garmin_act_db.managed_session() as self.garmin_act_db_session:
33
+ self._write_message_types(fit_file, fit_file.message_types)
34
+
35
+ def _plugin_dispatch(self, handler_name, *args, **kwargs):
36
+ return super()._plugin_dispatch(self.activity_fit_file_plugins, handler_name, *args, **kwargs)
37
+
38
+ def _write_device_info_entry(self, fit_file, message_fields):
39
+ device_serial_number = super()._write_device_info_entry(fit_file, message_fields)
40
+ if device_serial_number:
41
+ activity_id = File.id_from_path(fit_file.filename)
42
+ entry = {'activity_id' : activity_id, 'device_serial_number' : device_serial_number}
43
+ if not ActivitiesDevices.s_exists(self.garmin_act_db_session, entry):
44
+ root_logger.debug("_write_device_info_entry activity_id %s, device serial number %s doesn't exist", activity_id, device_serial_number)
45
+ self.garmin_act_db_session.add(ActivitiesDevices(**entry))
46
+
47
+ def _write_lap(self, fit_file, message_type, messages):
48
+ """Write all lap messages to the database."""
49
+ for lap_num, message in enumerate(messages):
50
+ self._write_lap_entry(fit_file, message.fields, lap_num)
51
+
52
+ def _write_split(self, fit_file, message_type, messages):
53
+ """Write all split messages to the database."""
54
+ for split_num, message in enumerate(messages):
55
+ self._write_split_entry(fit_file, message.fields, split_num)
56
+
57
+ def _write_record(self, fit_file, message_type, messages):
58
+ """Write all record messages to the database."""
59
+ for record_num, message in enumerate(messages):
60
+ self._write_record_entry(fit_file, message.fields, record_num)
61
+
62
+ def _write_record_entry(self, fit_file, message_fields, record_num):
63
+ # We don't get record data from multiple sources so we don't need to coellesce data in the DB.
64
+ # It's fastest to just write the new data out if it doesn't currently exist.
65
+ activity_id = File.id_from_path(fit_file.filename)
66
+ plugin_record = self._plugin_dispatch('write_record_entry', self.garmin_act_db_session, fit_file, activity_id, message_fields, record_num)
67
+ if not ActivityRecords.s_exists(self.garmin_act_db_session, {'activity_id' : activity_id, 'record' : record_num}):
68
+ record = {
69
+ 'activity_id' : activity_id,
70
+ 'record' : record_num,
71
+ 'timestamp' : fit_file.utc_datetime_to_local(message_fields.timestamp),
72
+ 'position_lat' : message_fields.get('position_lat'),
73
+ 'position_long' : message_fields.get('position_long'),
74
+ 'distance' : message_fields.get('distance'),
75
+ 'cadence' : message_fields.get('cadence'),
76
+ 'hr' : message_fields.get('heart_rate'),
77
+ 'rr' : message_fields.get('respiration_rate'),
78
+ 'altitude' : message_fields.get('altitude'),
79
+ 'speed' : message_fields.get('speed'),
80
+ 'temperature' : message_fields.get('temperature'),
81
+ }
82
+ record.update(plugin_record)
83
+ root_logger.debug("_write_record_entry activity_id %s, record %s doesn't exist", activity_id, record_num)
84
+ self.garmin_act_db_session.add(ActivityRecords(**record))
85
+
86
+ def _write_lap_entry(self, fit_file, message_fields, lap_num):
87
+ # we don't get laps data from multiple sources so we don't need to coellesce data in the DB.
88
+ # It's fastest to just write new data out if the it doesn't currently exist.
89
+ activity_id = File.id_from_path(fit_file.filename)
90
+ plugin_lap = self._plugin_dispatch('write_lap_entry', self.garmin_act_db_session, fit_file, activity_id, message_fields, lap_num)
91
+ if not ActivityLaps.s_exists(self.garmin_act_db_session, {'activity_id' : activity_id, 'lap' : lap_num}):
92
+ lap = {
93
+ 'activity_id' : File.id_from_path(fit_file.filename),
94
+ 'lap' : lap_num,
95
+ 'start_time' : fit_file.utc_datetime_to_local(message_fields.start_time),
96
+ 'stop_time' : fit_file.utc_datetime_to_local(message_fields.timestamp),
97
+ 'elapsed_time' : message_fields.get('total_elapsed_time'),
98
+ 'moving_time' : message_fields.get('total_timer_time'),
99
+ 'start_lat' : message_fields.get('start_position_lat'),
100
+ 'start_long' : message_fields.get('start_position_long'),
101
+ 'stop_lat' : message_fields.get('end_position_lat'),
102
+ 'stop_long' : message_fields.get('end_position_long'),
103
+ 'distance' : message_fields.get('total_distance'),
104
+ 'cycles' : message_fields.get('total_cycles'),
105
+ 'avg_hr' : message_fields.get('avg_heart_rate'),
106
+ 'max_hr' : message_fields.get('max_heart_rate'),
107
+ 'avg_rr' : message_fields.get('avg_respiration_rate'),
108
+ 'max_rr' : message_fields.get('max_respiration_rate'),
109
+ 'calories' : message_fields.get('total_calories'),
110
+ 'avg_cadence' : message_fields.get('avg_cadence'),
111
+ 'max_cadence' : message_fields.get('max_cadence'),
112
+ 'avg_speed' : message_fields.get('avg_speed'),
113
+ 'max_speed' : message_fields.get('max_speed'),
114
+ 'ascent' : message_fields.get('total_ascent'),
115
+ 'descent' : message_fields.get('total_descent'),
116
+ 'max_temperature' : message_fields.get('max_temperature'),
117
+ 'avg_temperature' : message_fields.get('avg_temperature'),
118
+ }
119
+ lap.update(plugin_lap)
120
+ root_logger.debug("writing lap %r for %s", lap, fit_file.filename)
121
+ self.garmin_act_db_session.add(ActivityLaps(**lap))
122
+
123
+ def _write_split_entry(self, fit_file, message_fields, split_num):
124
+ # we don't get splits data from multiple sources so we don't need to coellesce data in the DB.
125
+ # It's fastest to just write new data out if the it doesn't currently exist.
126
+ activity_id = File.id_from_path(fit_file.filename)
127
+ plugin_split = self._plugin_dispatch('write_split_entry', self.garmin_act_db_session, fit_file, activity_id, message_fields, split_num)
128
+
129
+ if not ActivitySplits.s_exists(self.garmin_act_db_session, {'activity_id' : activity_id, 'split' : split_num}):
130
+ split = {
131
+ 'activity_id' : File.id_from_path(fit_file.filename),
132
+ 'split' : split_num,
133
+ 'start_time' : fit_file.utc_datetime_to_local(message_fields.start_time),
134
+ 'stop_time' : fit_file.utc_datetime_to_local(message_fields.timestamp),
135
+ 'elapsed_time' : message_fields.get('total_elapsed_time'),
136
+ 'moving_time' : message_fields.get('total_timer_time'),
137
+ 'avg_hr' : message_fields.get('avg_heart_rate'),
138
+ 'max_hr' : message_fields.get('max_heart_rate'),
139
+ 'calories' : message_fields.get('total_calories'),
140
+ 'ascent' : message_fields.get('total_ascent'),
141
+ 'descent' : message_fields.get('total_descent'),
142
+ 'avg_speed' : message_fields.get('avg_vertical_speed'),
143
+ }
144
+ bouldering_font_grade = {
145
+ 0: '1',
146
+ 1: '2',
147
+ 2: '3',
148
+ 3: '4',
149
+ 4: '4+',
150
+ 5: '5',
151
+ 6: '5+',
152
+ 7: '6A',
153
+ 8: '6A+',
154
+ 9: '6B',
155
+ 10: '6B+',
156
+ 11: '6C',
157
+ 12: '6C+',
158
+ 13: '7A',
159
+ 14: '7A+',
160
+ 15: '7B',
161
+ 16: '7B+',
162
+ 17: '7C',
163
+ 18: '7C+',
164
+ 19: '8A',
165
+ 20: '8A+',
166
+ 21: '8B',
167
+ 22: '8B+',
168
+ 23: '8C',
169
+ 24: '8C+',
170
+ 25: '9A',
171
+ }
172
+ indoor_font_grade = {
173
+ 0: '1',
174
+ 1: '2',
175
+ 2: '3',
176
+ 3: '4a',
177
+ 4: '4b',
178
+ 5: '4c',
179
+ 6: '5a',
180
+ 7: '5b',
181
+ 8: '5c',
182
+ 9: '6a',
183
+ 10: '6a+',
184
+ 11: '6b',
185
+ 12: '6b+',
186
+ 13: '6c',
187
+ 14: '6c+',
188
+ 15: '7a',
189
+ 16: '7a+',
190
+ 17: '7b',
191
+ 18: '7b+',
192
+ 19: '7c',
193
+ 20: '7c+',
194
+ 21: '8a',
195
+ 22: '8a+',
196
+ 23: '8b',
197
+ 24: '8b+',
198
+ 25: '8c',
199
+ 26: '8c+',
200
+ 27: '9a',
201
+ 28: '9a+',
202
+ 29: '9b',
203
+ }
204
+
205
+ route = {
206
+ 'activity_id' : activity_id,
207
+ 'grade' : message_fields.get('grade'),
208
+ 'completed' : message_fields.get('completed'),
209
+ 'falls' : message_fields.get('falls'),
210
+ }
211
+
212
+ if 'grade' in route and route.get('grade') is not None:
213
+ if fitfile.SubSport.bouldering == fit_file.sub_sport_type:
214
+ route['grade'] = bouldering_font_grade[int(route['grade'])]
215
+ elif fitfile.SubSport.indoor_climbing == fit_file.sub_sport_type:
216
+ route['grade'] = indoor_font_grade[int(route['grade'])]
217
+
218
+ split.update(plugin_split)
219
+ split.update(route)
220
+
221
+ # there are some empty splits we want to filter out
222
+ if route.get('grade') is not None and route.get('completed') is not None:
223
+ root_logger.debug("writing split %r for %s", split, fit_file.filename)
224
+ self.garmin_act_db_session.add(ActivitySplits(**split))
225
+
226
+ def _write_steps_entry(self, fit_file, activity_id, sub_sport, message_fields):
227
+ steps = {
228
+ 'activity_id' : activity_id,
229
+ 'steps' : message_fields.get('total_steps'),
230
+ 'avg_pace' : fitfile.conversions.perhour_speed_to_pace(message_fields.avg_speed),
231
+ 'max_pace' : fitfile.conversions.perhour_speed_to_pace(message_fields.max_speed),
232
+ 'avg_steps_per_min' : message_fields.get('avg_steps_per_min'),
233
+ 'max_steps_per_min' : message_fields.get('max_steps_per_min'),
234
+ 'avg_step_length' : message_fields.get('avg_step_length'),
235
+ 'avg_vertical_ratio' : message_fields.get('avg_vertical_ratio'),
236
+ 'avg_vertical_oscillation' : message_fields.get('avg_vertical_oscillation'),
237
+ 'avg_gct_balance' : message_fields.get('avg_stance_time_balance'),
238
+ 'avg_ground_contact_time' : message_fields.get('avg_stance_time'),
239
+ 'avg_stance_time_percent' : message_fields.get('avg_stance_time_percent'),
240
+ }
241
+ steps.update(self._plugin_dispatch('write_steps_entry', self.garmin_act_db_session, fit_file, activity_id, sub_sport, message_fields))
242
+ root_logger.debug("_write_steps_entry: %r", steps)
243
+ StepsActivities.s_insert_or_update(self.garmin_act_db_session, steps, ignore_none=True, ignore_zero=True)
244
+
245
+ def _write_running_entry(self, fit_file, activity_id, sub_sport, message_fields):
246
+ return self._write_steps_entry(fit_file, activity_id, sub_sport, message_fields)
247
+
248
+ def _write_walking_entry(self, fit_file, activity_id, sub_sport, message_fields):
249
+ return self._write_steps_entry(fit_file, activity_id, sub_sport, message_fields)
250
+
251
+ def _write_hiking_entry(self, fit_file, activity_id, sub_sport, message_fields):
252
+ return self._write_steps_entry(fit_file, activity_id, sub_sport, message_fields)
253
+
254
+ def _write_cycling_entry(self, fit_file, activity_id, sub_sport, message_fields):
255
+ ride = {
256
+ 'activity_id' : activity_id,
257
+ 'strokes' : message_fields.get('total_strokes'),
258
+ }
259
+ ride.update(self._plugin_dispatch('write_cycle_entry', self.garmin_act_db_session, fit_file, activity_id, sub_sport, message_fields))
260
+ CycleActivities.s_insert_or_update(self.garmin_act_db_session, ride, ignore_none=True, ignore_zero=True)
261
+
262
+ def _write_rock_climbing_entry(self, fit_file, activity_id, sub_sport, message_fields):
263
+ entry = {
264
+ 'activity_id' : activity_id,
265
+ 'total_routes': len([s for s in fit_file.split if s.fields.get('grade')]),
266
+ }
267
+ entry.update(self._plugin_dispatch('write_rock_climbing_entry', self.garmin_act_db_session, fit_file, activity_id, sub_sport, message_fields))
268
+ ClimbingActivities.s_insert_or_update(self.garmin_act_db_session, entry, ignore_none=True, ignore_zero=True)
269
+
270
+ def _write_stand_up_paddleboarding_entry(self, fit_file, activity_id, sub_sport, message_fields):
271
+ root_logger.debug("sup sport entry: %r", message_fields)
272
+ paddle = {
273
+ 'activity_id' : activity_id,
274
+ 'strokes' : message_fields.get('total_strokes'),
275
+ 'avg_stroke_distance' : message_fields.get('avg_stroke_distance'),
276
+ }
277
+ paddle.update(self._plugin_dispatch('write_paddle_entry', self.garmin_act_db_session, fit_file, activity_id, sub_sport, message_fields))
278
+ PaddleActivities.s_insert_or_update(self.garmin_act_db_session, paddle, ignore_none=True, ignore_zero=True)
279
+
280
+ def _write_rowing_entry(self, fit_file, activity_id, sub_sport, message_fields):
281
+ root_logger.debug("row sport entry: %r", message_fields)
282
+ return self._write_stand_up_paddleboarding_entry(fit_file, activity_id, sub_sport, message_fields)
283
+
284
+ def _write_boating_entry(self, fit_file, activity_id, sub_sport, message_fields):
285
+ root_logger.debug("boating sport entry: %r", message_fields)
286
+
287
+ def _write_fitness_equipment_entry(self, fit_file, activity_id, sub_sport, message_fields):
288
+ try:
289
+ function = getattr(self, '_write_' + sub_sport.name + '_entry')
290
+ function(fit_file, activity_id, sub_sport, message_fields)
291
+ except AttributeError:
292
+ root_logger.info("No sub sport handler type %s from %s: %s", sub_sport, fit_file.filename, message_fields)
293
+
294
+ def _write_alpine_skiing_entry(self, fit_file, activity_id, sub_sport, message_fields):
295
+ root_logger.debug("Skiing sport entry: %r", message_fields)
296
+
297
+ def _write_swimming_entry(self, fit_file, activity_id, sub_sport, message_fields):
298
+ root_logger.debug("Swimming sport entry: %r", message_fields)
299
+
300
+ def _write_training_entry(self, fit_file, activity_id, sub_sport, message_fields):
301
+ root_logger.debug("Training sport entry: %r", message_fields)
302
+
303
+ def _write_transition_entry(self, fit_file, activity_id, sub_sport, message_fields):
304
+ root_logger.debug("Transition sport entry: %r", message_fields)
305
+
306
+ def _write_generic_entry(self, fit_file, activity_id, sub_sport, message_fields):
307
+ root_logger.debug("Generic sport entry: %r", message_fields)
308
+
309
+ def __choose_sport(self, current_sport, current_sub_sport, new_sport, new_sub_sport):
310
+ sport = fitfile.Sport.strict_from_string(current_sport)
311
+ sub_sport = fitfile.SubSport.strict_from_string(current_sub_sport)
312
+ if new_sport is not None and (sport is None or (not sport.preferred() and new_sport.preferred())):
313
+ sport = new_sport
314
+ if new_sub_sport is not None and (sub_sport is None or (not sub_sport.preferred() and new_sub_sport.preferred())):
315
+ sub_sport = new_sub_sport
316
+ return {'sport' : fitfile.field_enums.name_for_enum(sport), 'sub_sport' : fitfile.field_enums.name_for_enum(sub_sport)}
317
+
318
+ def _write_session_entry(self, fit_file, message_fields):
319
+ activity_id = File.id_from_path(fit_file.filename)
320
+ sport = message_fields.sport
321
+ sub_sport = message_fields.sub_sport
322
+ activity = {
323
+ 'activity_id' : activity_id,
324
+ 'start_time' : fit_file.utc_datetime_to_local(message_fields.start_time),
325
+ 'stop_time' : fit_file.utc_datetime_to_local(message_fields.timestamp),
326
+ 'elapsed_time' : message_fields.total_elapsed_time,
327
+ 'moving_time' : message_fields.get('total_timer_time'),
328
+ 'start_lat' : message_fields.get('start_position_lat'),
329
+ 'start_long' : message_fields.get('start_position_long'),
330
+ 'stop_lat' : message_fields.get('end_position_lat'),
331
+ 'stop_long' : message_fields.get('end_position_long'),
332
+ 'distance' : message_fields.get('total_distance'),
333
+ 'cycles' : message_fields.get('total_cycles'),
334
+ 'laps' : message_fields.get('num_laps'),
335
+ 'avg_hr' : message_fields.get('avg_heart_rate'),
336
+ 'max_hr' : message_fields.get('max_heart_rate'),
337
+ 'avg_rr' : message_fields.get('avg_respiration_rate'),
338
+ 'max_rr' : message_fields.get('max_respiration_rate'),
339
+ 'calories' : message_fields.get('total_calories'),
340
+ 'avg_cadence' : message_fields.get('avg_cadence'),
341
+ 'max_cadence' : message_fields.get('max_cadence'),
342
+ 'avg_speed' : message_fields.get('avg_speed'),
343
+ 'max_speed' : message_fields.get('max_speed'),
344
+ 'ascent' : message_fields.get('total_ascent'),
345
+ 'descent' : message_fields.get('total_descent'),
346
+ 'max_temperature' : message_fields.get('max_temperature'),
347
+ 'avg_temperature' : message_fields.get('avg_temperature'),
348
+ 'training_effect' : message_fields.get('total_training_effect'),
349
+ 'anaerobic_training_effect' : message_fields.get('total_anaerobic_training_effect')
350
+ }
351
+ activity.update(self._plugin_dispatch('write_session_entry', self.garmin_act_db_session, fit_file, activity_id, message_fields))
352
+ # json metadata gives better values for sport and subsport, so use existing value if set
353
+ current = Activities.s_get(self.garmin_act_db_session, activity_id)
354
+ if current:
355
+ activity.update(self.__choose_sport(current.sport, current.sub_sport, sport, sub_sport))
356
+ root_logger.debug("Updating with %r", activity)
357
+ current.update_from_dict(activity, ignore_none=True, ignore_zero=True)
358
+ else:
359
+ activity.update({'sport': sport.name, 'sub_sport': sub_sport.name})
360
+ root_logger.debug("Adding %r", activity)
361
+ self.garmin_act_db_session.add(Activities(**activity))
362
+ if sport is not None:
363
+ function_name = '_write_' + sport.name + '_entry'
364
+ try:
365
+ function = getattr(self, function_name, None)
366
+ if function is not None:
367
+ function(fit_file, activity_id, sub_sport, message_fields)
368
+ else:
369
+ root_logger.warning("No sport handler for type %s from %s: %s", sport, fit_file.filename, message_fields)
370
+ except Exception as e:
371
+ root_logger.error("Exception in %s from %s: %s", function_name, fit_file.filename, e)
372
+
373
+ def _write_hr_zones_timer_entry(self, fit_file, message_fields):
374
+ """Write hz zones message to the database."""
375
+ root_logger.info("writing hr zone data for %s", fit_file.filename)
376
+ hr_zones_type = message_fields.get('hr_zones_timer_type')
377
+ if hr_zones_type is fitfile.field_enums.HeartRateZonesTimerType.lap:
378
+ self._write_hr_zones_timer_lap_entry(fit_file, message_fields)
379
+ elif hr_zones_type is fitfile.field_enums.HeartRateZonesTimerType.session:
380
+ self._write_hr_zones_timer_session_entry(fit_file, message_fields)
381
+
382
+ def __hr_zone_data(self, message_fields):
383
+ hr_zones_time = message_fields.get('hr_zones_time')
384
+ zone_data = {
385
+ 'hr_zones_method' : message_fields.get('hr_zones_method'),
386
+ 'hrz_1_time' : hr_zones_time[1],
387
+ 'hrz_2_time' : hr_zones_time[2],
388
+ 'hrz_3_time' : hr_zones_time[3],
389
+ 'hrz_4_time' : hr_zones_time[4],
390
+ 'hrz_5_time' : hr_zones_time[5]
391
+ }
392
+ hr_zones = message_fields.get('hr_zones')
393
+ if hr_zones:
394
+ zone_data.update({
395
+ 'hrz_1_hr' : hr_zones[0],
396
+ 'hrz_2_hr' : hr_zones[1],
397
+ 'hrz_3_hr' : hr_zones[2],
398
+ 'hrz_4_hr' : hr_zones[3],
399
+ 'hrz_5_hr' : hr_zones[4],
400
+ })
401
+ return zone_data
402
+
403
+ def _write_hr_zones_timer_lap_entry(self, fit_file, message_fields):
404
+ """Write lap hz zones message to the database."""
405
+ root_logger.info("writing lap hr zone data %r for %s", message_fields, fit_file.filename)
406
+ activity_id = File.id_from_path(fit_file.filename)
407
+ lap = {
408
+ 'activity_id' : activity_id,
409
+ 'lap' : message_fields.get('record_num'),
410
+ }
411
+ lap.update(self.__hr_zone_data(message_fields))
412
+ root_logger.info("writing lap hr zone data %r for %s", lap, fit_file.filename)
413
+ ActivityLaps.s_insert_or_update(self.garmin_act_db_session, lap, ignore_none=True, ignore_zero=True)
414
+
415
+ def _write_hr_zones_timer_session_entry(self, fit_file, message_fields):
416
+ """Write session hz zones message to the database."""
417
+ root_logger.info("writing session hr zone data %r for %s", message_fields, fit_file.filename)
418
+ activity_id = File.id_from_path(fit_file.filename)
419
+ session = {
420
+ 'activity_id' : activity_id,
421
+ }
422
+ session.update(self.__hr_zone_data(message_fields))
423
+ root_logger.debug("writing session hr zone data %r for %s", session, fit_file.filename)
424
+ Activities.s_insert_or_update(self.garmin_act_db_session, session, ignore_none=True, ignore_zero=True)
@@ -0,0 +1,45 @@
1
+ """Base class for GarminDb plugins."""
2
+
3
+ __author__ = "Tom Goetz"
4
+ __copyright__ = "Copyright Tom Goetz"
5
+ __license__ = "GPL"
6
+
7
+ import logging
8
+
9
+ from .plugin_base import PluginBase
10
+
11
+
12
+ logger = logging.getLogger(__file__)
13
+
14
+
15
+ class ActivityFitPluginBase(PluginBase):
16
+ """Base class for GarminDb activity FIT file plugins that handle data based on ids of applications or developer fields, sport or sub-sport ids, etc."""
17
+
18
+ _type = 'ActivityFit'
19
+
20
+ @classmethod
21
+ def matches_activity_file(cls, fit_file):
22
+ """Return if the file matches this plugin."""
23
+ if hasattr(cls, '_application_id'):
24
+ return cls._application_id in fit_file.dev_application_ids
25
+ if hasattr(cls, '_sport') and (fit_file.sport_type is None or fit_file.sport_type.value is not cls._sport):
26
+ return False
27
+ if hasattr(cls, '_sub_sport') and (fit_file.sub_sport_type is None or fit_file.sub_sport_type.value is not cls._sub_sport):
28
+ return False
29
+ if hasattr(cls, '_dev_fields'):
30
+ for dev_field in cls._dev_fields:
31
+ if dev_field not in fit_file.dev_fields:
32
+ return False
33
+ return True
34
+
35
+ @classmethod
36
+ def init_activity(cls, act_db_class, activities_table):
37
+ """Initialize an instance of the plugin as an activity FIT file plugin."""
38
+ logger.info("Initializing tables for activity plugin %s with activities table %s", cls.__name__, activities_table)
39
+ if hasattr(cls, '_records_tablename') and 'record' not in cls._tables:
40
+ cls._tables['record'] = activities_table.create(cls._records_tablename, act_db_class, cls._records_version, cls._records_pk, cls._records_cols)
41
+ if hasattr(cls, '_laps_tablename') and 'lap' not in cls._tables:
42
+ cls._tables['lap'] = activities_table.create(cls._laps_tablename, act_db_class, cls._laps_version, cls._laps_pk, cls._laps_cols)
43
+ if hasattr(cls, '_sessions_tablename') and 'session' not in cls._tables:
44
+ cls._tables['session'] = activities_table.create(cls._sessions_tablename, act_db_class, cls._sessions_version, cols=cls._sessions_cols,
45
+ create_view=cls._views['activity_view'], vars={'activities_table': activities_table})