garmin_cli/cli/commands/
devices.rs1use duckdb::Connection;
4
5use crate::client::GarminClient;
6use crate::config::CredentialStore;
7use crate::error::Result;
8use crate::storage::default_storage_path;
9
10use super::auth::refresh_token;
11
12pub async fn list(profile: Option<String>) -> Result<()> {
14 let store = CredentialStore::new(profile)?;
15 let (oauth1, oauth2) = refresh_token(&store).await?;
16
17 let client = GarminClient::new(&oauth1.domain);
18
19 let path = "/device-service/deviceregistration/devices";
20
21 let devices: Vec<serde_json::Value> = client.get_json(&oauth2, path).await?;
22
23 if devices.is_empty() {
24 println!("No devices found.");
25 return Ok(());
26 }
27
28 println!(
29 "{:<20} {:<25} {:<15} {:<12}",
30 "Device", "Model", "Software", "Last Sync"
31 );
32 println!("{}", "-".repeat(75));
33
34 for device in &devices {
35 let name = device
36 .get("displayName")
37 .or_else(|| device.get("deviceTypeName"))
38 .and_then(|v| v.as_str())
39 .unwrap_or("Unknown");
40
41 let model = device
42 .get("partNumber")
43 .and_then(|v| v.as_str())
44 .unwrap_or("-");
45
46 let software = device
47 .get("currentFirmwareVersion")
48 .and_then(|v| v.as_str())
49 .unwrap_or("-");
50
51 let last_sync = device
52 .get("lastSyncTime")
53 .and_then(|v| v.as_str())
54 .map(|s| s.split('T').next().unwrap_or(s))
55 .unwrap_or("-");
56
57 println!(
58 "{:<20} {:<25} {:<15} {:<12}",
59 truncate(name, 19),
60 truncate(model, 24),
61 truncate(software, 14),
62 last_sync
63 );
64 }
65
66 println!("\nTotal: {} device(s)", devices.len());
67
68 Ok(())
69}
70
71pub async fn get(device_id: &str, profile: Option<String>) -> Result<()> {
73 let store = CredentialStore::new(profile)?;
74 let (oauth1, oauth2) = refresh_token(&store).await?;
75
76 let client = GarminClient::new(&oauth1.domain);
77
78 let path = format!(
79 "/device-service/deviceservice/device-info/settings/{}",
80 device_id
81 );
82
83 let data: serde_json::Value = client.get_json(&oauth2, &path).await?;
84
85 println!("{}", serde_json::to_string_pretty(&data)?);
86
87 Ok(())
88}
89
90fn truncate(s: &str, max_len: usize) -> String {
91 if s.len() <= max_len {
92 s.to_string()
93 } else {
94 format!("{}...", &s[..max_len.saturating_sub(3)])
95 }
96}
97
98pub async fn history(storage_path: Option<String>) -> Result<()> {
100 let storage_path = storage_path
101 .map(std::path::PathBuf::from)
102 .unwrap_or_else(default_storage_path);
103
104 let activities_path = storage_path.join("activities");
105 if !activities_path.exists() {
106 println!("No activities found at: {}", activities_path.display());
107 println!("Run 'garmin sync run' to sync your activities first.");
108 return Ok(());
109 }
110
111 let conn =
113 Connection::open_in_memory().map_err(|e| crate::GarminError::Database(e.to_string()))?;
114
115 let glob_pattern = format!("{}/*.parquet", activities_path.display());
116
117 let mut stmt = conn
119 .prepare(&format!(
120 "SELECT
121 json_extract_string(raw_json, '$.metadataDTO.deviceMetaDataDTO.deviceId') as device_id,
122 json_extract(raw_json, '$.metadataDTO.deviceMetaDataDTO.deviceTypePk') as device_type,
123 MIN(start_time_local) as first_activity,
124 MAX(start_time_local) as last_activity,
125 COUNT(*) as activity_count
126 FROM '{}'
127 WHERE raw_json IS NOT NULL
128 AND json_extract_string(raw_json, '$.metadataDTO.deviceMetaDataDTO.deviceId') IS NOT NULL
129 GROUP BY 1, 2
130 ORDER BY first_activity",
131 glob_pattern
132 ))
133 .map_err(|e| crate::GarminError::Database(e.to_string()))?;
134
135 #[allow(clippy::type_complexity)]
136 let devices: Vec<(
137 Option<String>,
138 Option<i64>,
139 Option<String>,
140 Option<String>,
141 i64,
142 )> = stmt
143 .query_map([], |row| {
144 Ok((
145 row.get::<_, Option<String>>(0)?,
146 row.get::<_, Option<i64>>(1)?,
147 row.get::<_, Option<String>>(2)?,
148 row.get::<_, Option<String>>(3)?,
149 row.get::<_, i64>(4)?,
150 ))
151 })
152 .map_err(|e| crate::GarminError::Database(e.to_string()))?
153 .filter_map(|r| r.ok())
154 .collect();
155
156 if devices.is_empty() {
157 println!("No device history found in synced activities.");
158 println!("Make sure you have synced activities with 'garmin sync run'.");
159 return Ok(());
160 }
161
162 println!("Device history (from synced activities):\n");
163 println!(
164 "{:<15} {:<12} {:<12} {:<12} {:>10}",
165 "Device ID", "Type PK", "First Used", "Last Used", "Activities"
166 );
167 println!("{}", "-".repeat(65));
168
169 for (device_id, device_type, first, last, count) in &devices {
170 let device_id_str = device_id.as_deref().unwrap_or("-");
171 let device_type_str = device_type
172 .map(|t| t.to_string())
173 .unwrap_or_else(|| "-".to_string());
174 let first_date = first
175 .as_ref()
176 .and_then(|s| s.split(' ').next())
177 .unwrap_or("-");
178 let last_date = last
179 .as_ref()
180 .and_then(|s| s.split(' ').next())
181 .unwrap_or("-");
182
183 println!(
184 "{:<15} {:<12} {:<12} {:<12} {:>10}",
185 truncate(device_id_str, 14),
186 truncate(&device_type_str, 11),
187 first_date,
188 last_date,
189 count
190 );
191 }
192
193 println!("\nTotal: {} device(s) found", devices.len());
194
195 println!("\nNote: Device Type PK can be looked up in Garmin's device database.");
197 println!("Common types: 37010=Forerunner 955, 30380=Edge 810, etc.");
198
199 Ok(())
200}