Skip to content

Commit 14d5559

Browse files
authored
Add HRV support. You can use either --all or --hrv to import HRV. (#299)
* Add HRV support. You can use either --all or --hrv to import HRV.
1 parent eb1829d commit 14d5559

11 files changed

Lines changed: 131 additions & 13 deletions

Plugins

garmindb/GarminConnectConfig.json.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"weight_start_date" : "12/31/2019",
1616
"sleep_start_date" : "12/31/2019",
1717
"rhr_start_date" : "12/31/2019",
18+
"hrv_start_date" : "12/31/2019",
1819
"monitoring_start_date" : "12/31/2019",
1920
"download_latest_activities" : 25,
2021
"download_all_activities" : 1000
@@ -31,7 +32,8 @@
3132
"sleep" : true,
3233
"rhr" : true,
3334
"weight" : true,
34-
"activities" : true
35+
"activities" : true,
36+
"hrv" : true
3537
},
3638
"course_views": {
3739
"steps" : []

garmindb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .version import format_version, log_version, python_version_check
3434

3535
from .import_monitoring import GarminMonitoringFitData, GarminSleepFitData, GarminSummaryData, GarminUserSettings, GarminSocialProfile, GarminPersonalInformation, GarminWeightData, \
36-
GarminSleepData, GarminRhrData, GarminSettingsFitData, GarminHydrationData
36+
GarminSleepData, GarminRhrData, GarminSettingsFitData, GarminHydrationData, GarminHrvData
3737
from .activities_fit_data import GarminActivitiesFitData
3838
from .garmin_tcx_data import GarminTcxData
3939
from .garmin_json_data import GarminJsonSummaryData, GarminJsonDetailsData

garmindb/download.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Download():
4242
garmin_connect_usersummary_url = "/usersummary-service/usersummary"
4343
garmin_connect_daily_summary_url = garmin_connect_usersummary_url + "/daily"
4444
garmin_connect_daily_hydration_url = garmin_connect_usersummary_url + "/hydration/allData"
45+
garmin_connect_hrv_url = "/hrv-service/hrv"
4546

4647
# https://connect.garmin.com/modern/proxy/usersummary-service/usersummary/hydration/allData/2019-11-29
4748

@@ -319,3 +320,17 @@ def get_hydration(self, directory_func, date, days, overwrite):
319320
"""Download the hydration data from Garmin Connect and save to a JSON file."""
320321
root_logger.info("Getting hydration: %s (%d)", date, days)
321322
self.__get_stat(self.__get_hydration_day, directory_func, date, days, overwrite)
323+
324+
def __get_hrv_day(self, directory, day, overwrite=False):
325+
date_str = day.strftime('%Y-%m-%d')
326+
json_filename = f'{directory}/hrv_{date_str}'
327+
url = f'{self.garmin_connect_hrv_url}/{date_str}'
328+
try:
329+
self.save_json_to_file(json_filename, self.garth.connectapi(url), overwrite)
330+
except GarthHTTPError as e:
331+
root_logger.error("Exception getting daily summary %s", e)
332+
333+
def get_hrv(self, directory, date, days, overwrite):
334+
"""Download the heart rate variability (HRV) data from Garmin Connect and save to a JSON file."""
335+
root_logger.info("Getting hrv: %s (%d)", date, days)
336+
self.__get_stat(self.__get_hrv_day, directory, date, days, overwrite)

garmindb/garmin_connect_config_manager.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ def all_activity_count(self):
214214
def stat_start_date(self, stat_type):
215215
"""Return a tuple containing the start date and the number of days to fetch stats from."""
216216
start_date = self.get_node_value('data', stat_type + '_start_date')
217+
if start_date is None:
218+
# Fallback to a general start date or a reasonable default
219+
start_date = self.get_node_value('data', 'start_date')
220+
if start_date is None:
221+
# If still nothing, default to 20 days ago
222+
start_date = datetime.datetime.now().date() - datetime.timedelta(days=20)
217223
end_date = self.get_node_value_default('data', stat_type + '_end_date', datetime.datetime.now().date())
218224
days = (end_date - start_date).days
219225
return (start_date, days)

garmindb/garmindb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# flake8: noqa
88

9-
from .garmin_db import GarminDb, Attributes, Device, DeviceInfo, File, Weight, Stress, Sleep, SleepEvents, RestingHeartRate, DailySummary
9+
from .garmin_db import GarminDb, Attributes, Device, DeviceInfo, File, Weight, Stress, Sleep, SleepEvents, RestingHeartRate, DailySummary, Hrv
1010
from .monitoring_db import MonitoringDb, MonitoringInfo, MonitoringHeartRate, MonitoringIntensity, MonitoringClimb, Monitoring, \
1111
MonitoringRespirationRate, MonitoringPulseOx, MonitoringHrvValue, MonitoringHrvStatus
1212
from .activities_db import ActivitiesDb, Activities, ActivityLaps, ActivityRecords, ActivitiesDevices, ActivitySplits, SportActivities, StepsActivities, \

garmindb/garmindb/garmin_db.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,38 @@ def get_stats(cls, session, start_ts, end_ts):
325325
}
326326

