Skip to content

Instantly share code, notes, and snippets.

@abfo
Created January 22, 2019 00:27
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save abfo/cb96533af64f8c861cb77e790939d827 to your computer and use it in GitHub Desktop.
Save abfo/cb96533af64f8c861cb77e790939d827 to your computer and use it in GitHub Desktop.
Google fit to google sheets improved sample, handles steps, weight and distance for one or more days at a time. See https://ithoughthecamewithyou.com/post/export-google-fit-daily-steps-to-a-google-sheet for instructions.
// add your Google API Project OAuth client ID and client secret here
var ClientID = '';
var ClientSecret = '';
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Google Fit')
.addItem('Authorize if needed (does nothing if already authorized)', 'showSidebar')
.addItem('Get Metrics for Yesterday', 'getMetrics')
.addItem('Get Metrics for past 60 days', 'getHistory')
.addItem('Reset Settings', 'clearProps')
.addToUi();
}
function getMetrics() {
getMetricsForDays(1, 1, 'Metrics');
}
function getHistory() {
getMetricsForDays(1, 60, 'History');
}
// see step count example at https://developers.google.com/fit/scenarios/read-daily-step-total
// adapted below to handle multiple metrics (steps, weight, distance), only logged if present for day
function getMetricsForDays(fromDaysAgo, toDaysAgo, tabName) {
var start = new Date();
start.setHours(0,0,0,0);
start.setDate(start.getDate() - toDaysAgo);
var end = new Date();
end.setHours(23,59,59,999);
end.setDate(end.getDate() - fromDaysAgo);
var fitService = getFitService();
var request = {
"aggregateBy": [
{
"dataTypeName": "com.google.step_count.delta",
"dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
},
{
"dataTypeName": "com.google.weight.summary",
"dataSourceId": "derived:com.google.weight:com.google.android.gms:merge_weight"
},
{
"dataTypeName": "com.google.distance.delta",
"dataSourceId": "derived:com.google.distance.delta:com.google.android.gms:merge_distance_delta"
}
],
"bucketByTime": { "durationMillis": 86400000 },
"startTimeMillis": start.getTime(),
"endTimeMillis": end.getTime()
};
var response = UrlFetchApp.fetch('https://www.googleapis.com/fitness/v1/users/me/dataset:aggregate', {
headers: {
Authorization: 'Bearer ' + fitService.getAccessToken()
},
'method' : 'post',
'contentType' : 'application/json',
'payload' : JSON.stringify(request, null, 2)
});
var json = JSON.parse(response.getContentText());
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(tabName);
for(var b = 0; b < json.bucket.length; b++) {
// each bucket in our response should be a day
var bucketDate = new Date(parseInt(json.bucket[b].startTimeMillis, 10));
var steps = -1;
var weight = -1;
var distance = -1;
if (json.bucket[b].dataset[0].point.length > 0) {
steps = json.bucket[b].dataset[0].point[0].value[0].intVal;
}
if (json.bucket[b].dataset[1].point.length > 0) {
weight = json.bucket[b].dataset[1].point[0].value[0].fpVal;
}
if (json.bucket[b].dataset[2].point.length > 0) {
distance = json.bucket[b].dataset[2].point[0].value[0].fpVal;
}
sheet.appendRow([bucketDate,
steps == -1 ? ' ' : steps,
weight == -1 ? ' ' : weight,
distance == -1 ? ' ' : distance]);
}
}
// functions below adapted from Google OAuth example at https://github.com/googlesamples/apps-script-oauth2
function getFitService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('fit')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId(ClientID)
.setClientSecret(ClientSecret)
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scopes to request (space-separated for Google services).
// see https://developers.google.com/fit/rest/v1/authorization for a list of Google Fit scopes
.setScope('https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.body.read https://www.googleapis.com/auth/fitness.location.read')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
.setParam('login_hint', Session.getActiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Forces the approval prompt every time. This is useful for testing,
// but not desirable in a production application.
//.setParam('approval_prompt', 'force');
}
function showSidebar() {
var fitService = getFitService();
if (!fitService.hasAccess()) {
var authorizationUrl = fitService.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'<a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>. ' +
'Close this after you have finished.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
} else {
// ...
}
}
function authCallback(request) {
var fitService = getFitService();
var isAuthorized = fitService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function clearProps() {
PropertiesService.getUserProperties().deleteAllProperties();
}
@Nazar-Pa
Copy link

Hi first thank your for this helpful instruction. Could you maybe help. Have can I do that the daily step number is always synced on that spreadsheet (without clicking button every time) ?

@abfo
Copy link
Author

abfo commented Jun 29, 2020

You set up a trigger to run getMetrics every day. See the instructions in the blog post linked above.

@Nazar-Pa
Copy link

Nazar-Pa commented Jun 30, 2020

You set up a trigger to run getMetrics every day. See the instructions in the blog post linked above.

thanks for reply. I did as you said and set up a trigger to execute every 2 hours. But the getMetrics did not run every 2 hours and gives an error:
image

@abfo
Copy link
Author

abfo commented Jun 30, 2020

You're missing authorization. Run through the blog post carefully and make sure you have enabled the API, added your key to the script and authorized your sheet.

@Nazar-Pa
Copy link

Nazar-Pa commented Jun 30, 2020 via email

@abfo
Copy link
Author

abfo commented Jun 30, 2020

Try revoking access from Google Fit and then clear authentication settings from the sheet (menu item). Then authorize again and see if it works.

@Nazar-Pa
Copy link

Nazar-Pa commented Jul 2, 2020

Try revoking access from Google Fit and then clear authentication settings from the sheet (menu item). Then authorize again and see if it works.

Thank you very much, it works now

@Nazar-Pa
Copy link

Nazar-Pa commented Jul 3, 2020

Hi sir, it's me again. I want to get number of steps through my web application. It actually works for me by sending "post" request, and I am getting the current number of steps. But as access token periodically expires I want set "offline access" to the access token as you did in your code above. Do you have an idea how can I do that in my case? Here is also my code -->

data = {
"aggregateBy": [{
"dataTypeName": "com.google.step_count.delta",
"dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
}],
"bucketByTime": { "durationMillis": 86400000 },
"startTimeMillis": 1593640800000,
"endTimeMillis": 1593727199999
}

var options = {
method: 'POST',
body: data,
json: true,
url: 'https://www.googleapis.com/fitness/v1/users/me/dataset:aggregate',
headers: {
'Authorization':'Bearer ya29.a0AfH6SMAAT2yPO27Wdi9s4yr0qju4XknUXcM55wFZyngx-o8a8xhxfMXxtJod4skftKdWAEx6oggdCTm-GGSWqS8dLKPo9V5scfwM6Af8QA8HWekWxbf2JX_wozJ1OIsE9gVecJ-8ADX0kHjHpnJCKG0jzgXHQVFT0Gc'
}
};

function callback(error, response, body) {
if (!error && response.statusCode == 200) {

console.log(body.bucket[0].dataset[0].point[0].value[0].intVal);

}
}

request(options, callback);

@abfo
Copy link
Author

abfo commented Jul 3, 2020

You need to save a refresh token and then you can avoid constant reauthentication. Look for an OAuth 2 library for your platform ideally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment