nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
385 lines (320 loc) • 12.4 kB
JavaScript
'use strict';
var _ = require('lodash');
var moment = require('moment');
var times = require('../times');
var ALL_STATUS_FIELDS = ['reservoir', 'battery', 'clock', 'status', 'device'];
function init (ctx) {
var translate = ctx.language.translate;
var timeago = require('./timeago')(ctx);
var openaps = require('./openaps')(ctx);
var levels = ctx.levels;
var pump = {
name: 'pump'
, label: 'Pump'
, pluginType: 'pill-status'
};
pump.getPrefs = function getPrefs (sbx) {
function cleanList (value) {
return decodeURIComponent(value || '').toLowerCase().split(' ');
}
function isEmpty (list) {
return _.isEmpty(list) || _.isEmpty(list[0]);
}
var fields = cleanList(sbx.extendedSettings.fields);
fields = isEmpty(fields) ? ['reservoir'] : fields;
var retroFields = cleanList(sbx.extendedSettings.retroFields);
retroFields = isEmpty(retroFields) ? ['reservoir', 'battery'] : retroFields;
var profile = sbx.data.profile;
var warnBattQuietNight = sbx.extendedSettings.warnBattQuietNight;
if (warnBattQuietNight && (!profile || !profile.hasData() || !profile.getTimezone())) {
console.warn('PUMP_WARN_BATT_QUIET_NIGHT requires a treatment profile with time zone set to obtain user time zone');
warnBattQuietNight = false;
}
return {
fields: fields
, retroFields: retroFields
, warnClock: sbx.extendedSettings.warnClock || 30
, urgentClock: sbx.extendedSettings.urgentClock || 60
, warnRes: sbx.extendedSettings.warnRes || 10
, urgentRes: sbx.extendedSettings.urgentRes || 5
, warnBattV: sbx.extendedSettings.warnBattV || 1.35
, urgentBattV: sbx.extendedSettings.urgentBattV || 1.3
, warnBattP: sbx.extendedSettings.warnBattP || 30
, urgentBattP: sbx.extendedSettings.urgentBattP || 20
, warnOnSuspend: sbx.extendedSettings.warnOnSuspend || false
, enableAlerts: sbx.extendedSettings.enableAlerts || false
, warnBattQuietNight: warnBattQuietNight || false
, dayStart: sbx.settings.dayStart
, dayEnd: sbx.settings.dayEnd
};
};
pump.setProperties = function setProperties (sbx) {
sbx.offerProperty('pump', function setPump ( ) {
var prefs = pump.getPrefs(sbx);
var recentMills = sbx.time - times.mins(prefs.urgentClock * 2).msecs;
var filtered = _.filter(sbx.data.devicestatus, function (status) {
return ('pump' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills;
});
var pumpStatus = null;
_.forEach(filtered, function each (status) {
status.clockMills = status.pump && status.pump.clock ? moment(status.pump.clock).valueOf() : status.mills;
if (!pumpStatus || status.clockMills > pumpStatus.clockMills) {
pumpStatus = status;
}
});
pumpStatus = pumpStatus || { };
pumpStatus.data = prepareData(pumpStatus, prefs, sbx);
return pumpStatus;
});
};
pump.checkNotifications = function checkNotifications (sbx) {
var prefs = pump.getPrefs(sbx);
if (!prefs.enableAlerts) { return; }
pump.warnOnSuspend = prefs.warnOnSuspend;
var data = prepareData(sbx.properties.pump, prefs, sbx);
if (data.level >= levels.WARN) {
sbx.notifications.requestNotify({
level: data.level
, title: data.title
, message: data.message
, pushoverSound: 'echo'
, group: 'Pump'
, plugin: pump
});
}
};
pump.updateVisualisation = function updateVisualisation (sbx) {
var prop = sbx.properties.pump;
var prefs = pump.getPrefs(sbx);
var result = prepareData(prop, prefs, sbx);
var values = [ ];
var info = [ ];
var selectedFields = sbx.data.inRetroMode ? prefs.retroFields : prefs.fields;
_.forEach(ALL_STATUS_FIELDS, function eachField (fieldName) {
var field = result[fieldName];
if (field) {
var selected = _.indexOf(selectedFields, fieldName) > -1;
if (selected) {
values.push(field.display);
} else {
info.push({label: field.label, value: field.display});
}
}
});
if (result.extended) {
info.push({label: '------------', value: ''});
_.forOwn(result.extended, function(value, key) {
info.push({ label: key, value: value });
});
}
sbx.pluginBase.updatePillText(pump, {
value: values.join(' ')
, info: info
, label: translate('Pump')
, pillClass: statusClass(result.level)
});
};
function virtAsstReservoirHandler (next, slots, sbx) {
var reservoir = _.get(sbx, 'properties.pump.pump.reservoir');
if (reservoir || reservoir === 0) {
var response = translate('virtAsstReservoir', {
params: [
reservoir
]
});
next(translate('virtAsstTitlePumpReservoir'), response);
} else {
next(translate('virtAsstTitlePumpReservoir'), translate('virtAsstUnknown'));
}
}
function virtAsstBatteryHandler (next, slots, sbx) {
var battery = _.get(sbx, 'properties.pump.data.battery');
if (battery) {
var response = translate('virtAsstPumpBattery', {
params: [
battery.value,
battery.unit
]
});
next(translate('virtAsstTitlePumpBattery'), response);
} else {
next(translate('virtAsstTitlePumpBattery'), translate('virtAsstUnknown'));
}
}
pump.virtAsst = {
intentHandlers:[
{
// backwards compatibility
intent: 'InsulinRemaining',
intentHandler: virtAsstReservoirHandler
}
, {
// backwards compatibility
intent: 'PumpBattery',
intentHandler: virtAsstBatteryHandler
}
, {
intent: 'MetricNow'
, metrics: ['pump reservoir']
, intentHandler: virtAsstReservoirHandler
}
, {
intent: 'MetricNow'
, metrics: ['pump battery']
, intentHandler: virtAsstBatteryHandler
}
]
};
function statusClass (level) {
var cls = 'current';
if (level === levels.WARN) {
cls = 'warn';
} else if (level === levels.URGENT) {
cls = 'urgent';
}
return cls;
}
function updateClock (prefs, result, sbx) {
if (result.clock) {
result.clock.label = 'Last Clock';
result.clock.display = timeFormat(result.clock.value, sbx);
var urgent = moment(sbx.time).subtract(prefs.urgentClock, 'minutes');
var warn = moment(sbx.time).subtract(prefs.warnClock, 'minutes');
if (urgent.isAfter(result.clock.value)) {
result.clock.level = levels.URGENT;
result.clock.message = 'URGENT: Pump data stale';
} else if (warn.isAfter(result.clock.value)) {
result.clock.level = levels.WARN;
result.clock.message = 'Warning, Pump data stale';
} else {
result.clock.level = levels.NONE;
}
}
}
function updateReservoir (prefs, result) {
if (result.reservoir) {
result.reservoir.label = 'Reservoir';
if (result.reservoir_display_override) {
result.reservoir.display = result.reservoir_display_override;
} else {
result.reservoir.display = result.reservoir.value.toPrecision(3) + 'U';
}
if (result.reservoir.value < prefs.urgentRes) {
result.reservoir.level = levels.URGENT;
result.reservoir.message = 'URGENT: Pump Reservoir Low';
} else if (result.reservoir.value < prefs.warnRes) {
result.reservoir.level = levels.WARN;
result.reservoir.message = 'Warning, Pump Reservoir Low';
} else {
result.reservoir.level = levels.NONE;
}
} else if (result.manufacturer === 'Insulet' && result.model === 'Eros') {
result.reservoir = {
label: 'Reservoir', display: '50+ U'
}
}
}
function updateBattery (type, prefs, result, batteryWarn) {
if (result.battery) {
result.battery.label = 'Battery';
result.battery.display = result.battery.value + type;
var urgent = type === 'v' ? prefs.urgentBattV : prefs.urgentBattP;
var warn = type === 'v' ? prefs.warnBattV : prefs.warnBattP;
if (result.battery.value < urgent && batteryWarn) {
result.battery.level = levels.URGENT;
result.battery.message = 'URGENT: Pump Battery Low';
} else if (result.battery.value < warn && batteryWarn) {
result.battery.level = levels.WARN;
result.battery.message = 'Warning, Pump Battery Low';
} else {
result.battery.level = levels.NONE;
}
}
}
function buildMessage (result) {
if (result.level > levels.NONE) {
var message = [];
if (result.battery) {
message.push('Pump Battery: ' + result.battery.display);
}
if (result.reservoir) {
message.push('Pump Reservoir: ' + result.reservoir.display);
}
result.message = message.join('\n');
}
}
function updateStatus(pump, result) {
if (pump.status) {
var status = pump.status.status || 'normal';
if (pump.status.bolusing) {
status = 'bolusing';
} else if (pump.status.suspended) {
status = 'suspended';
if (pump.warnOnSuspend && pump.status.suspended) {
result.status.level = levels.WARN;
result.status.message = 'Pump Suspended';
}
}
result.status = { value: status, display: status, label: translate('Status') };
}
}
function prepareData (prop, prefs, sbx) {
var pump = (prop && prop.pump) || { };
var time = (sbx.data.profile && sbx.data.profile.getTimezone()) ? moment(sbx.time).tz(sbx.data.profile.getTimezone()) : moment(sbx.time);
var now = time.hours() + time.minutes() / 60.0 + time.seconds() / 3600.0;
var batteryWarn = !(prefs.warnBattQuietNight && (now < prefs.dayStart || now > prefs.dayEnd));
var result = {
level: levels.NONE
, clock: pump.clock ? { value: moment(pump.clock) } : null
, reservoir: pump.reservoir || pump.reservoir === 0 ? { value: pump.reservoir } : null
, reservoir_display_override: pump.reservoir_display_override || null
, manufacturer: pump.manufacturer
, model: pump.model
, extended: pump.extended || null
};
updateClock(prefs, result, sbx);
updateReservoir(prefs, result);
updateStatus(pump, result);
if (pump.battery && pump.battery.percent) {
result.battery = { value: pump.battery.percent, unit: 'percent' };
updateBattery('%', prefs, result, batteryWarn);
} else if (pump.battery && pump.battery.voltage) {
result.battery = { value: pump.battery.voltage, unit: 'volts'};
updateBattery('v', prefs, result, batteryWarn);
}
result.device = { label: translate('Device'), display: prop.device };
result.title = 'Pump Status';
result.level = levels.NONE;
//TODO: A new Pump Offline marker? Something generic? Use something new instead of a treatment?
if (openaps.findOfflineMarker(sbx)) {
console.info('OpenAPS known offline, not checking for alerts');
} else {
_.forEach(ALL_STATUS_FIELDS, function eachField(fieldName) {
var field = result[fieldName];
if (field && field.level > result.level) {
result.level = field.level;
result.title = field.message;
}
});
}
buildMessage(result);
return result;
}
function timeFormat (m, sbx) {
var when;
if (m && sbx.data.inRetroMode) {
when = m.format('LT');
} else if (m) {
when = formatAgo(m, sbx.time);
} else {
when = 'unknown';
}
return when;
}
function formatAgo (m, nowMills) {
var ago = timeago.calcDisplay({mills: m.valueOf()}, nowMills);
return translate('%1' + ago.shortLabel + (ago.shortLabel.length === 1 ? ' ago' : ''), { params: [(ago.value ? ago.value : '')]});
}
return pump;
}
module.exports = init;