Skip to main content

garmin_cli/cli/commands/
weight.rs

1//! Weight and body composition commands for garmin-cli
2
3use crate::client::GarminClient;
4use crate::config::CredentialStore;
5use crate::error::Result;
6use chrono::{Duration, Local};
7
8use super::auth::refresh_token;
9
10/// List weight entries in a date range
11pub async fn list(from: Option<String>, to: Option<String>, profile: Option<String>) -> Result<()> {
12    let store = CredentialStore::new(profile)?;
13    let (oauth1, oauth2) = refresh_token(&store).await?;
14
15    let client = GarminClient::new(&oauth1.domain);
16
17    // Default to last 30 days if no dates provided
18    let today = Local::now().date_naive();
19    let end_date = to.unwrap_or_else(|| today.format("%Y-%m-%d").to_string());
20    let start_date =
21        from.unwrap_or_else(|| (today - Duration::days(30)).format("%Y-%m-%d").to_string());
22
23    let path = format!(
24        "/weight-service/weight/dateRange?startDate={}&endDate={}",
25        start_date, end_date
26    );
27
28    let data: serde_json::Value = client.get_json(&oauth2, &path).await?;
29
30    // Extract weight entries from response
31    if let Some(entries) = data.get("dateWeightList").and_then(|v| v.as_array()) {
32        if entries.is_empty() {
33            println!("No weight entries found.");
34            return Ok(());
35        }
36
37        println!(
38            "{:<12} {:>10} {:>10} {:>8} {:>8} {:>8} {:>8}",
39            "Date", "Weight", "BMI", "Fat %", "Muscle %", "Water %", "Bone"
40        );
41        println!("{}", "-".repeat(75));
42
43        for entry in entries {
44            let date = entry
45                .get("calendarDate")
46                .and_then(|v| v.as_str())
47                .unwrap_or("-");
48
49            let weight = entry
50                .get("weight")
51                .and_then(|v| v.as_f64())
52                .map(|w| format!("{:.1} kg", w / 1000.0))
53                .unwrap_or_else(|| "-".to_string());
54
55            let bmi = entry
56                .get("bmi")
57                .and_then(|v| v.as_f64())
58                .map(|b| format!("{:.1}", b))
59                .unwrap_or_else(|| "-".to_string());
60
61            let body_fat = entry
62                .get("bodyFat")
63                .and_then(|v| v.as_f64())
64                .map(|f| format!("{:.1}", f))
65                .unwrap_or_else(|| "-".to_string());
66
67            let muscle_mass = entry
68                .get("muscleMass")
69                .and_then(|v| v.as_f64())
70                .map(|m| format!("{:.1}", m / 1000.0))
71                .unwrap_or_else(|| "-".to_string());
72
73            let body_water = entry
74                .get("bodyWater")
75                .and_then(|v| v.as_f64())
76                .map(|w| format!("{:.1}", w))
77                .unwrap_or_else(|| "-".to_string());
78
79            let bone_mass = entry
80                .get("boneMass")
81                .and_then(|v| v.as_f64())
82                .map(|b| format!("{:.1}", b / 1000.0))
83                .unwrap_or_else(|| "-".to_string());
84
85            println!(
86                "{:<12} {:>10} {:>10} {:>8} {:>8} {:>8} {:>8}",
87                date, weight, bmi, body_fat, muscle_mass, body_water, bone_mass
88            );
89        }
90
91        println!(
92            "\nShowing {} entries from {} to {}",
93            entries.len(),
94            start_date,
95            end_date
96        );
97    } else {
98        // Try alternative response format
99        println!("{}", serde_json::to_string_pretty(&data)?);
100    }
101
102    Ok(())
103}
104
105/// Add a weight entry
106pub async fn add(weight: f64, unit: &str, profile: Option<String>) -> Result<()> {
107    let store = CredentialStore::new(profile)?;
108    let (oauth1, oauth2) = refresh_token(&store).await?;
109
110    let client = GarminClient::new(&oauth1.domain);
111
112    // Convert to grams (Garmin API uses grams)
113    let weight_grams = match unit.to_lowercase().as_str() {
114        "kg" => (weight * 1000.0) as i64,
115        "lbs" | "lb" => (weight * 453.592) as i64,
116        _ => {
117            return Err(crate::error::GarminError::invalid_response(
118                "Invalid unit. Use 'kg' or 'lbs'",
119            ));
120        }
121    };
122
123    let today = Local::now().format("%Y-%m-%d").to_string();
124    let timestamp = Local::now().timestamp_millis();
125
126    let body = serde_json::json!({
127        "dateTimestamp": timestamp,
128        "gmtTimestamp": timestamp,
129        "weight": weight_grams,
130        "unitKey": "kg",
131        "calendarDate": today
132    });
133
134    let path = "/weight-service/user-weight";
135
136    let response = client.post_json(&oauth2, path, &body).await?;
137
138    println!("Weight entry added: {:.1} {} on {}", weight, unit, today);
139
140    if let Some(id) = response.get("samplePk").and_then(|v| v.as_i64()) {
141        println!("Entry ID: {}", id);
142    }
143
144    Ok(())
145}
146
147/// Get latest weight
148pub async fn latest(profile: Option<String>) -> Result<()> {
149    let store = CredentialStore::new(profile)?;
150    let (oauth1, oauth2) = refresh_token(&store).await?;
151
152    let client = GarminClient::new(&oauth1.domain);
153
154    let path = "/weight-service/weight/latest";
155
156    let data: serde_json::Value = client.get_json(&oauth2, path).await?;
157
158    let date = data
159        .get("calendarDate")
160        .and_then(|v| v.as_str())
161        .unwrap_or("-");
162
163    let weight = data
164        .get("weight")
165        .and_then(|v| v.as_f64())
166        .map(|w| w / 1000.0);
167
168    let bmi = data.get("bmi").and_then(|v| v.as_f64());
169    let body_fat = data.get("bodyFat").and_then(|v| v.as_f64());
170
171    println!("Latest Weight Entry");
172    println!("{}", "-".repeat(30));
173    println!("Date:     {}", date);
174    println!(
175        "Weight:   {}",
176        weight
177            .map(|w| format!("{:.1} kg", w))
178            .unwrap_or("-".to_string())
179    );
180    println!(
181        "BMI:      {}",
182        bmi.map(|b| format!("{:.1}", b)).unwrap_or("-".to_string())
183    );
184    println!(
185        "Body Fat: {}",
186        body_fat
187            .map(|f| format!("{:.1}%", f))
188            .unwrap_or("-".to_string())
189    );
190
191    Ok(())
192}