UNPKG

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.

444 lines (375 loc) 15.3 kB
'use strict'; var _ = require('lodash'); var moment = require('moment'); var times = require('../times'); function init(ctx) { var levels = ctx.levels; var utils = require('../utils')(ctx); var firstPrefs = true; var lastStateNotification = null; var translate = ctx.language.translate; var sensorState = { name: 'xdripjs' , label: 'CGM Status' , pluginType: 'pill-status' }; sensorState.getPrefs = function getPrefs(sbx) { var prefs = { enableAlerts: sbx.extendedSettings.enableAlerts || false , warnBatV: sbx.extendedSettings.warnBatV || 300 , stateNotifyIntrvl: sbx.extendedSettings.stateNotifyIntrvl || 0.5 }; if (firstPrefs) { firstPrefs = false; console.info('xdripjs Prefs:', prefs); } return prefs; }; sensorState.setProperties = function setProperties (sbx) { sbx.offerProperty('sensorState', function setProp ( ) { return sensorState.getStateString(sbx); }); }; sensorState.checkNotifications = function checkNotifications(sbx) { var info = sbx.properties.sensorState; if (info && info.notification) { var notification = _.extend({}, info.notification, { plugin: sensorState , debug: { stateString: info.lastStateString } }); sbx.notifications.requestNotify(notification); } }; sensorState.getStateString = function findLatestState(sbx) { var prefs = sensorState.getPrefs(sbx); var recentHours = 24; var recentMills = sbx.time - times.hours(recentHours).msecs; var result = { seenDevices: { } , latest: null , lastDevice: null , lastState: null , lastStateString: null , lastStateStringShort: null , lastSessionStart: null , lastStateTime: null , lastTxId: null , lastTxStatus: null , lastTxStatusString: null , lastTxStatusStringShort: null , lastTxActivation: null , lastMode: null , lastRssi: null , lastUnfiltered: null , lastFiltered: null , lastNoise: null , lastNoiseString: null , lastSlope: null , lastIntercept: null , lastCalType: null , lastCalibrationDate: null , lastBatteryTimestamp: null , lastVoltageA: null , lastVoltageB: null , lastTemperature: null , lastResistance: null }; function toMoments (status) { return { when: moment(status.mills) , timestamp: status.xdripjs && status.xdripjs.timestamp && moment(status.xdripjs.timestamp) }; } function getDevice(status) { var uri = status.device || 'device'; var device = result.seenDevices[uri]; if (!device) { device = { name: utils.deviceName(uri) , uri: uri }; result.seenDevices[uri] = device; } return device; } var recentData = _.chain(sbx.data.devicestatus) .filter(function (status) { return ('xdripjs' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills; }) .value( ); recentData = _.sortBy(recentData, 'xdripjs.timestamp'); _.forEach(recentData, function eachStatus (status) { getDevice(status); var moments = toMoments(status); if (status.xdripjs && (!result.latest || moments.timestamp && moments.timestamp.isAfter(result.lastStateTime))) { result.latest = status; result.lastStateTime = moment(status.xdripjs.timestamp); } }); var sendNotification = false; var sound = 'incoming'; var message; var title; var sensorInfo = result.latest; result.level = levels.NONE; if (sensorInfo && sensorInfo.xdripjs) { if (sensorInfo.xdripjs.state != 0x6) { // Send warning notification for all states that are not 'OK' // but only send state notifications at interval preference if (!lastStateNotification || (lastStateNotification.state != sensorInfo.xdripjs.state) || !prefs.stateNotifyIntrvl || (moment().diff(lastStateNotification.timestamp, 'minutes') > (prefs.stateNotifyIntrvl*60))) { sendNotification = true; lastStateNotification = { timestamp: moment() , state: sensorInfo.xdripjs.state }; } message = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; title = 'CGM Transmitter state: ' + sensorInfo.xdripjs.stateString; if (sensorInfo.xdripjs.state == 0x7) { // If it is a calibration request, only use INFO result.level = levels.INFO; } else { result.level = levels.WARN; } } if (sensorInfo.xdripjs.voltagea && (sensorInfo.xdripjs.voltagea < prefs.warnBatV)) { sendNotification = true; message = 'CGM Transmitter Battery A Low Voltage: ' + sensorInfo.xdripjs.voltagea; title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } if (sensorInfo.xdripjs.voltageb && (sensorInfo.xdripjs.voltageb < (prefs.warnBatV - 10))) { sendNotification = true; message = 'CGM Transmitter Battery B Low Voltage: ' + sensorInfo.xdripjs.voltageb; title = 'CGM Transmitter Battery Low'; result.level = levels.WARN; } if (prefs.enableAlerts && sendNotification) { result.notification = { title: title , message: message , pushoverSound: sound , level: result.level , group: 'xDrip-js' }; } result.lastState = sensorInfo.xdripjs.state; result.lastStateString = sensorInfo.xdripjs.stateString; result.lastStateStringShort = sensorInfo.xdripjs.stateStringShort; result.lastSessionStart = sensorInfo.xdripjs.sessionStart; result.lastTxId = sensorInfo.xdripjs.txId; result.lastTxStatus = sensorInfo.xdripjs.txStatus; result.lastTxStatusString = sensorInfo.xdripjs.txStatusString; result.lastTxStatusStringShort = sensorInfo.xdripjs.txStatusStringShort; result.lastTxActivation = sensorInfo.xdripjs.txActivation; result.lastMode = sensorInfo.xdripjs.mode; result.lastRssi = sensorInfo.xdripjs.rssi; result.lastUnfiltered = sensorInfo.xdripjs.unfiltered; result.lastFiltered = sensorInfo.xdripjs.filtered; result.lastNoise = sensorInfo.xdripjs.noise; result.lastNoiseString = sensorInfo.xdripjs.noiseString; result.lastSlope = Math.round(sensorInfo.xdripjs.slope * 100) / 100.0; result.lastIntercept = Math.round(sensorInfo.xdripjs.intercept * 100) / 100.0; result.lastCalType = sensorInfo.xdripjs.calType; result.lastCalibrationDate = sensorInfo.xdripjs.lastCalibrationDate; result.lastBatteryTimestamp = sensorInfo.xdripjs.batteryTimestamp; result.lastVoltageA = sensorInfo.xdripjs.voltagea; result.lastVoltageB = sensorInfo.xdripjs.voltageb; result.lastTemperature = sensorInfo.xdripjs.temperature; result.lastResistance = sensorInfo.xdripjs.resistance; } return result; }; sensorState.updateVisualisation = function updateVisualisation (sbx) { var sensor = sbx.properties.sensorState; var sessionDuration = 'Unknown'; var info = []; _.forIn(sensor.seenDevices, function seenDevice (device) { info.push( { label: 'Seen: ', value: device.name } ); }); info.push( { label: 'State Time: ', value: (sensor && sensor.lastStateTime && moment().diff(sensor.lastStateTime, 'minutes') + ' minutes ago') || 'Unknown' } ); info.push( { label: 'Mode: ', value: (sensor && sensor.lastMode) || 'Unknown' } ); info.push( { label: 'Status: ', value: (sensor && sensor.lastStateString) || 'Unknown' } ); // session start is only valid if in a session if (sensor && sensor.lastSessionStart && (sensor.lastState != 0x1)) { var diffTime = moment().diff(moment(sensor.lastSessionStart)); var duration = moment.duration(diffTime); sessionDuration = duration.days() + ' days ' + duration.hours() + ' hours'; info.push( { label: 'Session Age: ', value: sessionDuration } ); } info.push( { label: 'Tx ID: ', value: (sensor && sensor.lastTxId) || 'Unknown' } ); info.push( { label: 'Tx Status: ', value: (sensor && sensor.lastTxStatusString) || 'Unknown' } ); if (sensor) { if (sensor.lastTxActivation) { info.push( { label: 'Tx Age: ', value: moment().diff(moment(sensor.lastTxActivation), 'days') + ' days' } ); } if (sensor.lastRssi) { info.push( { label: 'RSSI: ', value: sensor.lastRssi } ); } if (sensor.lastUnfiltered) { info.push( { label: 'Unfiltered: ', value: sensor.lastUnfiltered } ); } if (sensor.lastFiltered) { info.push( { label: 'Filtered: ', value: sensor.lastFiltered } ); } if (sensor.lastNoiseString) { info.push( { label: 'Noise: ', value: sensor.lastNoiseString } ); } if (sensor.lastSlope) { info.push( { label: 'Slope: ', value: sensor.lastSlope } ); } if (sensor.lastIntercept) { info.push( { label: 'Intercept: ', value: sensor.lastIntercept } ); } if (sensor.lastCalType) { info.push( { label: 'CalType: ', value: sensor.lastCalType } ); } if (sensor.lastCalibrationDate) { info.push( { label: 'Calibration: ', value: moment().diff(moment(sensor.lastCalibrationDate), 'hours') + ' hours ago' } ); } if (sensor.lastBatteryTimestamp) { info.push( { label: 'Battery: ', value: moment().diff(moment(sensor.lastBatteryTimestamp), 'minutes') + ' minutes ago' } ); } if (sensor.lastVoltageA) { info.push( { label: 'VoltageA: ', value: sensor.lastVoltageA } ); } if (sensor.lastVoltageB) { info.push( { label: 'VoltageB: ', value: sensor.lastVoltageB } ); } if (sensor.lastTemperature) { info.push( { label: 'Temperature: ', value: sensor.lastTemperature } ); } if (sensor.lastResistance) { info.push( { label: 'Resistance: ', value: sensor.lastResistance } ); } var statusClass = null; if (sensor.level === levels.URGENT) { statusClass = 'urgent'; } else if (sensor.level === levels.WARN) { statusClass = 'warn'; } else if (sensor.level === levels.INFO) { // Still highlight even the 'INFO' events for now statusClass = 'warn'; } sbx.pluginBase.updatePillText(sensorState, { value: (sensor && sensor.lastStateStringShort) || (sensor && sensor.lastStateString) || 'Unknown' , label: 'CGM' , info: info , pillClass: statusClass }); } }; function virtAsstGenericCGMHandler(translateItem, field, next, sbx) { var response; var state = _.get(sbx, 'properties.sensorState.'+field); if (state) { response = translate('virtAsstCGM'+translateItem, { params:[ state , moment(sbx.properties.sensorState.lastStateTime).from(moment(sbx.time)) ] }); } else { response = translate('virtAsstUnknown'); } next(translate('virtAsstTitleCGM'+translateItem), response); } sensorState.virtAsst = { intentHandlers: [ { intent: 'MetricNow' , metrics: ['cgm mode'] , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Mode', 'lastMode', next, sbx);} } , { intent: 'MetricNow' , metrics: ['cgm status'] , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Status', 'lastStateString', next, sbx);} } , { intent: 'MetricNow' , metrics: ['cgm session age'] , intentHandler: function(next, slots, sbx){ var response; var lastSessionStart = _.get(sbx, 'properties.sensorState.lastSessionStart'); // session start is only valid if in a session if (lastSessionStart) { if (_.get(sbx, 'properties.sensorState.lastState') != 0x1) { var duration = moment.duration(moment().diff(moment(lastSessionStart))); response = translate('virtAsstCGMSessAge', { params: [ duration.days(), duration.hours() ] }); } else { response = translate('virtAsstCGMSessNotStarted'); } } else { response = translate('virtAsstUnknown'); } next(translate('virtAsstTitleCGMSessAge'), response); } } , { intent: 'MetricNow' , metrics: ['cgm tx status'] , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('TxStatus', 'lastTxStatusString', next, sbx);} } , { intent: 'MetricNow' , metrics: ['cgm tx age'] , intentHandler: function(next, slots, sbx){ var lastTxActivation = _.get(sbx, 'properties.sensorState.lastTxActivation'); next( translate('virtAsstTitleCGMTxAge'), lastTxActivation ? translate('virtAsstCGMTxAge', {params:[moment().diff(moment(lastTxActivation), 'days')]}) : translate('virtAsstUnknown') ); } } , { intent: 'MetricNow' , metrics: ['cgm noise'] , intentHandler: function(next, slots, sbx){virtAsstGenericCGMHandler('Noise', 'lastNoiseString', next, sbx);} } , { intent: 'MetricNow' , metrics: ['cgm battery'] , intentHandler: function(next, slots, sbx){ var response; var lastVoltageA = _.get(sbx, 'properties.sensorState.lastVoltageA'); var lastVoltageB = _.get(sbx, 'properties.sensorState.lastVoltageB'); var lastBatteryTimestamp = _.get(sbx, 'properties.sensorState.lastBatteryTimestamp'); if (lastVoltageA || lastVoltageB) { if (lastVoltageA && lastVoltageB) { response = translate('virtAsstCGMBattTwo', { params:[ (lastVoltageA / 100) , (lastVoltageB / 100) , moment(lastBatteryTimestamp).from(moment(sbx.time)) ] }); } else { var finalValue = lastVoltageA ? lastVoltageA : lastVoltageB; response = translate('virtAsstCGMBattOne', { params:[ (finalValue / 100) , moment(lastBatteryTimestamp).from(moment(sbx.time)) ] }); } } else { response = translate('virtAsstUnknown'); } next(translate('virtAsstTitleCGMBatt'), response); } } ] }; return sensorState; } module.exports = init;