diff --git a/demo.py b/demo.py index ca947072..7d7cb39d 100755 --- a/demo.py +++ b/demo.py @@ -4294,25 +4294,27 @@ 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" - # 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 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 - 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, 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" # ---------------------------------------------------------------------------