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.
- garmindb/GarminConnectConfig.json.example +51 -0
- garmindb/__init__.py +39 -0
- garmindb/activities_fit_data.py +29 -0
- garmindb/activity_fit_file_processor.py +424 -0
- garmindb/activity_fit_plugin_base.py +45 -0
- garmindb/analyze.py +201 -0
- garmindb/checkup.py +118 -0
- garmindb/copy.py +64 -0
- garmindb/download.py +321 -0
- garmindb/export_activities.py +52 -0
- garmindb/fit_data.py +59 -0
- garmindb/fit_file_processor.py +250 -0
- garmindb/fitbitdb/__init__.py +11 -0
- garmindb/fitbitdb/analyze.py +84 -0
- garmindb/fitbitdb/fitbit_db.py +157 -0
- garmindb/fitbitdb/import_csv.py +73 -0
- garmindb/garmin_connect_config_manager.py +268 -0
- garmindb/garmin_connect_enums.py +346 -0
- garmindb/garmin_json_data.py +326 -0
- garmindb/garmin_tcx_data.py +148 -0
- garmindb/garmindb/__init__.py +14 -0
- garmindb/garmindb/activities_db.py +646 -0
- garmindb/garmindb/garmin_db.py +476 -0
- garmindb/garmindb/garmin_summary_db.py +121 -0
- garmindb/garmindb/monitoring_db.py +292 -0
- garmindb/import_monitoring.py +483 -0
- garmindb/monitoring_fit_file_processor.py +111 -0
- garmindb/monitoring_fit_plugin_base.py +26 -0
- garmindb/mshealthdb/__init__.py +11 -0
- garmindb/mshealthdb/analyze.py +90 -0
- garmindb/mshealthdb/import_csv.py +129 -0
- garmindb/mshealthdb/mshealth_db.py +195 -0
- garmindb/open_with_basecamp.py +17 -0
- garmindb/open_with_google_earth.py +17 -0
- garmindb/plugin_base.py +29 -0
- garmindb/plugin_manager.py +34 -0
- garmindb/sleep_fit_file_processor.py +45 -0
- garmindb/statistics.py +28 -0
- garmindb/summarydb/__init__.py +10 -0
- garmindb/summarydb/summary_base.py +223 -0
- garmindb/summarydb/summary_db.py +86 -0
- garmindb/tcx.py +128 -0
- garmindb/version.py +32 -0
- garmindb/version_info.py +17 -0
- garmindb-3.6.5.data/scripts/fitbit.py +69 -0
- garmindb-3.6.5.data/scripts/garmindb_bug_report.py +61 -0
- garmindb-3.6.5.data/scripts/garmindb_checkup.py +47 -0
- garmindb-3.6.5.data/scripts/garmindb_cli.py +374 -0
- garmindb-3.6.5.data/scripts/mshealth.py +71 -0
- garmindb-3.6.5.dist-info/METADATA +113 -0
- garmindb-3.6.5.dist-info/RECORD +54 -0
- garmindb-3.6.5.dist-info/WHEEL +5 -0
- garmindb-3.6.5.dist-info/licenses/LICENSE +339 -0
- 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})
|