mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Merge branch 'dev' into wip/tokens
This commit is contained in:
@@ -84,6 +84,7 @@ Community maintained fork of the
|
||||
- [`mmconnect` (MiniMed Connect bridge)](#mmconnect-minimed-connect-bridge)
|
||||
- [`pump` (Pump Monitoring)](#pump-pump-monitoring)
|
||||
- [`openaps` (OpenAPS)](#openaps-openaps)
|
||||
- [`loop` (Loop)](#loop-loop)
|
||||
- [Extended Settings](#extended-settings)
|
||||
- [Pushover](#pushover)
|
||||
- [IFTTT Maker](#ifttt-maker)
|
||||
@@ -142,11 +143,11 @@ The server status and settings are available from `/api/v1/status.json`.
|
||||
|
||||
By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days.
|
||||
You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for.
|
||||
|
||||
|
||||
#### Example Queries
|
||||
|
||||
(replace `http://localhost:1337` with your base url, YOUR-SITE)
|
||||
|
||||
|
||||
* 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100`
|
||||
* BGs between 2 days: `http://localhost:1337/api/v1/entries/sgv.json?find[dateString][$gte]=2015-08-28&find[dateString][$lte]=2015-08-30`
|
||||
* Juice Box corrections in a year: `http://localhost:1337/api/v1/treatments.json?count=1000&find[carbs]=15&find[eventType]=Carb+Correction&find[created_at][$gte]=2015`
|
||||
@@ -180,7 +181,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
### Alarms
|
||||
|
||||
These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser)
|
||||
|
||||
|
||||
* `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's
|
||||
* `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent
|
||||
* `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart
|
||||
@@ -223,12 +224,12 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
* `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off`
|
||||
* `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm
|
||||
* `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled
|
||||
* `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"`
|
||||
* `SHOW_FORECAST` (`ar2`) - plugin forecasts that should be shown by default, supports space delimited values such as `"ar2 openaps"`
|
||||
* `LANGUAGE` (`en`) - language of Nightscout. If not available english is used
|
||||
* `SCALE_Y` (`log`) - The type of scaling used for the Y axis of the charts system wide.
|
||||
* The default `log` (logarithmic) option will let you see more detail towards the lower range, while still showing the full CGM range.
|
||||
* The `linear` option has equidistant tick marks, the range used is dynamic so that space at the top of chart isn't wasted.
|
||||
* The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale.
|
||||
* The `log-dynamic` is similar to the default `log` options, but uses the same dynamic range and the `linear` scale.
|
||||
* `EDIT_MODE` (`on`) - possible values `on` or `off`. Enable or disable icon allowing enter treatments edit mode
|
||||
|
||||
### Plugins
|
||||
@@ -238,7 +239,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
The built-in/example plugins that are available by default are listed below. The plugins may still need to be enabled by adding to the `ENABLE` environment variable.
|
||||
|
||||
#### Default Plugins
|
||||
|
||||
|
||||
These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"`
|
||||
|
||||
##### `delta` (BG Delta)
|
||||
@@ -289,7 +290,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
|
||||
##### `careportal` (Careportal)
|
||||
An optional form to enter treatments.
|
||||
|
||||
|
||||
##### `boluscalc` (Bolus Wizard)
|
||||
|
||||
##### `food` (Custom Foods)
|
||||
@@ -378,15 +379,22 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
##### `openaps` (OpenAPS)
|
||||
Integrated OpenAPS loop monitoring, uses these extended settings:
|
||||
* Requires `DEVICESTATUS_ADVANCED="true"` to be set
|
||||
* `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts.
|
||||
* `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered
|
||||
* `OPENAPS_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when OpenAPS isn't looping. If OpenAPS is going to offline for a period of time, you can add an `OpenAPS Offline` event for the expected duration from Careportal to avoid getting alerts.
|
||||
* `OPENAPS_WARN` (`30`) - The number of minutes since the last loop that needs to be exceed before an alert is triggered
|
||||
* `OPENAPS_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceed before an urgent alarm is triggered
|
||||
* `OPENAPS_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display by default. Any of the following fields: `status-symbol`, `status-label`, `iob`, `meal-assist`, `freq`, and `rssi`
|
||||
* `OPENAPS_RETRO_FIELDS` (`status-symbol status-label iob meal-assist rssi`) - The fields to display in retro mode. Any of the above fields.
|
||||
|
||||
|
||||
Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker).
|
||||
|
||||
|
||||
##### `loop` (Loop)
|
||||
iOS Loop app monitoring, uses these extended settings:
|
||||
* Requires `DEVICESTATUS_ADVANCED="true"` to be set
|
||||
* `LOOP_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications when Loop isn't looping.
|
||||
* `LOOP_WARN` (`30`) - The number of minutes since the last loop that needs to be exceeded before an alert is triggered
|
||||
* `LOOP_URGENT` (`60`) - The number of minutes since the last loop that needs to be exceeded before an urgent alarm is triggered
|
||||
* Add `loop` to `SHOW_FORECAST` to show forecasted BG.
|
||||
|
||||
|
||||
#### Extended Settings
|
||||
Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin.
|
||||
@@ -403,7 +411,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
You’ll need to [Create a Pushover Application](https://pushover.net/apps/build). You only need to set the Application name, you can ignore all the other settings, but setting an Icon is a nice touch. Maybe you'd like to use [this one](https://raw.githubusercontent.com/nightscout/cgm-remote-monitor/master/static/images/large.png)?
|
||||
|
||||
Pushover is configured using the following Environment Variables:
|
||||
|
||||
|
||||
* `ENABLE` - `pushover` should be added to the list of plugin, for example: `ENABLE="pushover"`.
|
||||
* `PUSHOVER_API_TOKEN` - Used to enable pushover notifications, this token is specific to the application you create from in [Pushover](https://pushover.net/), ***[additional pushover information](#pushover)*** below.
|
||||
* `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site, this can also be a pushover delivery group key to send to a group rather than just a single user. This also supports a space delimited list of keys. To disable `INFO` level pushes set this to `off`.
|
||||
@@ -415,16 +423,16 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"`
|
||||
If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"`
|
||||
If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"`
|
||||
|
||||
|
||||
If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements
|
||||
|
||||
For testing/development try [localtunnel](http://localtunnel.me/).
|
||||
|
||||
#### IFTTT Maker
|
||||
In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker).
|
||||
|
||||
|
||||
With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more.
|
||||
|
||||
|
||||
1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join)
|
||||
2. Find your secret key on the [maker page](https://ifttt.com/maker)
|
||||
3. Configure Nightscout by setting these environment variables:
|
||||
@@ -432,7 +440,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
* `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys.
|
||||
* `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys.
|
||||
4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe)
|
||||
|
||||
|
||||
Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are:
|
||||
* `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging.
|
||||
* `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green.
|
||||
@@ -444,7 +452,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
|
||||
### Treatment Profile
|
||||
Some of the [plugins](#plugins) make use of a treatment profile that can be edited using the Profile Editor, see the link in the Settings drawer on your site.
|
||||
|
||||
|
||||
Treatment Profile Fields:
|
||||
|
||||
* `timezone` (Time Zone) - time zone local to the patient. *Should be set.*
|
||||
@@ -456,7 +464,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.htm
|
||||
* `basal` The basal rate set on the pump.
|
||||
* `target_high` - Upper target for correction boluses.
|
||||
* `target_low` - Lower target for correction boluses.
|
||||
|
||||
|
||||
Some example profiles are [here](example-profiles.md).
|
||||
|
||||
## Setting environment variables
|
||||
@@ -493,7 +501,7 @@ the web interface on [http://192.168.33.10:1337](http://192.168.33.10:1337)
|
||||
|
||||
## Installation on Windows
|
||||
|
||||
If you have access to local computing resources and want to maintain more control over your data, you can host Nightscout and its database outside of the cloud. Windows Server supports MongoDB, Node.js, and Nightscout [installed on a single system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10.
|
||||
If you have access to local computing resources and want to maintain more control over your data, you can host Nightscout and its database outside of the cloud. Windows Server supports MongoDB, Node.js, and Nightscout [installed on a single system](https://github.com/jaylagorio/Nightscout-on-Windows-Server). Although the instructions are intended for Windows Server the procedure is compatible with client versions of Windows such as Windows 7 and Windows 10.
|
||||
|
||||
More questions?
|
||||
---------------
|
||||
|
||||
+1
-1
@@ -5591,7 +5591,7 @@ function init() {
|
||||
cs: 'Portál ošetření'
|
||||
,sv: 'Care Portal'
|
||||
,it: 'Somministrazioni'
|
||||
,de: 'Betreuungs-Portal'
|
||||
,de: 'Behandlungs-Portal'
|
||||
,ro: 'Care Portal'
|
||||
,bg: 'Въвеждане на данни'
|
||||
,nb: 'Omsorgsportal'
|
||||
|
||||
@@ -30,6 +30,7 @@ function init() {
|
||||
, require('./careportal')()
|
||||
, require('./pump')()
|
||||
, require('./openaps')()
|
||||
, require('./loop')()
|
||||
, require('./boluswizardpreview')()
|
||||
, require('./cannulaage')()
|
||||
, require('./sensorage')()
|
||||
@@ -50,6 +51,7 @@ function init() {
|
||||
, require('./cob')()
|
||||
, require('./pump')()
|
||||
, require('./openaps')()
|
||||
, require('./loop')()
|
||||
, require('./boluswizardpreview')()
|
||||
, require('./cannulaage')()
|
||||
, require('./sensorage')()
|
||||
@@ -177,4 +179,4 @@ function init() {
|
||||
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
module.exports = init;
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var moment = require('moment');
|
||||
var times = require('../times');
|
||||
var levels = require('../levels');
|
||||
var timeago = require('./timeago')();
|
||||
|
||||
// var ALL_STATUS_FIELDS = ['status-symbol', 'status-label', 'iob', 'freq', 'rssi']; Unused variable
|
||||
|
||||
function init() {
|
||||
|
||||
var loop = {
|
||||
name: 'loop'
|
||||
, label: 'Loop'
|
||||
, pluginType: 'pill-status'
|
||||
};
|
||||
|
||||
var firstPrefs = true;
|
||||
|
||||
loop.getPrefs = function getPrefs(sbx) {
|
||||
|
||||
var prefs = {
|
||||
warn: sbx.extendedSettings.warn ? sbx.extendedSettings.warn : 30
|
||||
, urgent: sbx.extendedSettings.urgent ? sbx.extendedSettings.urgent : 60
|
||||
, enableAlerts: sbx.extendedSettings.enableAlerts
|
||||
};
|
||||
|
||||
if (firstPrefs) {
|
||||
firstPrefs = false;
|
||||
console.info(' Prefs:', prefs);
|
||||
}
|
||||
|
||||
return prefs;
|
||||
};
|
||||
|
||||
loop.setProperties = function setProperties (sbx) {
|
||||
sbx.offerProperty('loop', function setLoop ( ) {
|
||||
return loop.analyzeData(sbx);
|
||||
});
|
||||
};
|
||||
|
||||
loop.analyzeData = function analyzeData (sbx) {
|
||||
var recentHours = 6;
|
||||
var recentMills = sbx.time - times.hours(recentHours).msecs;
|
||||
|
||||
var recentData = _.chain(sbx.data.devicestatus)
|
||||
.filter(function (status) {
|
||||
return ('loop' in status) && sbx.entryMills(status) <= sbx.time && sbx.entryMills(status) >= recentMills;
|
||||
}).value( );
|
||||
|
||||
var prefs = loop.getPrefs(sbx);
|
||||
var recent = moment(sbx.time).subtract(prefs.warn / 2, 'minutes');
|
||||
|
||||
function getDisplayForStatus (status) {
|
||||
|
||||
var desc = {
|
||||
symbol: '⚠'
|
||||
, code: 'warning'
|
||||
, label: 'Warning'
|
||||
};
|
||||
|
||||
if (!status) {
|
||||
return desc;
|
||||
}
|
||||
|
||||
if (status.failureReason || (status.enacted && !status.enacted.received)) {
|
||||
desc.symbol = 'x';
|
||||
desc.code = 'error';
|
||||
desc.label = 'Error';
|
||||
} else if (status.enacted && moment(status.timestamp).isAfter(recent)) {
|
||||
desc.symbol = '⌁';
|
||||
desc.code = 'enacted';
|
||||
desc.label = 'Enacted';
|
||||
} else if (status.recommendedTempBasal && moment(status.recommendedTempBasal.timestamp).isAfter(recent)) {
|
||||
desc.symbol = '⏀';
|
||||
desc.code = 'recommendation';
|
||||
desc.label = 'Recomendation';
|
||||
} else if (status.moment && status.moment.isAfter(recent)) {
|
||||
desc.symbol = '↻';
|
||||
desc.code = 'looping';
|
||||
desc.label = 'Looping';
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
var result = {
|
||||
lastLoop: null
|
||||
, lastEnacted: null
|
||||
, lastOkMoment: null
|
||||
};
|
||||
|
||||
function assignLastEnacted (loopStatus) {
|
||||
var enacted = loopStatus.enacted;
|
||||
if (enacted && enacted.timestamp) {
|
||||
enacted.moment = moment(enacted.timestamp);
|
||||
if (!result.lastEnacted || enacted.moment.isAfter(result.lastEnacted.moment)) {
|
||||
result.lastEnacted = enacted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assignLastLoop (loopStatus) {
|
||||
if (!result.lastLoop || loopStatus.moment.isAfter(result.lastLoop.moment)) {
|
||||
result.lastLoop = loopStatus;
|
||||
}
|
||||
}
|
||||
|
||||
function assignLastOkMoment (loopStatus) {
|
||||
if (!loopStatus.failureReason && (!result.lastOkMoment || loopStatus.moment.isAfter(result.lastOkMoment))) {
|
||||
result.lastOkMoment = loopStatus.moment;
|
||||
}
|
||||
}
|
||||
|
||||
_.forEach(recentData, function eachStatus (status) {
|
||||
if (status && status.loop && status.loop.timestamp) {
|
||||
var loopStatus = status.loop;
|
||||
loopStatus.moment = moment(loopStatus.timestamp);
|
||||
assignLastEnacted(loopStatus);
|
||||
assignLastLoop(loopStatus);
|
||||
assignLastOkMoment(loopStatus);
|
||||
}
|
||||
});
|
||||
|
||||
result.display = getDisplayForStatus(result.lastLoop);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
loop.checkNotifications = function checkNotifications(sbx) {
|
||||
var prefs = loop.getPrefs(sbx);
|
||||
|
||||
if (!prefs.enableAlerts) { return; }
|
||||
|
||||
var prop = sbx.properties.loop;
|
||||
|
||||
if (!prop.lastLoop) {
|
||||
console.info('Loop hasn\'t reported a loop yet');
|
||||
return;
|
||||
}
|
||||
|
||||
var now = moment();
|
||||
var level = statusLevel(prop, prefs, sbx);
|
||||
if (level >= levels.WARN) {
|
||||
sbx.notifications.requestNotify({
|
||||
level: level
|
||||
, title: 'Loop isn\'t looping'
|
||||
, message: 'Last Loop: ' + formatAgo(prop.lastLoop.moment, now.valueOf())
|
||||
, pushoverSound: 'echo'
|
||||
, group: 'Loop'
|
||||
, plugin: loop
|
||||
, debug: prop
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loop.updateVisualisation = function updateVisualisation (sbx) {
|
||||
var prop = sbx.properties.loop;
|
||||
|
||||
var prefs = loop.getPrefs(sbx);
|
||||
|
||||
function valueString (prefix, value) {
|
||||
return (value != null) ? prefix + value : '';
|
||||
}
|
||||
|
||||
var events = [ ];
|
||||
|
||||
function addRecommendedTempBasal() {
|
||||
if (prop.lastLoop && prop.lastLoop.recommendedTempBasal) {
|
||||
|
||||
var recommendedTempBasal = prop.lastLoop.recommendedTempBasal;
|
||||
|
||||
var valueParts = [
|
||||
'Suggested Temp: ' + recommendedTempBasal.rate + 'U/hour for ' +
|
||||
recommendedTempBasal.duration + 'm'
|
||||
];
|
||||
|
||||
valueParts = concatIOB(valueParts);
|
||||
valueParts = concatCOB(valueParts);
|
||||
|
||||
events.push({
|
||||
time: moment(recommendedTempBasal.timestamp)
|
||||
, value: valueParts.join('')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addLastEnacted() {
|
||||
if (prop.lastEnacted) {
|
||||
var canceled = prop.lastEnacted.rate === 0 && prop.lastEnacted.duration === 0;
|
||||
|
||||
var valueParts = [
|
||||
, '<b>Temp Basal' + (canceled ? ' Canceled' : ' Started') + '</b>'
|
||||
, canceled ? '' : ' ' + prop.lastEnacted.rate.toFixed(2) + 'U/hour for ' + prop.lastEnacted.duration + 'm'
|
||||
, valueString(', ', prop.lastEnacted.reason)
|
||||
];
|
||||
|
||||
valueParts = concatIOB(valueParts);
|
||||
valueParts = concatCOB(valueParts);
|
||||
|
||||
events.push({
|
||||
time: prop.lastEnacted.moment
|
||||
, value: valueParts.join('')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function concatIOB (valueParts) {
|
||||
if (prop.lastLoop && prop.lastLoop.iob) {
|
||||
var iob = prop.lastLoop.iob;
|
||||
valueParts = valueParts.concat([
|
||||
', IOB: '
|
||||
, sbx.roundInsulinForDisplayFormat(iob.iob) + 'U'
|
||||
, iob.basaliob ? ', Basal IOB ' + sbx.roundInsulinForDisplayFormat(iob.basaliob) + 'U' : ''
|
||||
]);
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
function concatCOB (valueParts) {
|
||||
if (prop.lastLoop && prop.lastLoop.cob) {
|
||||
cob = prop.lastLoop.cob;
|
||||
valueParts = valueParts.concat([
|
||||
', COB: '
|
||||
, cob.cob + 'g'
|
||||
]);
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
function getForecastPoints ( ) {
|
||||
var points = [ ];
|
||||
|
||||
function toPoints (startTime, offset) {
|
||||
return function toPoint (value, index) {
|
||||
return {
|
||||
mgdl: value
|
||||
, color: '#ff00ff'
|
||||
, mills: startTime.valueOf() + times.mins(5 * index).msecs + offset
|
||||
, noFade: true
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (prop.lastLoop && prop.lastLoop.predicted && prop.lastLoop.predicted.startDate) {
|
||||
var predicted = prop.lastLoop.predicted;
|
||||
var startTime = moment(predicted.startDate);
|
||||
if (predicted.values) {
|
||||
points = points.concat(_.map(predicted.values, toPoints(startTime, 0)));
|
||||
}
|
||||
// if (prop.lastPredBGs.IOB) {
|
||||
// points = points.concat(_.map(prop.lastPredBGs.IOB, toPoints(moment, 3000)));
|
||||
// }
|
||||
// if (prop.lastPredBGs.COB) {
|
||||
// points = points.concat(_.map(prop.lastPredBGs.COB, toPoints(moment, 7000)));
|
||||
// }
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
if ('error' === prop.display.code) {
|
||||
events.push({
|
||||
time: prop.lastLoop.moment
|
||||
, value: valueString('Error: ', prop.lastLoop.failureReason)
|
||||
});
|
||||
addRecommendedTempBasal();
|
||||
} else if ('enacted' === prop.display.code) {
|
||||
addLastEnacted();
|
||||
} else if ('looping' === prop.display.code) {
|
||||
addLastEnacted();
|
||||
} else {
|
||||
addRecommendedTempBasal();
|
||||
}
|
||||
|
||||
var sorted = _.sortBy(events, function toMill(event) {
|
||||
return event.time.valueOf();
|
||||
}).reverse();
|
||||
|
||||
var info = _.map(sorted, function eventToInfo (event) {
|
||||
return {
|
||||
label: timeAt(false, sbx) + timeFormat(event.time, sbx)
|
||||
, value: event.value
|
||||
};
|
||||
});
|
||||
|
||||
var loopName = 'Loop';
|
||||
|
||||
if (prop.lastLoop && prop.lastLoop.name) {
|
||||
loopName = prop.lastLoop.name;
|
||||
}
|
||||
|
||||
var label = loopName + ' ' + prop.display.symbol;
|
||||
|
||||
var lastLoopMoment = prop.lastLoop ? prop.lastLoop.moment : null;
|
||||
|
||||
sbx.pluginBase.updatePillText(loop, {
|
||||
value: timeFormat(lastLoopMoment, sbx)
|
||||
, label: label
|
||||
, info: info
|
||||
, pillClass: statusClass(prop, prefs, sbx)
|
||||
});
|
||||
|
||||
var forecastPoints = getForecastPoints();
|
||||
if (forecastPoints && forecastPoints.length > 0) {
|
||||
sbx.pluginBase.addForecastPoints(forecastPoints, {type: 'loop', label: 'Loop Forecasts'});
|
||||
}
|
||||
};
|
||||
|
||||
function statusClass (prop, prefs, sbx) {
|
||||
var level = statusLevel(prop, prefs, sbx);
|
||||
var cls = 'current';
|
||||
|
||||
if (level === levels.WARN) {
|
||||
cls = 'warn';
|
||||
} else if (level === levels.URGENT) {
|
||||
cls = 'urgent';
|
||||
}
|
||||
|
||||
return cls;
|
||||
}
|
||||
|
||||
function statusLevel (prop, prefs, sbx) {
|
||||
var level = levels.NONE;
|
||||
var now = moment(sbx.time);
|
||||
|
||||
if (prop.lastOkMoment) {
|
||||
var urgentTime = prop.lastOkMoment.clone().add(prefs.urgent, 'minutes');
|
||||
var warningTime = prop.lastOkMoment.clone().add(prefs.warn, 'minutes');
|
||||
|
||||
if (urgentTime.isBefore(now)) {
|
||||
level = levels.URGENT;
|
||||
} else if (warningTime.isBefore(now)) {
|
||||
level = levels.WARN;
|
||||
}
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
return loop;
|
||||
|
||||
}
|
||||
|
||||
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 (ago.value ? ago.value : '') + ago.shortLabel + (ago.shortLabel.length === 1 ? ' ago' : '');
|
||||
}
|
||||
|
||||
function timeAt (prefix, sbx) {
|
||||
return sbx.data.inRetroMode ? (prefix ? ' ' : '') + '@ ' : (prefix ? ', ' : '');
|
||||
}
|
||||
|
||||
module.exports = init;
|
||||
@@ -0,0 +1,237 @@
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var should = require('should');
|
||||
var moment = require('moment');
|
||||
|
||||
var env = require('../env')();
|
||||
var loop = require('../lib/plugins/loop')();
|
||||
var sandbox = require('../lib/sandbox')();
|
||||
var levels = require('../lib/levels');
|
||||
|
||||
var statuses = [
|
||||
{
|
||||
'created_at':'2016-08-13T20:09:15Z',
|
||||
'device':'loop://ExamplePhone',
|
||||
'loop':{
|
||||
'enacted':{
|
||||
'timestamp':'2016-08-13T20:09:15Z',
|
||||
'rate':0.875,
|
||||
'duration':30,
|
||||
'received':true
|
||||
},
|
||||
'version':'0.9.1',
|
||||
'recommendedBolus':0,
|
||||
'timestamp':'2016-08-13T20:09:15Z',
|
||||
'predicted':{
|
||||
'startDate':'2016-08-13T20:03:47Z',
|
||||
'values':[
|
||||
149,
|
||||
149,
|
||||
148,
|
||||
148,
|
||||
147,
|
||||
147
|
||||
]
|
||||
},
|
||||
'iob':{
|
||||
'timestamp':'2016-08-13T20:05:00Z',
|
||||
'iob':0.1733152537837709
|
||||
},
|
||||
'name':'Loop'
|
||||
}
|
||||
},
|
||||
{
|
||||
'created_at':'2016-08-13T20:04:15Z',
|
||||
'device':'loop://ExamplePhone',
|
||||
'loop':{
|
||||
'version':'0.9.1',
|
||||
'recommendedBolus':0,
|
||||
'timestamp':'2016-08-13T20:04:15Z',
|
||||
'failureReason':'SomeError',
|
||||
'name':'Loop'
|
||||
}
|
||||
},
|
||||
{
|
||||
'created_at':'2016-08-13T01:13:20Z',
|
||||
'device':'loop://ExamplePhone',
|
||||
'loop':{
|
||||
'timestamp':'2016-08-13T01:18:20Z',
|
||||
'version':'0.9.1',
|
||||
'iob':{
|
||||
'timestamp':'2016-08-13T01:15:00Z',
|
||||
'iob':-0.1205140849137931
|
||||
},
|
||||
'name':'Loop'
|
||||
}
|
||||
},
|
||||
{
|
||||
'created_at':'2016-08-13T01:13:20Z',
|
||||
'device':'loop://ExamplePhone',
|
||||
'loop':{
|
||||
'timestamp':'2016-08-13T01:13:20Z',
|
||||
'version':'0.9.1',
|
||||
'iob':{
|
||||
'timestamp':'2016-08-13T01:10:00Z',
|
||||
'iob':-0.1205140849137931
|
||||
},
|
||||
'failureReason':'StaleDataError(\"Glucose Date: 2016-08-12 23:23:49 +0000 or Pump status date: 2016-08-13 01:13:10 +0000 older than 15.0 min\")',
|
||||
'name':'Loop'
|
||||
}
|
||||
},
|
||||
{
|
||||
'created_at':'2016-08-13T01:13:15Z',
|
||||
'pump':{
|
||||
'reservoir':90.5,
|
||||
'clock':'2016-08-13T01:13:10Z',
|
||||
'battery':{
|
||||
'status':'normal',
|
||||
'voltage':1.5
|
||||
},
|
||||
'pumpID':'543204'
|
||||
},
|
||||
'device':'loop://ExamplePhone',
|
||||
'uploader':{
|
||||
'timestamp':'2016-08-13T01:13:15Z',
|
||||
'battery':43,
|
||||
'name':'ExamplePhone'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
var now = moment(statuses[0].created_at);
|
||||
|
||||
_.forEach(statuses, function updateMills (status) {
|
||||
status.mills = moment(status.created_at).valueOf();
|
||||
});
|
||||
|
||||
describe('loop', function ( ) {
|
||||
|
||||
it('should set the property and update the pill and add forecast points', function (done) {
|
||||
var ctx = {
|
||||
settings: {
|
||||
units: 'mg/dl'
|
||||
}
|
||||
, pluginBase: {
|
||||
updatePillText: function mockedUpdatePillText (plugin, options) {
|
||||
options.label.should.equal('Loop ⌁');
|
||||
options.value.should.equal('1m ago');
|
||||
var first = _.first(options.info);
|
||||
first.label.should.equal('1m ago');
|
||||
first.value.should.equal('<b>Temp Basal Started</b> 0.88U/hour for 30m, IOB: 0.17U');
|
||||
}
|
||||
, addForecastPoints: function mockAddForecastPoints (points) {
|
||||
points.length.should.equal(6);
|
||||
done();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sbx = sandbox.clientInit(ctx, now.valueOf(), {devicestatus: statuses});
|
||||
|
||||
var unmockedOfferProperty = sbx.offerProperty;
|
||||
sbx.offerProperty = function mockedOfferProperty (name, setter) {
|
||||
name.should.equal('loop');
|
||||
var result = setter();
|
||||
should.exist(result);
|
||||
|
||||
result.display.symbol.should.equal('⌁');
|
||||
result.display.code.should.equal('enacted');
|
||||
|
||||
sbx.offerProperty = unmockedOfferProperty;
|
||||
unmockedOfferProperty(name, setter);
|
||||
};
|
||||
|
||||
loop.setProperties(sbx);
|
||||
loop.updateVisualisation(sbx);
|
||||
});
|
||||
|
||||
it('should show errors', function (done) {
|
||||
var ctx = {
|
||||
settings: {
|
||||
units: 'mg/dl'
|
||||
}
|
||||
, pluginBase: {
|
||||
updatePillText: function mockedUpdatePillText (plugin, options) {
|
||||
options.label.should.equal('Loop x');
|
||||
options.value.should.equal('1m ago');
|
||||
var first = _.first(options.info);
|
||||
first.label.should.equal('1m ago');
|
||||
first.value.should.equal('Error: SomeError');
|
||||
done();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var errorTime = moment(statuses[1].created_at);
|
||||
|
||||
var sbx = sandbox.clientInit(ctx, errorTime.valueOf(), {devicestatus: statuses});
|
||||
|
||||
var unmockedOfferProperty = sbx.offerProperty;
|
||||
sbx.offerProperty = function mockedOfferProperty (name, setter) {
|
||||
name.should.equal('loop');
|
||||
var result = setter();
|
||||
should.exist(result);
|
||||
|
||||
result.display.symbol.should.equal('x');
|
||||
result.display.code.should.equal('error');
|
||||
|
||||
sbx.offerProperty = unmockedOfferProperty;
|
||||
unmockedOfferProperty(name, setter);
|
||||
};
|
||||
|
||||
loop.setProperties(sbx);
|
||||
|
||||
loop.updateVisualisation(sbx);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should check the recieved flag to see if it was received', function (done) {
|
||||
var ctx = {
|
||||
settings: {
|
||||
units: 'mg/dl'
|
||||
}
|
||||
, notifications: require('../lib/notifications')(env, ctx)
|
||||
};
|
||||
|
||||
ctx.notifications.initRequests();
|
||||
|
||||
var notStatuses = _.cloneDeep(statuses);
|
||||
notStatuses[0].loop.enacted.received = false;
|
||||
var sbx = require('../lib/sandbox')().clientInit(ctx, now, {devicestatus: notStatuses});
|
||||
|
||||
sbx.offerProperty = function mockedOfferProperty (name, setter) {
|
||||
name.should.equal('loop');
|
||||
var result = setter();
|
||||
should.exist(result);
|
||||
result.display.symbol.should.equal('x');
|
||||
result.display.code.should.equal('error');
|
||||
done();
|
||||
};
|
||||
|
||||
loop.setProperties(sbx);
|
||||
});
|
||||
|
||||
it('should generate an alert for a stuck loop', function (done) {
|
||||
var ctx = {
|
||||
settings: {
|
||||
units: 'mg/dl'
|
||||
}
|
||||
, notifications: require('../lib/notifications')(env, ctx)
|
||||
};
|
||||
|
||||
ctx.notifications.initRequests();
|
||||
|
||||
var sbx = sandbox.clientInit(ctx, now.add(2, 'hours').valueOf(), {devicestatus: statuses});
|
||||
sbx.extendedSettings = { 'enableAlerts': 'TRUE' };
|
||||
loop.setProperties(sbx);
|
||||
loop.checkNotifications(sbx);
|
||||
|
||||
var highest = ctx.notifications.findHighestAlarm('Loop');
|
||||
highest.level.should.equal(levels.URGENT);
|
||||
highest.title.should.equal('Loop isn\'t looping');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user