From 06a3c97c5d893ea683cb084c0c844e76c4fe59f8 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 12 Sep 2026 12:40:55 +0000 Subject: [PATCH 1/3] fix: make pulse optional in set_blood_pressure Garmin Connect's own web UI accepts a blood pressure entry without a heart rate value, but set_blood_pressure's pulse param was a required int with no way to omit it (#426). pulse is now optional and left out of the outgoing payload entirely, and out of validation, when not given. --- garminconnect/__init__.py | 18 ++++++++++++------ tests/test_garmin_unit.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/garminconnect/__init__.py b/garminconnect/__init__.py index 1d4c13bb..714d28be 100644 --- a/garminconnect/__init__.py +++ b/garminconnect/__init__.py @@ -1415,11 +1415,15 @@ def set_blood_pressure( self, systolic: int, diastolic: int, - pulse: int, + pulse: int | None = None, timestamp: str = "", notes: str = "", ) -> dict[str, Any]: - """Add blood pressure measurement.""" + """Add blood pressure measurement. + + pulse is optional - Garmin Connect's own UI accepts a blood + pressure entry without a heart rate value (#426). + """ url = f"{self.garmin_connect_set_blood_pressure_endpoint}" dt = datetime.fromisoformat(timestamp) if timestamp else datetime.now() # Apply timezone offset to get UTC/GMT time @@ -1429,15 +1433,17 @@ def set_blood_pressure( "measurementTimestampGMT": _fmt_ts(dtGMT), "systolic": systolic, "diastolic": diastolic, - "pulse": pulse, "sourceType": "MANUAL", "notes": notes, } - for name, val, lo, hi in ( + checks = [ ("systolic", systolic, 70, 260), ("diastolic", diastolic, 40, 150), - ("pulse", pulse, 20, 250), - ): + ] + if pulse is not None: + checks.append(("pulse", pulse, 20, 250)) + payload["pulse"] = pulse + for name, val, lo, hi in checks: if not isinstance(val, int) or not (lo <= val <= hi): raise ValueError(f"{name} must be an int in [{lo}, {hi}]") logger.debug("Adding blood pressure") diff --git a/tests/test_garmin_unit.py b/tests/test_garmin_unit.py index b2cea382..54ebd244 100644 --- a/tests/test_garmin_unit.py +++ b/tests/test_garmin_unit.py @@ -2550,6 +2550,36 @@ def test_returns_empty_dict_when_nothing_scheduled( assert garmin.get_next_scheduled_workout() == {} +# --------------------------------------------------------------------------- +# set_blood_pressure: pulse is optional, matching Garmin Connect's own UI (#426) +# --------------------------------------------------------------------------- + + +class TestSetBloodPressure: + def test_includes_pulse_when_given(self, garmin: garminconnect.Garmin): + with patch.object(garmin.client, "post") as mock_post: + mock_post.return_value.json.return_value = {"success": True} + garmin.set_blood_pressure(120, 80, pulse=65) + + payload = mock_post.call_args.kwargs["json"] + assert payload["systolic"] == 120 + assert payload["diastolic"] == 80 + assert payload["pulse"] == 65 + + def test_omits_pulse_when_not_given(self, garmin: garminconnect.Garmin): + with patch.object(garmin.client, "post") as mock_post: + mock_post.return_value.json.return_value = {"success": True} + result = garmin.set_blood_pressure(120, 80) + + assert result == {"success": True} + payload = mock_post.call_args.kwargs["json"] + assert "pulse" not in payload + + def test_rejects_out_of_range_pulse(self, garmin: garminconnect.Garmin): + with pytest.raises(ValueError, match="pulse"): + garmin.set_blood_pressure(120, 80, pulse=300) + + # --------------------------------------------------------------------------- # create_gear: matches the payload captured from Garmin Connect's "Add Gear" # --------------------------------------------------------------------------- From cb38dc4738064224c06ddcb661daa9c0e9bf2941 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 12 Sep 2026 12:47:45 +0000 Subject: [PATCH 2/3] demo: let set_blood_pressure_data omit pulse Exercises the new optional-pulse path end to end instead of always defaulting to a fake 60 bpm reading. --- demo.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/demo.py b/demo.py index ca947072..bcbec82f 100755 --- a/demo.py +++ b/demo.py @@ -4294,9 +4294,9 @@ def set_blood_pressure_data(api: Garmin) -> None: diastolic_input = input("Diastolic pressure [80]: ").strip() diastolic = int(diastolic_input) if diastolic_input else 80 - # Get pulse - pulse_input = input("Pulse rate [60]: ").strip() - pulse = int(pulse_input) if pulse_input else 60 + # Get pulse (optional - Garmin Connect's own UI allows omitting it) + pulse_input = input("Pulse rate (optional, press Enter to omit): ").strip() + pulse = int(pulse_input) if pulse_input else None # Get notes (optional) notes = input("Notes (optional): ").strip() or "Added via demo.py" @@ -4308,11 +4308,12 @@ def set_blood_pressure_data(api: Garmin) -> None: if not (30 <= diastolic <= 200): print("❌ Invalid diastolic pressure (should be between 30-200)") return - if not (30 <= pulse <= 250): + if pulse is not None and not (30 <= pulse <= 250): print("❌ Invalid pulse rate (should be between 30-250)") return - print(f"📊 Recording: {systolic}/{diastolic} mmHg, pulse {pulse} bpm") + pulse_desc = f"pulse {pulse} bpm" if pulse is not None else "no pulse" + print(f"📊 Recording: {systolic}/{diastolic} mmHg, {pulse_desc}") call_and_display( api.set_blood_pressure, From 16c2bc256548889f80d2074915b3dd45bcf3a1c3 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Sat, 12 Sep 2026 15:03:07 +0200 Subject: [PATCH 3/3] fix: align demo.py's blood-pressure range checks with the library demo.py's local pre-validation used different systolic/diastolic/pulse ranges than Garmin.set_blood_pressure's own checks (e.g. pulse 30-250 vs the library's 20-250), so a value the demo accepted could still be rejected by the API call, and a value the API would accept could be wrongly rejected by the demo first. Match all three ranges to the library's checks (systolic 70-260, diastolic 40-150, pulse 20-250). --- demo.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/demo.py b/demo.py index bcbec82f..7d7cb39d 100755 --- a/demo.py +++ b/demo.py @@ -4301,15 +4301,16 @@ def set_blood_pressure_data(api: Garmin) -> None: # Get notes (optional) notes = input("Notes (optional): ").strip() or "Added via demo.py" - # Validate ranges - if not (50 <= systolic <= 300): - print("❌ Invalid systolic pressure (should be between 50-300)") + # Validate ranges (must match Garmin.set_blood_pressure's own checks, + # so a value the demo accepts never gets rejected by the API call) + if not (70 <= systolic <= 260): + print("❌ Invalid systolic pressure (should be between 70-260)") return - if not (30 <= diastolic <= 200): - print("❌ Invalid diastolic pressure (should be between 30-200)") + if not (40 <= diastolic <= 150): + print("❌ Invalid diastolic pressure (should be between 40-150)") return - if pulse is not None and not (30 <= pulse <= 250): - print("❌ Invalid pulse rate (should be between 30-250)") + if pulse is not None and not (20 <= pulse <= 250): + print("❌ Invalid pulse rate (should be between 20-250)") return pulse_desc = f"pulse {pulse} bpm" if pulse is not None else "no pulse"