├── readme.md └── fitbit.js /readme.md: -------------------------------------------------------------------------------- 1 | This little script runs in the Google App Script environment. 2 | 3 | Specifically it runs in [Google Spreadsheets][0]. It lets you suck down your Fitbit data and the do all kinds of analysis. It's also an easy way to get started with the Fitbit API. 4 | 5 | Sadly to get started is a bit of a pain: 6 | 7 | 1. Create a new Google Spreadsheet. 8 | 2. Go to Tools-->Script Editor 9 | 3. Replace the template with fitbit.js & reload the spreadsheet 10 | 4. From the Fitbit menu that should appear, run the Configure option 11 | 5. Follow all the instructions given in the form that pops up 12 | 6. Run the "Authorize" menu option -- this will run through the oauth dance. 13 | 7. Run the 'Refresh fitbit Time Data" menu option to get your data 14 | 8. Profit! 15 | 16 | [0]: http://drive.google.com 17 | [1]: https://github.com/loghound/Fitbit-for-Google-App-Script 18 | -------------------------------------------------------------------------------- /fitbit.js: -------------------------------------------------------------------------------- 1 | // This script will pull down your fitbit data 2 | // and push it into a spreadsheet 3 | // Units are metric (kg, km) unless otherwise noted 4 | // Suggestions/comments/improvements? Let me know loghound@gmail.com 5 | // 6 | // 7 | /**** Length of time to look at. 8 | * From fitbit documentation values are 9 | * 1d, 7d, 30d, 1w, 1m, 3m, 6m, 1y, max. 10 | */ 11 | var period = "1y"; 12 | 13 | /** 14 | * Key of ScriptProperty for Fitbit consumer key. 15 | * @type {String} 16 | * @const 17 | */ 18 | var CLIENT_ID_PROPERTY_NAME = "fitbitClientID"; 19 | 20 | /** 21 | * Key of ScriptProperty for Fitbit consumer secret. 22 | * @type {String} 23 | * @const 24 | */ 25 | var CONSUMER_SECRET_PROPERTY_NAME = "fitbitConsumerSecret"; 26 | 27 | /** 28 | * Key of Project. 29 | * @type {String} 30 | * @const 31 | */ 32 | var PROJECT_KEY_PROPERTY_NAME = "projectKey"; 33 | 34 | 35 | /** 36 | * Default loggable resources. 37 | * 38 | * @type String[] 39 | * @const 40 | */ 41 | var LOGGABLES = [ "activities/log/steps", "activities/log/distance", 42 | "activities/log/activeScore", "activities/log/activityCalories", 43 | "activities/log/calories", "foods/log/caloriesIn", 44 | "activities/log/minutesSedentary", 45 | "activities/log/minutesLightlyActive", 46 | "activities/log/minutesFairlyActive", 47 | "activities/log/minutesVeryActive", "sleep/timeInBed", 48 | "sleep/minutesAsleep", "sleep/minutesAwake", "sleep/awakeningsCount", 49 | "body/weight", "body/bmi", "body/fat" ]; 50 | 51 | /** 52 | * Default fetchable periods. 53 | * 54 | * @type String[] 55 | * @const 56 | */ 57 | var PERIODS = [ "1d", "7d", "30d", "1w", "1m", "3m", "6m", "1y", "max" ]; 58 | 59 | /** 60 | * Instance of PropertiesService for access to ScriptProperties 61 | * 62 | * @type {Object} scriptProperties 63 | */ 64 | var scriptProperties = PropertiesService.getScriptProperties(); 65 | 66 | function refreshTimeSeries() { 67 | 68 | // if the user has never configured ask him to do it here 69 | if (!isConfigured()) { 70 | renderFitbitConfigurationDialog(); 71 | return; 72 | } 73 | 74 | Logger.log('Refreshing timeseries data...'); 75 | var user = authorize().user; 76 | Logger.log(user) 77 | var doc = SpreadsheetApp.getActiveSpreadsheet() 78 | doc.setFrozenRows(2); 79 | // header rows 80 | doc.getRange("a1").setValue(user.displayName); 81 | doc.getRange("a1").setNote("DOB:" + user.dateOfBirth); 82 | doc.getRange("b1").setValue( 83 | user.locale); 84 | // add the loggables for the last update 85 | doc.getRange("c1").setValue("Loggables:"); 86 | doc.getRange("c1").setNote(getLoggables()); 87 | // period for the last update 88 | doc.getRange("d1").setValue("Period: " + getPeriod()); 89 | doc.getRange("e1").setValue("=image(\"" + user.avatar + "\";1)"); 90 | 91 | // get inspired here http://wiki.fitbit.com/display/API/API-Get-Time-Series 92 | var activities = getLoggables(); 93 | for ( var activity in activities) { 94 | Logger.log('Refreshing ' + activity) 95 | var dateString = "today"; 96 | var currentActivity = activities[activity]; 97 | try { 98 | var service = getService(); 99 | 100 | var options = { 101 | "method" : "GET", 102 | "headers": { 103 | "Authorization": "Bearer " + service.getAccessToken() 104 | } 105 | }; 106 | 107 | if (service.hasAccess()) { 108 | var url = "https://api.fitbit.com/1/user/-/" 109 | + currentActivity + "/date/" + dateString + "/" 110 | + getPeriod() + ".json"; 111 | Logger.log(options) 112 | var result = UrlFetchApp.fetch(url, options); 113 | } 114 | } catch (exception) { 115 | Logger.log(exception); 116 | } 117 | Logger.log(result); 118 | var o = JSON.parse(result.getContentText()); 119 | 120 | // set title 121 | var titleCell = doc.getRange("a2"); 122 | titleCell.setValue("Date"); 123 | var cell = doc.getRange('a3'); 124 | 125 | // fill data 126 | for ( var i in o) { 127 | // set title for this column 128 | var title = i.substring(i.lastIndexOf('-') + 1); 129 | titleCell.offset(0, 1 + activity * 1.0).setValue(title); 130 | 131 | var row = o[i]; 132 | var row_index = 0; 133 | for ( var j in row) { 134 | var val = row[j]; 135 | 136 | // Convert the date from the API to a real GS date needed for finding the right row. 137 | var dateParts = val["dateTime"].split("-"); 138 | var date = new Date(dateParts[0], (dateParts[1]-1), dateParts[2], 0, 0, 0, 0); 139 | 140 | // Have we found a row yet? or do we need to look for it? 141 | if ( row_index != 0 ) { 142 | row_index++; 143 | } else { 144 | row_index = findRow(date); 145 | } 146 | // Insert Date into first column 147 | doc.getActiveSheet().getRange(row_index, 1).setValue(val["dateTime"]); 148 | // Insert value 149 | doc.getActiveSheet().getRange(row_index, 2 + activity * 1.0).setValue(Number(val["value"])); 150 | } 151 | } 152 | } 153 | } 154 | 155 | function isConfigured() { 156 | return getConsumerKey() != "" && getConsumerSecret() != ""; 157 | } 158 | 159 | /** 160 | * @return String OAuth consumer key to use when tweeting. 161 | */ 162 | function getConsumerKey() { 163 | var key = scriptProperties.getProperty(CLIENT_ID_PROPERTY_NAME); 164 | if (key == null) { 165 | key = ""; 166 | } 167 | return key; 168 | } 169 | 170 | /** 171 | * @param String OAuth consumer key to use when tweeting. 172 | */ 173 | function setConsumerKey(key) { 174 | scriptProperties.setProperty(CLIENT_ID_PROPERTY_NAME, key); 175 | } 176 | 177 | /** 178 | * @return String Project key 179 | */ 180 | function getProjectKey() { 181 | var key = scriptProperties.getProperty(PROJECT_KEY_PROPERTY_NAME); 182 | if (key == null) { 183 | key = ""; 184 | } 185 | return key; 186 | } 187 | 188 | /** 189 | * @param String Project key 190 | */ 191 | function setProjectKey(key) { 192 | scriptProperties.setProperty(PROJECT_KEY_PROPERTY_NAME, key); 193 | } 194 | 195 | /** 196 | * @param Array 197 | * of String for loggable resources, i.e. "foods/log/caloriesIn" 198 | */ 199 | function setLoggables(loggable) { 200 | scriptProperties.setProperty('loggables', loggable); 201 | } 202 | 203 | /** 204 | * Returns the loggable resources as String[] 205 | * 206 | * @return String[] loggable resources 207 | */ 208 | function getLoggables() { 209 | var loggable = scriptProperties.getProperty('loggables'); 210 | if (loggable == null) { 211 | loggable = LOGGABLES; 212 | } else { 213 | loggable = loggable.split(','); 214 | } 215 | return loggable; 216 | } 217 | 218 | function setPeriod(period) { 219 | scriptProperties.setProperty('period', period); 220 | } 221 | 222 | function getPeriod() { 223 | var period = scriptProperties.getProperty('period'); 224 | if (period == null) { 225 | period = "30d"; 226 | } 227 | return period; 228 | } 229 | 230 | /** 231 | * @return String OAuth consumer secret to use when tweeting. 232 | */ 233 | function getConsumerSecret() { 234 | var secret = scriptProperties.getProperty(CONSUMER_SECRET_PROPERTY_NAME); 235 | if (secret == null) { 236 | secret = ""; 237 | } 238 | return secret; 239 | } 240 | 241 | /** 242 | * @param String OAuth consumer secret to use when tweeting. 243 | */ 244 | function setConsumerSecret(secret) { 245 | scriptProperties.setProperty(CONSUMER_SECRET_PROPERTY_NAME, secret); 246 | } 247 | 248 | /** Retrieve config params from the UI and store them. */ 249 | function saveConfiguration(e) { 250 | 251 | setConsumerKey(e.parameter.clientID); 252 | setConsumerSecret(e.parameter.consumerSecret); 253 | setProjectKey(e.parameter.projectKey); 254 | setLoggables(e.parameter.loggables); 255 | setPeriod(e.parameter.period); 256 | var app = UiApp.getActiveApplication(); 257 | app.close(); 258 | return app; 259 | } 260 | /** 261 | * Configure all UI components and display a dialog to allow the user to 262 | * configure approvers. 263 | */ 264 | function renderFitbitConfigurationDialog() { 265 | var doc = SpreadsheetApp.getActiveSpreadsheet(); 266 | var app = UiApp.createApplication().setTitle("Configure Fitbit"); 267 | app.setStyleAttribute("padding", "10px"); 268 | app.setHeight('380'); 269 | 270 | var helpLabel = app 271 | .createLabel("From here you will configure access to fitbit -- Just supply your own" 272 | + "client id and secret from dev.fitbit.com. " 273 | + " You can find the project key by loading the script in the script editor" 274 | + " (tools->Script Editor..) and opening the project properties (file->Project properties). \n\n" 275 | + " While in the script editor, you also need to add the OAuth2 library following these instructions: https://github.com/googlesamples/apps-script-oauth2/tree/0e7bcd464962321a75ccb97256d5373b27c4c2e1#setup. \n\n" 276 | + " You also need to setup your Redirect URI at fitbit, substituting in your project key you just found and " 277 | + "using the instructions here: https://github.com/googlesamples/apps-script-oauth2/tree/0e7bcd464962321a75ccb97256d5373b27c4c2e1#redirect-uri \n\n" 278 | + "Important: To authorize this app you need to run 'Authorize' from the fitbit menu."); 279 | helpLabel.setStyleAttribute("text-align", "justify"); 280 | helpLabel.setWidth("95%"); 281 | var consumerKeyLabel = app.createLabel("Fitbit OAuth 2.0 Client ID:"); 282 | var consumerKey = app.createTextBox(); 283 | consumerKey.setName("clientID"); 284 | consumerKey.setWidth("100%"); 285 | consumerKey.setText(getConsumerKey()); 286 | var consumerSecretLabel = app.createLabel("Fitbit OAuth Client (Consumer) Secret:"); 287 | var consumerSecret = app.createTextBox(); 288 | consumerSecret.setName("consumerSecret"); 289 | consumerSecret.setWidth("100%"); 290 | consumerSecret.setText(getConsumerSecret()); 291 | var projectKeyLabel = app.createLabel("Project Key:"); 292 | var projectKey = app.createTextBox(); 293 | projectKey.setName("projectKey"); 294 | projectKey.setWidth("100%"); 295 | projectKey.setText(getProjectKey()); 296 | 297 | var saveHandler = app.createServerClickHandler("saveConfiguration"); 298 | var saveButton = app.createButton("Save Configuration", saveHandler); 299 | 300 | var listPanel = app.createGrid(6, 3); 301 | listPanel.setWidget(1, 0, consumerKeyLabel); 302 | listPanel.setWidget(1, 1, consumerKey); 303 | listPanel.setWidget(2, 0, consumerSecretLabel); 304 | listPanel.setWidget(2, 1, consumerSecret); 305 | listPanel.setWidget(3, 0, projectKeyLabel); 306 | listPanel.setWidget(3, 1, projectKey); 307 | 308 | // add checkboxes to select loggables 309 | var loggables = app.createListBox(true).setId("loggables").setName("loggables"); 310 | loggables.setVisibleItemCount(3); 311 | var current_loggables = getLoggables(); 312 | for ( var resource in LOGGABLES) { 313 | loggables.addItem(LOGGABLES[resource]); 314 | if (current_loggables.indexOf(LOGGABLES[resource]) > -1) { 315 | loggables.setItemSelected(parseInt(resource), true); 316 | } 317 | } 318 | listPanel.setWidget(4, 0, app.createLabel("Resources:")); 319 | listPanel.setWidget(4, 1, loggables); 320 | 321 | var period = app.createListBox(false).setId("period").setName("period"); 322 | period.setVisibleItemCount(1); 323 | // add valid timeperiods 324 | for ( var resource in PERIODS) { 325 | period.addItem(PERIODS[resource]); 326 | } 327 | period.setSelectedIndex(PERIODS.indexOf(getPeriod())); 328 | listPanel.setWidget(5, 0, app.createLabel("Period:")); 329 | listPanel.setWidget(5, 1, period); 330 | 331 | // Ensure that all form fields get sent along to the handler 332 | saveHandler.addCallbackElement(listPanel); 333 | 334 | var dialogPanel = app.createFlowPanel(); 335 | dialogPanel.add(helpLabel); 336 | dialogPanel.add(listPanel); 337 | dialogPanel.add(saveButton); 338 | app.add(dialogPanel); 339 | doc.show(app); 340 | } 341 | 342 | function getService() { 343 | //Implement updated OAuth 2 support 344 | //When using new OAuth1 library, callback URL length is too long, therefore doesn't work: 345 | // https://github.com/googlesamples/apps-script-oauth1/issues/8 346 | // 347 | //Fitbit API: 348 | //https://wiki.fitbit.com/display/API/OAuth+2.0 349 | //Google App Script OAuth2 instructions: 350 | //https://github.com/googlesamples/apps-script-oauth2 351 | 352 | //modified from : https://github.com/googlesamples/apps-script-oauth1/issues/8#issuecomment-100309694 353 | 354 | return OAuth2.createService('fitbit') 355 | .setAuthorizationBaseUrl('https://www.fitbit.com/oauth2/authorize') 356 | .setTokenUrl('https://api.fitbit.com/oauth2/token') 357 | .setClientId(getConsumerKey()) 358 | .setClientSecret(getConsumerSecret()) 359 | .setProjectKey(getProjectKey()) 360 | .setCallbackFunction('fitbitAuthCallback') 361 | .setPropertyStore(PropertiesService.getScriptProperties()) 362 | .setScope('activity') 363 | .setTokenHeaders({ 364 | 'Authorization': 'Basic ' + Utilities.base64Encode(getConsumerKey() + ':' + getConsumerSecret()) 365 | }); 366 | 367 | } 368 | 369 | function authorize() { 370 | var service = getService() 371 | 372 | if (service.hasAccess()) { 373 | var url = 'https://api.fitbit.com/1/user/-/profile.json'; 374 | var response = UrlFetchApp.fetch(url, { 375 | headers: { 376 | 'Authorization': 'Bearer ' + service.getAccessToken() 377 | } 378 | }); 379 | Logger.log(JSON.stringify(JSON.parse(response.getContentText()), null, 2)); 380 | return JSON.parse(response.getContentText()) 381 | } else { 382 | var authorizationUrl = service.getAuthorizationUrl(); 383 | var template = HtmlService.createTemplate( 384 | 'Authorize. ' + 385 | 'Reopen the sidebar when the authorization is complete.'); 386 | template.authorizationUrl = authorizationUrl; 387 | var page = template.evaluate(); 388 | SpreadsheetApp.getUi().showSidebar(page); 389 | } 390 | } 391 | 392 | // modified from: https://github.com/googlesamples/apps-script-oauth1/tree/9d074adc735e35c8966bcfa30114c205d69ab44e#3-handle-the-callback 393 | function fitbitAuthCallback(request) { 394 | var service = getService(); 395 | var isAuthorized = service.handleCallback(request); 396 | if (isAuthorized) { 397 | return HtmlService.createHtmlOutput('Success! You can close this page.'); 398 | } else { 399 | return HtmlService.createHtmlOutput('Denied. You can close this page'); 400 | } 401 | } 402 | 403 | /** When the spreadsheet is opened, add a Fitbit menu. */ 404 | function onOpen() { 405 | var ss = SpreadsheetApp.getActiveSpreadsheet(); 406 | var menuEntries = [{ 407 | name: "Refresh fitbit Time Data", 408 | functionName: "refreshTimeSeries" 409 | }, 410 | { 411 | name: "Configure", 412 | functionName: "renderFitbitConfigurationDialog" 413 | }, 414 | { 415 | name: "Authorize", 416 | functionName: "authorize" 417 | }]; 418 | ss.addMenu("Fitbit", menuEntries); 419 | } 420 | 421 | function onInstall() { 422 | onOpen(); 423 | // put the menu when script is installed 424 | } 425 | 426 | // Find the right row for a date. 427 | function findRow(date) { 428 | var doc = SpreadsheetApp.getActiveSpreadsheet(); 429 | var cell = doc.getRange("A3"); 430 | 431 | // Find the first cell in first column which is either empty, 432 | // or has an equal or bigger date than the one we are looking for. 433 | while ((cell.getValue() != "") && (cell.getValue() < date)) { 434 | cell = cell.offset(1,0); 435 | } 436 | // If the cell we found has a newer date than ours, we need to 437 | // insert a new row right before that. 438 | if (cell.getValue() > date) { 439 | doc.insertRowBefore(cell.getRow()) 440 | } 441 | // return only the number of the row. 442 | return (cell.getRow()); 443 | } 444 | --------------------------------------------------------------------------------