@gooin/garmin-connect 1.4.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Oskar Bernberg
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.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+
2
+
3
+ # garmin-connect
4
+ A powerful JavaScript library for connecting to Garmin Connect for sending and receiving health and workout data. It comes with some predefined methods to get and set different kinds of data for your Garmin account, but also have the possibility to make [custom requests](#custom-requests) `GET`, `POST` and `PUT` are currently supported. This makes it easy to implement whatever may be missing to suite your needs.
5
+
6
+ ## Prerequisites
7
+ This library will require you to add a configuration file to your project root called `garmin.config.json` containing your username and password for the Garmin Connect service.
8
+ ```json
9
+ {
10
+ "username": "my.email@example.com",
11
+ "password": "MySecretPassword"
12
+ }
13
+ ```
14
+ ## How to install
15
+ ```shell
16
+ $ npm install garmin-connect
17
+ ```
18
+ ## How to use
19
+ ```js
20
+ const { GarminConnect } = require('garmin-connect');
21
+ // Create a new Garmin Connect Client
22
+ const GCClient = new GarminConnect();
23
+ // Uses credentials from garmin.config.json or uses supplied params
24
+ await GCClient.login('my.email@example.com', 'MySecretPassword');
25
+ const userInfo = await GCClient.getUserInfo();
26
+ ```
27
+ Now you can check `userInfo.emailAddress` to verify that your login was successful.
28
+
29
+ ## Reusing your session
30
+ This is an experimental feature and might not yet provide full stability.
31
+
32
+ After a successful login the ```sessionJson``` getter and setter can be used to export and restore your session.
33
+ ```js
34
+ // Exporting the session
35
+ const session = GCClient.sessionJson;
36
+
37
+ // Use this instead of GCClient.login() to restore the session
38
+ // This will throw an error if the stored session cannot be reused
39
+ GCClient.restore(session);
40
+ ```
41
+ The exported session should be serializable and can be stored as a JSON string.
42
+
43
+ A stored session can only be reused once and will need to be stored after each request. This can be done by attaching some storage to the ```sessionChange``` event.
44
+ ```js
45
+ GCClient.onSessionChange(session => {
46
+ /*
47
+ Your choice of storage here
48
+ node-persist will probably work in most cases
49
+ */
50
+ });
51
+ ```
52
+
53
+ ### Login fallback
54
+ To make sure to use a stored session if possible, but fallback to regular login, one can use the ```restoreOrLogin``` method.
55
+ The arguments ```username``` and ```password``` are both optional and the regular ```.login()``` will be
56
+ called if session restore fails.
57
+ ```js
58
+ await GCClient.restoreOrLogin(session, username, password);
59
+ ```
60
+
61
+ ## Events
62
+
63
+ * ```sessionChange``` will trigger on a change in the current ```sessionJson```
64
+
65
+ To attach a listener to an event, use the ```.on()``` method.
66
+ ```js
67
+ GCClient.on('sessionChange', session => console.log(session));
68
+ ```
69
+ There's currently no way of removing listeners.
70
+
71
+ ## Reading data
72
+ ### User info
73
+ Receive basic user information
74
+ ```js
75
+ GCClient.getUserInfo();
76
+ ```
77
+ ### Social Profile
78
+ Receive social user information
79
+ ```js
80
+ GCClient.getSocialProfile();
81
+ ```
82
+ ### Social Connections
83
+ Get a list of all social connections
84
+ ```js
85
+ GCClient.getSocialConnections();
86
+ ```
87
+ ### Device info
88
+ Get a list of all registered devices including model numbers and firmware versions.
89
+ ```js
90
+ GCClient.getDeviceInfo();
91
+ ```
92
+ ### Activities
93
+ To get a list of recent activities, use the `getActivities` method. This function takes two arguments, *start* and *limit*, which is used for pagination. Both are optional and will default to whatever Garmin Connect is using. To be sure to get all activities, use this correctly.
94
+ ```js
95
+ // Get a list of default length with most recent activities
96
+ GCClient.getActivities();
97
+ // Get activities 10 through 15. (start 10, limit 5)
98
+ GCClient.getActivities(10, 5);
99
+ ```
100
+ ### Activity details
101
+ Use the activityId to get details about that specific activity.
102
+ ```js
103
+ const activities = await GCClient.getActivities(0, 1);
104
+ const id = activities[0].activityId;
105
+ // Use the id as a parameter
106
+ GCClient.getActivity({ activityId: id });
107
+ // Or the whole activity response
108
+ GCClient.getActivity(activities[0]);
109
+ ```
110
+ ### Activities
111
+ To get a list of activities in your news feed, use the `getNewsFeed` method. This function takes two arguments, *start* and *limit*, which is used for pagination. Both are optional and will default to whatever Garmin Connect is using. To be sure to get all activities, use this correctly.
112
+ ```js
113
+ // Get the news feed with a default length with most recent activities
114
+ GCClient.getNewsFeed();
115
+ // Get activities in feed, 10 through 15. (start 10, limit 5)
116
+ GCClient.getNewsFeed(10, 5);
117
+ ```
118
+ ### Download original activity data
119
+ Use the activityId to download the original activity data. Usually this is supplied as a .zip file.
120
+ ```js
121
+ const [activity] = await GCClient.getActivities(0, 1);
122
+ // Directory path is optional and defaults to the current working directory.
123
+ // Downloads filename will be supplied by Garmin.
124
+ GCClient.downloadOriginalActivityData(activity, './some/path/that/exists');
125
+ ```
126
+ ### Upload activity file
127
+ Uploads an activity file as a new Activity. The file can be a `gpx`, `tcx`, or `fit` file. If the activity already exists, the result will have a status code of 409.
128
+ Upload fixed in 1.4.4, Garmin changed the upload api, the response `detailedImportResult` doesn't contain the new activityId.
129
+ ```js
130
+
131
+ const upload = await GCClient.uploadActivity('./some/path/to/file.fit');
132
+ // not working
133
+ const activityId = upload.detailedImportResult.successes[0].internalId;
134
+
135
+ const uploadId = upload.detailedImportResult.uploadId;
136
+ ```
137
+ ### Step count
138
+ Get timestamp and number of steps taken for a specific date.
139
+ ```js
140
+ // This will default to today if no date is supplied
141
+ const steps = await GCClient.getSteps(new Date('2020-03-24'));
142
+ ```
143
+ ### Heart rate
144
+ Get heart rate for a specific date.
145
+ ```js
146
+ // This will default to today if no date is supplied
147
+ const heartRate = await GCClient.getHeartRate(new Date('2020-03-24'));
148
+ ```
149
+ ### Sleep summary
150
+ Get the summary of how well you've slept for a specific date.
151
+ ```js
152
+ // This will default to today if no date is supplied
153
+ const sleep = await GCClient.getSleep(new Date('2020-03-24'));
154
+ ```
155
+ ### Detailed sleep data
156
+ Get the details of your sleep for a specific date.
157
+ ```js
158
+ // This will default to today if no date is supplied
159
+ const detailedSleep = await GCClient.getSleepData(new Date('2020-03-24'));
160
+ ```
161
+ ## Modifying data
162
+ ### Update activity
163
+ ```js
164
+ const activities = await GCClient.getActivities(0, 1);
165
+ const activity = activities[0];
166
+ activity['activityName'] = 'The Updated Name';
167
+ await GCClient.updateActivity(activity);
168
+ ```
169
+ ### Delete an activity
170
+ Deletes an activty.
171
+ ```js
172
+ const activities = await GCClient.getActivities(0, 1);
173
+ const activity = activities[0];
174
+ await GCClient.deleteActivity(activity);
175
+ ```
176
+ ### Add weight
177
+ To add a new weight measurement, use `setBodyWeight`. Here you specify your weight in *kg*.
178
+ ```js
179
+ GCClient.setBodyWeight(81.4);
180
+ ```
181
+ Will set your current weight to 81.4kg. The unit used might be tied to your preferred weight settings.
182
+ ### Add workout
183
+ To add a custom workout, use the `addWorkout` or more specifically `addRunningWorkout`.
184
+ ```js
185
+ GCClient.addRunningWorkout('My 5k run', 5000, 'Some description');
186
+ ```
187
+ Will add a running workout of 5km called 'My 5k run' and return a JSON object representing the saved workout.
188
+ ### Schedule workout
189
+ To add a workout to your calendar, first find your workout and then add it to a specific date.
190
+ ```js
191
+ const workouts = await GCClient.getWorkouts();
192
+ const id = workouts[0].workoutId;
193
+ GCClient.scheduleWorkout({ workoutId: id }, new Date('2020-03-24'));
194
+ ```
195
+ This will add the workout to a specific date in your calendar and make it show up automatically if you're using any of the Garmin watches.
196
+ ### Delete workout
197
+ Deleting a workout is very similar to [scheduling](#schedule-workout) one.
198
+ ```js
199
+ const workouts = await GCClient.getWorkouts();
200
+ const id = workouts[0].workoutId;
201
+ GCClient.deleteWorkout({ workoutId: id });
202
+ ```
203
+
204
+ ## Custom requests
205
+ This library will handle custom requests to your active Garmin Connect session. There are a lot of different url's that is used, which means that this library probably wont cover them all. By using the network analyze tool you can find url's that are used by Garmin Connect to fetch data.
206
+
207
+ Let's assume I found a `GET` requests to the following url:
208
+ ```
209
+ https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailyHeartRate/22f5f84c-de9d-4ad6-97f2-201097b3b983?date=2020-03-24
210
+ ```
211
+ The request can be sent using `GCClient` by running
212
+ ```js
213
+ // You can get your displayName by using the getUserInfo method;
214
+ const displayName = '22f5f84c-de9d-4ad6-97f2-201097b3b983';
215
+ const url = 'https://connect.garmin.com/modern/proxy/wellness-service/wellness/dailyHeartRate/';
216
+ const dateString = '2020-03-24';
217
+ GCClient.get(url + displayName, { date: dateString });
218
+ ```
219
+ and will net you the same result as using the provided way
220
+ ```js
221
+ GCClient.getHeartRate();
222
+ ```
223
+ Notice how the client will keep track of the url's, your user information as well as keeping the session alive.
224
+ ## Limitations
225
+ For now, this library only supports the following:
226
+ * Get user info
227
+ * Get social user info
228
+ * Get heart rate
229
+ * Set body weight
230
+ * Get list of workouts
231
+ * Add new workouts
232
+ * Add workouts to you calendar
233
+ * Remove previously added workouts
234
+ * Get list of activities
235
+ * Get details about one specific activity
236
+ * Get the step count
@@ -0,0 +1,140 @@
1
+ const cloudscraper = require('cloudscraper');
2
+ const qs = require('qs');
3
+ const request = require('request');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const asJson = (body) => {
8
+ try {
9
+ const jsonBody = JSON.parse(body);
10
+ return jsonBody;
11
+ } catch (e) {
12
+ // Do nothing
13
+ }
14
+ return body;
15
+ };
16
+
17
+ class CFClient {
18
+ constructor(headers) {
19
+ this.cloudscraper = cloudscraper;
20
+ this.queryString = qs;
21
+ this.cookies = request.jar();
22
+ this.headers = headers || {};
23
+ }
24
+
25
+ serializeCookies() {
26
+ // eslint-disable-next-line no-underscore-dangle
27
+ return this.cookies._jar.serializeSync();
28
+ }
29
+
30
+ importCookies(cookies) {
31
+ // eslint-disable-next-line no-underscore-dangle
32
+ const deserialized = this.cookies._jar.constructor.deserializeSync(cookies);
33
+ this.cookies = request.jar();
34
+ // eslint-disable-next-line no-underscore-dangle
35
+ this.cookies._jar = deserialized;
36
+ }
37
+
38
+ async scraper(options) {
39
+ return new Promise((resolve) => {
40
+ this.cloudscraper(
41
+ options,
42
+ (err, res) => {
43
+ resolve(res);
44
+ },
45
+ );
46
+ });
47
+ }
48
+
49
+ /**
50
+ * @param {string} downloadDir
51
+ * @param {string} url
52
+ * @param {*} data
53
+ */
54
+ async downloadBlob(downloadDir = '', url, data) {
55
+ const queryData = this.queryString.stringify(data);
56
+ const queryDataString = queryData ? `?${queryData}` : '';
57
+ const options = {
58
+ method: 'GET',
59
+ jar: this.cookies,
60
+ uri: `${url}${queryDataString}`,
61
+ headers: this.headers,
62
+ encoding: 0,
63
+ };
64
+ return new Promise((resolve) => {
65
+ this.cloudscraper(
66
+ options,
67
+ async (err, response, body) => {
68
+ const { headers } = response || {};
69
+ const { 'content-disposition': contentDisposition } = headers || {};
70
+ const downloadDirNormalized = path.normalize(downloadDir);
71
+ if (contentDisposition) {
72
+ const defaultName = `garmin_connect_download_${Date.now()}`;
73
+ const [, fileName = defaultName] = contentDisposition.match(/filename="?([^"]+)"?/);
74
+ const filePath = path.resolve(downloadDirNormalized, fileName);
75
+ fs.writeFileSync(filePath, body);
76
+ resolve(filePath);
77
+ }
78
+ },
79
+ );
80
+ });
81
+ }
82
+
83
+ async get(url, data) {
84
+ const queryData = this.queryString.stringify(data);
85
+ const queryDataString = queryData ? `?${queryData}` : '';
86
+ const options = {
87
+ method: 'GET',
88
+ jar: this.cookies,
89
+ uri: `${url}${queryDataString}`,
90
+ headers: this.headers,
91
+ };
92
+ const { body } = await this.scraper(options);
93
+ return asJson(body);
94
+ }
95
+
96
+ async post(url, data) {
97
+ const options = {
98
+ method: 'POST',
99
+ uri: url,
100
+ jar: this.cookies,
101
+ formData: data,
102
+ headers: this.headers,
103
+ };
104
+ const { body } = await this.scraper(options);
105
+ return asJson(body);
106
+ }
107
+
108
+ async postJson(url, data, params, headers) {
109
+ const options = {
110
+ method: 'POST',
111
+ uri: url,
112
+ jar: this.cookies,
113
+ json: data,
114
+ headers: {
115
+ ...this.headers,
116
+ ...headers,
117
+ 'Content-Type': 'application/json',
118
+ },
119
+ };
120
+ const { body } = await this.scraper(options);
121
+ return asJson(body);
122
+ }
123
+
124
+ async putJson(url, data) {
125
+ const options = {
126
+ method: 'PUT',
127
+ uri: url,
128
+ jar: this.cookies,
129
+ json: data,
130
+ headers: {
131
+ ...this.headers,
132
+ 'Content-Type': 'application/json',
133
+ },
134
+ };
135
+ const { body } = await this.scraper(options);
136
+ return asJson(body);
137
+ }
138
+ }
139
+
140
+ module.exports = CFClient;
@@ -0,0 +1,189 @@
1
+ const axios = require('axios');
2
+ const qs = require('qs');
3
+ const fs = require('fs');
4
+ const stream = require('stream');
5
+ const util = require('util');
6
+ const path = require('path');
7
+
8
+ const pipeline = util.promisify(stream.pipeline);
9
+
10
+ class Client {
11
+ constructor(headers) {
12
+ this.axios = axios;
13
+ this.queryString = qs;
14
+ this.fs = fs;
15
+ this.headers = headers || {};
16
+ this.cookies = {};
17
+ }
18
+
19
+ setCookie(name, value) {
20
+ this.cookies[name] = value;
21
+ this.headers.Cookie = this.getCookieString();
22
+ }
23
+
24
+ parseCookies(response) {
25
+ const setCookies = response && response.headers && response.headers['set-cookie'];
26
+ if (setCookies) {
27
+ setCookies.forEach((c) => {
28
+ const [cookieValue] = c.split(';');
29
+ const [name, value] = cookieValue.split('=');
30
+ this.setCookie(name, value);
31
+ });
32
+ }
33
+ return response;
34
+ }
35
+
36
+ getCookie(name) {
37
+ return this.cookies[name];
38
+ }
39
+
40
+ getCookieString() {
41
+ return Object.entries(this.cookies).map((e) => `${e[0]}=${e[1]}`).join('; ');
42
+ }
43
+
44
+ post(url, data, params) {
45
+ return this.axios({
46
+ method: 'POST',
47
+ params,
48
+ url,
49
+ data: this.queryString.stringify(data),
50
+ headers: {
51
+ ...this.headers,
52
+ 'Content-Type': 'application/x-www-form-urlencoded',
53
+ },
54
+ maxRedirects: 0,
55
+ })
56
+ .catch((r) => {
57
+ const { response } = r || {};
58
+ this.parseCookies(response);
59
+ if (response.status === 302 || response.status === 301) {
60
+ if (response.headers && response.headers.location) {
61
+ return this.post(response.headers.location, data, params);
62
+ }
63
+ }
64
+ return r;
65
+ })
66
+ .then((r) => this.parseCookies(r))
67
+ .then((r) => r && r.data);
68
+ }
69
+
70
+ postJson(url, data, params, headers = {}) {
71
+ return this.axios({
72
+ method: 'POST',
73
+ params,
74
+ url,
75
+ data: JSON.stringify(data, null, 4),
76
+ headers: {
77
+ ...this.headers,
78
+ ...headers,
79
+ 'Content-Type': 'application/json',
80
+ },
81
+ })
82
+ .then((r) => this.parseCookies(r))
83
+ .then((r) => r && r.data);
84
+ }
85
+
86
+ postBlob(url, formData, params, headers = {}) {
87
+ return this.axios({
88
+ method: 'POST',
89
+ params,
90
+ url,
91
+ data: formData,
92
+ headers: {
93
+ ...this.headers,
94
+ ...headers,
95
+ ...formData.getHeaders(),
96
+ },
97
+ })
98
+ .then((r) => this.parseCookies(r))
99
+ .then((r) => r && r.data);
100
+ }
101
+
102
+ putJson(url, data, params) {
103
+ return this.axios({
104
+ method: 'PUT',
105
+ params,
106
+ url,
107
+ data: JSON.stringify(data, null, 4),
108
+ headers: {
109
+ ...this.headers,
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ })
113
+ .then((r) => this.parseCookies(r))
114
+ .then((r) => r && r.data);
115
+ }
116
+
117
+ downloadBlob(downloadDir = '', url, data, params) {
118
+ const queryData = this.queryString.stringify(data);
119
+ const queryDataString = queryData ? `?${queryData}` : '';
120
+ return this.axios({
121
+ method: 'GET',
122
+ params,
123
+ responseType: 'stream',
124
+ url: `${url}${queryDataString}`,
125
+ headers: this.headers,
126
+ maxRedirects: 0,
127
+ })
128
+ .catch((r) => {
129
+ const { response } = r || {};
130
+ const { status, headers } = response || {};
131
+ const { location } = headers || {};
132
+ this.parseCookies(response);
133
+ if (status === 302 || status === 301) {
134
+ if (headers && location) {
135
+ return this.downloadBlob(location, data, params);
136
+ }
137
+ }
138
+ return r;
139
+ })
140
+ .then((r) => this.parseCookies(r))
141
+ .then(async (r) => {
142
+ const { headers } = r || {};
143
+ const { 'content-disposition': contentDisposition } = headers || {};
144
+ const downloadDirNormalized = path.normalize(downloadDir);
145
+ if (contentDisposition) {
146
+ const defaultName = `garmin_connect_download_${Date.now()}`;
147
+ const [, fileName = defaultName] = contentDisposition.match(/filename="(.+)"/);
148
+ const filePath = path.resolve(downloadDir, fileName);
149
+ await pipeline(r.data, this.fs.createWriteStream(filePath));
150
+ return filePath;
151
+ }
152
+ throw new Error(`Could not download file ${url} to ${downloadDirNormalized}`);
153
+ });
154
+ }
155
+
156
+ get(url, data, params) {
157
+ const queryData = this.queryString.stringify(data);
158
+ const queryDataString = queryData ? `?${queryData}` : '';
159
+ return this.axios({
160
+ method: 'GET',
161
+ params,
162
+ url: `${url}${queryDataString}`,
163
+ headers: this.headers,
164
+ maxRedirects: 0,
165
+ })
166
+ .catch((r) => {
167
+ const { response } = r || {};
168
+ const { status, headers } = response || {};
169
+ const { location } = headers || {};
170
+ this.parseCookies(response);
171
+ if (status === 302 || status === 301) {
172
+ if (headers && location) {
173
+ return this.get(location, data, params);
174
+ }
175
+ }
176
+ return r;
177
+ })
178
+ .then((r) => this.parseCookies(r))
179
+ .then((r) => {
180
+ if (typeof r === 'string') {
181
+ return r;
182
+ }
183
+ return r && r.data;
184
+ });
185
+ }
186
+ }
187
+
188
+
189
+ module.exports = Client;
@@ -0,0 +1,10 @@
1
+ const toDateString = (date) => {
2
+ const offset = date.getTimezoneOffset();
3
+ const offsetDate = new Date(date.getTime() - (offset * 60 * 1000));
4
+ const [dateString] = offsetDate.toISOString().split('T');
5
+ return dateString;
6
+ };
7
+
8
+ module.exports = {
9
+ toDateString,
10
+ };
@@ -0,0 +1,438 @@
1
+ const appRoot = require('app-root-path');
2
+ const fs = require('fs');
3
+
4
+ let config = {};
5
+ try {
6
+ // eslint-disable-next-line
7
+ config = require(`${appRoot}/garmin.config.json`);
8
+ } catch (e) {
9
+ // Do nothing
10
+ }
11
+
12
+ const CFClient = require('../common/CFClient');
13
+ const { Running } = require('./workouts');
14
+ const { toDateString } = require('../common/DateUtils');
15
+ const urls = require('./Urls');
16
+
17
+ const {
18
+ username: configUsername,
19
+ password: configPassword,
20
+ } = config;
21
+
22
+ const credentials = {
23
+ username: configUsername,
24
+ password: configPassword,
25
+ embed: 'false',
26
+ };
27
+
28
+ class GarminConnect {
29
+ constructor() {
30
+ const headers = {
31
+ origin: urls.GARMIN_SSO_ORIGIN,
32
+ nk: 'NT',
33
+ };
34
+ this.client = new CFClient(headers);
35
+ this.userHash = undefined;
36
+ this.listeners = {};
37
+ this.events = { sessionChange: 'sessionChange' };
38
+ }
39
+
40
+ get sessionJson() {
41
+ const cookies = this.client.serializeCookies();
42
+ return { cookies, userHash: this.userHash };
43
+ }
44
+
45
+ set sessionJson(json) {
46
+ const {
47
+ cookies,
48
+ userHash,
49
+ } = json || {};
50
+ if (cookies && userHash) {
51
+ this.userHash = userHash;
52
+ this.client.importCookies(cookies);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Add an event listener callback
58
+ * @param event
59
+ * @param callback
60
+ */
61
+ on(event, callback) {
62
+ if (event && callback && typeof event === 'string' && typeof callback === 'function') {
63
+ if (!this.listeners[event]) {
64
+ this.listeners[event] = [];
65
+ }
66
+ this.listeners[event].push(callback);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Method for triggering any event
72
+ * @param event
73
+ * @param data
74
+ */
75
+ triggerEvent(event, data) {
76
+ const callbacks = this.listeners[event] || [];
77
+ callbacks.forEach((cb) => cb(data));
78
+ }
79
+
80
+ /**
81
+ * Add a callback to the 'sessionChange' event
82
+ * @param callback
83
+ */
84
+ onSessionChange(callback) {
85
+ this.on(this.events.sessionChange, callback);
86
+ }
87
+
88
+ /**
89
+ * Restore an old session from storage and fallback to regular login
90
+ * @param json
91
+ * @param username
92
+ * @param password
93
+ * @returns {Promise<GarminConnect>}
94
+ */
95
+ async restoreOrLogin(json, username, password) {
96
+ return this.restore(json).catch((e) => {
97
+ console.warn(e);
98
+ return this.login(username, password);
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Restore an old session from storage
104
+ * @param json
105
+ * @returns {Promise<GarminConnect>}
106
+ */
107
+ async restore(json) {
108
+ this.sessionJson = json;
109
+ try {
110
+ const info = await this.getUserInfo();
111
+ const { displayName } = info || {};
112
+ if (displayName && displayName === this.userHash) {
113
+ // Session restoration was successful
114
+ return this;
115
+ }
116
+ throw new Error('Unable to restore session, user hash do not match');
117
+ } catch (e) {
118
+ throw new Error(`Unable to restore session due to: ${e}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Login to Garmin Connect
124
+ * @param username
125
+ * @param password
126
+ * @returns {Promise<*>}
127
+ */
128
+ async login(username, password) {
129
+ let tempCredentials = { ...credentials, rememberme: 'on' };
130
+ if (username && password) {
131
+ tempCredentials = {
132
+ ...credentials, username, password, rememberme: 'on',
133
+ };
134
+ }
135
+ await this.client.get(urls.SIGNIN_URL);
136
+ await this.client.post(urls.SIGNIN_URL, tempCredentials);
137
+ const userPreferences = await this.getUserInfo();
138
+ const { displayName } = userPreferences;
139
+ this.userHash = displayName;
140
+ return this;
141
+ }
142
+
143
+ // User info
144
+ /**
145
+ * Get basic user information
146
+ * @returns {Promise<*>}
147
+ */
148
+ async getUserInfo() {
149
+ return this.get(urls.userInfo());
150
+ }
151
+
152
+ /**
153
+ * Get social user information
154
+ * @returns {Promise<*>}
155
+ */
156
+ async getSocialProfile() {
157
+ return this.get(urls.socialProfile(this.userHash));
158
+ }
159
+
160
+ /**
161
+ * Get a list of all social connections
162
+ * @returns {Promise<*>}
163
+ */
164
+ async getSocialConnections() {
165
+ return this.get(urls.socialConnections(this.userHash));
166
+ }
167
+
168
+ // Devices
169
+ /**
170
+ * Get a list of all registered devices
171
+ * @returns {Promise<*>}
172
+ */
173
+ async getDeviceInfo() {
174
+ return this.get(urls.deviceInfo(this.userHash));
175
+ }
176
+
177
+ // Sleep data
178
+ /**
179
+ * Get detailed sleep data for a specific date
180
+ * @param date
181
+ * @returns {Promise<*>}
182
+ */
183
+ async getSleepData(date = new Date()) {
184
+ const dateString = toDateString(date);
185
+ return this.get(urls.dailySleepData(this.userHash), { date: dateString });
186
+ }
187
+
188
+ /**
189
+ * Get sleep data summary for a specific date
190
+ * @param date
191
+ * @returns {Promise<*>}
192
+ */
193
+ async getSleep(date = new Date()) {
194
+ const dateString = toDateString(date);
195
+ return this.get(urls.dailySleep(), { date: dateString });
196
+ }
197
+
198
+ // Heart rate
199
+ /**
200
+ * Get heart rate measurements for a specific date
201
+ * @param date
202
+ * @returns {Promise<*>}
203
+ */
204
+ async getHeartRate(date = new Date()) {
205
+ const dateString = toDateString(date);
206
+ return this.get(urls.dailyHeartRate(this.userHash), { date: dateString });
207
+ }
208
+
209
+ // Weight
210
+ /**
211
+ * Post a new body weight
212
+ * @param weight
213
+ * @returns {Promise<*>}
214
+ */
215
+ async setBodyWeight(weight) {
216
+ if (weight) {
217
+ const roundWeight = Math.round(weight * 1000);
218
+ const data = { userData: { weight: roundWeight } };
219
+ return this.put(urls.userSettings(), data);
220
+ }
221
+ return Promise.reject();
222
+ }
223
+
224
+ // Activites
225
+ /**
226
+ * Get list of activites
227
+ * @param start
228
+ * @param limit
229
+ * @returns {Promise<*>}
230
+ */
231
+ async getActivities(start, limit) {
232
+ return this.get(urls.activities(), { start, limit });
233
+ }
234
+
235
+ /**
236
+ * Get details about an activity
237
+ * @param activity
238
+ * @param maxChartSize
239
+ * @param maxPolylineSize
240
+ * @returns {Promise<*>}
241
+ */
242
+ async getActivity(activity, maxChartSize, maxPolylineSize) {
243
+ const { activityId } = activity || {};
244
+ if (activityId) {
245
+ return this.get(urls.activityDetails(activityId), { maxChartSize, maxPolylineSize });
246
+ }
247
+ return Promise.reject();
248
+ }
249
+
250
+ /**
251
+ * Get weather data from an activity
252
+ * @param activity
253
+ * @returns {Promise<*>}
254
+ */
255
+ async getActivityWeather(activity) {
256
+ const { activityId } = activity || {};
257
+ if (activityId) {
258
+ return this.get(urls.weather(activityId));
259
+ }
260
+ return Promise.reject();
261
+ }
262
+
263
+ /**
264
+ * Updates an activity
265
+ * @param activity
266
+ * @returns {Promise<*>}
267
+ */
268
+ async updateActivity(activity) {
269
+ return this.put(urls.activity(activity.activityId), activity);
270
+ }
271
+
272
+ /**
273
+ * Deletes an activity
274
+ * @param activity
275
+ * @returns {Promise<*>}
276
+ */
277
+ async deleteActivity(activity) {
278
+ const { activityId } = activity || {};
279
+ if (activityId) {
280
+ const headers = { 'x-http-method-override': 'DELETE' };
281
+ return this.client.postJson(urls.activity(activityId), undefined, undefined, headers);
282
+ }
283
+ return Promise.reject();
284
+ }
285
+
286
+ /**
287
+ * Get list of activities in your news feed
288
+ * @param start
289
+ * @param limit
290
+ * @returns {Promise<*>}
291
+ */
292
+ async getNewsFeed(start, limit) {
293
+ return this.get(urls.newsFeed(), { start, limit });
294
+ }
295
+
296
+ // Steps
297
+ /**
298
+ * Get step count for a specific date
299
+ * @param date
300
+ * @returns {Promise<*>}
301
+ */
302
+ async getSteps(date = new Date()) {
303
+ const dateString = toDateString(date);
304
+ return this.get(urls.dailySummaryChart(this.userHash), { date: dateString });
305
+ }
306
+
307
+ // Workouts
308
+ /**
309
+ * Get list of workouts
310
+ * @param start
311
+ * @param limit
312
+ * @returns {Promise<*>}
313
+ */
314
+ async getWorkouts(start, limit) {
315
+ return this.get(urls.workouts(), { start, limit });
316
+ }
317
+
318
+ /**
319
+ * Download original activity data to disk as zip
320
+ * Resolves to absolute path for the downloaded file
321
+ * @param activity : any
322
+ * @param dir Will default to current working directory
323
+ * @param type : string - Will default to 'zip'. Other possible values are 'tcx', 'gpx' or 'kml'.
324
+ * @returns {Promise<*>}
325
+ */
326
+ async downloadOriginalActivityData(activity, dir, type = '') {
327
+ const { activityId } = activity || {};
328
+ if (activityId) {
329
+ const url = type === '' || type === 'zip'
330
+ ? urls.originalFile(activityId)
331
+ : urls.exportFile(activityId, type);
332
+ return this.client.downloadBlob(dir, url);
333
+ }
334
+ return Promise.reject();
335
+ }
336
+
337
+ /**
338
+ * Uploads an activity file ('gpx', 'tcx', or 'fit')
339
+ * @param file the file to upload
340
+ * @param format the format of the file. If undefined, the extension of the file will be used.
341
+ * @returns {Promise<*>}
342
+ */
343
+ async uploadActivity(file, format) {
344
+ // throw new Error('uploadActivity method is disabled in this version');
345
+
346
+ const detectedFormat = format || `.${file.split('.').pop()}`;
347
+ if (detectedFormat !== '.gpx' && detectedFormat !== '.tcx' && detectedFormat !== '.fit') {
348
+ Promise.reject();
349
+ }
350
+
351
+ const fileBinary = fs.createReadStream(file);
352
+ return this.client.post(urls.upload(format), {
353
+ file: fileBinary,
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Adds a running workout with one step of completeing a set distance.
359
+ * @param name
360
+ * @param meters
361
+ * @param description
362
+ * @returns {Promise<*>}
363
+ */
364
+ async addRunningWorkout(name, meters, description) {
365
+ const running = new Running();
366
+ running.name = name;
367
+ running.distance = meters;
368
+ running.description = description;
369
+ return this.addWorkout(running);
370
+ }
371
+
372
+ /**
373
+ * Add a new workout preset.
374
+ * @param workout
375
+ * @returns {Promise<*>}
376
+ */
377
+ async addWorkout(workout) {
378
+ if (workout.isValid()) {
379
+ const data = { ...workout.toJson() };
380
+ if (!data.description) {
381
+ data.description = 'Added by garmin-connect for Node.js';
382
+ }
383
+ return this.post(urls.workout(), data);
384
+ }
385
+ return Promise.reject();
386
+ }
387
+
388
+ /**
389
+ * Add a workout to your workout calendar.
390
+ * @param workout
391
+ * @param date
392
+ * @returns {Promise<*>}
393
+ */
394
+ async scheduleWorkout(workout, date) {
395
+ const { workoutId } = workout || {};
396
+ if (workoutId && date) {
397
+ const dateString = toDateString(date);
398
+ return this.post(urls.schedule(workoutId), { date: dateString });
399
+ }
400
+ return Promise.reject();
401
+ }
402
+
403
+ /**
404
+ * Delete a workout based on a workout object.
405
+ * @param workout
406
+ * @returns {Promise<*>}
407
+ */
408
+ async deleteWorkout(workout) {
409
+ const { workoutId } = workout || {};
410
+ if (workoutId) {
411
+ const headers = { 'x-http-method-override': 'DELETE' };
412
+ return this.client.postJson(urls.workout(workoutId), undefined, undefined, headers);
413
+ }
414
+ return Promise.reject();
415
+ }
416
+
417
+ // General methods
418
+
419
+ async get(url, data) {
420
+ const response = await this.client.get(url, data);
421
+ this.triggerEvent(this.events.sessionChange, this.sessionJson);
422
+ return response;
423
+ }
424
+
425
+ async post(url, data) {
426
+ const response = await this.client.postJson(url, data);
427
+ this.triggerEvent(this.events.sessionChange, this.sessionJson);
428
+ return response;
429
+ }
430
+
431
+ async put(url, data) {
432
+ const response = await this.client.putJson(url, data);
433
+ this.triggerEvent(this.events.sessionChange, this.sessionJson);
434
+ return response;
435
+ }
436
+ }
437
+
438
+ module.exports = GarminConnect;
@@ -0,0 +1,102 @@
1
+ const GC_MODERN = 'https://connect.garmin.com/modern';
2
+ const GARMIN_SSO_ORIGIN = 'https://sso.garmin.com';
3
+ const GARMIN_SSO = `${GARMIN_SSO_ORIGIN}/sso`;
4
+ const BASE_URL = `${GC_MODERN}/proxy`;
5
+ const SIGNIN_URL = `${GARMIN_SSO}/signin`;
6
+ const LOGIN_URL = `${GARMIN_SSO}/login`;
7
+
8
+ const ACTIVITY_SERVICE = `${BASE_URL}/activity-service`;
9
+ const ACTIVITYLIST_SERVICE = `${BASE_URL}/activitylist-service`;
10
+ const CURRENT_USER_SERVICE = `${GC_MODERN}/currentuser-service/user/info`;
11
+ const DEVICE_SERVICE = `${BASE_URL}/device-service`;
12
+ const DOWNLOAD_SERVICE = `${BASE_URL}/download-service`;
13
+ const USERPROFILE_SERVICE = `${BASE_URL}/userprofile-service`;
14
+ const WELLNESS_SERVICE = `${BASE_URL}/wellness-service`;
15
+ const WORKOUT_SERVICE = `${BASE_URL}/workout-service`;
16
+ const UPLOAD_SERVICE = `${BASE_URL}/upload-service`;
17
+
18
+ const USER_SETTINGS = `${USERPROFILE_SERVICE}/userprofile/user-settings/`;
19
+
20
+ const activity = (id) => `${ACTIVITY_SERVICE}/activity/${id}`;
21
+
22
+ const weather = (id) => `${activity(id)}/weather`;
23
+
24
+ const activityDetails = (id) => `${activity(id)}/details`;
25
+
26
+ const activities = () => `${ACTIVITYLIST_SERVICE}/activities/search/activities`;
27
+
28
+ const dailyHeartRate = (userHash) => `${WELLNESS_SERVICE}/wellness/dailyHeartRate/${userHash}`;
29
+
30
+ const dailySleep = () => `${WELLNESS_SERVICE}/wellness/dailySleep`;
31
+
32
+ const dailySleepData = (userHash) => `${WELLNESS_SERVICE}/wellness/dailySleepData/${userHash}`;
33
+
34
+ const dailySummaryChart = (userHash) => `${WELLNESS_SERVICE}/wellness/dailySummaryChart/${userHash}`;
35
+
36
+ const deviceInfo = (userHash) => `${DEVICE_SERVICE}/deviceservice/device-info/all/${userHash}`;
37
+
38
+ const schedule = (id) => `${WORKOUT_SERVICE}/schedule/${id}`;
39
+
40
+ const userInfo = () => CURRENT_USER_SERVICE;
41
+
42
+ const socialProfile = (userHash) => `${USERPROFILE_SERVICE}/socialProfile/${userHash}`;
43
+
44
+ const userSettings = () => USER_SETTINGS;
45
+
46
+ const originalFile = (id) => `${DOWNLOAD_SERVICE}/files/activity/${id}`;
47
+
48
+ /**
49
+ *
50
+ * @param id {string}
51
+ * @param type "tcx" | "gpx" | "kml"
52
+ * @return {`${string}/export/${string}/activity/${string}`}
53
+ */
54
+ const exportFile = (id, type) => `${DOWNLOAD_SERVICE}/export/${type}/activity/${id}`;
55
+
56
+ const workout = (id) => {
57
+ if (id) {
58
+ return `${WORKOUT_SERVICE}/workout/${id}`;
59
+ }
60
+ return `${WORKOUT_SERVICE}/workout`;
61
+ };
62
+
63
+ const workouts = () => `${WORKOUT_SERVICE}/workouts`;
64
+
65
+ const socialConnections = (userHash) => `${USERPROFILE_SERVICE}/socialProfile/connections/${userHash}`;
66
+
67
+ const newsFeed = () => `${ACTIVITYLIST_SERVICE}/activities/subscriptionFeed`;
68
+
69
+ const upload = (format) => `${UPLOAD_SERVICE}/upload/${format}`;
70
+
71
+ module.exports = {
72
+ GC_MODERN,
73
+ GARMIN_SSO_ORIGIN,
74
+ GARMIN_SSO,
75
+ BASE_URL,
76
+ SIGNIN_URL,
77
+ LOGIN_URL,
78
+ CURRENT_USER_SERVICE,
79
+ USERPROFILE_SERVICE,
80
+ WELLNESS_SERVICE,
81
+ WORKOUT_SERVICE,
82
+ activity,
83
+ weather,
84
+ activityDetails,
85
+ activities,
86
+ dailyHeartRate,
87
+ dailySleep,
88
+ dailySleepData,
89
+ dailySummaryChart,
90
+ deviceInfo,
91
+ schedule,
92
+ userInfo,
93
+ socialProfile,
94
+ userSettings,
95
+ workout,
96
+ workouts,
97
+ originalFile,
98
+ exportFile,
99
+ socialConnections,
100
+ newsFeed,
101
+ upload,
102
+ };
@@ -0,0 +1,53 @@
1
+ const template = require('./templates/RunningTemplate');
2
+
3
+ class Running {
4
+ constructor() {
5
+ this.data = template();
6
+ }
7
+
8
+ get name() {
9
+ return this.data.workoutName;
10
+ }
11
+
12
+ set name(name) {
13
+ this.data.workoutName = `${name}`;
14
+ }
15
+
16
+ get distance() {
17
+ return this.data.workoutSegments[0].workoutSteps[0].endConditionValue;
18
+ }
19
+
20
+ set distance(meters) {
21
+ this.data.workoutSegments[0].workoutSteps[0].endConditionValue = Math.round(meters);
22
+ }
23
+
24
+ get workoutId() {
25
+ return this.data.workoutId;
26
+ }
27
+
28
+ set workoutId(workoutId) {
29
+ this.data.workoutId = workoutId;
30
+ }
31
+
32
+ get description() {
33
+ return this.data.description;
34
+ }
35
+
36
+ set description(description) {
37
+ this.data.description = description;
38
+ }
39
+
40
+ isValid() {
41
+ return !!(this.name && this.distance);
42
+ }
43
+
44
+ toJson() {
45
+ return this.data;
46
+ }
47
+
48
+ toString() {
49
+ return `${this.name}, ${(this.distance / 1000).toFixed(2)}km`;
50
+ }
51
+ }
52
+
53
+ module.exports = Running;
@@ -0,0 +1,5 @@
1
+ const Running = require('./Running');
2
+
3
+ module.exports = {
4
+ Running,
5
+ };
@@ -0,0 +1,48 @@
1
+ module.exports = () => (
2
+ {
3
+ sportType: {
4
+ sportTypeId: 1,
5
+ sportTypeKey: 'running',
6
+ },
7
+ workoutName: null,
8
+ workoutSegments: [
9
+ {
10
+ segmentOrder: 1,
11
+ sportType: {
12
+ sportTypeId: 1,
13
+ sportTypeKey: 'running',
14
+ },
15
+ workoutSteps: [
16
+ {
17
+ type: 'ExecutableStepDTO',
18
+ stepId: null,
19
+ stepOrder: 1,
20
+ childStepId: null,
21
+ description: null,
22
+ stepType: {
23
+ stepTypeId: 3,
24
+ stepTypeKey: 'interval',
25
+ },
26
+ endCondition: {
27
+ conditionTypeKey: 'distance',
28
+ conditionTypeId: 3,
29
+ },
30
+ preferredEndConditionUnit: {
31
+ unitKey: 'kilometer',
32
+ },
33
+ endConditionValue: null,
34
+ endConditionCompare: null,
35
+ endConditionZone: null,
36
+ targetType: {
37
+ workoutTargetTypeId: 1,
38
+ workoutTargetTypeKey: 'no.target',
39
+ },
40
+ targetValueOne: null,
41
+ targetValueTwo: null,
42
+ zoneNumber: null,
43
+ },
44
+ ],
45
+ },
46
+ ],
47
+ }
48
+ );
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const GarminConnect = require('./garmin/GarminConnect');
2
+
3
+ module.exports = { GarminConnect };
@@ -0,0 +1,19 @@
1
+ const { GarminConnect } = require('garmin-connect');
2
+
3
+ // Has to be run in an async function to be able to use the await keyword
4
+ const main = async () => {
5
+ // Create a new Garmin Connect Client
6
+ const GCClient = new GarminConnect();
7
+
8
+ // Uses credentials from garmin.config.json or uses supplied params
9
+ await GCClient.login('my.email@example.com', 'MySecretPassword');
10
+
11
+ // Get user info
12
+ const info = await GCClient.getUserInfo();
13
+
14
+ // Log info to make sure signin was successful
15
+ console.log(info);
16
+ };
17
+
18
+ // Run the code
19
+ main();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@gooin/garmin-connect",
3
+ "version": "1.4.4",
4
+ "description": "Makes it simple to interface with Garmin Connect to get or set any data point",
5
+ "main": "./dist/index.js",
6
+ "scripts": {
7
+ "build": "rm -rf ./dist/ && mkdir ./dist/ && cp -r ./src/* ./dist/",
8
+ "prepack": "npm run build"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Pythe1337N/garmin-connect.git"
13
+ },
14
+ "keywords": [
15
+ "garmin",
16
+ "connect",
17
+ "scraper",
18
+ "weight",
19
+ "running",
20
+ "workout",
21
+ "activity",
22
+ "activities",
23
+ "download",
24
+ "downloader"
25
+ ],
26
+ "author": "Oskar Bernberg (Pythe1337N)",
27
+ "license": "MIT",
28
+ "files": [
29
+ "dist/*",
30
+ "examples/*"
31
+ ],
32
+ "devDependencies": {
33
+ "eslint": "^8.13.0",
34
+ "eslint-config-airbnb-base": "^15.0.0",
35
+ "eslint-plugin-import": "^2.26.0",
36
+ "eslint-plugin-jest": "^26.1.4",
37
+ "pre-commit": "^1.2.2"
38
+ },
39
+ "precommit": "build",
40
+ "homepage": "https://github.com/Pythe1337N/garmin-connect#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/Pythe1337N/garmin-connect/issues"
43
+ },
44
+ "runkitExampleFilename": "./examples/example.js",
45
+ "dependencies": {
46
+ "app-root-path": "^3.0.0",
47
+ "axios": "^0.26.1",
48
+ "cloudscraper": "^4.6.0",
49
+ "form-data": "^4.0.0",
50
+ "qs": "^6.10.3",
51
+ "request": "^2.88.2"
52
+ }
53
+ }