mirror of
https://github.com/hanzoskill/garmin-health-analysis.git
synced 2026-10-30 21:01:12 +00:00
239 lines
7.6 KiB
Python
239 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Query Garmin data by time - "what was my heart rate at 3pm yesterday?"
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
from garmin_auth import get_client
|
|
|
|
|
|
def parse_time(time_str, date_str=None):
|
|
"""Parse various time formats into datetime."""
|
|
if not date_str:
|
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
# Try different formats
|
|
formats = [
|
|
"%Y-%m-%d %H:%M:%S",
|
|
"%Y-%m-%d %H:%M",
|
|
"%H:%M:%S",
|
|
"%H:%M",
|
|
"%I:%M %p", # 3:00 PM
|
|
"%I %p", # 3 PM
|
|
]
|
|
|
|
for fmt in formats:
|
|
try:
|
|
if "%Y" in fmt:
|
|
return datetime.strptime(time_str, fmt)
|
|
else:
|
|
return datetime.strptime(f"{date_str} {time_str}", f"%Y-%m-%d {fmt}")
|
|
except ValueError:
|
|
continue
|
|
|
|
raise ValueError(f"Could not parse time: {time_str}")
|
|
|
|
|
|
def find_closest_datapoint(target_time, data_array, timestamp_key="startTimeInSeconds"):
|
|
"""Find the closest data point to a target time."""
|
|
if not data_array:
|
|
return None
|
|
|
|
target_timestamp = int(target_time.timestamp())
|
|
|
|
# Handle different timestamp formats
|
|
def get_timestamp(item):
|
|
if timestamp_key in item:
|
|
ts = item[timestamp_key]
|
|
# Handle milliseconds
|
|
if ts > 10000000000:
|
|
ts = ts // 1000
|
|
return ts
|
|
elif "startGMT" in item:
|
|
# ISO format or timestamp
|
|
start_gmt = item["startGMT"]
|
|
if isinstance(start_gmt, str):
|
|
return int(datetime.fromisoformat(start_gmt.replace("Z", "+00:00")).timestamp())
|
|
else:
|
|
return start_gmt // 1000 if start_gmt > 10000000000 else start_gmt
|
|
return None
|
|
|
|
closest = None
|
|
min_diff = float('inf')
|
|
|
|
for item in data_array:
|
|
ts = get_timestamp(item)
|
|
if ts is None:
|
|
continue
|
|
|
|
diff = abs(ts - target_timestamp)
|
|
if diff < min_diff:
|
|
min_diff = diff
|
|
closest = item
|
|
|
|
return closest
|
|
|
|
|
|
def query_heart_rate_at_time(client, time_str, date_str=None):
|
|
"""Get heart rate at a specific time."""
|
|
target_time = parse_time(time_str, date_str)
|
|
date = target_time.strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
# Get intraday heart rate data
|
|
data = client.get_heart_rates(date)
|
|
|
|
if not data:
|
|
return {"error": "No heart rate data for this date", "date": date}
|
|
|
|
# Data format varies - try different keys
|
|
hr_array = data.get("heartRateValues") or data.get("allDayHR") or []
|
|
|
|
# heartRateValues format: [[timestamp_ms, hr_value], ...]
|
|
# Convert to dict format for our function
|
|
if hr_array and isinstance(hr_array[0], list):
|
|
hr_array = [{"startTimeInSeconds": ts//1000, "heartRateValue": val} for ts, val in hr_array]
|
|
|
|
closest = find_closest_datapoint(target_time, hr_array)
|
|
|
|
if closest:
|
|
return {
|
|
"requested_time": time_str,
|
|
"actual_time": datetime.fromtimestamp(
|
|
closest.get("startTimeInSeconds", 0)
|
|
).strftime("%Y-%m-%d %H:%M:%S"),
|
|
"heart_rate": closest.get("heartRateValue") or closest.get("value"),
|
|
"date": date
|
|
}
|
|
else:
|
|
return {"error": "No data point found near requested time", "date": date}
|
|
|
|
except Exception as e:
|
|
return {"error": str(e), "date": date}
|
|
|
|
|
|
def query_stress_at_time(client, time_str, date_str=None):
|
|
"""Get stress level at a specific time."""
|
|
target_time = parse_time(time_str, date_str)
|
|
date = target_time.strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
data = client.get_all_day_stress(date)
|
|
|
|
if not data:
|
|
return {"error": "No stress data for this date", "date": date}
|
|
|
|
stress_values = data.get("stressValuesArray") or []
|
|
|
|
closest = find_closest_datapoint(target_time, stress_values)
|
|
|
|
if closest:
|
|
return {
|
|
"requested_time": time_str,
|
|
"stress_level": closest.get("stressLevel") or closest.get("value"),
|
|
"date": date
|
|
}
|
|
else:
|
|
return {"error": "No data point found near requested time", "date": date}
|
|
|
|
except Exception as e:
|
|
return {"error": str(e), "date": date}
|
|
|
|
|
|
def query_body_battery_at_time(client, time_str, date_str=None):
|
|
"""Get Body Battery level at a specific time."""
|
|
target_time = parse_time(time_str, date_str)
|
|
date = target_time.strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
data = client.get_body_battery(date)
|
|
|
|
if not data or len(data) == 0:
|
|
return {"error": "No Body Battery data for this date", "date": date}
|
|
|
|
# Body Battery is in bodyBatteryValuesArray
|
|
bb_values = data[0].get("bodyBatteryValuesArray", [])
|
|
|
|
# Convert [timestamp, value] pairs to dicts
|
|
bb_dicts = [{"startTimeInSeconds": ts//1000, "value": val} for ts, val in bb_values]
|
|
|
|
closest = find_closest_datapoint(target_time, bb_dicts)
|
|
|
|
if closest:
|
|
return {
|
|
"requested_time": time_str,
|
|
"body_battery": closest.get("value"),
|
|
"date": date
|
|
}
|
|
else:
|
|
return {"error": "No data point found near requested time", "date": date}
|
|
|
|
except Exception as e:
|
|
return {"error": str(e), "date": date}
|
|
|
|
|
|
def query_steps_at_time(client, time_str, date_str=None):
|
|
"""Get step count at a specific time."""
|
|
target_time = parse_time(time_str, date_str)
|
|
date = target_time.strftime("%Y-%m-%d")
|
|
|
|
try:
|
|
data = client.get_steps_data(date)
|
|
|
|
if not data:
|
|
return {"error": "No steps data for this date", "date": date}
|
|
|
|
# Steps are usually cumulative throughout the day
|
|
step_values = data.get("stepsArray") or []
|
|
|
|
closest = find_closest_datapoint(target_time, step_values)
|
|
|
|
if closest:
|
|
return {
|
|
"requested_time": time_str,
|
|
"steps": closest.get("steps") or closest.get("value"),
|
|
"date": date
|
|
}
|
|
else:
|
|
return {"error": "No data point found near requested time", "date": date}
|
|
|
|
except Exception as e:
|
|
return {"error": str(e), "date": date}
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Query Garmin data by time")
|
|
parser.add_argument("metric", choices=["heart_rate", "stress", "body_battery", "steps"],
|
|
help="Metric to query")
|
|
parser.add_argument("time", help="Time (e.g., '3:00 PM', '15:00', '2024-01-15 14:30')")
|
|
parser.add_argument("--date", help="Date if not included in time (YYYY-MM-DD)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
client = get_client()
|
|
if not client:
|
|
print('{"error": "Not authenticated"}')
|
|
sys.exit(1)
|
|
|
|
if args.metric == "heart_rate":
|
|
result = query_heart_rate_at_time(client, args.time, args.date)
|
|
elif args.metric == "stress":
|
|
result = query_stress_at_time(client, args.time, args.date)
|
|
elif args.metric == "body_battery":
|
|
result = query_body_battery_at_time(client, args.time, args.date)
|
|
elif args.metric == "steps":
|
|
result = query_steps_at_time(client, args.time, args.date)
|
|
|
|
print(json.dumps(result, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|