Created
February 1, 2019 20:43
-
-
Save imana97/1acb846af3baca2a7d767e8d1eb7e367 to your computer and use it in GitHub Desktop.
example of how to fetch data from fitbit.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Contact Info: | |
* Created by imano on 6/27/17. | |
*/ | |
var express = require('express'); | |
var router = express.Router(); | |
var FitbitApiClient = require('fitbit-node'); | |
var config = require('../config-server'); | |
var cookieParser = require('cookie-parser'); | |
var moment = require('moment'); | |
var logger = require('../helper/logger'); | |
var cc = require('../helper/console-color-ref'); | |
var clog = require('../helper/clog'); | |
const settings = require('../settings'); | |
router.use(cookieParser()); | |
var CLIENT_ID = '******'; | |
var CLIENT_SECRET = '*********'; | |
var fitbit = new FitbitApiClient({ | |
clientId: CLIENT_ID, | |
clientSecret: CLIENT_SECRET, | |
apiVersion: '1.2' | |
}); | |
// parse objects | |
var Notification = Parse.Object.extend('HC_notification'); | |
var SensorSubscription = Parse.Object.extend('HC_sensor_subscription'); | |
/** | |
* url to authenticate fitbit with deephealth | |
*/ | |
router.get('/auth/:userId', function (req, res) { | |
res.cookie('fitbitUser', req.params.userId); | |
//TODO: BECAUSE THIS REDIRECTS TO NUCOACH, IT CAUSES A PROBLEM IN COOKIES. update the app. | |
res.redirect(fitbit.getAuthorizeUrl( | |
'activity heartrate location nutrition profile settings sleep social weight', | |
config.serverURL + '/sensors/fitbit/callback')); | |
}); | |
/** | |
* create subscription, once the callback is called. | |
*/ | |
router.get('/callback', function (req, res) { | |
var userId = req.cookies.fitbitUser || -1; | |
fitbit.getAccessToken(req.query.code, config.serverURL + '/sensors/fitbit/callback').then(function (result) { | |
// use the access token to fetch the user's profile information | |
// if we are here, we have the token and user ID | |
new Parse.Query(Parse.User) | |
.include('personalProfile.fitbit') | |
.get(userId, { | |
useMasterKey: true, | |
success: function (user) { | |
// success | |
if (user) { | |
// user exist | |
console.log('user exist'); | |
if (user.get('personalProfile').get('fitbit')) { | |
console.log('fitbit subscription already exist, update the token'); | |
user.get('personalProfile').get('fitbit').set('auth', result) | |
.save(null, { | |
useMasterKey: true, | |
success: function () { | |
new Notification() | |
.set('user', user) | |
.set('title', 'NUcoach') | |
.set('body', 'Successfully updated your Fitbit subscription.') | |
.save(null, {useMasterKey: true}); | |
res.send('successfully updated your Fitbit subscription. You may close this window'); | |
}, error: function (o, e) { | |
res.send(e); | |
} | |
}); | |
} else { | |
// fitbit subscription does not exist, create one | |
var sub = new SensorSubscription(); | |
sub.set('user', user) | |
.set('sensor', { | |
"__type": "Pointer", | |
"className": "DHL_sensor", | |
"objectId": "XIfI16u3hb" | |
}) | |
.set('auth', result) | |
.set('deleted', false); | |
var acl = new Parse.ACL(user); | |
// let the sensorSubscriptions role read the subscriptions. | |
acl.setRoleReadAccess('sensorSubscriptions', true); | |
sub.setACL(acl); | |
sub.save(null, { | |
useMasterKey: true, | |
success: function (newSubscription) { | |
// subscription created | |
user.get('personalProfile').set('fitbit', newSubscription) | |
.save(null, { | |
useMasterKey: true, | |
success: function () { | |
new Notification() | |
.set('user', user) | |
.set('title', 'NUcoach') | |
.set('body', 'Successfully linked your Fitbit account.') | |
.save(null, {useMasterKey: true}); | |
res.send('successfully linked your Fitbit account. You may close this window'); | |
}, error: function (o, e) { | |
res.send(e); | |
} | |
}); | |
}, | |
error: function (o, e) { | |
res.send(e); | |
} | |
}); | |
} | |
} else { | |
res.send('User does not exist'); | |
} | |
}, error: function (u, e) { | |
// cant get the user | |
res.send(e); | |
} | |
}); | |
}).catch(function (error) { | |
res.send(error); | |
}); | |
}); | |
// todo: work in progress. | |
var FitbitSync = function (subscr) { | |
// fitbit summary class | |
var _Summary = Parse.Object.extend('DHL_fitbit_summary'); | |
var _Sleep = Parse.Object.extend('DHL_fitbit_sleep'); | |
// subscription object with sensor and user info | |
var _subscription = subscr; | |
// get fitbit access token from the subscription | |
var _auth = _subscription.get('auth'); | |
// today date in YYYY-MM-DD format | |
var _day = moment().format('YYYY-MM-DD'); | |
// today date in YYYYMMDD format , (number) | |
var _dayNumber = parseInt(moment().format('YYYYMMDD')); | |
/** | |
* This function should update token | |
* @param newToken | |
* @private | |
*/ | |
var _updateToken = function (newToken) { | |
_auth = newToken; | |
_subscription.set('auth', _auth) | |
.save(null, { | |
useMasterKey: true, | |
success: function () { | |
logger.log('Updated token for (subscription ID): ' + _subscription.id, ['fitbit', 'subscription', 'token', 'update']); | |
}, | |
error: function (obj, err) { | |
logger.log('Error updating token:' + err.message, ['error', 'fitbit', 'subscription', 'token', 'update']); | |
} | |
}); | |
}; | |
/** | |
* get data from fitbit api. | |
* @param path | |
* @param resolve | |
* @param reject | |
* @private | |
*/ | |
var _getData = function (path, resolve, reject) { | |
clog('getting data for', path, cc.BgCyan, 'FITBIT PATH'); | |
// First, we try to get the data with the current access token available. | |
fitbit.get(path, _auth.access_token).then(function (result) { | |
if ((result[0].errors && result[0].errors.length > 0 && result[0].errors[0].errorType === 'expired_token')) { | |
console.log('we are here...'); | |
logger.log('token expired for' + _subscription.id, ['fitbit', 'subscription', 'token', 'expired']); | |
fitbit.refreshAccessToken(_auth.access_token, _auth.refresh_token, 28800).then(function (newToken) { | |
clog('refreshing token', newToken.FgYellow, 'Refresh'); | |
_updateToken(newToken); | |
fitbit.get(path, _auth.access_token).then(function (result) { | |
resolve(result[0]); | |
}, function (err) { | |
reject(err); | |
clog('getting new token: ', error, cc.FgRed, 'after token update'); | |
}); | |
}, function (error) { | |
logger.log('token update faild:' + JSON.stringify(error), ['fitbit', 'subscription', 'token', 'update', 'fail']); | |
reject(error); | |
}); | |
} else { | |
// token is valid | |
resolve(result[0]); | |
} | |
}, function (err) { | |
logger.log(JSON.stringify(err), ['fitbit', 'getData', 'http', 'fail']); | |
reject(err); | |
}); | |
}; | |
/** | |
* get data in an async function | |
*/ | |
/** | |
* get data from fitbit api. | |
* @param path | |
* @param resolve | |
* @param reject | |
* @private | |
*/ | |
const _asyncGetData = function (path) { | |
return new Promise(function (resolve, reject) { | |
// First, we try to get the data with the current access token available. | |
fitbit.get(path, _auth.access_token).then(function (result) { | |
if ((result[0].errors && result[0].errors.length > 0 && result[0].errors[0].errorType === 'expired_token')) { | |
reject({message: 'token expired'}); | |
} else { | |
// token is valid | |
resolve(result[0]); | |
} | |
}, function (err) { | |
reject(err); | |
}); | |
}); | |
}; | |
/** | |
* check if reminder should be sent to the user. | |
* @private | |
*/ | |
var _reminder = function () { | |
try { | |
// if last sync does not exists, check updated at... | |
var dateToCheck = _subscription.get('updatedAt'); | |
if (Date.now() > (dateToCheck.getTime() + settings.sensors.fitbit.reminderSendThresholdMS)) { | |
// more than 12 hours since the last sync | |
console.log('send Notification'); | |
// send notification | |
var text = ""; | |
try { | |
text += moment((_subscription.get('lastSync') || _subscription.get('updatedAt'))).fromNow() + " you walked " + | |
(_subscription.get('lastData').steps || 2100) + " steps. "; | |
} catch (error) { | |
logger.log(error.message, ['fitbit', 'error', 'reminder']); | |
} | |
text += "Please open Fitbit application and check your today's steps"; | |
// send notification | |
new Notification() | |
.set('user', _subscription.get('user')) | |
.set('title', 'NUcoach') | |
.set('body', text) | |
.save(null, {useMasterKey: true}); | |
// update last sync, so we don't send a new one. | |
_subscription.save(null, {useMasterKey: true}); | |
} else { | |
console.info(Date.now() - (dateToCheck.getTime() + 43200000)); | |
console.log('no notification sent. less than 12 hours'); | |
} | |
} catch (error) { | |
console.log(cc.FgRed, error.message); | |
logger.log(error.message, ['fitbit', 'error', 'reminder']); | |
} | |
}; | |
/** | |
* Get the summary data for a day | |
* @param day the day number in yyyyMMdd format | |
* @param success | |
* @param error | |
* @private | |
*/ | |
var _getFitbitSummary = function (day, success, error) { | |
_getData('/activities/date/' + day + '.json', function (summaryObj) { | |
if (summaryObj.errors) { | |
error(summaryObj.errors); | |
} else { | |
success(summaryObj.summary); | |
} | |
// todo: implement error | |
}, function (error) { | |
clog('fetch data error: ', JSON.stringify(error), cc.FgRed, 'update token error'); | |
}); | |
}; | |
/** | |
* For the time being, this function never completes. | |
* @param day | |
* @return {Promise<void>} | |
* @private | |
*/ | |
const _syncFitbitSleep = async (day,final=false) => { | |
try { | |
const sleepData = await _asyncGetData('/sleep/date/' + day + '.json'); | |
// there are sleep data available for this day, so we proceed. | |
if (sleepData.sleep && Array.isArray(sleepData.sleep) && sleepData.summary) { | |
// see if this object exist | |
const parseObj = await new Parse.Query(_Sleep) | |
.equalTo('day', _dayNumber) | |
.equalTo('subscription', _subscription) | |
.first({useMasterKey: true}); | |
if (parseObj) { | |
// update | |
await parseObj | |
.increment('updateCount') | |
.set('sleeps', sleepData.sleep) | |
.set('completed', final) | |
.set('totalMinutesAsleep', sleepData.summary.totalMinutesAsleep) | |
.set('totalSleepRecords', sleepData.summary.totalSleepRecords) | |
.set('totalTimeInBed', sleepData.summary.totalTimeInBed) | |
.save(null, {useMasterKey: true}); | |
} else { | |
// create new object | |
const acl = new Parse.ACL(_subscription.get('user')); | |
acl.setRoleReadAccess('DHL_fitbit_sleep', true); | |
await new _Sleep() | |
.set('day', _dayNumber) | |
.set('subscription', _subscription) | |
.set('user', _subscription.get('user')) | |
.set('updateCount', 1) | |
.set('completed', final) | |
.set('sleeps', sleepData.sleep) | |
.set('totalMinutesAsleep', sleepData.summary.totalMinutesAsleep) | |
.set('totalSleepRecords', sleepData.summary.totalSleepRecords) | |
.set('totalTimeInBed', sleepData.summary.totalTimeInBed) | |
.setACL(acl) | |
.save(null, {useMasterKey: true}); | |
} | |
} | |
} catch (error) { | |
logger.log(error.message, ['fitbit', 'error', 'sleep', 'sync']); | |
clog('fetch data error: ', JSON.stringify(error), cc.FgRed, 'update token error'); | |
} | |
}; | |
/** | |
* get the class summary obj from parse db. | |
* @param success The success callback | |
* @param error The error callback | |
* @private | |
*/ | |
var _getParseSummary = function (success, error) { | |
new Parse.Query(_Summary) | |
.equalTo('day', _dayNumber) | |
.equalTo('subscription', _subscription) | |
.first({useMasterKey: true, success: success, error: error}); | |
}; | |
/** | |
* compare fitbit data object with parse data object. | |
* @param fitbitData | |
* @param parseObj | |
* @returns {boolean} | |
* @private | |
*/ | |
var _fitbitDataEqualParseObj = function (fitbitData, parseObj) { | |
var flag = 0; | |
for (var i in fitbitData) { | |
console.log(cc.FgWhite, i, parseObj.get(i), fitbitData[i]); | |
if (parseObj.get(i) == fitbitData[i]) { | |
flag++; | |
console.log('flag updated'); | |
} | |
} | |
console.log('flag number is ', flag); | |
// flag is usually 1 because calories update regardless of sync | |
if (flag <= 0) { | |
return false; | |
} else { | |
return true; | |
} | |
}; | |
/** | |
* update a fitbit summary object with summary data. | |
* @param summaryObj Parse summary object | |
* @param summaryData Fitbit summary data object | |
* @param subsWrite Set to True if this is the last update. | |
* @private | |
*/ | |
var _updateSummaryObj = function (summaryObj, summaryData, subsWrite) { | |
clog('updating summary data', summaryData, cc.FgCyan, 'Updating summary'); | |
for (var i in summaryData) { | |
console.log(typeof (summaryData[i])); | |
if (typeof (summaryData[i]) !== 'object') { | |
console.log(summaryData[i], ' updated'); | |
summaryObj.set(i, summaryData[i]); | |
} | |
} | |
// update object count | |
summaryObj.increment('updateCount'); | |
if (subsWrite) { | |
summaryObj.set('completed', true); | |
} | |
summaryObj.save(null, { | |
useMasterKey: true, | |
success: function () { | |
console.log(cc.FgGreen, 'object updated'); | |
// update the last data of subscription only of it does not belong to previous days... | |
if (!subsWrite) { | |
_subscription.set('lastData', summaryData).set('lastSync', new Date()).save(null, {useMasterKey: true}); | |
} | |
// object updated. run interval | |
}, error: function (o, e) { | |
console.log(cc.FgRed, e); | |
// error updating the summary object | |
} | |
}); | |
}; | |
/** | |
* Create a new Parse summary object with the given fitbit summary Data. | |
* @param summaryData Fitbit summary data object. | |
* @private | |
*/ | |
var _createSummaryObj = function (summaryData) { | |
var summary = new _Summary(); | |
// initial values | |
summary.set('updateCount', 1) | |
.set('day', _dayNumber) | |
.set('errorCount', 0) | |
.set('completed', false) | |
.set('subscription', _subscription) | |
.set('user', _subscription.get('user')); | |
// ACL | |
var acl = new Parse.ACL(_subscription.get('user')); | |
acl.setRoleReadAccess('DHL_fitbit_summary', true); | |
summary.setACL(acl); | |
// sensor data | |
for (var n in summaryData) { | |
if (typeof summaryData[n] !== 'object') { | |
summary.set(n, summaryData[n]); | |
} | |
} | |
summary.save(null, { | |
useMasterKey: true, | |
success: function () { | |
_subscription.set('lastData', summaryData).save(null, {useMasterKey: true}); | |
} | |
}); | |
}; | |
/** | |
* This method fetch all incomplete parse summary objects and sync them with the latest data available on fitbit | |
* cloud. | |
* @private | |
*/ | |
var _syncPreviousSummaries = function () { | |
// query all incomplete objects of the Summary class except the current day. | |
new Parse.Query(_Summary) | |
.equalTo('completed', false) | |
.equalTo('subscription', _subscription) | |
.notEqualTo('day', _dayNumber) | |
.find({ | |
useMasterKey: true, | |
success: function (needToSyncObjects) { | |
needToSyncObjects.forEach(function (needToSync) { | |
try { | |
// get the day number from the object and convert the format from yyyyMMdd to yyyy-MM-dd | |
var xDay = moment(needToSync.get('day'), 'YYYYMMDD').format('YYYY-MM-DD'); | |
_getFitbitSummary(xDay, function (summaryData) { | |
if (!summaryData.errors) { | |
_updateSummaryObj(needToSync, summaryData, true); | |
} else { | |
logger.log('can not fetch the previous data data for day ' + xDay, ['fitbit', 'error', 'summary', 'sync', 'http', 'previous_day']); | |
} | |
}, function (error) { | |
console.log(error); | |
}) | |
} catch (error) { | |
console.log(error.message); | |
logger.log(error.message, ['fitbit', 'error', 'summary', 'sync', 'previous_day', 'try_catch']); | |
} | |
}); | |
}, error: function (obj, err) { | |
console.log(err.message); | |
logger.log(error.message, ['fitbit', 'error', 'summary', 'sync', 'incomplete', 'previous_day']); | |
} | |
}); | |
}; | |
const _syncPreviousSleeps = async () => { | |
try { // query all incomplete objects of the Summary class except the current day. | |
const needToSyncObjects = new Parse.Query(_Sleep) | |
.equalTo('completed', false) | |
.equalTo('subscription', _subscription) | |
.notEqualTo('day', _dayNumber) | |
.find({useMasterKey: true}); | |
for (const needToSync of needToSyncObjects) { | |
try { | |
// get the day number from the object and convert the format from yyyyMMdd to yyyy-MM-dd | |
const xDay = moment(needToSync.get('day'), 'YYYYMMDD').format('YYYY-MM-DD'); | |
await _syncFitbitSleep(xDay, true); | |
} catch (error) { | |
console.log(error.message); | |
logger.log(error.message, ['fitbit', 'error', 'sleep', 'sync', 'previous_day', 'try_catch']); | |
} | |
} | |
} catch (e) { | |
console.log(e.message); | |
} | |
}; | |
/** | |
* tries to sync fitbit summary object with parse summary object. | |
*/ | |
this.syncSummary = function () { | |
// first, get the summary data from fitbit | |
_getFitbitSummary(_day, function (summaryData) { | |
// if error exist | |
if (summaryData.errors) { | |
clog('Error in summary data', summaryData, cc.FgRed, 'Error'); | |
logger.log(summaryData.message, ['fitbit', 'error', 'summary', 'sync']); | |
} else { | |
clog('Summary data is valid', summaryData, cc.FgGreen, 'fitbit data'); | |
// we have the data here. Let's see if the user has synced already | |
_getParseSummary(function (summaryObj) { | |
console.log(cc.FgGreen, 'summary object is', summaryObj); | |
clog('Summary object is', summaryObj, cc.FgCyan, 'parse data'); | |
if (summaryObj) { | |
clog('Summary object exist'); | |
if (_fitbitDataEqualParseObj(summaryData, summaryObj)) { | |
// no new data, check reminder | |
clog('no new data, trigger reminder', 1, cc.FgRed); | |
// no new data, setup the reminder | |
_reminder(); | |
} else { | |
clog('update available', '', cc.FgRed, 'update available'); | |
// update summary obj | |
_updateSummaryObj(summaryObj, summaryData); | |
// sync previous incomplete summaries... | |
_syncPreviousSummaries(); | |
// sync previous incompleted sleeps sessions... | |
_syncPreviousSleeps(); | |
} | |
} else { | |
console.log(cc.FgRed, 'summary object NOT exist'); | |
_createSummaryObj(summaryData); | |
} | |
}, function (obj, err) { | |
console.log(cc.FgRed, err); | |
logger.log(err.message, ['fitbit', 'error', 'summary', 'sync']); | |
}); | |
} | |
}, function (error) { | |
clog('can not get fitbit summary', error.message, cc.FgRed, 'Error'); | |
logger.log(error.message, ['fitbit', 'error', 'summary', 'sync', 'http']); | |
}); | |
// In parallel, sync sleep | |
_syncFitbitSleep(_day); | |
}; // end of syncSummary | |
}; | |
/** | |
* SYNC INTERVAL | |
* | |
* Sync misfit every N milliseconds. | |
*/ | |
setInterval(function () { | |
new Parse.Query('HC_sensor_subscription') | |
.include('user.personalProfile') | |
.equalTo('sensor', {"__type": "Pointer", "className": "DHL_sensor", "objectId": "XIfI16u3hb"}) // fitbit sensor object | |
.equalTo('deleted', false) | |
.include('sensor') | |
.find({ | |
useMasterKey: true, | |
success: function (subscriptions) { | |
// new log for the number of subscriptions | |
logger.log('for ' + subscriptions.length, ['fitbit', 'sync', 'schedule']); | |
subscriptions.forEach(function (subscription, key) { | |
setTimeout(function () { | |
new FitbitSync(subscription).syncSummary(); | |
}, key * 5000); | |
}); | |
} | |
}); | |
}, settings.sensors.fitbit.dataFetchIntervalMS); | |
// COOKIE TEST. | |
// export routers | |
module.exports = {router: router}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment