Implement Garmin biometric fetching functions (P2.1)
Add specific fetchers for HRV, Body Battery, and Intensity Minutes to enable real biometric data collection from Garmin Connect API. Functions added: - fetchHrvStatus(): Returns "Balanced", "Unbalanced", or "Unknown" - fetchBodyBattery(): Returns current BB and yesterday's low value - fetchIntensityMinutes(): Returns 7-day rolling sum of activity All functions gracefully handle API failures with safe defaults. Test count expanded from 14 to 33 covering all scenarios. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// ABOUTME: Garmin Connect API client using stored OAuth tokens.
|
||||
// ABOUTME: Fetches body battery, HRV, and intensity minutes from Garmin.
|
||||
import type { GarminTokens } from "@/types";
|
||||
import type { GarminTokens, HrvStatus } from "@/types";
|
||||
|
||||
const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy";
|
||||
|
||||
@@ -8,6 +8,11 @@ interface GarminApiOptions {
|
||||
oauth2Token: string;
|
||||
}
|
||||
|
||||
export interface BodyBatteryData {
|
||||
current: number | null;
|
||||
yesterdayLow: number | null;
|
||||
}
|
||||
|
||||
export async function fetchGarminData(
|
||||
endpoint: string,
|
||||
options: GarminApiOptions,
|
||||
@@ -37,3 +42,101 @@ export function daysUntilExpiry(tokens: GarminTokens): number {
|
||||
const diffMs = expiresAt.getTime() - now.getTime();
|
||||
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
export async function fetchHrvStatus(
|
||||
date: string,
|
||||
oauth2Token: string,
|
||||
): Promise<HrvStatus> {
|
||||
try {
|
||||
const response = await fetch(`${GARMIN_BASE_URL}/hrv-service/hrv/${date}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${oauth2Token}`,
|
||||
NK: "NT",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const status = data?.hrvSummary?.status;
|
||||
|
||||
if (status === "BALANCED") {
|
||||
return "Balanced";
|
||||
}
|
||||
if (status === "UNBALANCED") {
|
||||
return "Unbalanced";
|
||||
}
|
||||
return "Unknown";
|
||||
} catch {
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchBodyBattery(
|
||||
date: string,
|
||||
oauth2Token: string,
|
||||
): Promise<BodyBatteryData> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${GARMIN_BASE_URL}/usersummary-service/stats/bodyBattery/dates/${date}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${oauth2Token}`,
|
||||
NK: "NT",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { current: null, yesterdayLow: null };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const currentData = data?.bodyBatteryValuesArray?.[0];
|
||||
const current = currentData?.charged ?? null;
|
||||
|
||||
const yesterdayStats = data?.bodyBatteryStatList?.[0];
|
||||
const yesterdayLow = yesterdayStats?.min ?? null;
|
||||
|
||||
return { current, yesterdayLow };
|
||||
} catch {
|
||||
return { current: null, yesterdayLow: null };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchIntensityMinutes(
|
||||
oauth2Token: string,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${GARMIN_BASE_URL}/fitnessstats-service/activity`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${oauth2Token}`,
|
||||
NK: "NT",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const weeklyTotal = data?.weeklyTotal;
|
||||
|
||||
if (!weeklyTotal) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const moderate = weeklyTotal.moderateIntensityMinutes ?? 0;
|
||||
const vigorous = weeklyTotal.vigorousIntensityMinutes ?? 0;
|
||||
|
||||
return moderate + vigorous;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user