Skip to content

Commit

Permalink
Represent pump status as 'device status' entry
Browse files Browse the repository at this point in the history
  • Loading branch information
mddub committed Jan 7, 2016
1 parent 7907df2 commit a8a1656
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 158 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minimed-connect-to-nightscout",
"version": "0.2.4",
"version": "1.0.0",
"description": "Send Medtronic pump and CGM data to Nightscout.",
"author": "Mark Wilson <[email protected]>",
"repository": {
Expand Down
69 changes: 45 additions & 24 deletions run.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,52 +38,73 @@ var client = carelink.Client({
password: config.password,
maxRetryDuration: config.maxRetryDuration
});
var endpoint = (config.nsBaseUrl ? config.nsBaseUrl : 'https://' + config.nsHost) + '/api/v1/entries.json';
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';

logger.setVerbose(config.verbose);

var filterRecentSgvs = (function() {
var lastSgvDate = 0;
function makeRecencyFilter(timeFn) {
var lastTime = 0;

return function(entries) {
return function(items) {
var out = [];
entries.forEach(function(entry) {
if (entry['type'] !== 'sgv' || entry['date'] > lastSgvDate) {
out.push(entry);
items.forEach(function(item) {
if (timeFn(item) > lastTime) {
out.push(item);
}
});
out.filter(function(e) { return e['type'] === 'sgv'; })
.forEach(function(e) {
lastSgvDate = Math.max(lastSgvDate, e['date']);
});
out.forEach(function(item) {
lastTime = Math.max(lastTime, timeFn(item));
});

return out;
};
})();
}

var filterSgvs = makeRecencyFilter(function(item) {
return item['date'];
});

var filterDeviceStatus = 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);
callback();
} else {
nightscout.upload(items, endpoint, config.nsSecret, function(err, response) {
if (err) {
// Continue gathering data from CareLink even if Nightscout can't be reached
console.log(err);
}
callback();
});
}
}

(function requestLoop() {
client.fetch(function(err, data) {
if (err) {
throw new Error(err);
} else {
var allEntries = transform(data, config.sgvLimit);
var transformed = transform(data, config.sgvLimit);

// 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.
// Otherwise we'll overwrite existing sgv entries and remove their trend data.
var newEntries = filterRecentSgvs(allEntries);

if (newEntries.length > 0) {
nightscout.upload(newEntries, endpoint, config.nsSecret, function(err, response) {
if (err) {
// Continue gathering data from CareLink even if Nightscout can't be reached
console.log(err);
}
var newSgvs = filterSgvs(transformed.entries);

// Nightscout's entries collection upserts based on date, but the devicestatus collection
// does not do the same for created_at, so we need to de-dupe them here.
var newDeviceStatuses = filterDeviceStatus(transformed.devicestatus);

uploadMaybe(newSgvs, entriesUrl, function() {
uploadMaybe(newDeviceStatuses, devicestatusUrl, function() {
setTimeout(requestLoop, config.interval);
});
} else {
setTimeout(requestLoop, config.interval);
}
});
}
});
})();
68 changes: 33 additions & 35 deletions test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ var samples = require('./_samples'),
describe('integration test: missingLastSgv', function() {
var sample = samples.missingLastSgv;
var transformed = transform(sample);
var pumpStatuses = _.filter(transformed, {type: 'pump_status'});
var sgvs = _.filter(transformed, {type: 'sgv'});
var pumpStatuses = transformed.devicestatus;
var sgvs = transformed.entries;

it('should set the pump status time based on the "last device data update time"', function() {
expect(pumpStatuses[0]['date']).to.equal(sample['lastMedicalDeviceDataUpdateServerTime']);
expect(pumpStatuses[0]['dateString']).to.equal(new Date(sample['lastMedicalDeviceDataUpdateServerTime']).toISOString());
expect(pumpStatuses[0]['created_at']).to.equal(new Date(sample['lastMedicalDeviceDataUpdateServerTime']).toISOString());
});

it('should have one pump_status and 5 sgv entries', function() {
Expand All @@ -40,31 +39,30 @@ describe('integration test: missingLastSgv', function() {

it('should include pump status data, including active insulin', function() {
_.forEach({
activeInsulin: 4.85,
calibStatus: 'LESS_THAN_TWELVE_HRS',
conduitBatteryLevel: 29,
conduitInRange: true,
conduitMedicalDeviceInRange: true,
conduitSensorInRange: true,
device: 'connect://paradigm',
medicalDeviceBatteryLevelPercent: 75,
reservoirAmount: 60,
reservoirLevelPercent: 25,
sensorDurationHours: 73,
sensorState: 'NORMAL',
timeToNextCalibHours: 10
'uploader.battery': 29,
'pump.battery.percent': 75,
'pump.reservoir': 60,
'pump.iob.bolusiob': 4.85,
'pump.iob.timestamp': new Date(sample['lastMedicalDeviceDataUpdateServerTime']).toISOString(),
'connect.calibStatus': 'LESS_THAN_TWELVE_HRS',
'connect.conduitInRange': true,
'connect.conduitMedicalDeviceInRange': true,
'connect.conduitSensorInRange': true,
'connect.sensorDurationHours': 73,
'connect.sensorState': 'NORMAL',
'connect.timeToNextCalibHours': 10,
'device': 'connect://paradigm',
}, function(val, key) {
expect(pumpStatuses[0][key]).to.be(val);
expect(_.get(pumpStatuses[0], key)).to.be(val);
});
});
});

describe('integration test: withTrend', function() {
var sample = samples.withTrend;
var transformed = transform(sample);
var pumpStatuses = _.filter(transformed, {type: 'pump_status'});

var sgvs = _.filter(transformed, {type: 'sgv'});
var pumpStatuses = transformed.devicestatus;
var sgvs = transformed.entries;

it('should have one pump_status and 6 sgv entries', function() {
expect(pumpStatuses.length).to.be(1);
Expand Down Expand Up @@ -97,21 +95,21 @@ describe('integration test: withTrend', function() {

it('should include pump status data, including active insulin', function() {
_.forEach({
activeInsulin: 1.35,
calibStatus: 'LESS_THAN_NINE_HRS',
conduitBatteryLevel: 86,
conduitInRange: true,
conduitMedicalDeviceInRange: true,
conduitSensorInRange: true,
device: 'connect://paradigm',
medicalDeviceBatteryLevelPercent: 50,
reservoirAmount: 67,
reservoirLevelPercent: 50,
sensorDurationHours: 137,
sensorState: 'NORMAL',
timeToNextCalibHours: 6
'uploader.battery': 86,
'pump.battery.percent': 50,
'pump.reservoir': 67,
'pump.iob.bolusiob': 1.35,
'pump.iob.timestamp': new Date(sample['lastMedicalDeviceDataUpdateServerTime']).toISOString(),
'connect.calibStatus': 'LESS_THAN_NINE_HRS',
'connect.conduitInRange': true,
'connect.conduitMedicalDeviceInRange': true,
'connect.conduitSensorInRange': true,
'connect.sensorDurationHours': 137,
'connect.sensorState': 'NORMAL',
'connect.timeToNextCalibHours': 6,
'device': 'connect://paradigm',
}, function(val, key) {
expect(pumpStatuses[0][key]).to.be(val);
expect(_.get(pumpStatuses[0], key)).to.be(val);
});
});
});
80 changes: 40 additions & 40 deletions test/transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ describe('transform()', function() {
var data = f.data();

expect(
_.filter(transform(data), {type: 'sgv'}).length
transform(data).entries.length
).to.eql(data['sgs'].length);

expect(
_.filter(transform(data, 4), {type: 'sgv'}).length
transform(data, 4).entries.length
).to.be(4);
});

it('should include pump device family', function() {
expect(
transform(
f.data({'medicalDeviceFamily': 'foo'})
)[0]['device']
).entries[0]['device']
).to.be('connect://foo');
});

Expand All @@ -36,47 +36,41 @@ describe('transform()', function() {
expect(
transform(
f.data({'currentServerTime': now, 'lastMedicalDeviceDataUpdateServerTime': boundary})
).length
).entries.length
).to.be.greaterThan(0);

expect(
transform(
f.data({'currentServerTime': now, 'lastMedicalDeviceDataUpdateServerTime': boundary - 1})
).length
).entries.length
).to.be(0);
});

describe('active insulin', function() {
it('should include active insulin', function() {
var pumpStatus = _.filter(
transform(
f.data({'activeInsulin': {
'datetime' : 'Oct 17, 2015 09:09:14',
'version' : 1,
'amount' : 1.275,
'kind' : 'Insulin'
}})
),
{type: 'pump_status'}
)[0];

expect(pumpStatus['activeInsulin']).to.be(1.275);
var pumpStatus = transform(
f.data({'activeInsulin': {
'datetime' : 'Oct 17, 2015 09:09:14',
'version' : 1,
'amount' : 1.275,
'kind' : 'Insulin'
}})
).devicestatus[0];

expect(_.get(pumpStatus, 'pump.iob.bolusiob')).to.be(1.275);
});

it('should ignore activeInsulin values of -1', function() {
var pumpStatus = _.filter(
transform(
f.data({'activeInsulin': {
'datetime' : 'Oct 17, 2015 09:09:14',
'version' : 1,
'amount' : -1,
'kind' : 'Insulin'
}})
),
{type: 'pump_status'}
)[0];

expect(pumpStatus['activeInsulin']).to.be(undefined);
var pumpStatus = transform(
f.data({'activeInsulin': {
'datetime' : 'Oct 17, 2015 09:09:14',
'version' : 1,
'amount' : -1,
'kind' : 'Insulin'
}})
).devicestatus[0];

expect(_.get(pumpStatus, 'pump.iob.bolusiob')).to.be(undefined);
});
});

Expand All @@ -88,15 +82,12 @@ describe('transform()', function() {
];

function transformedSGs(valDatePairs) {
return _.filter(
transform(
f.data({
'lastSGTrend': 'UP_DOUBLE',
'sgs': valDatePairs.map(Function.prototype.apply.bind(f.makeSG, null))
})
),
{type: 'sgv'}
);
return transform(
f.data({
'lastSGTrend': 'UP_DOUBLE',
'sgs': valDatePairs.map(Function.prototype.apply.bind(f.makeSG, null))
})
).entries;
}

it('should add the trend to the last sgv', function() {
Expand All @@ -115,4 +106,13 @@ describe('transform()', function() {
expect(sgvs[sgvs.length - 1]['trend']).to.be(undefined);
});
});

describe('uploader battery', function() {
it('should use the Connect battery level as uploader.battery', function() {
var pumpStatus = transform(
f.data({'conduitBatteryLevel': 76})
).devicestatus[0];
expect(pumpStatus.uploader.battery).to.be(76);
});
});
});
Loading

0 comments on commit a8a1656

Please sign in to comment.