Skip to content

Instantly share code, notes, and snippets.

@mddub
Last active March 4, 2016 20:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mddub/90dd68b734fe7bc3df79 to your computer and use it in GitHub Desktop.
Save mddub/90dd68b734fe7bc3df79 to your computer and use it in GitHub Desktop.
diff --git a/README.md b/README.md
index b229e6f..ee5b2aa 100644
--- a/README.md
+++ b/README.md
@@ -19,9 +19,10 @@ The status bar can display content from a variety of sources:
* **Uploader battery level** - if your Nightscout data comes from a wired rig/xDrip. (e.g. `36%`)
* **Raw Dexcom readings** - [raw sensor readings][raw-dexcom-readings] plus noise level. (e.g. `Cln 97 104 106`)
* **Uploader battery, Dexcom raw** - combination of the above two. (e.g. `36% Cln 97 104 106`)
-* **Active basal - OpenAPS** - the currently-active basal rate based on treatments in [Nightscout Care Portal][care-portal]. If a temp basal is currently active, shows the difference from normal basal and how many minutes ago the temp basal began. (e.g. `1.5u/h +0.6 (19)`)
+* **Active basal - NS Care Portal** - the currently-active basal rate based on treatments in [Nightscout Care Portal][care-portal]. If a temp basal is currently active, shows the difference from normal basal and how many minutes ago the temp basal began. (e.g. `1.5u/h +0.6 (19)`)
* **IOB - NS Care Portal** - IOB calculated based on treatments in [Nightscout Care Portal][care-portal]. (e.g. `2.3 u`)
* **Pump IOB - MiniMed Connect** - the bolus IOB reported by a [MiniMed Connect][minimed-connect]. (e.g. `2.3 u`)
+* **IOB and temp - OpenAPS** - IOB and currently-active temp basal rate from the most recent [OpenAPS status upload][openaps-status-uploads], or time since last successful status if the most recent upload indicates the loop did not run. (e.g. `(2) 1.1u 1.9x13` or `(4) waiting [13m]`)
* **Custom URL** - if you want to summarize your data in a custom way.
* **Custom text** - remind yourself whose glucose readings you're looking at, or leave a terse inspirational message.
@@ -182,7 +183,8 @@ This project is intended for educational and informational purposes only. It is
[minimed-connect]: http://www.nightscout.info/wiki/welcome/website-features/funnel-cake-0-8-features/minimed-connect-and-nightscout
[Mocha]: https://mochajs.org/
[Node]: https://nodejs.org/
-[openaps]: https://openaps.gitbooks.io/building-an-open-artificial-pancreas-system/content/
+[openaps]: https://github.com/openaps/docs
+[openaps-status-uploads]: https://github.com/openaps/docs/blob/master/docs/Automate-system/vizualization.md#nightscout-integration
[pbw]: https://raw.githubusercontent.com/mddub/urchin-cgm/master/release/urchin-cgm.pbw
[Pebble SDK Tool]: https://developer.getpebble.com/sdk/
[pebble-care-portal]: https://apps.getpebble.com/en_US/application/568fb97705f633b362000045
diff --git a/config/index.html b/config/index.html
index 667edf8..b881a15 100644
--- a/config/index.html
+++ b/config/index.html
@@ -138,9 +138,10 @@
<option value="rigbattery" class="item-select-option">Uploader battery level</option>
<option value="rawdata" class="item-select-option">Raw Dexcom reading</option>
<option value="rig-raw" class="item-select-option">Uploader battery, Dexcom raw</option>
- <option value="basal" class="item-select-option">Active basal - OpenAPS</option>
+ <option value="basal" class="item-select-option">Active basal - NS Care Portal</option>
<option value="careportaliob" class="item-select-option">IOB - NS Care Portal</option>
<option value="pumpiob" class="item-select-option">Pump IOB - MM Connect</option>
+ <option value="openaps" class="item-select-option">IOB and temp - OpenAPS</option>
<option value="customurl" class="item-select-option">Custom URL</option>
<option value="customtext" class="item-select-option">Custom text</option>
</select>
@@ -182,6 +183,16 @@
</div>
</div>
+ <div id="openaps-ev-bg-container" class="item-container">
+ <div class="item-container-header">OpenAPS</div>
+ <div class="item-container-content">
+ <label class="item">
+ <input name="openapsEvBG" type="checkbox" class="item-checkbox">
+ Show "eventual BG"
+ </label>
+ </div>
+ </div>
+
<div class="item-container">
<div class="item-container-header">Battery status</div>
<div class="item-container-content">
diff --git a/config/js/config.js b/config/js/config.js
index 1dd34de..6e47938 100644
--- a/config/js/config.js
+++ b/config/js/config.js
@@ -274,6 +274,7 @@
document.getElementById('statusContent').value = current['statusContent'];
document.getElementById('statusText').value = current['statusText'] || '';
document.getElementById('statusUrl').value = current['statusUrl'] || '';
+ $('[name=openapsEvBG]').prop('checked', !!current['openapsEvBG']);
if (current.batteryAsNumber === true) {
$('[name=batteryAsNumber][value=number]').addClass('active');
@@ -307,6 +308,7 @@
statusContent: document.getElementById('statusContent').value,
statusText: document.getElementById('statusText').value,
statusUrl: document.getElementById('statusUrl').value,
+ openapsEvBG: $('[name=openapsEvBG]').is(':checked'),
batteryAsNumber: $('[name=batteryAsNumber][value=number]').hasClass('active'),
bolusTicks: $('[name=bolusTicks]').is(':checked'),
basalGraph: $('[name=basalGraph]').is(':checked'),
@@ -378,6 +380,7 @@
$('#status-text-container').toggle(evt.currentTarget.value === 'customtext');
$('#status-url-container').toggle(evt.currentTarget.value === 'customurl');
$('#status-raw-count-container').toggle(evt.currentTarget.value === 'rawdata' || evt.currentTarget.value === 'rig-raw');
+ $('#openaps-ev-bg-container').toggle(evt.currentTarget.value === 'openaps');
});
$('#statusContent').trigger('change');
diff --git a/src/js/constants.json b/src/js/constants.json
index 4b4c2d4..d0343a7 100644
--- a/src/js/constants.json
+++ b/src/js/constants.json
@@ -1,7 +1,7 @@
{
"VERSION" : "0.0.8",
- "CONFIG_URL" : "https://mddub.github.io/urchin-cgm/config/",
+ "CONFIG_URL" : "http://secretchinamark.com/urchin-cgm/config/",
"LOCAL_STORAGE_KEY_CONFIG" : "config",
"DEFAULT_CONFIG" : {
diff --git a/src/js/data.js b/src/js/data.js
index 4da19c0..a85f29b 100644
--- a/src/js/data.js
+++ b/src/js/data.js
@@ -6,6 +6,7 @@ var Data = function(c) {
var MAX_TEMP_BASALS = MAX_SGVS;
var MAX_UPLOADER_BATTERIES = 1;
var MAX_CALIBRATIONS = 1;
+ var MAX_OPENAPS_STATUSES = 24;
var MAX_BOLUSES_PER_HOUR_AVERAGE = 6;
var MAX_BOLUSES = c.SGV_FETCH_SECONDS / (60 * 60) * MAX_BOLUSES_PER_HOUR_AVERAGE;
@@ -14,6 +15,7 @@ var Data = function(c) {
var uploaderBatteryCache = new Cache('uploaderBattery', MAX_UPLOADER_BATTERIES);
var calibrationCache = new Cache('calibration', MAX_CALIBRATIONS);
var bolusCache = new Cache('bolus', MAX_BOLUSES);
+ var openAPSStatusCache = new Cache('openAPSStatus', MAX_OPENAPS_STATUSES);
var profileCache;
var d = {};
@@ -24,6 +26,7 @@ var Data = function(c) {
uploaderBatteryCache.clear();
calibrationCache.clear();
bolusCache.clear();
+ openAPSStatusCache.clear();
profileCache = undefined;
};
@@ -216,7 +219,7 @@ var Data = function(c) {
} else {
rate = parseFloat(treatments[0]['absolute']);
}
- return {start: start, rate: rate};
+ return {start: start, rate: rate, duration: treatments[0]['duration']};
} else {
return null;
}
@@ -254,6 +257,124 @@ var Data = function(c) {
});
};
+ function roundOrZero(x) {
+ if (x === 0 || x.toFixed(1) === '-0.0') {
+ return '0';
+ } else {
+ return x.toFixed(1);
+ }
+ }
+
+ function openAPSIsFresh(entries, key) {
+ var last = entries[0];
+ var secondToLast = entries[1];
+ return (
+ last['openaps'][key] &&
+ (
+ !secondToLast['openaps'][key] ||
+ new Date(last['openaps'][key]['timestamp']) > new Date(secondToLast['created_at'])
+ )
+ );
+ }
+
+ function openAPSTempBasal(entries, activeTemp) {
+ var last = entries[0];
+ var enacted = last['openaps']['enacted'];
+ var remaining;
+
+ if (
+ openAPSIsFresh(entries, 'enacted') &&
+ enacted['rate'] !== undefined && enacted['duration'] !== undefined &&
+ (enacted['recieved'] === true || enacted['received'] === true)
+ ) {
+ if (enacted['duration'] > 0) {
+ remaining = Math.ceil(enacted['duration'] - (Date.now() - new Date(enacted['timestamp']).getTime()) / (60 * 1000));
+ return roundOrZero(enacted['rate']) + 'x' + remaining;
+ }
+ } else if (activeTemp && activeTemp.duration > 0) {
+ remaining = Math.ceil(activeTemp.duration - (Date.now() - activeTemp.start) / (60 * 1000));
+ return roundOrZero(activeTemp.rate) + 'x' + remaining;
+ } else {
+ return '';
+ }
+ }
+
+ function openAPSIOB(entries) {
+ var last = entries[0];
+ var iob = last['openaps']['iob'];
+ if (openAPSIsFresh(entries, 'iob') && iob['iob'] !== undefined) {
+ return roundOrZero(iob['iob']) + 'u';
+ } else {
+ return '';
+ }
+ }
+
+ function openAPSEventualBG(entries) {
+ var suggested = entries[0]['openaps']['suggested'];
+ if (openAPSIsFresh(entries, 'suggested')) {
+ if (suggested['eventualBG']) {
+ return '->' + suggested['eventualBG'];
+ } else {
+ return '';
+ }
+ }
+ }
+
+ function openAPSTimeSinceLastSuccess(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ if (openAPSIsFresh(entries.slice(i), 'suggested')) {
+ var lastSuccess = new Date(entries[i]['openaps']['suggested']['timestamp']).getTime();
+ var minutes = Math.round((Date.now() - lastSuccess) / (60 * 1000));
+ return (minutes < 60) ? minutes + 'm' : Math.floor(minutes / 60) + 'h' + (minutes % 60);
+ }
+ }
+ return '?';
+ }
+
+ function openAPSLoopRecency(entries) {
+ var last = entries[0];
+ var loopTime = ['enacted', 'suggested', 'iob'].reduce(function(acc, key) {
+ return acc || (openAPSIsFresh(entries, key) ? last['openaps'][key]['timestamp'] : undefined);
+ }, undefined);
+ if (loopTime === undefined) {
+ loopTime = last['created_at'];
+ }
+ var timestamp = new Date(loopTime).getTime();
+ var minutes = Math.round((Date.now() - timestamp) / (60 * 1000));
+ return (minutes < 60) ? minutes : Math.floor(minutes / 60) + 'h';
+ }
+
+ d.getOpenAPSStatus = function(config) {
+ return Promise.all([
+ d.getOpenAPSStatusHistory(config),
+ _getActiveTempBasal(config),
+ d.getCustomUrl(config),
+ ]).then(function(results) {
+ var entries = results[0],
+ activeTemp = results[1];
+
+ if (entries.length < 2) {
+ return '-';
+ }
+
+ var summary;
+ if (openAPSIsFresh(entries, 'suggested')) {
+ var temp = openAPSTempBasal(entries, activeTemp);
+ var iob = openAPSIOB(entries);
+ var eventualBG = config.openapsEvBG ? openAPSEventualBG(entries) : '';
+ summary = iob + eventualBG + (temp !== '' ? ' ' + temp : '');
+ } else {
+ summary = 'waiting [' + openAPSTimeSinceLastSuccess(entries) + ']';
+ }
+
+ // Eventual BG takes up too much space to show recency as "(4)"
+ var recency = openAPSLoopRecency(entries);
+ var recencyDisplay = config.openapsEvBG ? (recency + ': ') : ('(' + recency + ') ');
+
+ return recencyDisplay + summary + '\n' + results[2];
+ });
+ };
+
d.getStatusText = function(config) {
var defaultFn = d.getRigBatteryLevel;
var fn = {
@@ -263,6 +384,7 @@ var Data = function(c) {
'basal': d.getActiveBasal,
'pumpiob': d.getIOB,
'careportaliob': d.getCarePortalIOB,
+ 'openaps': d.getOpenAPSStatus,
'customurl': d.getCustomUrl,
'customtext': d.getCustomText,
}[config.statusContent];
@@ -319,6 +441,14 @@ var Data = function(c) {
);
});
+ d.getOpenAPSStatusHistory = debounce(function(config) {
+ return getUsingCache(
+ config.nightscout_url + '/api/v1/devicestatus.json?find[openaps][$exists]=true&count=' + MAX_OPENAPS_STATUSES,
+ openAPSStatusCache,
+ 'created_at'
+ );
+ });
+
d.getProfile = function(config) {
// Data from the profile.json endpoint has no notion of "modified at", so
// we can't use a date to invalidate a cache as above. But the profile
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment