ruby_garmin_connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +295 -0
- data/lib/garmin_connect/api/activities.rb +190 -0
- data/lib/garmin_connect/api/badges.rb +94 -0
- data/lib/garmin_connect/api/body_composition.rb +127 -0
- data/lib/garmin_connect/api/devices.rb +118 -0
- data/lib/garmin_connect/api/health.rb +139 -0
- data/lib/garmin_connect/api/metrics.rb +177 -0
- data/lib/garmin_connect/api/user.rb +39 -0
- data/lib/garmin_connect/api/wellness.rb +45 -0
- data/lib/garmin_connect/api/workouts.rb +62 -0
- data/lib/garmin_connect/auth/oauth1_token.rb +43 -0
- data/lib/garmin_connect/auth/oauth2_token.rb +70 -0
- data/lib/garmin_connect/auth/sso.rb +303 -0
- data/lib/garmin_connect/auth/token_store.rb +79 -0
- data/lib/garmin_connect/client.rb +213 -0
- data/lib/garmin_connect/connection.rb +148 -0
- data/lib/garmin_connect/errors.rb +46 -0
- data/lib/garmin_connect/version.rb +5 -0
- data/lib/garmin_connect.rb +35 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '08c6d390104bc4f19e46f95d0726171890e2c0f3c41e165b1afca94031c0f243'
|
|
4
|
+
data.tar.gz: 8225469c95d81af76180c1e6e1c4229f3caaa27cef9eb675e1c514ec208b1145
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e3f740b9dda27508940450bacfe50f7ad74e060f2c9f97100500fcb1b200424cfb425efd0a187d61f5d55e03af04e3ada0b2029284a6542adbefb70f971c0ede
|
|
7
|
+
data.tar.gz: 2609e8c7e033db637fa8358c409cda7d5d31c8018b6d43a7506a309140febaa90c7ce271a79600cdeea40b0a5f278304f7511ccb045cfe6b7b2ac706ada4e6e1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-02-11)
|
|
4
|
+
|
|
5
|
+
- Initial release
|
|
6
|
+
- Full Garmin Connect OAuth authentication (SSO + OAuth1 + OAuth2)
|
|
7
|
+
- MFA support
|
|
8
|
+
- Token persistence (file and string-based, garth-compatible)
|
|
9
|
+
- Automatic token refresh
|
|
10
|
+
- 108 API methods across 9 categories:
|
|
11
|
+
- User & Profile
|
|
12
|
+
- Daily Health (steps, HR, HRV, sleep, stress, body battery, SpO2, respiration)
|
|
13
|
+
- Activities (list, details, CRUD, download in 5 formats, upload)
|
|
14
|
+
- Body Composition & Weight (weigh-ins, hydration, blood pressure)
|
|
15
|
+
- Advanced Metrics (VO2 max, training readiness, endurance/hill score, race predictions, lactate threshold)
|
|
16
|
+
- Devices & Gear (device settings, gear management, gear-activity linking)
|
|
17
|
+
- Badges, Challenges & Goals (earned/available badges, challenges, personal records, goals)
|
|
18
|
+
- Workouts & Training Plans
|
|
19
|
+
- Wellness (menstrual cycle, pregnancy, lifestyle logging, GraphQL)
|
|
20
|
+
- Retry with exponential backoff on server errors
|
|
21
|
+
- Typed error hierarchy
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DRBragg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# GarminConnect
|
|
2
|
+
|
|
3
|
+
A Ruby client for the Garmin Connect API. Provides access to health, fitness, activity, and device data from your Garmin account.
|
|
4
|
+
|
|
5
|
+
Inspired by [python-garminconnect](https://github.com/cyberjunky/python-garminconnect), rebuilt idiomatically for Ruby.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "garmin_connect"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install directly:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
gem install garmin_connect
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "garmin_connect"
|
|
25
|
+
|
|
26
|
+
# Login with credentials (tokens are saved to ~/.garminconnect automatically)
|
|
27
|
+
client = GarminConnect.login(email: "you@example.com", password: "your-password")
|
|
28
|
+
|
|
29
|
+
# Subsequent sessions resume from saved tokens (no re-login for ~1 year)
|
|
30
|
+
client = GarminConnect.login
|
|
31
|
+
|
|
32
|
+
# Get today's stats
|
|
33
|
+
puts client.daily_summary
|
|
34
|
+
puts client.heart_rates
|
|
35
|
+
puts client.sleep_data
|
|
36
|
+
puts client.stress
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
The gem uses the same OAuth flow as the Garmin Connect mobile app:
|
|
42
|
+
|
|
43
|
+
1. SSO login with your email/password
|
|
44
|
+
2. Exchange for an OAuth1 token (~1 year lifetime)
|
|
45
|
+
3. Exchange OAuth1 for an OAuth2 Bearer token (~20 hours)
|
|
46
|
+
4. Auto-refresh when the OAuth2 token expires
|
|
47
|
+
|
|
48
|
+
### MFA Support
|
|
49
|
+
|
|
50
|
+
If your account has MFA enabled, the gem prompts via `$stdin` by default. You can provide a custom handler:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
client = GarminConnect::Client.new(
|
|
54
|
+
email: "you@example.com",
|
|
55
|
+
password: "your-password",
|
|
56
|
+
mfa_handler: -> { print "MFA code: "; gets.chomp }
|
|
57
|
+
)
|
|
58
|
+
client.login
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Token Storage
|
|
62
|
+
|
|
63
|
+
Tokens are saved to `~/.garminconnect` by default. You can customize this:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# Custom directory
|
|
67
|
+
client = GarminConnect::Client.new(token_dir: "/path/to/tokens")
|
|
68
|
+
|
|
69
|
+
# Base64-encoded string (useful for environment variables)
|
|
70
|
+
encoded = client.dump_tokens
|
|
71
|
+
client = GarminConnect::Client.new(token_string: encoded, token_dir: nil)
|
|
72
|
+
|
|
73
|
+
# Disable persistence entirely
|
|
74
|
+
client = GarminConnect::Client.new(token_dir: nil)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Token Interoperability
|
|
78
|
+
|
|
79
|
+
Token files are compatible with [garth](https://github.com/matin/garth) (the Python auth library). If you have existing tokens from the Python library, point `token_dir` at the same directory.
|
|
80
|
+
|
|
81
|
+
## API Reference
|
|
82
|
+
|
|
83
|
+
### User & Profile
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
client.user_settings # Measurement system, sleep settings, etc.
|
|
87
|
+
client.user_profile # Profile configuration
|
|
88
|
+
client.personal_information # Age, gender, email, biometric profile
|
|
89
|
+
client.display_name # "YourDisplayName"
|
|
90
|
+
client.full_name # "Your Full Name"
|
|
91
|
+
client.unit_system # "statute_us" or "metric"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Daily Health
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
client.daily_summary(date) # Steps, calories, distance, active minutes
|
|
98
|
+
client.heart_rates(date) # Heart rate data with resting HR
|
|
99
|
+
client.resting_heart_rate(start_date, end_date)
|
|
100
|
+
client.hrv(date) # Heart rate variability
|
|
101
|
+
client.sleep_data(date) # Sleep stages, duration, scores
|
|
102
|
+
client.stress(date) # All-day stress levels
|
|
103
|
+
client.body_battery(start, end) # Body battery reports
|
|
104
|
+
client.body_battery_events(date)
|
|
105
|
+
client.steps_data(date) # Steps chart data
|
|
106
|
+
client.floors(date) # Floors climbed
|
|
107
|
+
client.respiration(date) # Respiration rate
|
|
108
|
+
client.spo2(date) # Blood oxygen
|
|
109
|
+
client.intensity_minutes(date) # Intensity minutes
|
|
110
|
+
client.daily_events(date) # Auto-detected activities
|
|
111
|
+
client.request_reload(date) # Request data reload from device
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Activities
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
# Listing
|
|
118
|
+
client.activities(start: 0, limit: 20)
|
|
119
|
+
client.activities_by_date("2026-01-01", "2026-01-31")
|
|
120
|
+
client.activity_count
|
|
121
|
+
client.last_activity
|
|
122
|
+
|
|
123
|
+
# Details
|
|
124
|
+
client.activity(activity_id)
|
|
125
|
+
client.activity_details(activity_id)
|
|
126
|
+
client.activity_splits(activity_id)
|
|
127
|
+
client.activity_typed_splits(activity_id)
|
|
128
|
+
client.activity_split_summaries(activity_id)
|
|
129
|
+
client.activity_weather(activity_id)
|
|
130
|
+
client.activity_hr_zones(activity_id)
|
|
131
|
+
client.activity_power_zones(activity_id)
|
|
132
|
+
client.activity_exercise_sets(activity_id)
|
|
133
|
+
client.activity_types
|
|
134
|
+
|
|
135
|
+
# CRUD
|
|
136
|
+
client.create_activity(payload_hash)
|
|
137
|
+
client.rename_activity(activity_id, "Morning Run")
|
|
138
|
+
client.update_activity_type(activity_id, type_dto)
|
|
139
|
+
client.delete_activity(activity_id)
|
|
140
|
+
|
|
141
|
+
# Download & Upload
|
|
142
|
+
client.download_activity(activity_id, format: :original) # :tcx, :gpx, :kml, :csv
|
|
143
|
+
client.upload_activity("/path/to/file.fit")
|
|
144
|
+
|
|
145
|
+
# Progress
|
|
146
|
+
client.progress_summary("2026-01-01", "2026-12-31", metric: "distance")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Body Composition & Weight
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
client.body_composition(start_date, end_date)
|
|
153
|
+
client.weigh_ins(start_date, end_date)
|
|
154
|
+
client.daily_weigh_ins(date)
|
|
155
|
+
client.add_weigh_in(84.5, date: "2026-02-11", unit_key: "kg")
|
|
156
|
+
client.delete_weigh_in(date, weight_pk)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Hydration
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
client.hydration(date)
|
|
163
|
+
client.log_hydration(250, date: "2026-02-11") # 250 ml
|
|
164
|
+
client.log_hydration(-250, date: "2026-02-11") # Subtract
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Blood Pressure
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
client.blood_pressure(start_date, end_date)
|
|
171
|
+
client.log_blood_pressure(systolic: 120, diastolic: 80, pulse: 65)
|
|
172
|
+
client.delete_blood_pressure(date, version)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Advanced Metrics
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
client.max_metrics(date) # VO2 Max
|
|
179
|
+
client.training_readiness(date) # Training readiness score
|
|
180
|
+
client.training_status(date) # Aggregated training status
|
|
181
|
+
client.endurance_score(date) # Single day
|
|
182
|
+
client.endurance_score(start_date: s, end_date: e) # Date range
|
|
183
|
+
client.hill_score(date)
|
|
184
|
+
client.race_predictions # 5k, 10k, half, full marathon
|
|
185
|
+
client.fitness_age(date)
|
|
186
|
+
client.lactate_threshold # Latest
|
|
187
|
+
client.lactate_threshold_history(start_date, end_date)
|
|
188
|
+
client.cycling_ftp # Functional Threshold Power
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Historical Data
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
client.daily_steps(start_date, end_date) # Auto-chunked at 28 days
|
|
195
|
+
client.weekly_steps(end_date, weeks: 52)
|
|
196
|
+
client.weekly_stress(end_date, weeks: 52)
|
|
197
|
+
client.weekly_intensity_minutes(start_date, end_date)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Devices & Gear
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Devices
|
|
204
|
+
client.devices
|
|
205
|
+
client.device_settings(device_id)
|
|
206
|
+
client.last_used_device
|
|
207
|
+
client.primary_training_device
|
|
208
|
+
client.device_solar_data(device_id, start_date, end_date)
|
|
209
|
+
client.device_alarms # Alarms across all devices
|
|
210
|
+
|
|
211
|
+
# Gear
|
|
212
|
+
client.gear
|
|
213
|
+
client.activity_gear(activity_id)
|
|
214
|
+
client.gear_stats(gear_uuid)
|
|
215
|
+
client.gear_defaults
|
|
216
|
+
client.set_gear_default(gear_uuid, activity_type)
|
|
217
|
+
client.link_gear(gear_uuid, activity_id)
|
|
218
|
+
client.unlink_gear(gear_uuid, activity_id)
|
|
219
|
+
client.gear_activities(gear_uuid)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Badges, Challenges & Goals
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
client.earned_badges
|
|
226
|
+
client.available_badges
|
|
227
|
+
client.in_progress_badges
|
|
228
|
+
client.adhoc_challenges
|
|
229
|
+
client.badge_challenges
|
|
230
|
+
client.available_badge_challenges
|
|
231
|
+
client.non_completed_badge_challenges
|
|
232
|
+
client.virtual_challenges
|
|
233
|
+
client.personal_records
|
|
234
|
+
client.goals(status: "active")
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Workouts & Training Plans
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
client.workouts(start: 0, limit: 20)
|
|
241
|
+
client.workout(workout_id)
|
|
242
|
+
client.download_workout(workout_id)
|
|
243
|
+
client.create_workout(payload_hash)
|
|
244
|
+
client.scheduled_workout(id)
|
|
245
|
+
client.training_plans
|
|
246
|
+
client.training_plan(plan_id)
|
|
247
|
+
client.adaptive_training_plan(plan_id)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Wellness & Misc
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
client.menstrual_data(date)
|
|
254
|
+
client.menstrual_calendar(start_date, end_date)
|
|
255
|
+
client.pregnancy_summary
|
|
256
|
+
client.lifestyle_logging(date)
|
|
257
|
+
client.graphql(query_string, variables: {})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Error Handling
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
begin
|
|
264
|
+
client.daily_summary
|
|
265
|
+
rescue GarminConnect::UnauthorizedError
|
|
266
|
+
# Token expired or invalid
|
|
267
|
+
rescue GarminConnect::TooManyRequestsError
|
|
268
|
+
# Rate limited, back off
|
|
269
|
+
rescue GarminConnect::NotFoundError
|
|
270
|
+
# Resource doesn't exist
|
|
271
|
+
rescue GarminConnect::ServerError
|
|
272
|
+
# Garmin's servers are having issues
|
|
273
|
+
rescue GarminConnect::HTTPError => e
|
|
274
|
+
# Any other HTTP error
|
|
275
|
+
puts e.status
|
|
276
|
+
puts e.body
|
|
277
|
+
rescue GarminConnect::AuthenticationError
|
|
278
|
+
# Login/token issues
|
|
279
|
+
rescue GarminConnect::Error
|
|
280
|
+
# Catch-all for gem errors
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Development
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
git clone https://github.com/drbragg/garmin_connect.git
|
|
288
|
+
cd garmin_connect
|
|
289
|
+
bundle install
|
|
290
|
+
bundle exec rspec
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GarminConnect
|
|
4
|
+
module API
|
|
5
|
+
# Activity listing, details, CRUD, and download/upload endpoints.
|
|
6
|
+
module Activities
|
|
7
|
+
DOWNLOAD_FORMATS = {
|
|
8
|
+
original: "/download-service/files/activity/%s",
|
|
9
|
+
tcx: "/download-service/export/tcx/activity/%s",
|
|
10
|
+
gpx: "/download-service/export/gpx/activity/%s",
|
|
11
|
+
kml: "/download-service/export/kml/activity/%s",
|
|
12
|
+
csv: "/download-service/export/csv/activity/%s"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# List activities with optional filters.
|
|
16
|
+
# @param start [Integer] pagination offset
|
|
17
|
+
# @param limit [Integer] max results
|
|
18
|
+
# @param activity_type [String, nil] filter by activity type
|
|
19
|
+
# @param sort_order [String] "asc" or "desc"
|
|
20
|
+
def activities(start: 0, limit: 20, activity_type: nil, sort_order: "desc")
|
|
21
|
+
params = { "start" => start, "limit" => limit }
|
|
22
|
+
params["activityType"] = activity_type if activity_type
|
|
23
|
+
params["sortOrder"] = sort_order
|
|
24
|
+
|
|
25
|
+
connection.get("/activitylist-service/activities/search/activities", params: params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get activities within a date range.
|
|
29
|
+
# @param start_date [Date, String]
|
|
30
|
+
# @param end_date [Date, String]
|
|
31
|
+
# @param activity_type [String, nil]
|
|
32
|
+
def activities_by_date(start_date, end_date, activity_type: nil)
|
|
33
|
+
params = {
|
|
34
|
+
"startDate" => format_date(start_date),
|
|
35
|
+
"endDate" => format_date(end_date),
|
|
36
|
+
"start" => 0,
|
|
37
|
+
"limit" => 100
|
|
38
|
+
}
|
|
39
|
+
params["activityType"] = activity_type if activity_type
|
|
40
|
+
|
|
41
|
+
connection.get("/activitylist-service/activities/search/activities", params: params)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get the total activity count.
|
|
45
|
+
def activity_count
|
|
46
|
+
connection.get("/activitylist-service/activities/count")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Get the most recent activity.
|
|
50
|
+
def last_activity
|
|
51
|
+
results = activities(start: 0, limit: 1)
|
|
52
|
+
results&.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get a single activity's summary.
|
|
56
|
+
# @param activity_id [String, Integer]
|
|
57
|
+
def activity(activity_id)
|
|
58
|
+
connection.get("/activity-service/activity/#{activity_id}")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get detailed activity data (charts, polylines).
|
|
62
|
+
# @param activity_id [String, Integer]
|
|
63
|
+
# @param max_chart_size [Integer]
|
|
64
|
+
# @param max_polyline_size [Integer]
|
|
65
|
+
def activity_details(activity_id, max_chart_size: 2000, max_polyline_size: 4000)
|
|
66
|
+
connection.get(
|
|
67
|
+
"/activity-service/activity/#{activity_id}/details",
|
|
68
|
+
params: { "maxChartSize" => max_chart_size, "maxPolylineSize" => max_polyline_size }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Get activity splits.
|
|
73
|
+
def activity_splits(activity_id)
|
|
74
|
+
connection.get("/activity-service/activity/#{activity_id}/splits")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Get typed activity splits.
|
|
78
|
+
def activity_typed_splits(activity_id)
|
|
79
|
+
connection.get("/activity-service/activity/#{activity_id}/typedsplits")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get activity split summaries.
|
|
83
|
+
def activity_split_summaries(activity_id)
|
|
84
|
+
connection.get("/activity-service/activity/#{activity_id}/split_summaries")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get weather data for an activity.
|
|
88
|
+
def activity_weather(activity_id)
|
|
89
|
+
connection.get("/activity-service/activity/#{activity_id}/weather")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get heart rate time-in-zones for an activity.
|
|
93
|
+
def activity_hr_zones(activity_id)
|
|
94
|
+
connection.get("/activity-service/activity/#{activity_id}/hrTimeInZones")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Get power time-in-zones for an activity.
|
|
98
|
+
def activity_power_zones(activity_id)
|
|
99
|
+
connection.get("/activity-service/activity/#{activity_id}/powerTimeInZones")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get exercise sets for an activity.
|
|
103
|
+
def activity_exercise_sets(activity_id)
|
|
104
|
+
connection.get("/activity-service/activity/#{activity_id}/exerciseSets")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Get all available activity types.
|
|
108
|
+
def activity_types
|
|
109
|
+
connection.get("/activity-service/activity/activityTypes")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get heart rate activities for a specific date.
|
|
113
|
+
def heart_rate_activities(date = today)
|
|
114
|
+
connection.get("/mobile-gateway/heartRate/forDate/#{format_date(date)}")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# --- CRUD ---
|
|
118
|
+
|
|
119
|
+
# Create a manual activity from a hash/JSON payload.
|
|
120
|
+
# @param payload [Hash] the activity data
|
|
121
|
+
def create_activity(payload)
|
|
122
|
+
connection.post("/activity-service/activity", body: payload)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Rename an activity.
|
|
126
|
+
# @param activity_id [String, Integer]
|
|
127
|
+
# @param name [String]
|
|
128
|
+
def rename_activity(activity_id, name)
|
|
129
|
+
connection.put(
|
|
130
|
+
"/activity-service/activity/#{activity_id}",
|
|
131
|
+
body: { "activityId" => activity_id, "activityName" => name }
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Change an activity's type.
|
|
136
|
+
# @param activity_id [String, Integer]
|
|
137
|
+
# @param activity_type [Hash] the activityTypeDTO
|
|
138
|
+
def update_activity_type(activity_id, activity_type)
|
|
139
|
+
connection.put(
|
|
140
|
+
"/activity-service/activity/#{activity_id}",
|
|
141
|
+
body: { "activityId" => activity_id, "activityTypeDTO" => activity_type }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Delete an activity.
|
|
146
|
+
def delete_activity(activity_id)
|
|
147
|
+
connection.delete("/activity-service/activity/#{activity_id}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# --- Download / Upload ---
|
|
151
|
+
|
|
152
|
+
# Download an activity file.
|
|
153
|
+
# @param activity_id [String, Integer]
|
|
154
|
+
# @param format [Symbol] :original, :tcx, :gpx, :kml, or :csv
|
|
155
|
+
# @return [String] raw file bytes
|
|
156
|
+
def download_activity(activity_id, format: :original)
|
|
157
|
+
path = DOWNLOAD_FORMATS.fetch(format) do
|
|
158
|
+
raise ArgumentError, "Unknown format: #{format}. Use: #{DOWNLOAD_FORMATS.keys.join(", ")}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
connection.download(path % activity_id)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Upload an activity file (FIT, GPX, or TCX).
|
|
165
|
+
# @param file_path [String] path to the file
|
|
166
|
+
def upload_activity(file_path)
|
|
167
|
+
connection.upload("/upload-service/upload", file_path: file_path)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# --- Progress ---
|
|
171
|
+
|
|
172
|
+
# Get progress summary between dates.
|
|
173
|
+
# @param start_date [Date, String]
|
|
174
|
+
# @param end_date [Date, String]
|
|
175
|
+
# @param metric [String] e.g., "distance", "duration", "elevationGain"
|
|
176
|
+
def progress_summary(start_date, end_date, metric: "distance")
|
|
177
|
+
connection.get(
|
|
178
|
+
"/fitnessstats-service/activity",
|
|
179
|
+
params: {
|
|
180
|
+
"startDate" => format_date(start_date),
|
|
181
|
+
"endDate" => format_date(end_date),
|
|
182
|
+
"aggregation" => "lifetime",
|
|
183
|
+
"groupByParentActivityType" => true,
|
|
184
|
+
"metric" => metric
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GarminConnect
|
|
4
|
+
module API
|
|
5
|
+
# Badges, challenges, personal records, and goals endpoints.
|
|
6
|
+
module Badges
|
|
7
|
+
# Get all earned badges.
|
|
8
|
+
def earned_badges
|
|
9
|
+
connection.get("/badge-service/badge/earned")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get all available badges.
|
|
13
|
+
def available_badges
|
|
14
|
+
connection.get(
|
|
15
|
+
"/badge-service/badge/available",
|
|
16
|
+
params: { "showExclusiveBadge" => true }
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get badges currently in progress.
|
|
21
|
+
def in_progress_badges
|
|
22
|
+
earned = earned_badges
|
|
23
|
+
available = available_badges
|
|
24
|
+
return [] unless earned.is_a?(Array) && available.is_a?(Array)
|
|
25
|
+
|
|
26
|
+
earned_ids = earned.map { |b| b["badgeId"] }.compact.to_set
|
|
27
|
+
available.reject { |b| earned_ids.include?(b["badgeId"]) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get historical ad-hoc challenges.
|
|
31
|
+
# @param start [Integer] pagination offset
|
|
32
|
+
# @param limit [Integer]
|
|
33
|
+
def adhoc_challenges(start: 0, limit: 20)
|
|
34
|
+
connection.get(
|
|
35
|
+
"/adhocchallenge-service/adHocChallenge/historical",
|
|
36
|
+
params: { "start" => start, "limit" => limit }
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get completed badge challenges.
|
|
41
|
+
def badge_challenges(start: 0, limit: 20)
|
|
42
|
+
connection.get(
|
|
43
|
+
"/badgechallenge-service/badgeChallenge/completed",
|
|
44
|
+
params: { "start" => start, "limit" => limit }
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get available badge challenges.
|
|
49
|
+
def available_badge_challenges(start: 0, limit: 20)
|
|
50
|
+
connection.get(
|
|
51
|
+
"/badgechallenge-service/badgeChallenge/available",
|
|
52
|
+
params: { "start" => start, "limit" => limit }
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get non-completed badge challenges.
|
|
57
|
+
def non_completed_badge_challenges(start: 0, limit: 20)
|
|
58
|
+
connection.get(
|
|
59
|
+
"/badgechallenge-service/badgeChallenge/non-completed",
|
|
60
|
+
params: { "start" => start, "limit" => limit }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get in-progress virtual challenges.
|
|
65
|
+
def virtual_challenges(start: 0, limit: 20)
|
|
66
|
+
connection.get(
|
|
67
|
+
"/badgechallenge-service/virtualChallenge/inProgress",
|
|
68
|
+
params: { "start" => start, "limit" => limit }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# --- Personal Records ---
|
|
73
|
+
|
|
74
|
+
# Get personal records.
|
|
75
|
+
# @param display_name [String] defaults to the logged-in user
|
|
76
|
+
def personal_records(display_name = self.display_name)
|
|
77
|
+
connection.get("/personalrecord-service/personalrecord/prs/#{display_name}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- Goals ---
|
|
81
|
+
|
|
82
|
+
# Get goals by status.
|
|
83
|
+
# @param status [String] "active", "future", or "past"
|
|
84
|
+
# @param start [Integer] pagination offset
|
|
85
|
+
# @param limit [Integer]
|
|
86
|
+
def goals(status: "active", start: 0, limit: 20)
|
|
87
|
+
connection.get(
|
|
88
|
+
"/goal-service/goal/goals",
|
|
89
|
+
params: { "status" => status, "start" => start, "limit" => limit, "sortOrder" => "asc" }
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|