Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add meals/insulin and BG checks #43

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions carelink.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,6 @@ var Client = exports.Client = function (options) {
dataRetrievalUrl = resp.data.blePereodicDataEndpoint;
}
if(dataRetrievalUrl) {
// HOTFIX
// https://github.com/nightscout/minimed-connect-to-nightscout/issues/39
dataRetrievalUrl = dataRetrievalUrl.replace('/carepartner/v6/display/message', '/carepartner/v5/display/message');
logger.log('GET data (as carepartner) ' + dataRetrievalUrl);
var body = {
username: options.username,
Expand Down
23 changes: 21 additions & 2 deletions run.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ var config = {
nsSecret: readEnv('API_SECRET'),
interval: parseInt(readEnv('CARELINK_REQUEST_INTERVAL', 60 * 1000), 10),
sgvLimit: parseInt(readEnv('CARELINK_SGV_LIMIT', 24), 10),
treatmentLimit: parseInt(readEnv('CARELINK_TREATMENT_LIMIT', 24), 10),
bgCheckLimit: parseInt(readEnv('CARELINK_BG_CHECK_LIMIT', 24), 10),
maxRetryDuration: parseInt(readEnv('CARELINK_MAX_RETRY_DURATION', carelink.defaultMaxRetryDuration), 10),
verbose: !readEnv('CARELINK_QUIET', true),
deviceInterval: 5.1 * 60 * 1000,
Expand All @@ -49,6 +51,7 @@ var client = carelink.Client({
});
var entriesUrl = (config.nsBaseUrl ? config.nsBaseUrl : 'https://' + config.nsHost) + '/api/v1/entries.json';
var devicestatusUrl = (config.nsBaseUrl ? config.nsBaseUrl : 'https://' + config.nsHost) + '/api/v1/devicestatus.json';
var treatmentsUrl = (config.nsBaseUrl ? config.nsBaseUrl : 'https://' + config.nsHost) + '/api/v1/treatments.json';

logger.setVerbose(config.verbose);

Expand All @@ -60,6 +63,14 @@ var filterDeviceStatus = filter.makeRecencyFilter(function(item) {
return new Date(item['created_at']).getTime();
});

var filterTreatments = filter.makeRecencyFilter(function(item) {
return new Date(item['created_at']).getTime();
});

var filterbgCheckEntries = filter.makeRecencyFilter(function(item) {
return new Date(item['created_at']).getTime();
});

function uploadMaybe(items, endpoint, callback) {
if (items.length === 0) {
logger.log('No new items for ' + endpoint);
Expand All @@ -82,7 +93,7 @@ function requestLoop() {
console.log(err);
setTimeout(requestLoop, config.deviceInterval);
} else {
let transformed = transform(data, config.sgvLimit);
let transformed = transform(data, config.sgvLimit, config.treatmentLimit, config.bgCheckLimit);

// Because of Nightscout's upsert semantics and the fact that CareLink provides trend
// data only for the most recent sgv, we need to filter out sgvs we've already sent.
Expand All @@ -93,6 +104,10 @@ function requestLoop() {
// does not do the same for created_at, so we need to de-dupe them here.
let newDeviceStatuses = filterDeviceStatus(transformed.devicestatus);

let newTreatments = filterTreatments(transformed.treatments);

let newbgCheckEntries = filterbgCheckEntries(transformed.bgCheckEntries);

// Calculate interval by the device next upload time
let interval = config.deviceInterval - (data.currentServerTime - data.lastMedicalDeviceDataUpdateServerTime);
if (interval > config.deviceInterval || interval < 0)
Expand All @@ -102,7 +117,11 @@ function requestLoop() {

uploadMaybe(newSgvs, entriesUrl, function() {
uploadMaybe(newDeviceStatuses, devicestatusUrl, function() {
setTimeout(requestLoop, interval);
uploadMaybe(newTreatments, treatmentsUrl, function() {
uploadMaybe(newbgCheckEntries, treatmentsUrl, function() {
setTimeout(requestLoop, interval);
});
});
});
});
}
Expand Down
78 changes: 76 additions & 2 deletions transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,85 @@ function sgvEntries(data, offset, offsetMilliseconds) {
return sgvs;
}

module.exports = function (data, sgvLimit) {
function bgCheckEntries(data, offset, offsetMilliseconds) {
var allowedTypes = ["CALIBRATION","BG_READING","BG"];

if (!data['markers'] || !data['markers'].length || data['medicalDeviceFamily'] !== "NGP") {
return [];
}

return data['markers'].filter(function (marker) {
return allowedTypes.indexOf(marker['type']) >= 0 && marker['value'] && marker['value'] != 0;
}).map(function (marker) {
//return marker
var timestamp = parsePumpTime(marker['dateTime'], offset, offsetMilliseconds, data['medicalDeviceFamily']);
return {
'eventType': "BG Check",
'created_at': timestamp,
'dateString': timestampAsString(timestamp),
'dateTime': marker['dateTime'],
'glucose': marker['value'],
"glucoseType": "Finger"
};
}).sort(function(a,b) { return a['created_at'] - b['created_at']; });
}

function treatmentEntries(data, offset, offsetMilliseconds) {
var allowedTypes = ["INSULIN","MEAL"];

if (!data['markers'] || !data['markers'].length || data['medicalDeviceFamily'] !== "NGP") {
return [];
}

var treatments = data['markers'].filter(function (marker) {
return allowedTypes.indexOf(marker['type']) >= 0;
}).map(function (marker) {
//return marker
var timestamp = parsePumpTime(marker['dateTime'], offset, offsetMilliseconds, data['medicalDeviceFamily']);
return {
'eventType': marker['type'].charAt(0).toLocaleUpperCase() + marker['type'].toLocaleLowerCase().slice(1),
'created_at': timestamp,
'dateString': timestampAsString(timestamp),
'dateTime': marker['dateTime'],
'carbs': marker['amount'] || 0,
'insulin': marker['deliveredExtendedAmount'] != null && marker['deliveredFastAmount'] != null ? marker.deliveredExtendedAmount + marker.deliveredFastAmount : 0
};
}).sort(function(a,b) { return a['created_at'] - b['created_at']; });

return mergeInsulinWithMealTreatments(treatments);
}

function mergeInsulinWithMealTreatments(treatments) {
treatments.forEach((treatment) => {
if(treatment.eventType?.toLocaleUpperCase() === "MEAL") {
var matchingInsulin = treatments.find((candidate) => {
return candidate.dateTime === treatment.dateTime && candidate.eventType?.toLocaleUpperCase() === "INSULIN";
});
if(matchingInsulin) {
treatment.insulin = matchingInsulin.insulin;
matchingInsulin.carbs = 0;
matchingInsulin.insulin = 0;
// Set carbs and insulin for a meal to 0 when it's merged so we can delete these later
}
}
});

treatments = treatments.filter((treatment) => {
return treatment.carbs != 0 || treatment.insulin != 0;
});

return treatments;
}

module.exports = function (data, sgvLimit, treatmentLimit, bgCheckLimit) {
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 {
devicestatus: [],
entries: [],
treatments: [],
bgCheckEntries:[]
};
}

Expand All @@ -186,5 +258,7 @@ module.exports = function (data, sgvLimit) {
// XXX: lower-case and singular for consistency with cgm-remote-monitor collection name
devicestatus: [deviceStatusEntry(data, offset, offsetMilliseconds)],
entries: _.takeRight(sgvEntries(data, offset, offsetMilliseconds), sgvLimit),
treatments: _.takeRight(treatmentEntries(data, offset, offsetMilliseconds),treatmentLimit),
bgCheckEntries: _.takeRight(bgCheckEntries(data, offset, offsetMilliseconds),bgCheckLimit),
};
};
};