Skip to content

Commit 1dd0f34

Browse files
committed
Add --hrv parameter to fetch HRV data
1 parent 0ff96f3 commit 1dd0f34

5 files changed

Lines changed: 100 additions & 0 deletions

File tree

fitbit_cli/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ def parse_arguments():
137137
metavar="DATE[,DATE]|RELATIVE",
138138
help="Show Breathing Rate Summary by Interval.",
139139
)
140+
group.add_argument(
141+
"--hrv",
142+
type=parse_date_range,
143+
nargs="?",
144+
const=(datetime.today().date(), None),
145+
metavar="DATE[,DATE]|RELATIVE",
146+
help="Show HRV Summary by Interval.",
147+
)
140148
group.add_argument(
141149
"-t",
142150
"--activities",

fitbit_cli/fitbit_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ def get_breathing_rate_intraday(self, start_date, end_date=None):
150150
response = self.make_request("GET", url)
151151
return response.json()
152152

153+
def get_hrv_summary(self, start_date, end_date=None):
154+
"""Get HRV Summary by Interval and Date"""
155+
156+
date_range = f"{start_date}/{end_date}" if end_date else start_date
157+
url = f"https://api.fitbit.com/1/user/-/hrv/date/{date_range}.json"
158+
response = self.make_request("GET", url)
159+
return response.json()
160+
161+
def get_hrv_intraday(self, start_date, end_date=None):
162+
"""Get HRV Intraday by Interval and Date"""
163+
164+
date_range = f"{start_date}/{end_date}" if end_date else start_date
165+
url = f"https://api.fitbit.com/1/user/-/hrv/date/{date_range}/all.json"
166+
response = self.make_request("GET", url)
167+
return response.json()
168+
153169
def get_daily_activity_summary(self, start_date):
154170
"""Get Daily Activity Summary"""
155171

fitbit_cli/formatter.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,38 @@ def display_breathing_rate(breathing_rate_data, as_json=False):
279279
return None
280280

281281

282+
def display_hrv(hrv_data, as_json=False):
283+
"""HRV data formatter"""
284+
285+
if as_json:
286+
return {
287+
"hrv": [
288+
{
289+
"date": h.get("dateTime"),
290+
"daily_rmssd": h.get("value", {}).get("dailyRmssd"),
291+
"deep_rmssd": h.get("value", {}).get("deepRmssd"),
292+
}
293+
for h in hrv_data.get("hrv", [])
294+
]
295+
}
296+
297+
table = Table(title="HRV Data Summary :heartpulse:", show_header=True)
298+
299+
table.add_column("Date :calendar:")
300+
table.add_column("Daily RMSSD :chart_with_upwards_trend:")
301+
table.add_column("Deep RMSSD :sleeping:")
302+
303+
for hrv in hrv_data.get("hrv", []):
304+
table.add_row(
305+
hrv.get("dateTime", "N/A"),
306+
str(hrv.get("value", {}).get("dailyRmssd", "N/A")),
307+
str(hrv.get("value", {}).get("deepRmssd", "N/A")),
308+
)
309+
310+
CONSOLE.print(table)
311+
return None
312+
313+
282314
def display_devices(devices, as_json=False):
283315
"""Devices list formatter"""
284316

fitbit_cli/output.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def json_display(fitbit, args):
7272
fitbit.get_breathing_rate_summary(*args.breathing_rate), as_json=True
7373
)
7474
)
75+
if args.hrv:
76+
result.update(fmt.display_hrv(fitbit.get_hrv_summary(*args.hrv), as_json=True))
7577
if args.activities:
7678
activity_data = collect_activities(fitbit, args)
7779
if profile is None:
@@ -102,6 +104,8 @@ def raw_json_display(fitbit, args):
102104
result["breathing_rate"] = fitbit.get_breathing_rate_summary(
103105
*args.breathing_rate
104106
)
107+
if args.hrv:
108+
result["hrv"] = fitbit.get_hrv_summary(*args.hrv)
105109
if args.activities:
106110
result["activities"] = collect_activities(fitbit, args)
107111

@@ -129,6 +133,8 @@ def table_display(fitbit, args):
129133
fmt.display_breathing_rate(
130134
fitbit.get_breathing_rate_summary(*args.breathing_rate)
131135
)
136+
if args.hrv:
137+
fmt.display_hrv(fitbit.get_hrv_summary(*args.hrv))
132138
if args.activities:
133139
activity_data = collect_activities(fitbit, args)
134140
if profile is None:

tests/cli_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,44 @@ def test_init_auth_alone_parses_successfully(self):
185185
args = parse_arguments()
186186
self.assertTrue(args.init_auth)
187187

188+
@patch("sys.argv", ["fitbit-cli", "--hrv"])
189+
def test_hrv_flag_parses_successfully(self):
190+
"""Test that --hrv flag parses without error."""
191+
args = parse_arguments()
192+
self.assertIsNotNone(args.hrv)
193+
194+
@patch("sys.argv", ["fitbit-cli", "--json", "--hrv"])
195+
def test_hrv_with_json_flag_parses_successfully(self):
196+
"""Test that --hrv combined with --json parses without error."""
197+
args = parse_arguments()
198+
self.assertTrue(args.json)
199+
self.assertIsNotNone(args.hrv)
200+
201+
@patch("sys.argv", ["fitbit-cli", "--raw-json", "--hrv"])
202+
def test_hrv_with_raw_json_flag_parses_successfully(self):
203+
"""Test that --hrv combined with --raw-json parses without error."""
204+
args = parse_arguments()
205+
self.assertTrue(args.raw_json)
206+
self.assertIsNotNone(args.hrv)
207+
208+
@patch("sys.argv", ["fitbit-cli", "--hrv", "2024-01-01"])
209+
def test_hrv_with_single_date_parses_successfully(self):
210+
"""Test that --hrv with a single date parses without error."""
211+
args = parse_arguments()
212+
self.assertIsNotNone(args.hrv)
213+
214+
@patch("sys.argv", ["fitbit-cli", "--hrv", "2024-01-01,2024-01-07"])
215+
def test_hrv_with_date_range_parses_successfully(self):
216+
"""Test that --hrv with a date range parses without error."""
217+
args = parse_arguments()
218+
self.assertIsNotNone(args.hrv)
219+
220+
@patch("sys.argv", ["fitbit-cli", "--hrv", "last-week"])
221+
def test_hrv_with_relative_date_parses_successfully(self):
222+
"""Test that --hrv with a relative date parses without error."""
223+
args = parse_arguments()
224+
self.assertIsNotNone(args.hrv)
225+
188226

189227
if __name__ == "__main__":
190228
unittest.main()

0 commit comments

Comments
 (0)