history.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. var tz = require('moment-timezone');
  2. var basalprofile = require('../profile/basal.js');
  3. var _ = require('lodash');
  4. var moment = require('moment');
  5. function splitTimespanWithOneSplitter(event,splitter) {
  6. var resultArray = [event];
  7. if (splitter.type === 'recurring') {
  8. var startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes();
  9. var endMinutes = startMinutes + event.duration;
  10. // 1440 = one day; no clean way to check if the event overlaps midnight
  11. // so checking if end of event in minutes is past midnight
  12. if (event.duration > 30 || (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || (endMinutes > 1440 && splitter.minutes < (endMinutes - 1440))) {
  13. var event1 = _.cloneDeep(event);
  14. var event2 = _.cloneDeep(event);
  15. var event1Duration = 0;
  16. if (event.duration > 30) {
  17. event1Duration = 30;
  18. } else {
  19. var splitPoint = splitter.minutes;
  20. if (endMinutes > 1440) { splitPoint = 1440; }
  21. event1Duration = splitPoint - startMinutes;
  22. }
  23. var event1EndDate = moment(event.started_at).add(event1Duration,'minutes');
  24. event1.duration = event1Duration;
  25. event2.duration = event.duration - event1Duration;
  26. event2.timestamp = event1EndDate.format();
  27. event2.started_at = new Date(event2.timestamp);
  28. event2.date = event2.started_at.getTime();
  29. resultArray = [event1,event2];
  30. }
  31. }
  32. return resultArray;
  33. }
  34. function splitTimespan(event, splitterMoments) {
  35. var results = [event];
  36. var splitFound = true;
  37. while(splitFound) {
  38. var resultArray = [];
  39. splitFound = false;
  40. _.forEach(results,function split(o) {
  41. _.forEach(splitterMoments,function split(p) {
  42. var splitResult = splitTimespanWithOneSplitter(o,p);
  43. if (splitResult.length > 1) {
  44. resultArray = resultArray.concat(splitResult);
  45. splitFound = true;
  46. return false;
  47. }
  48. });
  49. if (!splitFound) resultArray = resultArray.concat([o]);
  50. });
  51. results = resultArray;
  52. }
  53. return results;
  54. }
  55. // Split currentEvent around any conflicting suspends
  56. // by removing the time period from the event that
  57. // overlaps with any suspend.
  58. function splitAroundSuspends (currentEvent, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended) {
  59. var events = [];
  60. var firstResumeStarted = new Date(firstResumeTime);
  61. var firstResumeDate = firstResumeStarted.getTime()
  62. var lastSuspendStarted = new Date(lastSuspendTime);
  63. var lastSuspendDate = lastSuspendStarted.getTime();
  64. if (suspendedPrior && (currentEvent.date < firstResumeDate)) {
  65. if ((currentEvent.date+currentEvent.duration*60*1000) < firstResumeDate) {
  66. currentEvent.duration = 0;
  67. } else {
  68. currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000;
  69. currentEvent.started_at = new Date(tz(firstResumeTime));
  70. currentEvent.date = firstResumeDate
  71. }
  72. }
  73. if (currentlySuspended && ((currentEvent.date+currentEvent.duration*60*1000) > lastSuspendTime)) {
  74. if (currentEvent.date > lastSuspendTime) {
  75. currentEvent.duration = 0;
  76. } else {
  77. currentEvent.duration = (firstResumeDate - currentEvent.date)/60/1000;
  78. }
  79. }
  80. events.push(currentEvent);
  81. if (currentEvent.duration === 0) {
  82. // bail out rather than wasting time going through the rest of the suspend events
  83. return events;
  84. }
  85. for (var i=0; i < pumpSuspends.length; i++) {
  86. var suspend = pumpSuspends[i];
  87. for (var j=0; j < events.length; j++) {
  88. if ((events[j].date <= suspend.date) && (events[j].date+events[j].duration*60*1000) > suspend.date) {
  89. // event started before the suspend, but finished after the suspend started
  90. if ((events[j].date+events[j].duration*60*1000) > (suspend.date+suspend.duration*60*1000)) {
  91. var event2 = _.cloneDeep(events[j]);
  92. var event2StartDate = moment(suspend.started_at).add(suspend.duration,'minutes');
  93. event2.timestamp = event2StartDate.format();
  94. event2.started_at = new Date(tz(event2.timestamp));
  95. event2.date = suspend.date+suspend.duration*60*1000;
  96. event2.duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000;
  97. events.push(event2);
  98. }
  99. events[j].duration = (suspend.date-events[j].date)/60/1000;
  100. } else if ((suspend.date <= events[j].date) && (suspend.date+suspend.duration*60*1000 > events[j].date)) {
  101. // suspend started before the event, but finished after the event started
  102. events[j].duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000;
  103. var eventStartDate = moment(suspend.started_at).add(suspend.duration,'minutes');
  104. events[j].timestamp = eventStartDate.format();
  105. events[j].started_at = new Date(tz(events[j].timestamp));
  106. events[j].date = suspend.date + suspend.duration*60*1000;
  107. }
  108. }
  109. }
  110. return events;
  111. }
  112. function calcTempTreatments (inputs, zeroTempDuration) {
  113. var pumpHistory = inputs.history;
  114. var pumpHistory24 = inputs.history24;
  115. var profile_data = inputs.profile;
  116. var autosens_data = inputs.autosens;
  117. var tempHistory = [];
  118. var tempBoluses = [];
  119. var pumpSuspends = [];
  120. var pumpResumes = [];
  121. var suspendedPrior = false;
  122. var firstResumeTime, lastSuspendTime;
  123. var currentlySuspended = false;
  124. var suspendError = false;
  125. var now = new Date(tz(inputs.clock));
  126. if(inputs.history24) {
  127. var pumpHistory = [ ].concat(inputs.history).concat(inputs.history24);
  128. }
  129. var lastRecordTime = now;
  130. // Gather the times the pump was suspended and resumed
  131. for (var i=0; i < pumpHistory.length; i++) {
  132. var temp = {};
  133. var current = pumpHistory[i];
  134. if (current._type === "PumpSuspend") {
  135. temp.timestamp = current.timestamp;
  136. temp.started_at = new Date(tz(current.timestamp));
  137. temp.date = temp.started_at.getTime();
  138. pumpSuspends.push(temp);
  139. } else if (current._type === "PumpResume") {
  140. temp.timestamp = current.timestamp;
  141. temp.started_at = new Date(tz(current.timestamp));
  142. temp.date = temp.started_at.getTime();
  143. pumpResumes.push(temp);
  144. }
  145. }
  146. pumpSuspends = _.sortBy(pumpSuspends, 'date');
  147. pumpResumes = _.sortBy(pumpResumes, 'date');
  148. if (pumpResumes.length > 0) {
  149. firstResumeTime = pumpResumes[0].timestamp;
  150. // Check to see if our first resume was prior to our first suspend
  151. // indicating suspend was prior to our first event.
  152. if (pumpSuspends.length === 0 || (pumpResumes[0].date < pumpSuspends[0].date)) {
  153. suspendedPrior = true;
  154. }
  155. }
  156. var j=0; // matching pumpResumes entry;
  157. // Match the resumes with the suspends to get durations
  158. for (i=0; i < pumpSuspends.length; i++) {
  159. for (; j < pumpResumes.length; j++) {
  160. if (pumpResumes[j].date > pumpSuspends[i].date) {
  161. break;
  162. }
  163. }
  164. if ((j >= pumpResumes.length) && !currentlySuspended) {
  165. // even though it isn't the last suspend, we have reached
  166. // the final suspend. Set resume last so the
  167. // algorithm knows to suspend all the way
  168. // through the last record beginning at the last suspend
  169. // since we don't have a matching resume.
  170. currentlySuspended = 1;
  171. lastSuspendTime = pumpSuspends[i].timestamp;
  172. break;
  173. }
  174. pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date)/60/1000;
  175. }
  176. // These checks indicate something isn't quite aligned.
  177. // Perhaps more resumes that suspends or vice versa...
  178. if (!suspendedPrior && !currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) {
  179. console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+")!");
  180. } else if (suspendedPrior && !currentlySuspended && ((pumpResumes.length-1) !== pumpSuspends.length)) {
  181. console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to history block!");
  182. } else if (!suspendedPrior && currentlySuspended && (pumpResumes.length !== (pumpSuspends.length-1))) {
  183. console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended past end of history block!");
  184. } else if (suspendedPrior && currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) {
  185. console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to and past end of history block!");
  186. }
  187. if (i < (pumpSuspends.length-1)) {
  188. // truncate any extra suspends. if we had any extras
  189. // the error checks above would have issued a error log message
  190. pumpSuspends.splice(i+1, pumpSuspends.length-i-1);
  191. }
  192. // Pick relevant events for processing and clean the data
  193. for (i=0; i < pumpHistory.length; i++) {
  194. var current = pumpHistory[i];
  195. if (current.bolus && current.bolus._type === "Bolus") {
  196. var temp = current;
  197. current = temp.bolus;
  198. }
  199. if (current.created_at) {
  200. current.timestamp = current.created_at;
  201. }
  202. var currentRecordTime = new Date(tz(current.timestamp));
  203. //console.error(current);
  204. //console.error(currentRecordTime,lastRecordTime);
  205. // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes)
  206. if (currentRecordTime > lastRecordTime) {
  207. //console.error("",currentRecordTime," > ",lastRecordTime);
  208. //process.stderr.write(".");
  209. continue;
  210. } else {
  211. lastRecordTime = currentRecordTime;
  212. }
  213. if (current._type === "Bolus") {
  214. var temp = {};
  215. temp.timestamp = current.timestamp;
  216. temp.started_at = new Date(tz(current.timestamp));
  217. if (temp.started_at > now) {
  218. //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at);
  219. process.stderr.write(" "+current.amount+"U @ "+temp.started_at);
  220. } else {
  221. temp.date = temp.started_at.getTime();
  222. temp.insulin = current.amount;
  223. tempBoluses.push(temp);
  224. }
  225. } else if (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard") {
  226. //imports treatments entered through Nightscout Care Portal
  227. //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard
  228. var temp = {};
  229. temp.timestamp = current.created_at;
  230. temp.started_at = new Date(tz(temp.timestamp));
  231. temp.date = temp.started_at.getTime();
  232. temp.insulin = current.insulin;
  233. tempBoluses.push(temp);
  234. } else if (current.enteredBy === "xdrip") {
  235. var temp = {};
  236. temp.timestamp = current.timestamp;
  237. temp.started_at = new Date(tz(temp.timestamp));
  238. temp.date = temp.started_at.getTime();
  239. temp.insulin = current.insulin;
  240. tempBoluses.push(temp);
  241. } else if (current.enteredBy ==="HAPP_App" && current.insulin) {
  242. var temp = {};
  243. temp.timestamp = current.created_at;
  244. temp.started_at = new Date(tz(temp.timestamp));
  245. temp.date = temp.started_at.getTime();
  246. temp.insulin = current.insulin;
  247. tempBoluses.push(temp);
  248. } else if (current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) {
  249. var temp = {};
  250. temp.rate = current.absolute;
  251. temp.duration = current.duration;
  252. temp.timestamp = current.created_at;
  253. temp.started_at = new Date(tz(temp.timestamp));
  254. temp.date = temp.started_at.getTime();
  255. tempHistory.push(temp);
  256. } else if (current.eventType === "Temp Basal") {
  257. var temp = {};
  258. temp.rate = current.rate;
  259. temp.duration = current.duration;
  260. // Loop reports the amount of insulin actually delivered while the temp basal was running
  261. // use that to calculate the effective temp basal rate
  262. if (typeof current.amount !== 'undefined') {
  263. temp.rate = current.amount / current.duration * 60;
  264. }
  265. temp.timestamp = current.timestamp;
  266. temp.started_at = new Date(tz(temp.timestamp));
  267. temp.date = temp.started_at.getTime();
  268. tempHistory.push(temp);
  269. } else if (current._type === "TempBasal") {
  270. if (current.temp === 'percent') {
  271. continue;
  272. }
  273. var rate = current.rate;
  274. var timestamp = current.timestamp;
  275. var duration;
  276. if (i>0 && pumpHistory[i-1].timestamp === timestamp && pumpHistory[i-1]._type === "TempBasalDuration") {
  277. duration = pumpHistory[i-1]['duration (min)'];
  278. } else {
  279. for (var iter=0; iter < pumpHistory.length; iter++) {
  280. if (pumpHistory[iter].timestamp === timestamp && pumpHistory[iter]._type === "TempBasalDuration") {
  281. duration = pumpHistory[iter]['duration (min)'];
  282. break;
  283. }
  284. }
  285. if (duration === undefined) {
  286. console.error("No duration found for "+rate+" U/hr basal "+timestamp, pumpHistory[i - 1], current, pumpHistory[i + 1]);
  287. }
  288. }
  289. var temp = {};
  290. temp.rate = rate;
  291. temp.timestamp = current.timestamp;
  292. temp.started_at = new Date(tz(temp.timestamp));
  293. temp.date = temp.started_at.getTime();
  294. temp.duration = duration;
  295. tempHistory.push(temp);
  296. }
  297. // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation
  298. var temp = {};
  299. temp.rate = 0;
  300. // start the zero temp 1m in the future to avoid clock skew
  301. temp.started_at = new Date(now.getTime() + (1 * 60 * 1000));
  302. temp.date = temp.started_at.getTime();
  303. if (zeroTempDuration) {
  304. temp.duration = zeroTempDuration;
  305. } else {
  306. temp.duration = 0;
  307. }
  308. tempHistory.push(temp);
  309. }
  310. // Check for overlapping events and adjust event lengths in case of overlap
  311. tempHistory = _.sortBy(tempHistory, 'date');
  312. for (i=0; i+1 < tempHistory.length; i++) {
  313. if (tempHistory[i].date + tempHistory[i].duration*60*1000 > tempHistory[i+1].date) {
  314. tempHistory[i].duration = (tempHistory[i+1].date - tempHistory[i].date)/60/1000;
  315. // Delete AndroidAPS "Cancel TBR records" in which duration is not populated
  316. if (tempHistory[i+1].duration === null) {
  317. tempHistory.splice(i+1, 1);
  318. }
  319. }
  320. }
  321. // Create an array of moments to slit the temps by
  322. // currently supports basal changes
  323. var splitterEvents = [];
  324. _.forEach(profile_data.basalprofile,function addSplitter(o) {
  325. var splitterEvent = {};
  326. splitterEvent.type = 'recurring';
  327. splitterEvent.minutes = o.minutes;
  328. splitterEvents.push(splitterEvent);
  329. });
  330. // iterate through the events and split at basal break points if needed
  331. var splitHistoryByBasal = [];
  332. _.forEach(tempHistory, function splitEvent(o) {
  333. splitHistoryByBasal = splitHistoryByBasal.concat(splitTimespan(o,splitterEvents));
  334. });
  335. tempHistory = _.sortBy(tempHistory, function(o) { return o.date; });
  336. var suspend_zeros_iob = false;
  337. if (typeof profile_data.suspend_zeros_iob !== 'undefined') {
  338. suspend_zeros_iob = profile_data.suspend_zeros_iob;
  339. }
  340. if (suspend_zeros_iob) {
  341. // iterate through the events and adjust their
  342. // times as required to account for pump suspends
  343. var splitHistory = [];
  344. _.forEach(splitHistoryByBasal, function splitSuspendEvent(o) {
  345. var splitEvents = splitAroundSuspends(o, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended);
  346. splitHistory = splitHistory.concat(splitEvents);
  347. });
  348. var zTempSuspendBasals = [];
  349. // Any existing temp basals during times the pump was suspended are now deleted
  350. // Add 0 temp basals to negate the profile basal rates during times pump is suspended
  351. _.forEach(pumpSuspends, function createTempBasal(o) {
  352. var zTempBasal = [{
  353. _type: 'SuspendBasal',
  354. rate: 0,
  355. duration: o.duration,
  356. date: o.date,
  357. started_at: o.started_at
  358. }];
  359. zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
  360. });
  361. // Add temp suspend basal for maximum DIA (8) up to the resume time
  362. // if there is no matching suspend in the history before the first
  363. // resume
  364. var max_dia_ago = now.getTime() - 8*60*60*1000;
  365. var firstResumeStarted = new Date(firstResumeTime);
  366. var firstResumeDate = firstResumeStarted.getTime()
  367. // impact on IOB only matters if the resume occurred
  368. // after DIA hours before now.
  369. // otherwise, first resume date can be ignored. Whatever
  370. // insulin is present prior to resume will be aged
  371. // out due to DIA.
  372. if (suspendedPrior && (max_dia_ago < firstResumeDate)) {
  373. var suspendStart = new Date(max_dia_ago);
  374. var suspendStartDate = suspendStart.getTime()
  375. var started_at = new Date(tz(suspendStart.toISOString()));
  376. var zTempBasal = [{
  377. // add _type to aid debugging. It isn't used
  378. // anywhere.
  379. _type: 'SuspendBasal',
  380. rate: 0,
  381. duration: (firstResumeDate - max_dia_ago)/60/1000,
  382. date: suspendStartDate,
  383. started_at: started_at
  384. }];
  385. zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
  386. }
  387. if (currentlySuspended) {
  388. var suspendStart = new Date(lastSuspendTime);
  389. var suspendStartDate = suspendStart.getTime()
  390. var started_at = new Date(tz(suspendStart.toISOString()));
  391. var zTempBasal = [{
  392. _type: 'SuspendBasal',
  393. rate: 0,
  394. duration: (now - suspendStartDate)/60/1000,
  395. date: suspendStartDate,
  396. timestamp: lastSuspendTime,
  397. started_at: started_at
  398. }];
  399. zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal);
  400. }
  401. // Add the new 0 temp basals to the splitHistory.
  402. // We have to split the new zero temp basals by the profile
  403. // basals just like the other temp basals.
  404. _.forEach(zTempSuspendBasals, function splitEvent(o) {
  405. splitHistory = splitHistory.concat(splitTimespan(o,splitterEvents));
  406. });
  407. } else {
  408. splitHistory = splitHistoryByBasal;
  409. }
  410. splitHistory = _.sortBy(splitHistory, function(o) { return o.date; });
  411. // tempHistory = splitHistory;
  412. // iterate through the temp basals and create bolus events from temps that affect IOB
  413. var tempBolusSize;
  414. for (i=0; i < splitHistory.length; i++) {
  415. var currentItem = splitHistory[i];
  416. if (currentItem.duration > 0) {
  417. var currentRate = profile_data.current_basal;
  418. if (!_.isEmpty(profile_data.basalprofile)) {
  419. currentRate = basalprofile.basalLookup(profile_data.basalprofile,new Date(currentItem.timestamp));
  420. }
  421. if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') {
  422. target_bg = (profile_data.min_bg + profile_data.max_bg) / 2;
  423. }
  424. //if (profile_data.temptargetSet && target_bg > 110) {
  425. //sensitivityRatio = 2/(2+(target_bg-100)/40);
  426. //currentRate = profile_data.current_basal * sensitivityRatio;
  427. //}
  428. var sensitivityRatio;
  429. var profile = profile_data;
  430. var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled basal (which might change)
  431. if ( profile.half_basal_exercise_target ) {
  432. var halfBasalTarget = profile.half_basal_exercise_target;
  433. } else {
  434. var halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%)
  435. }
  436. if ( profile.exercise_mode && profile.temptargetSet && target_bg >= normalTarget + 5 ) {
  437. // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44
  438. // e.g.: Sensitivity ratio set to 0.8 based on temp target of 120; Adjusting basal from 1.65 to 1.35; ISF from 58.9 to 73.6
  439. var c = halfBasalTarget - normalTarget;
  440. sensitivityRatio = c/(c+target_bg-normalTarget);
  441. } else if (typeof autosens_data !== 'undefined' ) {
  442. sensitivityRatio = autosens_data.ratio;
  443. //process.stderr.write("Autosens ratio: "+sensitivityRatio+"; ");
  444. }
  445. if ( sensitivityRatio ) {
  446. currentRate = currentRate * sensitivityRatio;
  447. }
  448. var netBasalRate = currentItem.rate - currentRate;
  449. if (netBasalRate < 0) { tempBolusSize = -0.05; }
  450. else { tempBolusSize = 0.05; }
  451. var netBasalAmount = Math.round(netBasalRate*currentItem.duration*10/6)/100
  452. var tempBolusCount = Math.round(netBasalAmount / tempBolusSize);
  453. var tempBolusSpacing = currentItem.duration / tempBolusCount;
  454. for (j=0; j < tempBolusCount; j++) {
  455. var tempBolus = {};
  456. tempBolus.insulin = tempBolusSize;
  457. tempBolus.date = currentItem.date + j * tempBolusSpacing*60*1000;
  458. tempBolus.created_at = new Date(tempBolus.date);
  459. tempBoluses.push(tempBolus);
  460. }
  461. }
  462. }
  463. var all_data = [ ].concat(tempBoluses).concat(tempHistory);
  464. all_data = _.sortBy(all_data, 'date');
  465. return all_data;
  466. }
  467. exports = module.exports = calcTempTreatments;