diff --git a/nightscout.js b/nightscout.js index b95833f..290e1cc 100644 --- a/nightscout.js +++ b/nightscout.js @@ -6,111 +6,6 @@ var crypto = require('crypto'), var logger = require('./logger'); -var STALE_DATA_THRESHOLD_MINUTES = 20; -var PUMP_STATUS_ENTRY_TYPE = 'pump_status'; -var SENSOR_GLUCOSE_ENTRY_TYPE = 'sgv'; - -function parsePumpTime(pumpTimeString, offset) { - return Date.parse(pumpTimeString + ' ' + offset); -} - -function addTimeToEntry(timestamp, entry) { - entry['date'] = timestamp; - entry['dateString'] = new Date(timestamp).toISOString(); - return entry; -} - -var guessPumpOffset = (function() { - var lastGuess; - - // From my observations, sMedicalDeviceTime is advanced by the server even when the app is - // not reporting data or the pump is not connected, so its difference from server time is - // always close to a whole number of hours, and can be used to guess the pump's timezone: - // https://gist.github.com/mddub/f673570e6427c93784bf - return function(data) { - var pumpTimeAsIfUTC = Date.parse(data['sMedicalDeviceTime'] + ' +0'); - var serverTimeUTC = data['currentServerTime']; - var hours = Math.round((pumpTimeAsIfUTC - serverTimeUTC) / (60*60*1000)); - var offset = (hours >= 0 ? '+' : '-') + (Math.abs(hours) < 10 ? '0' : '') + Math.abs(hours) + '00'; - if (offset !== lastGuess) { - logger.log('Guessed pump timezone ' + offset + ' (pump time: "' + data['sMedicalDeviceTime'] + '"; server time: ' + new Date(data['currentServerTime']) + ')'); - } - lastGuess = offset; - return offset; - }; -})(); - -function pumpStatusEntry(data) { - var entry = {'type': PUMP_STATUS_ENTRY_TYPE}; - - [ - 'conduitBatteryLevel', - 'conduitInRange', - 'conduitMedicalDeviceInRange', - 'reservoirLevelPercent', - 'reservoirAmount', - 'medicalDeviceBatteryLevelPercent' - ].forEach(function(key) { - if(data[key] !== undefined) { - entry[key] = data[key]; - } - }); - - if(data['activeInsulin'] && data['activeInsulin']['amount'] >= 0) { - entry['activeInsulin'] = data['activeInsulin']['amount']; - } - - return addTimeToEntry(data['lastMedicalDeviceDataUpdateServerTime'], entry); -} - -function sgvEntries(data) { - var offset = guessPumpOffset(data); - - if(data['sgs'] && data['sgs'].length) { - return data['sgs'].filter(function(entry) { - return entry['kind'] === 'SG' && entry['sg'] !== 0; - }).map(function(sgv) { - return addTimeToEntry( - parsePumpTime(sgv['datetime'], offset), - { - 'type': SENSOR_GLUCOSE_ENTRY_TYPE, - 'sgv': sgv['sg'], - } - ); - }); - } else { - return []; - } -} - -var transform = module.exports.transform = function(data, sgvLimit) { - var recency = (data['currentServerTime'] - data['lastMedicalDeviceDataUpdateServerTime']) / (60 * 1000); - if (recency > STALE_DATA_THRESHOLD_MINUTES) { - logger.log('Stale CareLink data: ' + recency.toFixed(2) + ' minutes old'); - return []; - } - - if (sgvLimit === undefined) { - sgvLimit = Infinity; - } - - var entries = []; - - entries.push(pumpStatusEntry(data)); - - var sgvs = sgvEntries(data); - // TODO: this assumes sgvs are ordered by date ascending - for(var i = Math.max(0, sgvs.length - sgvLimit); i < sgvs.length; i++) { - entries.push(sgvs[i]); - } - - entries.forEach(function(entry) { - entry['device'] = 'MiniMed Connect ' + data['medicalDeviceFamily'] + ' ' + data['medicalDeviceSerialNumber']; - }); - - return entries; -}; - var upload = module.exports.upload = function(entries, endpoint, secret, callback) { logger.log('POST ' + endpoint + ' ' + JSON.stringify(entries)); request.post( diff --git a/run.js b/run.js index 4ad3b16..c88c10a 100644 --- a/run.js +++ b/run.js @@ -2,7 +2,8 @@ "use strict"; var carelink = require('./carelink'), - nightscout = require('./nightscout'); + nightscout = require('./nightscout'), + transform = require('./transform'); function readEnv(key, defaultVal) { var val = process.env[key] || @@ -37,7 +38,7 @@ var endpoint = (config.nsBaseUrl ? config.nsBaseUrl : 'https://' + config.nsHost if (err) { throw new Error(err); } else { - var entries = nightscout.transform(data, config.sgvLimit); + var entries = transform(data, config.sgvLimit); if (entries.length > 0) { nightscout.upload(entries, endpoint, config.nsSecret, function(err, response) { if (err) { diff --git a/transform.js b/transform.js new file mode 100644 index 0000000..489ba22 --- /dev/null +++ b/transform.js @@ -0,0 +1,109 @@ +/* jshint node: true */ +"use strict"; + +var logger = require('./logger'); + +var STALE_DATA_THRESHOLD_MINUTES = 20; +var PUMP_STATUS_ENTRY_TYPE = 'pump_status'; +var SENSOR_GLUCOSE_ENTRY_TYPE = 'sgv'; + +function parsePumpTime(pumpTimeString, offset) { + return Date.parse(pumpTimeString + ' ' + offset); +} + +function addTimeToEntry(timestamp, entry) { + entry['date'] = timestamp; + entry['dateString'] = new Date(timestamp).toISOString(); + return entry; +} + +var guessPumpOffset = (function() { + var lastGuess; + + // From my observations, sMedicalDeviceTime is advanced by the server even when the app is + // not reporting data or the pump is not connected, so its difference from server time is + // always close to a whole number of hours, and can be used to guess the pump's timezone: + // https://gist.github.com/mddub/f673570e6427c93784bf + return function(data) { + var pumpTimeAsIfUTC = Date.parse(data['sMedicalDeviceTime'] + ' +0'); + var serverTimeUTC = data['currentServerTime']; + var hours = Math.round((pumpTimeAsIfUTC - serverTimeUTC) / (60*60*1000)); + var offset = (hours >= 0 ? '+' : '-') + (Math.abs(hours) < 10 ? '0' : '') + Math.abs(hours) + '00'; + if (offset !== lastGuess) { + logger.log('Guessed pump timezone ' + offset + ' (pump time: "' + data['sMedicalDeviceTime'] + '"; server time: ' + new Date(data['currentServerTime']) + ')'); + } + lastGuess = offset; + return offset; + }; +})(); + +function pumpStatusEntry(data) { + var entry = {'type': PUMP_STATUS_ENTRY_TYPE}; + + [ + 'conduitBatteryLevel', + 'conduitInRange', + 'conduitMedicalDeviceInRange', + 'reservoirLevelPercent', + 'reservoirAmount', + 'medicalDeviceBatteryLevelPercent' + ].forEach(function(key) { + if(data[key] !== undefined) { + entry[key] = data[key]; + } + }); + + if(data['activeInsulin'] && data['activeInsulin']['amount'] >= 0) { + entry['activeInsulin'] = data['activeInsulin']['amount']; + } + + return addTimeToEntry(data['lastMedicalDeviceDataUpdateServerTime'], entry); +} + +function sgvEntries(data) { + var offset = guessPumpOffset(data); + + if(data['sgs'] && data['sgs'].length) { + return data['sgs'].filter(function(entry) { + return entry['kind'] === 'SG' && entry['sg'] !== 0; + }).map(function(sgv) { + return addTimeToEntry( + parsePumpTime(sgv['datetime'], offset), + { + 'type': SENSOR_GLUCOSE_ENTRY_TYPE, + 'sgv': sgv['sg'], + } + ); + }); + } else { + return []; + } +} + +var transform = module.exports = function(data, sgvLimit) { + var recency = (data['currentServerTime'] - data['lastMedicalDeviceDataUpdateServerTime']) / (60 * 1000); + if (recency > STALE_DATA_THRESHOLD_MINUTES) { + logger.log('Stale CareLink data: ' + recency.toFixed(2) + ' minutes old'); + return []; + } + + if (sgvLimit === undefined) { + sgvLimit = Infinity; + } + + var entries = []; + + entries.push(pumpStatusEntry(data)); + + var sgvs = sgvEntries(data); + // TODO: this assumes sgvs are ordered by date ascending + for(var i = Math.max(0, sgvs.length - sgvLimit); i < sgvs.length; i++) { + entries.push(sgvs[i]); + } + + entries.forEach(function(entry) { + entry['device'] = 'MiniMed Connect ' + data['medicalDeviceFamily'] + ' ' + data['medicalDeviceSerialNumber']; + }); + + return entries; +};