From 1dd0f34474ffb9478798cfa90159c7c26c073551 Mon Sep 17 00:00:00 2001 From: eskimokk Date: Wed, 1 Apr 2026 18:54:01 +0800 Subject: [PATCH 1/2] Add --hrv parameter to fetch HRV data --- fitbit_cli/cli.py | 8 ++++++++ fitbit_cli/fitbit_api.py | 16 ++++++++++++++++ fitbit_cli/formatter.py | 32 ++++++++++++++++++++++++++++++++ fitbit_cli/output.py | 6 ++++++ tests/cli_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+) diff --git a/fitbit_cli/cli.py b/fitbit_cli/cli.py index fd67f2e..a6891fb 100644 --- a/fitbit_cli/cli.py +++ b/fitbit_cli/cli.py @@ -137,6 +137,14 @@ def parse_arguments(): metavar="DATE[,DATE]|RELATIVE", help="Show Breathing Rate Summary by Interval.", ) + group.add_argument( + "--hrv", + type=parse_date_range, + nargs="?", + const=(datetime.today().date(), None), + metavar="DATE[,DATE]|RELATIVE", + help="Show HRV Summary by Interval.", + ) group.add_argument( "-t", "--activities", diff --git a/fitbit_cli/fitbit_api.py b/fitbit_cli/fitbit_api.py index da7bfdd..e9eee4b 100644 --- a/fitbit_cli/fitbit_api.py +++ b/fitbit_cli/fitbit_api.py @@ -150,6 +150,22 @@ def get_breathing_rate_intraday(self, start_date, end_date=None): response = self.make_request("GET", url) return response.json() + def get_hrv_summary(self, start_date, end_date=None): + """Get HRV Summary by Interval and Date""" + + date_range = f"{start_date}/{end_date}" if end_date else start_date + url = f"https://api.fitbit.com/1/user/-/hrv/date/{date_range}.json" + response = self.make_request("GET", url) + return response.json() + + def get_hrv_intraday(self, start_date, end_date=None): + """Get HRV Intraday by Interval and Date""" + + date_range = f"{start_date}/{end_date}" if end_date else start_date + url = f"https://api.fitbit.com/1/user/-/hrv/date/{date_range}/all.json" + response = self.make_request("GET", url) + return response.json() + def get_daily_activity_summary(self, start_date): """Get Daily Activity Summary""" diff --git a/fitbit_cli/formatter.py b/fitbit_cli/formatter.py index 55560f4..3281de1 100644 --- a/fitbit_cli/formatter.py +++ b/fitbit_cli/formatter.py @@ -279,6 +279,38 @@ def display_breathing_rate(breathing_rate_data, as_json=False): return None +def display_hrv(hrv_data, as_json=False): + """HRV data formatter""" + + if as_json: + return { + "hrv": [ + { + "date": h.get("dateTime"), + "daily_rmssd": h.get("value", {}).get("dailyRmssd"), + "deep_rmssd": h.get("value", {}).get("deepRmssd"), + } + for h in hrv_data.get("hrv", []) + ] + } + + table = Table(title="HRV Data Summary :heartpulse:", show_header=True) + + table.add_column("Date :calendar:") + table.add_column("Daily RMSSD :chart_with_upwards_trend:") + table.add_column("Deep RMSSD :sleeping:") + + for hrv in hrv_data.get("hrv", []): + table.add_row( + hrv.get("dateTime", "N/A"), + str(hrv.get("value", {}).get("dailyRmssd", "N/A")), + str(hrv.get("value", {}).get("deepRmssd", "N/A")), + ) + + CONSOLE.print(table) + return None + + def display_devices(devices, as_json=False): """Devices list formatter""" diff --git a/fitbit_cli/output.py b/fitbit_cli/output.py index a94ae4b..135aaaf 100644 --- a/fitbit_cli/output.py +++ b/fitbit_cli/output.py @@ -72,6 +72,8 @@ def json_display(fitbit, args): fitbit.get_breathing_rate_summary(*args.breathing_rate), as_json=True ) ) + if args.hrv: + result.update(fmt.display_hrv(fitbit.get_hrv_summary(*args.hrv), as_json=True)) if args.activities: activity_data = collect_activities(fitbit, args) if profile is None: @@ -102,6 +104,8 @@ def raw_json_display(fitbit, args): result["breathing_rate"] = fitbit.get_breathing_rate_summary( *args.breathing_rate ) + if args.hrv: + result["hrv"] = fitbit.get_hrv_summary(*args.hrv) if args.activities: result["activities"] = collect_activities(fitbit, args) @@ -129,6 +133,8 @@ def table_display(fitbit, args): fmt.display_breathing_rate( fitbit.get_breathing_rate_summary(*args.breathing_rate) ) + if args.hrv: + fmt.display_hrv(fitbit.get_hrv_summary(*args.hrv)) if args.activities: activity_data = collect_activities(fitbit, args) if profile is None: diff --git a/tests/cli_test.py b/tests/cli_test.py index 4325108..c011bda 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -185,6 +185,44 @@ def test_init_auth_alone_parses_successfully(self): args = parse_arguments() self.assertTrue(args.init_auth) + @patch("sys.argv", ["fitbit-cli", "--hrv"]) + def test_hrv_flag_parses_successfully(self): + """Test that --hrv flag parses without error.""" + args = parse_arguments() + self.assertIsNotNone(args.hrv) + + @patch("sys.argv", ["fitbit-cli", "--json", "--hrv"]) + def test_hrv_with_json_flag_parses_successfully(self): + """Test that --hrv combined with --json parses without error.""" + args = parse_arguments() + self.assertTrue(args.json) + self.assertIsNotNone(args.hrv) + + @patch("sys.argv", ["fitbit-cli", "--raw-json", "--hrv"]) + def test_hrv_with_raw_json_flag_parses_successfully(self): + """Test that --hrv combined with --raw-json parses without error.""" + args = parse_arguments() + self.assertTrue(args.raw_json) + self.assertIsNotNone(args.hrv) + + @patch("sys.argv", ["fitbit-cli", "--hrv", "2024-01-01"]) + def test_hrv_with_single_date_parses_successfully(self): + """Test that --hrv with a single date parses without error.""" + args = parse_arguments() + self.assertIsNotNone(args.hrv) + + @patch("sys.argv", ["fitbit-cli", "--hrv", "2024-01-01,2024-01-07"]) + def test_hrv_with_date_range_parses_successfully(self): + """Test that --hrv with a date range parses without error.""" + args = parse_arguments() + self.assertIsNotNone(args.hrv) + + @patch("sys.argv", ["fitbit-cli", "--hrv", "last-week"]) + def test_hrv_with_relative_date_parses_successfully(self): + """Test that --hrv with a relative date parses without error.""" + args = parse_arguments() + self.assertIsNotNone(args.hrv) + if __name__ == "__main__": unittest.main() From 075f50167faf5408977ea7403cc7cb62881884bb Mon Sep 17 00:00:00 2001 From: eskimokk Date: Fri, 3 Apr 2026 10:10:04 +0800 Subject: [PATCH 2/2] add -H/--hrv paramter to fetch HRV summary data (and keep clean) --- README.md | 5 ++++- fitbit_cli/__init__.py | 2 +- fitbit_cli/cli.py | 1 + fitbit_cli/fitbit_api.py | 8 -------- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 18dce63..7053a67 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Access your Fitbit data directly from your terminal 💻. View 💤 sleep logs, | [Get AZM Time Series by Interval](https://dev.fitbit.com/build/reference/web-api/active-zone-minutes-timeseries/get-azm-timeseries-by-interval/) | ✅ | | [Get Breathing Rate Summary by Interval](https://dev.fitbit.com/build/reference/web-api/breathing-rate/get-br-summary-by-interval/) | ✅ | | [Get Daily Activity Summary](https://dev.fitbit.com/build/reference/web-api/activity/get-daily-activity-summary/) | ✅ | +| [Get HRV Summary by Interval](https://dev.fitbit.com/build/reference/web-api/heartrate-variability/get-hrv-summary-by-interval/) | ✅ | ## Usage Guide @@ -45,7 +46,7 @@ python -m pip install fitbit-cli ```bash fitbit-cli -h usage: fitbit-cli [-h] [-i] [-j] [-r] [-s [DATE[,DATE]|RELATIVE]] [-o [DATE[,DATE]|RELATIVE]] [-e [DATE[,DATE]|RELATIVE]] [-a [DATE[,DATE]|RELATIVE]] - [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v] + [-b [DATE[,DATE]|RELATIVE]] [-t [DATE[,DATE]|RELATIVE]] [-H [DATE[,DATE]|RELATIVE]] [-u] [-d] [-v] Fitbit CLI -- Access your Fitbit data at your terminal. @@ -73,6 +74,8 @@ APIs: Show Breathing Rate Summary by Interval. -t, --activities [DATE[,DATE]|RELATIVE] Show Daily Activity Summary. + -H, --hrv [DATE[,DATE]|RELATIVE] + Show HRV Summary by Interval. -u, --user-profile Show Profile. -d, --devices Show Devices. ``` diff --git a/fitbit_cli/__init__.py b/fitbit_cli/__init__.py index 0c530e1..aae3fc8 100644 --- a/fitbit_cli/__init__.py +++ b/fitbit_cli/__init__.py @@ -3,4 +3,4 @@ fitbit_cli Module """ -__version__ = "1.6.0" +__version__ = "1.7.0" diff --git a/fitbit_cli/cli.py b/fitbit_cli/cli.py index a6891fb..ec19d53 100644 --- a/fitbit_cli/cli.py +++ b/fitbit_cli/cli.py @@ -138,6 +138,7 @@ def parse_arguments(): help="Show Breathing Rate Summary by Interval.", ) group.add_argument( + "-H", "--hrv", type=parse_date_range, nargs="?", diff --git a/fitbit_cli/fitbit_api.py b/fitbit_cli/fitbit_api.py index e9eee4b..11a9919 100644 --- a/fitbit_cli/fitbit_api.py +++ b/fitbit_cli/fitbit_api.py @@ -158,14 +158,6 @@ def get_hrv_summary(self, start_date, end_date=None): response = self.make_request("GET", url) return response.json() - def get_hrv_intraday(self, start_date, end_date=None): - """Get HRV Intraday by Interval and Date""" - - date_range = f"{start_date}/{end_date}" if end_date else start_date - url = f"https://api.fitbit.com/1/user/-/hrv/date/{date_range}/all.json" - response = self.make_request("GET", url) - return response.json() - def get_daily_activity_summary(self, start_date): """Get Daily Activity Summary"""