327327

328+
class Hrv(GarminDb.Base, idbutils.DbObject):
329+
"""Class representing daily Heart Rate Variability (HRV) data."""
330+
331+
__tablename__ = 'hrv'
332+
333+
db = GarminDb
334+
table_version = 1
335+
_col_units = {
336+
'weekly_avg': 'ms',
337+
'last_night_avg': 'ms',
338+
'last_night_5min_high': 'ms',
339+
'baseline_low': 'ms',
340+
'baseline_upper': 'ms'
341+
}
342+
343+
day = Column(Date, primary_key=True)
344+
weekly_avg = Column(Integer)
345+
last_night_avg = Column(Integer)
346+
last_night_5min_high = Column(Integer)
347+
baseline_low = Column(Integer)
348+
baseline_upper = Column(Integer)
349+
status = Column(String)
350+
351+
@classmethod
352+
def get_stats(cls, session, start_ts, end_ts):
353+
"""Return a dictionary of aggregate statistics for the given time period."""
354+
return {
355+
'hrv_weekly_avg': cls.s_get_col_avg(session, cls.weekly_avg, start_ts, end_ts, ignore_le_zero=True),
356+
'hrv_last_night_avg': cls.s_get_col_avg(session, cls.last_night_avg, start_ts, end_ts, ignore_le_zero=True),
357+
}
358+
359+
328360
class DailySummary(GarminDb.Base, idbutils.DbObject):
329361
"""Class representing a Garmin daily summary."""
330362

garmindb/import_monitoring.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import fitfile
1414
from idbutils import JsonFileProcessor, Conversions
1515

16-
from .garmindb import GarminDb, Attributes, Weight, Sleep, SleepEvents, RestingHeartRate, DailySummary
16+
from .garmindb import GarminDb, Attributes, Weight, Sleep, SleepEvents, RestingHeartRate, DailySummary, Hrv
1717
from .fit_data import FitData
1818

1919

@@ -479,3 +479,44 @@ def _process_json(self, json_data):
479479
DailySummary.insert_or_update(
480480
self.garmin_db, summary, ignore_none=True)
481481
return 1
482+
483+
484+
class GarminHrvData(JsonFileProcessor):
485+
"""Class for importing JSON formatted Garmin Connect heart rate variability (HRV) data into a database."""
486+
487+
def __init__(self, db_params, input_dir, latest, debug):
488+
"""
489+
Return an instance of GarminHrvData.
490+
491+
Parameters:
492+
----------
493+
db_params (object): configuration data for accessing the database
494+
input_dir (string): directory (full path) to check for HRV data files
495+
latest (Boolean): check for latest files only
496+
debug (Boolean): enable debug logging
497+
498+
"""
499+
super().__init__(r'hrv_\d{4}-\d{2}-\d{2}\.json', input_dir=input_dir, latest=latest, debug=debug)
500+
self.garmin_db = GarminDb(db_params)
501+
self.conversions = {'calendarDate': self._parse_date}
502+
503+
def _process_json(self, json_data):
504+
hrv_summary = json_data.get('hrvSummary')
505+
if hrv_summary is None:
506+
return 0
507+
day = hrv_summary.get('calendarDate')
508+
if day is None:
509+
return 0
510+
if isinstance(day, str):
511+
day = self._parse_date(day)
512+
point = {
513+
'day': day.date() if hasattr(day, 'date') else day,
514+
'weekly_avg': self._get_field(hrv_summary, 'weeklyAvg', int),
515+
'last_night_avg': self._get_field(hrv_summary, 'lastNightAvg', int),
516+
'last_night_5min_high': self._get_field(hrv_summary, 'lastNight5MinHigh', int),
517+
'baseline_low': self._get_field(hrv_summary.get('baseline', {}), 'balancedLow', int),
518+
'baseline_upper': self._get_field(hrv_summary.get('baseline', {}), 'balancedUpper', int),
519+
'status': self._get_field(hrv_summary, 'status', str)
520+
}
521+
Hrv.insert_or_update(self.garmin_db, point, ignore_none=True)
522+
return 1

garmindb/statistics.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class Statistics(enum.Enum):
1818
rhr = 5
1919
weight = 6
2020
activities = 7
21+
hrv = 8
2122

2223
@classmethod
2324
def from_string(cls, string):

scripts/garmindb_cli.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import glob
2121

2222
from garmindb import python_version_check, log_version, format_version
23-
from garmindb.garmindb import GarminDb, Attributes, Sleep, Weight, RestingHeartRate, MonitoringDb, MonitoringHeartRate, ActivitiesDb, GarminSummaryDb
23+
from garmindb.garmindb import GarminDb, Attributes, Sleep, Weight, RestingHeartRate, Hrv, MonitoringDb, MonitoringHeartRate, ActivitiesDb, GarminSummaryDb
2424
from garmindb.summarydb import SummaryDb
2525

2626
from garmindb import Download, Copy, Analyze
@@ -50,6 +50,7 @@ class GarminDbMain():
5050
Statistics.sleep : GarminDb,
5151
Statistics.rhr : GarminDb,
5252
Statistics.weight : GarminDb,
53+
Statistics.hrv : GarminDb,
5354
Statistics.activities : ActivitiesDb
5455
}
5556

@@ -159,6 +160,14 @@ def download_data(self, overwrite, latest, stats):
159160
download.get_rhr(rhr_dir, date, days, overwrite)
160161
root_logger.info("Saved rhr files for %s (%d) to %s for processing", date, days, rhr_dir)
161162

163+
if Statistics.hrv in stats:
164+
date, days = self.__get_date_and_days(GarminDb(self.gc_config.get_db_params()), latest, Hrv, Hrv.day, 'hrv')
165+
if days > 0:
166+
hrv_dir = self.gc_config.get_rhr_dir() # HRV tends to be in the same place as RHR or monitoring
167+
root_logger.info("Date range to update: %s (%d) to %s", date, days, hrv_dir)
168+
download.get_hrv(hrv_dir, date, days, overwrite)
169+
root_logger.info("Saved hrv files for %s (%d) to %s for processing", date, days, hrv_dir)
170+
162171

163172
def import_data(self, debug, latest, stats):
164173
"""Import previously downloaded Garmin data into the database."""
@@ -221,6 +230,13 @@ def import_data(self, debug, latest, stats):
221230
if grhrd.file_count() > 0:
222231
grhrd.process()
223232

233+
if Statistics.hrv in stats:
234+
from garmindb import GarminHrvData
235+
hrv_dir = self.gc_config.get_rhr_dir()
236+
ghrvd = GarminHrvData(self.gc_config.get_db_params(), hrv_dir, latest, debug)
237+
if ghrvd.file_count() > 0:
238+
ghrvd.process()
239+
224240
if Statistics.activities in stats:
225241
activities_dir = self.gc_config.get_activities_dir()
226242
# Tcx fields are less precise than the JSON files, so load Tcx first and overwrite with better JSON values.
@@ -313,6 +329,7 @@ def main(argv):
313329
stats_group.add_argument("-a", "--activities", help="Download and/or import activities data.", dest='stats', action='append_const', const=Statistics.activities)
314330
stats_group.add_argument("-m", "--monitoring", help="Download and/or import monitoring data.", dest='stats', action='append_const', const=Statistics.monitoring)
315331
stats_group.add_argument("-r", "--rhr", help="Download and/or import resting heart rate data.", dest='stats', action='append_const', const=Statistics.rhr)
332+
stats_group.add_argument("--hrv", help="Download and/or import heart rate variability data.", dest='stats', action='append_const', const=Statistics.hrv)
316333
stats_group.add_argument("-s", "--sleep", help="Download and/or import sleep data.", dest='stats', action='append_const', const=Statistics.sleep)
317334
stats_group.add_argument("-w", "--weight", help="Download and/or import weight data.", dest='stats', action='append_const', const=Statistics.weight)
318335
modifiers_group = parser.add_argument_group('Modifiers')

0 commit comments

Comments
 (0)