Skip to content

Instantly share code, notes, and snippets.

@imana97
Created February 1, 2019 20:43
Show Gist options
  • Save imana97/1acb846af3baca2a7d767e8d1eb7e367 to your computer and use it in GitHub Desktop.
Save imana97/1acb846af3baca2a7d767e8d1eb7e367 to your computer and use it in GitHub Desktop.
example of how to fetch data from fitbit.
/**
* 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