Skip to content
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
88 changes: 88 additions & 0 deletions lib/data/ddata.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,95 @@ function init () {
}
};

/**
* Infer the effective end of an AAPS infinite DISABLED_LOOP from a later running mode.
* The input is not mutated; only treatments whose duration is changed are cloned,
* and no changes are persisted to MongoDB.
*/
ddata.normalizeAapsRunningModes = function normalizeAapsRunningModes (treatments) {
var infiniteDurationInMinutes = 2147483647;
var infiniteDurationInMilliseconds = times.mins(infiniteDurationInMinutes).msecs;
var runningModes = ['CLOSED_LOOP', 'OPEN_LOOP', 'CLOSED_LOOP_LGS'];
var normalized = treatments.slice();
var pendingDisabledIndexes = {};
var orderedIndexes = normalized.map(function treatmentIndex (treatment, index) {
return index;
}).filter(function hasValidMills (index) {
var mills = normalized[index].mills;
return mills != null
&& (typeof mills === 'number' || typeof mills === 'string')
&& !(typeof mills === 'string' && mills.trim() === '')
&& Number.isFinite(Number(mills));
}).sort(function sortByMills (firstIndex, secondIndex) {
return Number(normalized[firstIndex].mills) - Number(normalized[secondIndex].mills)
|| firstIndex - secondIndex;
});

function sourceKey (treatment) {
if (treatment.pumpSerial != null) {
return 'pump:' + (treatment.pumpType || '') + ':' + treatment.pumpSerial;
}
if (treatment.enteredBy) {
return 'enteredBy:' + treatment.enteredBy;
}
return 'default';
}

function isInfiniteDisabledLoop (treatment) {
// AAPS reports originalDuration in milliseconds.
return treatment.mode === 'DISABLED_LOOP'
&& (Number(treatment.duration) >= infiniteDurationInMinutes
|| Number(treatment.durationInMilliseconds) >= infiniteDurationInMilliseconds
|| Number(treatment.originalDuration) >= infiniteDurationInMilliseconds);
}

orderedIndexes.forEach(function normalizeRunningMode (index) {
var treatment = normalized[index];
if (treatment.eventType !== 'OpenAPS Offline' || treatment.isValid === false) {
return;
}

var key = sourceKey(treatment);
if (isInfiniteDisabledLoop(treatment)) {
if (!Object.prototype.hasOwnProperty.call(pendingDisabledIndexes, key)) {
pendingDisabledIndexes[key] = [];
}
pendingDisabledIndexes[key].push(index);
return;
}

if (runningModes.indexOf(treatment.mode) === -1
|| !Object.prototype.hasOwnProperty.call(pendingDisabledIndexes, key)) {
return;
}

var enabledMills = Number(treatment.mills);
var stillPending = pendingDisabledIndexes[key].filter(function closeDisabledLoop (pendingDisabledIndex) {
var pendingDisabled = normalized[pendingDisabledIndex];
var durationInMilliseconds = enabledMills - Number(pendingDisabled.mills);
if (durationInMilliseconds <= 0) {
return true;
}

pendingDisabled = Object.assign({}, pendingDisabled);
pendingDisabled.duration = times.msecs(durationInMilliseconds).mins;
pendingDisabled.durationInMilliseconds = durationInMilliseconds;
pendingDisabled.endmills = enabledMills;
normalized[pendingDisabledIndex] = pendingDisabled;
return false;
});
if (stillPending.length) {
pendingDisabledIndexes[key] = stillPending;
} else {
delete pendingDisabledIndexes[key];
}
});

return normalized;
};

