ts-garmin 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.
package/dist/index.js ADDED
@@ -0,0 +1,1511 @@
1
+ // src/garmin/GarminConnect.ts
2
+ import * as fs2 from "node:fs";
3
+ import * as path from "node:path";
4
+
5
+ // src/common/HttpClient.ts
6
+ import crypto from "node:crypto";
7
+ var CSRF_RE = /name="_csrf"\s+value="(.+?)"/;
8
+ var TICKET_RE = /ticket=([^"]+)"/;
9
+ var ACCOUNT_LOCKED_RE = /var statuss*=s*"([^"]*)"/;
10
+ var PAGE_TITLE_RE = /<title>([^<]*)<\/title>/;
11
+ var USER_AGENT_CONNECTMOBILE = "com.garmin.android.apps.connectmobile";
12
+ var USER_AGENT_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36";
13
+ var OAUTH_CONSUMER_URL = "https://thegarth.s3.amazonaws.com/oauth_consumer.json";
14
+ var isRefreshing = false;
15
+ var refreshSubscribers = [];
16
+ function encodeRFC3986(str) {
17
+ return encodeURIComponent(str).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
18
+ }
19
+ function generateNonce() {
20
+ return crypto.randomBytes(16).toString("hex");
21
+ }
22
+ function generateTimestamp() {
23
+ return Math.floor(Date.now() / 1000).toString();
24
+ }
25
+ function hmacSha1(baseString, key) {
26
+ return crypto.createHmac("sha1", key).update(baseString).digest("base64");
27
+ }
28
+ function buildOAuthHeader(consumer, url, method, token, extraParams) {
29
+ const oauthParams = {
30
+ oauth_consumer_key: consumer.key,
31
+ oauth_signature_method: "HMAC-SHA1",
32
+ oauth_timestamp: generateTimestamp(),
33
+ oauth_nonce: generateNonce(),
34
+ oauth_version: "1.0"
35
+ };
36
+ if (token) {
37
+ oauthParams.oauth_token = token.key;
38
+ }
39
+ const allParams = { ...oauthParams };
40
+ if (extraParams) {
41
+ Object.assign(allParams, extraParams);
42
+ }
43
+ const urlObj = new URL(url);
44
+ urlObj.searchParams.forEach((value, key) => {
45
+ allParams[key] = value;
46
+ });
47
+ const sortedParams = Object.keys(allParams).sort().map((key) => `${encodeRFC3986(key)}=${encodeRFC3986(allParams[key])}`).join("&");
48
+ const baseUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;
49
+ const baseString = `${method.toUpperCase()}&${encodeRFC3986(baseUrl)}&${encodeRFC3986(sortedParams)}`;
50
+ const signingKey = `${encodeRFC3986(consumer.secret)}&${token ? encodeRFC3986(token.secret) : ""}`;
51
+ oauthParams.oauth_signature = hmacSha1(baseString, signingKey);
52
+ const headerParams = Object.keys(oauthParams).filter((key) => key.startsWith("oauth_")).sort().map((key) => `${encodeRFC3986(key)}="${encodeRFC3986(oauthParams[key])}"`).join(", ");
53
+ return `OAuth ${headerParams}`;
54
+ }
55
+ function stringifyParams(params) {
56
+ return Object.entries(params).filter(([_, v]) => v !== undefined && v !== null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
57
+ }
58
+ function parseQueryString(str) {
59
+ const result = {};
60
+ str.split("&").forEach((pair) => {
61
+ const [key, value] = pair.split("=");
62
+ if (key)
63
+ result[decodeURIComponent(key)] = decodeURIComponent(value || "");
64
+ });
65
+ return result;
66
+ }
67
+
68
+ class HttpClient {
69
+ url;
70
+ oauth1Token;
71
+ oauth2Token;
72
+ OAUTH_CONSUMER;
73
+ cookies = new Map;
74
+ constructor(url) {
75
+ this.url = url;
76
+ }
77
+ getCookieHeader() {
78
+ return Array.from(this.cookies.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
79
+ }
80
+ extractCookies(response) {
81
+ const setCookie = response.headers.get("set-cookie");
82
+ if (setCookie) {
83
+ const cookieParts = setCookie.split(/,(?=\s*\w+=)/);
84
+ for (const part of cookieParts) {
85
+ const cookieValue = part.split(";")[0].trim();
86
+ const [name, value] = cookieValue.split("=");
87
+ if (name && value) {
88
+ this.cookies.set(name.trim(), value.trim());
89
+ }
90
+ }
91
+ }
92
+ }
93
+ async fetchOauthConsumer() {
94
+ const response = await fetch(OAUTH_CONSUMER_URL);
95
+ const data = await response.json();
96
+ this.OAUTH_CONSUMER = {
97
+ key: data.consumer_key,
98
+ secret: data.consumer_secret
99
+ };
100
+ }
101
+ async checkTokenValid() {
102
+ if (this.oauth2Token) {
103
+ const nowSeconds = Math.floor(Date.now() / 1000);
104
+ if (this.oauth2Token.expires_at < nowSeconds) {
105
+ console.error("Token expired!");
106
+ await this.refreshOauth2Token();
107
+ }
108
+ }
109
+ }
110
+ async makeRequest(url, options = {}) {
111
+ const headers = {
112
+ ...options.headers || {}
113
+ };
114
+ if (this.oauth2Token) {
115
+ headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
116
+ }
117
+ if (this.cookies.size > 0) {
118
+ headers.Cookie = this.getCookieHeader();
119
+ }
120
+ const { responseType, _retry, ...fetchOptions } = options;
121
+ const response = await fetch(url, {
122
+ ...fetchOptions,
123
+ headers
124
+ });
125
+ this.extractCookies(response);
126
+ if (response.status === 401 && this.oauth2Token && !_retry) {
127
+ if (isRefreshing) {
128
+ const token = await new Promise((resolve) => {
129
+ refreshSubscribers.push(resolve);
130
+ });
131
+ headers.Authorization = `Bearer ${token}`;
132
+ return this.makeRequest(url, { ...options, headers, _retry: true });
133
+ }
134
+ isRefreshing = true;
135
+ await this.refreshOauth2Token();
136
+ isRefreshing = false;
137
+ refreshSubscribers.forEach((sub) => sub(this.oauth2Token.access_token));
138
+ refreshSubscribers = [];
139
+ headers.Authorization = `Bearer ${this.oauth2Token.access_token}`;
140
+ return this.makeRequest(url, { ...options, headers, _retry: true });
141
+ }
142
+ if (!response.ok) {
143
+ const text = await response.text();
144
+ throw new Error(`HTTP Error: ${response.status} ${response.statusText} - ${text}`);
145
+ }
146
+ const contentType = response.headers.get("content-type");
147
+ if (contentType?.includes("application/json")) {
148
+ return response.json();
149
+ }
150
+ if (responseType === "arraybuffer") {
151
+ return response.arrayBuffer();
152
+ }
153
+ return response.text();
154
+ }
155
+ async get(url, config) {
156
+ let fullUrl = url;
157
+ if (config?.params) {
158
+ const queryString = stringifyParams(config.params);
159
+ fullUrl = queryString ? `${url}${url.includes("?") ? "&" : "?"}${queryString}` : url;
160
+ }
161
+ return this.makeRequest(fullUrl, {
162
+ method: "GET",
163
+ headers: config?.headers,
164
+ responseType: config?.responseType
165
+ });
166
+ }
167
+ async post(url, data, config) {
168
+ const headers = { ...config?.headers };
169
+ let body;
170
+ if (data !== null && data !== undefined) {
171
+ if (typeof data === "object" && !(data instanceof FormData)) {
172
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
173
+ body = JSON.stringify(data);
174
+ } else if (typeof data === "string") {
175
+ body = data;
176
+ }
177
+ }
178
+ return this.makeRequest(url, {
179
+ method: "POST",
180
+ headers,
181
+ body
182
+ });
183
+ }
184
+ async put(url, data, config) {
185
+ const headers = { ...config?.headers };
186
+ let body;
187
+ if (data !== null && data !== undefined) {
188
+ if (typeof data === "object") {
189
+ headers["Content-Type"] = headers["Content-Type"] || "application/json";
190
+ body = JSON.stringify(data);
191
+ } else if (typeof data === "string") {
192
+ body = data;
193
+ }
194
+ }
195
+ return this.makeRequest(url, {
196
+ method: "PUT",
197
+ headers,
198
+ body
199
+ });
200
+ }
201
+ async delete(url, config) {
202
+ return this.makeRequest(url, {
203
+ method: "POST",
204
+ headers: {
205
+ ...config?.headers,
206
+ "X-Http-Method-Override": "DELETE"
207
+ }
208
+ });
209
+ }
210
+ async login(username, password) {
211
+ await this.fetchOauthConsumer();
212
+ const ticket = await this.getLoginTicket(username, password);
213
+ const oauth1 = await this.getOauth1Token(ticket);
214
+ await this.exchange(oauth1);
215
+ return this;
216
+ }
217
+ async getLoginTicket(username, password) {
218
+ const step1Params = {
219
+ clientId: "GarminConnect",
220
+ locale: "en",
221
+ service: this.url.GC_MODERN
222
+ };
223
+ const step1Url = `${this.url.GARMIN_SSO_EMBED}?${stringifyParams(step1Params)}`;
224
+ const step1Response = await fetch(step1Url);
225
+ this.extractCookies(step1Response);
226
+ const step2Params = {
227
+ id: "gauth-widget",
228
+ embedWidget: "true",
229
+ locale: "en",
230
+ gauthHost: this.url.GARMIN_SSO_EMBED
231
+ };
232
+ const step2Url = `${this.url.SIGNIN_URL}?${stringifyParams(step2Params)}`;
233
+ const step2Response = await fetch(step2Url, {
234
+ headers: {
235
+ Cookie: this.getCookieHeader()
236
+ }
237
+ });
238
+ this.extractCookies(step2Response);
239
+ const step2Result = await step2Response.text();
240
+ const csrfRegResult = CSRF_RE.exec(step2Result);
241
+ if (!csrfRegResult) {
242
+ throw new Error("login - csrf not found");
243
+ }
244
+ const csrfToken = csrfRegResult[1];
245
+ const signinParams = {
246
+ id: "gauth-widget",
247
+ embedWidget: "true",
248
+ clientId: "GarminConnect",
249
+ locale: "en",
250
+ gauthHost: this.url.GARMIN_SSO_EMBED,
251
+ service: this.url.GARMIN_SSO_EMBED,
252
+ source: this.url.GARMIN_SSO_EMBED,
253
+ redirectAfterAccountLoginUrl: this.url.GARMIN_SSO_EMBED,
254
+ redirectAfterAccountCreationUrl: this.url.GARMIN_SSO_EMBED
255
+ };
256
+ const step3Url = `${this.url.SIGNIN_URL}?${stringifyParams(signinParams)}`;
257
+ const formData = new URLSearchParams;
258
+ formData.append("username", username);
259
+ formData.append("password", password);
260
+ formData.append("embed", "true");
261
+ formData.append("_csrf", csrfToken);
262
+ const step3Response = await fetch(step3Url, {
263
+ method: "POST",
264
+ headers: {
265
+ "Content-Type": "application/x-www-form-urlencoded",
266
+ Dnt: "1",
267
+ Origin: this.url.GARMIN_SSO_ORIGIN,
268
+ Referer: this.url.SIGNIN_URL,
269
+ "User-Agent": USER_AGENT_BROWSER,
270
+ Cookie: this.getCookieHeader()
271
+ },
272
+ body: formData.toString()
273
+ });
274
+ this.extractCookies(step3Response);
275
+ const step3Result = await step3Response.text();
276
+ this.handleAccountLocked(step3Result);
277
+ this.handlePageTitle(step3Result);
278
+ const ticketRegResult = TICKET_RE.exec(step3Result);
279
+ if (!ticketRegResult) {
280
+ throw new Error("login failed (Ticket not found or MFA), please check username and password");
281
+ }
282
+ return ticketRegResult[1];
283
+ }
284
+ handlePageTitle(htmlStr) {
285
+ const pageTitleRegResult = PAGE_TITLE_RE.exec(htmlStr);
286
+ if (pageTitleRegResult) {
287
+ const title = pageTitleRegResult[1];
288
+ if (title.includes("Update Phone Number")) {
289
+ throw new Error("login failed (Update Phone number), please update your phone number");
290
+ }
291
+ }
292
+ }
293
+ handleAccountLocked(htmlStr) {
294
+ const accountLockedRegResult = ACCOUNT_LOCKED_RE.exec(htmlStr);
295
+ if (accountLockedRegResult) {
296
+ const msg = accountLockedRegResult[1];
297
+ console.error(msg);
298
+ throw new Error("login failed (AccountLocked), please open connect web page to unlock your account");
299
+ }
300
+ }
301
+ async refreshOauth2Token() {
302
+ if (!this.OAUTH_CONSUMER) {
303
+ await this.fetchOauthConsumer();
304
+ }
305
+ if (!this.oauth2Token || !this.oauth1Token) {
306
+ throw new Error("No Oauth2Token or Oauth1Token");
307
+ }
308
+ await this.exchange({
309
+ token: this.oauth1Token
310
+ });
311
+ }
312
+ async getOauth1Token(ticket) {
313
+ if (!this.OAUTH_CONSUMER) {
314
+ throw new Error("No OAUTH_CONSUMER");
315
+ }
316
+ const params = {
317
+ ticket,
318
+ "login-url": this.url.GARMIN_SSO_EMBED,
319
+ "accepts-mfa-tokens": "true"
320
+ };
321
+ const url = `${this.url.OAUTH_URL}/preauthorized?${stringifyParams(params)}`;
322
+ const authHeader = buildOAuthHeader(this.OAUTH_CONSUMER, url, "GET");
323
+ const response = await fetch(url, {
324
+ headers: {
325
+ Authorization: authHeader,
326
+ "User-Agent": USER_AGENT_CONNECTMOBILE
327
+ }
328
+ });
329
+ const responseText = await response.text();
330
+ const token = parseQueryString(responseText);
331
+ this.oauth1Token = token;
332
+ return { token };
333
+ }
334
+ async exchange(oauth1) {
335
+ if (!this.OAUTH_CONSUMER) {
336
+ throw new Error("No OAUTH_CONSUMER");
337
+ }
338
+ const token = {
339
+ key: oauth1.token.oauth_token,
340
+ secret: oauth1.token.oauth_token_secret
341
+ };
342
+ const baseUrl = `${this.url.OAUTH_URL}/exchange/user/2.0`;
343
+ const authHeader = buildOAuthHeader(this.OAUTH_CONSUMER, baseUrl, "POST", token);
344
+ const response = await fetch(baseUrl, {
345
+ method: "POST",
346
+ headers: {
347
+ Authorization: authHeader,
348
+ "User-Agent": USER_AGENT_CONNECTMOBILE,
349
+ "Content-Type": "application/x-www-form-urlencoded"
350
+ }
351
+ });
352
+ const responseData = await response.json();
353
+ this.oauth2Token = this.setOauth2TokenExpiresAt(responseData);
354
+ }
355
+ setOauth2TokenExpiresAt(token) {
356
+ const now = Date.now();
357
+ const nowSeconds = Math.floor(now / 1000);
358
+ token.last_update_date = new Date(now).toISOString();
359
+ token.expires_date = new Date(now + token.expires_in * 1000).toISOString();
360
+ token.expires_at = nowSeconds + token.expires_in;
361
+ token.refresh_token_expires_at = nowSeconds + token.refresh_token_expires_in;
362
+ return token;
363
+ }
364
+ }
365
+
366
+ // src/utils.ts
367
+ import * as fs from "node:fs";
368
+ function checkIsDirectory(filePath) {
369
+ return fs.existsSync(filePath) && fs.lstatSync(filePath).isDirectory();
370
+ }
371
+ function createDirectory(directoryPath) {
372
+ fs.mkdirSync(directoryPath, { recursive: true });
373
+ }
374
+ function writeToFile(filePath, data) {
375
+ fs.writeFileSync(filePath, data);
376
+ }
377
+
378
+ // src/garmin/UrlClass.ts
379
+ class UrlClass {
380
+ domain;
381
+ GC_MODERN;
382
+ GARMIN_SSO_ORIGIN;
383
+ GC_API;
384
+ constructor(domain = "garmin.com") {
385
+ this.domain = domain;
386
+ this.GC_MODERN = `https://connect.${this.domain}/modern`;
387
+ this.GARMIN_SSO_ORIGIN = `https://sso.${this.domain}`;
388
+ this.GC_API = `https://connectapi.${this.domain}`;
389
+ }
390
+ get GARMIN_SSO() {
391
+ return `${this.GARMIN_SSO_ORIGIN}/sso`;
392
+ }
393
+ get GARMIN_SSO_EMBED() {
394
+ return `${this.GARMIN_SSO_ORIGIN}/sso/embed`;
395
+ }
396
+ get BASE_URL() {
397
+ return `${this.GC_MODERN}/proxy`;
398
+ }
399
+ get SIGNIN_URL() {
400
+ return `${this.GARMIN_SSO}/signin`;
401
+ }
402
+ get LOGIN_URL() {
403
+ return `${this.GARMIN_SSO}/login`;
404
+ }
405
+ get OAUTH_URL() {
406
+ return `${this.GC_API}/oauth-service/oauth`;
407
+ }
408
+ get USER_SETTINGS() {
409
+ return `${this.GC_API}/userprofile-service/userprofile/user-settings/`;
410
+ }
411
+ get USER_PROFILE() {
412
+ return `${this.GC_API}/userprofile-service/socialProfile`;
413
+ }
414
+ get ACTIVITIES() {
415
+ return `${this.GC_API}/activitylist-service/activities/search/activities`;
416
+ }
417
+ get ACTIVITY() {
418
+ return `${this.GC_API}/activity-service/activity/`;
419
+ }
420
+ ACTIVITY_BY_ID(activityId) {
421
+ return `${this.GC_API}/activity-service/activity/${activityId}`;
422
+ }
423
+ get STAT_ACTIVITIES() {
424
+ return `${this.GC_API}/fitnessstats-service/activity`;
425
+ }
426
+ get DOWNLOAD_ZIP() {
427
+ return `${this.GC_API}/download-service/files/activity/`;
428
+ }
429
+ get DOWNLOAD_GPX() {
430
+ return `${this.GC_API}/download-service/export/gpx/activity/`;
431
+ }
432
+ get DOWNLOAD_TCX() {
433
+ return `${this.GC_API}/download-service/export/tcx/activity/`;
434
+ }
435
+ get DOWNLOAD_KML() {
436
+ return `${this.GC_API}/download-service/export/kml/activity/`;
437
+ }
438
+ get UPLOAD() {
439
+ return `${this.GC_API}/upload-service/upload/`;
440
+ }
441
+ get IMPORT_DATA() {
442
+ return `${this.GC_API}/modern/import-data`;
443
+ }
444
+ get DAILY_STEPS() {
445
+ return `${this.GC_API}/usersummary-service/stats/steps/daily/`;
446
+ }
447
+ get DAILY_SLEEP() {
448
+ return `${this.GC_API}/sleep-service/sleep/dailySleepData`;
449
+ }
450
+ get DAILY_WEIGHT() {
451
+ return `${this.GC_API}/weight-service/weight/dayview`;
452
+ }
453
+ get UPDATE_WEIGHT() {
454
+ return `${this.GC_API}/weight-service/user-weight`;
455
+ }
456
+ get DAILY_HYDRATION() {
457
+ return `${this.GC_API}/usersummary-service/usersummary/hydration/allData`;
458
+ }
459
+ get HYDRATION_LOG() {
460
+ return `${this.GC_API}/usersummary-service/usersummary/hydration/log`;
461
+ }
462
+ get GOLF_SCORECARD_SUMMARY() {
463
+ return `${this.GC_API}/gcs-golfcommunity/api/v2/scorecard/summary`;
464
+ }
465
+ get GOLF_SCORECARD_DETAIL() {
466
+ return `${this.GC_API}/gcs-golfcommunity/api/v2/scorecard/detail`;
467
+ }
468
+ get DAILY_HEART_RATE() {
469
+ return `${this.GC_API}/wellness-service/wellness/dailyHeartRate`;
470
+ }
471
+ WORKOUT(id) {
472
+ if (id) {
473
+ return `${this.GC_API}/workout-service/workout/${id}`;
474
+ }
475
+ return `${this.GC_API}/workout-service/workout`;
476
+ }
477
+ get WORKOUTS() {
478
+ return `${this.GC_API}/workout-service/workouts`;
479
+ }
480
+ GEAR_OF_ACTIVITY(activityId) {
481
+ return `${this.GC_API}/gear-service/gear/filterGear?activityId=${activityId}`;
482
+ }
483
+ GEAR(userProfilePk, availableGearDate) {
484
+ return `${this.GC_API}/gear-service/gear/filterGear?userProfilePk=${userProfilePk}${availableGearDate ? `&availableGearDate=${availableGearDate}` : ""}`;
485
+ }
486
+ LINK_GEAR_TO_ACTIVITY(activityId, gearId) {
487
+ return `${this.GC_API}/gear-service/gear/link/${gearId}/activity/${activityId}`;
488
+ }
489
+ UNLINK_GEAR_FROM_ACTIVITY(activityId, gearId) {
490
+ return `${this.GC_API}/gear-service/gear/unlink/${gearId}/activity/${activityId}`;
491
+ }
492
+ WORKOUTS_LIST(start = 1, limit = 999, myWorkoutsOnly = true, sharedWorkoutsOnly = false, orderBy = "UPDATE_DATE", orderSeq = "DESC", includeAtp = false) {
493
+ return `${this.GC_API}/workout-service/workouts?start=${start}&limit=${limit}&myWorkoutsOnly=${myWorkoutsOnly}&sharedWorkoutsOnly=${sharedWorkoutsOnly}&orderBy=${orderBy}&orderSeq=${orderSeq}&includeAtp=${includeAtp}`;
494
+ }
495
+ SCHEDULE_WORKOUT(workoutId) {
496
+ return `${this.GC_API}/workout-service/schedule/${workoutId}`;
497
+ }
498
+ get IMPORT_GPX_FILE() {
499
+ return `${this.GC_API}/course-service/course/import`;
500
+ }
501
+ EXPORT_COURSE_GPX_FILE(courseId) {
502
+ return `${this.GC_API}/course-service/course/gpx/${courseId}`;
503
+ }
504
+ get CREATE_COURSE_GPX_FILE() {
505
+ return `${this.GC_API}/course-service/course`;
506
+ }
507
+ UPDATE_COURSE_GPX_FILE(courseId) {
508
+ return `${this.GC_API}/course-service/course/${courseId}`;
509
+ }
510
+ get LIST_COURSES() {
511
+ return `${this.GC_API}/web-gateway/course/owner/`;
512
+ }
513
+ CALENDAR_YEAR(year) {
514
+ return `${this.GC_API}/calendar-service/year/${year}`;
515
+ }
516
+ CALENDAR_MONTH(year, month) {
517
+ return `${this.GC_API}/calendar-service/year/${year}/month/${month}`;
518
+ }
519
+ CALENDAR_WEEK(year, month, day, firstDateOfWeek = 1) {
520
+ return `${this.GC_API}/calendar-service/year/${year}/month/${month}/day/${day}/start/${firstDateOfWeek}`;
521
+ }
522
+ DAILY_STRESS(date) {
523
+ return `${this.GC_API}/wellness-service/wellness/dailyStress/${date}`;
524
+ }
525
+ HRV_DATA(date) {
526
+ return `${this.GC_API}/hrv-service/hrv/${date}`;
527
+ }
528
+ TRAINING_READINESS(date) {
529
+ return `${this.GC_API}/metrics-service/metrics/trainingreadiness/${date}`;
530
+ }
531
+ BODY_BATTERY(date) {
532
+ return `${this.GC_API}/wellness-service/wellness/bodyBattery?date=${date}`;
533
+ }
534
+ DAILY_SUMMARY(userId, date) {
535
+ return `${this.GC_API}/usersummary-service/usersummary/daily/${userId}?calendarDate=${date}`;
536
+ }
537
+ }
538
+
539
+ // src/garmin/types/activity.ts
540
+ var ActivityType;
541
+ ((ActivityType2) => {
542
+ ActivityType2["Cycling"] = "cycling";
543
+ ActivityType2["FitnessEquipment"] = "fitness_equipment";
544
+ ActivityType2["Walking"] = "walking";
545
+ ActivityType2["Hiking"] = "hiking";
546
+ ActivityType2["Other"] = "other";
547
+ ActivityType2["WaterSport"] = "water_sports";
548
+ ActivityType2["Running"] = "street_running";
549
+ })(ActivityType ||= {});
550
+ var ActivitySubType;
551
+ ((ActivitySubType2) => {
552
+ ActivitySubType2["IndoorCardio"] = "indoor_cardio";
553
+ ActivitySubType2["StrengthTraining"] = "strength_training";
554
+ ActivitySubType2["HIIT"] = "hiit";
555
+ ActivitySubType2["Yoga"] = "yoga";
556
+ ActivitySubType2["IndoorCycling"] = "indoor_cycling";
557
+ ActivitySubType2["Breathwork"] = "breathwork";
558
+ ActivitySubType2["Surfing"] = "surfing";
559
+ ActivitySubType2["StreetRunning"] = "street_running";
560
+ ActivitySubType2["TrailRunning"] = "trail_running";
561
+ ActivitySubType2["IndoorRunning"] = "indoor_running";
562
+ })(ActivitySubType ||= {});
563
+ // src/garmin/types/gpx.ts
564
+ var GpxActivityType;
565
+ ((GpxActivityType2) => {
566
+ GpxActivityType2[GpxActivityType2["Running"] = 1] = "Running";
567
+ GpxActivityType2[GpxActivityType2["Running_Road"] = 1] = "Running_Road";
568
+ GpxActivityType2[GpxActivityType2["Running_Trail"] = 1] = "Running_Trail";
569
+ GpxActivityType2[GpxActivityType2["Cycling"] = 10] = "Cycling";
570
+ GpxActivityType2[GpxActivityType2["Cycling_Road"] = 10] = "Cycling_Road";
571
+ GpxActivityType2[GpxActivityType2["Cycling_Gravel"] = 143] = "Cycling_Gravel";
572
+ GpxActivityType2[GpxActivityType2["Hiking"] = 3] = "Hiking";
573
+ GpxActivityType2[GpxActivityType2["Other"] = 4] = "Other";
574
+ })(GpxActivityType ||= {});
575
+
576
+ // src/garmin/types/index.ts
577
+ var ExportFileType;
578
+ ((ExportFileType2) => {
579
+ ExportFileType2["tcx"] = "tcx";
580
+ ExportFileType2["gpx"] = "gpx";
581
+ ExportFileType2["kml"] = "kml";
582
+ ExportFileType2["zip"] = "zip";
583
+ })(ExportFileType ||= {});
584
+ var UploadFileType;
585
+ ((UploadFileType2) => {
586
+ UploadFileType2["tcx"] = "tcx";
587
+ UploadFileType2["gpx"] = "gpx";
588
+ UploadFileType2["fit"] = "fit";
589
+ })(UploadFileType ||= {});
590
+
591
+ // src/garmin/common/DateUtils.ts
592
+ function toDateString(date) {
593
+ const offset = date.getTimezoneOffset();
594
+ const offsetDate = new Date(date.getTime() - offset * 60 * 1000);
595
+ const [dateString] = offsetDate.toISOString().split("T");
596
+ return dateString;
597
+ }
598
+ function calculateTimeDifference(sleepStartTimestampGMT, sleepEndTimestampGMT) {
599
+ const timeDifferenceInSeconds = (sleepEndTimestampGMT - sleepStartTimestampGMT) / 1000;
600
+ const hours = Math.floor(timeDifferenceInSeconds / 3600);
601
+ const minutes = Math.floor(timeDifferenceInSeconds % 3600 / 60);
602
+ return { hours, minutes };
603
+ }
604
+ function getLocalTimestamp(date, timezone) {
605
+ const localTimestampISO = date.toISOString().substring(0, 23);
606
+ const localTimestamp = new Date(localTimestampISO).toLocaleString("en-US", {
607
+ timeZone: timezone,
608
+ hour12: false
609
+ });
610
+ const formattedLocalTimestamp = new Date(localTimestamp).toISOString().substring(0, 23);
611
+ return formattedLocalTimestamp;
612
+ }
613
+
614
+ // src/garmin/common/WeightUtils.ts
615
+ function gramsToPounds(weightInGrams) {
616
+ const gramsPerPound = 453.592;
617
+ return weightInGrams / gramsPerPound;
618
+ }
619
+
620
+ // src/garmin/common/HydrationUtils.ts
621
+ function convertMLToOunces(valueInML) {
622
+ const conversionFactor = 0.033814;
623
+ return valueInML * conversionFactor;
624
+ }
625
+ function convertOuncesToML(ounces) {
626
+ const ouncesToMillilitersConversionFactor = 29.5735;
627
+ return ounces * ouncesToMillilitersConversionFactor;
628
+ }
629
+
630
+ // src/garmin/common/GpxUtils.ts
631
+ function courseRequestTemplate(activityType, courseName, geoPoints, coursePoints = []) {
632
+ return {
633
+ activityTypePk: activityType,
634
+ hasTurnDetectionDisabled: false,
635
+ geoPoints,
636
+ courseLines: [],
637
+ coursePoints,
638
+ startPoint: geoPoints[0],
639
+ elapsedSeconds: null,
640
+ openStreetMap: false,
641
+ coordinateSystem: "WGS84",
642
+ rulePK: 2,
643
+ courseName,
644
+ matchedToSegments: false,
645
+ includeLaps: false,
646
+ hasPaceBand: false,
647
+ hasPowerGuide: false,
648
+ favorite: false,
649
+ speedMeterPerSecond: null,
650
+ sourceTypeId: 3
651
+ };
652
+ }
653
+
654
+ // src/garmin/GarminConnect.ts
655
+ class GarminConnect {
656
+ client;
657
+ credentials;
658
+ listeners;
659
+ url;
660
+ constructor(credentials, domain = "garmin.com") {
661
+ if (!credentials) {
662
+ throw new Error("Missing credentials");
663
+ }
664
+ this.credentials = credentials;
665
+ this.url = new UrlClass(domain);
666
+ this.client = new HttpClient(this.url);
667
+ this.listeners = {};
668
+ }
669
+ async login(username, password) {
670
+ if (username && password) {
671
+ this.credentials.username = username;
672
+ this.credentials.password = password;
673
+ }
674
+ await this.client.login(this.credentials.username, this.credentials.password);
675
+ return this;
676
+ }
677
+ exportTokenToFile(dirPath) {
678
+ if (!checkIsDirectory(dirPath)) {
679
+ createDirectory(dirPath);
680
+ }
681
+ if (this.client.oauth1Token) {
682
+ writeToFile(path.join(dirPath, "oauth1_token.json"), JSON.stringify(this.client.oauth1Token));
683
+ }
684
+ if (this.client.oauth2Token) {
685
+ writeToFile(path.join(dirPath, "oauth2_token.json"), JSON.stringify(this.client.oauth2Token));
686
+ }
687
+ }
688
+ loadTokenByFile(dirPath) {
689
+ if (!checkIsDirectory(dirPath)) {
690
+ throw new Error(`loadTokenByFile: Directory not found: ${dirPath}`);
691
+ }
692
+ const oauth1Data = fs2.readFileSync(path.join(dirPath, "oauth1_token.json"), "utf-8");
693
+ const oauth1 = JSON.parse(oauth1Data);
694
+ this.client.oauth1Token = oauth1;
695
+ const oauth2Data = fs2.readFileSync(path.join(dirPath, "oauth2_token.json"), "utf-8");
696
+ const oauth2 = JSON.parse(oauth2Data);
697
+ this.client.oauth2Token = oauth2;
698
+ }
699
+ exportToken() {
700
+ if (!this.client.oauth1Token || !this.client.oauth2Token) {
701
+ throw new Error("exportToken: Token not found");
702
+ }
703
+ return {
704
+ oauth1: this.client.oauth1Token,
705
+ oauth2: this.client.oauth2Token
706
+ };
707
+ }
708
+ loadToken(oauth1, oauth2) {
709
+ this.client.oauth1Token = oauth1;
710
+ this.client.oauth2Token = oauth2;
711
+ }
712
+ async getUserSettings() {
713
+ return this.client.get(this.url.USER_SETTINGS);
714
+ }
715
+ async getUserProfile() {
716
+ return this.client.get(this.url.USER_PROFILE);
717
+ }
718
+ async getActivities(start, limit, activityType, subActivityType) {
719
+ return this.client.get(this.url.ACTIVITIES, {
720
+ params: { start, limit, activityType, subActivityType }
721
+ });
722
+ }
723
+ async getActivity(activity2) {
724
+ if (!activity2.activityId)
725
+ throw new Error("Missing activityId");
726
+ return this.client.get(`${this.url.ACTIVITY}${activity2.activityId}`);
727
+ }
728
+ async countActivities() {
729
+ const now = new Date;
730
+ const endDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
731
+ return this.client.get(this.url.STAT_ACTIVITIES, {
732
+ params: {
733
+ aggregation: "lifetime",
734
+ startDate: "1970-01-01",
735
+ endDate,
736
+ metric: "duration"
737
+ }
738
+ });
739
+ }
740
+ async downloadOriginalActivityData(activity2, dir, type = "zip") {
741
+ if (!activity2.activityId)
742
+ throw new Error("Missing activityId");
743
+ if (!checkIsDirectory(dir)) {
744
+ createDirectory(dir);
745
+ }
746
+ let fileBuffer;
747
+ let url;
748
+ if (type === "tcx") {
749
+ url = `${this.url.DOWNLOAD_TCX}${activity2.activityId}`;
750
+ } else if (type === "gpx") {
751
+ url = `${this.url.DOWNLOAD_GPX}${activity2.activityId}`;
752
+ } else if (type === "kml") {
753
+ url = `${this.url.DOWNLOAD_KML}${activity2.activityId}`;
754
+ } else if (type === "zip") {
755
+ url = `${this.url.DOWNLOAD_ZIP}${activity2.activityId}`;
756
+ } else {
757
+ throw new Error(`downloadOriginalActivityData - Invalid type: ${type}`);
758
+ }
759
+ fileBuffer = await this.client.get(url, { responseType: "arraybuffer" });
760
+ writeToFile(path.join(dir, `${activity2.activityId}.${type}`), Buffer.from(fileBuffer));
761
+ }
762
+ async uploadActivity(file, format = "fit") {
763
+ const detectedFormat = (format || path.extname(file))?.toLowerCase();
764
+ if (!Object.values(UploadFileType).includes(detectedFormat)) {
765
+ throw new Error(`uploadActivity - Invalid format: ${format}`);
766
+ }
767
+ const fileBuffer = fs2.readFileSync(file);
768
+ const formData = new FormData;
769
+ formData.append("userfile", new Blob([fileBuffer]), path.basename(file));
770
+ const response = await fetch(`${this.url.UPLOAD}.${format}`, {
771
+ method: "POST",
772
+ headers: {
773
+ Authorization: `Bearer ${this.client.oauth2Token?.access_token}`
774
+ },
775
+ body: formData
776
+ });
777
+ return response.json();
778
+ }
779
+ async deleteActivity(activity2) {
780
+ if (!activity2.activityId)
781
+ throw new Error("Missing activityId");
782
+ await this.client.delete(`${this.url.ACTIVITY}${activity2.activityId}`);
783
+ }
784
+ async getWorkouts(start, limit) {
785
+ return this.client.get(this.url.WORKOUTS, {
786
+ params: { start, limit }
787
+ });
788
+ }
789
+ async getWorkoutDetail(workout2) {
790
+ if (!workout2.workoutId)
791
+ throw new Error("Missing workoutId");
792
+ return this.client.get(this.url.WORKOUT(workout2.workoutId));
793
+ }
794
+ async createWorkout(workout2) {
795
+ return this.client.post(this.url.WORKOUT(), workout2);
796
+ }
797
+ async deleteWorkout(workout2) {
798
+ if (!workout2.workoutId)
799
+ throw new Error("Missing workout");
800
+ return this.client.delete(this.url.WORKOUT(workout2.workoutId));
801
+ }
802
+ async scheduleWorkout(workout2, scheduleDate) {
803
+ return this.client.post(this.url.SCHEDULE_WORKOUT(Number.parseInt(workout2.workoutId)), { date: scheduleDate });
804
+ }
805
+ async getSteps(date = new Date) {
806
+ const dateString = toDateString(date);
807
+ const days = await this.client.get(`${this.url.DAILY_STEPS}${dateString}/${dateString}`);
808
+ const dayStats = days.find(({ calendarDate }) => calendarDate === dateString);
809
+ if (!dayStats) {
810
+ throw new Error("Can't find daily steps for this date.");
811
+ }
812
+ return dayStats.totalSteps;
813
+ }
814
+ async getSleepData(date = new Date) {
815
+ try {
816
+ const dateString = toDateString(date);
817
+ const sleepData = await this.client.get(this.url.DAILY_SLEEP, {
818
+ params: { date: dateString }
819
+ });
820
+ if (!sleepData) {
821
+ throw new Error("Invalid or empty sleep data response.");
822
+ }
823
+ return sleepData;
824
+ } catch (error) {
825
+ throw new Error(`Error in getSleepData: ${error instanceof Error ? error.message : error}`);
826
+ }
827
+ }
828
+ async getSleepDuration(date = new Date) {
829
+ try {
830
+ const sleepData = await this.getSleepData(date);
831
+ if (!sleepData || !sleepData.dailySleepDTO || sleepData.dailySleepDTO.sleepStartTimestampGMT === undefined || sleepData.dailySleepDTO.sleepEndTimestampGMT === undefined) {
832
+ throw new Error("Invalid or missing sleep data for the specified date.");
833
+ }
834
+ const { hours, minutes } = calculateTimeDifference(sleepData.dailySleepDTO.sleepStartTimestampGMT, sleepData.dailySleepDTO.sleepEndTimestampGMT);
835
+ return { hours, minutes };
836
+ } catch (error) {
837
+ throw new Error(`Error in getSleepDuration: ${error instanceof Error ? error.message : error}`);
838
+ }
839
+ }
840
+ async getDailyWeightData(date = new Date) {
841
+ try {
842
+ const dateString = toDateString(date);
843
+ const weightData = await this.client.get(`${this.url.DAILY_WEIGHT}/${dateString}`);
844
+ if (!weightData) {
845
+ throw new Error("Invalid or empty weight data response.");
846
+ }
847
+ return weightData;
848
+ } catch (error) {
849
+ throw new Error(`Error in getDailyWeightData: ${error instanceof Error ? error.message : error}`);
850
+ }
851
+ }
852
+ async getDailyWeightInPounds(date = new Date) {
853
+ const weightData = await this.getDailyWeightData(date);
854
+ if (weightData.totalAverage && typeof weightData.totalAverage.weight === "number") {
855
+ return gramsToPounds(weightData.totalAverage.weight);
856
+ } else {
857
+ throw new Error("Can't find valid daily weight for this date.");
858
+ }
859
+ }
860
+ async getDailyHydration(date = new Date) {
861
+ try {
862
+ const dateString = toDateString(date);
863
+ const hydrationData = await this.client.get(`${this.url.DAILY_HYDRATION}/${dateString}`);
864
+ if (!hydrationData || !hydrationData.valueInML) {
865
+ throw new Error("Invalid or empty hydration data response.");
866
+ }
867
+ return convertMLToOunces(hydrationData.valueInML);
868
+ } catch (error) {
869
+ throw new Error(`Error in getDailyHydration: ${error instanceof Error ? error.message : error}`);
870
+ }
871
+ }
872
+ async updateWeight(date = new Date, lbs, timezone) {
873
+ try {
874
+ const weightData = await this.client.post(this.url.UPDATE_WEIGHT, {
875
+ dateTimestamp: getLocalTimestamp(date, timezone),
876
+ gmtTimestamp: date.toISOString().substring(0, 23),
877
+ unitKey: "lbs",
878
+ value: lbs
879
+ });
880
+ return weightData;
881
+ } catch (error) {
882
+ throw new Error(`Error in updateWeight: ${error instanceof Error ? error.message : error}`);
883
+ }
884
+ }
885
+ async updateHydrationLogOunces(date = new Date, valueInOz) {
886
+ try {
887
+ const dateString = toDateString(date);
888
+ const hydrationData = await this.client.put(this.url.HYDRATION_LOG, {
889
+ calendarDate: dateString,
890
+ valueInML: convertOuncesToML(valueInOz),
891
+ userProfileId: (await this.getUserProfile()).profileId,
892
+ timestampLocal: date.toISOString().substring(0, 23)
893
+ });
894
+ return hydrationData;
895
+ } catch (error) {
896
+ throw new Error(`Error in updateHydrationLogOunces: ${error instanceof Error ? error.message : error}`);
897
+ }
898
+ }
899
+ async getGolfSummary() {
900
+ try {
901
+ const golfSummary = await this.client.get(this.url.GOLF_SCORECARD_SUMMARY);
902
+ if (!golfSummary) {
903
+ throw new Error("Invalid or empty golf summary data response.");
904
+ }
905
+ return golfSummary;
906
+ } catch (error) {
907
+ throw new Error(`Error in getGolfSummary: ${error instanceof Error ? error.message : error}`);
908
+ }
909
+ }
910
+ async getGolfScorecard(scorecardId) {
911
+ try {
912
+ const golfScorecard = await this.client.get(this.url.GOLF_SCORECARD_DETAIL, {
913
+ params: { "scorecard-ids": scorecardId }
914
+ });
915
+ if (!golfScorecard) {
916
+ throw new Error("Invalid or empty golf scorecard data response.");
917
+ }
918
+ return golfScorecard;
919
+ } catch (error) {
920
+ throw new Error(`Error in getGolfScorecard: ${error instanceof Error ? error.message : error}`);
921
+ }
922
+ }
923
+ async getHeartRate(date = new Date) {
924
+ try {
925
+ const dateString = toDateString(date);
926
+ const heartRate = await this.client.get(this.url.DAILY_HEART_RATE, {
927
+ params: { date: dateString }
928
+ });
929
+ return heartRate;
930
+ } catch (error) {
931
+ throw new Error(`Error in getHeartRate: ${error instanceof Error ? error.message : error}`);
932
+ }
933
+ }
934
+ async getStressData(date = new Date) {
935
+ try {
936
+ const dateString = toDateString(date);
937
+ const stressData = await this.client.get(this.url.DAILY_STRESS(dateString));
938
+ if (!stressData) {
939
+ throw new Error("Invalid or empty stress data response.");
940
+ }
941
+ return stressData;
942
+ } catch (error) {
943
+ throw new Error(`Error in getStressData: ${error instanceof Error ? error.message : error}`);
944
+ }
945
+ }
946
+ async getHrvData(date = new Date) {
947
+ try {
948
+ const dateString = toDateString(date);
949
+ const hrvData = await this.client.get(this.url.HRV_DATA(dateString));
950
+ if (!hrvData) {
951
+ throw new Error("Invalid or empty HRV data response.");
952
+ }
953
+ return hrvData;
954
+ } catch (error) {
955
+ throw new Error(`Error in getHrvData: ${error instanceof Error ? error.message : error}`);
956
+ }
957
+ }
958
+ async getTrainingReadiness(date = new Date) {
959
+ try {
960
+ const dateString = toDateString(date);
961
+ const trainingReadiness = await this.client.get(this.url.TRAINING_READINESS(dateString));
962
+ if (!trainingReadiness) {
963
+ throw new Error("Invalid or empty training readiness data response.");
964
+ }
965
+ return trainingReadiness;
966
+ } catch (error) {
967
+ throw new Error(`Error in getTrainingReadiness: ${error instanceof Error ? error.message : error}`);
968
+ }
969
+ }
970
+ async getBodyBattery(date = new Date) {
971
+ try {
972
+ const dateString = toDateString(date);
973
+ const bodyBattery = await this.client.get(this.url.BODY_BATTERY(dateString));
974
+ if (!bodyBattery) {
975
+ throw new Error("Invalid or empty body battery data response.");
976
+ }
977
+ return bodyBattery;
978
+ } catch (error) {
979
+ throw new Error(`Error in getBodyBattery: ${error instanceof Error ? error.message : error}`);
980
+ }
981
+ }
982
+ async getDailySummary(date = new Date) {
983
+ try {
984
+ const dateString = toDateString(date);
985
+ const profile = await this.getUserProfile();
986
+ const summary = await this.client.get(this.url.DAILY_SUMMARY(profile.profileId, dateString));
987
+ if (!summary) {
988
+ throw new Error("Invalid or empty daily summary response.");
989
+ }
990
+ return summary;
991
+ } catch (error) {
992
+ throw new Error(`Error in getDailySummary: ${error instanceof Error ? error.message : error}`);
993
+ }
994
+ }
995
+ async getGear(availableGearDate) {
996
+ const id = (await this.getUserProfile()).profileId;
997
+ return this.client.get(this.url.GEAR(id, availableGearDate));
998
+ }
999
+ async getGearsForActivity(activityId) {
1000
+ return this.client.get(this.url.GEAR_OF_ACTIVITY(activityId));
1001
+ }
1002
+ async linkGearToActivity(activityId, gearId) {
1003
+ return this.client.put(this.url.LINK_GEAR_TO_ACTIVITY(activityId, gearId), {});
1004
+ }
1005
+ async unlinkGearFromActivity(activityId, gearId) {
1006
+ return this.client.put(this.url.UNLINK_GEAR_FROM_ACTIVITY(activityId, gearId), {});
1007
+ }
1008
+ async workouts() {
1009
+ return this.client.get(this.url.WORKOUTS_LIST());
1010
+ }
1011
+ async importGpx(fileName, fileContent) {
1012
+ const formData = new FormData;
1013
+ formData.append("file", new Blob([fileContent], { type: "application/octet-stream" }), fileName);
1014
+ const response = await fetch(this.url.IMPORT_GPX_FILE, {
1015
+ method: "POST",
1016
+ headers: {
1017
+ Authorization: `Bearer ${this.client.oauth2Token?.access_token}`
1018
+ },
1019
+ body: formData
1020
+ });
1021
+ return response.json();
1022
+ }
1023
+ async createCourse(activityType, courseName, geoPoints, coursePoints = []) {
1024
+ return this.client.post(this.url.CREATE_COURSE_GPX_FILE, courseRequestTemplate(activityType, courseName, geoPoints, coursePoints));
1025
+ }
1026
+ async listCourses() {
1027
+ return this.client.get(this.url.LIST_COURSES);
1028
+ }
1029
+ async exportCourseAsGpx(courseId) {
1030
+ return this.client.get(this.url.EXPORT_COURSE_GPX_FILE(courseId), {
1031
+ responseType: "text"
1032
+ });
1033
+ }
1034
+ async getYearCalendarEvents(year) {
1035
+ return this.client.get(this.url.CALENDAR_YEAR(year));
1036
+ }
1037
+ async getMonthCalendarEvents(year, month) {
1038
+ return this.client.get(this.url.CALENDAR_MONTH(year, month));
1039
+ }
1040
+ async getWeekCalendarEvents(year, month, day, firstDayOfWeek) {
1041
+ return this.client.get(this.url.CALENDAR_WEEK(year, month, day, firstDayOfWeek));
1042
+ }
1043
+ async renameActivity(activityId, newName) {
1044
+ if (!activityId)
1045
+ throw new Error("Missing activityId");
1046
+ if (!newName)
1047
+ throw new Error("Missing newName");
1048
+ await this.client.put(this.url.ACTIVITY_BY_ID(activityId), {
1049
+ activityName: newName
1050
+ });
1051
+ }
1052
+ async get(url, data) {
1053
+ const response = await this.client.get(url, data);
1054
+ return response;
1055
+ }
1056
+ async post(url, _data) {
1057
+ const response = await this.client.post(url, _data, {});
1058
+ return response;
1059
+ }
1060
+ async put(url, _data) {
1061
+ const response = await this.client.put(url, _data, {});
1062
+ return response;
1063
+ }
1064
+ }
1065
+ // src/garmin/workout-builder/duration.ts
1066
+ class Duration {
1067
+ }
1068
+
1069
+ class NoDuration extends Duration {
1070
+ build() {
1071
+ throw new Error("Please provide a duration for this step (Other than NoDuration).");
1072
+ }
1073
+ }
1074
+
1075
+ class TimeDuration extends Duration {
1076
+ durationInSeconds;
1077
+ constructor(durationInSeconds) {
1078
+ super();
1079
+ this.durationInSeconds = durationInSeconds;
1080
+ }
1081
+ static hhmmss(h, m, s) {
1082
+ return new TimeDuration(h * 3600 + m * 60 + s);
1083
+ }
1084
+ static fromHours(h) {
1085
+ return new TimeDuration(h * 3600);
1086
+ }
1087
+ static fromMinutes(m) {
1088
+ return new TimeDuration(m * 60);
1089
+ }
1090
+ static fromSeconds(s) {
1091
+ return new TimeDuration(s);
1092
+ }
1093
+ build() {
1094
+ return {
1095
+ endCondition: {
1096
+ conditionTypeId: 2,
1097
+ conditionTypeKey: "time",
1098
+ displayable: true,
1099
+ displayOrder: 1
1100
+ },
1101
+ endConditionCompare: null,
1102
+ endConditionValue: this.durationInSeconds,
1103
+ endConditionZone: null
1104
+ };
1105
+ }
1106
+ }
1107
+
1108
+ class DistanceDuration extends Duration {
1109
+ meters;
1110
+ constructor(meters) {
1111
+ super();
1112
+ this.meters = meters;
1113
+ }
1114
+ static fromKilometers(km) {
1115
+ return new DistanceDuration(km * 1000);
1116
+ }
1117
+ static fromMeters(m) {
1118
+ return new DistanceDuration(m);
1119
+ }
1120
+ build() {
1121
+ return {
1122
+ endCondition: {
1123
+ conditionTypeId: 3,
1124
+ conditionTypeKey: "distance",
1125
+ displayOrder: 3,
1126
+ displayable: true
1127
+ },
1128
+ endConditionValue: this.meters,
1129
+ preferredEndConditionUnit: {
1130
+ unitKey: "kilometer"
1131
+ },
1132
+ endConditionCompare: null
1133
+ };
1134
+ }
1135
+ }
1136
+
1137
+ class LapPressDuration extends Duration {
1138
+ build() {
1139
+ return {
1140
+ endCondition: {
1141
+ conditionTypeId: 1,
1142
+ conditionTypeKey: "lap.button",
1143
+ displayOrder: 1,
1144
+ displayable: true
1145
+ },
1146
+ endConditionValue: null,
1147
+ preferredEndConditionUnit: null,
1148
+ endConditionCompare: null
1149
+ };
1150
+ }
1151
+ }
1152
+
1153
+ class CaloriesDuration extends Duration {
1154
+ calories;
1155
+ constructor(calories) {
1156
+ super();
1157
+ this.calories = calories;
1158
+ }
1159
+ build() {
1160
+ return {
1161
+ endCondition: {
1162
+ conditionTypeId: 4,
1163
+ conditionTypeKey: "calories",
1164
+ displayOrder: 4,
1165
+ displayable: true
1166
+ },
1167
+ endConditionValue: this.calories
1168
+ };
1169
+ }
1170
+ }
1171
+
1172
+ class HeartRateDuration extends Duration {
1173
+ bpm;
1174
+ compare;
1175
+ constructor(bpm, compare) {
1176
+ super();
1177
+ this.bpm = bpm;
1178
+ this.compare = compare;
1179
+ }
1180
+ static greaterThan(bpm) {
1181
+ return new HeartRateDuration(bpm, "gt");
1182
+ }
1183
+ static lessThan(bpm) {
1184
+ return new HeartRateDuration(bpm, "lt");
1185
+ }
1186
+ build() {
1187
+ return {
1188
+ endCondition: {
1189
+ conditionTypeId: 6,
1190
+ conditionTypeKey: "heart.rate",
1191
+ displayOrder: 6,
1192
+ displayable: true
1193
+ },
1194
+ endConditionValue: this.bpm,
1195
+ endConditionCompare: this.compare
1196
+ };
1197
+ }
1198
+ }
1199
+ // src/garmin/workout-builder/target.ts
1200
+ class Target {
1201
+ }
1202
+
1203
+ class NoTarget extends Target {
1204
+ build() {
1205
+ return {
1206
+ targetType: {
1207
+ workoutTargetTypeId: 1,
1208
+ workoutTargetTypeKey: "no.target",
1209
+ displayOrder: 1
1210
+ }
1211
+ };
1212
+ }
1213
+ }
1214
+
1215
+ class PaceTarget extends Target {
1216
+ minPace;
1217
+ maxPace;
1218
+ constructor(minPace, maxPace) {
1219
+ super();
1220
+ this.minPace = minPace;
1221
+ this.maxPace = maxPace;
1222
+ }
1223
+ static pace(paceMinutes, paceSeconds, margin = 0) {
1224
+ const minPace = paceMinutes * 60 + paceSeconds + margin;
1225
+ const maxPace = paceMinutes * 60 + paceSeconds - margin;
1226
+ return new PaceTarget(1000 / minPace, 1000 / maxPace);
1227
+ }
1228
+ build() {
1229
+ return {
1230
+ targetType: {
1231
+ workoutTargetTypeId: 6,
1232
+ workoutTargetTypeKey: "pace.zone",
1233
+ displayOrder: 6
1234
+ },
1235
+ targetValueOne: this.minPace,
1236
+ targetValueTwo: this.maxPace,
1237
+ targetValueUnit: null
1238
+ };
1239
+ }
1240
+ }
1241
+
1242
+ class CadenceTarget extends Target {
1243
+ minCadence;
1244
+ maxCadence;
1245
+ constructor(minCadence, maxCadence) {
1246
+ super();
1247
+ this.minCadence = minCadence;
1248
+ this.maxCadence = maxCadence;
1249
+ }
1250
+ static cadence(cadence, margin) {
1251
+ return new CadenceTarget(cadence - margin, cadence + margin);
1252
+ }
1253
+ build() {
1254
+ return {
1255
+ targetType: {
1256
+ workoutTargetTypeId: 3,
1257
+ workoutTargetTypeKey: "cadence",
1258
+ displayOrder: 3
1259
+ },
1260
+ targetValueOne: this.minCadence,
1261
+ targetValueTwo: this.maxCadence,
1262
+ targetValueUnit: null
1263
+ };
1264
+ }
1265
+ }
1266
+
1267
+ class HrmZoneTarget extends Target {
1268
+ hrmZone;
1269
+ constructor(hrmZone) {
1270
+ super();
1271
+ this.hrmZone = hrmZone;
1272
+ }
1273
+ build() {
1274
+ return {
1275
+ targetType: {
1276
+ workoutTargetTypeId: 4,
1277
+ workoutTargetTypeKey: "heart.rate.zone",
1278
+ displayOrder: 4
1279
+ },
1280
+ zoneNumber: this.hrmZone
1281
+ };
1282
+ }
1283
+ }
1284
+
1285
+ class HrmTarget extends Target {
1286
+ minHrm;
1287
+ maxHrm;
1288
+ constructor(minHrm, maxHrm) {
1289
+ super();
1290
+ this.minHrm = minHrm;
1291
+ this.maxHrm = maxHrm;
1292
+ }
1293
+ static hrm(hrm, margin) {
1294
+ return new HrmTarget(hrm - margin, hrm + margin);
1295
+ }
1296
+ build() {
1297
+ return {
1298
+ targetType: {
1299
+ workoutTargetTypeId: 4,
1300
+ workoutTargetTypeKey: "heart.rate.zone",
1301
+ displayOrder: 4
1302
+ },
1303
+ targetValueOne: this.minHrm,
1304
+ targetValueTwo: this.maxHrm,
1305
+ targetValueUnit: null
1306
+ };
1307
+ }
1308
+ }
1309
+
1310
+ class PowerZoneTarget extends Target {
1311
+ powerZone;
1312
+ constructor(powerZone) {
1313
+ super();
1314
+ this.powerZone = powerZone;
1315
+ }
1316
+ build() {
1317
+ return {
1318
+ targetType: {
1319
+ workoutTargetTypeId: 2,
1320
+ workoutTargetTypeKey: "power.zone",
1321
+ displayOrder: 2
1322
+ },
1323
+ zoneNumber: this.powerZone
1324
+ };
1325
+ }
1326
+ }
1327
+
1328
+ class PowerZone extends Target {
1329
+ minPower;
1330
+ maxPower;
1331
+ constructor(minPower, maxPower) {
1332
+ super();
1333
+ this.minPower = minPower;
1334
+ this.maxPower = maxPower;
1335
+ }
1336
+ static power(power, margin) {
1337
+ return new PowerZone(power - margin, power + margin);
1338
+ }
1339
+ build() {
1340
+ return {
1341
+ targetType: {
1342
+ workoutTargetTypeId: 2,
1343
+ workoutTargetTypeKey: "power.zone",
1344
+ displayOrder: 2
1345
+ },
1346
+ targetValueOne: this.minPower,
1347
+ targetValueTwo: this.maxPower
1348
+ };
1349
+ }
1350
+ }
1351
+
1352
+ // src/garmin/workout-builder/step.ts
1353
+ class Step {
1354
+ stepType;
1355
+ duration;
1356
+ target;
1357
+ notes;
1358
+ constructor(stepType, duration = new NoDuration, target = new NoTarget, notes = "") {
1359
+ this.stepType = stepType;
1360
+ this.duration = duration;
1361
+ this.target = target;
1362
+ this.notes = notes;
1363
+ }
1364
+ build(index) {
1365
+ return {
1366
+ category: null,
1367
+ childStepId: null,
1368
+ description: this.notes,
1369
+ equipmentType: {
1370
+ displayOrder: null,
1371
+ equipmentTypeId: null,
1372
+ equipmentTypeKey: null
1373
+ },
1374
+ exerciseName: null,
1375
+ preferredEndConditionUnit: null,
1376
+ providerExerciseSourceId: null,
1377
+ secondaryTargetType: null,
1378
+ secondaryTargetValueOne: null,
1379
+ secondaryTargetValueTwo: null,
1380
+ secondaryTargetValueUnit: null,
1381
+ secondaryZoneNumber: null,
1382
+ stepAudioNote: null,
1383
+ stepId: index,
1384
+ stepOrder: index,
1385
+ strokeType: {},
1386
+ targetValueOne: null,
1387
+ targetValueTwo: null,
1388
+ targetValueUnit: null,
1389
+ type: "ExecutableStepDTO",
1390
+ weightUnit: null,
1391
+ weightValue: null,
1392
+ workoutProvider: null,
1393
+ zoneNumber: null,
1394
+ ...this.stepType.build(),
1395
+ ...this.duration.build(),
1396
+ ...this.target.build()
1397
+ };
1398
+ }
1399
+ }
1400
+
1401
+ class StepType {
1402
+ type;
1403
+ stepId;
1404
+ constructor(type, stepId) {
1405
+ this.type = type;
1406
+ this.stepId = stepId;
1407
+ }
1408
+ build() {
1409
+ return {
1410
+ stepType: {
1411
+ stepTypeId: this.stepId,
1412
+ stepTypeKey: this.type,
1413
+ displayOrder: this.stepId
1414
+ }
1415
+ };
1416
+ }
1417
+ static WarmUp = new StepType("warmup", 1);
1418
+ static Run = new StepType("interval", 3);
1419
+ static Recovery = new StepType("recovery", 4);
1420
+ static Rest = new StepType("rest", 5);
1421
+ static Cooldown = new StepType("cooldown", 2);
1422
+ static Other = new StepType("other", 7);
1423
+ }
1424
+ // src/garmin/workout-builder/workout-builder.ts
1425
+ class WorkoutBuilder {
1426
+ workoutType;
1427
+ workoutName;
1428
+ workoutDescription;
1429
+ steps = [];
1430
+ constructor(workoutType, workoutName, workoutDescription = "") {
1431
+ this.workoutType = workoutType;
1432
+ this.workoutName = workoutName;
1433
+ this.workoutDescription = workoutDescription;
1434
+ }
1435
+ addStep(step) {
1436
+ this.steps.push(step);
1437
+ return this;
1438
+ }
1439
+ build() {
1440
+ return {
1441
+ ...this.workoutType.build(),
1442
+ subSportType: null,
1443
+ workoutName: this.workoutName,
1444
+ description: this.workoutDescription,
1445
+ estimatedDistanceUnit: { unitKey: null },
1446
+ workoutSegments: [
1447
+ {
1448
+ segmentOrder: 1,
1449
+ ...this.workoutType.build(),
1450
+ workoutSteps: this.steps.map((step, index) => step.build(index + 1))
1451
+ }
1452
+ ],
1453
+ avgTrainingSpeed: null,
1454
+ estimatedDurationInSecs: 0,
1455
+ estimatedDistanceInMeters: null,
1456
+ estimateType: null,
1457
+ isWheelchair: false
1458
+ };
1459
+ }
1460
+ }
1461
+ // src/garmin/workout-builder/workout-type.ts
1462
+ class WorkoutType {
1463
+ sportTypeKey;
1464
+ sportTypeId;
1465
+ constructor(sportTypeKey, sportTypeId) {
1466
+ this.sportTypeKey = sportTypeKey;
1467
+ this.sportTypeId = sportTypeId;
1468
+ }
1469
+ build() {
1470
+ return {
1471
+ sportType: {
1472
+ sportTypeId: this.sportTypeId,
1473
+ sportTypeKey: this.sportTypeKey,
1474
+ displayOrder: this.sportTypeId
1475
+ }
1476
+ };
1477
+ }
1478
+ static Running = new WorkoutType("running", 1);
1479
+ static Cycling = new WorkoutType("cycling", 2);
1480
+ static Swimming = new WorkoutType("swimming", 4);
1481
+ static Strength = new WorkoutType("strength_training", 5);
1482
+ static Cardio = new WorkoutType("cardio_training", 6);
1483
+ }
1484
+ export {
1485
+ courseRequestTemplate,
1486
+ WorkoutType,
1487
+ WorkoutBuilder,
1488
+ UploadFileType,
1489
+ TimeDuration,
1490
+ Target,
1491
+ StepType,
1492
+ Step,
1493
+ PowerZoneTarget,
1494
+ PowerZone,
1495
+ PaceTarget,
1496
+ NoTarget,
1497
+ NoDuration,
1498
+ LapPressDuration,
1499
+ HrmZoneTarget,
1500
+ HrmTarget,
1501
+ HeartRateDuration,
1502
+ GpxActivityType,
1503
+ GarminConnect,
1504
+ ExportFileType,
1505
+ Duration,
1506
+ DistanceDuration,
1507
+ CaloriesDuration,
1508
+ CadenceTarget,
1509
+ ActivityType,
1510
+ ActivitySubType
1511
+ };