ddata.processTreatments = function processTreatments (preserveOrignalTreatments) {
ddata.treatments = ddata.normalizeAapsRunningModes(ddata.treatments);

// filter & prepare 'Site Change' events
ddata.sitechangeTreatments = ddata.treatments.filter(function filterSensor (t) {
Expand Down
8 changes: 0 additions & 8 deletions lib/profile/profileeditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,6 @@ var init = function init () {
}
}).done(initeditor);

// convert simple values to ranges if needed (delegates to pure core)
function convertToRanges(profile) {
var res = coreMigrate.convertToRanges(profile, defaultprofile);
if (res.rangesMismatch) {
window.alert(translate('Time ranges of target_low and target_high don\'t match. Values are restored to defaults.'));
}
}

function initeditor() {
$('#pe_history').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.history);
$('#pe_multiple').toggle(client.settings.extendedSettings.profile && client.settings.extendedSettings.profile.multiple);
Expand Down
270 changes: 270 additions & 0 deletions tests/ddata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,276 @@ describe('ddata', function ( ) {
merged[0].identifier.should.equal('loop-id');
});

describe('normalizeAapsRunningModes', function () {
var infiniteDuration = 2147483647;
var infiniteDurationInMilliseconds = infiniteDuration * 60000;

function disabledAt(mills) {
return {
eventType: 'OpenAPS Offline',
mode: 'DISABLED_LOOP',
mills: mills,
duration: infiniteDuration,
durationInMilliseconds: infiniteDurationInMilliseconds,
originalDuration: infiniteDurationInMilliseconds,
endmills: mills + infiniteDurationInMilliseconds
};
}

['CLOSED_LOOP', 'OPEN_LOOP', 'CLOSED_LOOP_LGS'].forEach(function (mode) {
it('ends an infinite DISABLED_LOOP when followed by ' + mode, function () {
var ddata = require('../lib/data/ddata')();
var disabled = disabledAt(1000);
var treatments = [
disabled,
{ eventType: 'OpenAPS Offline', mode: mode, mills: 1801000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].duration.should.equal(30);
normalized[0].durationInMilliseconds.should.equal(1800000);
normalized[0].endmills.should.equal(1801000);
disabled.duration.should.equal(infiniteDuration);
disabled.endmills.should.equal(1000 + infiniteDurationInMilliseconds);
});
});

it('does not change legacy OpenAPS Offline treatments without a mode', function () {
var ddata = require('../lib/data/ddata')();
var treatments = [
{ eventType: 'OpenAPS Offline', mills: 1000, duration: infiniteDuration },
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 1801000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].duration.should.equal(infiniteDuration);
should.not.exist(normalized[0].durationInMilliseconds);
should.not.exist(normalized[0].endmills);
});

it('does not change finite DISABLED_LOOP treatments', function () {
var ddata = require('../lib/data/ddata')();
var treatments = [
{ eventType: 'OpenAPS Offline', mode: 'DISABLED_LOOP', mills: 1000, duration: 60 },
{ eventType: 'OpenAPS Offline', mode: 'OPEN_LOOP', mills: 1801000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].duration.should.equal(60);
should.not.exist(normalized[0].durationInMilliseconds);
should.not.exist(normalized[0].endmills);
});

it('tracks pending DISABLED_LOOP treatments by source', function () {
var ddata = require('../lib/data/ddata')();
var first = Object.assign(disabledAt(1000), { pumpType: 'AAPS', pumpSerial: 'first' });
var second = Object.assign(disabledAt(2000), { pumpType: 'AAPS', pumpSerial: 'second' });
var treatments = [
first,
second,
{
eventType: 'OpenAPS Offline',
mode: 'CLOSED_LOOP',
mills: 1801000,
duration: 0,
pumpType: 'AAPS',
pumpSerial: 'first'
}
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].duration.should.equal(30);
normalized[1].should.equal(second);
normalized[1].duration.should.equal(infiniteDuration);
});

it('ignores invalid RunningMode treatments', function () {
var ddata = require('../lib/data/ddata')();
var disabled = disabledAt(1000);
var treatments = [
disabled,
{
eventType: 'OpenAPS Offline',
mode: 'CLOSED_LOOP',
mills: 1801000,
duration: 0,
isValid: false
}
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].should.equal(disabled);
normalized[0].duration.should.equal(infiniteDuration);
});

it('only clones treatments that are modified', function () {
var ddata = require('../lib/data/ddata')();
var disabled = disabledAt(1000);
var running = { eventType: 'OpenAPS Offline', mode: 'OPEN_LOOP', mills: 1801000, duration: 0 };
var unrelated = { eventType: 'Note', mills: 2000000, notes: 'unchanged' };
var treatments = [disabled, running, unrelated];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].should.not.equal(disabled);
normalized[1].should.equal(running);
normalized[2].should.equal(unrelated);
disabled.duration.should.equal(infiniteDuration);
});

it('does not create a zero-length Offline period', function () {
var ddata = require('../lib/data/ddata')();
var disabled = disabledAt(1000);
var treatments = [
disabled,
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 1000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].should.equal(disabled);
normalized[0].duration.should.equal(infiniteDuration);
});

it('ends duplicate infinite DISABLED_LOOP treatments at the same enabled mode', function () {
var ddata = require('../lib/data/ddata')();
var first = disabledAt(1000);
var second = disabledAt(2000);
var treatments = [
first,
second,
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 61000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].durationInMilliseconds.should.equal(60000);
normalized[0].endmills.should.equal(61000);
normalized[1].durationInMilliseconds.should.equal(59000);
normalized[1].endmills.should.equal(61000);
first.duration.should.equal(infiniteDuration);
second.duration.should.equal(infiniteDuration);
});

it('normalizes out-of-order treatments while preserving their order', function () {
var ddata = require('../lib/data/ddata')();
var running = { eventType: 'OpenAPS Offline', mode: 'OPEN_LOOP', mills: 61000, duration: 0 };
var disabled = disabledAt(1000);
var treatments = [running, disabled];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].should.equal(running);
normalized[1].durationInMilliseconds.should.equal(60000);
normalized[1].endmills.should.equal(61000);
disabled.duration.should.equal(infiniteDuration);
});

[
{ name: 'null', mills: null },
{ name: 'missing' },
{ name: 'nonnumeric', mills: 'not-a-time' },
{ name: 'NaN', mills: NaN },
{ name: 'infinite', mills: Infinity }
].forEach(function (invalidTime) {
it('ignores a DISABLED_LOOP treatment with a ' + invalidTime.name + ' timestamp', function () {
var ddata = require('../lib/data/ddata')();
var disabled = disabledAt(invalidTime.mills);
if (!Object.prototype.hasOwnProperty.call(invalidTime, 'mills')) {
delete disabled.mills;
}
var treatments = [
disabled,
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 61000, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].should.equal(disabled);
normalized[0].duration.should.equal(infiniteDuration);
});
});

it('preserves millisecond precision for a transition shorter than one minute', function () {
var ddata = require('../lib/data/ddata')();
var treatments = [
disabledAt(1000),
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 27584, duration: 0 }
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].durationInMilliseconds.should.equal(26584);
normalized[0].duration.should.equal(26584 / 60000);
normalized[0].endmills.should.equal(27584);
});

it('normalizes multiple enable and disable cycles for the same source', function () {
var ddata = require('../lib/data/ddata')();
var firstDisabled = Object.assign(disabledAt(1000), { pumpSerial: 'same' });
var secondDisabled = Object.assign(disabledAt(121000), { pumpSerial: 'same' });
var treatments = [
secondDisabled,
{
eventType: 'OpenAPS Offline',
mode: 'OPEN_LOOP',
mills: 181000,
duration: 0,
pumpSerial: 'same'
},
firstDisabled,
{
eventType: 'OpenAPS Offline',
mode: 'CLOSED_LOOP',
mills: 61000,
duration: 0,
pumpSerial: 'same'
}
];

var normalized = ddata.normalizeAapsRunningModes(treatments);

normalized[0].durationInMilliseconds.should.equal(60000);
normalized[0].endmills.should.equal(181000);
normalized[1].should.equal(treatments[1]);
normalized[2].durationInMilliseconds.should.equal(60000);
normalized[2].endmills.should.equal(61000);
normalized[3].should.equal(treatments[3]);
});

it('is idempotent', function () {
var ddata = require('../lib/data/ddata')();
var treatments = [
disabledAt(1000),
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 1801000, duration: 0 }
];

var once = ddata.normalizeAapsRunningModes(treatments);
var twice = ddata.normalizeAapsRunningModes(once);

twice.should.deepEqual(once);
});

it('is applied by processTreatments', function () {
var ddata = require('../lib/data/ddata')();
ddata.treatments = [
disabledAt(1000),
{ eventType: 'OpenAPS Offline', mode: 'CLOSED_LOOP', mills: 1801000, duration: 0 }
];

ddata.processTreatments(false);

ddata.treatments[0].duration.should.equal(30);
ddata.treatments[0].durationInMilliseconds.should.equal(1800000);
ddata.treatments[0].endmills.should.equal(1801000);
});
});

// TODO: ensure partition function gets called via:
// Properties
// * ddata.devicestatus
Expand Down
Loading