From cc10e24e271f26c52503ad5f9f2a2d4d065e3eac Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 3 May 2024 09:24:12 +0000 Subject: [PATCH 01/15] add vscode devcontainer --- .devcontainer/devcontainer.json | 44 ++++++++++++++ .devcontainer/docker-compose.yml | 24 ++++++++ .gitignore | 2 +- docker-compose.override.yml.dist | 2 + docker-compose.yml | 7 +++ docker/node/.zshrc | 101 +++++++++++++++++++++++++++++++ docker/node/Dockerfile | 7 +++ 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 docker-compose.override.yml.dist create mode 100644 docker-compose.yml create mode 100644 docker/node/.zshrc create mode 100644 docker/node/Dockerfile diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..be4a28874 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose +{ + "name": "oref0", + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml", + "../docker-compose.override.yml", + "docker-compose.yml" + ], + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "node", + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + // "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "workspaceFolder": "/app", + "features": {}, + "customizations": { + "vscode": { + "extensions": [ + "Orta.vscode-jest", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "rvest.vs-code-prettier-eslint" + ] + } + } + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + // Uncomment the next line to run commands after the container is created. + // "postCreateCommand": "cat /etc/os-release", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "devcontainer" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..4839895e9 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,24 @@ +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + node: + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + # build: + # context: . + # dockerfile: .devcontainer/Dockerfile + + # volumes: + # Update this to wherever you want VS Code to mount the folder of your project + # - ..:/workspaces:cached + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + command: /bin/sh -c "while sleep 1000; do :; done" diff --git a/.gitignore b/.gitignore index a24874fdb..a11727c23 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ package-lock.json *.pyc bash-unit-test-temp - +docker-compose.override.yml diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist new file mode 100644 index 000000000..3eb667497 --- /dev/null +++ b/docker-compose.override.yml.dist @@ -0,0 +1,2 @@ +services: + node: {} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..ae0904588 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + node: + build: + context: ./docker/node + working_dir: '/app' + volumes: + - ./:/app diff --git a/docker/node/.zshrc b/docker/node/.zshrc new file mode 100644 index 000000000..cd5391959 --- /dev/null +++ b/docker/node/.zshrc @@ -0,0 +1,101 @@ +# If you come from bash you might have to change your $PATH. +# export PATH=$HOME/bin:/usr/local/bin:$PATH + +# Path to your oh-my-zsh installation. +export ZSH="$HOME/.oh-my-zsh" + +# Set name of the theme to load --- if set to "random", it will +# load a random theme each time oh-my-zsh is loaded, in which case, +# to know which specific one was loaded, run: echo $RANDOM_THEME +# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes +ZSH_THEME="robbyrussell" + +# Set list of themes to pick from when loading at random +# Setting this variable when ZSH_THEME=random will cause zsh to load +# a theme from this variable instead of looking in $ZSH/themes/ +# If set to an empty array, this variable will have no effect. +# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) + +# Uncomment the following line to use case-sensitive completion. +# CASE_SENSITIVE="true" + +# Uncomment the following line to use hyphen-insensitive completion. +# Case-sensitive completion must be off. _ and - will be interchangeable. +# HYPHEN_INSENSITIVE="true" + +# Uncomment one of the following lines to change the auto-update behavior +# zstyle ':omz:update' mode disabled # disable automatic updates +# zstyle ':omz:update' mode auto # update automatically without asking +# zstyle ':omz:update' mode reminder # just remind me to update when it's time + +# Uncomment the following line to change how often to auto-update (in days). +# zstyle ':omz:update' frequency 13 + +# Uncomment the following line if pasting URLs and other text is messed up. +# DISABLE_MAGIC_FUNCTIONS="true" + +# Uncomment the following line to disable colors in ls. +# DISABLE_LS_COLORS="true" + +# Uncomment the following line to disable auto-setting terminal title. +# DISABLE_AUTO_TITLE="true" + +# Uncomment the following line to enable command auto-correction. +# ENABLE_CORRECTION="true" + +# Uncomment the following line to display red dots whilst waiting for completion. +# You can also set it to another string to have that shown instead of the default red dots. +# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" +# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) +# COMPLETION_WAITING_DOTS="true" + +# Uncomment the following line if you want to disable marking untracked files +# under VCS as dirty. This makes repository status check for large repositories +# much, much faster. +# DISABLE_UNTRACKED_FILES_DIRTY="true" + +# Uncomment the following line if you want to change the command execution time +# stamp shown in the history command output. +# You can set one of the optional three formats: +# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" +# or set a custom format using the strftime function format specifications, +# see 'man strftime' for details. +# HIST_STAMPS="mm/dd/yyyy" + +# Would you like to use another custom folder than $ZSH/custom? +# ZSH_CUSTOM=/path/to/new-custom-folder + +# Which plugins would you like to load? +# Standard plugins can be found in $ZSH/plugins/ +# Custom plugins may be added to $ZSH_CUSTOM/plugins/ +# Example format: plugins=(rails git textmate ruby lighthouse) +# Add wisely, as too many plugins slow down shell startup. +plugins=(git) + +source $ZSH/oh-my-zsh.sh + +# User configuration + +# export MANPATH="/usr/local/man:$MANPATH" + +# You may need to manually set your language environment +# export LANG=en_US.UTF-8 + +# Preferred editor for local and remote sessions +# if [[ -n $SSH_CONNECTION ]]; then +# export EDITOR='vim' +# else +# export EDITOR='mvim' +# fi + +# Compilation flags +# export ARCHFLAGS="-arch x86_64" + +# Set personal aliases, overriding those provided by oh-my-zsh libs, +# plugins, and themes. Aliases can be placed here, though oh-my-zsh +# users are encouraged to define aliases within the ZSH_CUSTOM folder. +# For a full list of active aliases, run `alias`. +# +# Example aliases +# alias zshconfig="mate ~/.zshrc" +# alias ohmyzsh="mate ~/.oh-my-zsh" diff --git a/docker/node/Dockerfile b/docker/node/Dockerfile new file mode 100644 index 000000000..837890e52 --- /dev/null +++ b/docker/node/Dockerfile @@ -0,0 +1,7 @@ +ARG NODE_VERSION="20-slim" +FROM node:${NODE_VERSION} +RUN apt update -y && apt install -y openssh-client curl zsh git vim make jq python3 +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \ + && chsh -s $(which zsh) +COPY .zshrc /root/.zshrc +RUN corepack enable From 74fe892ba80ca898b58807657731489e69de36c8 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 3 May 2024 13:30:10 +0000 Subject: [PATCH 02/15] prepare for typescript --- package.json | 9 ++++++++- tsconfig.json | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tsconfig.json diff --git a/package.json b/package.json index bffeac98a..8f1e8ac1b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "openaps oref0 reference implementation of the reference design", "scripts": { "test": "make test", - "global-install": "npm install && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g" + "global-install": "npm install && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", + "tsc": "tsc" }, "repository": { "type": "git", @@ -107,9 +108,15 @@ "network": "^0.4.1", "request": "^2.88.0", "share2nightscout-bridge": "^0.2.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5", "yargs": "^13.2.2" }, "devDependencies": { + "@types/lodash": "^4.17.0", + "@types/mocha": "^10.0.6", + "@types/node": "^20.12.8", "coveralls": "^3.0.3", "istanbul": "^0.4.5", "mocha": "^5.2.0", diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..e7f51d49e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "outDir": "./dist", + "module": "CommonJS", + "target": "ES2022", + "allowJs": true, + "composite": false, + "declaration": false, + "declarationMap": false, + "sourceMap": false, + "inlineSourceMap": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "Node", + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "stripInternal": true + }, + "include": [ + "lib", + "tests" + ], + "exclude": [ + "node_modules" + ], + "ts-node": { + "swc": false, + "transpileOnly": true + } +} From 5a0a71679e3e01d0c77cfb20c9617b0c4c073630 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Thu, 1 Aug 2024 16:15:49 +0000 Subject: [PATCH 03/15] fix dependencies --- package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8f1e8ac1b..aea2df863 100644 --- a/package.json +++ b/package.json @@ -108,20 +108,21 @@ "network": "^0.4.1", "request": "^2.88.0", "share2nightscout-bridge": "^0.2.1", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.4.5", "yargs": "^13.2.2" }, "devDependencies": { "@types/lodash": "^4.17.0", "@types/mocha": "^10.0.6", + "@types/moment-timezone": "^0.5.30", "@types/node": "^20.12.8", "coveralls": "^3.0.3", "istanbul": "^0.4.5", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", - "should": "^13.2.3" + "should": "^13.2.3", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" }, "config": { "blanket": { From 5fd46fd28296cf433483576efd5a9ee1bed8c49c Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Thu, 8 Aug 2024 04:44:05 +0200 Subject: [PATCH 04/15] commit iob --- lib/determine-basal/{cob.js => cob.ts} | 136 ++-- ...{determine-basal.js => determine-basal.ts} | 267 +++++--- lib/iob/InsulinTreatment.ts | 19 + lib/iob/{calculate.js => calculate.ts} | 23 +- lib/iob/history.js | 572 ---------------- lib/iob/history.ts | 622 ++++++++++++++++++ lib/iob/{index.js => index.ts} | 80 ++- lib/iob/{total.js => total.ts} | 42 +- 8 files changed, 983 insertions(+), 778 deletions(-) rename lib/determine-basal/{cob.js => cob.ts} (65%) rename lib/determine-basal/{determine-basal.js => determine-basal.ts} (90%) create mode 100644 lib/iob/InsulinTreatment.ts rename lib/iob/{calculate.js => calculate.ts} (88%) delete mode 100644 lib/iob/history.js create mode 100644 lib/iob/history.ts rename lib/iob/{index.js => index.ts} (57%) rename lib/iob/{total.js => total.ts} (77%) diff --git a/lib/determine-basal/cob.js b/lib/determine-basal/cob.ts similarity index 65% rename from lib/determine-basal/cob.js rename to lib/determine-basal/cob.ts index 903409ca6..2bd18b07c 100644 --- a/lib/determine-basal/cob.js +++ b/lib/determine-basal/cob.ts @@ -1,37 +1,58 @@ -'use strict'; +import * as basal from '../profile/basal' +import get_iob from '../iob' +import find_insulin, { Input as IOBInput } from '../iob/history' +import isf from '../profile/isf' +import { GlucoseEntry } from '../types/GlucoseEntry'; +import { BasalSchedule } from '../types/Profile'; + +export interface DetectCOBInput { + glucose_data: GlucoseEntry[] + iob_inputs: IOBInput + basalprofile?: BasalSchedule[] + mealTime: number + ciTime?: number +} + +function getDateFromEntry(entry: GlucoseEntry) { + if (entry.date) { + return entry.date + } else if (entry.display_time) { + return Date.parse(entry.display_time) + } else if (entry.dateString) { + return Date.parse(entry.dateString) + } -var basal = require('../profile/basal'); -var get_iob = require('../iob'); -var find_insulin = require('../iob/history'); -var isf = require('../profile/isf'); + throw new TypeError('Unable to find a date in GlucoseEntry') +} -function detectCarbAbsorption(inputs) { +export default function detectCarbAbsorption(inputs: DetectCOBInput) { - var glucose_data = inputs.glucose_data.map(function prepGlucose (obj) { - //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv; - return obj; - }); + const glucose_data = inputs.glucose_data.reduce( + (b, a) => { + const glucose = a.glucose || a.sgv + return glucose ? [...b, { ...a, glucose, date: getDateFromEntry(a) }] : b + }, + [] as (GlucoseEntry & { glucose: number, date: number })[] + ) var iob_inputs = inputs.iob_inputs; var basalprofile = inputs.basalprofile; /* TODO why does declaring profile break tests-command-behavior.tests.sh? because it is a global variable used in other places.*/ var profile = inputs.iob_inputs.profile; var mealTime = new Date(inputs.mealTime); - var ciTime = new Date(inputs.ciTime); + var ciTime = inputs.ciTime ? new Date(inputs.ciTime) : undefined; //console.error(mealTime, ciTime); // get treatments from pumphistory once, not every time we get_iob() var treatments = find_insulin(inputs.iob_inputs); - var avgDeltas = []; - var bgis = []; - var deviations = []; - var deviationSum = 0; + if (! glucose_data.length) { + // @todo: return something empty + } + var carbsAbsorbed = 0; - var bucketed_data = []; - bucketed_data[0] = glucose_data[0]; + var bucketed_data = glucose_data.slice(0, 1) var j=0; var foundPreMealBG = false; var lastbgi = 0; @@ -41,42 +62,33 @@ function detectCarbAbsorption(inputs) { } for (var i=1; i < glucose_data.length; ++i) { - var bgTime; + const currentGlucose = glucose_data[i] + var bgTime = new Date(currentGlucose.date) var lastbgTime; - if (glucose_data[i].display_time) { - bgTime = new Date(glucose_data[i].display_time.replace('T', ' ')); - } else if (glucose_data[i].dateString) { - bgTime = new Date(glucose_data[i].dateString); - } else { console.error("Could not determine BG time"); } - if (! glucose_data[i].glucose || glucose_data[i].glucose < 39) { -//console.error("skipping:",glucose_data[i].glucose); + if (currentGlucose.glucose < 39) { + //console.error("skipping:",glucose_data[i].glucose); continue; } // only consider BGs for 6h after a meal for calculating COB - var hoursAfterMeal = (bgTime-mealTime)/(60*60*1000); + var hoursAfterMeal = (bgTime.getTime() - mealTime.getTime())/(60*60*1000); if (hoursAfterMeal > 6 || foundPreMealBG) { continue; } else if (hoursAfterMeal < 0) { -//console.error("Found pre-meal BG:",glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100); + //console.error("Found pre-meal BG:",glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100); foundPreMealBG = true; } -//console.error(glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100, bucketed_data[bucketed_data.length-1].display_time); + //console.error(glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100, bucketed_data[bucketed_data.length-1].display_time); // only consider last ~45m of data in CI mode // this allows us to calculate deviations for the last ~30m if (typeof ciTime !== 'undefined') { - var hoursAgo = (ciTime-bgTime)/(45*60*1000); + var hoursAgo = (ciTime.getTime() - bgTime.getTime())/(45*60*1000); if (hoursAgo > 1 || hoursAgo < 0) { continue; } } - if (bucketed_data[bucketed_data.length-1].display_time) { - lastbgTime = new Date(bucketed_data[bucketed_data.length-1].display_time.replace('T', ' ')); - } else if ((lastbgi >= 0) && glucose_data[lastbgi].display_time) { - lastbgTime = new Date(glucose_data[lastbgi].display_time.replace('T', ' ')); - } else if ((lastbgi >= 0) && glucose_data[lastbgi].dateString) { - lastbgTime = new Date(glucose_data[lastbgi].dateString); - } else { console.error("Could not determine last BG time"); } - var elapsed_minutes = (bgTime - lastbgTime)/(60*1000); + const lastBucketedData = bucketed_data[bucketed_data.length-1] + lastbgTime = new Date(lastBucketedData.date); + var elapsed_minutes = (bgTime.getTime() - lastbgTime.getTime())/(60*1000); //console.error(bgTime, lastbgTime, elapsed_minutes); if(Math.abs(elapsed_minutes) > 8) { // interpolate missing data points @@ -84,16 +96,16 @@ function detectCarbAbsorption(inputs) { // cap interpolation at a maximum of 4h elapsed_minutes = Math.min(240,Math.abs(elapsed_minutes)); //console.error(elapsed_minutes); - while(elapsed_minutes > 5) { - var previousbgTime = new Date(lastbgTime.getTime() - 5 * 60*1000); + while (elapsed_minutes > 5) { + var previousbgTime: Date = new Date(lastbgTime.getTime() - 5 * 60*1000); j++; - bucketed_data[j] = []; - bucketed_data[j].date = previousbgTime.getTime(); + var gapDelta = glucose_data[i].glucose - lastbg; - //console.error(gapDelta, lastbg, elapsed_minutes); var previousbg = lastbg + (5/elapsed_minutes * gapDelta); - bucketed_data[j].glucose = Math.round(previousbg); - //console.error("Interpolated", bucketed_data[j]); + bucketed_data[j] = { + date: previousbgTime.getTime(), + glucose: Math.round(previousbg), + }; elapsed_minutes = elapsed_minutes - 5; lastbg = previousbg; @@ -111,14 +123,18 @@ function detectCarbAbsorption(inputs) { lastbgi = i; //console.error(bucketed_data[j].date) } - var currentDeviation; + var currentDeviation = 0; var slopeFromMaxDeviation = 0; var slopeFromMinDeviation = 999; var maxDeviation = 0; var minDeviation = 999; var allDeviations = []; //console.error(bucketed_data); - var lastIsfResult = null; + var lastIsfResult = undefined; + if (!profile.isfProfile) { + console.error("No isfProfile found in Profile"); + throw new TypeError("No isfProfile found in Profile") + } for (i=0; i < bucketed_data.length-3; ++i) { bgTime = new Date(bucketed_data[i].date); @@ -127,7 +143,7 @@ function detectCarbAbsorption(inputs) { //console.error(bgTime , bucketed_data[i].glucose, bucketed_data[i].date); var bg; - var avgDelta; + let avgDelta; var delta; if (typeof(bucketed_data[i].glucose) !== 'undefined') { bg = bucketed_data[i].glucose; @@ -135,38 +151,38 @@ function detectCarbAbsorption(inputs) { process.stderr.write("!"); continue; } - avgDelta = (bg - bucketed_data[i+3].glucose)/3; + avgDelta = Math.round(((bg - bucketed_data[i+3].glucose)/3) * 100) / 100; delta = (bg - bucketed_data[i+1].glucose); - } else { console.error("Could not find glucose data"); } + } else { + console.error("Could not find glucose data"); + continue; + } - avgDelta = avgDelta.toFixed(2); - iob_inputs.clock=bgTime; - iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime); + iob_inputs.clock=bgTime.toISOString(); + iob_inputs.profile.current_basal = basal.basalLookup(basalprofile || [], bgTime); //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); var iob = get_iob(iob_inputs, true, treatments)[0]; //console.error("After: ", new Date().getTime()); //console.error(JSON.stringify(iob)); - var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100; - bgi = bgi.toFixed(2); + var bgi = Math.round(( -iob.activity * sens * 5 ) * 100) / 100; //console.error(delta); - var deviation = delta-bgi; - deviation = deviation.toFixed(2); + var deviation = Math.round((delta-bgi) * 100) / 100; //if (deviation < 0 && deviation > -2) { console.error("BG: "+bg+", avgDelta: "+avgDelta+", BGI: "+bgi+", deviation: "+deviation); } // calculate the deviation right now, for use in min_5m if (i===0) { currentDeviation = Math.round((avgDelta-bgi)*1000)/1000; - if (ciTime > bgTime) { + if (ciTime && ciTime > bgTime) { //console.error("currentDeviation:",currentDeviation,avgDelta,bgi); allDeviations.push(Math.round(currentDeviation)); } if (currentDeviation/2 > profile.min_5m_carbimpact) { //console.error("currentDeviation",currentDeviation,"/2 > min_5m_carbimpact",profile.min_5m_carbimpact); } - } else if (ciTime > bgTime) { + } else if (ciTime && ciTime > bgTime) { var avgDeviation = Math.round((avgDelta-bgi)*1000)/1000; - var deviationSlope = (avgDeviation-currentDeviation)/(bgTime-ciTime)*1000*60*5; + var deviationSlope = (avgDeviation - currentDeviation)/(bgTime.getTime()-ciTime.getTime())*1000*60*5; //console.error(avgDeviation,currentDeviation,bgTime,ciTime) if (avgDeviation > maxDeviation) { slopeFromMaxDeviation = Math.min(0, deviationSlope); diff --git a/lib/determine-basal/determine-basal.js b/lib/determine-basal/determine-basal.ts similarity index 90% rename from lib/determine-basal/determine-basal.js rename to lib/determine-basal/determine-basal.ts index 1a1a286cc..170e790ef 100644 --- a/lib/determine-basal/determine-basal.js +++ b/lib/determine-basal/determine-basal.ts @@ -13,22 +13,25 @@ THE SOFTWARE. */ +import { Profile } from "../types/Profile"; + // Define various functions used later on, in the main function determine_basal() below -var round_basal = require('../round-basal') +import round_basal from '../round-basal' +import { Autosens } from "../types/Autosens"; // Rounds value to 'digits' decimal places -function round(value, digits) +function round(value: number, digits?: number) { - if (! digits) { digits = 0; } - var scale = Math.pow(10, digits); + var scale = Math.pow(10, digits || 0); + return Math.round(value * scale) / scale; } // we expect BG to rise or fall at the rate of BGI, // adjusted by the rate at which BG would need to rise / // fall to get eventualBG to target over 2 hours -function calculate_expected_delta(target_bg, eventual_bg, bgi) { +function calculate_expected_delta(target_bg: number, eventual_bg: number, bgi: number ) { // (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24 var five_min_blocks = (2 * 60) / 5; var target_delta = target_bg - eventual_bg; @@ -36,7 +39,7 @@ function calculate_expected_delta(target_bg, eventual_bg, bgi) { } -function convert_bg(value, profile) +function convert_bg(value: number, profile: Profile) { if (profile.out_units === "mmol/L") { @@ -48,13 +51,24 @@ function convert_bg(value, profile) } } +interface MealData { + bwFound?: boolean + mealCOB: number + carbs: number + bwCarbs?: number + lastCarbTime: number + slopeFromMaxDeviation: number + slopeFromMinDeviation: number + reason?: string +} + function enable_smb( - profile, - microBolusAllowed, - meal_data, - bg, - target_bg, - high_bg + profile: Profile, + microBolusAllowed: boolean, + meal_data: MealData, + bg: number, + target_bg: number, + high_bg: number | undefined ) { // disable SMB when a high temptarget is set if (! microBolusAllowed) { @@ -110,7 +124,7 @@ function enable_smb( } // enable SMB if high bg is found - if (profile.enableSMB_high_bg === true && high_bg !== null && bg >= high_bg) { + if (profile.enableSMB_high_bg === true && high_bg && bg >= high_bg) { console.error("Checking BG to see if High for SMB enablement."); console.error("Current BG", bg, " | High BG ", high_bg); if (meal_data.bwFound) { @@ -125,16 +139,86 @@ function enable_smb( return false; } -var determine_basal = function determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, microBolusAllowed, reservoir_data, currentTime) { +interface GlucoseStatus { + glucose: number + delta: number + noise: number + date: string | number + short_avgdelta: number + long_avgdelta: number + device?: string + last_cal?: number +} + +interface IOBTick { + activity: number + iob: number + lastTemp?: { + date: number + duration: number + rate: number + }, + iobWithZeroTemp: { + activity: number + } + lastBolusTime: number +} + +interface CurrentTemp { + timestamp: string, + temp: "absolute" | string, + rate: number, + duration: number +} + +var determine_basal = function determine_basal( + glucose_status: GlucoseStatus, + currenttemp: CurrentTemp, + iobArray: IOBTick | IOBTick[], + profile: Profile, + autosens_data: Autosens | undefined, + meal_data: MealData, + tempBasalFunctions: any, + microBolusAllowed: boolean, + reservoir_data: number, + currentTime?: Date +) { // Set variables required for evaluating error conditions - var rT = {}; //short for requestedTemp + var rT: { + error?: string + reason?: string + deliverAt?: Date + rate?: number + duration?: number + temp?: string + predBGs?: { + IOB?: number[] + ZT?: number[] + COB?: number[] + UAM?: number[] + } + eventualBG?: number + COB?: number + IOB?: number + BGI?: number + deviation?: number + ISF?: number + CR?: number + target_bg?: number + carbsReq?: number + insulinReq?: number + units?: number, + [k: string]: unknown + } = {}; //short for requestedTemp var deliverAt = new Date(); if (currentTime) { deliverAt = currentTime; } + console.log({ deliverAt, currentTime }) + if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { rT.error ='Error: could not get current basal rate'; return rT; @@ -147,19 +231,14 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ systemTime = currentTime; } var bgTime = new Date(glucose_status.date); - var minAgo = round( (systemTime - bgTime) / 60 / 1000 ,1); + var minAgo = round( (systemTime.getTime() - bgTime.getTime()) / 60 / 1000, 1); var bg = glucose_status.glucose; var noise = glucose_status.noise; // Prep various delta variables. - var tick; + var tick = glucose_status.delta > -0.5 ? "+" + round(glucose_status.delta,0) : round(glucose_status.delta,0) - if (glucose_status.delta > -0.5) { - tick = "+" + round(glucose_status.delta,0); - } else { - tick = round(glucose_status.delta,0); - } //var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta); var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta); @@ -226,37 +305,31 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver // if min and max are set, then set target to their average - var target_bg; - var min_bg; - var max_bg; - var high_bg; - if (typeof profile.min_bg !== 'undefined') { - min_bg = profile.min_bg; - } - if (typeof profile.max_bg !== 'undefined') { - max_bg = profile.max_bg; - } - if (typeof profile.enableSMB_high_bg_target !== 'undefined') { - high_bg = profile.enableSMB_high_bg_target; - } - if (typeof profile.min_bg !== 'undefined' && typeof profile.max_bg !== 'undefined') { - target_bg = (profile.min_bg + profile.max_bg) / 2; - } else { + var target_bg: number; + var min_bg = profile.min_bg as number; + var max_bg = profile.max_bg as number; + var high_bg = profile.enableSMB_high_bg_target + + if (min_bg === undefined || max_bg === undefined) { rT.error ='Error: could not determine target_bg. '; return rT; } + target_bg = (min_bg + max_bg) / 2; + // Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens - var sensitivityRatio; + var sensitivityRatio: number | undefined; var high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity; var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled target (which might change) + var halfBasalTarget; if ( profile.half_basal_exercise_target ) { - var halfBasalTarget = profile.half_basal_exercise_target; + halfBasalTarget = profile.half_basal_exercise_target; } else { halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limited to autosens_max (1.2x by default) } - if ( high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget + + if (profile.autosens_max !== undefined && high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget || profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ) { // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 // 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 @@ -266,19 +339,19 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // with low TT and lowTTlowersSensitivity we need autosens_max as a value // we use multiplication instead of the division to avoid "division by zero error" if (c * (c + target_bg-normalTarget) <= 0.0) { - sensitivityRatio = profile.autosens_max; - } - else { + sensitivityRatio = profile.autosens_max!; + } else { sensitivityRatio = c/(c+target_bg-normalTarget); } // limit sensitivityRatio to profile.autosens_max (1.2x by default) - sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max); + sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max!); sensitivityRatio = round(sensitivityRatio,2); process.stderr.write("Sensitivity ratio set to "+sensitivityRatio+" based on temp target of "+target_bg+"; "); } else if (typeof autosens_data !== 'undefined' && autosens_data) { sensitivityRatio = autosens_data.ratio; process.stderr.write("Autosens ratio: "+sensitivityRatio+"; "); } + if (sensitivityRatio) { basal = profile.current_basal * sensitivityRatio; basal = round_basal(basal, profile); @@ -310,12 +383,12 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } } -// Raise target for noisy / raw CGM data. + // Raise target for noisy / raw CGM data. if (glucose_status.noise >= 2) { // increase target at least 10% (default 30%) for raw / noisy data - var noisyCGMTargetMultiplier = Math.max( 1.1, profile.noisyCGMTargetMultiplier ); + var noisyCGMTargetMultiplier = Math.max( 1.1, profile.noisyCGMTargetMultiplier || 0); // don't allow maxRaw above 250 - var maxRaw = Math.min( 250, profile.maxRaw ); + //var maxRaw = Math.min( 250, profile.maxRaw ); var adjustedMinBG = round(Math.min(200, min_bg * noisyCGMTargetMultiplier )); var adjustedTargetBG = round(Math.min(200, target_bg * noisyCGMTargetMultiplier )); var adjustedMaxBG = round(Math.min(200, max_bg * noisyCGMTargetMultiplier )); @@ -328,14 +401,14 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // min_bg of 90 -> threshold of 65, 100 -> 70 110 -> 75, and 130 -> 85 var threshold = min_bg - 0.5*(min_bg-40); -// If iob_data or its required properties are missing, return. -// This has to be checked after checking that we're not in one of the CGM-data-related error conditions handled above, -// and before attempting to use iob_data below. + // If iob_data or its required properties are missing, return. + // This has to be checked after checking that we're not in one of the CGM-data-related error conditions handled above, + // and before attempting to use iob_data below. -// Adjust ISF based on sensitivityRatio - var profile_sens = round(profile.sens,1) + // Adjust ISF based on sensitivityRatio + var profile_sens = round(profile.sens, 1) var sens = profile.sens; - if (typeof autosens_data !== 'undefined' && autosens_data) { + if (typeof autosens_data !== 'undefined' && autosens_data && sensitivityRatio) { sens = profile.sens / sensitivityRatio; sens = round(sens, 1); if (sens !== profile_sens) { @@ -347,17 +420,13 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } console.error("; CR:",profile.carb_ratio); - if (typeof iob_data === 'undefined' ) { + let iob_data = Array.isArray(iobArray) ? iobArray[0] : iobArray + + if (! iob_data) { rT.error ='Error: iob_data undefined. '; return rT; } - var iobArray = iob_data; - if (typeof(iob_data.length) && iob_data.length > 1) { - iob_data = iobArray[0]; - //console.error(JSON.stringify(iob_data[0])); - } - if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' ) { rT.error ='Error: iob_data missing some property. '; return rT; @@ -375,8 +444,9 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ //console.error("currenttemp:",currenttemp,"lastTemp:",JSON.stringify(iob_data.lastTemp),"lastTempAge:",lastTempAge,"m"); var tempModulus = (lastTempAge + currenttemp.duration) % 30; console.error("currenttemp:",currenttemp,"lastTempAge:",lastTempAge,"m","tempModulus:",tempModulus,"m"); + const rTDeliveredAt = deliverAt rT.temp = 'absolute'; - rT.deliverAt = deliverAt; + rT.deliverAt = rTDeliveredAt; if ( microBolusAllowed && currenttemp && iob_data.lastTemp && currenttemp.rate !== iob_data.lastTemp.rate && lastTempAge > 10 && currenttemp.duration ) { rT.reason = "Warning: currenttemp rate "+currenttemp.rate+" != lastTemp rate "+iob_data.lastTemp.rate+" from pumphistory; canceling temp"; return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); @@ -439,10 +509,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ // Generate predicted future BGs based on IOB, COB, and current absorption rate // Initialize and calculate variables used for predicting BGs - var COBpredBGs = []; - var IOBpredBGs = []; - var UAMpredBGs = []; - var ZTpredBGs = []; + var COBpredBGs: number[] = []; + var IOBpredBGs: number[] = []; + var UAMpredBGs: number[] = []; + var ZTpredBGs: number[] = []; COBpredBGs.push(bg); IOBpredBGs.push(bg); ZTpredBGs.push(bg); @@ -458,7 +528,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ ); // enable UAM (if enabled in preferences) - var enableUAM=(profile.enableUAM); + var enableUAM = profile.enableUAM; //console.error(meal_data); @@ -515,8 +585,12 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var totalCA = totalCI / csf; var remainingCarbsCap = 90; // default to 90 var remainingCarbsFraction = 1; - if (profile.remainingCarbsCap) { remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); } - if (profile.remainingCarbsFraction) { remainingCarbsFraction = Math.min(1,profile.remainingCarbsFraction); } + if (profile.remainingCarbsCap) { + remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); + } + if (profile.remainingCarbsFraction) { + remainingCarbsFraction = Math.min(1,profile.remainingCarbsFraction); + } var remainingCarbsIgnore = 1 - remainingCarbsFraction; var remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs*remainingCarbsIgnore); remainingCarbs = Math.min(remainingCarbsCap,remainingCarbs); @@ -560,18 +634,20 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var IOBpredBG = eventualBG; var maxIOBPredBG = bg; var maxCOBPredBG = bg; - var maxUAMPredBG = bg; - var eventualPredBG = bg; + //var maxUAMPredBG = bg; + //var eventualPredBG = bg; var lastIOBpredBG; var lastCOBpredBG; var lastUAMpredBG; - var lastZTpredBG; + //var lastZTpredBG; var UAMduration = 0; var remainingCItotal = 0; - var remainingCIs = []; - var predCIs = []; + var remainingCIs: number[] = []; + var predCIs: number[] = []; + var COBpredBG: number | undefined; + var UAMpredBG: number | undefined; try { - iobArray.forEach(function(iobTick) { + (Array.isArray(iobArray) ? iobArray : []).forEach(function(iobTick) { //console.error(iobTick); var predBGI = round(( -iobTick.activity * sens * 5 ), 2); var predZTBGI = round(( -iobTick.iobWithZeroTemp.activity * sens * 5 ), 2); @@ -635,11 +711,12 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ if ( (cid || remainingCIpeak > 0) && COBpredBGs.length > insulinPeak5m && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); } if ( (cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; } if ( enableUAM && UAMpredBGs.length > 12 && (UAMpredBG < minUAMPredBG) ) { minUAMPredBG = round(UAMpredBG); } - if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; } + //if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; } }); // set eventualBG to include effect of carbs //console.error("PredBGs:",JSON.stringify(predBGs)); } catch (e) { + console.error(e) console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled"); } if (meal_data.mealCOB) { @@ -665,7 +742,7 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ else { ZTpredBGs.pop(); } } rT.predBGs.ZT = ZTpredBGs; - lastZTpredBG=round(ZTpredBGs[ZTpredBGs.length-1]); + //lastZTpredBG=round(ZTpredBGs[ZTpredBGs.length-1]); if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { COBpredBGs.forEach(function(p, i, theArray) { theArray[i] = round(Math.min(401,Math.max(39,p))); @@ -708,14 +785,14 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ var fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs; // if we have COB and UAM is enabled, average both - if ( minUAMPredBG < 999 && minCOBPredBG < 999 ) { + if (UAMpredBG && COBpredBG && minUAMPredBG < 999 && minCOBPredBG < 999 ) { // weight COBpredBG vs. UAMpredBG based on how many carbs remain as COB avgPredBG = round( (1-fractionCarbsLeft)*UAMpredBG + fractionCarbsLeft*COBpredBG ); // if UAM is disabled, average IOB and COB - } else if ( minCOBPredBG < 999 ) { + } else if (COBpredBG && minCOBPredBG < 999 ) { avgPredBG = round( (IOBpredBG + COBpredBG)/2 ); // if we have UAM but no COB, average IOB and UAM - } else if ( minUAMPredBG < 999 ) { + } else if (UAMpredBG && minUAMPredBG < 999 ) { avgPredBG = round( (IOBpredBG + UAMpredBG)/2 ); } else { avgPredBG = round( IOBpredBG ); @@ -809,10 +886,10 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ rT.CR=round(profile.carb_ratio, 2); rT.target_bg=convert_bg(target_bg, profile); rT.reason="COB: " + rT.COB + ", Dev: " + rT.deviation + ", BGI: " + rT.BGI+ ", ISF: " + rT.ISF + ", CR: " + rT.CR + ", minPredBG: " + convert_bg(minPredBG, profile) + ", minGuardBG: " + convert_bg(minGuardBG, profile) + ", IOBpredBG: " + convert_bg(lastIOBpredBG, profile); - if (lastCOBpredBG > 0) { + if (lastCOBpredBG && lastCOBpredBG > 0) { rT.reason += ", COBpredBG: " + convert_bg(lastCOBpredBG, profile); } - if (lastUAMpredBG > 0) { + if (lastUAMpredBG && lastUAMpredBG > 0) { rT.reason += ", UAMpredBG: " + convert_bg(lastUAMpredBG, profile) } rT.reason += "; "; @@ -866,11 +943,8 @@ var determine_basal = function determine_basal(glucose_status, currenttemp, iob_ } // Disable SMB for sudden rises (often caused by calibrations or activation/deactivation of Dexcom's noise-filtering algorithm) // Added maxDelta_bg_threshold as a hidden preference and included a cap at 0.3 as a safety limit -var maxDelta_bg_threshold; - if (typeof profile.maxDelta_bg_threshold === 'undefined') { - maxDelta_bg_threshold = 0.2; - } - if (typeof profile.maxDelta_bg_threshold !== 'undefined') { + let maxDelta_bg_threshold = 0.2 + if (profile.maxDelta_bg_threshold !== undefined) { maxDelta_bg_threshold = Math.min(profile.maxDelta_bg_threshold, 0.3); } if ( maxDelta > maxDelta_bg_threshold * bg ) { @@ -879,7 +953,7 @@ var maxDelta_bg_threshold; enableSMB = false; } -// Calculate carbsReq (carbs required to avoid a hypo) + // Calculate carbsReq (carbs required to avoid a hypo) console.error("BG projected to remain above",convert_bg(min_bg, profile),"for",minutesAboveMinBG,"minutes"); if ( minutesAboveThreshold < 240 || minutesAboveMinBG < 60 ) { console.error("BG projected to remain above",convert_bg(threshold,profile),"for",minutesAboveThreshold,"minutes"); @@ -897,7 +971,7 @@ var maxDelta_bg_threshold; console.error("naive_eventualBG: " + convert_bg(naive_eventualBG,profile) + ", bgUndershoot: " + convert_bg(bgUndershoot,profile) + ", zeroTempDuration: " + zeroTempDuration + ", zeroTempEffect: " + zeroTempEffect +", carbsReq: " + carbsReq); if ( meal_data.reason == "Could not parse clock data" ) { console.error("carbsReq unknown: Could not parse clock data"); - } else if ( carbsReq >= profile.carbsReqThreshold && minutesAboveThreshold <= 45 ) { + } else if ( profile.carbsReqThreshold !== undefined && carbsReq >= profile.carbsReqThreshold && minutesAboveThreshold <= 45 ) { rT.carbsReq = carbsReq; rT.reason += carbsReq + " add'l carbs req w/in " + minutesAboveThreshold + "m; "; } @@ -922,8 +996,8 @@ var maxDelta_bg_threshold; // if not in LGS mode, cancel temps before the top of the hour to reduce beeping/vibration // console.error(profile.skip_neutral_temps, rT.deliverAt.getMinutes()); - if ( profile.skip_neutral_temps && rT.deliverAt.getMinutes() >= 55 ) { - rT.reason += "; Canceling temp at " + rT.deliverAt.getMinutes() + "m past the hour. "; + if ( profile.skip_neutral_temps && rTDeliveredAt.getMinutes() >= 55 ) { + rT.reason += "; Canceling temp at " + rTDeliveredAt.getMinutes() + "m past the hour. "; return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); } @@ -937,7 +1011,7 @@ var maxDelta_bg_threshold; return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); } if (glucose_status.delta > minDelta) { - rT.reason += ", but Delta " + convert_bg(tick, profile) + " > expectedDelta " + convert_bg(expectedDelta, profile); + rT.reason += ", but Delta " + convert_bg(Number(tick), profile) + " > expectedDelta " + convert_bg(expectedDelta, profile); } else { rT.reason += ", but Min. Delta " + minDelta.toFixed(2) + " > Exp. Delta " + convert_bg(expectedDelta, profile); } @@ -1009,7 +1083,7 @@ var maxDelta_bg_threshold; // if in SMB mode, don't cancel SMB zero temp if (! (microBolusAllowed && enableSMB)) { if (glucose_status.delta < minDelta) { - rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Delta " + convert_bg(tick, profile) + " < Exp. Delta " + convert_bg(expectedDelta, profile); + rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Delta " + convert_bg(Number(tick), profile) + " < Exp. Delta " + convert_bg(expectedDelta, profile); } else { rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " > " + convert_bg(min_bg, profile) + " but Min. Delta " + minDelta.toFixed(2) + " < Exp. Delta " + convert_bg(expectedDelta, profile); } @@ -1042,7 +1116,7 @@ var maxDelta_bg_threshold; if ( eventualBG >= max_bg ) { rT.reason += "Eventual BG " + convert_bg(eventualBG, profile) + " >= " + convert_bg(max_bg, profile) + ", "; } - if (iob_data.iob > max_iob) { + if (max_iob !== undefined && iob_data.iob > max_iob) { rT.reason += "IOB " + round(iob_data.iob,2) + " > max_iob " + max_iob; if (currenttemp.duration > 15 && (round_basal(basal, profile) === round_basal(currenttemp.rate, profile))) { rT.reason += ", temp " + currenttemp.rate + " ~ req " + basal + "U/hr. "; @@ -1057,7 +1131,7 @@ var maxDelta_bg_threshold; //console.error(minPredBG,eventualBG); insulinReq = round( (Math.min(minPredBG,eventualBG) - target_bg) / sens, 2); // if that would put us over max_iob, then reduce accordingly - if (insulinReq > max_iob-iob_data.iob) { + if (max_iob !== undefined && insulinReq > max_iob-iob_data.iob) { rT.reason += "max_iob " + max_iob + ", "; insulinReq = max_iob-iob_data.iob; } @@ -1094,7 +1168,7 @@ var maxDelta_bg_threshold; maxBolus = round( profile.current_basal * profile.maxSMBBasalMinutes / 60 ,1); } // bolus 1/2 the insulinReq, up to maxBolus, rounding down to nearest bolus increment - bolusIncrement = 0.1; + var bolusIncrement = 0.1; if (profile.bolus_increment) { bolusIncrement=profile.bolus_increment }; var roundSMBTo = 1 / bolusIncrement; var microBolus = Math.floor(Math.min(insulinReq/2,maxBolus)*roundSMBTo)/roundSMBTo; @@ -1189,4 +1263,5 @@ var maxDelta_bg_threshold; }; +export default determine_basal module.exports = determine_basal; diff --git a/lib/iob/InsulinTreatment.ts b/lib/iob/InsulinTreatment.ts new file mode 100644 index 000000000..4e119c9b9 --- /dev/null +++ b/lib/iob/InsulinTreatment.ts @@ -0,0 +1,19 @@ +export interface BasalTreatment { + timestamp: string; + started_at: Date; + date: number; + rate: number; + duration: number; +} + +export interface BolusTreatment { + timestamp: string; + started_at: Date; + date: number; + insulin: number; +} + +export type InsulinTreatment = BasalTreatment | BolusTreatment + +export const isBasalTreatment = (treatment: A): treatment is A & BasalTreatment => Object.prototype.hasOwnProperty.call(treatment, 'rate') +export const isBolusTreatment = (treatment: A): treatment is A & BolusTreatment => Object.prototype.hasOwnProperty.call(treatment, 'insulin') diff --git a/lib/iob/calculate.js b/lib/iob/calculate.ts similarity index 88% rename from lib/iob/calculate.js rename to lib/iob/calculate.ts index 904e953f4..b0228ba49 100644 --- a/lib/iob/calculate.js +++ b/lib/iob/calculate.ts @@ -1,6 +1,12 @@ -'use strict'; +import { Profile } from "../types/Profile"; +import { BolusTreatment, InsulinTreatment, isBolusTreatment } from "./InsulinTreatment"; -function iobCalc(treatment, time, curve, dia, peak, profile) { +interface IobCalcResult { + activityContrib?: number; + iobContrib?: number; +} + +export default function iobCalc(treatment: InsulinTreatment, time: Date | undefined, curve: 'bilinear' | string, dia: number, peak: number, profile: Profile): IobCalcResult { // iobCalc returns two variables: // activityContrib = units of treatment.insulin used in previous minute // iobContrib = units of treatment.insulin still remaining at a given point in time @@ -11,14 +17,11 @@ function iobCalc(treatment, time, curve, dia, peak, profile) { // An exponential insulin action curve (which takes both a dia and a peak parameter) // (which functional form to use is specified in the user's profile) - if (treatment.insulin) { + if (isBolusTreatment(treatment)) { - // Calc minutes since bolus (minsAgo) - if (typeof time === 'undefined') { - time = new Date(); - } + time = time || new Date() var bolusTime = new Date(treatment.date); - var minsAgo = Math.round((time - bolusTime) / 1000 / 60); + var minsAgo = Math.round((time.getTime() - bolusTime.getTime()) / 1000 / 60); if (curve === 'bilinear') { @@ -33,7 +36,7 @@ function iobCalc(treatment, time, curve, dia, peak, profile) { } -function iobCalcBilinear(treatment, minsAgo, dia) { +function iobCalcBilinear(treatment: BolusTreatment, minsAgo: number, dia: number) { var default_dia = 3.0 // assumed duration of insulin activity, in hours var peak = 75; // assumed peak insulin activity, in minutes @@ -80,7 +83,7 @@ function iobCalcBilinear(treatment, minsAgo, dia) { } -function iobCalcExponential(treatment, minsAgo, dia, peak, profile) { +function iobCalcExponential(treatment: BolusTreatment, minsAgo: number, dia: number, peak: number, profile: Profile) { // Use custom peak time (in minutes) if value is valid if ( profile.curve === "rapid-acting" ) { diff --git a/lib/iob/history.js b/lib/iob/history.js deleted file mode 100644 index 5c7ffe67a..000000000 --- a/lib/iob/history.js +++ /dev/null @@ -1,572 +0,0 @@ -'use strict'; - -var tz = require('moment-timezone'); -var basalprofile = require('../profile/basal.js'); -var _ = require('lodash'); -var moment = require('moment'); - -function splitTimespanWithOneSplitter(event,splitter) { - - var resultArray = [event]; - - if (splitter.type === 'recurring') { - - var startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes(); - var endMinutes = startMinutes + event.duration; - - // 1440 = one day; no clean way to check if the event overlaps midnight - // so checking if end of event in minutes is past midnight - - if (event.duration > 30 || (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || (endMinutes > 1440 && splitter.minutes < (endMinutes - 1440))) { - - var event1 = _.cloneDeep(event); - var event2 = _.cloneDeep(event); - - var event1Duration = 0; - - if (event.duration > 30) { - event1Duration = 30; - } else { - var splitPoint = splitter.minutes; - if (endMinutes > 1440) { splitPoint = 1440; } - event1Duration = splitPoint - startMinutes; - } - - var event1EndDate = moment(event.started_at).add(event1Duration,'minutes'); - - event1.duration = event1Duration; - - event2.duration = event.duration - event1Duration; - event2.timestamp = event1EndDate.format(); - event2.started_at = new Date(event2.timestamp); - event2.date = event2.started_at.getTime(); - - resultArray = [event1,event2]; - } - } - - return resultArray; -} - -function splitTimespan(event, splitterMoments) { - - var results = [event]; - - var splitFound = true; - - while(splitFound) { - - var resultArray = []; - splitFound = false; - - _.forEach(results,function split(o) { - _.forEach(splitterMoments,function split(p) { - var splitResult = splitTimespanWithOneSplitter(o,p); - if (splitResult.length > 1) { - resultArray = resultArray.concat(splitResult); - splitFound = true; - return false; - } - }); - - if (!splitFound) resultArray = resultArray.concat([o]); - - }); - - results = resultArray; - } - - return results; -} - -// Split currentEvent around any conflicting suspends -// by removing the time period from the event that -// overlaps with any suspend. -function splitAroundSuspends (currentEvent, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended) { - var events = []; - - var firstResumeStarted = new Date(firstResumeTime); - var firstResumeDate = firstResumeStarted.getTime() - - var lastSuspendStarted = new Date(lastSuspendTime); - var lastSuspendDate = lastSuspendStarted.getTime(); - - if (suspendedPrior && (currentEvent.date < firstResumeDate)) { - if ((currentEvent.date+currentEvent.duration*60*1000) < firstResumeDate) { - currentEvent.duration = 0; - } else { - currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000; - - currentEvent.started_at = new Date(tz(firstResumeTime)); - currentEvent.date = firstResumeDate - } - } - - if (currentlySuspended && ((currentEvent.date+currentEvent.duration*60*1000) > lastSuspendTime)) { - if (currentEvent.date > lastSuspendTime) { - currentEvent.duration = 0; - } else { - currentEvent.duration = (firstResumeDate - currentEvent.date)/60/1000; - } - } - - events.push(currentEvent); - - if (currentEvent.duration === 0) { - // bail out rather than wasting time going through the rest of the suspend events - return events; - } - - for (var i=0; i < pumpSuspends.length; i++) { - var suspend = pumpSuspends[i]; - - for (var j=0; j < events.length; j++) { - - if ((events[j].date <= suspend.date) && (events[j].date+events[j].duration*60*1000) > suspend.date) { - // event started before the suspend, but finished after the suspend started - - if ((events[j].date+events[j].duration*60*1000) > (suspend.date+suspend.duration*60*1000)) { - var event2 = _.cloneDeep(events[j]); - - var event2StartDate = moment(suspend.started_at).add(suspend.duration,'minutes'); - - event2.timestamp = event2StartDate.format(); - event2.started_at = new Date(tz(event2.timestamp)); - event2.date = suspend.date+suspend.duration*60*1000; - - event2.duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000; - - events.push(event2); - } - - events[j].duration = (suspend.date-events[j].date)/60/1000; - - } else if ((suspend.date <= events[j].date) && (suspend.date+suspend.duration*60*1000 > events[j].date)) { - // suspend started before the event, but finished after the event started - - events[j].duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000; - - var eventStartDate = moment(suspend.started_at).add(suspend.duration,'minutes'); - - events[j].timestamp = eventStartDate.format(); - events[j].started_at = new Date(tz(events[j].timestamp)); - events[j].date = suspend.date + suspend.duration*60*1000; - } - } - } - - return events; -} - -function calcTempTreatments (inputs, zeroTempDuration) { - var pumpHistory = inputs.history; - var pumpHistory24 = inputs.history24; - var profile_data = inputs.profile; - var autosens_data = inputs.autosens; - var tempHistory = []; - var tempBoluses = []; - var pumpSuspends = []; - var pumpResumes = []; - var suspendedPrior = false; - var firstResumeTime, lastSuspendTime; - var currentlySuspended = false; - var suspendError = false; - - var now = new Date(tz(inputs.clock)); - - if(inputs.history24) { - var pumpHistory = [ ].concat(inputs.history).concat(inputs.history24); - } - - var lastRecordTime = now; - - // Gather the times the pump was suspended and resumed - for (var i=0; i < pumpHistory.length; i++) { - var temp = {}; - - var current = pumpHistory[i]; - - if (current._type === "PumpSuspend") { - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(current.timestamp)); - temp.date = temp.started_at.getTime(); - pumpSuspends.push(temp); - } else if (current._type === "PumpResume") { - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(current.timestamp)); - temp.date = temp.started_at.getTime(); - pumpResumes.push(temp); - } - } - - pumpSuspends = _.sortBy(pumpSuspends, 'date'); - - pumpResumes = _.sortBy(pumpResumes, 'date'); - - if (pumpResumes.length > 0) { - firstResumeTime = pumpResumes[0].timestamp; - - // Check to see if our first resume was prior to our first suspend - // indicating suspend was prior to our first event. - if (pumpSuspends.length === 0 || (pumpResumes[0].date < pumpSuspends[0].date)) { - suspendedPrior = true; - } - - } - - var j=0; // matching pumpResumes entry; - - // Match the resumes with the suspends to get durations - for (i=0; i < pumpSuspends.length; i++) { - for (; j < pumpResumes.length; j++) { - if (pumpResumes[j].date > pumpSuspends[i].date) { - break; - } - } - - if ((j >= pumpResumes.length) && !currentlySuspended) { - // even though it isn't the last suspend, we have reached - // the final suspend. Set resume last so the - // algorithm knows to suspend all the way - // through the last record beginning at the last suspend - // since we don't have a matching resume. - currentlySuspended = 1; - lastSuspendTime = pumpSuspends[i].timestamp; - - break; - } - - pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date)/60/1000; - - } - - // These checks indicate something isn't quite aligned. - // Perhaps more resumes that suspends or vice versa... - if (!suspendedPrior && !currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+")!"); - } else if (suspendedPrior && !currentlySuspended && ((pumpResumes.length-1) !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to history block!"); - } else if (!suspendedPrior && currentlySuspended && (pumpResumes.length !== (pumpSuspends.length-1))) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended past end of history block!"); - } else if (suspendedPrior && currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to and past end of history block!"); - } - - if (i < (pumpSuspends.length-1)) { - // truncate any extra suspends. if we had any extras - // the error checks above would have issued a error log message - pumpSuspends.splice(i+1, pumpSuspends.length-i-1); - } - - // Pick relevant events for processing and clean the data - - for (i=0; i < pumpHistory.length; i++) { - var current = pumpHistory[i]; - if (current.bolus && current.bolus._type === "Bolus") { - var temp = current; - current = temp.bolus; - } - if (current.created_at) { - current.timestamp = current.created_at; - } - var currentRecordTime = new Date(tz(current.timestamp)); - //console.error(current); - //console.error(currentRecordTime,lastRecordTime); - // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes) - if (currentRecordTime > lastRecordTime) { - //console.error("",currentRecordTime," > ",lastRecordTime); - //process.stderr.write("."); - continue; - } else { - lastRecordTime = currentRecordTime; - } - if (current._type === "Bolus") { - var temp = {}; - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(current.timestamp)); - if (temp.started_at > now) { - //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at); - process.stderr.write(" "+current.amount+"U @ "+temp.started_at); - } else { - temp.date = temp.started_at.getTime(); - temp.insulin = current.amount; - tempBoluses.push(temp); - } - } else if (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard") { - //imports treatments entered through Nightscout Care Portal - //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard - var temp = {}; - temp.timestamp = current.created_at; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - temp.insulin = current.insulin; - tempBoluses.push(temp); - } else if (current.enteredBy === "xdrip") { - var temp = {}; - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - temp.insulin = current.insulin; - tempBoluses.push(temp); - } else if (current.enteredBy ==="HAPP_App" && current.insulin) { - var temp = {}; - temp.timestamp = current.created_at; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - temp.insulin = current.insulin; - tempBoluses.push(temp); - } else if (current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) { - var temp = {}; - temp.rate = current.absolute; - temp.duration = current.duration; - temp.timestamp = current.created_at; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - tempHistory.push(temp); - } else if (current.eventType === "Temp Basal") { - var temp = {}; - temp.rate = current.rate; - temp.duration = current.duration; - // Loop reports the amount of insulin actually delivered while the temp basal was running - // use that to calculate the effective temp basal rate - if (typeof current.amount !== 'undefined') { - temp.rate = current.amount / current.duration * 60; - } - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - tempHistory.push(temp); - } else if (current._type === "TempBasal") { - if (current.temp === 'percent') { - continue; - } - var rate = current.rate; - var timestamp = current.timestamp; - var duration; - if (i>0 && pumpHistory[i-1].timestamp === timestamp && pumpHistory[i-1]._type === "TempBasalDuration") { - duration = pumpHistory[i-1]['duration (min)']; - } else { - for (var iter=0; iter < pumpHistory.length; iter++) { - if (pumpHistory[iter].timestamp === timestamp && pumpHistory[iter]._type === "TempBasalDuration") { - duration = pumpHistory[iter]['duration (min)']; - break; - } - } - - if (duration === undefined) { - console.error("No duration found for "+rate+" U/hr basal "+timestamp, pumpHistory[i - 1], current, pumpHistory[i + 1]); - } - } - var temp = {}; - temp.rate = rate; - temp.timestamp = current.timestamp; - temp.started_at = new Date(tz(temp.timestamp)); - temp.date = temp.started_at.getTime(); - temp.duration = duration; - tempHistory.push(temp); - } - // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation - var temp = {}; - temp.rate = 0; - // start the zero temp 1m in the future to avoid clock skew - temp.started_at = new Date(now.getTime() + (1 * 60 * 1000)); - temp.date = temp.started_at.getTime(); - if (zeroTempDuration) { - temp.duration = zeroTempDuration; - } else { - temp.duration = 0; - } - tempHistory.push(temp); - } - - // Check for overlapping events and adjust event lengths in case of overlap - - tempHistory = _.sortBy(tempHistory, 'date'); - - for (i=0; i+1 < tempHistory.length; i++) { - if (tempHistory[i].date + tempHistory[i].duration*60*1000 > tempHistory[i+1].date) { - tempHistory[i].duration = (tempHistory[i+1].date - tempHistory[i].date)/60/1000; - // Delete AndroidAPS "Cancel TBR records" in which duration is not populated - if (tempHistory[i+1].duration === null) { - tempHistory.splice(i+1, 1); - } - } - } - - // Create an array of moments to slit the temps by - // currently supports basal changes - - var splitterEvents = []; - - _.forEach(profile_data.basalprofile,function addSplitter(o) { - var splitterEvent = {}; - splitterEvent.type = 'recurring'; - splitterEvent.minutes = o.minutes; - splitterEvents.push(splitterEvent); - }); - - // iterate through the events and split at basal break points if needed - - var splitHistoryByBasal = []; - - _.forEach(tempHistory, function splitEvent(o) { - splitHistoryByBasal = splitHistoryByBasal.concat(splitTimespan(o,splitterEvents)); - }); - - tempHistory = _.sortBy(tempHistory, function(o) { return o.date; }); - - var suspend_zeros_iob = false; - - if (typeof profile_data.suspend_zeros_iob !== 'undefined') { - suspend_zeros_iob = profile_data.suspend_zeros_iob; - } - - if (suspend_zeros_iob) { - // iterate through the events and adjust their - // times as required to account for pump suspends - var splitHistory = []; - - _.forEach(splitHistoryByBasal, function splitSuspendEvent(o) { - var splitEvents = splitAroundSuspends(o, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended); - splitHistory = splitHistory.concat(splitEvents); - }); - - var zTempSuspendBasals = []; - - // Any existing temp basals during times the pump was suspended are now deleted - // Add 0 temp basals to negate the profile basal rates during times pump is suspended - _.forEach(pumpSuspends, function createTempBasal(o) { - var zTempBasal = [{ - _type: 'SuspendBasal', - rate: 0, - duration: o.duration, - date: o.date, - started_at: o.started_at - }]; - zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal); - }); - - // Add temp suspend basal for maximum DIA (8) up to the resume time - // if there is no matching suspend in the history before the first - // resume - var max_dia_ago = now.getTime() - 8*60*60*1000; - var firstResumeStarted = new Date(firstResumeTime); - var firstResumeDate = firstResumeStarted.getTime() - - // impact on IOB only matters if the resume occurred - // after DIA hours before now. - // otherwise, first resume date can be ignored. Whatever - // insulin is present prior to resume will be aged - // out due to DIA. - if (suspendedPrior && (max_dia_ago < firstResumeDate)) { - - var suspendStart = new Date(max_dia_ago); - var suspendStartDate = suspendStart.getTime() - var started_at = new Date(tz(suspendStart.toISOString())); - - var zTempBasal = [{ - // add _type to aid debugging. It isn't used - // anywhere. - _type: 'SuspendBasal', - rate: 0, - duration: (firstResumeDate - max_dia_ago)/60/1000, - date: suspendStartDate, - started_at: started_at - }]; - zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal); - } - - if (currentlySuspended) { - var suspendStart = new Date(lastSuspendTime); - var suspendStartDate = suspendStart.getTime() - var started_at = new Date(tz(suspendStart.toISOString())); - - var zTempBasal = [{ - _type: 'SuspendBasal', - rate: 0, - duration: (now - suspendStartDate)/60/1000, - date: suspendStartDate, - timestamp: lastSuspendTime, - started_at: started_at - }]; - zTempSuspendBasals = zTempSuspendBasals.concat(zTempBasal); - } - - // Add the new 0 temp basals to the splitHistory. - // We have to split the new zero temp basals by the profile - // basals just like the other temp basals. - _.forEach(zTempSuspendBasals, function splitEvent(o) { - splitHistory = splitHistory.concat(splitTimespan(o,splitterEvents)); - }); - } else { - splitHistory = splitHistoryByBasal; - } - - splitHistory = _.sortBy(splitHistory, function(o) { return o.date; }); - - // tempHistory = splitHistory; - - // iterate through the temp basals and create bolus events from temps that affect IOB - - var tempBolusSize; - - for (i=0; i < splitHistory.length; i++) { - - var currentItem = splitHistory[i]; - - if (currentItem.duration > 0) { - var target_bg; - - var currentRate = profile_data.current_basal; - if (!_.isEmpty(profile_data.basalprofile)) { - currentRate = basalprofile.basalLookup(profile_data.basalprofile,new Date(currentItem.timestamp)); - } - - if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') { - target_bg = (profile_data.min_bg + profile_data.max_bg) / 2; - } - //if (profile_data.temptargetSet && target_bg > 110) { - //sensitivityRatio = 2/(2+(target_bg-100)/40); - //currentRate = profile_data.current_basal * sensitivityRatio; - //} - var sensitivityRatio; - var profile = profile_data; - var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled basal (which might change) - if ( profile.half_basal_exercise_target ) { - var halfBasalTarget = profile.half_basal_exercise_target; - } else { - var halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) - } - if ( profile.exercise_mode && profile.temptargetSet && target_bg >= normalTarget + 5 ) { - // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 - // 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 - var c = halfBasalTarget - normalTarget; - sensitivityRatio = c/(c+target_bg-normalTarget); - } else if (typeof autosens_data !== 'undefined' ) { - sensitivityRatio = autosens_data.ratio; - //process.stderr.write("Autosens ratio: "+sensitivityRatio+"; "); - } - if ( sensitivityRatio ) { - currentRate = currentRate * sensitivityRatio; - } - - var netBasalRate = currentItem.rate - currentRate; - if (netBasalRate < 0) { tempBolusSize = -0.05; } - else { tempBolusSize = 0.05; } - var netBasalAmount = Math.round(netBasalRate*currentItem.duration*10/6)/100 - var tempBolusCount = Math.round(netBasalAmount / tempBolusSize); - var tempBolusSpacing = currentItem.duration / tempBolusCount; - for (j=0; j < tempBolusCount; j++) { - var tempBolus = {}; - tempBolus.insulin = tempBolusSize; - tempBolus.date = currentItem.date + j * tempBolusSpacing*60*1000; - tempBolus.created_at = new Date(tempBolus.date); - tempBoluses.push(tempBolus); - } - } - } - var all_data = [ ].concat(tempBoluses).concat(tempHistory); - all_data = _.sortBy(all_data, 'date'); - return all_data; -} -exports = module.exports = calcTempTreatments; diff --git a/lib/iob/history.ts b/lib/iob/history.ts new file mode 100644 index 000000000..bbe4f7b0d --- /dev/null +++ b/lib/iob/history.ts @@ -0,0 +1,622 @@ +import * as basalprofile from '../profile/basal' +import { Profile } from '../types/Profile'; +import { toLocalDate } from '../date'; +import { PumpHistoryEvent } from '../types/PumpHistoryEvent'; +import { NightscoutTreatment } from '../types/NightscoutTreatment'; +import * as t from 'io-ts' +import { Autosens } from '../types/Autosens'; +import { BasalTreatment, BolusTreatment, InsulinTreatment } from "./InsulinTreatment"; +import * as date from '../date' + +interface Splitter { + type: 'recurring' + minutes: number +} + +interface PumpSuspendResume { + timestamp: string + started_at: Date + date: number + duration: number +} + +const Input = t.intersection([ + t.type({ + history: t.array(t.union([ + NightscoutTreatment, + PumpHistoryEvent + ])), + profile: Profile, + }), + t.partial({ + history24: t.array(t.union([ + NightscoutTreatment, + PumpHistoryEvent + ])), + autosens: Autosens, + clock: t.string, + }) +]) + +export type Input = t.TypeOf + +function splitTimespanWithOneSplitter(event: BasalTreatment, splitter: Splitter) { + + if (splitter.type !== 'recurring') { + return [event] + } + + var startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes(); + var endMinutes = startMinutes + event.duration; + + if (! (event.duration > 30 || (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || (endMinutes > 1440 && splitter.minutes < (endMinutes - 1440)))) { + return [event] + } + + // 1440 = one day; no clean way to check if the event overlaps midnight + // so checking if end of event in minutes is past midnight + + var event1Duration = 0; + + if (event.duration > 30) { + event1Duration = 30; + } else { + var splitPoint = splitter.minutes; + if (endMinutes > 1440) { + splitPoint = 1440; + } + event1Duration = splitPoint - startMinutes; + } + + const event1EndDate = new Date(event.started_at) + event1EndDate.setMinutes(event1EndDate.getMinutes() + event1Duration) + + var event1 = { + ...event, + duration: event1Duration + }; + var event2 = { + ...event, + duration: event.duration - event1Duration, + timestamp: date.format(event1EndDate), + started_at: event1EndDate, + date: event1EndDate.getTime(), + } + + return [event1, event2]; +} + +function splitTimespan(event: BasalTreatment, splitterMoments: Splitter[]) { + + var results = [event]; + + var splitFound = true; + + while(splitFound) { + + let resultArray: BasalTreatment[] = []; + splitFound = false; + + for (let i = 0; i < results.length; i++) { + const o = results[i] + for (let j = 0; j < splitterMoments.length; j++) { + const p = splitterMoments[j] + var splitResult = splitTimespanWithOneSplitter(o,p); + if (splitResult.length > 1) { + resultArray = resultArray.concat(splitResult); + splitFound = true; + break; + } + } + + if (!splitFound) { + resultArray = resultArray.concat([o]); + }; + } + + results = resultArray; + } + + return results; +} + +// Split currentEvent around any conflicting suspends +// by removing the time period from the event that +// overlaps with any suspend. +function splitAroundSuspends (currentEvent: BasalTreatment, pumpSuspends: PumpSuspendResume[], firstResumeTime: string | undefined, suspendedPrior: boolean, lastSuspendTime: string | undefined, currentlySuspended: boolean): BasalTreatment[] { + var events = []; + + // @todo: check why it can be undefined + var firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date(0); + var firstResumeDate = firstResumeStarted.getTime() + + // @todo: check why it can be undefined + var lastSuspendStarted = lastSuspendTime ? new Date(lastSuspendTime) : new Date(); + var lastSuspendDate = lastSuspendStarted.getTime(); + + if (suspendedPrior && (currentEvent.date < firstResumeDate)) { + if ((currentEvent.date+currentEvent.duration*60*1000) < firstResumeDate) { + currentEvent.duration = 0; + } else { + currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000; + + currentEvent.started_at = toLocalDate(firstResumeStarted); + currentEvent.date = firstResumeDate + } + } + + if (currentlySuspended && ((currentEvent.date+currentEvent.duration*60*1000) > lastSuspendDate)) { + if (currentEvent.date > lastSuspendDate) { + currentEvent.duration = 0; + } else { + currentEvent.duration = (firstResumeDate - currentEvent.date)/60/1000; + } + } + + events.push(currentEvent); + + if (currentEvent.duration === 0) { + // bail out rather than wasting time going through the rest of the suspend events + return events; + } + + for (var i=0; i < pumpSuspends.length; i++) { + var suspend = pumpSuspends[i]; + + for (var j=0; j < events.length; j++) { + + if ((events[j].date <= suspend.date) && (events[j].date+events[j].duration*60*1000) > suspend.date) { + // event started before the suspend, but finished after the suspend started + + if ((events[j].date+events[j].duration*60*1000) > (suspend.date+suspend.duration*60*1000)) { + var event2StartDate = new Date(suspend.started_at) + event2StartDate.setMinutes(event2StartDate.getMinutes() + suspend.duration) + + events.push({ + ...events[j], + timestamp: date.format(event2StartDate), + started_at: toLocalDate(event2StartDate), + date: suspend.date+suspend.duration*60*1000, + duration: ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000, + }); + } + + events[j].duration = (suspend.date-events[j].date)/60/1000; + + } else if ((suspend.date <= events[j].date) && (suspend.date+suspend.duration*60*1000 > events[j].date)) { + // suspend started before the event, but finished after the event started + + events[j].duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000; + + const eventStartDate = new Date(suspend.started_at) + eventStartDate.setMinutes(eventStartDate.getMinutes() + suspend.duration); + + events[j].timestamp = date.format(eventStartDate); + events[j].started_at = toLocalDate(new Date(events[j].timestamp)); + events[j].date = suspend.date + suspend.duration*60*1000; + } + } + } + + return events; +} + + +export default function calcTempTreatments (inputs: Input, zeroTempDuration?: number): InsulinTreatment[] { + let pumpHistory = [...inputs.history, ...(inputs.history24 || [])] + var profile_data = inputs.profile; + var autosens_data = inputs.autosens; + var tempHistory: BasalTreatment[] = []; + var tempBoluses: BolusTreatment[] = []; + var pumpSuspends: PumpSuspendResume[] = []; + var pumpResumes: PumpSuspendResume[] = []; + var suspendedPrior = false; + var firstResumeTime: string | undefined + let lastSuspendTime: string | undefined; + var currentlySuspended = false; + + // @todo: check if clock can be undefined + var now = toLocalDate(inputs.clock ? new Date(inputs.clock) : new Date()); + + var lastRecordTime = now; + + // Gather the times the pump was suspended and resumed + for (var i=0; i < pumpHistory.length; i++) { + const current = pumpHistory[i]; + + if (! PumpHistoryEvent.is(current) || (current._type !== 'PumpSuspend' && current._type !== 'PumpResume')) { + continue; + } + + const started_at = toLocalDate(new Date(current.timestamp)) + const temp = { + timestamp: current.timestamp, + started_at, + date: started_at.getTime(), + duration: 0, + } + + if (current._type === "PumpSuspend") { + pumpSuspends.push(temp); + } else if (current._type === "PumpResume") { + pumpResumes.push(temp); + } + } + + pumpSuspends = pumpSuspends.sort((a, b) => a.date - b.date) + pumpResumes = pumpResumes.sort((a, b) => a.date - b.date) + + if (pumpResumes.length > 0) { + firstResumeTime = pumpResumes[0].timestamp; + + // Check to see if our first resume was prior to our first suspend + // indicating suspend was prior to our first event. + if (pumpSuspends.length === 0 || (pumpResumes[0].date < pumpSuspends[0].date)) { + suspendedPrior = true; + } + + } + + var j=0; // matching pumpResumes entry; + + // Match the resumes with the suspends to get durations + for (i=0; i < pumpSuspends.length; i++) { + for (; j < pumpResumes.length; j++) { + if (pumpResumes[j].date > pumpSuspends[i].date) { + break; + } + } + + if ((j >= pumpResumes.length) && !currentlySuspended) { + // even though it isn't the last suspend, we have reached + // the final suspend. Set resume last so the + // algorithm knows to suspend all the way + // through the last record beginning at the last suspend + // since we don't have a matching resume. + currentlySuspended = true; + lastSuspendTime = pumpSuspends[i].timestamp; + + break; + } + + pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date)/60/1000; + + } + + // These checks indicate something isn't quite aligned. + // Perhaps more resumes that suspends or vice versa... + if (!suspendedPrior && !currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { + console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+")!"); + } else if (suspendedPrior && !currentlySuspended && ((pumpResumes.length-1) !== pumpSuspends.length)) { + console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to history block!"); + } else if (!suspendedPrior && currentlySuspended && (pumpResumes.length !== (pumpSuspends.length-1))) { + console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended past end of history block!"); + } else if (suspendedPrior && currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { + console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to and past end of history block!"); + } + + if (i < (pumpSuspends.length-1)) { + // truncate any extra suspends. if we had any extras + // the error checks above would have issued a error log message + pumpSuspends.splice(i+1, pumpSuspends.length-i-1); + } + + // Pick relevant events for processing and clean the data + + for (i=0; i < pumpHistory.length; i++) { + let current: NightscoutTreatment | PumpHistoryEvent = pumpHistory[i]; + if (NightscoutTreatment.is(current) && current.bolus && current.bolus._type === "Bolus") { + current = current.bolus; + } + + const timestamp = NightscoutTreatment.is(current) ? current.created_at : current.timestamp + var currentRecordTime = toLocalDate(new Date(timestamp)) + //console.error(current); + //console.error(currentRecordTime,lastRecordTime); + // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes) + if (currentRecordTime > lastRecordTime) { + //console.error("",currentRecordTime," > ",lastRecordTime); + //process.stderr.write("."); + continue; + } else { + lastRecordTime = currentRecordTime; + } + if (PumpHistoryEvent.is(current) && current._type === "Bolus") { + const started_at = toLocalDate(new Date(current.timestamp)); + if (started_at > now) { + //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at); + process.stderr.write(" "+current.amount+"U @ "+started_at); + } else { + // @todo check for undefined insulin + tempBoluses.push({ + timestamp: current.timestamp, + started_at, + date: started_at.getTime(), + insulin: current.amount!, + }); + } + } else if (NightscoutTreatment.is(current) && (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard")) { + //imports treatments entered through Nightscout Care Portal + //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard + const started_at = toLocalDate(new Date(current.created_at)) + // @todo check for undefined insulin + tempBoluses.push({ + timestamp: current.created_at, + started_at, + date: started_at.getTime(), + insulin: current.insulin! + }); + } else if (NightscoutTreatment.is(current) && current.enteredBy === "xdrip") { + const started_at = toLocalDate(new Date(current.created_at)) + // @todo check for undefined insulin + tempBoluses.push({ + timestamp: current.created_at, + started_at, + date: started_at.getTime(), + insulin: current.insulin! + }); + } else if (NightscoutTreatment.is(current) && current.enteredBy ==="HAPP_App" && current.insulin) { + const started_at = toLocalDate(new Date(current.created_at)) + // @todo check for undefined insulin + tempBoluses.push({ + timestamp: current.created_at, + started_at, + date: started_at.getTime(), + insulin: current.insulin! + }); + } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) { + const started_at = toLocalDate(new Date(current.created_at)) + // @todo check for undefined rate and duration + tempHistory.push({ + timestamp: current.created_at, + started_at, + date: started_at.getTime(), + rate: current.absolute!, + duration: current.duration!, + }); + } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal") { + const started_at = toLocalDate(new Date(current.created_at)) + let rate = current.rate + // Loop reports the amount of insulin actually delivered while the temp basal was running + // use that to calculate the effective temp basal rate + if (typeof current.amount !== 'undefined') { + // @todo: fix type for duration possibly undefined + rate = current.amount / current.duration! * 60; + } + // @todo check for undefined rate and duration + tempHistory.push({ + timestamp: current.created_at, + started_at, + date: started_at.getTime(), + rate: rate!, + duration: current.duration!, + }); + } else if (PumpHistoryEvent.is(current) && current._type === "TempBasal") { + if (current.temp === 'percent') { + continue; + } + var rate = current.rate; + let duration; + const previous = i > 0 ? pumpHistory[i-1] : undefined + if (PumpHistoryEvent.is(previous) && previous.timestamp === timestamp && previous._type === "TempBasalDuration") { + duration = previous['duration (min)']; + } else { + for (var iter=0; iter < pumpHistory.length; iter++) { + const item = pumpHistory[iter] + if (PumpHistoryEvent.is(item) && item.timestamp === timestamp && item._type === "TempBasalDuration") { + duration = item['duration (min)']; + break; + } + } + + if (duration === undefined) { + console.error("No duration found for "+rate+" U/hr basal "+timestamp, pumpHistory[i - 1], current, pumpHistory[i + 1]); + } + } + + const started_at = toLocalDate(new Date(current.timestamp)) + // @todo check for undefined rate and duration + tempHistory.push({ + timestamp: current.timestamp, + started_at, + date: started_at.getTime(), + rate: rate!, + duration: duration!, + }); + } + + // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation + // start the zero temp 1m in the future to avoid clock skew + const started_at = new Date(now.getTime() + (1 * 60 * 1000)); + tempHistory.push({ + timestamp: started_at.toISOString(), + started_at, + date: started_at.getTime(), + rate: 0, + duration: zeroTempDuration || 0 + }); + } + + // Check for overlapping events and adjust event lengths in case of overlap + + tempHistory = tempHistory.sort((a, b) => a.date - b.date) + + for (i=0; i < tempHistory.length -1; i++) { + const item = tempHistory[i] + const next = tempHistory[i+1] + // @todo: check duration when undefined (or null) + if (item.date + (item.duration || 0) * 60*1000 > next.date) { + tempHistory[i].duration = (next.date - item.date)/60/1000; + // Delete AndroidAPS "Cancel TBR records" in which duration is not populated + if (next.duration === null || next.duration === undefined) { + tempHistory.splice(i+1, 1); + } + } + } + + // Create an array of moments to slit the temps by + // currently supports basal changes + + const splitterEvents = (profile_data.basalprofile || []).map(o => ({ + type: 'recurring' as const, + minutes: o.minutes, + })) + + // iterate through the events and split at basal break points if needed + const splitHistoryByBasal = tempHistory.reduce( + (b, o) => ([...b, ...splitTimespan(o, splitterEvents)]), + [] as BasalTreatment[] + ) + + // @todo: not necessary: remove + tempHistory = tempHistory.sort((a, b) => a.date - b.date) + + var suspend_zeros_iob = profile_data.suspend_zeros_iob || false; + let splitHistory = splitHistoryByBasal; + + if (suspend_zeros_iob) { + // iterate through the events and adjust their + // times as required to account for pump suspends + splitHistory = splitHistoryByBasal.reduce( + (b, a) => [...b, ...splitAroundSuspends(a, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended)], + [] as BasalTreatment[] + ) + + // Any existing temp basals during times the pump was suspended are now deleted + // Add 0 temp basals to negate the profile basal rates during times pump is suspended + let zTempSuspendBasals = pumpSuspends.reduce( + (b, a) => [...b, { ...a, rate: 0 }], + [] as (PumpSuspendResume & { rate: number })[] + ) + + // Add temp suspend basal for maximum DIA (8) up to the resume time + // if there is no matching suspend in the history before the first + // resume + var max_dia_ago = now.getTime() - 8*60*60*1000; + // @todo check why firstResumeStarted can be undefined + var firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date(); + var firstResumeDate = firstResumeStarted.getTime() + + // impact on IOB only matters if the resume occurred + // after DIA hours before now. + // otherwise, first resume date can be ignored. Whatever + // insulin is present prior to resume will be aged + // out due to DIA. + if (suspendedPrior && (max_dia_ago < firstResumeDate)) { + + var suspendStart = new Date(max_dia_ago); + var suspendStartDate = suspendStart.getTime() + var started_at = toLocalDate(suspendStart); + + zTempSuspendBasals.push({ + rate: 0, + duration: (firstResumeDate - max_dia_ago)/60/1000, + date: suspendStartDate, + started_at, + timestamp: suspendStart.toISOString() + }) + } + + if (currentlySuspended) { + // @todo check why lastSuspendTime can be undefined + var suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date(); + var suspendStartDate = suspendStart.getTime() + var started_at = toLocalDate(suspendStart); + + // @todo check why lastSuspendTime can be undefined + zTempSuspendBasals.push({ + rate: 0, + duration: (now.getTime() - suspendStartDate)/60/1000, + date: suspendStartDate, + started_at, + timestamp: lastSuspendTime!, + }) + } + + // Add the new 0 temp basals to the splitHistory. + // We have to split the new zero temp basals by the profile + // basals just like the other temp basals. + splitHistory = zTempSuspendBasals.reduce( + (b, a) => [...b, ...splitTimespan(a, splitterEvents)], + splitHistory + ) + } + + splitHistory = splitHistory.sort((a, b) => a.date - b.date) + + // tempHistory = splitHistory; + + // iterate through the temp basals and create bolus events from temps that affect IOB + + for (i=0; i < splitHistory.length; i++) { + + var currentItem = splitHistory[i]; + + if (currentItem.duration > 0) { + var target_bg; + + var currentRate = profile_data.current_basal; + if (profile_data.basalprofile && profile_data.basalprofile.length > 0) { + currentRate = basalprofile.basalLookup( + profile_data.basalprofile, + new Date(currentItem.timestamp) + ); + } + + if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') { + target_bg = (profile_data.min_bg + profile_data.max_bg) / 2; + } + //if (profile_data.temptargetSet && target_bg > 110) { + //sensitivityRatio = 2/(2+(target_bg-100)/40); + //currentRate = profile_data.current_basal * sensitivityRatio; + //} + var sensitivityRatio; + var profile = profile_data; + var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled basal (which might change) + if ( profile.half_basal_exercise_target ) { + var halfBasalTarget = profile.half_basal_exercise_target; + } else { + halfBasalTarget = 160 as t.Int; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + } + if ( profile.exercise_mode && profile.temptargetSet && target_bg && target_bg >= normalTarget + 5 ) { + // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 + // 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 + var c = halfBasalTarget - normalTarget; + sensitivityRatio = c/(c+target_bg-normalTarget); + } else if (typeof autosens_data !== 'undefined' ) { + sensitivityRatio = autosens_data.ratio; + //process.stderr.write("Autosens ratio: "+sensitivityRatio+"; "); + } + + // @check why currentRate can be undefined + currentRate = currentRate || 0 + if ( sensitivityRatio ) { + currentRate = currentRate * sensitivityRatio; + } + + var netBasalRate = currentItem.rate - currentRate; + const tempBolusSize = netBasalRate < 0 ? -0.05 : 0.05; + var netBasalAmount = Math.round(netBasalRate*currentItem.duration*10/6)/100 + var tempBolusCount = Math.round(netBasalAmount / tempBolusSize); + var tempBolusSpacing = currentItem.duration / tempBolusCount; + for (j=0; j < tempBolusCount; j++) { + const tempBolusDate = currentItem.date + j * tempBolusSpacing*60*1000 + tempBoluses.push({ + insulin: tempBolusSize, + date: tempBolusDate, + started_at: new Date(tempBolusDate), + timestamp: new Date(tempBolusDate).toISOString() + }); + } + } + } + + const all_data = [ + ...tempBoluses, + ...tempHistory + ] + + return all_data.sort((a, b) => a.date - b.date) +} + +exports = module.exports = calcTempTreatments; diff --git a/lib/iob/index.js b/lib/iob/index.ts similarity index 57% rename from lib/iob/index.js rename to lib/iob/index.ts index fd64e3473..27116fd63 100644 --- a/lib/iob/index.js +++ b/lib/iob/index.ts @@ -1,52 +1,75 @@ -'use strict'; +import find_insulin from './history' +import type { Input } from './history' +import sum from './total' +import { toLocalDate } from '../date'; +import { InsulinTreatment, isBasalTreatment, isBolusTreatment } from './InsulinTreatment'; -var tz = require('moment-timezone'); -var find_insulin = require('./history'); -var calculate = require('./calculate'); -var sum = require('./total'); +interface IOB { + iob: number; + activity: number; + basaliob: number; + bolusiob: number; + netbasalinsulin: number; + bolusinsulin: number; + time: Date; +} + +interface IOBItem extends IOB { + iobWithZeroTemp?: IOB + lastBolusTime?: number + lastTemp?: { + date: number + duration: number + } +} -function generate (inputs, currentIOBOnly, treatments) { +export default function generate (inputs: Input, currentIOBOnly: boolean = false, treatments?: InsulinTreatment[]) { + let treatmentsWithZeroTemp: InsulinTreatment[] = [] if (!treatments) { - var treatments = find_insulin(inputs); + treatments = find_insulin(inputs); // calculate IOB based on continuous future zero temping as well - var treatmentsWithZeroTemp = find_insulin(inputs, 240); - } else { - var treatmentsWithZeroTemp = []; + treatmentsWithZeroTemp = find_insulin(inputs, 240); } //console.error(treatments.length, treatmentsWithZeroTemp.length); //console.error(treatments[treatments.length-1], treatmentsWithZeroTemp[treatmentsWithZeroTemp.length-1]) var opts = { - treatments: treatments - , profile: inputs.profile - , calculate: calculate + treatments: treatments, + profile: inputs.profile, + autosens: inputs.autosens }; - if ( inputs.autosens ) { - opts.autosens = inputs.autosens; - } var optsWithZeroTemp = { - treatments: treatmentsWithZeroTemp - , profile: inputs.profile - , calculate: calculate + treatments: treatmentsWithZeroTemp, + profile: inputs.profile, }; - var iobArray = []; + if (!inputs.clock) { + console.error("Clock is not defined"); + return [] + } + + var iobArray: IOBItem[] = []; //console.error(inputs.clock); if (! /(Z|[+-][0-2][0-9]:?[034][05])+/.test(inputs.clock) ) { console.error("Warning: clock input " + inputs.clock + " is unzoned; please pass clock-zoned.json instead"); } - var clock = new Date(tz(inputs.clock)); + var clock = toLocalDate(new Date(inputs.clock)); var lastBolusTime = new Date(0).getTime(); //clock.getTime()); - var lastTemp = {}; - lastTemp.date = new Date(0).getTime(); //clock.getTime()); + var lastTemp = { + date: new Date(0).getTime(), //clock.getTime()); + duration: 0, + }; //console.error(treatments[treatments.length-1]); treatments.forEach(function(treatment) { - if (treatment.insulin && treatment.started_at) { - lastBolusTime = Math.max(lastBolusTime,treatment.started_at); + if (isBolusTreatment(treatment) && treatment.insulin > 0) { + if (treatment.started_at.getTime() > lastBolusTime) { + lastBolusTime = treatment.started_at.getTime() + } + //lastBolusTime = Math.max(lastBolusTime, treatment.started_at.getTime()); //console.error(treatment.insulin,treatment.started_at,lastBolusTime); - } else if (typeof(treatment.rate) === 'number' && treatment.duration ) { + } else if (isBasalTreatment(treatment) && treatment.duration > 0) { if ( treatment.date > lastTemp.date ) { lastTemp = treatment; lastTemp.duration = Math.round(lastTemp.duration*100)/100; @@ -57,6 +80,7 @@ function generate (inputs, currentIOBOnly, treatments) { //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at) //if (treatment.insulin && treatment.started_at) { console.error(treatment.insulin,treatment.started_at,lastBolusTime); } }); + var iStop; if (currentIOBOnly) { // for COB calculation, we only need the zeroth element of iobArray @@ -70,6 +94,10 @@ function generate (inputs, currentIOBOnly, treatments) { //console.error(t); var iob = sum(opts, t); var iobWithZeroTemp = sum(optsWithZeroTemp, t); + + if (! iob || !iobWithZeroTemp) { + continue; + } //console.error(opts.treatments[opts.treatments.length-1], optsWithZeroTemp.treatments[optsWithZeroTemp.treatments.length-1]) iobArray.push(iob); //console.error(iob.iob, iobWithZeroTemp.iob); diff --git a/lib/iob/total.js b/lib/iob/total.ts similarity index 77% rename from lib/iob/total.js rename to lib/iob/total.ts index bc2c39bc1..c82132733 100644 --- a/lib/iob/total.js +++ b/lib/iob/total.ts @@ -1,12 +1,21 @@ -'use strict'; +import { Autosens } from "../types/Autosens"; +import { InsulineCurve, Profile } from "../types/Profile"; +import { InsulinTreatment, isBolusTreatment } from "./InsulinTreatment"; +import calculate from './calculate' -function iobTotal(opts, time) { +interface Options { + treatments: InsulinTreatment[]; + profile: Profile; + autosens?: Autosens +} + +export default function iobTotal(opts: Options, time: Date) { var now = time.getTime(); - var iobCalc = opts.calculate; + var iobCalc = calculate; var treatments = opts.treatments; var profile_data = opts.profile; - var dia = profile_data.dia; + var dia = profile_data.dia || 3; var peak = 0; var iob = 0; var basaliob = 0; @@ -15,7 +24,9 @@ function iobTotal(opts, time) { var bolusinsulin = 0; //var bolussnooze = 0; var activity = 0; - if (!treatments) return {}; + if (!treatments) { + return null + }; //if (typeof time === 'undefined') { //var time = new Date(); //} @@ -26,7 +37,13 @@ function iobTotal(opts, time) { dia = 3; } - var curveDefaults = { + var curveDefaults: { + [k in InsulineCurve]: { + requireLongDia: boolean, + peak: number, + tdMin?: number + } + } = { 'bilinear': { requireLongDia: false, peak: 75 // not really used, but prevents having to check later @@ -43,15 +60,12 @@ function iobTotal(opts, time) { }, }; - var curve = 'bilinear'; - - if (profile_data.curve !== undefined) { - curve = profile_data.curve.toLowerCase(); - } + let curve = profile_data.curve || 'bilinear'; - if (!(curve in curveDefaults)) { + // @todo: remove when decoding + if (!InsulineCurve.is(curve)) { console.error('Unsupported curve function: "' + curve + '". Supported curves: "bilinear", "rapid-acting" (Novolog, Novorapid, Humalog, Apidra) and "ultra-rapid" (Fiasp). Defaulting to "rapid-acting".'); - curve = 'rapid-acting'; + curve = 'rapid-acting' as InsulineCurve; } var defaults = curveDefaults[curve]; @@ -77,7 +91,7 @@ function iobTotal(opts, time) { // {"insulin":0.05,"date":1507266530000,"created_at":"2017-10-06T05:08:50.000Z"} // boluses look like: // {"timestamp":"2017-10-05T22:06:31-07:00","started_at":"2017-10-06T05:06:31.000Z","date":1507266391000,"insulin":0.5} - if (treatment.insulin && tIOB && tIOB.iobContrib) { + if (isBolusTreatment(treatment) && treatment.insulin && tIOB && tIOB.iobContrib) { if (treatment.insulin < 0.1) { basaliob += tIOB.iobContrib; netbasalinsulin += treatment.insulin; From 81af6b1dbed598e8f0b43e3023e00bfb966131b0 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Thu, 8 Aug 2024 18:14:53 +0200 Subject: [PATCH 05/15] typescript migration --- .devcontainer/devcontainer.json | 24 - .eslintrc.js | 360 +- .gitignore | 1 + .prettierrc.json | 9 + .vscode/settings.json | 3 + CONTRIBUTING.md | 26 + Makefile | 2 +- bin/ns-status.js | 2 +- bin/oref0-autosens-history.js | 6 +- bin/oref0-autotune-core.js | 2 +- bin/oref0-autotune-prep.js | 2 +- bin/oref0-calculate-glucose-noise.js | 2 +- bin/oref0-calculate-iob.js | 4 +- bin/oref0-detect-sensitivity.js | 2 +- bin/oref0-determine-basal.js | 6 +- bin/oref0-find-insulin-uses.js | 2 +- bin/oref0-get-ns-entries.js | 2 +- bin/oref0-get-profile.js | 4 +- bin/oref0-meal.js | 6 +- bin/oref0-normalize-temps.js | 6 +- bin/oref0-raw.js | 4 +- bin/oref0-shared-node.js | 4 +- bin/oref0-upgrade.sh | 2 +- docker-compose.override.yml.dist | 2 - docker/node/Dockerfile | 1 + jest.config.ts | 24 + lib/autotune-prep/categorize.js | 3 +- lib/autotune/{index.js => index.ts} | 66 +- lib/{basal-set-temp.js => basal-set-temp.ts} | 34 +- .../bin/utils.js | 0 ...glucose-stats.js => calc-glucose-stats.ts} | 26 +- lib/date.ts | 24 + lib/determine-basal/autosens.js | 3 +- lib/determine-basal/determine-basal.ts | 2 - ...lucose-get-last.js => glucose-get-last.ts} | 68 +- lib/{glucose-stats.js => glucose-stats.ts} | 49 +- lib/iob/history.ts | 32 +- lib/iob/index.ts | 4 +- lib/meal/MealTreatment.ts | 8 + lib/meal/history.js | 141 - lib/meal/history.ts | 140 + lib/meal/index.js | 24 - lib/meal/index.ts | 36 + lib/meal/{total.js => total.ts} | 67 +- lib/medtronic-clock.js | 13 - lib/medtronic-clock.ts | 11 + lib/{percentile.js => percentile.ts} | 7 +- lib/profile/{basal.js => basal.ts} | 25 +- lib/profile/carbs.js | 2 +- lib/profile/index.js | 13 +- lib/profile/{isf.js => isf.ts} | 14 +- lib/profile/targets.js | 2 +- lib/{round-basal.js => round-basal.ts} | 34 +- lib/types/Autosens.ts | 13 + lib/types/EventType.ts | 28 + lib/types/GlucoseEntry.ts | 11 + lib/types/LocalDateFromDate.ts | 11 + lib/types/NightscoutTreatment.ts | 63 + lib/types/Profile.ts | 343 ++ lib/types/PumpHistoryEvent.ts | 90 + lib/types/renameKey.ts | 29 + package.json | 30 +- tests/basal.test.js | 36 - tests/basal.test.ts | 27 + tests/{bolus.test.js => bolus.test.ts} | 4 +- ...ck-syntax.test.js => check-syntax.test.ts} | 14 +- ...{cobhistory.test.js => cobhistory.test.ts} | 35 +- tests/command-behavior.tests.sh | 4 + tests/date.test.ts | 42 + tests/determine-basal.data.json | 4619 +++++++++++++++++ tests/determine-basal.data.test.ts | 288 + ...-basal.test.js => determine-basal.test.ts} | 2 - ...ucose.test.js => get-last-glucose.test.ts} | 2 +- ...glucose-noise.js => glucose-noise.test.ts} | 0 tests/iob.data.json | 4026 ++++++++++++++ tests/iob.data.test.ts | 17 + tests/{iob.test.js => iob.test.ts} | 14 +- tests/json-wf.sh | 2 +- tests/{profile.test.js => profile.test.ts} | 16 +- ...p-basal.test.js => set-temp-basal.test.ts} | 0 ...n-shell.test.js => tests-in-shell.test.ts} | 17 +- ...ucose.test.js => with-raw-glucose.test.ts} | 0 tsconfig.json | 24 +- tsconfig.test.json | 6 + 84 files changed, 10361 insertions(+), 808 deletions(-) create mode 100644 .prettierrc.json create mode 100644 .vscode/settings.json delete mode 100644 docker-compose.override.yml.dist create mode 100644 jest.config.ts rename lib/autotune/{index.js => index.ts} (94%) rename lib/{basal-set-temp.js => basal-set-temp.ts} (66%) rename bin/oref0-shared-node-utils.js => lib/bin/utils.js (100%) rename lib/{calc-glucose-stats.js => calc-glucose-stats.ts} (53%) create mode 100644 lib/date.ts rename lib/{glucose-get-last.js => glucose-get-last.ts} (68%) rename lib/{glucose-stats.js => glucose-stats.ts} (85%) create mode 100644 lib/meal/MealTreatment.ts delete mode 100644 lib/meal/history.js create mode 100644 lib/meal/history.ts delete mode 100644 lib/meal/index.js create mode 100644 lib/meal/index.ts rename lib/meal/{total.js => total.ts} (73%) delete mode 100644 lib/medtronic-clock.js create mode 100644 lib/medtronic-clock.ts rename lib/{percentile.js => percentile.ts} (85%) rename lib/profile/{basal.js => basal.ts} (60%) rename lib/profile/{isf.js => isf.ts} (72%) rename lib/{round-basal.js => round-basal.ts} (52%) create mode 100644 lib/types/Autosens.ts create mode 100644 lib/types/EventType.ts create mode 100644 lib/types/GlucoseEntry.ts create mode 100644 lib/types/LocalDateFromDate.ts create mode 100644 lib/types/NightscoutTreatment.ts create mode 100644 lib/types/Profile.ts create mode 100644 lib/types/PumpHistoryEvent.ts create mode 100644 lib/types/renameKey.ts delete mode 100644 tests/basal.test.js create mode 100644 tests/basal.test.ts rename tests/{bolus.test.js => bolus.test.ts} (98%) rename tests/{check-syntax.test.js => check-syntax.test.ts} (92%) rename tests/{cobhistory.test.js => cobhistory.test.ts} (56%) create mode 100644 tests/date.test.ts create mode 100644 tests/determine-basal.data.json create mode 100644 tests/determine-basal.data.test.ts rename tests/{determine-basal.test.js => determine-basal.test.ts} (99%) rename tests/{get-last-glucose.test.js => get-last-glucose.test.ts} (97%) rename tests/{glucose-noise.js => glucose-noise.test.ts} (100%) create mode 100644 tests/iob.data.json create mode 100644 tests/iob.data.test.ts rename tests/{iob.test.js => iob.test.ts} (99%) rename tests/{profile.test.js => profile.test.ts} (68%) rename tests/{set-temp-basal.test.js => set-temp-basal.test.ts} (100%) rename tests/{tests-in-shell.test.js => tests-in-shell.test.ts} (83%) rename tests/{with-raw-glucose.test.js => with-raw-glucose.test.ts} (100%) create mode 100644 tsconfig.test.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index be4a28874..8e1640e6b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,20 +1,10 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { "name": "oref0", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. "dockerComposeFile": [ "../docker-compose.yml", - "../docker-compose.override.yml", "docker-compose.yml" ], - // The 'service' property is the name of the service for the container that VS Code should - // use. Update this value and .devcontainer/docker-compose.yml to the real service name. "service": "node", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - // "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/app", "features": {}, "customizations": { @@ -27,18 +17,4 @@ ] } } - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - // "postCreateCommand": "cat /etc/os-release", - // Configure tool-specific properties. - // "customizations": {}, - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "devcontainer" } diff --git a/.eslintrc.js b/.eslintrc.js index b57fda9dc..a4e6fe5b7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,259 +1,121 @@ module.exports = { - "env": { - "commonjs": true, - "node": true, + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', }, - "extends": "eslint:recommended", - "globals": { + env: { + node: true, }, - "parserOptions": { - "ecmaVersion": 5 + parserOptions: { + ecmaVersion: 2019, + project: './tsconfig.json', + sourceType: 'module', }, - "rules": { - "accessor-pairs": "error", - "array-bracket-newline": "off", - "array-bracket-spacing": "off", - "array-callback-return": "off", - "array-element-newline": "off", - "arrow-body-style": "error", - "arrow-parens": "error", - "arrow-spacing": "error", - "block-scoped-var": "off", - "block-spacing": "off", - "brace-style": "off", - "callback-return": "error", - "camelcase": "off", - "capitalized-comments": "off", - "class-methods-use-this": "error", - "comma-dangle": "off", - "comma-spacing": "off", - "comma-style": "off", - "complexity": "off", - "computed-property-spacing": [ - "error", - "never" - ], - "consistent-return": "off", - "consistent-this": "off", - "curly": "off", - "default-case": "off", - "dot-location": [ - "error", - "property" - ], - "dot-notation": "off", - "eol-last": "off", - "eqeqeq": "error", - "func-call-spacing": "off", - "func-name-matching": "error", - "func-names": "off", - "func-style": "off", - "function-paren-newline": "off", - "generator-star-spacing": "error", - "global-require": "off", - "guard-for-in": "off", - "handle-callback-err": "error", - "id-blacklist": "error", - "id-length": "off", - "id-match": "error", - "implicit-arrow-linebreak": "error", - "indent": "off", - "indent-legacy": "off", - "init-declarations": "off", - "jsx-quotes": "error", - "key-spacing": "off", - "keyword-spacing": "off", - "line-comment-position": "off", - "linebreak-style": [ - "error", - "unix" - ], - "lines-around-comment": "off", - "lines-around-directive": "off", - "lines-between-class-members": "error", - "max-depth": "off", - "max-len": "off", - "max-lines": "off", - "max-lines-per-function": "off", - "max-nested-callbacks": "error", - "max-params": "off", - "max-statements": "off", - "max-statements-per-line": "off", - "multiline-comment-style": "off", - "multiline-ternary": "off", - "new-parens": "off", - "newline-after-var": "off", - "newline-before-return": "off", - "newline-per-chained-call": "off", - "no-alert": "error", - "no-array-constructor": "error", - "no-await-in-loop": "error", - "no-bitwise": "off", - "no-buffer-constructor": "error", - "no-caller": "error", - "no-catch-shadow": "off", - "no-confusing-arrow": "error", - "no-console":"off", - "no-continue": "off", - "no-div-regex": "error", - "no-duplicate-imports": "error", - "no-else-return": "off", - "no-empty-function": "off", - "no-eq-null": "off", - "no-eval": "error", - "no-extend-native": "error", - "no-extra-bind": "error", - "no-extra-label": "error", - "no-extra-parens": "off", - "no-floating-decimal": "off", - "no-implicit-coercion": [ - "error", + plugins: ['import', 'fp-ts'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:node/recommended', + 'plugin:prettier/recommended', + 'plugin:fp-ts/all', + 'plugin:import/typescript', + ], + settings: { + 'import/resolver': { + typescript: true, + node: { + extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'], + }, + }, + }, + rules: { + complexity: 'off', + curly: 'error', + 'default-case': 'off', + 'dot-notation': 'off', + eqeqeq: 'error', + 'guard-for-in': 'error', + 'id-match': 'error', + 'no-bitwise': 'error', + 'no-console': 'error', + 'no-eq-null': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-invalid-this': 'off', + 'no-lone-blocks': 'error', + 'no-native-reassign': 'error', + 'no-nested-ternary': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-param-reassign': 'error', + 'no-redeclare': 'off', + 'no-shadow': 'off', + 'no-undef-init': 'error', + 'no-unused-vars': 'off', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-var': 'error', + 'no-void': 'error', + 'new-cap': [ + 'error', { - "boolean": false, - "number": false, - "string": false - } + newIsCap: true, + capIsNew: false, + }, ], - "no-implicit-globals": "off", - "no-implied-eval": "error", - "no-inline-comments": "off", - "no-invalid-this": "off", - "no-iterator": "error", - "no-label-var": "error", - "no-labels": "error", - "no-lone-blocks": "off", - "no-lonely-if": "off", - "no-loop-func": "off", - "no-magic-numbers": "off", - "no-mixed-operators": "off", - "no-mixed-requires": "error", - "no-multi-assign": "off", - "no-multi-spaces": "off", - "no-multi-str": "error", - "no-multiple-empty-lines": "off", - "no-native-reassign": "error", - "no-negated-condition": "off", - "no-negated-in-lhs": "error", - "no-nested-ternary": "off", - "no-new": "error", - "no-new-func": "error", - "no-new-object": "error", - "no-new-require": "error", - "no-new-wrappers": "error", - "no-octal-escape": "error", - "no-param-reassign": "off", - "no-path-concat": "error", - "no-plusplus": "off", - "no-process-env": "error", - "no-process-exit": "off", - "no-proto": "off", - "no-prototype-builtins": "off", - "no-restricted-globals": "error", - "no-restricted-imports": "error", - "no-restricted-modules": "error", - "no-restricted-properties": "error", - "no-restricted-syntax": "error", - "no-return-assign": "off", - "no-return-await": "error", - "no-script-url": "error", - "no-self-assign": [ - "error", + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'prefer-rest-params': 'error', + 'prefer-template': 'error', + 'wrap-iife': ['error', 'inside'], + + 'node/no-unsupported-features/es-syntax': ['error', { ignores: ['modules'] }], + 'node/no-missing-import': 'off', + 'import/no-unresolved': 'error', + + '@typescript-eslint/dot-notation': 'error', + '@typescript-eslint/no-namespace': 'warn', + '@typescript-eslint/consistent-type-definitions': 'error', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/1071, + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-unused-vars': [ + 'warn', { - "props": false - } + argsIgnorePattern: '^_', + }, ], - "no-self-compare": "off", - "no-sequences": "off", - "no-shadow": "off", - "no-shadow-restricted-names": "error", - "no-spaced-func": "off", - "no-sync": "off", - "no-tabs": "off", - "no-template-curly-in-string": "error", - "no-ternary": "off", - "no-throw-literal": "off", - "no-trailing-spaces": "off", - "no-undef-init": "error", - "no-undefined": "off", - "no-underscore-dangle": "off", - "no-unmodified-loop-condition": "off", - "no-unneeded-ternary": "off", - "no-unused-expressions": "off", - "no-use-before-define": "off", - "no-useless-call": "off", - "no-useless-computed-key": "error", - "no-useless-concat": "off", - "no-useless-constructor": "error", - "no-useless-rename": "error", - "no-useless-return": "off", - "no-var": "off", - "no-void": "off", - "no-warning-comments": "off", - "no-whitespace-before-property": "error", - "no-with": "error", - "nonblock-statement-body-position": [ - "error", - "any" - ], - "object-curly-newline": "off", - "object-curly-spacing": "off", - "object-property-newline": "off", - "object-shorthand": "off", - "one-var": "off", - "one-var-declaration-per-line": "off", - "operator-assignment": "off", - "operator-linebreak": "off", - "padded-blocks": "off", - "padding-line-between-statements": "error", - "prefer-arrow-callback": "off", - "prefer-const": "error", - "prefer-destructuring": "off", - "prefer-numeric-literals": "error", - "prefer-object-spread": "off", - "prefer-promise-reject-errors": "error", - "prefer-reflect": "off", - "prefer-rest-params": "off", - "prefer-spread": "off", - "prefer-template": "off", - "quote-props": "off", - "quotes": "off", - "radix": "off", - "require-await": "error", - "require-jsdoc": "off", - "require-unicode-regexp": "off", - "rest-spread-spacing": "error", - "semi": "off", - "semi-spacing": "off", - "semi-style": "off", - "sort-imports": "error", - "sort-keys": "off", - "sort-vars": "off", - "space-before-blocks": "off", - "space-before-function-paren": "off", - "space-in-parens": "off", - "space-infix-ops": "off", - "space-unary-ops": "off", - "spaced-comment": "off", - "strict": "off", - "switch-colon-spacing": "off", - "symbol-description": "error", - "template-curly-spacing": "error", - "template-tag-spacing": "error", - "unicode-bom": [ - "error", - "never" - ], - "valid-jsdoc": "off", - "valid-typeof": [ - "error", + '@typescript-eslint/no-shadow': ['error', { hoist: 'all', ignoreTypeValueShadow: true }], + + 'prettier/prettier': 'error', + + 'import/no-duplicates': ['error', { 'prefer-inline': false }], + 'import/no-deprecated': 'off', // https://github.com/import-js/eslint-plugin-import/issues/1532 + 'import/no-unresolved': 'off', + 'import/export': 'off', + 'import/order': [ + 'error', { - "requireStringLiterals": false - } + groups: ['external', 'builtin', 'parent', 'sibling', 'index'], + pathGroups: [ + { + pattern: '*.scss', + group: 'parent', + position: 'after', + }, + ], + alphabetize: { + order: 'asc', + }, + }, ], - "vars-on-top": "off", - "wrap-iife": "off", - "wrap-regex": "off", - "yield-star-spacing": "error", - "yoda": "off" - } -}; \ No newline at end of file + + 'fp-ts/no-module-imports': 'off', + }, +} diff --git a/.gitignore b/.gitignore index a11727c23..b6372066b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ package-lock.json bash-unit-test-temp docker-compose.override.yml +dist/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..b9589803e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "printWidth": 120, + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true, + "endOfLine": "lf", + "arrowParens": "avoid" +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..72446f434 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4706a25e..38313ee8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,3 +37,29 @@ knowledge. If you're unfamiliar with GitHub and/or coding, [check out these othe See [OpenAPS.org](http://OpenAPS.org/) for background on the OpenAPS movement and project. +## Using VSCode devcontainer (with docker) + +This is the recommanded way in order to develop in a standard development environment without installing dependencies in your host system. + +[VSCode](https://code.visualstudio.com) should automatically recognize the [devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) setup, building the image for development environment. + +The default container timezone is `Europe/Rome`. +This allows to test dates timezone logic. + +### Setup + +Install dependencies and compile typescript files. + +``` +$ npm install && npm run build +``` + +## Build with docker + +If you just need to build the library, you can use docker compose: + +``` +$ docker compose run --rm node sh -c "npm install && npm run build" +``` + +The output directory of `lib/*` will be `dist/`. diff --git a/Makefile b/Makefile index 339ddc79c..0d80d68f1 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ all: test report: # report results to community test: - ./node_modules/.bin/mocha -c ${TESTS} + npm test travis: ${ISTANBUL} cover ${MOCHA} --include-all-sources true --report lcovonly -- -R tap ${TESTS} diff --git a/bin/ns-status.js b/bin/ns-status.js index 4d0543fe1..50d9ed3d1 100755 --- a/bin/ns-status.js +++ b/bin/ns-status.js @@ -5,7 +5,7 @@ var os = require("os"); var fs = require('fs'); var moment = require("moment"); -var requireUtils = require('../lib/require-utils'); +var requireUtils = require('../dist/require-utils'); var requireWithTimestamp = requireUtils.requireWithTimestamp; var safeLoadFile = requireUtils.safeLoadFile; diff --git a/bin/oref0-autosens-history.js b/bin/oref0-autosens-history.js index 94d3d0c5c..8d42e0771 100755 --- a/bin/oref0-autosens-history.js +++ b/bin/oref0-autosens-history.js @@ -16,8 +16,8 @@ THE SOFTWARE. */ -var basal = require('../lib/profile/basal'); -var detectSensitivity = require('../lib/determine-basal/autosens'); +var basal = require('../dist/profile/basal'); +var detectSensitivity = require('../dist/determine-basal/autosens'); if (!module.parent) { //var detectsensitivity = init(); // I don't see where this variable is used, so deleted it. @@ -179,4 +179,4 @@ function convertBasal(item) //"minutes": Math.round(item.timeAsSeconds / 60), "rate": item.value }; -} \ No newline at end of file +} diff --git a/bin/oref0-autotune-core.js b/bin/oref0-autotune-core.js index be51dae7b..8d90fc019 100755 --- a/bin/oref0-autotune-core.js +++ b/bin/oref0-autotune-core.js @@ -19,7 +19,7 @@ THE SOFTWARE. */ -var autotune = require('../lib/autotune'); +var autotune = require('../dist/autotune'); var stringify = require('json-stable-stringify'); if (!module.parent) { diff --git a/bin/oref0-autotune-prep.js b/bin/oref0-autotune-prep.js index 4c781cb5c..cf506ba3a 100755 --- a/bin/oref0-autotune-prep.js +++ b/bin/oref0-autotune-prep.js @@ -20,7 +20,7 @@ */ -var generate = require('../lib/autotune-prep'); +var generate = require('../dist/autotune-prep'); var _ = require('lodash'); var moment = require('moment'); diff --git a/bin/oref0-calculate-glucose-noise.js b/bin/oref0-calculate-glucose-noise.js index 796f956b6..50e01d541 100755 --- a/bin/oref0-calculate-glucose-noise.js +++ b/bin/oref0-calculate-glucose-noise.js @@ -16,7 +16,7 @@ */ -var generate = require('../lib/calc-glucose-stats').updateGlucoseStats; +var generate = require('../dist/calc-glucose-stats').updateGlucoseStats; function usage ( ) { console.log('usage: ', process.argv.slice(0, 2), ''); diff --git a/bin/oref0-calculate-iob.js b/bin/oref0-calculate-iob.js index b0723def9..1f07f48bd 100755 --- a/bin/oref0-calculate-iob.js +++ b/bin/oref0-calculate-iob.js @@ -18,7 +18,7 @@ */ -var generate = require('../lib/iob'); +var generate = require('../dist/iob'); var fs = require('fs'); function usage ( ) { console.log('usage: ', process.argv.slice(0, 2), ' [autosens.json] [pumphistory-24h-zoned.json]'); @@ -92,4 +92,4 @@ if (!module.parent) { console.log(result); } -exports = module.exports = oref0_calculate_iob \ No newline at end of file +exports = module.exports = oref0_calculate_iob diff --git a/bin/oref0-detect-sensitivity.js b/bin/oref0-detect-sensitivity.js index d2ddfe710..1d72065dd 100755 --- a/bin/oref0-detect-sensitivity.js +++ b/bin/oref0-detect-sensitivity.js @@ -14,7 +14,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var detectSensitivity = require('../lib/determine-basal/autosens'); +var detectSensitivity = require('../dist/determine-basal/autosens'); if (!module.parent) { var argv = require('yargs') diff --git a/bin/oref0-determine-basal.js b/bin/oref0-determine-basal.js index d6c93ee7d..a1d8e31ec 100755 --- a/bin/oref0-determine-basal.js +++ b/bin/oref0-determine-basal.js @@ -14,8 +14,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var getLastGlucose = require('../lib/glucose-get-last'); -var determine_basal = require('../lib/determine-basal/determine-basal'); +var getLastGlucose = require('../dist/glucose-get-last'); +var determine_basal = require('../dist/determine-basal/determine-basal'); /* istanbul ignore next */ if (!module.parent) { @@ -210,7 +210,7 @@ if (!module.parent) { //console.error(JSON.stringify(currenttemp)); //console.error(JSON.stringify(profile)); - var tempBasalFunctions = require('../lib/basal-set-temp'); + var tempBasalFunctions = require('../dist/basal-set-temp'); var rT = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, params['microbolus'], reservoir_data, currentTime); diff --git a/bin/oref0-find-insulin-uses.js b/bin/oref0-find-insulin-uses.js index 05eddf6d1..5423684cf 100755 --- a/bin/oref0-find-insulin-uses.js +++ b/bin/oref0-find-insulin-uses.js @@ -18,7 +18,7 @@ */ -var find_insulin = require('../lib/iob/history'); +var find_insulin = require('../dist/iob/history'); if (!module.parent) { var argv = require('yargs') diff --git a/bin/oref0-get-ns-entries.js b/bin/oref0-get-ns-entries.js index 4932aac31..e0b5496d6 100755 --- a/bin/oref0-get-ns-entries.js +++ b/bin/oref0-get-ns-entries.js @@ -26,7 +26,7 @@ var request = require('request'); var _ = require('lodash'); var fs = require('fs'); var network = require('network'); -var shared_node = require('./oref0-shared-node-utils'); +var shared_node = require('../dist/bin/utils'); var console_error = shared_node.console_error; var console_log = shared_node.console_log; var initFinalResults = shared_node.initFinalResults; diff --git a/bin/oref0-get-profile.js b/bin/oref0-get-profile.js index 0f08ff1f9..82f8e57e8 100755 --- a/bin/oref0-get-profile.js +++ b/bin/oref0-get-profile.js @@ -18,8 +18,8 @@ */ var fs = require('fs'); -var generate = require('../lib/profile/'); -var shared_node_utils = require('./oref0-shared-node-utils'); +var generate = require('../dist/profile/'); +var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; var process_exit = shared_node_utils.process_exit; diff --git a/bin/oref0-meal.js b/bin/oref0-meal.js index 50ad3d1d0..79f9dbeb9 100755 --- a/bin/oref0-meal.js +++ b/bin/oref0-meal.js @@ -20,8 +20,8 @@ */ -var generate = require('../lib/meal'); -var shared_node_utils = require('./oref0-shared-node-utils'); +var generate = require('../dist/meal'); +var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; var process_exit = shared_node_utils.process_exit; @@ -151,4 +151,4 @@ if (!module.parent) { process.exit(final_result.return_val); } -exports = module.exports = oref0_meal \ No newline at end of file +exports = module.exports = oref0_meal diff --git a/bin/oref0-normalize-temps.js b/bin/oref0-normalize-temps.js index 2acdb6f70..f720390f2 100755 --- a/bin/oref0-normalize-temps.js +++ b/bin/oref0-normalize-temps.js @@ -15,9 +15,9 @@ */ -var find_insulin = require('../lib/temps'); -var find_bolus = require('../lib/bolus'); -var describe_pump = require('../lib/pump'); +var find_insulin = require('../dist/temps'); +var find_bolus = require('../dist/bolus'); +var describe_pump = require('../dist/pump'); var fs = require('fs'); diff --git a/bin/oref0-raw.js b/bin/oref0-raw.js index fa4530410..e9fc1d5aa 100755 --- a/bin/oref0-raw.js +++ b/bin/oref0-raw.js @@ -1,8 +1,8 @@ #!/usr/bin/env node 'use strict'; -var safeRequire = require('../lib/require-utils').safeRequire; -var withRawGlucose = require('../lib/with-raw-glucose'); +var safeRequire = require('../dist/require-utils').safeRequire; +var withRawGlucose = require('../dist/with-raw-glucose'); /* Fills CGM data doesn't already contain an EVG, if we have unfiltered, filtered, and a cal diff --git a/bin/oref0-shared-node.js b/bin/oref0-shared-node.js index 2110cc737..ce1841a97 100644 --- a/bin/oref0-shared-node.js +++ b/bin/oref0-shared-node.js @@ -10,8 +10,8 @@ var oref0_meal = require("./oref0-meal"); var oref0_get_profile = require("./oref0-get-profile"); var oref0_get_ns_entries = require("./oref0-get-ns-entries"); var fs = require('fs'); -var requireUtils = require('../lib/require-utils'); -var shared_node_utils = require('./oref0-shared-node-utils'); +var requireUtils = require('../dist/require-utils'); +var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; var initFinalResults = shared_node_utils.initFinalResults; diff --git a/bin/oref0-upgrade.sh b/bin/oref0-upgrade.sh index 7134951f5..7e3d3e73d 100755 --- a/bin/oref0-upgrade.sh +++ b/bin/oref0-upgrade.sh @@ -16,5 +16,5 @@ verify_installed socat verify_installed ntp if [ ! -e /usr/local/bin/oref0-shared-node-loop ] ; then - ln -s ../lib/node_modules/oref0/bin/oref0-shared-node-loop.sh /usr/local/bin/oref0-shared-node-loop + ln -s ../dist/node_modules/oref0/bin/oref0-shared-node-loop.sh /usr/local/bin/oref0-shared-node-loop fi diff --git a/docker-compose.override.yml.dist b/docker-compose.override.yml.dist deleted file mode 100644 index 3eb667497..000000000 --- a/docker-compose.override.yml.dist +++ /dev/null @@ -1,2 +0,0 @@ -services: - node: {} diff --git a/docker/node/Dockerfile b/docker/node/Dockerfile index 837890e52..ba4065250 100644 --- a/docker/node/Dockerfile +++ b/docker/node/Dockerfile @@ -5,3 +5,4 @@ RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master && chsh -s $(which zsh) COPY .zshrc /root/.zshrc RUN corepack enable +ENV TZ='Europe/Rome' diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 000000000..afdb7a7ac --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,24 @@ +import type { Config } from 'jest'; + +const config: Config = { + verbose: true, + //preset: "ts-jest/presets/js-with-ts", + preset: "ts-jest/presets/default", + collectCoverage: true, + testEnvironment: "node", + testMatch: [ + //"**/*.test.(ts|js)" + "**/*.test.ts" + ], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: "./tsconfig.test.json", + diagnostics: false + } + ], + }, +} + +export default config; diff --git a/lib/autotune-prep/categorize.js b/lib/autotune-prep/categorize.js index 4166bb5d0..c6d7e6e01 100644 --- a/lib/autotune-prep/categorize.js +++ b/lib/autotune-prep/categorize.js @@ -1,11 +1,12 @@ 'use strict'; -var tz = require('moment-timezone'); var basal = require('../profile/basal'); var getIOB = require('../iob'); var ISF = require('../profile/isf'); var find_insulin = require('../iob/history'); var dosed = require('./dosed'); +var date = require('../date'); +var tz = date.tz; // main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals. diff --git a/lib/autotune/index.js b/lib/autotune/index.ts similarity index 94% rename from lib/autotune/index.js rename to lib/autotune/index.ts index 5b165c919..86e3e2b40 100644 --- a/lib/autotune/index.js +++ b/lib/autotune/index.ts @@ -2,7 +2,7 @@ var percentile = require('../percentile') // does three things - tunes basals, ISF, and CSF -function tuneAllTheThings (inputs) { +function tuneAllTheThings (inputs: any) { var previousAutotune = inputs.previousAutotune; //console.error(previousAutotune); @@ -29,16 +29,19 @@ function tuneAllTheThings (inputs) { } //console.error(DIA, peak); - // conditional on there being a pump profile; if not then skip - if (pumpProfile) { var pumpISFProfile = pumpProfile.isfProfile; } - if (pumpISFProfile && pumpISFProfile.sensitivities[0]) { - var pumpISF = pumpISFProfile.sensitivities[0].sensitivity; - var pumpCarbRatio = pumpProfile.carb_ratio; - var pumpCSF = pumpISF / pumpCarbRatio; + var pumpISF: any + var pumpCSF: any + var pumpCarbRatio: any + + if (pumpProfile && pumpProfile.isfProfile) { + pumpISF = pumpProfile.isfProfile.sensitivities[0].sensitivity + pumpCarbRatio = pumpProfile.carb_ratio + pumpCSF = pumpISF / pumpCarbRatio + if (! carbRatio) { carbRatio = pumpCarbRatio; } + if (! CSF) { CSF = pumpCSF; } + if (! ISF) { ISF = pumpISF; } } - if (! carbRatio) { carbRatio = pumpCarbRatio; } - if (! CSF) { CSF = pumpCSF; } - if (! ISF) { ISF = pumpISF; } + //console.error(CSF); var preppedGlucose = inputs.preppedGlucose; var CSFGlucose = preppedGlucose.CSFGlucoseData; @@ -148,13 +151,13 @@ function tuneAllTheThings (inputs) { var CRTotalCarbs = 0; var CRTotalInsulin = 0; - CRData.forEach(function(CRDatum) { + CRData.forEach(function(CRDatum: any) { var CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG; var CRInsulinReq = CRBGChange / ISF; - var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB; + //var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB; CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq; //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRDatum.CRInsulinTotal); - var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000; + //var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000; //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRDatum.CRInsulinTotal); //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulin,"CRDatum.CRInsulinTotal:",CRDatum.CRInsulinTotal,"CR:",CR); if (CRDatum.CRInsulinTotal > 0) { @@ -209,7 +212,7 @@ function tuneAllTheThings (inputs) { // look at net deviations for each hour for (var hour=0; hour < 24; hour++) { - var deviations = 0; + let deviations = 0; for (i=0; i < basalGlucose.length; ++i) { var BGTime; @@ -221,6 +224,7 @@ function tuneAllTheThings (inputs) { BGTime = new Date(basalGlucose[i].dateString); } else { console.error("Could not determine last BG time"); + continue; } var myHour = BGTime.getHours(); @@ -264,19 +268,20 @@ function tuneAllTheThings (inputs) { } } } + + var autotuneMin: number = 0.7 + var autotuneMax: number = 1.2 + if (pumpBasalProfile && pumpBasalProfile[0]) { for (hour=0; hour < 24; hour++) { //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); // cap adjustments at autosens_max and autosens_min + if (typeof pumpProfile.autosens_max !== 'undefined') { - var autotuneMax = pumpProfile.autosens_max; - } else { - var autotuneMax = 1.2; + autotuneMax = pumpProfile.autosens_max; } if (typeof pumpProfile.autosens_min !== 'undefined') { - var autotuneMin = pumpProfile.autosens_min; - } else { - var autotuneMin = 0.7; + autotuneMin = pumpProfile.autosens_min; } var maxRate = hourlyPumpProfile[hour].rate * autotuneMax; var minRate = hourlyPumpProfile[hour].rate * autotuneMin; @@ -352,7 +357,7 @@ function tuneAllTheThings (inputs) { deviations += parseFloat(CSFGlucose[i].deviation); // compare the sum of deviations from start to end vs. current CSF * mealCarbs //console.error(CSF,mealCarbs); - var csfRise = CSF * mealCarbs; + //var csfRise = CSF * mealCarbs; //console.error(deviations,ISF); //console.error("csfRise:",csfRise,"deviations:",deviations); totalMealCarbs += mealCarbs; @@ -444,13 +449,13 @@ function tuneAllTheThings (inputs) { // calculate median deviation and bgi in data attributable to ISF - var deviations = []; + let deviationsArray: number[] = []; var BGIs = []; var avgDeltas = []; var ratios = []; for (i=0; i < ISFGlucose.length; ++i) { - deviation = parseFloat(ISFGlucose[i].deviation); - deviations.push(deviation); + var deviation = parseFloat(ISFGlucose[i].deviation); + deviationsArray.push(deviation); var BGI = parseFloat(ISFGlucose[i].BGI); BGIs.push(BGI); var avgDelta = parseFloat(ISFGlucose[i].avgDelta); @@ -459,11 +464,11 @@ function tuneAllTheThings (inputs) { //console.error("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio); ratios.push(ratio); } - avgDeltas.sort(function(a, b){return a-b}); - BGIs.sort(function(a, b){return a-b}); - deviations.sort(function(a, b){return a-b}); - ratios.sort(function(a, b){return a-b}); - var p50deviation = percentile(deviations, 0.50); + avgDeltas.sort(function(a: number, b: number){return a-b}); + BGIs.sort(function(a: number, b: number){return a-b}); + deviationsArray.sort(function(a: number, b: number){return a-b}); + ratios.sort(function(a: number, b: number){return a-b}); + var p50deviation = percentile(deviationsArray, 0.50); var p50BGI = percentile(BGIs, 0.50); var p50ratios = Math.round( percentile(ratios, 0.50) * 1000)/1000; var fullNewISF = ISF; @@ -489,6 +494,7 @@ function tuneAllTheThings (inputs) { var maxISF = pumpISF / autotuneMin; // high autosens ratio = low ISF var minISF = pumpISF / autotuneMax; + var newISF = ISF; if (typeof(pumpISF) !== 'undefined') { if ( fullNewISF < 0 ) { var adjustedISF = ISF; @@ -506,7 +512,7 @@ function tuneAllTheThings (inputs) { } // and apply 20% of that adjustment - var newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF ); + newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF ); if (newISF > maxISF) { console.error("Limiting ISF of",newISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")"); diff --git a/lib/basal-set-temp.js b/lib/basal-set-temp.ts similarity index 66% rename from lib/basal-set-temp.js rename to lib/basal-set-temp.ts index 3037243f4..fd38cb7a2 100644 --- a/lib/basal-set-temp.js +++ b/lib/basal-set-temp.ts @@ -1,24 +1,33 @@ -'use strict'; +import { Profile } from "./types/Profile"; -function reason(rT, msg) { +interface RT { + reason?: string + duration?: number + rate?: number +} + +interface Temp { + duration: number + rate: number +} + +function reason(rT: RT, msg: string) { rT.reason = (rT.reason ? rT.reason + '. ' : '') + msg; console.error(msg); } -var tempBasalFunctions = {}; - -tempBasalFunctions.getMaxSafeBasal = function getMaxSafeBasal(profile) { +export function getMaxSafeBasal(profile: Profile) { - var max_daily_safety_multiplier = (isNaN(profile.max_daily_safety_multiplier) || profile.max_daily_safety_multiplier === null) ? 3 : profile.max_daily_safety_multiplier; - var current_basal_safety_multiplier = (isNaN(profile.current_basal_safety_multiplier) || profile.current_basal_safety_multiplier === null) ? 4 : profile.current_basal_safety_multiplier; + var max_daily_safety_multiplier = profile.max_daily_safety_multiplier || 3; + var current_basal_safety_multiplier = profile.current_basal_safety_multiplier || 4; - return Math.min(profile.max_basal, max_daily_safety_multiplier * profile.max_daily_basal, current_basal_safety_multiplier * profile.current_basal); + return Math.min(profile.max_basal || 0, max_daily_safety_multiplier * (profile.max_daily_basal || 0), current_basal_safety_multiplier * (profile.current_basal || 0)); }; -tempBasalFunctions.setTempBasal = function setTempBasal(rate, duration, profile, rT, currenttemp) { +export function setTempBasal(rate: number, duration: number, profile: Profile, rT: RT, currenttemp?: Temp) { //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); - var maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile); + var maxSafeBasal = getMaxSafeBasal(profile); var round_basal = require('./round-basal'); if (rate < 0) { @@ -57,4 +66,7 @@ tempBasalFunctions.setTempBasal = function setTempBasal(rate, duration, profile, } }; -module.exports = tempBasalFunctions; +module.exports = { + getMaxSafeBasal, + setTempBasal +}; diff --git a/bin/oref0-shared-node-utils.js b/lib/bin/utils.js similarity index 100% rename from bin/oref0-shared-node-utils.js rename to lib/bin/utils.js diff --git a/lib/calc-glucose-stats.js b/lib/calc-glucose-stats.ts similarity index 53% rename from lib/calc-glucose-stats.js rename to lib/calc-glucose-stats.ts index 0b1b3694e..fcc6b754d 100644 --- a/lib/calc-glucose-stats.js +++ b/lib/calc-glucose-stats.ts @@ -1,15 +1,17 @@ -const moment = require('moment'); -const _ = require('lodash'); -const stats = require('./glucose-stats'); +import * as stats from './glucose-stats' -module.exports = {}; -const calcStatsExports = module.exports; +interface Options { + glucose_hist: any[] +} -calcStatsExports.updateGlucoseStats = (options) => { - var hist = _.map(_.sortBy(options.glucose_hist, 'dateString'), function readDate(value) { - value.readDateMills = moment(value.dateString).valueOf(); - return value; - }); +export function updateGlucoseStats(options: Options) { + const hist = (options.glucose_hist || []) + .map(value => ({ + ...value, + // @todo: in tests, dateString is undefined + readDateMills: value.dateString ? new Date(value.dateString).getTime() : Date.now(), + })) + .sort((a, b) => a.readDateMills - b.readDateMills) if (hist && hist.length > 0) { var noise_val = stats.calcSensorNoise(null, hist, null, null); @@ -28,3 +30,7 @@ calcStatsExports.updateGlucoseStats = (options) => { return options.glucose_hist; }; + +exports = module.exports = { + updateGlucoseStats, +} diff --git a/lib/date.ts b/lib/date.ts new file mode 100644 index 000000000..e8ee53648 --- /dev/null +++ b/lib/date.ts @@ -0,0 +1,24 @@ +export const tz = (a: Date | string): Date => { + return new Date(new Date(a).toISOString()); +} + + +export const format = (a: Date) => { + const newDate = new Date(a) + const absOffset = Math.abs(a.getTimezoneOffset()) + const hours = Math.ceil(absOffset / 60) + const minutes = absOffset - (hours * 60) + const sign = a.getTimezoneOffset() < 0 ? '+' : '-' + newDate.setMinutes(a.getMinutes() - a.getTimezoneOffset()) + + return newDate.toISOString().substring(0, 19) + + sign + + hours.toString().padStart(2, '0') + + ':' + + minutes.toString().padStart(2, '0') +} + +exports = exports.default = { + tz: tz, + format, +} diff --git a/lib/determine-basal/autosens.js b/lib/determine-basal/autosens.js index 6a8b89a41..10f31c087 100644 --- a/lib/determine-basal/autosens.js +++ b/lib/determine-basal/autosens.js @@ -5,8 +5,9 @@ var get_iob = require('../iob'); var find_insulin = require('../iob/history'); var isf = require('../profile/isf'); var find_meals = require('../meal/history'); -var tz = require('moment-timezone'); var percentile = require('../percentile'); +var date = require('../date'); +var tz = date.tz; function detectSensitivity(inputs) { diff --git a/lib/determine-basal/determine-basal.ts b/lib/determine-basal/determine-basal.ts index 170e790ef..bcdabead4 100644 --- a/lib/determine-basal/determine-basal.ts +++ b/lib/determine-basal/determine-basal.ts @@ -217,8 +217,6 @@ var determine_basal = function determine_basal( deliverAt = currentTime; } - console.log({ deliverAt, currentTime }) - if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { rT.error ='Error: could not get current basal rate'; return rT; diff --git a/lib/glucose-get-last.js b/lib/glucose-get-last.ts similarity index 68% rename from lib/glucose-get-last.js rename to lib/glucose-get-last.ts index bca3bfbc0..f34fd44a0 100644 --- a/lib/glucose-get-last.js +++ b/lib/glucose-get-last.ts @@ -1,25 +1,33 @@ -function getDateFromEntry(entry) { - return entry.date || Date.parse(entry.display_time) || Date.parse(entry.dateString); +import { GlucoseEntry } from "./types/GlucoseEntry"; + +function getDateFromEntry(entry: GlucoseEntry) { + if (entry.date) { + return entry.date + } else if (entry.display_time) { + return Date.parse(entry.display_time) + } else if (entry.dateString) { + return Date.parse(entry.dateString) + } + + throw new TypeError('Unable to find a date in GlucoseEntry') } -var getLastGlucose = function (data) { - data = data.filter(function(obj) { - return obj.glucose || obj.sgv; - }).map(function prepGlucose (obj) { - //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv; - if ( obj.glucose !== null ) { - return obj; - } - }); +var getLastGlucose = function (input: GlucoseEntry[]) { + const data = input.reduce( + (b, a) => { + const glucose = a.glucose || a.sgv + return glucose ? [...b, { ...a, glucose }] : b + }, + [] as (GlucoseEntry & { glucose: number })[] + ) - var now = data[0]; - var now_date = getDateFromEntry(now); - var change; - var last_deltas = []; - var short_deltas = []; - var long_deltas = []; - var last_cal = 0; + let now = data[0]; + let now_date = getDateFromEntry(now); + let change; + let last_deltas = []; + let short_deltas = []; + let long_deltas = []; + let last_cal = 0; //console.error(now.glucose); for (var i=1; i < data.length; i++) { @@ -30,16 +38,19 @@ var getLastGlucose = function (data) { } // only use data from the same device as the most recent BG data point if (typeof data[i] !== 'undefined' && data[i].glucose > 38 && data[i].device === now.device) { - var then = data[i]; - var then_date = getDateFromEntry(then); - var avgdelta = 0; - var minutesago; + let then = data[i]; + let then_date = getDateFromEntry(then); + let avgdelta = 0; + let minutesago; if (typeof then_date !== 'undefined' && typeof now_date !== 'undefined') { minutesago = Math.round( (now_date - then_date) / (1000 * 60) ); // multiply by 5 to get the same units as delta, i.e. mg/dL/5m change = now.glucose - then.glucose; avgdelta = change/minutesago * 5; - } else { console.error("Error: date field not found: cannot calculate avgdelta"); } + } else { + console.error("Error: date field not found: cannot calculate avgdelta"); + continue; + } //if (i < 5) { //console.error(then.glucose, minutesago, avgdelta); //} @@ -63,9 +74,9 @@ var getLastGlucose = function (data) { } } } - var last_delta = 0; - var short_avgdelta = 0; - var long_avgdelta = 0; + let last_delta = 0; + let short_avgdelta = 0; + let long_avgdelta = 0; if (last_deltas.length > 0) { last_delta = last_deltas.reduce(function(a, b) { return a + b; }) / last_deltas.length; } @@ -79,7 +90,7 @@ var getLastGlucose = function (data) { return { delta: Math.round( last_delta * 100 ) / 100 , glucose: Math.round( now.glucose * 100 ) / 100 - , noise: Math.round(now.noise) + , noise: Math.round(now.noise || 0) , short_avgdelta: Math.round( short_avgdelta * 100 ) / 100 , long_avgdelta: Math.round( long_avgdelta * 100 ) / 100 , date: now_date @@ -88,4 +99,5 @@ var getLastGlucose = function (data) { }; }; +export default getLastGlucose module.exports = getLastGlucose; diff --git a/lib/glucose-stats.js b/lib/glucose-stats.ts similarity index 85% rename from lib/glucose-stats.js rename to lib/glucose-stats.ts index 71563d628..fa1cd1410 100644 --- a/lib/glucose-stats.js +++ b/lib/glucose-stats.ts @@ -1,16 +1,3 @@ - - -const moment = require('moment'); - -const log = console.error; - -/* eslint-disable-next-line no-unused-vars */ -const error = console.error; -const debug = console.error; - -module.exports = {}; -const calcStatsExports = module.exports; - // Calculate the sum of the distance of all points (sod) // Calculate the overall distance between the first and the last point (overallDistance) // Calculate the noise as the following formula: 1 - sod / overallDistance @@ -26,8 +13,11 @@ const calcStatsExports = module.exports; // real glucose -- glucose value in mg/dL // real readDate -- milliseconds since Epoch // },... + +import { updateGlucoseStats } from "./calc-glucose-stats"; + // ] -const calcNoise = (sgvArr) => { +const calcNoise = (sgvArr: any) => { let noise = 0; const n = sgvArr.length; @@ -80,7 +70,7 @@ const calcNoise = (sgvArr) => { return noise; }; -calcStatsExports.calcSensorNoise = (calcGlucose, glucoseHist, lastCal, sgv) => { +export function calcSensorNoise(calcGlucose: any, glucoseHist: any, lastCal: any, sgv: any) { const MAXRECORDS = 8; const MINRECORDS = 4; const sgvArr = []; @@ -125,7 +115,7 @@ calcStatsExports.calcSensorNoise = (calcGlucose, glucoseHist, lastCal, sgv) => { }; // Return 10 minute trend total -calcStatsExports.calcTrend = (calcGlucose, glucoseHist, lastCal, sgv) => { +export function calcTrend(calcGlucose: any, glucoseHist: any, lastCal: any, sgv: any) { let sgvHist = null; let trend = 0; @@ -134,13 +124,14 @@ calcStatsExports.calcTrend = (calcGlucose, glucoseHist, lastCal, sgv) => { let maxDate = null; let timeSpan = 0; let totalDelta = 0; - const currentTime = sgv ? moment(sgv.readDateMills) - : moment(glucoseHist[glucoseHist.length - 1].readDateMills); + const currentTime = new Date( + sgv ? sgv.readDateMills : glucoseHist[glucoseHist.length - 1].readDateMills + ); sgvHist = []; // delete any deltas > 16 minutes and any that don't have an unfiltered value (backfill records) - let minDate = currentTime.valueOf() - 16 * 60 * 1000; + let minDate = currentTime.getTime() - 16 * 60 * 1000; for (let i = 0; i < glucoseHist.length; i += 1) { if (lastCal && (glucoseHist[i].readDateMills >= minDate) && ('unfiltered' in glucoseHist[i]) && (glucoseHist[i].unfiltered > 100)) { sgvHist.push({ @@ -183,14 +174,14 @@ calcStatsExports.calcTrend = (calcGlucose, glucoseHist, lastCal, sgv) => { trend = 10 * totalDelta / timeSpan; } } else { - debug(`Not enough history for trend calculation: ${glucoseHist.length}`); + console.error(`Not enough history for trend calculation: ${glucoseHist.length}`); } return trend; }; // Return sensor noise -calcStatsExports.calcNSNoise = (noise, glucoseHist) => { +export function calcNSNoise(noise: number, glucoseHist: any) { let nsNoise = 0; // Unknown const currSGV = glucoseHist[glucoseHist.length - 1]; let deltaSGV = 0; @@ -206,15 +197,15 @@ calcStatsExports.calcNSNoise = (noise, glucoseHist) => { if (!currSGV) { nsNoise = 1; } else if (currSGV.glucose > 400) { - log(`Glucose ${currSGV.glucose} > 400 - setting noise level Heavy`); + console.error(`Glucose ${currSGV.glucose} > 400 - setting noise level Heavy`); nsNoise = 4; } else if (currSGV.glucose < 40) { - log(`Glucose ${currSGV.glucose} < 40 - setting noise level Light`); + console.error(`Glucose ${currSGV.glucose} < 40 - setting noise level Light`); nsNoise = 2; } else if (Math.abs(deltaSGV) > 30) { // This is OK even during a calibration jump because we don't want OpenAPS to be too // agressive with the "false" trend implied by a large positive jump - log(`Glucose change ${deltaSGV} out of range [-30, 30] - setting noise level Heavy`); + console.error(`Glucose change ${deltaSGV} out of range [-30, 30] - setting noise level Heavy`); nsNoise = 4; } else if (noise < 0.35) { nsNoise = 1; // Clean @@ -229,7 +220,7 @@ calcStatsExports.calcNSNoise = (noise, glucoseHist) => { return nsNoise; }; -calcStatsExports.NSNoiseString = (nsNoise) => { +export function NSNoiseString(nsNoise: any) { switch (nsNoise) { case 1: return 'Clean'; @@ -244,3 +235,11 @@ calcStatsExports.NSNoiseString = (nsNoise) => { return 'Unknown'; } }; + +exports = module.exports = { + updateGlucoseStats, + calcSensorNoise, + calcTrend, + calcNSNoise, + NSNoiseString, +} diff --git a/lib/iob/history.ts b/lib/iob/history.ts index bbe4f7b0d..834e7b41b 100644 --- a/lib/iob/history.ts +++ b/lib/iob/history.ts @@ -1,6 +1,6 @@ import * as basalprofile from '../profile/basal' import { Profile } from '../types/Profile'; -import { toLocalDate } from '../date'; +import { tz } from '../date'; import { PumpHistoryEvent } from '../types/PumpHistoryEvent'; import { NightscoutTreatment } from '../types/NightscoutTreatment'; import * as t from 'io-ts' @@ -140,7 +140,7 @@ function splitAroundSuspends (currentEvent: BasalTreatment, pumpSuspends: PumpSu } else { currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000; - currentEvent.started_at = toLocalDate(firstResumeStarted); + currentEvent.started_at = tz(firstResumeStarted); currentEvent.date = firstResumeDate } } @@ -175,7 +175,7 @@ function splitAroundSuspends (currentEvent: BasalTreatment, pumpSuspends: PumpSu events.push({ ...events[j], timestamp: date.format(event2StartDate), - started_at: toLocalDate(event2StartDate), + started_at: tz(event2StartDate), date: suspend.date+suspend.duration*60*1000, duration: ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000, }); @@ -192,7 +192,7 @@ function splitAroundSuspends (currentEvent: BasalTreatment, pumpSuspends: PumpSu eventStartDate.setMinutes(eventStartDate.getMinutes() + suspend.duration); events[j].timestamp = date.format(eventStartDate); - events[j].started_at = toLocalDate(new Date(events[j].timestamp)); + events[j].started_at = tz(new Date(events[j].timestamp)); events[j].date = suspend.date + suspend.duration*60*1000; } } @@ -216,7 +216,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu var currentlySuspended = false; // @todo: check if clock can be undefined - var now = toLocalDate(inputs.clock ? new Date(inputs.clock) : new Date()); + var now = tz(inputs.clock ? new Date(inputs.clock) : new Date()); var lastRecordTime = now; @@ -228,7 +228,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu continue; } - const started_at = toLocalDate(new Date(current.timestamp)) + const started_at = tz(new Date(current.timestamp)) const temp = { timestamp: current.timestamp, started_at, @@ -310,7 +310,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu } const timestamp = NightscoutTreatment.is(current) ? current.created_at : current.timestamp - var currentRecordTime = toLocalDate(new Date(timestamp)) + var currentRecordTime = tz(new Date(timestamp)) //console.error(current); //console.error(currentRecordTime,lastRecordTime); // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes) @@ -322,7 +322,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu lastRecordTime = currentRecordTime; } if (PumpHistoryEvent.is(current) && current._type === "Bolus") { - const started_at = toLocalDate(new Date(current.timestamp)); + const started_at = tz(new Date(current.timestamp)); if (started_at > now) { //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at); process.stderr.write(" "+current.amount+"U @ "+started_at); @@ -338,7 +338,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu } else if (NightscoutTreatment.is(current) && (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard")) { //imports treatments entered through Nightscout Care Portal //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard - const started_at = toLocalDate(new Date(current.created_at)) + const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ timestamp: current.created_at, @@ -347,7 +347,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu insulin: current.insulin! }); } else if (NightscoutTreatment.is(current) && current.enteredBy === "xdrip") { - const started_at = toLocalDate(new Date(current.created_at)) + const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ timestamp: current.created_at, @@ -356,7 +356,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu insulin: current.insulin! }); } else if (NightscoutTreatment.is(current) && current.enteredBy ==="HAPP_App" && current.insulin) { - const started_at = toLocalDate(new Date(current.created_at)) + const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ timestamp: current.created_at, @@ -365,7 +365,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu insulin: current.insulin! }); } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) { - const started_at = toLocalDate(new Date(current.created_at)) + const started_at = tz(new Date(current.created_at)) // @todo check for undefined rate and duration tempHistory.push({ timestamp: current.created_at, @@ -375,7 +375,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu duration: current.duration!, }); } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal") { - const started_at = toLocalDate(new Date(current.created_at)) + const started_at = tz(new Date(current.created_at)) let rate = current.rate // Loop reports the amount of insulin actually delivered while the temp basal was running // use that to calculate the effective temp basal rate @@ -414,7 +414,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu } } - const started_at = toLocalDate(new Date(current.timestamp)) + const started_at = tz(new Date(current.timestamp)) // @todo check for undefined rate and duration tempHistory.push({ timestamp: current.timestamp, @@ -506,7 +506,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu var suspendStart = new Date(max_dia_ago); var suspendStartDate = suspendStart.getTime() - var started_at = toLocalDate(suspendStart); + var started_at = tz(suspendStart); zTempSuspendBasals.push({ rate: 0, @@ -521,7 +521,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu // @todo check why lastSuspendTime can be undefined var suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date(); var suspendStartDate = suspendStart.getTime() - var started_at = toLocalDate(suspendStart); + var started_at = tz(suspendStart); // @todo check why lastSuspendTime can be undefined zTempSuspendBasals.push({ diff --git a/lib/iob/index.ts b/lib/iob/index.ts index 27116fd63..a6073a8fe 100644 --- a/lib/iob/index.ts +++ b/lib/iob/index.ts @@ -1,7 +1,7 @@ import find_insulin from './history' import type { Input } from './history' import sum from './total' -import { toLocalDate } from '../date'; +import { tz } from '../date'; import { InsulinTreatment, isBasalTreatment, isBolusTreatment } from './InsulinTreatment'; interface IOB { @@ -54,7 +54,7 @@ export default function generate (inputs: Input, currentIOBOnly: boolean = false if (! /(Z|[+-][0-2][0-9]:?[034][05])+/.test(inputs.clock) ) { console.error("Warning: clock input " + inputs.clock + " is unzoned; please pass clock-zoned.json instead"); } - var clock = toLocalDate(new Date(inputs.clock)); + var clock = tz(new Date(inputs.clock)); var lastBolusTime = new Date(0).getTime(); //clock.getTime()); var lastTemp = { diff --git a/lib/meal/MealTreatment.ts b/lib/meal/MealTreatment.ts new file mode 100644 index 000000000..53e687d82 --- /dev/null +++ b/lib/meal/MealTreatment.ts @@ -0,0 +1,8 @@ +export interface MealTreatment { + timestamp: string + carbs: number + nsCarbs: number + bwCarbs: number + bolus: number + journalCarbs: number +} diff --git a/lib/meal/history.js b/lib/meal/history.js deleted file mode 100644 index 62d8628c0..000000000 --- a/lib/meal/history.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; - -function arrayHasElementWithSameTimestampAndProperty(array,t,propname) { - for (var j=0; j < array.length; j++) { - var element = array[j]; - if (element.timestamp === t && element[propname] !== undefined) return true; - if ( element[propname] !== undefined ) { - var eDate = new Date(element.timestamp); - var tDate = new Date(t); - var tMin = new Date(tDate.getTime() - 2000); - var tMax = new Date(tDate.getTime() + 2000); - //console.error(tDate, tMin, tMax); - if (eDate > tMin && eDate < tMax) return true; - } - } - return false; -} - -function findMealInputs (inputs) { - var pumpHistory = inputs.history; - var carbHistory = inputs.carbs; - var profile_data = inputs.profile; - var mealInputs = []; - var bolusWizardInputs = []; - var duplicates = 0; - - for (var i=0; i < carbHistory.length; i++) { - var current = carbHistory[i]; - if (current.carbs && current.created_at) { - var temp = {}; - temp.timestamp = current.created_at; - temp.carbs = current.carbs; - temp.nsCarbs = current.carbs; - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.created_at,"carbs")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } - } - - for (i=0; i < pumpHistory.length; i++) { - current = pumpHistory[i]; - if (current._type === "Bolus" && current.timestamp) { - //console.log(pumpHistory[i]); - temp = {}; - temp.timestamp = current.timestamp; - temp.bolus = current.amount; - - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"bolus")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } else if (current._type === "BolusWizard" && current.timestamp) { - // Delay process the BolusWizard entries to make sure we've seen all possible that correspond to the bolus wizard. - // More specifically, we need to make sure we process the corresponding bolus entry first. - bolusWizardInputs.push(current); - - } else if ((current._type === "Meal Bolus" || current._type === "Correction Bolus" || current._type === "Snack Bolus" || current._type === "Bolus Wizard" || current._type === "Carb Correction") && current.created_at) { - //imports carbs entered through Nightscout Care Portal - //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard - temp = {}; - temp.timestamp = current.created_at; - temp.carbs = current.carbs; - temp.nsCarbs = current.carbs; - // don't enter the treatment if there's another treatment with the same exact timestamp - // to prevent duped carb entries from multiple sources - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.created_at,"carbs")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } else if (current.enteredBy === "xdrip") { - temp = {}; - temp.timestamp = current.created_at; - temp.carbs = current.carbs; - temp.nsCarbs = current.carbs; - temp.bolus = current.insulin; - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } else if (current.carbs > 0) { - temp = {}; - temp.carbs = current.carbs; - temp.nsCarbs = current.carbs; - temp.timestamp = current.created_at; - temp.bolus = current.insulin; - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } else if (current._type === "JournalEntryMealMarker" && current.carb_input > 0 && current.timestamp) { - temp = {}; - temp.timestamp = current.timestamp; - temp.carbs = current.carb_input; - temp.journalCarbs = current.carb_input; - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) { - mealInputs.push(temp); - } else { - duplicates += 1; - } - } - } - - for(i=0; i < bolusWizardInputs.length; i++) { - current = bolusWizardInputs[i]; - //console.log(bolusWizardInputs[i]); - temp = {}; - temp.timestamp = current.timestamp; - temp.carbs = current.carb_input; - temp.bwCarbs = current.carb_input; - - // don't enter the treatment if there's another treatment with the same exact timestamp - // to prevent duped carb entries from multiple sources - if (!arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"carbs")) { - if (arrayHasElementWithSameTimestampAndProperty(mealInputs,current.timestamp,"bolus")) { - mealInputs.push(temp); - //bwCarbs += temp.carbs; - } else { - console.error("Skipping bolus wizard entry", i, "in the pump history with",current.carb_input,"g carbs and no insulin."); - if (current.carb_input === 0) { - console.error("This is caused by a BolusWizard without carbs. If you specified insulin, it will be noted as a seperate Bolus"); - } - if (current.timestamp) { - console.error("Timestamp of bolus wizard:", current.timestamp); - } - } - } else { - duplicates += 1; - } - } - //if (duplicates > 0) console.error("Removed duplicate bolus/carb entries:" + duplicates); - - return mealInputs; -} - -exports = module.exports = findMealInputs; diff --git a/lib/meal/history.ts b/lib/meal/history.ts new file mode 100644 index 000000000..d9fd041ec --- /dev/null +++ b/lib/meal/history.ts @@ -0,0 +1,140 @@ +//import { PumpEntry, PumpEntryBolusWizard } from "../types/PumpEntry.ts.bak"; + +import { NightscoutTreatment } from "../types/NightscoutTreatment"; +import { PumpHistoryEvent } from "../types/PumpHistoryEvent"; +import { MealTreatment } from "./MealTreatment"; +import { struct, eqStrict, fromEquals } from 'fp-ts/Eq' +import { uniq } from 'fp-ts/Array' + +export interface CarbEntry { + carbs?: number + created_at?: string +} + +export interface Input { + history: Array + carbs: CarbEntry[] +} + +interface TempMealTreatment extends MealTreatment { + hasBolus: boolean, + hasCarbs: boolean, +} + +const createMeal = (timestamp: string, partial: Partial): TempMealTreatment => ({ + timestamp, + carbs: partial.carbs || 0, + nsCarbs: partial.nsCarbs || 0, + bwCarbs: partial.bwCarbs || 0, + bolus: partial.bolus || 0, + journalCarbs: partial.journalCarbs || 0, + hasBolus: partial.bolus !== undefined, + hasCarbs: partial.carbs !== undefined, +}) + +export default function findMealInputs (inputs: Input): MealTreatment[] { + var pumpHistory = inputs.history; + var carbHistory = inputs.carbs; + let mealInputs: TempMealTreatment[] = []; + var bolusWizardInputs: PumpHistoryEvent[] = []; + + const timestampEq = fromEquals((a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000) + + for (var i=0; i < carbHistory.length; i++) { + const current = carbHistory[i]; + if (current.carbs && current.created_at) { + mealInputs.push(createMeal( + current.created_at, + { + carbs: current.carbs !== null ? current.carbs : undefined, + nsCarbs: current.carbs !== null ? current.carbs : undefined, + } + )) + } + } + + for (i=0; i < pumpHistory.length; i++) { + const current = pumpHistory[i]; + if (PumpHistoryEvent.is(current) && current._type === "Bolus" && current.timestamp) { + //console.log(pumpHistory[i]); + mealInputs.push(createMeal(current.timestamp, { bolus: current.amount })) + } else if (PumpHistoryEvent.is(current) && current._type === "BolusWizard" && current.timestamp) { + // Delay process the BolusWizard entries to make sure we've seen all possible that correspond to the bolus wizard. + // More specifically, we need to make sure we process the corresponding bolus entry first. + bolusWizardInputs.push(current); + + } else if (NightscoutTreatment.is(current) && current.created_at && (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard" || current.eventType === "Carb Correction")) { + //imports carbs entered through Nightscout Care Portal + //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard + + // don't enter the treatment if there's another treatment with the same exact timestamp + // to prevent duped carb entries from multiple sources + mealInputs.push(createMeal( + current.created_at, { + carbs: current.carbs !== null ? current.carbs : undefined, + nsCarbs: current.carbs !== null ? current.carbs : undefined, + } + )) + } else if (NightscoutTreatment.is(current) && current.enteredBy === "xdrip" && current.created_at) { + mealInputs.push(createMeal( + current.created_at, + { + carbs: current.carbs !== null ? current.carbs : undefined, + nsCarbs: current.carbs !== null ? current.carbs : undefined, + bolus: current.insulin !== null ? current.insulin : undefined, + } + )) + } else if (NightscoutTreatment.is(current) && current.carbs && current.carbs > 0 && current.created_at) { + mealInputs.push(createMeal( + current.created_at, + { + carbs: current.carbs !== null ? current.carbs : undefined, + nsCarbs: current.carbs !== null ? current.carbs : undefined, + bolus: current.insulin !== null ? current.insulin : undefined, + } + )) + } else if (PumpHistoryEvent.is(current) && current._type === "JournalEntryMealMarker" && current.carb_input && current.carb_input > 0) { + mealInputs.push(createMeal( + current.timestamp, + { + carbs: current.carb_input, + journalCarbs: current.carb_input, + } + )) + } + } + + for(i=0; i < bolusWizardInputs.length; i++) { + const current = bolusWizardInputs[i]; + //console.log(bolusWizardInputs[i]); + const temp = createMeal( + current.timestamp, + { + carbs: current.carb_input, + bwCarbs: current.carb_input, + } + ) + + // don't enter the treatment if there's another treatment with the same exact timestamp + // to prevent duped carb entries from multiple sources + if (mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasCarbs)) { + continue; + } + + if (!mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasBolus)) { + console.error("Skipping bolus wizard entry", i, "in the pump history with",current.carb_input,"g carbs and no insulin."); + continue; + } + + mealInputs.push(temp); + } + + const eq = struct({ + timestamp: timestampEq, + carbs: eqStrict, + bolus: eqStrict, + }) + return uniq(eq)(mealInputs).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) +} + +exports = module.exports = findMealInputs; diff --git a/lib/meal/index.js b/lib/meal/index.js deleted file mode 100644 index 40f2907fc..000000000 --- a/lib/meal/index.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var tz = require('moment-timezone'); -var find_meals = require('./history'); -var sum = require('./total'); - -function generate (inputs) { - - var treatments = find_meals(inputs); - - var opts = { - treatments: treatments - , profile: inputs.profile - , pumphistory: inputs.history - , glucose: inputs.glucose - , basalprofile: inputs.basalprofile - }; - - var clock = new Date(tz(inputs.clock)); - - return /* meal_data */ sum(opts, clock); -} - -exports = module.exports = generate; diff --git a/lib/meal/index.ts b/lib/meal/index.ts new file mode 100644 index 000000000..8484e9a31 --- /dev/null +++ b/lib/meal/index.ts @@ -0,0 +1,36 @@ +import { GlucoseEntry } from '../types/GlucoseEntry'; +import { tz } from '../date'; +import { NightscoutTreatment } from '../types/NightscoutTreatment'; +import { BasalSchedule, Profile } from '../types/Profile'; +import { PumpHistoryEvent } from '../types/PumpHistoryEvent'; +import find_meals, { CarbEntry } from './history' +import sum from './total' + +interface Input { + history: Array + carbs: CarbEntry[] + profile: Profile + basalprofile?: BasalSchedule[] + glucose?: GlucoseEntry[] + clock: string +} + +export default function generate (inputs: Input) { + + var treatments = find_meals(inputs); + + var opts = { + treatments: treatments, + profile: inputs.profile, + pumphistory: inputs.history, + glucose: inputs.glucose, + basalprofile: inputs.basalprofile, + clock: inputs.clock + }; + + var clock = tz(new Date(inputs.clock)); + + return /* meal_data */ sum(opts, clock); +} + +exports = module.exports = generate; diff --git a/lib/meal/total.js b/lib/meal/total.ts similarity index 73% rename from lib/meal/total.js rename to lib/meal/total.ts index 1320f3642..9b4d0023f 100644 --- a/lib/meal/total.js +++ b/lib/meal/total.ts @@ -1,14 +1,24 @@ -'use strict'; +import detectCarbAbsorption, { DetectCOBInput } from '../determine-basal/cob' +import { GlucoseEntry } from "../types/GlucoseEntry"; +import { tz } from '../date'; +import { NightscoutTreatment } from "../types/NightscoutTreatment"; +import { BasalSchedule, Profile } from "../types/Profile"; +import { PumpHistoryEvent } from "../types/PumpHistoryEvent"; +import { MealTreatment } from "./MealTreatment"; -var tz = require('moment-timezone'); -var detectCarbAbsorption = require('../determine-basal/cob'); +export interface Options { + treatments?: Array + pumphistory: Array + profile: Profile + basalprofile?: BasalSchedule[] + glucose?: GlucoseEntry[] + clock: string +} -function recentCarbs(opts, time) { +export default function recentCarbs(opts: Options, time: Date) { var treatments = opts.treatments; var profile_data = opts.profile; - if (typeof(opts.glucose) !== 'undefined') { - var glucose_data = opts.glucose; - } + var glucose_data = opts.glucose; var carbs = 0; var nsCarbs = 0; var bwCarbs = 0; @@ -16,25 +26,28 @@ function recentCarbs(opts, time) { var bwFound = false; var mealCarbTime = time.getTime(); var lastCarbTime = 0; - if (!treatments) return {}; + + if (!treatments) { + return {}; + } //console.error(glucose_data); var iob_inputs = { - profile: profile_data - , history: opts.pumphistory + profile: profile_data, + history: opts.pumphistory, }; - var COB_inputs = { - glucose_data: glucose_data - , iob_inputs: iob_inputs - , basalprofile: opts.basalprofile - , mealTime: mealCarbTime + var COB_inputs: DetectCOBInput = { + glucose_data: glucose_data || [], + iob_inputs: iob_inputs, + basalprofile: opts.basalprofile, + mealTime: mealCarbTime, }; var mealCOB = 0; // this sorts the treatments collection in order. treatments.sort(function (a, b) { - var aDate = new Date(tz(a.timestamp)); - var bDate = new Date(tz(b.timestamp)); + var aDate = new Date(a.timestamp); + var bDate = new Date(b.timestamp); //console.error(aDate); return bDate.getTime() - aDate.getTime(); }); @@ -47,22 +60,22 @@ function recentCarbs(opts, time) { var now = time.getTime(); // consider carbs from up to 6 hours ago in calculating COB var carbWindow = now - 6 * 60*60*1000; - var treatmentDate = new Date(tz(treatment.timestamp)); + var treatmentDate = tz(new Date(treatment.timestamp)); var treatmentTime = treatmentDate.getTime(); if (treatmentTime > carbWindow && treatmentTime <= now) { if (treatment.carbs >= 1) { if (treatment.nsCarbs >= 1) { - nsCarbs += parseFloat(treatment.nsCarbs); + nsCarbs += treatment.nsCarbs; } else if (treatment.bwCarbs >= 1) { - bwCarbs += parseFloat(treatment.bwCarbs); + bwCarbs += treatment.bwCarbs; bwFound = true; } else if (treatment.journalCarbs >= 1) { - journalCarbs += parseFloat(treatment.journalCarbs); + journalCarbs += treatment.journalCarbs; } else { console.error("Treatment carbs unclassified:",treatment); } //console.error(treatment.carbs, maxCarbs, treatmentDate); - carbs += parseFloat(treatment.carbs); + carbs += treatment.carbs; COB_inputs.mealTime = treatmentTime; lastCarbTime = Math.max(lastCarbTime,treatmentTime); var myCarbsAbsorbed = detectCarbAbsorption(COB_inputs).carbsAbsorbed; //?????????????????????????????? here prfile was defined @@ -73,13 +86,13 @@ function recentCarbs(opts, time) { console.error("Bad myMealCOB:",myMealCOB, "mealCOB:",mealCOB, "carbs:",carbs,"myCarbsAbsorbed:",myCarbsAbsorbed); } if (myMealCOB < mealCOB) { - carbsToRemove += parseFloat(treatment.carbs); + carbsToRemove += treatment.carbs; if (treatment.nsCarbs >= 1) { - nsCarbsToRemove += parseFloat(treatment.nsCarbs); + nsCarbsToRemove += treatment.nsCarbs; } else if (treatment.bwCarbs >= 1) { - bwCarbsToRemove += parseFloat(treatment.bwCarbs); + bwCarbsToRemove += treatment.bwCarbs; } else if (treatment.journalCarbs >= 1) { - journalCarbsToRemove += parseFloat(treatment.journalCarbs); + journalCarbsToRemove += treatment.journalCarbs; } } else { carbsToRemove = 0; @@ -105,7 +118,7 @@ function recentCarbs(opts, time) { //console.error(c.currentDeviation, c.slopeFromMaxDeviation); // set a hard upper limit on COB to mitigate impact of erroneous or malicious carb entry - if (typeof(profile_data.maxCOB) === 'number' && ! isNaN(profile_data.maxCOB)) { + if (profile_data.maxCOB !== undefined) { mealCOB = Math.min( profile_data.maxCOB, mealCOB ); } else { console.error("Bad profile.maxCOB:",profile_data.maxCOB); diff --git a/lib/medtronic-clock.js b/lib/medtronic-clock.js deleted file mode 100644 index 543ad4303..000000000 --- a/lib/medtronic-clock.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -function getTime(minutes) { - var baseTime = new Date(); - baseTime.setHours('00'); - baseTime.setMinutes('00'); - baseTime.setSeconds('00'); - - return baseTime.getTime() + minutes * 60 * 1000; -} - -exports = module.exports = getTime; - diff --git a/lib/medtronic-clock.ts b/lib/medtronic-clock.ts new file mode 100644 index 000000000..e55f57c13 --- /dev/null +++ b/lib/medtronic-clock.ts @@ -0,0 +1,11 @@ +export default function getTime(minutes: number) { + var baseTime = new Date(); + baseTime.setHours(0); + baseTime.setMinutes(0); + baseTime.setSeconds(0); + + return baseTime.getTime() + minutes * 60 * 1000; +} + +exports = module.exports = getTime; + diff --git a/lib/percentile.js b/lib/percentile.ts similarity index 85% rename from lib/percentile.js rename to lib/percentile.ts index 4598ef51f..74a03565c 100644 --- a/lib/percentile.js +++ b/lib/percentile.ts @@ -1,8 +1,7 @@ -'use strict'; // From https://gist.github.com/IceCreamYou/6ffa1b18c4c8f6aeaad2 // Returns the value at a given percentile in a sorted numeric array. // "Linear interpolation between closest ranks" method -module.exports = function percentile(arr, p) { +export default function percentile(arr: number[], p: number) { if (arr.length === 0) return 0; if (typeof p !== 'number') throw new TypeError('p must be a number'); if (p <= 0) return arr[0]; @@ -15,4 +14,6 @@ module.exports = function percentile(arr, p) { if (upper >= arr.length) return arr[lower]; return arr[lower] * (1 - weight) + arr[upper] * weight; -} \ No newline at end of file +} + +exports = module.exports = percentile diff --git a/lib/profile/basal.js b/lib/profile/basal.ts similarity index 60% rename from lib/profile/basal.js rename to lib/profile/basal.ts index 241587f0d..5781fe054 100644 --- a/lib/profile/basal.js +++ b/lib/profile/basal.ts @@ -1,17 +1,12 @@ -'use strict'; - -var _ = require('lodash'); +import type { BasalSchedule } from "../types/Profile"; /* Return basal rate(U / hr) at the provided timeOfDay */ -function basalLookup (schedules, now) { - - var nowDate = now; +export function basalLookup (schedules: BasalSchedule[], now?: Date) { - if (typeof(now) === 'undefined') { - nowDate = new Date(); - } + var nowDate = now || new Date(); - var basalprofile_data = _.sortBy(schedules, function(o) { return o.i; }); + // @todo: check `i` because it can be undefined + const basalprofile_data = schedules.sort((a, b) => Number(a.i) - Number(b.i)) var basalRate = basalprofile_data[basalprofile_data.length-1].rate if (basalRate === 0) { // TODO - shared node - move this print to shared object. @@ -29,19 +24,17 @@ function basalLookup (schedules, now) { return Math.round(basalRate*1000)/1000; } - -function maxDailyBasal (inputs) { - var maxRate = _.maxBy(inputs.basals,function(o) { return Number(o.rate); }); - return (Number(maxRate.rate) *1000)/1000; +export function maxDailyBasal (inputs: { basals: { rate: string | number}[] }): number { + const max = inputs.basals.reduce((b, a) => Number(a.rate) > b ? Number(a.rate) : b, 0) + return (Number(max) *1000)/1000 } /*Return maximum daily basal rate(U / hr) from profile.basals */ -function maxBasalLookup (inputs) { +export function maxBasalLookup (inputs: { settings: { maxBasal: number }}): number { return inputs.settings.maxBasal; } - exports.maxDailyBasal = maxDailyBasal; exports.maxBasalLookup = maxBasalLookup; exports.basalLookup = basalLookup; diff --git a/lib/profile/carbs.js b/lib/profile/carbs.js index 8efdebf0c..ad43b5fae 100644 --- a/lib/profile/carbs.js +++ b/lib/profile/carbs.js @@ -1,7 +1,7 @@ 'use strict'; var getTime = require('../medtronic-clock'); -var shared_node_utils = require('../../bin/oref0-shared-node-utils'); +var shared_node_utils = require('../bin/utils'); var console_error = shared_node_utils.console_error; function carbRatioLookup (final_result, inputs, profile) { diff --git a/lib/profile/index.js b/lib/profile/index.js index 8ede7f428..79b07d25b 100644 --- a/lib/profile/index.js +++ b/lib/profile/index.js @@ -4,11 +4,9 @@ var basal = require('./basal'); var targets = require('./targets'); var isf = require('./isf'); var carb_ratios = require('./carbs'); -var _ = require('lodash'); -var shared_node_utils = require('../../bin/oref0-shared-node-utils'); +var shared_node_utils = require('../bin/utils'); var console_error = shared_node_utils.console_error; -var console_log = shared_node_utils.console_log; function defaults ( ) { return /* profile */ { @@ -129,8 +127,8 @@ function generate (final_result, inputs, opts) { profile.current_basal = basal.basalLookup(inputs.basals); profile.basalprofile = inputs.basals; - - _.forEach(profile.basalprofile, function(basalentry) { + + (profile.basalprofile || []).forEach(function(basalentry) { basalentry.rate = +(Math.round(basalentry.rate + "e+3") + "e-3"); }); @@ -154,8 +152,8 @@ function generate (final_result, inputs, opts) { profile.min_bg = Math.round(range.min_bg); profile.max_bg = Math.round(range.max_bg); profile.bg_targets = inputs.targets; - - _.forEach(profile.bg_targets.targets, function(bg_entry) { + + (profile.bg_targets.targets || []).forEach(function(bg_entry) { bg_entry.high = Math.round(bg_entry.high); bg_entry.low = Math.round(bg_entry.low); bg_entry.min_bg = Math.round(bg_entry.min_bg); @@ -178,6 +176,7 @@ function generate (final_result, inputs, opts) { } else { console_error(final_result, "Profile wasn't given carb ratio data, cannot calculate carb_ratio"); } + return profile; } diff --git a/lib/profile/isf.js b/lib/profile/isf.ts similarity index 72% rename from lib/profile/isf.js rename to lib/profile/isf.ts index 27cdca6e7..1cffcbb2a 100644 --- a/lib/profile/isf.js +++ b/lib/profile/isf.ts @@ -1,14 +1,8 @@ -'use strict'; +import { ISFProfile, ISFSensitivity } from "../types/Profile"; -var _ = require('lodash'); +export default function isfLookup(isf_profile: ISFProfile, timestamp: Date | undefined, lastResult?: ISFSensitivity): [number, ISFSensitivity | undefined] { -function isfLookup(isf_data, timestamp, lastResult) { - - var nowDate = timestamp; - - if (typeof(timestamp) === 'undefined') { - nowDate = new Date(); - } + var nowDate = timestamp || new Date(); var nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes(); @@ -16,7 +10,7 @@ function isfLookup(isf_data, timestamp, lastResult) { return [lastResult.sensitivity, lastResult]; } - isf_data = _.sortBy(isf_data.sensitivities, function(o) { return o.offset; }); + let isf_data = isf_profile.sensitivities.sort((a, b) => a.offset - b.offset) var isfSchedule = isf_data[isf_data.length - 1]; diff --git a/lib/profile/targets.js b/lib/profile/targets.js index 31a140a91..a91fc23b6 100644 --- a/lib/profile/targets.js +++ b/lib/profile/targets.js @@ -1,7 +1,7 @@ 'use strict'; var getTime = require('../medtronic-clock'); -var shared_node_utils = require('../../bin/oref0-shared-node-utils'); +var shared_node_utils = require('../bin/utils'); var console_error = shared_node_utils.console_error; function bgTargetsLookup (final_result, inputs, profile) { diff --git a/lib/round-basal.js b/lib/round-basal.ts similarity index 52% rename from lib/round-basal.js rename to lib/round-basal.ts index 503a93a51..9952ae9e9 100644 --- a/lib/round-basal.js +++ b/lib/round-basal.ts @@ -1,6 +1,6 @@ -var endsWith = require('lodash/endsWith'); +import { Profile } from "./types/Profile"; -var round_basal = function round_basal(basal, profile) { +const round_basal = (basal: number, profile?: Profile) => { /* x23 and x54 pumps change basal increment depending on how much basal is being delivered: 0.025u for 0.025 < x < 0.975 @@ -8,37 +8,25 @@ var round_basal = function round_basal(basal, profile) { 0.1u for 10 < x To round numbers nicely for the pump, use a scale factor of (1 / increment). */ - var lowest_rate_scale = 20; + let lowest_rate_scale = 20; - // Has profile even been passed in? - if (typeof profile !== 'undefined') - { - // Make sure optional model has been set - if (typeof profile.model === 'string') - { - if (endsWith(profile.model, "54") || endsWith(profile.model, "23")) - { - lowest_rate_scale = 40; - } - } + // Make sure optional model has been set + if (profile?.model?.endsWith('54') || profile?.model?.endsWith('23')) { + lowest_rate_scale = 40; } - var rounded_basal = basal; + let rounded_basal = basal; // Shouldn't need to check against 0 as pumps can't deliver negative basal anyway? - if (basal < 1) - { + if (basal < 1) { rounded_basal = Math.round(basal * lowest_rate_scale) / lowest_rate_scale; - } - else if (basal < 10) - { + } else if (basal < 10) { rounded_basal = Math.round(basal * 20) / 20; - } - else - { + } else { rounded_basal = Math.round(basal * 10) / 10; } return rounded_basal; } +export default round_basal exports = module.exports = round_basal diff --git a/lib/types/Autosens.ts b/lib/types/Autosens.ts new file mode 100644 index 000000000..f611334dd --- /dev/null +++ b/lib/types/Autosens.ts @@ -0,0 +1,13 @@ +import * as t from 'io-ts' + +export const Autosens = t.intersection([ + t.type({ + timestamp: t.string, + ratio: t.number, + }), + t.partial({ + newisf: t.number + }) +]); + +export type Autosens = t.TypeOf diff --git a/lib/types/EventType.ts b/lib/types/EventType.ts new file mode 100644 index 000000000..f1099b24e --- /dev/null +++ b/lib/types/EventType.ts @@ -0,0 +1,28 @@ +export enum EventType { + bolus = "Bolus", + smb = "SMB", + isExternal = "External Insulin", + mealBolus = "Meal Bolus", + correctionBolus = "Correction Bolus", + snackBolus = "Snack Bolus", + bolusWizard = "BolusWizard", + tempBasal = "TempBasal", + tempBasalDuration = "TempBasalDuration", + pumpSuspend = "PumpSuspend", + pumpResume = "PumpResume", + pumpAlarm = "PumpAlarm", + pumpBattery = "PumpBattery", + rewind = "Rewind", + prime = "Prime", + journalCarbs = "JournalEntryMealMarker", + nsTempBasal = "Temp Basal", + nsCarbCorrection = "Carb Correction", + nsTempTarget = "Temporary Target", + nsInsulinChange = "Insulin Change", + nsSiteChange = "Site Change", + nsBatteryChange = "Pump Battery Change", + nsAnnouncement = "Announcement", + nsSensorChange = "Sensor Start", + capillaryGlucose = "BG Check", + nsExercise = "Exercise", +} diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts new file mode 100644 index 000000000..b60efb0e8 --- /dev/null +++ b/lib/types/GlucoseEntry.ts @@ -0,0 +1,11 @@ + +export type GlucoseEntry = { + date?: number + display_time?: string + dateString?: string + sgv?: number + glucose?: number + type?: 'sgv' | 'cal' | string + device?: string + noise?: number +} diff --git a/lib/types/LocalDateFromDate.ts b/lib/types/LocalDateFromDate.ts new file mode 100644 index 000000000..359c4906c --- /dev/null +++ b/lib/types/LocalDateFromDate.ts @@ -0,0 +1,11 @@ +import { date } from 'io-ts-types/date' +import * as t from 'io-ts' +import { right } from 'fp-ts/Either' +import { tz } from '../date'; + +export const LocalDateFromDate = new t.Type( + 'LocalDateFromDate', + (a) => date.is(a), + (i) => right(tz(i)), + a => a, +) diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts new file mode 100644 index 000000000..3243b90d6 --- /dev/null +++ b/lib/types/NightscoutTreatment.ts @@ -0,0 +1,63 @@ +import * as t from 'io-ts' +import { EventType, PumpHistoryEvent } from './PumpHistoryEvent' + +export interface NightscoutTreatment { + eventType: EventType + created_at: string + id?: string + duration?: number + rawDuration?: PumpHistoryEvent + rawRate?: PumpHistoryEvent + absolute?: number + rate?: number + enteredBy?: string + bolus?: PumpHistoryEvent + insulin?: number | null + notes?: string + carbs?: number | null + fat?: number + protein?: number + foodType?: string + targetTop?: number + targetBottom?: number + glucoseType?: string + glucose?: number + units?: string + fpuID?: string + // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running + amount?: number +} + +export const NightscoutTreatment: t.Type = t.intersection([ + t.type({ + eventType: EventType, + created_at: t.string, + }), + t.partial({ + id: t.string, + duration: t.number, + rawDuration: PumpHistoryEvent, + rawRate: PumpHistoryEvent, + absolute: t.number, + rate: t.number, + enteredBy: t.string, + bolus: PumpHistoryEvent, + insulin: t.union([t.number, t.null]), + notes: t.string, + carbs: t.union([t.number, t.null]), + fat: t.number, + protein: t.number, + foodType: t.string, + targetTop: t.number, + targetBottom: t.number, + glucoseType: t.union([ + t.literal('Finger'), + t.string, + ]), + glucose: t.number, + units: t.string, + fpuID: t.string, + // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running + amount: t.number, + }) +], 'NightscoutTreatment') diff --git a/lib/types/Profile.ts b/lib/types/Profile.ts new file mode 100644 index 000000000..d7146525d --- /dev/null +++ b/lib/types/Profile.ts @@ -0,0 +1,343 @@ +import * as t from 'io-ts' + +export interface BasalSchedule { + i?: number + start: string, + minutes: number, + rate: number +} + +export const BasalSchedule: t.Type = t.intersection([ + t.type({ + start: t.string, + minutes: t.number, + rate: t.number + }), + t.partial({ + i: t.number + }) +], 'BasalSchedule') + + +/** + * { + "max_iob": 14, + "max_daily_safety_multiplier": 3, + "current_basal_safety_multiplier": 4, + "autosens_max": 1.3, + "autosens_min": 0.7, + "rewind_resets_autosens": true, + "high_temptarget_raises_sensitivity": true, + "low_temptarget_lowers_sensitivity": true, + "sensitivity_raises_target": true, + "resistance_lowers_target": false, + "exercise_mode": false, + "half_basal_exercise_target": 160, + "maxCOB": 120, + "skip_neutral_temps": false, + "unsuspend_if_no_temp": false, + "min_5m_carbimpact": 8, + "autotune_isf_adjustmentFraction": 1, + "remainingCarbsFraction": 1, + "remainingCarbsCap": 90, + "enableUAM": true, + "A52_risk_enable": false, + "enableSMB_with_COB": true, + "enableSMB_with_temptarget": true, + "enableSMB_always": false, + "enableSMB_after_carbs": true, + "allowSMB_with_high_temptarget": false, + "maxSMBBasalMinutes": 90, + "maxUAMSMBBasalMinutes": 120, + "SMBInterval": 3, + "bolus_increment": 0.05, + "maxDelta_bg_threshold": 0.2, + "curve": "ultra-rapid", + "useCustomPeakTime": false, + "insulinPeakTime": 55, + "carbsReqThreshold": 1, + "offline_hotspot": false, + "noisyCGMTargetMultiplier": 1.3, + "suspend_zeros_iob": false, + "enableEnliteBgproxy": false, + "calc_glucose_noise": false, + "target_bg": false, + "smb_delivery_ratio": 0.7, + "adjustmentFactor": 0.7, + "useNewFormula": true, + "enableDynamicCR": true, + "sigmoid": true, + "weightPercentage": 0.65, + "tddAdjBasal": true, + "enableSMB_high_bg": true, + "enableSMB_high_bg_target": 110, + "threshold_setting": 65, + "dia": 9, + "model": "722", + "current_basal": 0.7, + "basalprofile": [ + { + "rate": 0.7, + "start": "00:00:00", + "minutes": 0 + }, + { + "rate": 0.7, + "minutes": 180, + "start": "03:00:00" + }, + { + "start": "09:00:00", + "minutes": 540, + "rate": 0.7 + } + ], + "max_daily_basal": 0.7, + "max_basal": 3, + "out_units": "mg/dL", + "min_bg": 98, + "max_bg": 98, + "bg_targets": { + "units": "mg/dL", + "user_preferred_units": "mg/dL", + "targets": [ + { + "start": "00:00:00", + "high": 98, + "offset": 0, + "low": 98, + "max_bg": 98, + "min_bg": 98 + } + ] + }, + "sens": 65, + "isfProfile": { + "user_preferred_units": "mg/dL", + "units": "mg/dL", + "sensitivities": [ + { + "start": "00:00:00", + "sensitivity": 65, + "offset": 0, + "endOffset": 1440 + } + ] + }, + "carb_ratio": 6.5, + "carb_ratios": { + "units": "grams", + "schedule": [ + { + "start": "00:00:00", + "offset": 0, + "ratio": 6 + }, + { + "ratio": 5, + "start": "08:30:00", + "offset": 510 + }, + { + "start": "11:30:00", + "ratio": 6, + "offset": 690 + }, + { + "offset": 960, + "ratio": 6.5, + "start": "16:00:00" + } + ] + } +} + */ + +export interface CarbRatioSchedule { + start: string + offsset: number + ratio: number +} + +export const CarbRatioSchedule: t.Type = t.type({ + start: t.string, + offsset: t.number, + ratio: t.number, +}, 'CarbRatioSchedule') + +export interface CarbRatios { + units: string + schedule: Array, +} + +export const CarbRatios: t.Type = t.type({ + units: t.string, + schedule: t.array(CarbRatioSchedule), +}, 'CarbRatios') + +export type InsulineCurve = 'bilinear' | 'rapid-acting' | 'ultra-rapid' +export const InsulineCurve: t.Type = t.keyof({ + 'bilinear': null, + 'rapid-acting': null, + 'ultra-rapid': null, +}, 'InsulineCurve') + + +export interface ISFSensitivity { + i?: number + offset: number + endOffset: number + sensitivity: number + start?: string + x?: number +} + +export const ISFSensitivity: t.Type = t.intersection([ + t.type({ + offset: t.number, + endOffset: t.number, + sensitivity: t.number, + }), + t.partial({ + i: t.number, + start: t.string, + x: t.number + }) +], 'ISFSensitivity') + +export interface ISFProfile { + sensitivities: Array + units?: string + user_preferred_units?: string +} + +export const ISFProfile: t.Type = t.intersection([ + t.type({ + sensitivities: t.array(ISFSensitivity), + }), + t.partial({ + units: t.string, + user_preferred_units: t.string + }) +], 'ISFProfile') + +export type GlucoseUnits = 'mg/dL' | 'mmol/L' +export const GlucoseUnits = t.keyof({ + 'mg/dL': null, + 'mmol/L': null, +}, 'GlucoseUnits') + +export interface Profile { + basalprofile: Array + sens: number + carb_ratio: number + min_5m_carbimpact: number + out_units?: GlucoseUnits + max_daily_safety_multiplier?: number + current_basal_safety_multiplier?: number + model?: string + curve?: InsulineCurve + dia?: number + useCustomPeakTime?: boolean + insulinPeakTime?: number + remainingCarbsCap?: number + remainingCarbsFraction?: number + maxCOB?: number + max_iob?: number + min_bg?: number + max_bg?: number + A52_risk_enable?: boolean + noisyCGMTargetMultiplier?: number + maxRaw?: number + low_temptarget_lowers_sensitivity?: boolean + high_temptarget_raises_sensitivity?: boolean + sensitivity_raises_target?: boolean + resistance_lowers_target?: boolean + autosens_max?: number + allowSMB_with_high_temptarget?: boolean + enableSMB_high_bg_target?: number + enableSMB_with_temptarget?: boolean + enableSMB_after_carbs?: boolean + enableSMB_with_COB?: boolean + enableSMB_high_bg?: boolean + enableSMB_always?: boolean + enableUAM?: boolean + suspend_zeros_iob?: boolean + current_basal?: number + half_basal_exercise_target?: number + exercise_mode?: boolean + temptargetSet?: unknown + max_daily_basal?: number + max_basal?: number + maxDelta_bg_threshold?: number + bg_targets?: unknown + isfProfile?: ISFProfile + carb_ratios?: CarbRatios + carbsReqThreshold?: number + skip_neutral_temps?: boolean + maxSMBBasalMinutes?: number + maxUAMSMBBasalMinutes?: number + bolus_increment?: number + SMBInterval?: number +} + +export const Profile: t.Type = t.intersection([ + t.type({ + basalprofile: t.array(BasalSchedule), + sens: t.number, + carb_ratio: t.number, + min_5m_carbimpact: t.number, + }), + t.partial({ + out_units: t.keyof({ + 'mg/dL': null, + 'mmol/L': null, + }), + max_daily_safety_multiplier: t.number, + current_basal_safety_multiplier: t.number, + model: t.string, + curve: InsulineCurve, + dia: t.number, + useCustomPeakTime: t.boolean, + insulinPeakTime: t.number, + remainingCarbsCap: t.number, + remainingCarbsFraction: t.number, + maxCOB: t.number, + max_iob: t.number, + min_bg: t.number, + max_bg: t.number, + A52_risk_enable: t.boolean, + noisyCGMTargetMultiplier: t.number, + maxRaw: t.number, + low_temptarget_lowers_sensitivity: t.boolean, + high_temptarget_raises_sensitivity: t.boolean, + sensitivity_raises_target: t.boolean, + resistance_lowers_target: t.boolean, + autosens_max: t.number, + allowSMB_with_high_temptarget: t.boolean, + enableSMB_high_bg_target: t.number, + enableSMB_with_temptarget: t.boolean, + enableSMB_after_carbs: t.boolean, + enableSMB_with_COB: t.boolean, + enableSMB_high_bg: t.boolean, + enableSMB_always: t.boolean, + enableUAM: t.boolean, + suspend_zeros_iob: t.boolean, + current_basal: t.number, + half_basal_exercise_target: t.number, + exercise_mode: t.boolean, + temptargetSet: t.unknown, + max_daily_basal: t.number, + max_basal: t.number, + maxDelta_bg_threshold: t.number, + bg_targets: t.unknown, + isfProfile: ISFProfile, + carb_ratios: CarbRatios, + carbsReqThreshold: t.number, + skip_neutral_temps: t.boolean, + maxSMBBasalMinutes: t.number, + maxUAMSMBBasalMinutes: t.number, + bolus_increment: t.number, + SMBInterval: t.number, + }) +], 'Profile'); diff --git a/lib/types/PumpHistoryEvent.ts b/lib/types/PumpHistoryEvent.ts new file mode 100644 index 000000000..0c009eac7 --- /dev/null +++ b/lib/types/PumpHistoryEvent.ts @@ -0,0 +1,90 @@ +import * as t from 'io-ts' + +export const NightscoutEventType = t.keyof({ + 'Temp Basal': null, + 'Carb Correction': null, + 'Temporary Target': null, + 'Insulin Change': null, + 'Site Change': null, + 'Pump Battery Change': null, + 'Announcement': null, + 'Sensor Start': null, + 'BG Check': null, + 'Exercise': null, + 'Bolus Wizard': null, +}) + +export const EventType = t.union([ + NightscoutEventType, + t.keyof({ + 'Bolus': null, + 'SMB': null, + 'External Insulin': null, + 'Meal Bolus': null, + 'Correction Bolus': null, + 'Snack Bolus': null, + 'BolusWizard': null, + 'TempBasal': null, + 'TempBasalDuration': null, + 'PumpSuspend': null, + 'PumpResume': null, + 'PumpAlarm': null, + 'PumpBattery': null, + 'Rewind': null, + 'Prime': null, + 'JournalEntryMealMarker': null, + 'SuspendBasal': null, + }), + t.string, +]) + +export type EventType = t.TypeOf + +export const TempType = t.keyof({ + absolute: null, + percent: null, +}) + +export type TempType = t.TypeOf + +const PumpEventBase = t.type({ + _type: EventType, + timestamp: t.string, +}) + +export const PumpHistoryEvent = t.intersection([ + PumpEventBase, + t.partial({ + id: t.string, + amount: t.number, + duration: t.number, + 'duration (min)': t.number, + rate: t.number, + temp: TempType, + carb_input: t.number, + note: t.string, + isSMB: t.boolean, + isExternal: t.boolean, + // @todo: check: used in iob/history + //date: t.number, + }) +]) + +export interface PumpHistoryEvent { + _type: EventType, + timestamp: string, + id?: string, + amount?: number, + duration?: number, + 'duration (min)'?: number, + rate?: number, + temp?: TempType, + carb_input?: number, + note?: string, + isSMB?: boolean, + isExternal?: boolean, + // @todo: check: used in iob/history + //date?: number, +} + +export type PumpHistoryEvent2 = t.TypeOf diff --git a/lib/types/renameKey.ts b/lib/types/renameKey.ts new file mode 100644 index 000000000..1b1cdf679 --- /dev/null +++ b/lib/types/renameKey.ts @@ -0,0 +1,29 @@ +import { flow, pipe } from 'fp-ts/function'; +import * as t from 'io-ts' +import { map, chain } from 'fp-ts/Either' + +export const renameKey = (from: KA, to: KO) => (a: A) => new t.Type, Record>( + a.name, + a.is, + (i, c) => pipe( + t.record(t.string, t.unknown).validate(i, c), + map(b => { + if (Object.prototype.hasOwnProperty.call(b, from)) { + b[to] = b[from] + delete b[from] + } + return b + }), + chain(b => a.validate(b, c)) + ), + flow( + a.encode, + b => { + if (Object.prototype.hasOwnProperty.call(b, to)) { + b[from] = b[to] + delete b[to] + } + return b + } + ) +) diff --git a/package.json b/package.json index aea2df863..7cbb2fbc6 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "0.7.1", "description": "openaps oref0 reference implementation of the reference design", "scripts": { - "test": "make test", - "global-install": "npm install && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", - "tsc": "tsc" + "test": "jest", + "global-install": "npm install && npm run build && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", + "build": "tsc", + "lint": "eslint lib/" }, "repository": { "type": "git", @@ -100,26 +101,43 @@ }, "homepage": "https://github.com/openaps/oref0", "dependencies": { + "fp-ts": "^2.16.9", + "io-ts": "^2.2.21", + "io-ts-types": "^0.5.19", "json": "^9.0.6", "json-stable-stringify": "^1.0.1", "lodash": "^4.17.15", "moment": "^2.24.0", - "moment-timezone": "0.5.23", + "moment-timezone": "^0.5.45", "network": "^0.4.1", "request": "^2.88.0", "share2nightscout-bridge": "^0.2.1", + "tslib": "^2.6.3", "yargs": "^13.2.2" }, "devDependencies": { + "@types/jest": "^29.5.12", "@types/lodash": "^4.17.0", "@types/mocha": "^10.0.6", - "@types/moment-timezone": "^0.5.30", - "@types/node": "^20.12.8", + "@types/node": "^16.0", + "@types/should": "^13.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", "coveralls": "^3.0.3", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-fp-ts": "^0.3.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.2.1", "istanbul": "^0.4.5", + "jest": "^29.7.0", "mocha": "^5.2.0", "mocha-lcov-reporter": "^1.3.0", + "prettier": "^3.3.3", "should": "^13.2.3", + "ts-jest": "^29.2.4", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" diff --git a/tests/basal.test.js b/tests/basal.test.js deleted file mode 100644 index 73ba0557a..000000000 --- a/tests/basal.test.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -require('should'); - -var moment = require('moment'); - -describe('Basal', function ( ) { - - var basalprofile = [{'i': 0, 'start': '00:00:00', 'rate': 0, 'minutes': 0}, - {'i': 1, 'start': '00:15:00', 'rate': 2, 'minutes': 15 }, - {'i': 1, 'start': '00:45:00', 'rate': 0.5, 'minutes': 45 }]; - - it('should find the right max daily basal', function() { - - var inputs = {'basals': basalprofile}; - var basal = require('../lib/profile/basal'); - var maxBasal = basal.maxDailyBasal(inputs); - maxBasal.should.equal(2); - - }); - - - it('should find the right basal for a given moment', function() { - - var inputs = {'basals': basalprofile}; - var startingPoint = new Date(moment('2016-06-13 00:20:00.000').format()); - var startingPoint2 = new Date(moment('2016-06-13 01:00:00.000').format()); - var basal = require('../lib/profile/basal'); - var b = basal.basalLookup(basalprofile,startingPoint); - b.should.equal(2); - b = basal.basalLookup(basalprofile,startingPoint2); - b.should.equal(0.5); - - }); - -}); diff --git a/tests/basal.test.ts b/tests/basal.test.ts new file mode 100644 index 000000000..24420f109 --- /dev/null +++ b/tests/basal.test.ts @@ -0,0 +1,27 @@ +import * as basal from '../lib/profile/basal' +import { BasalSchedule } from '../lib/types/Profile'; + +describe('Basal', function ( ) { + + const basalprofile: BasalSchedule[] = [ + {'i': 0, 'start': '00:00:00', 'rate': 0, 'minutes': 0}, + {'i': 1, 'start': '00:15:00', 'rate': 2, 'minutes': 15 }, + {'i': 1, 'start': '00:45:00', 'rate': 0.5, 'minutes': 45 } + ]; + + it('should find the right max daily basal', function() { + const inputs = {'basals': basalprofile}; + const maxBasal = basal.maxDailyBasal(inputs); + expect(maxBasal).toStrictEqual(2) + }); + + + it('should find the right basal for a given moment', function() { + const startingPoint = new Date('2016-06-13 00:20:00.000'); + const startingPoint2 = new Date('2016-06-13 01:00:00.000'); + let b = basal.basalLookup(basalprofile, startingPoint); + expect(b).toStrictEqual(2) + b = basal.basalLookup(basalprofile, startingPoint2); + expect(b).toStrictEqual(0.5) + }); +}); diff --git a/tests/bolus.test.js b/tests/bolus.test.ts similarity index 98% rename from tests/bolus.test.js rename to tests/bolus.test.ts index 1e4f487a2..0a0bf9ab3 100644 --- a/tests/bolus.test.js +++ b/tests/bolus.test.ts @@ -1,5 +1,3 @@ -'use strict'; - var should = require('should'); describe('bolus', function () { @@ -35,4 +33,4 @@ describe('bolus', function () { vals.length.should.equal(1); vals[0].insulin.should.equal('3.2'); }) -}); \ No newline at end of file +}); diff --git a/tests/check-syntax.test.js b/tests/check-syntax.test.ts similarity index 92% rename from tests/check-syntax.test.js rename to tests/check-syntax.test.ts index ba0da571c..04244697b 100644 --- a/tests/check-syntax.test.js +++ b/tests/check-syntax.test.ts @@ -8,10 +8,10 @@ var dirsToCheck = [ "bin", "lib", "www" ]; -var fs = require("fs"); -var path = require("path"); -var child_process = require("child_process"); var should = require('should'); +import * as fs from 'fs'; +import * as path from 'path' +import * as child_process from 'child_process' function getFileFormat(filename) { @@ -36,7 +36,7 @@ function checkFile(filename, type) case "sh": var script = child_process.spawnSync("bash", ["-n", filename], { timeout: 4000, //milliseconds - encoding: "UTF-8", + encoding: "utf-8", }); should.equal(script.status, 0, "Shell script "+filename+" contains a syntax error."); @@ -45,7 +45,7 @@ function checkFile(filename, type) case "js": var js = child_process.spawnSync("node", ["--check", filename], { timeout: 4000, //milliseconds - encoding: "UTF-8", + encoding: "utf-8", }); should.equal(js.status, 0, "Javascript file "+filename+" contains a syntax error."); @@ -57,7 +57,7 @@ function checkFile(filename, type) var py = child_process.spawnSync("python3", ["-m", "py_compile", filename], { timeout: 4000, //milliseconds - encoding: "UTF-8", + encoding: "utf-8", }); should.equal(py.status, 0, "Python file "+filename+" contains a syntax error."); @@ -93,7 +93,7 @@ describe("Syntax checks", function() { var type = getFileFormat(file); if(type !== "unknown") { it(file, function() { - this.timeout(4000); + //this.timeout(4000); checkFile(file, type); }); } diff --git a/tests/cobhistory.test.js b/tests/cobhistory.test.ts similarity index 56% rename from tests/cobhistory.test.js rename to tests/cobhistory.test.ts index 45536bc6d..12a52bebf 100644 --- a/tests/cobhistory.test.js +++ b/tests/cobhistory.test.ts @@ -1,11 +1,9 @@ -'use strict'; - -var should = require('should'); +import find_cob_iob_entries from '../lib/meal/history' +import { PumpHistoryEvent } from '../lib/types/PumpHistoryEvent'; describe('cobhistory', function ( ) { - var find_cob_iob_entries = require('../lib/meal/history'); - - var pumpHistory = [ + // @todo: test clock skew (the code accept a tollerance of ±2 seconds, or maybe 1,9999?) + var pumpHistory: PumpHistoryEvent[] = [ {"_type": "BolusWizard","timestamp": "2016-06-19T12:51:36-04:00","carb_input": 40}, {"_type": "Bolus","timestamp": "2016-06-19T12:52:36-04:00","amount": 4.4}, {"_type": "BolusWizard","timestamp": "2016-06-19T12:57:36-04:00","carb_input": 40}, @@ -16,29 +14,30 @@ describe('cobhistory', function ( ) { {"_type": "Bolus","timestamp": "2016-06-19T12:59:36-04:00","amount": 4.4}, {"_type": "BolusWizard","timestamp": "2016-06-19T12:59:36-04:00","carb_input": 40}, {"_type": "Bolus","timestamp": "2016-06-19T12:59:36-04:00","amount": 4.4} - ]; + ]; var carbHistory = [ - {"_type": "BolusWizard","created_at": "2016-06-19T12:59:36-04:00","carbs": 40}, - {"_type": "Bolus","created_at": "2016-06-19T12:59:36-04:00","amount": 4.4}, - {"_type": "BolusWizard","created_at": "2016-06-19T12:59:36-04:00","carbs": 40}, - {"_type": "Bolus","created_at": "2016-06-19T12:59:36-04:00","amount": 4.4} - ]; + {"_type": "BolusWizard","created_at": "2016-06-19T12:59:36-04:00","carbs": 40}, + {"_type": "Bolus","created_at": "2016-06-19T12:59:36-04:00","amount": 4.4}, + {"_type": "BolusWizard","created_at": "2016-06-19T12:59:36-04:00","carbs": 40}, + {"_type": "Bolus","created_at": "2016-06-19T12:59:36-04:00","amount": 4.4} + ]; //function determine_basal(glucose_status, currenttemp, iob_data, profile) it('should dedupe entries', function () { - var inputs = {}; - inputs.history = pumpHistory; - inputs.carbs = carbHistory; - inputs.profile = {}; + var inputs = { + history: pumpHistory, + carbs: carbHistory, + profile: {} + }; var output = find_cob_iob_entries(inputs); - console.log(output); + //console.log(output); // BolusWizard carb_input without a timestamp-matched Bolus will be ignored - output.length.should.equal(6); + expect(output.length).toStrictEqual(6) }); }); diff --git a/tests/command-behavior.tests.sh b/tests/command-behavior.tests.sh index 2741a64b0..b14cd3da7 100755 --- a/tests/command-behavior.tests.sh +++ b/tests/command-behavior.tests.sh @@ -16,6 +16,8 @@ cleanup () { main () { mkdir -p bash-unit-test-temp || fail_test "Unable to create temporary directory" + npm run build + cd bash-unit-test-temp generate_test_files @@ -124,6 +126,7 @@ test-autotune-prep () { ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) ERROR_LINES=$( cat stderr_output ) + [[ $(cat stderr_output | grep mealCOB | wc -l) -eq 82 ]] || fail_test "oref0-autotune-prep didn't contain expected stderr output" # Make sure output has expected data @@ -157,6 +160,7 @@ test-autotune-prep () { # Make sure output has expected data cat stdout_output | jq ".CRData | first" | grep -q CRInitialBG || fail_test "oref0-autotune-prep with carbhistory didn't contain expected CR Data output" + # @todo: why the first CSFGlucoseData should be null? cat stdout_output | jq ".CSFGlucoseData | first" | grep -q null || fail_test "oref0-autotune-prep with carbhistory didn't contain expected CSF Glucose Data" cat stdout_output | jq ".ISFGlucoseData | first" | grep -q dateString || fail_test "oref0-autotune-prep with carbhistory didn't contain expected ISF Glucose Data" cat stdout_output | jq ".basalGlucoseData | first" | grep -q dateString || fail_test "oref0-autotune-prep with carbhistory didn't contain expected basal Glucose Data" diff --git a/tests/date.test.ts b/tests/date.test.ts new file mode 100644 index 000000000..5cf0768c5 --- /dev/null +++ b/tests/date.test.ts @@ -0,0 +1,42 @@ +import * as _ from '../lib/date' +import tz from 'moment-timezone' +import moment from 'moment' + +describe('date', () => { + const dates = [ + '2024-08-07T22:41:23.660Z', + '2016-06-19T12:59:36-04:00', + '2016-06-19T12:59:36-06:00', + '2016-06-19T12:59:36+00:00', + '2016-06-19T12:59:36+02:15', + ] + describe('tz', () => { + it('should act like moment-timezone', () => { + dates.forEach(dateString => { + const a = new Date(tz(dateString)) + const b = _.tz(new Date(dateString)) + expect(b.toISOString()).toStrictEqual(a.toISOString()) + expect(b.getTimezoneOffset()).toStrictEqual(a.getTimezoneOffset()) + }) + }) + + it('should act like moment-timezone from string', () => { + dates.forEach(dateString => { + const a = new Date(tz(dateString)) + const b = _.tz(dateString) + expect(b.toISOString()).toStrictEqual(a.toISOString()) + expect(b.getTimezoneOffset()).toStrictEqual(a.getTimezoneOffset()) + }) + }) + }) + + describe('format', () => { + it('should act like moment.format()', () => { + dates.forEach(dateString => { + const a = moment(dateString).format() + const b = _.format(new Date(dateString)) + expect(b).toStrictEqual(a) + }) + }) + }) +}) diff --git a/tests/determine-basal.data.json b/tests/determine-basal.data.json new file mode 100644 index 000000000..ef0dc4310 --- /dev/null +++ b/tests/determine-basal.data.json @@ -0,0 +1,4619 @@ +{ + "glucose": [ + { + "direction": "Flat", + "_id": "9054433E-78DA-4EC9-9B6B-7FBC4E7BEDB7", + "sgv": 129, + "glucose": 129, + "date": 1723076455222, + "dateString": "2024-08-08T00:20:55.223Z", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.223Z", + "unfiltered": 130, + "sessionStartDate": "2024-07-28T22:42:38.223Z" + }, + { + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "unfiltered": 136, + "_id": "6C29D537-8B5B-46B8-992A-B3174B4200FD", + "sgv": 132, + "glucose": 132, + "dateString": "2024-08-08T00:10:57.871Z", + "date": 1723075857871, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.838Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "date": 1723075554869, + "activationDate": "2024-07-28T22:17:36.838Z", + "sgv": 132, + "dateString": "2024-08-08T00:05:54.870Z", + "unfiltered": 136, + "direction": "Flat", + "glucose": 132, + "_id": "9A96A2A2-B669-43AD-810F-CBD9A1659070", + "type": "sgv" + }, + { + "_id": "6AD74528-8934-438C-86A7-57560E9E3567", + "type": "sgv", + "date": 1723075252466, + "unfiltered": 136, + "glucose": 134, + "sgv": 134, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.838Z", + "dateString": "2024-08-08T00:00:52.466Z", + "sessionStartDate": "2024-07-28T22:42:36.838Z" + }, + { + "sgv": 135, + "date": 1723074952643, + "unfiltered": 135, + "glucose": 135, + "_id": "D2D62C2B-CDC6-4BB6-A4EB-037F3131676C", + "activationDate": "2024-07-28T22:17:36.838Z", + "dateString": "2024-08-07T23:55:52.644Z", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "direction": "Flat" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "sgv": 139, + "dateString": "2024-08-07T23:50:53.813Z", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.838Z", + "date": 1723074653812, + "unfiltered": 138, + "type": "sgv", + "_id": "E6BA7922-B64D-47BC-A5F3-B8E7C8757F75", + "glucose": 139 + }, + { + "unfiltered": 142, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.838Z", + "glucose": 141, + "date": 1723074356304, + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "_id": "BBDD879A-CB02-42E5-8D62-955B72C08308", + "sgv": 141, + "direction": "Flat", + "dateString": "2024-08-07T23:45:56.305Z" + }, + { + "_id": "9201FFA3-92B6-442D-8031-9DC520B08C75", + "glucose": 138, + "date": 1723074053203, + "activationDate": "2024-07-28T22:17:36.838Z", + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "dateString": "2024-08-07T23:40:53.204Z", + "type": "sgv", + "unfiltered": 140, + "direction": "Flat", + "sgv": 138 + }, + { + "activationDate": "2024-07-28T22:17:36.838Z", + "date": 1723073752922, + "dateString": "2024-08-07T23:35:52.923Z", + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "_id": "D93009EB-4617-4F7A-A2BE-A65FFB286EAD", + "sgv": 135, + "unfiltered": 137, + "direction": "Flat", + "type": "sgv", + "glucose": 135 + }, + { + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "glucose": 132, + "activationDate": "2024-07-28T22:17:36.838Z", + "unfiltered": 137, + "type": "sgv", + "sgv": 132, + "date": 1723073453816, + "_id": "7EE474D9-CF86-40CC-AB16-69FC64C60FDF", + "dateString": "2024-08-07T23:30:53.816Z" + }, + { + "sgv": 125, + "dateString": "2024-08-07T23:25:54.323Z", + "date": 1723073154323, + "direction": "Flat", + "_id": "F8DD3C52-F135-4019-AC41-7D18D6BBD626", + "activationDate": "2024-07-28T22:17:36.838Z", + "unfiltered": 130, + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "glucose": 125, + "type": "sgv" + }, + { + "activationDate": "2024-07-28T22:17:36.838Z", + "date": 1723072853570, + "type": "sgv", + "dateString": "2024-08-07T23:20:53.570Z", + "unfiltered": 122, + "sessionStartDate": "2024-07-28T22:42:36.838Z", + "_id": "2032E08B-191C-46E9-84E3-CEFFD1486947", + "direction": "Flat", + "sgv": 119, + "glucose": 119 + }, + { + "activationDate": "2024-07-28T22:17:36.838Z", + "direction": "Flat", + "glucose": 120, + "date": 1723072553837, + "dateString": "2024-08-07T23:15:53.838Z", + "unfiltered": 124, + "sgv": 120, + "_id": "9719225D-129A-4BDD-B9AB-AD7592F63F1D", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.838Z" + }, + { + "direction": "Flat", + "_id": "2564E4AB-5E32-4464-95F1-B1EF3ADFE9D4", + "glucose": 121, + "type": "sgv", + "dateString": "2024-08-07T23:10:57.855Z", + "unfiltered": 126, + "date": 1723072257854, + "activationDate": "2024-07-28T22:17:40.855Z", + "sessionStartDate": "2024-07-28T22:42:40.855Z", + "sgv": 121 + }, + { + "sgv": 121, + "activationDate": "2024-07-28T22:17:37.581Z", + "direction": "Flat", + "dateString": "2024-08-07T23:05:52.848Z", + "unfiltered": 125, + "type": "sgv", + "date": 1723071952847, + "sessionStartDate": "2024-07-28T22:42:37.581Z", + "_id": "9130F906-9077-463E-A830-7B7933957672", + "glucose": 121 + }, + { + "unfiltered": 126, + "_id": "843237E7-7A13-40C2-BF2D-9349E9874D4E", + "glucose": 124, + "activationDate": "2024-07-28T22:17:37.581Z", + "date": 1723071655635, + "sessionStartDate": "2024-07-28T22:42:37.581Z", + "dateString": "2024-08-07T23:00:55.636Z", + "sgv": 124, + "type": "sgv", + "direction": "Flat" + }, + { + "type": "sgv", + "unfiltered": 129, + "sgv": 128, + "glucose": 128, + "dateString": "2024-08-07T22:55:54.581Z", + "_id": "8E5FF7C3-4D97-4E86-AE99-C976953A4025", + "activationDate": "2024-07-28T22:17:37.581Z", + "sessionStartDate": "2024-07-28T22:42:37.581Z", + "date": 1723071354581, + "direction": "Flat" + }, + { + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:39.705Z", + "_id": "D294E882-BE18-417B-A298-87FC34B292D0", + "direction": "Flat", + "unfiltered": 132, + "activationDate": "2024-07-28T22:17:39.705Z", + "date": 1723071053347, + "sgv": 130, + "glucose": 130, + "dateString": "2024-08-07T22:50:53.347Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:39.705Z", + "unfiltered": 134, + "type": "sgv", + "date": 1723070756704, + "glucose": 133, + "_id": "09FCA1E4-A355-45F6-AD9B-0FAE29994AF2", + "dateString": "2024-08-07T22:45:56.705Z", + "sgv": 133, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:39.705Z" + }, + { + "direction": "Flat", + "glucose": 134, + "type": "sgv", + "sgv": 134, + "dateString": "2024-08-07T22:40:52.933Z", + "_id": "99862C27-30DD-42AD-8241-E6AC4AF613F9", + "activationDate": "2024-07-28T22:17:35.933Z", + "sessionStartDate": "2024-07-28T22:42:35.933Z", + "unfiltered": 135, + "date": 1723070452932 + }, + { + "_id": "3B6F1593-1618-446D-A061-80BB1C6E0612", + "sessionStartDate": "2024-07-28T22:42:36.555Z", + "direction": "Flat", + "glucose": 136, + "date": 1723070153555, + "dateString": "2024-08-07T22:35:53.555Z", + "sgv": 136, + "unfiltered": 138, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.555Z" + }, + { + "glucose": 136, + "activationDate": "2024-07-28T22:17:38.726Z", + "dateString": "2024-08-07T22:30:53.401Z", + "sgv": 136, + "date": 1723069853400, + "direction": "Flat", + "_id": "D73F1D7D-8CAA-462C-B0BE-33EA4DFEAAE2", + "type": "sgv", + "unfiltered": 139, + "sessionStartDate": "2024-07-28T22:42:38.726Z" + }, + { + "type": "sgv", + "direction": "Flat", + "dateString": "2024-08-07T22:25:52.946Z", + "sgv": 137, + "sessionStartDate": "2024-07-28T22:42:38.726Z", + "date": 1723069552946, + "activationDate": "2024-07-28T22:17:38.726Z", + "_id": "4AD327F3-841D-4324-8DD0-208A50526B43", + "glucose": 137, + "unfiltered": 140 + }, + { + "dateString": "2024-08-07T22:20:55.726Z", + "sessionStartDate": "2024-07-28T22:42:38.726Z", + "unfiltered": 142, + "direction": "Flat", + "glucose": 137, + "date": 1723069255726, + "sgv": 137, + "_id": "B06F6FB5-A818-412C-89ED-58B1222DDF74", + "activationDate": "2024-07-28T22:17:38.726Z", + "type": "sgv" + }, + { + "glucose": 135, + "dateString": "2024-08-07T22:10:52.785Z", + "sgv": 135, + "unfiltered": 139, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.785Z", + "_id": "FB6CD35F-B2F1-4A58-AB1C-E1C08738B48F", + "direction": "Flat", + "date": 1723068652785, + "activationDate": "2024-07-28T22:17:35.785Z" + }, + { + "dateString": "2024-08-07T22:05:54.284Z", + "date": 1723068354284, + "unfiltered": 137, + "sgv": 136, + "activationDate": "2024-07-28T22:17:37.662Z", + "glucose": 136, + "_id": "F31B1333-B382-4A2F-983E-344A2333B86A", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "type": "sgv" + }, + { + "direction": "Flat", + "unfiltered": 142, + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "_id": "B6879935-A7C9-4A3A-92B1-49FE83BD2A2E", + "activationDate": "2024-07-28T22:17:37.662Z", + "glucose": 139, + "type": "sgv", + "sgv": 139, + "date": 1723068053409, + "dateString": "2024-08-07T22:00:53.410Z" + }, + { + "unfiltered": 145, + "type": "sgv", + "sgv": 141, + "dateString": "2024-08-07T21:55:54.846Z", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "direction": "Flat", + "_id": "11F0100E-E5CC-49AD-9DBE-A55209F4D16B", + "date": 1723067754845, + "activationDate": "2024-07-28T22:17:37.662Z", + "glucose": 141 + }, + { + "sgv": 142, + "glucose": 142, + "date": 1723067452818, + "activationDate": "2024-07-28T22:17:37.662Z", + "unfiltered": 144, + "type": "sgv", + "direction": "Flat", + "_id": "7E45C3B0-D9C1-43FE-B63B-EA5148A4F015", + "dateString": "2024-08-07T21:50:52.819Z", + "sessionStartDate": "2024-07-28T22:42:37.662Z" + }, + { + "_id": "93508596-8429-4948-AE6F-844305CAAF28", + "direction": "Flat", + "dateString": "2024-08-07T21:45:54.566Z", + "sgv": 146, + "date": 1723067154565, + "unfiltered": 148, + "type": "sgv", + "activationDate": "2024-07-28T22:17:37.662Z", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "glucose": 146 + }, + { + "sgv": 148, + "dateString": "2024-08-07T21:40:53.243Z", + "date": 1723066853242, + "unfiltered": 149, + "activationDate": "2024-07-28T22:17:37.662Z", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "glucose": 148, + "type": "sgv", + "_id": "5260AC1B-107E-4555-A362-334304E5197D", + "direction": "Flat" + }, + { + "dateString": "2024-08-07T21:35:52.910Z", + "activationDate": "2024-07-28T22:17:37.662Z", + "_id": "745494CE-2189-4F3D-9EEF-FFBC31A62A2F", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "date": 1723066552909, + "sgv": 152, + "unfiltered": 151, + "type": "sgv", + "direction": "Flat", + "glucose": 152 + }, + { + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:37.662Z", + "glucose": 157, + "_id": "1C037C08-E40E-4251-8637-C1266293601D", + "activationDate": "2024-07-28T22:17:37.662Z", + "date": 1723066254661, + "dateString": "2024-08-07T21:30:54.662Z", + "unfiltered": 157, + "direction": "Flat", + "sgv": 157 + }, + { + "_id": "A3AAB217-D1CD-4250-A07E-085AEECC5CFB", + "sgv": 157, + "unfiltered": 159, + "direction": "Flat", + "dateString": "2024-08-07T21:25:52.978Z", + "date": 1723065952978, + "glucose": 157, + "type": "sgv", + "activationDate": "2024-07-28T22:17:35.978Z", + "sessionStartDate": "2024-07-28T22:42:35.978Z" + }, + { + "unfiltered": 157, + "_id": "2D90CA85-EE6C-4573-AB4D-7BC2C8ECA575", + "sgv": 154, + "dateString": "2024-08-07T21:20:53.091Z", + "date": 1723065653090, + "glucose": 154, + "activationDate": "2024-07-28T22:17:41.151Z", + "direction": "Flat", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:41.151Z" + }, + { + "dateString": "2024-08-07T21:15:55.452Z", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "glucose": 150, + "direction": "Flat", + "date": 1723065355451, + "type": "sgv", + "sgv": 150, + "activationDate": "2024-07-28T22:17:41.151Z", + "unfiltered": 154, + "_id": "5E97F615-ACE9-4509-BED1-8B10DA53C397" + }, + { + "date": 1723065058569, + "type": "sgv", + "unfiltered": 151, + "activationDate": "2024-07-28T22:17:41.151Z", + "direction": "Flat", + "_id": "452F3D84-66CF-4E5E-BFC4-32F9A9F34640", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "glucose": 147, + "sgv": 147, + "dateString": "2024-08-07T21:10:58.569Z" + }, + { + "_id": "30AA5753-2388-4CD5-8F4E-CEF57CA042FA", + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:41.151Z", + "sgv": 144, + "unfiltered": 148, + "date": 1723064754666, + "dateString": "2024-08-07T21:05:54.666Z", + "glucose": 144, + "sessionStartDate": "2024-07-28T22:42:41.151Z" + }, + { + "_id": "76838B56-22B7-4EB2-9C07-E820844D76A1", + "direction": "Flat", + "glucose": 144, + "date": 1723064458497, + "type": "sgv", + "sgv": 144, + "activationDate": "2024-07-28T22:17:41.151Z", + "dateString": "2024-08-07T21:00:58.497Z", + "unfiltered": 149, + "sessionStartDate": "2024-07-28T22:42:41.151Z" + }, + { + "dateString": "2024-08-07T20:55:52.880Z", + "date": 1723064152880, + "direction": "Flat", + "unfiltered": 145, + "_id": "C012EEF9-F62F-4884-8680-5967097762DC", + "activationDate": "2024-07-28T22:17:41.151Z", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "sgv": 144, + "glucose": 144, + "type": "sgv" + }, + { + "dateString": "2024-08-07T20:50:55.692Z", + "unfiltered": 147, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:41.151Z", + "type": "sgv", + "_id": "26E8056F-B25C-4BD6-8DD4-92271595E457", + "glucose": 148, + "sgv": 148, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "date": 1723063855691 + }, + { + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:41.151Z", + "glucose": 154, + "sgv": 154, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "dateString": "2024-08-07T20:45:58.683Z", + "date": 1723063558682, + "_id": "9EBDABDF-4CEE-400F-9AA3-5044D7E7350D", + "unfiltered": 154 + }, + { + "sgv": 157, + "glucose": 157, + "dateString": "2024-08-07T20:40:54.385Z", + "type": "sgv", + "unfiltered": 159, + "direction": "Flat", + "_id": "6F9454BC-771B-4013-9F41-B058DCE7CCEE", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "activationDate": "2024-07-28T22:17:41.151Z", + "date": 1723063254384 + }, + { + "date": 1723062953716, + "glucose": 158, + "dateString": "2024-08-07T20:35:53.717Z", + "sgv": 158, + "unfiltered": 159, + "activationDate": "2024-07-28T22:17:41.151Z", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "direction": "Flat", + "_id": "44F05F4C-236C-484C-BD7A-205F2AD33A60", + "type": "sgv" + }, + { + "date": 1723062652959, + "direction": "Flat", + "type": "sgv", + "dateString": "2024-08-07T20:30:52.959Z", + "sgv": 160, + "unfiltered": 160, + "_id": "AE2E42F2-D90A-47DD-B9D1-E5FD25EF144E", + "glucose": 160, + "activationDate": "2024-07-28T22:17:41.151Z", + "sessionStartDate": "2024-07-28T22:42:41.151Z" + }, + { + "activationDate": "2024-07-28T22:17:41.151Z", + "direction": "Flat", + "type": "sgv", + "_id": "6ABA3A18-1A92-4B84-B3D0-04624ADAFD20", + "date": 1723062352801, + "sgv": 163, + "unfiltered": 166, + "glucose": 163, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "dateString": "2024-08-07T20:25:52.802Z" + }, + { + "activationDate": "2024-07-28T22:17:41.151Z", + "dateString": "2024-08-07T20:20:54.804Z", + "glucose": 162, + "date": 1723062054804, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "type": "sgv", + "unfiltered": 166, + "sgv": 162, + "direction": "Flat", + "_id": "9069B6C4-B895-40BA-B27E-4F343FC6BE35" + }, + { + "direction": "Flat", + "_id": "64E9912A-48AC-4A7C-8758-B3B866BC655A", + "type": "sgv", + "date": 1723061753417, + "activationDate": "2024-07-28T22:17:41.151Z", + "glucose": 159, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "unfiltered": 164, + "dateString": "2024-08-07T20:15:53.417Z", + "sgv": 159 + }, + { + "_id": "F7781C37-0220-4570-98B6-85D5E7080816", + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "date": 1723061452809, + "unfiltered": 164, + "type": "sgv", + "glucose": 159, + "activationDate": "2024-07-28T22:17:41.151Z", + "sgv": 159, + "dateString": "2024-08-07T20:10:52.810Z", + "direction": "Flat" + }, + { + "_id": "1499E59D-3FD9-47B9-9119-5782917B4403", + "sgv": 159, + "unfiltered": 159, + "activationDate": "2024-07-28T22:17:41.151Z", + "dateString": "2024-08-07T20:05:53.041Z", + "glucose": 159, + "sessionStartDate": "2024-07-28T22:42:41.151Z", + "type": "sgv", + "date": 1723061153041, + "direction": "Flat" + }, + { + "date": 1723060858150, + "glucose": 166, + "dateString": "2024-08-07T20:00:58.151Z", + "sgv": 166, + "_id": "E2299823-A1F2-42D9-AE85-BA40CAD694D5", + "type": "sgv", + "direction": "Flat", + "unfiltered": 165, + "activationDate": "2024-07-28T22:17:41.151Z", + "sessionStartDate": "2024-07-28T22:42:41.151Z" + }, + { + "direction": "Flat", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.763Z", + "dateString": "2024-08-07T19:55:52.763Z", + "sgv": 172, + "unfiltered": 167, + "date": 1723060552762, + "_id": "1E0CE93D-DE66-43E1-B4FB-B63BA1998852", + "glucose": 172, + "activationDate": "2024-07-28T22:17:35.763Z" + }, + { + "_id": "ACB3196B-4B44-43E1-8DDB-7B4936B49ED9", + "glucose": 178, + "activationDate": "2024-07-28T22:17:35.733Z", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "type": "sgv", + "sgv": 178, + "dateString": "2024-08-07T19:50:53.747Z", + "unfiltered": 175, + "date": 1723060253747 + }, + { + "activationDate": "2024-07-28T22:17:35.733Z", + "sgv": 178, + "unfiltered": 179, + "_id": "7A3E9137-D0C3-492C-B30B-7E55E40D03C1", + "type": "sgv", + "date": 1723059955535, + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "glucose": 178, + "direction": "FortyFiveUp", + "dateString": "2024-08-07T19:45:55.535Z" + }, + { + "dateString": "2024-08-07T19:40:55.191Z", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "_id": "844AB716-D180-4934-9CA7-BA940B6A8815", + "type": "sgv", + "unfiltered": 174, + "sgv": 171, + "direction": "FortyFiveUp", + "date": 1723059655190, + "glucose": 171, + "activationDate": "2024-07-28T22:17:35.733Z" + }, + { + "date": 1723059356845, + "type": "sgv", + "direction": "FortyFiveUp", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "_id": "35E08F99-0BDE-4DEC-8D72-4D98DC1CE424", + "dateString": "2024-08-07T19:35:56.845Z", + "sgv": 160, + "glucose": 160, + "unfiltered": 168, + "activationDate": "2024-07-28T22:17:35.733Z" + }, + { + "dateString": "2024-08-07T19:30:57.484Z", + "unfiltered": 154, + "sgv": 147, + "date": 1723059057483, + "_id": "6ED0217B-826B-42D7-BDF9-A54F0049EB3E", + "glucose": 147, + "type": "sgv", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "activationDate": "2024-07-28T22:17:35.733Z" + }, + { + "unfiltered": 141, + "direction": "Flat", + "dateString": "2024-08-07T19:25:53.827Z", + "sgv": 138, + "date": 1723058753827, + "_id": "30B4131C-94D3-4127-91D4-498155BCB102", + "type": "sgv", + "activationDate": "2024-07-28T22:17:35.733Z", + "glucose": 138, + "sessionStartDate": "2024-07-28T22:42:35.733Z" + }, + { + "date": 1723058454759, + "activationDate": "2024-07-28T22:17:35.733Z", + "dateString": "2024-08-07T19:20:54.760Z", + "direction": "Flat", + "type": "sgv", + "sgv": 135, + "unfiltered": 135, + "_id": "DA2D9848-C797-4729-9676-ED24C0CCF412", + "glucose": 135, + "sessionStartDate": "2024-07-28T22:42:35.733Z" + }, + { + "direction": "Flat", + "date": 1723058153193, + "unfiltered": 136, + "dateString": "2024-08-07T19:15:53.194Z", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "type": "sgv", + "_id": "F9CC4D46-2C18-46DC-85CB-708F505C5E2E", + "sgv": 136, + "glucose": 136, + "activationDate": "2024-07-28T22:17:35.733Z" + }, + { + "glucose": 136, + "type": "sgv", + "dateString": "2024-08-07T19:10:52.733Z", + "date": 1723057852732, + "activationDate": "2024-07-28T22:17:35.733Z", + "sessionStartDate": "2024-07-28T22:42:35.733Z", + "sgv": 136, + "direction": "Flat", + "unfiltered": 137, + "_id": "9877D7DF-16D0-4DA5-9D54-49E4D6B7DDB8" + }, + { + "activationDate": "2024-07-28T22:17:36.522Z", + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "unfiltered": 132, + "dateString": "2024-08-07T19:05:53.708Z", + "date": 1723057553707, + "direction": "Flat", + "_id": "6B824F76-C0F3-46C1-94EF-0818984B529E", + "type": "sgv", + "glucose": 132, + "sgv": 132 + }, + { + "sgv": 128, + "activationDate": "2024-07-28T22:17:36.522Z", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "_id": "0171258A-C784-4956-823F-C177F87EAC3C", + "date": 1723057252923, + "glucose": 128, + "dateString": "2024-08-07T19:00:52.924Z", + "unfiltered": 131, + "direction": "Flat" + }, + { + "sgv": 124, + "direction": "Flat", + "dateString": "2024-08-07T18:56:00.389Z", + "activationDate": "2024-07-28T22:17:36.522Z", + "unfiltered": 127, + "_id": "AA555C08-35CC-4013-B104-B61E3A14F816", + "glucose": 124, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "date": 1723056960388 + }, + { + "dateString": "2024-08-07T18:50:54.322Z", + "glucose": 118, + "activationDate": "2024-07-28T22:17:36.522Z", + "date": 1723056654322, + "sgv": 118, + "direction": "Flat", + "_id": "3CA06D36-A527-4B41-B735-8FCB41C649B8", + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "type": "sgv", + "unfiltered": 120 + }, + { + "dateString": "2024-08-07T18:45:56.298Z", + "activationDate": "2024-07-28T22:17:36.522Z", + "date": 1723056356297, + "unfiltered": 113, + "direction": "Flat", + "_id": "A5EC31AC-894B-46FA-BFA9-B8BD6CDCA4EB", + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "glucose": 113, + "type": "sgv", + "sgv": 113 + }, + { + "sessionStartDate": "2024-07-28T22:42:36.522Z", + "sgv": 110, + "unfiltered": 113, + "dateString": "2024-08-07T18:40:53.522Z", + "type": "sgv", + "_id": "E45CF849-390A-4939-A6A6-9A9C366E3175", + "activationDate": "2024-07-28T22:17:36.522Z", + "glucose": 110, + "date": 1723056053521, + "direction": "Flat" + }, + { + "date": 1723055752621, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:35.763Z", + "sessionStartDate": "2024-07-28T22:42:35.763Z", + "_id": "87270B8B-389E-419B-A6FB-CB0CE2AEBC84", + "glucose": 106, + "sgv": 106, + "unfiltered": 112, + "type": "sgv", + "dateString": "2024-08-07T18:35:52.621Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:35.763Z", + "date": 1723055452763, + "activationDate": "2024-07-28T22:17:35.763Z", + "glucose": 98, + "_id": "81EF7FBB-AD24-4968-8B01-BB6D607ADED0", + "unfiltered": 103, + "type": "sgv", + "dateString": "2024-08-07T18:30:52.763Z", + "sgv": 98, + "direction": "Flat" + }, + { + "date": 1723055152652, + "sessionStartDate": "2024-07-28T22:42:35.652Z", + "unfiltered": 96, + "activationDate": "2024-07-28T22:17:35.652Z", + "_id": "A2C0A747-4238-418F-82F2-5441553E3DC1", + "type": "sgv", + "sgv": 93, + "direction": "Flat", + "dateString": "2024-08-07T18:25:52.652Z", + "glucose": 93 + }, + { + "date": 1723054853578, + "sgv": 93, + "unfiltered": 95, + "glucose": 93, + "type": "sgv", + "activationDate": "2024-07-28T22:17:37.616Z", + "_id": "EF8341E7-A673-43A7-8AF1-A572F8A9B29D", + "direction": "Flat", + "dateString": "2024-08-07T18:20:53.578Z", + "sessionStartDate": "2024-07-28T22:42:37.616Z" + }, + { + "dateString": "2024-08-07T18:15:53.945Z", + "unfiltered": 97, + "date": 1723054553945, + "sgv": 94, + "activationDate": "2024-07-28T22:17:37.616Z", + "glucose": 94, + "_id": "3786239A-F400-4DB7-979A-86130AB0E4AF", + "direction": "Flat", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:37.616Z" + }, + { + "activationDate": "2024-07-28T22:17:37.616Z", + "unfiltered": 98, + "sessionStartDate": "2024-07-28T22:42:37.616Z", + "direction": "Flat", + "_id": "64E33DE2-0CB9-4664-9FBD-4E7EF44AC27D", + "sgv": 94, + "type": "sgv", + "glucose": 94, + "date": 1723054256075, + "dateString": "2024-08-07T18:10:56.075Z" + }, + { + "dateString": "2024-08-07T18:05:52.312Z", + "glucose": 92, + "sessionStartDate": "2024-07-28T22:42:37.616Z", + "type": "sgv", + "unfiltered": 96, + "activationDate": "2024-07-28T22:17:37.616Z", + "direction": "Flat", + "sgv": 92, + "date": 1723053952312, + "_id": "5F2C99A1-D564-499D-98CF-C89751D36DB8" + }, + { + "sessionStartDate": "2024-07-28T22:42:37.616Z", + "unfiltered": 98, + "activationDate": "2024-07-28T22:17:37.616Z", + "date": 1723053654873, + "direction": "Flat", + "_id": "A1497641-1BE6-4DCB-8EE0-612FA770C5F6", + "dateString": "2024-08-07T18:00:54.874Z", + "type": "sgv", + "sgv": 94, + "glucose": 94 + }, + { + "_id": "C61C75B4-4227-4B5A-849C-59011743D1DE", + "sessionStartDate": "2024-07-28T22:42:37.616Z", + "dateString": "2024-08-07T17:55:55.570Z", + "glucose": 96, + "activationDate": "2024-07-28T22:17:37.616Z", + "date": 1723053355570, + "sgv": 96, + "unfiltered": 99, + "type": "sgv", + "direction": "Flat" + }, + { + "dateString": "2024-08-07T17:50:53.735Z", + "sgv": 99, + "unfiltered": 101, + "direction": "Flat", + "_id": "9EC0C191-DE7C-4B62-ADA8-C87F2AB5F80C", + "glucose": 99, + "date": 1723053053734, + "activationDate": "2024-07-28T22:17:37.616Z", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:37.616Z" + }, + { + "unfiltered": 101, + "direction": "Flat", + "glucose": 100, + "activationDate": "2024-07-28T22:17:37.616Z", + "type": "sgv", + "_id": "E3C2D563-9631-4AA7-AD57-CCE740B987C3", + "dateString": "2024-08-07T17:45:54.616Z", + "date": 1723052754616, + "sgv": 100, + "sessionStartDate": "2024-07-28T22:42:37.616Z" + }, + { + "date": 1723052453306, + "type": "sgv", + "dateString": "2024-08-07T17:40:53.306Z", + "sgv": 103, + "glucose": 103, + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "direction": "Flat", + "unfiltered": 105, + "_id": "52AAE7AD-6EFB-462D-95C0-ED3ED5351FFF", + "activationDate": "2024-07-28T22:17:35.909Z" + }, + { + "sgv": 105, + "unfiltered": 107, + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "date": 1723052157741, + "glucose": 105, + "type": "sgv", + "direction": "Flat", + "_id": "235D5ACA-A05B-4EC6-9B5E-E90D3F6C36FE", + "activationDate": "2024-07-28T22:17:35.909Z", + "dateString": "2024-08-07T17:35:57.741Z" + }, + { + "type": "sgv", + "activationDate": "2024-07-28T22:17:35.909Z", + "date": 1723051858959, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "sgv": 106, + "unfiltered": 109, + "dateString": "2024-08-07T17:30:58.960Z", + "_id": "565CCD3C-7706-4355-A5AF-E3E12DC785AE", + "glucose": 106 + }, + { + "_id": "31540D70-671B-44CF-B31F-4C0318F7E545", + "unfiltered": 108, + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "sgv": 106, + "type": "sgv", + "dateString": "2024-08-07T17:25:53.495Z", + "date": 1723051553494, + "activationDate": "2024-07-28T22:17:35.909Z", + "direction": "Flat", + "glucose": 106 + }, + { + "glucose": 106, + "date": 1723051252863, + "unfiltered": 109, + "type": "sgv", + "dateString": "2024-08-07T17:20:52.863Z", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "sgv": 106, + "activationDate": "2024-07-28T22:17:35.909Z", + "direction": "Flat", + "_id": "E768B1BE-0B02-480D-B413-DACC11E04F02" + }, + { + "sgv": 108, + "glucose": 108, + "activationDate": "2024-07-28T22:17:35.909Z", + "dateString": "2024-08-07T17:15:55.481Z", + "unfiltered": 110, + "type": "sgv", + "direction": "Flat", + "_id": "289B0219-753E-4394-9990-D3C9EEE93E2E", + "date": 1723050955481, + "sessionStartDate": "2024-07-28T22:42:35.909Z" + }, + { + "date": 1723050657408, + "activationDate": "2024-07-28T22:17:35.909Z", + "direction": "Flat", + "_id": "771694F1-3BA5-42A5-8E93-EA39DBF7B8FB", + "sgv": 110, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "unfiltered": 112, + "dateString": "2024-08-07T17:10:57.408Z", + "glucose": 110 + }, + { + "_id": "C75BB663-6771-4486-BDC7-02218A868303", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "activationDate": "2024-07-28T22:17:35.909Z", + "direction": "Flat", + "glucose": 112, + "sgv": 112, + "date": 1723050352470, + "unfiltered": 113, + "dateString": "2024-08-07T17:05:52.470Z" + }, + { + "sgv": 114, + "date": 1723050055049, + "direction": "Flat", + "glucose": 114, + "dateString": "2024-08-07T17:00:55.050Z", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "unfiltered": 115, + "type": "sgv", + "activationDate": "2024-07-28T22:17:35.909Z", + "_id": "B57F8360-373A-4D2C-91A4-2D62EF3C815D" + }, + { + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "dateString": "2024-08-07T16:55:55.823Z", + "date": 1723049755823, + "glucose": 114, + "type": "sgv", + "_id": "8C16BBD2-AAAC-4218-B27A-27BF9A16A78F", + "direction": "Flat", + "unfiltered": 114, + "sgv": 114, + "activationDate": "2024-07-28T22:17:35.909Z" + }, + { + "sgv": 116, + "dateString": "2024-08-07T16:50:54.230Z", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "type": "sgv", + "date": 1723049454229, + "_id": "2D4A4F8D-1B63-41D7-85FC-9606358326B2", + "activationDate": "2024-07-28T22:17:35.909Z", + "unfiltered": 117, + "glucose": 116 + }, + { + "activationDate": "2024-07-28T22:17:35.909Z", + "_id": "032FB897-6CD1-45E5-9E0B-FC347A7DBE6A", + "date": 1723049155863, + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "dateString": "2024-08-07T16:45:55.864Z", + "sgv": 118, + "direction": "Flat", + "glucose": 118, + "type": "sgv", + "unfiltered": 120 + }, + { + "_id": "08AF3F40-E52C-48D4-8DE6-43975E8C6E09", + "date": 1723048852909, + "dateString": "2024-08-07T16:40:52.909Z", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.909Z", + "unfiltered": 120, + "sgv": 118, + "direction": "Flat", + "glucose": 118, + "activationDate": "2024-07-28T22:17:35.909Z" + }, + { + "direction": "Flat", + "type": "sgv", + "dateString": "2024-08-07T16:30:52.690Z", + "date": 1723048252690, + "glucose": 119, + "activationDate": "2024-07-28T22:17:35.690Z", + "_id": "C115E86D-3624-4FFC-8F31-E0D28FBE9897", + "unfiltered": 122, + "sgv": 119, + "sessionStartDate": "2024-07-28T22:42:35.690Z" + }, + { + "type": "sgv", + "unfiltered": 122, + "sessionStartDate": "2024-07-28T22:42:35.450Z", + "activationDate": "2024-07-28T22:17:35.450Z", + "glucose": 119, + "dateString": "2024-08-07T16:25:52.450Z", + "sgv": 119, + "date": 1723047952449, + "direction": "Flat", + "_id": "3602D004-6A27-4D38-867D-3235D3E89EDB" + }, + { + "date": 1723047652741, + "dateString": "2024-08-07T16:20:52.741Z", + "_id": "0C759F34-4928-4BCA-B4A1-8C0901CA107F", + "sgv": 120, + "unfiltered": 121, + "direction": "Flat", + "glucose": 120, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:35.741Z", + "activationDate": "2024-07-28T22:17:35.741Z" + }, + { + "_id": "1EA4EFBD-7497-4909-B961-1292A209D53B", + "dateString": "2024-08-07T16:15:52.396Z", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "unfiltered": 122, + "date": 1723047352396, + "glucose": 121, + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 121 + }, + { + "dateString": "2024-08-07T16:10:58.945Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723047058945, + "_id": "D2EFFAA0-1EA0-485F-88B8-1CF11628C22E", + "unfiltered": 124, + "sgv": 122, + "glucose": 122, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "direction": "Flat" + }, + { + "sgv": 124, + "type": "sgv", + "date": 1723046753469, + "dateString": "2024-08-07T16:05:53.469Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 128, + "_id": "61C1EB0C-E60F-4A6B-8883-BC2D3C340250", + "glucose": 124, + "activationDate": "2024-07-28T22:17:38.002Z", + "direction": "Flat" + }, + { + "unfiltered": 129, + "date": 1723046453756, + "glucose": 125, + "_id": "2E40F12C-D831-4DB7-B648-7A117B609142", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T16:00:53.756Z", + "direction": "Flat", + "sgv": 125, + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "glucose": 124, + "date": 1723046152875, + "type": "sgv", + "_id": "B18DBDB7-AE4F-4831-98A9-EE5090B88905", + "sgv": 124, + "dateString": "2024-08-07T15:55:52.876Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 124, + "direction": "Flat" + }, + { + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "42AD7CB7-34EE-4624-B71E-77824A9694FF", + "unfiltered": 130, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T15:50:56.466Z", + "sgv": 129, + "date": 1723045856465, + "glucose": 129, + "type": "sgv", + "direction": "Flat" + }, + { + "unfiltered": 136, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "date": 1723045554228, + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 132, + "sgv": 132, + "dateString": "2024-08-07T15:45:54.229Z", + "direction": "Flat", + "_id": "FA55798B-5553-435C-B53A-F07DEEEA53F8" + }, + { + "dateString": "2024-08-07T15:40:54.483Z", + "date": 1723045254483, + "_id": "E2E624A0-1AA5-4D70-8B4E-1BC110609114", + "direction": "Flat", + "type": "sgv", + "unfiltered": 135, + "sgv": 131, + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 131, + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "date": 1723044954099, + "direction": "Flat", + "sgv": 131, + "_id": "885AD08F-088B-4219-8F06-E4C310714E70", + "dateString": "2024-08-07T15:35:54.099Z", + "type": "sgv", + "unfiltered": 132, + "glucose": 131, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "direction": "Flat", + "sgv": 133, + "unfiltered": 136, + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T15:30:55.209Z", + "_id": "ECA8DDDA-EE41-419B-B53B-0EF81114F83E", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "glucose": 133, + "date": 1723044655208 + }, + { + "_id": "98CC9DA6-E640-40B9-9CCB-3D82FBA6D099", + "glucose": 135, + "direction": "Flat", + "unfiltered": 138, + "sgv": 135, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T15:25:56.191Z", + "date": 1723044356191 + }, + { + "glucose": 137, + "dateString": "2024-08-07T15:20:59.184Z", + "direction": "Flat", + "date": 1723044059183, + "_id": "DB9596F8-DFDD-4497-AEF7-63911FBDE8C9", + "sgv": 137, + "unfiltered": 140, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "type": "sgv" + }, + { + "dateString": "2024-08-07T15:16:04.982Z", + "glucose": 140, + "_id": "9AF1D66B-76F5-4C7B-9572-2E2EB27B1195", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 140, + "date": 1723043764981, + "type": "sgv", + "unfiltered": 143 + }, + { + "date": 1723043455102, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 141, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "_id": "C6844046-E937-4D83-A733-1EE20FC1F343", + "dateString": "2024-08-07T15:10:55.102Z", + "glucose": 141, + "unfiltered": 142 + }, + { + "unfiltered": 143, + "glucose": 144, + "dateString": "2024-08-07T15:05:53.385Z", + "direction": "Flat", + "sgv": 144, + "date": 1723043153384, + "_id": "CA8D077B-437B-4F37-A3D6-5EDFD9377F3E", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "dateString": "2024-08-07T15:00:54.836Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 151, + "sgv": 148, + "glucose": 148, + "direction": "Flat", + "type": "sgv", + "_id": "3660FA8F-D503-436F-B70E-D459D2AC87ED", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723042854836 + }, + { + "direction": "Flat", + "glucose": 148, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T14:55:58.515Z", + "unfiltered": 152, + "date": 1723042558514, + "type": "sgv", + "_id": "C83C0CAA-7716-491A-843E-8EDB89EE0850", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 148 + }, + { + "glucose": 145, + "direction": "Flat", + "date": 1723042253614, + "type": "sgv", + "_id": "64B43573-8C50-4C39-9FE9-25DF2B65EFEF", + "sgv": 145, + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 150, + "dateString": "2024-08-07T14:50:53.615Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "sgv": 143, + "unfiltered": 147, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T14:45:54.133Z", + "glucose": 143, + "date": 1723041954132, + "direction": "Flat", + "_id": "915AA9F7-6042-436A-A857-5D3F2A4A4800", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "040C777D-0B6E-4889-BC9C-CA3FF8854790", + "dateString": "2024-08-07T14:40:54.202Z", + "unfiltered": 148, + "direction": "Flat", + "sgv": 146, + "date": 1723041654202, + "glucose": 146, + "type": "sgv" + }, + { + "sgv": 150, + "_id": "CBDB426B-7302-4817-82D9-FD332C232593", + "date": 1723041360531, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 153, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "dateString": "2024-08-07T14:36:00.532Z", + "glucose": 150 + }, + { + "unfiltered": 154, + "dateString": "2024-08-07T14:30:59.032Z", + "date": 1723041059032, + "type": "sgv", + "sgv": 155, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "EDB85F1B-CABA-4154-AEB1-C537805EA5DF", + "glucose": 155, + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "dateString": "2024-08-07T14:25:55.324Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 164, + "type": "sgv", + "direction": "Flat", + "date": 1723040755324, + "_id": "2281D81C-A50D-422A-8537-01EAAA8396C2", + "glucose": 164, + "unfiltered": 165 + }, + { + "dateString": "2024-08-07T14:20:54.899Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723040454899, + "unfiltered": 167, + "glucose": 167, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 167, + "direction": "Flat", + "_id": "0EE0CDD8-FCDE-42F7-9B7E-533F87669624" + }, + { + "unfiltered": 169, + "glucose": 169, + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723040158980, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "_id": "D2EAC43B-95EE-4E00-91E8-96E2A0746CE6", + "sgv": 169, + "type": "sgv", + "dateString": "2024-08-07T14:15:58.980Z", + "direction": "Flat" + }, + { + "unfiltered": 171, + "date": 1723039872523, + "glucose": 170, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 170, + "_id": "D52CC6B1-406C-41EF-B78F-6FF3ADDFABDB", + "direction": "Flat", + "dateString": "2024-08-07T14:11:12.523Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "type": "sgv", + "sgv": 169, + "date": 1723039552355, + "unfiltered": 170, + "dateString": "2024-08-07T14:05:52.356Z", + "direction": "Flat", + "_id": "15EC2AE9-0695-4070-B45D-D501DD2D3E75", + "glucose": 169, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "glucose": 168, + "type": "sgv", + "date": 1723039256881, + "_id": "92871BB6-D943-40B4-B976-441B1463240C", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 168, + "activationDate": "2024-07-28T22:17:38.002Z", + "direction": "Flat", + "dateString": "2024-08-07T14:00:56.882Z", + "unfiltered": 172 + }, + { + "_id": "AFBA8385-1358-4E37-A1CA-B94226FD17C7", + "direction": "Flat", + "glucose": 164, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T13:55:56.875Z", + "sgv": 164, + "date": 1723038956875, + "unfiltered": 170, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "glucose": 161, + "sgv": 161, + "direction": "Flat", + "dateString": "2024-08-07T13:51:01.121Z", + "date": 1723038661121, + "_id": "1A83EDC9-312F-445C-BE13-8469F49D2515", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 167 + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "0DF9E3AE-13B5-4DF8-8E66-D71B5571B4E7", + "type": "sgv", + "dateString": "2024-08-07T13:45:57.659Z", + "sgv": 160, + "unfiltered": 164, + "date": 1723038357659, + "direction": "Flat", + "glucose": 160 + }, + { + "date": 1723038054353, + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 165, + "glucose": 164, + "dateString": "2024-08-07T13:40:54.353Z", + "_id": "4142595B-DC71-4CE7-B99B-DB8C4BA5612D", + "sgv": 164 + }, + { + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 168, + "date": 1723037752607, + "dateString": "2024-08-07T13:35:52.608Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 166, + "direction": "Flat", + "_id": "29C48297-A807-41F1-8A51-BF0952017084", + "glucose": 168 + }, + { + "direction": "Flat", + "glucose": 178, + "dateString": "2024-08-07T13:30:53.138Z", + "date": 1723037453137, + "unfiltered": 180, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "43C0562F-B3C4-4CC7-833C-D8A841CC3CE4", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 178 + }, + { + "dateString": "2024-08-07T13:25:56.901Z", + "unfiltered": 182, + "sgv": 180, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723037156900, + "_id": "40032A38-B32B-4115-8E7E-C69092E03BE4", + "direction": "Flat", + "type": "sgv", + "glucose": 180 + }, + { + "direction": "Flat", + "type": "sgv", + "glucose": 182, + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723036855397, + "unfiltered": 182, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T13:20:55.397Z", + "_id": "4CF7712C-7478-4833-B894-3AEF65A97172", + "sgv": 182 + }, + { + "dateString": "2024-08-07T13:16:07.147Z", + "date": 1723036567147, + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 186, + "type": "sgv", + "glucose": 184, + "sgv": 184, + "direction": "Flat", + "_id": "922F881F-8D34-4330-9AFF-7FC00E3E6F4B", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "glucose": 185, + "dateString": "2024-08-07T13:10:54.809Z", + "direction": "Flat", + "_id": "740E5C97-DC2B-4797-BE86-6D280DDF5F86", + "sgv": 185, + "date": 1723036254808, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 186, + "type": "sgv" + }, + { + "direction": "Flat", + "_id": "354E77D4-C415-4295-B843-42F12BB49DDA", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "glucose": 184, + "type": "sgv", + "date": 1723035954797, + "sgv": 184, + "dateString": "2024-08-07T13:05:54.797Z", + "unfiltered": 185, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "glucose": 184, + "unfiltered": 185, + "_id": "BDD83BEA-0980-45B1-B349-6A890999F0A3", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T13:01:03.914Z", + "date": 1723035663913, + "sgv": 184, + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "direction": "Flat", + "date": 1723035356186, + "_id": "156094B7-C157-4B45-9F19-4BA7184F5E36", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T12:55:56.187Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 185, + "sgv": 183, + "type": "sgv", + "glucose": 183 + }, + { + "dateString": "2024-08-07T12:51:00.698Z", + "unfiltered": 182, + "direction": "Flat", + "type": "sgv", + "_id": "F71451EA-28E8-41B4-B76C-D3417477E55A", + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723035060698, + "sgv": 179, + "glucose": 179, + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "date": 1723034759012, + "unfiltered": 182, + "direction": "Flat", + "sgv": 178, + "_id": "8603965C-336D-416D-9625-0E2226A117A5", + "glucose": 178, + "dateString": "2024-08-07T12:45:59.013Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "type": "sgv" + }, + { + "glucose": 174, + "type": "sgv", + "sgv": 174, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "date": 1723034463870, + "unfiltered": 178, + "_id": "BB56AF0F-EC89-4A72-8DEB-641C8DE6C215", + "dateString": "2024-08-07T12:41:03.870Z" + }, + { + "dateString": "2024-08-07T12:35:52.665Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 176, + "direction": "Flat", + "glucose": 171, + "date": 1723034152664, + "_id": "9CB2A891-EA8E-4AB6-9C51-BF053DD8B8FF", + "sgv": 171, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "sgv": 168, + "dateString": "2024-08-07T12:30:52.876Z", + "direction": "Flat", + "_id": "DCC40BF5-06BB-49D6-9E09-B35AEA9F10EC", + "glucose": 168, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 170, + "date": 1723033852875, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "type": "sgv", + "date": 1723033557833, + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 168, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "_id": "61F04ABD-6741-4D00-A991-BD9D73A7BC48", + "unfiltered": 171, + "glucose": 168, + "dateString": "2024-08-07T12:25:57.834Z" + }, + { + "unfiltered": 172, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "_id": "2A690531-23B9-4045-911F-A564E8B1A73B", + "glucose": 169, + "sgv": 169, + "type": "sgv", + "direction": "Flat", + "dateString": "2024-08-07T12:20:54.610Z", + "date": 1723033254610 + }, + { + "glucose": 168, + "dateString": "2024-08-07T12:15:53.427Z", + "date": 1723032953427, + "direction": "Flat", + "unfiltered": 169, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 168, + "_id": "F30AFDBA-FBEC-41D4-86AA-DCAF9EC2B88A", + "type": "sgv" + }, + { + "sgv": 170, + "unfiltered": 170, + "glucose": 170, + "date": 1723032653837, + "direction": "Flat", + "_id": "86F28588-229C-45C9-AA01-741E77658ED9", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T12:10:53.838Z" + }, + { + "date": 1723032358196, + "_id": "FA40878C-D4F9-44BF-9381-3838DC6F9E9E", + "glucose": 174, + "dateString": "2024-08-07T12:05:58.196Z", + "unfiltered": 174, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "sgv": 174, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T12:00:57.489Z", + "date": 1723032057489, + "sgv": 174, + "direction": "Flat", + "_id": "C04C38EA-73A9-4980-8507-74CDDCFB420F", + "unfiltered": 175, + "glucose": 174, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "dateString": "2024-08-07T11:55:52.532Z", + "_id": "B274ED48-B575-4075-9C52-45E4CFD81054", + "glucose": 174, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 174, + "direction": "Flat", + "type": "sgv", + "date": 1723031752531, + "unfiltered": 178 + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 168, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "direction": "Flat", + "unfiltered": 173, + "glucose": 168, + "dateString": "2024-08-07T11:50:55.538Z", + "date": 1723031455538, + "_id": "EFBEBA7C-A3E7-4C05-8534-718F4144F22C" + }, + { + "date": 1723031159353, + "unfiltered": 172, + "glucose": 166, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "_id": "6ABC6F0E-0CC2-47BC-BFC9-B34F74FAF09D", + "activationDate": "2024-07-28T22:17:38.002Z", + "direction": "Flat", + "type": "sgv", + "dateString": "2024-08-07T11:45:59.353Z", + "sgv": 166 + }, + { + "type": "sgv", + "dateString": "2024-08-07T11:40:57.073Z", + "sgv": 160, + "unfiltered": 162, + "_id": "3F28C2EB-BDD1-42CC-8C0D-B1CFB88C5843", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723030857073, + "glucose": 160 + }, + { + "unfiltered": 162, + "dateString": "2024-08-07T11:35:53.527Z", + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 162, + "_id": "D6BD60C9-F3F4-4365-A160-9F161E9CF78B", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "sgv": 162, + "date": 1723030553527 + }, + { + "sgv": 166, + "dateString": "2024-08-07T11:30:53.893Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 165, + "_id": "D7830869-5C07-40A0-9D1B-C2E157EBE2A2", + "glucose": 166, + "date": 1723030253893, + "direction": "Flat" + }, + { + "glucose": 169, + "direction": "Flat", + "sgv": 169, + "unfiltered": 169, + "date": 1723029953153, + "_id": "EF5C36C1-2187-4E18-85B9-5201ADE74A80", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T11:25:53.153Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "type": "sgv", + "dateString": "2024-08-07T11:20:55.159Z", + "date": 1723029655159, + "_id": "DBFFA357-5ED2-4F0A-A734-DA85CD24B7D2", + "direction": "Flat", + "glucose": 170, + "unfiltered": 170, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 170 + }, + { + "sgv": 168, + "date": 1723029356023, + "_id": "0D362B29-E017-4AD2-9BE4-70F17E256020", + "glucose": 168, + "type": "sgv", + "dateString": "2024-08-07T11:15:56.024Z", + "unfiltered": 166, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "glucose": 165, + "direction": "FortyFiveUp", + "unfiltered": 167, + "sgv": 165, + "_id": "4A5FE0B5-AD82-465C-ADF3-49ED1A852F9A", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T11:10:57.787Z", + "date": 1723029057787 + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 163, + "_id": "7A7E2D62-FC9E-406C-975D-624041FD76A1", + "sgv": 158, + "direction": "FortyFiveUp", + "dateString": "2024-08-07T11:06:02.183Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723028762182, + "glucose": 158, + "type": "sgv" + }, + { + "dateString": "2024-08-07T11:00:52.772Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "FortyFiveUp", + "date": 1723028452772, + "glucose": 146, + "_id": "5B473A55-C578-418B-A44C-8A4D132A1DD1", + "unfiltered": 151, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 146 + }, + { + "type": "sgv", + "date": 1723028153894, + "sgv": 135, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 135, + "dateString": "2024-08-07T10:55:53.894Z", + "direction": "Flat", + "_id": "526FC10D-7209-4E51-BFBD-22FE888638D3", + "unfiltered": 137 + }, + { + "glucose": 131, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 135, + "_id": "AB687246-1D5D-44A9-A54F-B5504E10BF16", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T10:50:55.860Z", + "sgv": 131, + "direction": "Flat", + "date": 1723027855859 + }, + { + "direction": "Flat", + "glucose": 125, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 125, + "type": "sgv", + "date": 1723027555895, + "_id": "8F2B9CFC-6AAA-4AAC-B768-6E3A120C8B36", + "dateString": "2024-08-07T10:45:55.895Z", + "unfiltered": 130 + }, + { + "date": 1723027257261, + "dateString": "2024-08-07T10:40:57.262Z", + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 124, + "glucose": 120, + "_id": "3676EFE4-F3F8-4550-B985-DBB911F5E4D4", + "sgv": 120 + }, + { + "_id": "BBCAED57-38BE-4DB5-A909-A676AF295AEC", + "direction": "Flat", + "glucose": 119, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "date": 1723026954453, + "unfiltered": 121, + "dateString": "2024-08-07T10:35:54.454Z", + "sgv": 119, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "dateString": "2024-08-07T10:30:56.494Z", + "direction": "Flat", + "glucose": 118, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 120, + "_id": "AB56F3A6-02A8-4A59-B477-DDDAB46DE530", + "date": 1723026656493, + "sgv": 118, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "date": 1723026355080, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "glucose": 119, + "sgv": 119, + "_id": "82A938A1-22ED-4635-8973-5C1B16F42937", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T10:25:55.080Z", + "unfiltered": 121, + "direction": "Flat", + "type": "sgv" + }, + { + "_id": "52F5E80B-FE41-44A5-81C2-6063DFC24A67", + "direction": "Flat", + "type": "sgv", + "unfiltered": 123, + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T10:20:55.838Z", + "glucose": 120, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723026055837, + "sgv": 120 + }, + { + "date": 1723025754848, + "type": "sgv", + "direction": "Flat", + "_id": "AA898164-26FC-40E5-865A-CD7116B6D763", + "sgv": 121, + "glucose": 121, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 122, + "dateString": "2024-08-07T10:15:54.849Z" + }, + { + "dateString": "2024-08-07T10:10:56.316Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "DE52A040-4126-434F-AB9A-152F39B0DF4F", + "date": 1723025456315, + "sgv": 122, + "glucose": 122, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "unfiltered": 121 + }, + { + "_id": "BC479DA1-A603-47DC-9E11-5A523D71AE4B", + "date": 1723025154149, + "sgv": 122, + "direction": "Flat", + "unfiltered": 121, + "glucose": 122, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T10:05:54.149Z" + }, + { + "direction": "FortyFiveUp", + "_id": "DFC642A9-AF82-4007-B4D1-5DE0F5537D1C", + "unfiltered": 127, + "glucose": 123, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 123, + "dateString": "2024-08-07T10:00:54.146Z", + "date": 1723024854146 + }, + { + "sgv": 117, + "glucose": 117, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "_id": "F7C6499B-7BFC-4F13-90CC-7F9C3CD4E9B0", + "unfiltered": 124, + "direction": "FortyFiveUp", + "dateString": "2024-08-07T09:55:52.162Z", + "date": 1723024552161, + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "_id": "425BBF23-7788-4EDF-A6F0-BEAC4C14130C", + "sgv": 105, + "dateString": "2024-08-07T09:50:58.567Z", + "type": "sgv", + "glucose": 105, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 105, + "date": 1723024258566, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "type": "sgv", + "dateString": "2024-08-07T09:45:55.309Z", + "sgv": 105, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723023955308, + "unfiltered": 103, + "glucose": 105, + "_id": "43C03E0C-8A89-4D17-B768-8AFA1CAED50D" + }, + { + "sgv": 108, + "unfiltered": 112, + "dateString": "2024-08-07T09:40:55.401Z", + "_id": "1558EEF3-B2D8-447E-8C94-ABC22388A875", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723023655401, + "direction": "FortyFiveUp", + "glucose": 108, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "unfiltered": 108, + "type": "sgv", + "_id": "400A33B3-6AD0-4F98-A091-F469B31AF699", + "glucose": 103, + "date": 1723023355086, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 103, + "dateString": "2024-08-07T09:35:55.086Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "dateString": "2024-08-07T09:30:54.809Z", + "date": 1723023054808, + "direction": "Flat", + "_id": "A01CD9F4-518B-4203-A1D9-AB2CABBC596C", + "sgv": 95, + "type": "sgv", + "unfiltered": 98, + "glucose": 95 + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723022756813, + "sgv": 93, + "unfiltered": 96, + "direction": "Flat", + "_id": "0E4D9929-DC3B-4E01-92F0-5755BF753CA1", + "glucose": 93, + "dateString": "2024-08-07T09:25:56.813Z", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "_id": "150932F7-D021-473A-A8B0-7D6EC3A16775", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T09:20:53.734Z", + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723022453734, + "unfiltered": 99, + "glucose": 94, + "direction": "Flat", + "sgv": 94 + }, + { + "_id": "4A4AF1F1-5405-4D72-BBA5-216C5212AAEE", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "type": "sgv", + "dateString": "2024-08-07T09:15:55.502Z", + "sgv": 95, + "date": 1723022155501, + "glucose": 95, + "unfiltered": 101 + }, + { + "date": 1723021855212, + "direction": "Flat", + "glucose": 93, + "_id": "7C8FC8D1-6DE2-4A3E-9021-EA90C7556FB0", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 93, + "sgv": 93, + "dateString": "2024-08-07T09:10:55.212Z" + }, + { + "sgv": 100, + "unfiltered": 98, + "glucose": 100, + "_id": "5C4372CE-4D11-4A48-8A53-C79F5083CD6B", + "activationDate": "2024-07-28T22:17:38.002Z", + "type": "sgv", + "dateString": "2024-08-07T09:05:53.197Z", + "date": 1723021553196, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:38.002Z" + }, + { + "glucose": 108, + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "_id": "C936CD57-75FE-4BAD-A067-48088FDD7FB7", + "sgv": 108, + "date": 1723021255801, + "direction": "Flat", + "dateString": "2024-08-07T09:00:55.801Z", + "unfiltered": 110 + }, + { + "dateString": "2024-08-07T08:55:52.610Z", + "sgv": 109, + "unfiltered": 110, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "type": "sgv", + "date": 1723020952610, + "_id": "A4E66436-34EC-49C7-BFEA-813D411A9684", + "glucose": 109 + }, + { + "dateString": "2024-08-07T08:50:52.434Z", + "_id": "2E54A65E-929B-403F-9EE8-C4D74149A3D4", + "glucose": 109, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 110, + "date": 1723020652434, + "direction": "Flat", + "type": "sgv", + "sgv": 109, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723020359197, + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 108, + "unfiltered": 110, + "dateString": "2024-08-07T08:45:59.197Z", + "sgv": 108, + "direction": "Flat", + "_id": "5D66C1EC-983E-41B6-88FB-7C2C3EFF9F90" + }, + { + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T08:40:54.461Z", + "date": 1723020054461, + "direction": "Flat", + "sgv": 104, + "unfiltered": 106, + "_id": "ABB76A76-34DA-4080-B61D-2A36C9AB471E", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 104 + }, + { + "activationDate": "2024-07-28T22:17:38.002Z", + "sgv": 105, + "unfiltered": 111, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "date": 1723019754788, + "glucose": 105, + "_id": "6DCBB56C-846C-4C4C-A53E-B8A0E6BF8552", + "direction": "Flat", + "type": "sgv", + "dateString": "2024-08-07T08:35:54.788Z" + }, + { + "glucose": 103, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:38.002Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "type": "sgv", + "sgv": 103, + "date": 1723019455251, + "dateString": "2024-08-07T08:30:55.251Z", + "unfiltered": 112, + "_id": "B063DF7E-2435-4024-8CA5-666BCC74B036" + }, + { + "sgv": 94, + "type": "sgv", + "_id": "99910353-808E-4702-AA1D-E9C1E8B1425A", + "unfiltered": 86, + "date": 1723018552733, + "direction": "Flat", + "dateString": "2024-08-07T08:15:52.733Z", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "glucose": 94, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "sgv": 113, + "_id": "FC217D51-27E8-4CBE-93CB-B9ACB55A7421", + "activationDate": "2024-07-28T22:17:38.002Z", + "date": 1723018255244, + "unfiltered": 114, + "glucose": 113, + "direction": "Flat", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T08:10:55.245Z" + }, + { + "date": 1723017955115, + "direction": "Flat", + "_id": "39FD5817-5073-42D0-BB2D-FCEB700200A8", + "activationDate": "2024-07-28T22:17:38.002Z", + "unfiltered": 112, + "sgv": 112, + "glucose": 112, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "dateString": "2024-08-07T08:05:55.115Z" + }, + { + "_id": "E6A3E248-BA98-4922-9072-1DA3B0CEA333", + "type": "sgv", + "sgv": 112, + "dateString": "2024-08-07T08:00:52.582Z", + "unfiltered": 111, + "date": 1723017652582, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "glucose": 112, + "activationDate": "2024-07-28T22:17:38.002Z" + }, + { + "dateString": "2024-08-07T07:55:55.002Z", + "date": 1723017355002, + "direction": "Flat", + "_id": "8CC25AD4-AF24-443E-B537-ACDBA2F326AE", + "type": "sgv", + "activationDate": "2024-07-28T22:17:38.002Z", + "glucose": 112, + "sessionStartDate": "2024-07-28T22:42:38.002Z", + "unfiltered": 116, + "sgv": 112 + }, + { + "dateString": "2024-08-07T07:45:58.931Z", + "direction": "Flat", + "unfiltered": 110, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 107, + "_id": "4B71CA78-1E7E-42B5-A172-6E5F2F543928", + "glucose": 107, + "type": "sgv", + "date": 1723016758931 + }, + { + "_id": "ACE03A68-9294-4E52-9EF7-52B43903EC81", + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 104, + "sgv": 104, + "type": "sgv", + "unfiltered": 106, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723016452842, + "dateString": "2024-08-07T07:40:52.842Z" + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "type": "sgv", + "direction": "Flat", + "sgv": 100, + "date": 1723016152808, + "_id": "0C1FE02B-7721-46C8-B10A-1894F56056D0", + "dateString": "2024-08-07T07:35:52.809Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 100, + "unfiltered": 103 + }, + { + "direction": "Flat", + "unfiltered": 101, + "sgv": 98, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T07:30:56.338Z", + "date": 1723015856338, + "glucose": 98, + "_id": "B1D4F2AC-2AD1-43CA-88E3-F4481D79817D", + "type": "sgv" + }, + { + "unfiltered": 99, + "activationDate": "2024-07-28T22:17:36.105Z", + "direction": "Flat", + "_id": "DF03057D-E31C-4426-AA1F-99D8879AC0B2", + "date": 1723015557763, + "dateString": "2024-08-07T07:25:57.763Z", + "sgv": 96, + "glucose": 96, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "type": "sgv" + }, + { + "sgv": 95, + "unfiltered": 98, + "date": 1723015252958, + "glucose": 95, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T07:20:52.958Z", + "direction": "Flat", + "_id": "3B77CFB3-1827-490E-83C2-06AA85C6370E", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "date": 1723014962759, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 93, + "direction": "Flat", + "_id": "B03FED11-428E-469D-BB34-A3BD4CA5D899", + "glucose": 93, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T07:16:02.759Z", + "unfiltered": 96, + "type": "sgv" + }, + { + "unfiltered": 96, + "type": "sgv", + "date": 1723014654643, + "dateString": "2024-08-07T07:10:54.644Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "glucose": 90, + "sgv": 90, + "_id": "9C2A0C6A-A7D0-4EEB-8BE5-CE511CCF627F" + }, + { + "glucose": 87, + "unfiltered": 93, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "_id": "BF60A26B-3957-411C-8FB7-54BEDE4B0EB9", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 87, + "dateString": "2024-08-07T07:05:52.951Z", + "date": 1723014352951 + }, + { + "dateString": "2024-08-07T07:00:57.737Z", + "sgv": 84, + "direction": "Flat", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "_id": "236E314C-82DE-45B8-A9E8-405E6E8B7A8F", + "date": 1723014057736, + "unfiltered": 83, + "glucose": 84 + }, + { + "_id": "F4674F58-D645-4213-A21C-1FB756E7EAF5", + "dateString": "2024-08-07T06:55:57.708Z", + "type": "sgv", + "sgv": 89, + "date": 1723013757708, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 89, + "unfiltered": 89 + }, + { + "type": "sgv", + "unfiltered": 94, + "direction": "Flat", + "_id": "424E02AA-3D17-47D9-A3A7-7F2055FDBD24", + "sgv": 92, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723013457532, + "dateString": "2024-08-07T06:50:57.533Z", + "glucose": 92 + }, + { + "date": 1723013155520, + "unfiltered": 94, + "glucose": 92, + "sgv": 92, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T06:45:55.521Z", + "_id": "3064CC37-8175-4B4E-85F4-24A2BCF0D4B4", + "type": "sgv" + }, + { + "dateString": "2024-08-07T06:40:55.375Z", + "unfiltered": 95, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "_id": "DC79D2B2-D793-4731-8DC3-3425409474F5", + "glucose": 93, + "sgv": 93, + "date": 1723012855374, + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "unfiltered": 95, + "glucose": 92, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "dateString": "2024-08-07T06:35:53.867Z", + "date": 1723012553867, + "_id": "8B7B7B79-FA48-469A-B633-D494099050BB", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 92 + }, + { + "sgv": 93, + "direction": "Flat", + "glucose": 93, + "type": "sgv", + "dateString": "2024-08-07T06:30:55.521Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 96, + "_id": "46EAA890-2944-4F01-B8CB-B79AEEB96A93", + "date": 1723012255521, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "direction": "Flat", + "glucose": 94, + "_id": "F40F2295-4245-4CD3-93E7-55EA377CE7BF", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 97, + "dateString": "2024-08-07T06:25:54.429Z", + "type": "sgv", + "date": 1723011954428, + "sgv": 94 + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T06:20:52.659Z", + "sgv": 95, + "unfiltered": 98, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "1B110344-F576-41AF-B14F-0076A4403581", + "glucose": 95, + "type": "sgv", + "date": 1723011652658 + }, + { + "dateString": "2024-08-07T06:15:52.932Z", + "type": "sgv", + "unfiltered": 99, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 96, + "_id": "51AA8DF6-9CB7-4C91-97D9-EECB700DE793", + "sgv": 96, + "date": 1723011352931 + }, + { + "glucose": 97, + "unfiltered": 101, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "D8B53685-CC82-47BB-A927-39E523E49396", + "direction": "Flat", + "date": 1723011055036, + "type": "sgv", + "sgv": 97, + "dateString": "2024-08-07T06:10:55.037Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "direction": "Flat", + "sgv": 98, + "date": 1723010753653, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T06:05:53.653Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 98, + "_id": "0EF90E17-9FD5-4509-9724-22AF9F519D43", + "unfiltered": 102 + }, + { + "date": 1723010452316, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 99, + "glucose": 99, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T06:00:52.317Z", + "direction": "Flat", + "_id": "E148E5A2-A4C0-4818-9138-58797B940CFF", + "unfiltered": 102 + }, + { + "date": 1723010154380, + "glucose": 101, + "sgv": 101, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "D304BDCE-4B08-41D1-B172-C43D4AC00562", + "unfiltered": 103, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "type": "sgv", + "dateString": "2024-08-07T05:55:54.381Z" + }, + { + "direction": "Flat", + "sgv": 104, + "unfiltered": 104, + "activationDate": "2024-07-28T22:17:36.105Z", + "type": "sgv", + "_id": "0090C09B-E873-403F-8267-BB57F689FE75", + "glucose": 104, + "dateString": "2024-08-07T05:50:55.061Z", + "date": 1723009855061, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 108, + "_id": "FB74A045-4686-4B5B-AAE1-D1A3C57220D9", + "glucose": 108, + "date": 1723009555110, + "unfiltered": 107, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T05:45:55.110Z" + }, + { + "_id": "B553484A-F68B-48CD-86A4-67F0D527820B", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "unfiltered": 112, + "glucose": 111, + "sgv": 111, + "date": 1723009252490, + "direction": "Flat", + "dateString": "2024-08-07T05:40:52.490Z" + }, + { + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "506DDF2D-05C6-4C52-9527-F699A511026C", + "date": 1723008953645, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 110, + "direction": "Flat", + "glucose": 110, + "dateString": "2024-08-07T05:35:53.646Z", + "unfiltered": 112 + }, + { + "_id": "725C9DF7-987E-47D3-8F58-63E8A77B7178", + "glucose": 109, + "type": "sgv", + "unfiltered": 113, + "date": 1723008654487, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T05:30:54.487Z", + "sgv": 109, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 104, + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 107, + "sgv": 104, + "dateString": "2024-08-07T05:25:54.724Z", + "date": 1723008354723, + "direction": "Flat", + "type": "sgv", + "_id": "13620E18-EDEE-4BC0-9B9F-53F0798B63D2" + }, + { + "sgv": 102, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 102, + "unfiltered": 105, + "type": "sgv", + "dateString": "2024-08-07T05:20:53.580Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "FF62EC50-849F-4C6A-8D68-391368DFCF1A", + "date": 1723008053580, + "direction": "Flat" + }, + { + "direction": "Flat", + "type": "sgv", + "date": 1723007753032, + "sgv": 100, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 100, + "unfiltered": 104, + "_id": "A4D15DB7-12D6-4A1C-9D11-5598AFEC6F26", + "dateString": "2024-08-07T05:15:53.032Z" + }, + { + "unfiltered": 101, + "date": 1723007454078, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "B40E9193-F674-4B77-B274-9BDA8DEF17E9", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 98, + "glucose": 98, + "direction": "Flat", + "dateString": "2024-08-07T05:10:54.078Z" + }, + { + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "_id": "8A11746E-270C-48A6-A608-BF8A1E8854E7", + "glucose": 97, + "type": "sgv", + "unfiltered": 98, + "dateString": "2024-08-07T05:05:54.820Z", + "sgv": 97, + "date": 1723007154820 + }, + { + "dateString": "2024-08-07T05:00:56.394Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "_id": "2037B9CD-DF26-4834-87A6-70298D5FDCFA", + "direction": "Flat", + "glucose": 98, + "sgv": 98, + "unfiltered": 100, + "type": "sgv", + "date": 1723006856393 + }, + { + "_id": "6293599F-83AF-436C-B197-FF54D5B14B89", + "date": 1723006552992, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 97, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T04:55:52.993Z", + "sgv": 97, + "type": "sgv", + "direction": "Flat", + "unfiltered": 100 + }, + { + "_id": "DDFA27E0-4C53-4C47-A35B-F1B884F199C1", + "unfiltered": 99, + "type": "sgv", + "dateString": "2024-08-07T04:50:52.435Z", + "direction": "Flat", + "date": 1723006252435, + "glucose": 96, + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 96, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 92, + "type": "sgv", + "date": 1723005954347, + "glucose": 92, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "_id": "05BFEDBF-DEBF-4101-8424-1E67B13E491F", + "dateString": "2024-08-07T04:45:54.348Z", + "unfiltered": 96 + }, + { + "dateString": "2024-08-07T04:40:52.450Z", + "glucose": 88, + "type": "sgv", + "unfiltered": 91, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "D7C2EA87-1DAD-4959-90C2-1F9ABA461C66", + "date": 1723005652450, + "sgv": 88, + "direction": "Flat" + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "BED2B0CD-30C4-45E1-97A6-A2712A667C08", + "dateString": "2024-08-07T04:35:53.971Z", + "unfiltered": 88, + "sgv": 86, + "date": 1723005353971, + "direction": "Flat", + "glucose": 86, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "unfiltered": 87, + "type": "sgv", + "_id": "DBB0931E-2675-41F0-8B1A-1F234CED321B", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 84, + "glucose": 84, + "dateString": "2024-08-07T04:30:54.074Z", + "direction": "Flat", + "date": 1723005054074 + }, + { + "dateString": "2024-08-07T04:25:55.906Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723004755905, + "unfiltered": 86, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 82, + "sgv": 82, + "_id": "62CA880C-A538-4C24-9FCE-761AEF24F7B4", + "type": "sgv" + }, + { + "unfiltered": 83, + "date": 1723004453508, + "direction": "Flat", + "glucose": 80, + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 80, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "type": "sgv", + "_id": "F9F98671-E81B-496E-ADD0-84786729FFB2", + "dateString": "2024-08-07T04:20:53.509Z" + }, + { + "unfiltered": 84, + "dateString": "2024-08-07T04:15:52.286Z", + "type": "sgv", + "_id": "E2BD3614-7714-4240-A086-2978F59E26DB", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 79, + "date": 1723004152286, + "direction": "Flat", + "glucose": 79 + }, + { + "_id": "79096D7A-EEB7-44A1-976C-EE641E15D0AA", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723003852033, + "unfiltered": 80, + "sgv": 76, + "type": "sgv", + "glucose": 76, + "dateString": "2024-08-07T04:10:52.034Z", + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "date": 1723003554258, + "direction": "Flat", + "_id": "326F183C-66E8-4F7A-A670-A7468382B3F1", + "unfiltered": 76, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 76, + "glucose": 76, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T04:05:54.259Z" + }, + { + "glucose": 81, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 81, + "unfiltered": 82, + "dateString": "2024-08-07T04:01:04.723Z", + "direction": "Flat", + "_id": "D9D739C0-7361-4065-977E-C94B84F678B0", + "date": 1723003264722 + }, + { + "glucose": 83, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "unfiltered": 84, + "_id": "77DBF313-A086-40E4-9164-7DA434F765D6", + "date": 1723002954564, + "dateString": "2024-08-07T03:55:54.564Z", + "sgv": 83, + "direction": "Flat" + }, + { + "sgv": 84, + "direction": "Flat", + "unfiltered": 86, + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 84, + "type": "sgv", + "dateString": "2024-08-07T03:50:54.656Z", + "_id": "9436C7BC-516F-4C5E-B752-C41A58A293A0", + "date": 1723002654656, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "type": "sgv", + "date": 1723002354212, + "glucose": 85, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T03:45:54.213Z", + "sgv": 85, + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 87, + "_id": "F3F7B722-FD3A-4CDC-B56F-52310D66DFC1", + "direction": "Flat" + }, + { + "date": 1723002057088, + "_id": "317B5667-CA94-4E57-A3A7-21045C71DB56", + "type": "sgv", + "sgv": 86, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T03:40:57.089Z", + "glucose": 86, + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 89, + "direction": "Flat" + }, + { + "sgv": 89, + "unfiltered": 93, + "direction": "Flat", + "dateString": "2024-08-07T03:35:56.306Z", + "_id": "F9D9F492-F317-4664-944F-78C318960D87", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 89, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723001756305 + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1723001453114, + "_id": "88012590-7360-4C31-AE59-F17363E463D8", + "dateString": "2024-08-07T03:30:53.115Z", + "unfiltered": 91, + "type": "sgv", + "sgv": 88, + "direction": "Flat", + "glucose": 88 + }, + { + "unfiltered": 92, + "direction": "Flat", + "type": "sgv", + "sgv": 90, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T03:25:57.703Z", + "_id": "BF54CD02-2DFC-421B-A1E4-C44CB163AA0C", + "glucose": 90, + "date": 1723001157702, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "unfiltered": 95, + "_id": "49E1C052-7770-41AF-8C01-B065B1A6F4B3", + "type": "sgv", + "sgv": 94, + "direction": "Flat", + "date": 1723000855374, + "dateString": "2024-08-07T03:20:55.375Z", + "glucose": 94, + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "_id": "481F4254-208F-487F-A9DB-352149FF0F9B", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "direction": "Flat", + "dateString": "2024-08-07T03:15:58.467Z", + "date": 1723000558466, + "sgv": 98, + "unfiltered": 98, + "glucose": 98, + "type": "sgv" + }, + { + "sgv": 102, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T03:10:54.140Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 102, + "unfiltered": 102, + "direction": "Flat", + "date": 1723000254140, + "_id": "8AF1DD22-0B23-460C-80B6-CB148B2C96C1" + }, + { + "unfiltered": 104, + "date": 1722999954095, + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 104, + "sgv": 104, + "dateString": "2024-08-07T03:05:54.095Z", + "_id": "A2CD4F2B-F365-4FFE-8227-480EE833864A", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722999655702, + "dateString": "2024-08-07T03:00:55.703Z", + "unfiltered": 106, + "sgv": 106, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "897729E4-0E95-4E22-910E-DBE4BC37AFD8", + "glucose": 106, + "type": "sgv", + "direction": "Flat" + }, + { + "type": "sgv", + "dateString": "2024-08-07T02:55:57.056Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 107, + "unfiltered": 109, + "activationDate": "2024-07-28T22:17:36.105Z", + "glucose": 107, + "direction": "Flat", + "date": 1722999357056, + "_id": "EAD1B3A5-9C27-4B35-9631-D3EA9CD7985E" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T02:50:54.795Z", + "date": 1722999054794, + "glucose": 108, + "_id": "213D50B7-16C0-4070-9974-2A918EA7C16C", + "sgv": 108, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 112, + "type": "sgv" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722998754768, + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 106, + "sgv": 104, + "_id": "34412001-1C21-4281-9E3A-FB17B3C27B03", + "dateString": "2024-08-07T02:45:54.768Z", + "glucose": 104, + "type": "sgv" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722998453632, + "direction": "Flat", + "dateString": "2024-08-07T02:40:53.633Z", + "glucose": 104, + "_id": "EEE4FDF4-A60B-47F3-9E61-4C8996F4A12B", + "type": "sgv", + "unfiltered": 105, + "sgv": 104, + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "_id": "4D4BEAC8-0CE8-47F2-97B9-177D788E4D24", + "sgv": 104, + "glucose": 104, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T02:36:04.370Z", + "unfiltered": 105, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722998164370, + "direction": "Flat" + }, + { + "_id": "3F8B282E-F1B5-4A9B-98EF-B2EDDEA4BF90", + "glucose": 103, + "unfiltered": 104, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 103, + "date": 1722997852371, + "dateString": "2024-08-07T02:30:52.372Z", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "type": "sgv" + }, + { + "type": "sgv", + "dateString": "2024-08-07T02:25:54.038Z", + "sgv": 101, + "_id": "A87C8471-9E31-4E1B-8535-80E27DA6BA1C", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 103, + "direction": "Flat", + "date": 1722997554038, + "glucose": 101, + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "type": "sgv", + "dateString": "2024-08-07T02:20:53.141Z", + "sgv": 98, + "_id": "BB0E5F37-2E87-45E9-8272-E3D31D51DF09", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 101, + "glucose": 98, + "date": 1722997253140 + }, + { + "type": "sgv", + "_id": "14BADA3A-E9AE-48D5-B1FB-533EA88BA71B", + "glucose": 95, + "dateString": "2024-08-07T02:15:55.451Z", + "date": 1722996955450, + "unfiltered": 99, + "activationDate": "2024-07-28T22:17:36.105Z", + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 95 + }, + { + "sgv": 93, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T02:10:52.961Z", + "glucose": 93, + "unfiltered": 97, + "_id": "48D7C34A-A699-4A4C-BEA2-6413B836B1C3", + "type": "sgv", + "date": 1722996652961, + "direction": "Flat", + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "glucose": 91, + "type": "sgv", + "date": 1722996357072, + "_id": "3038EBEA-1C11-483E-8072-5FF52F4AD466", + "dateString": "2024-08-07T02:05:57.073Z", + "sgv": 91, + "unfiltered": 95, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "direction": "Flat", + "glucose": 90, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722996057407, + "sgv": 90, + "unfiltered": 93, + "_id": "0396B774-8575-4890-AC17-794AA9E54F41", + "dateString": "2024-08-07T02:00:57.408Z" + }, + { + "sgv": 90, + "dateString": "2024-08-07T01:55:58.576Z", + "type": "sgv", + "direction": "Flat", + "unfiltered": 93, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "_id": "EF795DE5-90EC-479B-AE01-3F22D6DB0659", + "date": 1722995758576, + "glucose": 90 + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "date": 1722995452667, + "dateString": "2024-08-07T01:50:52.668Z", + "unfiltered": 92, + "glucose": 91, + "_id": "7B247073-6AF5-47E3-85DF-E3F4C6E5A12F", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "sgv": 91 + }, + { + "sgv": 91, + "dateString": "2024-08-07T01:45:54.708Z", + "date": 1722995154708, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "unfiltered": 92, + "glucose": 91, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "E090AFE9-7BF3-4449-A20B-78E1F35539CA" + }, + { + "dateString": "2024-08-07T01:40:52.250Z", + "glucose": 94, + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 94, + "unfiltered": 97, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "F3B28AAE-770C-4354-8EC1-00DCEFD44E5A", + "date": 1722994852249, + "direction": "Flat" + }, + { + "_id": "3DAA8D50-1106-4C5A-87A2-1421FD5F5891", + "type": "sgv", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "unfiltered": 99, + "glucose": 95, + "direction": "Flat", + "sgv": 95, + "date": 1722994554826, + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T01:35:54.827Z" + }, + { + "glucose": 95, + "date": 1722994255498, + "direction": "Flat", + "_id": "AF7D2C06-819F-42C4-B77F-42DC6AA40F3B", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "unfiltered": 99, + "sgv": 95, + "dateString": "2024-08-07T01:30:55.498Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "type": "sgv" + }, + { + "type": "sgv", + "glucose": 95, + "unfiltered": 98, + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 95, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "dateString": "2024-08-07T01:25:54.255Z", + "date": 1722993954255, + "_id": "3DEB16F3-CB79-4B89-A46E-5772BE11AEE6" + }, + { + "type": "sgv", + "glucose": 97, + "sgv": 97, + "date": 1722993652904, + "direction": "Flat", + "_id": "CB1B7A93-B470-4026-9D34-B5EE6C5A616F", + "unfiltered": 100, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T01:20:52.905Z", + "activationDate": "2024-07-28T22:17:36.105Z" + }, + { + "dateString": "2024-08-07T01:15:55.618Z", + "sgv": 100, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722993355618, + "direction": "Flat", + "type": "sgv", + "unfiltered": 101, + "glucose": 100, + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "17331225-A120-44B2-B1D5-7017E81A5B6A" + }, + { + "direction": "Flat", + "_id": "36FF1588-1174-4EAD-8DED-3B5740F98816", + "glucose": 103, + "date": 1722993057685, + "type": "sgv", + "unfiltered": 102, + "dateString": "2024-08-07T01:10:57.685Z", + "sgv": 103, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z" + }, + { + "unfiltered": 109, + "type": "sgv", + "direction": "Flat", + "glucose": 108, + "_id": "379F4301-FE5F-42DB-8EA7-E1872C49A4E4", + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T01:06:12.845Z", + "date": 1722992772844, + "sgv": 108 + }, + { + "unfiltered": 112, + "glucose": 110, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "sgv": 110, + "direction": "Flat", + "_id": "D74370A0-39EB-48E9-B5DB-77645F393366", + "date": 1722992453462, + "dateString": "2024-08-07T01:00:53.463Z", + "type": "sgv" + }, + { + "sgv": 111, + "date": 1722992154209, + "unfiltered": 114, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T00:55:54.209Z", + "type": "sgv", + "_id": "DCD6D864-49C5-41D8-B126-68C546B93FE5", + "glucose": 111, + "direction": "Flat" + }, + { + "glucose": 112, + "_id": "1B9E6BD0-BB56-4547-99CA-4DF6D945408A", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T00:51:00.117Z", + "type": "sgv", + "unfiltered": 115, + "date": 1722991860117, + "activationDate": "2024-07-28T22:17:36.105Z", + "sgv": 112, + "direction": "Flat" + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T00:45:54.475Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "glucose": 114, + "sgv": 114, + "type": "sgv", + "date": 1722991554474, + "unfiltered": 118, + "_id": "765164A6-706F-4CCB-880B-6FE0D19B6F80" + }, + { + "unfiltered": 118, + "sgv": 116, + "direction": "Flat", + "_id": "D3F5690D-3372-4E8A-BBC2-B652DBA9A038", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "date": 1722991253139, + "glucose": 116, + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "dateString": "2024-08-07T00:40:53.139Z" + }, + { + "activationDate": "2024-07-28T22:17:36.105Z", + "_id": "80EF003E-5A20-4723-AEBE-BD673BE6A749", + "type": "sgv", + "glucose": 118, + "sgv": 118, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "dateString": "2024-08-07T00:35:57.469Z", + "direction": "Flat", + "date": 1722990957469, + "unfiltered": 121 + }, + { + "date": 1722990655301, + "type": "sgv", + "glucose": 122, + "_id": "A770DF9A-1439-4229-BB61-9F4BA717A4CD", + "sgv": 122, + "unfiltered": 124, + "activationDate": "2024-07-28T22:17:36.105Z", + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "direction": "Flat", + "dateString": "2024-08-07T00:30:55.301Z" + }, + { + "_id": "E37F6011-29A4-4DD8-AB3D-7D2512445E78", + "type": "sgv", + "activationDate": "2024-07-28T22:17:36.105Z", + "unfiltered": 130, + "sessionStartDate": "2024-07-28T22:42:36.105Z", + "glucose": 126, + "dateString": "2024-08-07T00:26:00.336Z", + "sgv": 126, + "date": 1722990360336, + "direction": "Flat" + } + ], + "tempBasal": { + "duration": 12, + "temp": "absolute", + "rate": 0.35, + "timestamp": "2024-08-08T00:28:38.051Z" + }, + "iob": [ + { + "iob": 0.724, + "activity": 0.0124, + "basaliob": -0.818, + "bolusiob": 1.542, + "netbasalinsulin": -4.3, + "bolusinsulin": 19.95, + "time": "2024-08-08T00:28:38.051Z", + "iobWithZeroTemp": { + "iob": 0.724, + "activity": 0.0124, + "basaliob": -0.818, + "bolusiob": 1.542, + "netbasalinsulin": -4.3, + "bolusinsulin": 19.95, + "time": "2024-08-08T00:28:38.051Z" + }, + "lastBolusTime": 1723073758662, + "lastTemp": { + "rate": 0.35, + "timestamp": "2024-08-08T00:11:04.248Z", + "started_at": "2024-08-08T00:11:04.248Z", + "date": 1723075864248, + "duration": 18.56 + } + }, + { + "iob": 0.664, + "activity": 0.0115, + "basaliob": -0.777, + "bolusiob": 1.441, + "netbasalinsulin": -4.3, + "bolusinsulin": 19.95, + "time": "2024-08-08T00:33:38.051Z", + "iobWithZeroTemp": { + "iob": 0.614, + "activity": 0.0114, + "basaliob": -0.827, + "bolusiob": 1.441, + "netbasalinsulin": -4.35, + "bolusinsulin": 19.95, + "time": "2024-08-08T00:33:38.051Z" + } + }, + { + "iob": 0.609, + "activity": 0.0106, + "basaliob": -0.736, + "bolusiob": 1.345, + "netbasalinsulin": -4.25, + "bolusinsulin": 19.8, + "time": "2024-08-08T00:38:38.051Z", + "iobWithZeroTemp": { + "iob": 0.46, + "activity": 0.0104, + "basaliob": -0.886, + "bolusiob": 1.345, + "netbasalinsulin": -4.4, + "bolusinsulin": 19.8, + "time": "2024-08-08T00:38:38.051Z" + } + }, + { + "iob": 0.558, + "activity": 0.0098, + "basaliob": -0.697, + "bolusiob": 1.255, + "netbasalinsulin": -4.2, + "bolusinsulin": 19.55, + "time": "2024-08-08T00:43:38.051Z", + "iobWithZeroTemp": { + "iob": 0.36, + "activity": 0.0094, + "basaliob": -0.895, + "bolusiob": 1.255, + "netbasalinsulin": -4.4, + "bolusinsulin": 19.55, + "time": "2024-08-08T00:43:38.051Z" + } + }, + { + "iob": 0.511, + "activity": 0.009, + "basaliob": -0.659, + "bolusiob": 1.17, + "netbasalinsulin": -4.15, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:48:38.051Z", + "iobWithZeroTemp": { + "iob": 0.266, + "activity": 0.0084, + "basaliob": -0.904, + "bolusiob": 1.17, + "netbasalinsulin": -4.4, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:48:38.051Z" + } + }, + { + "iob": 0.468, + "activity": 0.0083, + "basaliob": -0.621, + "bolusiob": 1.089, + "netbasalinsulin": -4.15, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:53:38.051Z", + "iobWithZeroTemp": { + "iob": 0.177, + "activity": 0.0073, + "basaliob": -0.912, + "bolusiob": 1.089, + "netbasalinsulin": -4.45, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:53:38.051Z" + } + }, + { + "iob": 0.428, + "activity": 0.0077, + "basaliob": -0.585, + "bolusiob": 1.013, + "netbasalinsulin": -4.1, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:58:38.051Z", + "iobWithZeroTemp": { + "iob": 0.093, + "activity": 0.0064, + "basaliob": -0.92, + "bolusiob": 1.013, + "netbasalinsulin": -4.45, + "bolusinsulin": 19.35, + "time": "2024-08-08T00:58:38.051Z" + } + }, + { + "iob": 0.391, + "activity": 0.0071, + "basaliob": -0.551, + "bolusiob": 0.941, + "netbasalinsulin": -4.1, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:03:38.051Z", + "iobWithZeroTemp": { + "iob": 0.013, + "activity": 0.0054, + "basaliob": -0.928, + "bolusiob": 0.941, + "netbasalinsulin": -4.5, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:03:38.051Z" + } + }, + { + "iob": 0.357, + "activity": 0.0065, + "basaliob": -0.518, + "bolusiob": 0.874, + "netbasalinsulin": -4.05, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:08:38.051Z", + "iobWithZeroTemp": { + "iob": -0.062, + "activity": 0.0045, + "basaliob": -0.936, + "bolusiob": 0.874, + "netbasalinsulin": -4.5, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:08:38.051Z" + } + }, + { + "iob": 0.325, + "activity": 0.006, + "basaliob": -0.486, + "bolusiob": 0.811, + "netbasalinsulin": -4, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:13:38.051Z", + "iobWithZeroTemp": { + "iob": -0.182, + "activity": 0.0036, + "basaliob": -0.993, + "bolusiob": 0.811, + "netbasalinsulin": -4.55, + "bolusinsulin": 19.25, + "time": "2024-08-08T01:13:38.051Z" + } + }, + { + "iob": 0.297, + "activity": 0.0055, + "basaliob": -0.455, + "bolusiob": 0.752, + "netbasalinsulin": -3.95, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:18:38.051Z", + "iobWithZeroTemp": { + "iob": -0.247, + "activity": 0.0027, + "basaliob": -0.999, + "bolusiob": 0.752, + "netbasalinsulin": -4.55, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:18:38.051Z" + } + }, + { + "iob": 0.27, + "activity": 0.0051, + "basaliob": -0.426, + "bolusiob": 0.697, + "netbasalinsulin": -3.9, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:23:38.051Z", + "iobWithZeroTemp": { + "iob": -0.309, + "activity": 0.0018, + "basaliob": -1.005, + "bolusiob": 0.697, + "netbasalinsulin": -4.55, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:23:38.051Z" + } + }, + { + "iob": 0.246, + "activity": 0.0046, + "basaliob": -0.399, + "bolusiob": 0.645, + "netbasalinsulin": -3.9, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:28:38.051Z", + "iobWithZeroTemp": { + "iob": -0.366, + "activity": 0.001, + "basaliob": -1.011, + "bolusiob": 0.645, + "netbasalinsulin": -4.6, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:28:38.051Z" + } + }, + { + "iob": 0.224, + "activity": 0.0043, + "basaliob": -0.373, + "bolusiob": 0.597, + "netbasalinsulin": -3.85, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:33:38.051Z", + "iobWithZeroTemp": { + "iob": -0.419, + "activity": 0.0002, + "basaliob": -1.016, + "bolusiob": 0.597, + "netbasalinsulin": -4.6, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:33:38.051Z" + } + }, + { + "iob": 0.204, + "activity": 0.0039, + "basaliob": -0.348, + "bolusiob": 0.552, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:38:38.051Z", + "iobWithZeroTemp": { + "iob": -0.518, + "activity": -0.0005, + "basaliob": -1.07, + "bolusiob": 0.552, + "netbasalinsulin": -4.65, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:38:38.051Z" + } + }, + { + "iob": 0.185, + "activity": 0.0036, + "basaliob": -0.325, + "bolusiob": 0.51, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:43:38.051Z", + "iobWithZeroTemp": { + "iob": -0.564, + "activity": -0.0012, + "basaliob": -1.073, + "bolusiob": 0.51, + "netbasalinsulin": -4.7, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:43:38.051Z" + } + }, + { + "iob": 0.168, + "activity": 0.0033, + "basaliob": -0.303, + "bolusiob": 0.47, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:48:38.051Z", + "iobWithZeroTemp": { + "iob": -0.606, + "activity": -0.0019, + "basaliob": -1.076, + "bolusiob": 0.47, + "netbasalinsulin": -4.75, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:48:38.051Z" + } + }, + { + "iob": 0.152, + "activity": 0.003, + "basaliob": -0.282, + "bolusiob": 0.434, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:53:38.051Z", + "iobWithZeroTemp": { + "iob": -0.645, + "activity": -0.0026, + "basaliob": -1.079, + "bolusiob": 0.434, + "netbasalinsulin": -4.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:53:38.051Z" + } + }, + { + "iob": 0.138, + "activity": 0.0027, + "basaliob": -0.262, + "bolusiob": 0.4, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:58:38.051Z", + "iobWithZeroTemp": { + "iob": -0.681, + "activity": -0.0032, + "basaliob": -1.08, + "bolusiob": 0.4, + "netbasalinsulin": -4.85, + "bolusinsulin": 19.15, + "time": "2024-08-08T01:58:38.051Z" + } + }, + { + "iob": 0.125, + "activity": 0.0025, + "basaliob": -0.244, + "bolusiob": 0.368, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.15, + "time": "2024-08-08T02:03:38.051Z", + "iobWithZeroTemp": { + "iob": -0.713, + "activity": -0.0037, + "basaliob": -1.081, + "bolusiob": 0.368, + "netbasalinsulin": -4.9, + "bolusinsulin": 19.15, + "time": "2024-08-08T02:03:38.051Z" + } + }, + { + "iob": 0.113, + "activity": 0.0023, + "basaliob": -0.226, + "bolusiob": 0.339, + "netbasalinsulin": -3.85, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:08:38.051Z", + "iobWithZeroTemp": { + "iob": -0.793, + "activity": -0.0043, + "basaliob": -1.132, + "bolusiob": 0.339, + "netbasalinsulin": -5.05, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:08:38.051Z" + } + }, + { + "iob": 0.102, + "activity": 0.0021, + "basaliob": -0.21, + "bolusiob": 0.312, + "netbasalinsulin": -3.85, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:13:38.051Z", + "iobWithZeroTemp": { + "iob": -0.821, + "activity": -0.0048, + "basaliob": -1.132, + "bolusiob": 0.312, + "netbasalinsulin": -5.1, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:13:38.051Z" + } + }, + { + "iob": 0.092, + "activity": 0.0019, + "basaliob": -0.195, + "bolusiob": 0.286, + "netbasalinsulin": -3.85, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:18:38.051Z", + "iobWithZeroTemp": { + "iob": -0.845, + "activity": -0.0053, + "basaliob": -1.132, + "bolusiob": 0.286, + "netbasalinsulin": -5.15, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:18:38.051Z" + } + }, + { + "iob": 0.083, + "activity": 0.0017, + "basaliob": -0.18, + "bolusiob": 0.263, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:23:38.051Z", + "iobWithZeroTemp": { + "iob": -0.868, + "activity": -0.0057, + "basaliob": -1.131, + "bolusiob": 0.263, + "netbasalinsulin": -5.15, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:23:38.051Z" + } + }, + { + "iob": 0.074, + "activity": 0.0016, + "basaliob": -0.167, + "bolusiob": 0.241, + "netbasalinsulin": -3.75, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:28:38.051Z", + "iobWithZeroTemp": { + "iob": -0.888, + "activity": -0.0061, + "basaliob": -1.13, + "bolusiob": 0.241, + "netbasalinsulin": -5.15, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:28:38.051Z" + } + }, + { + "iob": 0.067, + "activity": 0.0014, + "basaliob": -0.155, + "bolusiob": 0.221, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:33:38.051Z", + "iobWithZeroTemp": { + "iob": -0.907, + "activity": -0.0065, + "basaliob": -1.128, + "bolusiob": 0.221, + "netbasalinsulin": -5.25, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:33:38.051Z" + } + }, + { + "iob": 0.06, + "activity": 0.0013, + "basaliob": -0.143, + "bolusiob": 0.203, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:38:38.051Z", + "iobWithZeroTemp": { + "iob": -0.973, + "activity": -0.0069, + "basaliob": -1.176, + "bolusiob": 0.203, + "netbasalinsulin": -5.35, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:38:38.051Z" + } + }, + { + "iob": 0.054, + "activity": 0.0012, + "basaliob": -0.132, + "bolusiob": 0.186, + "netbasalinsulin": -3.75, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:43:38.051Z", + "iobWithZeroTemp": { + "iob": -0.987, + "activity": -0.0073, + "basaliob": -1.173, + "bolusiob": 0.186, + "netbasalinsulin": -5.35, + "bolusinsulin": 19.05, + "time": "2024-08-08T02:43:38.051Z" + } + }, + { + "iob": 0.048, + "activity": 0.0011, + "basaliob": -0.122, + "bolusiob": 0.17, + "netbasalinsulin": -3.75, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:48:38.051Z", + "iobWithZeroTemp": { + "iob": -1, + "activity": -0.0076, + "basaliob": -1.17, + "bolusiob": 0.17, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:48:38.051Z" + } + }, + { + "iob": 0.043, + "activity": 0.001, + "basaliob": -0.112, + "bolusiob": 0.155, + "netbasalinsulin": -3.7, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:53:38.051Z", + "iobWithZeroTemp": { + "iob": -1.011, + "activity": -0.0079, + "basaliob": -1.167, + "bolusiob": 0.155, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:53:38.051Z" + } + }, + { + "iob": 0.038, + "activity": 0.0009, + "basaliob": -0.104, + "bolusiob": 0.142, + "netbasalinsulin": -3.65, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:58:38.051Z", + "iobWithZeroTemp": { + "iob": -1.021, + "activity": -0.0082, + "basaliob": -1.163, + "bolusiob": 0.142, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T02:58:38.051Z" + } + }, + { + "iob": 0.034, + "activity": 0.0008, + "basaliob": -0.095, + "bolusiob": 0.13, + "netbasalinsulin": -3.55, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:03:38.051Z", + "iobWithZeroTemp": { + "iob": -1.029, + "activity": -0.0085, + "basaliob": -1.159, + "bolusiob": 0.13, + "netbasalinsulin": -5.35, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:03:38.051Z" + } + }, + { + "iob": 0.031, + "activity": 0.0007, + "basaliob": -0.088, + "bolusiob": 0.118, + "netbasalinsulin": -3.5, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:08:38.051Z", + "iobWithZeroTemp": { + "iob": -1.087, + "activity": -0.0087, + "basaliob": -1.205, + "bolusiob": 0.118, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:08:38.051Z" + } + }, + { + "iob": 0.027, + "activity": 0.0006, + "basaliob": -0.081, + "bolusiob": 0.108, + "netbasalinsulin": -3.45, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:13:38.051Z", + "iobWithZeroTemp": { + "iob": -1.092, + "activity": -0.0089, + "basaliob": -1.2, + "bolusiob": 0.108, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:13:38.051Z" + } + }, + { + "iob": 0.024, + "activity": 0.0006, + "basaliob": -0.074, + "bolusiob": 0.098, + "netbasalinsulin": -3.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:18:38.051Z", + "iobWithZeroTemp": { + "iob": -1.097, + "activity": -0.0092, + "basaliob": -1.196, + "bolusiob": 0.098, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:18:38.051Z" + } + }, + { + "iob": 0.021, + "activity": 0.0005, + "basaliob": -0.068, + "bolusiob": 0.09, + "netbasalinsulin": -3.35, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:23:38.051Z", + "iobWithZeroTemp": { + "iob": -1.101, + "activity": -0.0094, + "basaliob": -1.19, + "bolusiob": 0.09, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:23:38.051Z" + } + }, + { + "iob": 0.019, + "activity": 0.0005, + "basaliob": -0.063, + "bolusiob": 0.081, + "netbasalinsulin": -3.3, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:28:38.051Z", + "iobWithZeroTemp": { + "iob": -1.104, + "activity": -0.0095, + "basaliob": -1.185, + "bolusiob": 0.081, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:28:38.051Z" + } + }, + { + "iob": 0.017, + "activity": 0.0004, + "basaliob": -0.057, + "bolusiob": 0.074, + "netbasalinsulin": -3.25, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:33:38.051Z", + "iobWithZeroTemp": { + "iob": -1.106, + "activity": -0.0097, + "basaliob": -1.18, + "bolusiob": 0.074, + "netbasalinsulin": -5.4, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:33:38.051Z" + } + }, + { + "iob": 0.015, + "activity": 0.0004, + "basaliob": -0.053, + "bolusiob": 0.067, + "netbasalinsulin": -3.2, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:38:38.051Z", + "iobWithZeroTemp": { + "iob": -1.157, + "activity": -0.0099, + "basaliob": -1.224, + "bolusiob": 0.067, + "netbasalinsulin": -5.45, + "bolusinsulin": 16.85, + "time": "2024-08-08T03:38:38.051Z" + } + }, + { + "iob": 0.013, + "activity": 0.0003, + "basaliob": -0.048, + "bolusiob": 0.061, + "netbasalinsulin": -3.15, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:43:38.051Z", + "iobWithZeroTemp": { + "iob": -1.157, + "activity": -0.01, + "basaliob": -1.218, + "bolusiob": 0.061, + "netbasalinsulin": -5.45, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:43:38.051Z" + } + }, + { + "iob": 0.011, + "activity": 0.0003, + "basaliob": -0.044, + "bolusiob": 0.055, + "netbasalinsulin": -3.1, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:48:38.051Z", + "iobWithZeroTemp": { + "iob": -1.157, + "activity": -0.0102, + "basaliob": -1.212, + "bolusiob": 0.055, + "netbasalinsulin": -5.45, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:48:38.051Z" + } + }, + { + "iob": 0.01, + "activity": 0.0003, + "basaliob": -0.04, + "bolusiob": 0.05, + "netbasalinsulin": -3.05, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:53:38.051Z", + "iobWithZeroTemp": { + "iob": -1.155, + "activity": -0.0103, + "basaliob": -1.206, + "bolusiob": 0.05, + "netbasalinsulin": -5.45, + "bolusinsulin": 16.75, + "time": "2024-08-08T03:53:38.051Z" + } + }, + { + "iob": 0.009, + "activity": 0.0002, + "basaliob": -0.037, + "bolusiob": 0.045, + "netbasalinsulin": -3, + "bolusinsulin": 16.3, + "time": "2024-08-08T03:58:38.051Z", + "iobWithZeroTemp": { + "iob": -1.154, + "activity": -0.0104, + "basaliob": -1.199, + "bolusiob": 0.045, + "netbasalinsulin": -5.45, + "bolusinsulin": 16.3, + "time": "2024-08-08T03:58:38.051Z" + } + }, + { + "iob": 0.007, + "activity": 0.0002, + "basaliob": -0.034, + "bolusiob": 0.041, + "netbasalinsulin": -3, + "bolusinsulin": 15.7, + "time": "2024-08-08T04:03:38.051Z", + "iobWithZeroTemp": { + "iob": -1.152, + "activity": -0.0105, + "basaliob": -1.193, + "bolusiob": 0.041, + "netbasalinsulin": -5.5, + "bolusinsulin": 15.7, + "time": "2024-08-08T04:03:38.051Z" + } + }, + { + "iob": 0.006, + "activity": 0.0002, + "basaliob": -0.031, + "bolusiob": 0.037, + "netbasalinsulin": -2.95, + "bolusinsulin": 15.4, + "time": "2024-08-08T04:08:38.051Z", + "iobWithZeroTemp": { + "iob": -1.199, + "activity": -0.0106, + "basaliob": -1.236, + "bolusiob": 0.037, + "netbasalinsulin": -5.55, + "bolusinsulin": 15.4, + "time": "2024-08-08T04:08:38.051Z" + } + }, + { + "iob": 0.006, + "activity": 0.0002, + "basaliob": -0.028, + "bolusiob": 0.034, + "netbasalinsulin": -2.9, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:13:38.051Z", + "iobWithZeroTemp": { + "iob": -1.196, + "activity": -0.0107, + "basaliob": -1.229, + "bolusiob": 0.034, + "netbasalinsulin": -5.55, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:13:38.051Z" + } + }, + { + "iob": 0.005, + "activity": 0.0001, + "basaliob": -0.026, + "bolusiob": 0.03, + "netbasalinsulin": -2.8, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:18:38.051Z", + "iobWithZeroTemp": { + "iob": -1.192, + "activity": -0.0108, + "basaliob": -1.222, + "bolusiob": 0.03, + "netbasalinsulin": -5.5, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:18:38.051Z" + } + }, + { + "iob": 0.004, + "activity": 0.0001, + "basaliob": -0.023, + "bolusiob": 0.027, + "netbasalinsulin": -2.75, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:23:38.051Z", + "iobWithZeroTemp": { + "iob": -1.188, + "activity": -0.0109, + "basaliob": -1.215, + "bolusiob": 0.027, + "netbasalinsulin": -5.5, + "bolusinsulin": 8.1, + "time": "2024-08-08T04:23:38.051Z" + } + } + ], + "profile": { + "max_iob": 14, + "max_daily_safety_multiplier": 3, + "current_basal_safety_multiplier": 4, + "autosens_max": 1.3, + "autosens_min": 0.7, + "rewind_resets_autosens": true, + "high_temptarget_raises_sensitivity": true, + "low_temptarget_lowers_sensitivity": true, + "sensitivity_raises_target": true, + "resistance_lowers_target": false, + "exercise_mode": false, + "half_basal_exercise_target": 160, + "maxCOB": 120, + "skip_neutral_temps": false, + "unsuspend_if_no_temp": false, + "min_5m_carbimpact": 8, + "autotune_isf_adjustmentFraction": 1, + "remainingCarbsFraction": 1, + "remainingCarbsCap": 90, + "enableUAM": true, + "A52_risk_enable": false, + "enableSMB_with_COB": true, + "enableSMB_with_temptarget": true, + "enableSMB_always": false, + "enableSMB_after_carbs": true, + "allowSMB_with_high_temptarget": false, + "maxSMBBasalMinutes": 90, + "maxUAMSMBBasalMinutes": 120, + "SMBInterval": 3, + "bolus_increment": 0.05, + "maxDelta_bg_threshold": 0.2, + "curve": "ultra-rapid", + "useCustomPeakTime": false, + "insulinPeakTime": 55, + "carbsReqThreshold": 1, + "offline_hotspot": false, + "noisyCGMTargetMultiplier": 1.3, + "suspend_zeros_iob": false, + "enableEnliteBgproxy": false, + "calc_glucose_noise": false, + "target_bg": false, + "smb_delivery_ratio": 0.7, + "adjustmentFactor": 0.7, + "useNewFormula": true, + "enableDynamicCR": true, + "sigmoid": true, + "weightPercentage": 0.65, + "tddAdjBasal": true, + "enableSMB_high_bg": true, + "enableSMB_high_bg_target": 110, + "threshold_setting": 65, + "dia": 9, + "model": "722", + "current_basal": 0.7, + "basalprofile": [ + { + "minutes": 0, + "start": "00:00:00", + "rate": 0.7 + }, + { + "start": "03:00:00", + "minutes": 180, + "rate": 0.7 + }, + { + "start": "09:00:00", + "rate": 0.75, + "minutes": 540 + } + ], + "max_daily_basal": 0.75, + "max_basal": 3, + "out_units": "mg/dL", + "min_bg": 98, + "max_bg": 98, + "bg_targets": { + "units": "mg/dL", + "user_preferred_units": "mg/dL", + "targets": [ + { + "start": "00:00:00", + "high": 98, + "offset": 0, + "low": 98, + "max_bg": 98, + "min_bg": 98 + } + ] + }, + "sens": 65, + "isfProfile": { + "user_preferred_units": "mg/dL", + "units": "mg/dL", + "sensitivities": [ + { + "start": "00:00:00", + "sensitivity": 65, + "offset": 0, + "endOffset": 1440 + } + ] + }, + "carb_ratio": 6, + "carb_ratios": { + "schedule": [ + { + "offset": 0, + "start": "00:00:00", + "ratio": 6 + }, + { + "start": "08:30:00", + "offset": 510, + "ratio": 5 + }, + { + "offset": 690, + "start": "11:30:00", + "ratio": 5.5 + }, + { + "start": "16:00:00", + "ratio": 6.5, + "offset": 960 + } + ], + "units": "grams" + } + }, + "autosens": { + "newisf": 66, + "timestamp": "2024-08-08T00:05:57.631Z", + "ratio": 0.99 + }, + "meal": { + "carbs": 2, + "nsCarbs": 2, + "bwCarbs": 0, + "journalCarbs": 0, + "mealCOB": 2, + "currentDeviation": -0.86, + "maxDeviation": 0, + "minDeviation": -1.26, + "slopeFromMaxDeviation": 0, + "slopeFromMinDeviation": 0.088, + "allDeviations": [ + -1, + 0, + -1, + -1 + ], + "lastCarbTime": 1723076103157, + "bwFound": false + }, + "microBolusAllowed": true, + "reservoir": 3735928559, + "dynamicVariables": { + "end": 0, + "uamMinutes": 30, + "disableCGMError": true, + "smbMinutes": 30, + "preset": "", + "unlimited": true, + "weightedAverage": 38.4128, + "useOverride": false, + "advancedSettings": false, + "average_total_data": 41.85, + "date": "2024-08-08T00:28:38.369Z", + "isf": true, + "isfAndCr": false, + "smbIsAlwaysOff": false, + "hbt": 160, + "weigthPercentage": 0.65, + "maxIOB": 0, + "overrideTarget": 0, + "cr": true, + "isEnabled": false, + "overrideMaxIOB": false, + "presetActive": false, + "smbIsOff": false, + "start": 0, + "overridePercentage": 100, + "duration": 0, + "past2hoursAverage": 36.562 + }, + "clock": "2024-08-08T00:28:40.514Z", + "suggested": { + "temp": "absolute", + "bg": 129, + "tick": "+0", + "eventualBG": 148, + "insulinReq": 0.16, + "reservoir": 3735928559, + "deliverAt": "2024-08-08T00:28:40.514Z", + "sensitivityRatio": 1.15, + "predBGs": { + "IOB": [ + 129, + 129, + 128, + 128, + 128, + 127, + 126, + 126, + 125, + 124, + 123, + 122, + 120, + 119, + 118, + 117, + 116, + 115, + 114, + 113, + 112, + 112, + 111, + 110, + 110, + 110, + 109, + 109, + 108, + 108, + 108, + 108, + 107, + 107, + 107, + 107, + 107, + 107, + 106 + ], + "ZT": [ + 129, + 126, + 122, + 119, + 117, + 114, + 112, + 110, + 109, + 108, + 107, + 106, + 105, + 105, + 105, + 105, + 105 + ], + "COB": [ + 129, + 129, + 128, + 128, + 128, + 127, + 127, + 126, + 125, + 124, + 123, + 122, + 121, + 120, + 118, + 117, + 116, + 115, + 115, + 114, + 113, + 112, + 112, + 111, + 111, + 110, + 110, + 110, + 109, + 109, + 109, + 108, + 108, + 108, + 108, + 108, + 107 + ], + "UAM": [ + 129, + 129, + 129, + 129, + 130, + 130, + 131, + 131, + 132, + 133, + 134, + 134, + 135, + 136, + 137, + 138, + 139, + 140, + 141, + 142, + 143, + 143, + 144, + 145, + 146, + 146, + 147, + 147, + 148, + 148, + 148, + 149, + 149, + 149, + 149, + 149, + 149, + 149, + 148 + ] + }, + "COB": 2, + "IOB": 0.724, + "BGI": -3, + "deviation": 21, + "ISF": 57, + "CR": 5.2, + "target_bg": 98, + "reason": "COB: 2, Dev: 21, BGI: -3, ISF: 57, CR: 5.2, minPredBG: 107, minGuardBG: 107, IOBpredBG: 106, COBpredBG: 107, UAMpredBG: 148; Eventual BG 148 >= 98, insulinReq 0.16; setting 30m low temp of 0.05U/h. Microbolusing 0.05U. ", + "units": 0.05, + "rate": 0.05, + "duration": 30 + } +} diff --git a/tests/determine-basal.data.test.ts b/tests/determine-basal.data.test.ts new file mode 100644 index 000000000..7469c9e98 --- /dev/null +++ b/tests/determine-basal.data.test.ts @@ -0,0 +1,288 @@ +/** + * CODE FROM iAPS to test real data + * We can delete it. + */ +import data from './determine-basal.data.json' +import getLastGlucose from '../lib/glucose-get-last'; +import * as basalFunctions from '../lib/basal-set-temp' +import determine_basal from '../lib/determine-basal/determine-basal'; + +//для enact/smb-suggested.json параметры: monitor/iob.json monitor/temp_basal.json monitor/glucose.json settings/profile.json settings/autosens.json --meal monitor/meal.json --microbolus --reservoir monitor/reservoir.json + +function generate(iob, currenttemp, glucose, profile, autosens = null, meal = null, microbolusAllowed = true, reservoir = null, clock, dynamicVariables) { + // Needs to be updated here due to time format). + clock = new Date(clock) + + var autosens_data = null; + if (autosens) { + autosens_data = autosens; + } + + var reservoir_data = null; + if (reservoir) { + reservoir_data = reservoir; + } + + var meal_data = {}; + if (meal) { + meal_data = meal; + } + + // Overrides + if (dynamicVariables && dynamicVariables.useOverride) { + const factor = dynamicVariables.overridePercentage / 100; + if (factor != 1) { + // Basal + profile.current_basal *= factor; + // ISF and CR + if (dynamicVariables.isfAndCr) { + profile.sense /= factor; + profile.carb_ratio /= factor; + } else { + if (dynamicVariables.cr) { profile.carb_ratio /= factor; } + if (dynamicVariables.isf) { profile.sens /= factor; } + } + console.log("Override Active, " + dynamicVariables.overridePercentage + "%"); + } + // SMB Minutes + if (dynamicVariables.advancedSettings && dynamicVariables.smbMinutes !== profile.maxSMBBasalMinutes) { + console.log("SMB Max Minutes - setting overriden from " + profile.maxSMBBasalMinutes + " to " + dynamicVariables.smbMinutes); + profile.maxSMBBasalMinutes = dynamicVariables.smbMinutes; + } + // UAM Minutes + if (dynamicVariables.advancedSettings && dynamicVariables.uamMinutes !== profile.maxUAMSMBBasalMinutes) { + console.log("UAM Max Minutes - setting overriden from " + profile.maxUAMSMBBasalMinutes + " to " + dynamicVariables.uamMinutes); + profile.maxUAMSMBBasalMinutes = dynamicVariables.uamMinutes; + } + //Target + if (dynamicVariables.overrideTarget != 0 && dynamicVariables.overrideTarget != 6 && !profile.temptargetSet) { + profile.min_bg = dynamicVariables.overrideTarget; + profile.max_bg = profile.min_bg; + console.log("Override Active, new glucose target: " + dynamicVariables.overrideTarget); + } + + //SMBs + if (disableSMBs(dynamicVariables)) { + microbolusAllowed = false; + console.error("SMBs disabled by Override"); + } + + // Max IOB + if (dynamicVariables.advancedSettings && dynamicVariables.overrideMaxIOB) { + profile.max_iob = dynamicVariables.maxIOB; + console.log("Override Active, new maxIOB: " + profile.max_iob); + } + } + + // Half Basal Target + if (dynamicVariables.isEnabled) { + profile.half_basal_exercise_target = dynamicVariables.hbt; + console.log("Temp Target active, half_basal_exercise_target: " + dynamicVariables.hbt); + } + + // Dynamic ISF + if (profile.useNewFormula) { + dynisf(profile, autosens_data, dynamicVariables, glucose); + } + + // If ignoring flat CGM errors, circumvent also the Oref0 error + if (dynamicVariables.disableCGMError) { + if (glucose.length > 1 && Math.abs(glucose[0].glucose - glucose[1].glucose) < 5) { + if (glucose[1].glucose >= glucose[1].glucose) { + glucose[1].glucose -= 5; + } else {glucose[1].glucose += 5; } + console.log("Flat CGM by-passed."); + } + } + var glucose_status = getLastGlucose(glucose); + + // In case Basal Rate been set in midleware + if (profile.set_basal && profile.basal_rate) { + console.log("Basal Rate set by middleware to " + profile.basal_rate + " U/h."); + } + + return determine_basal(glucose_status, currenttemp, iob, profile, autosens_data, meal_data, basalFunctions, microbolusAllowed, reservoir_data, clock); +} + +// The Dynamic ISF layer +function dynisf(profile, autosens_data, dynamicVariables, glucose) { + console.log("Starting dynamic ISF layer."); + var dynISFenabled = true; + // One of two exercise settings (they share the same purpose). + var exerciseSetting = false; + if (profile.highTemptargetRaisesSensitivity || profile.exerciseMode || dynamicVariables.isEnabled) { + exerciseSetting = true; + } + + const target = profile.min_bg; + + // Turn dynISF off when using a temp target >= 118 (6.5 mol/l) and if an exercise setting is enabled. + if (target >= 118 && exerciseSetting) { + //dynISFenabled = false; + console.log("Dynamic ISF disabled due to a high temp target/exercise."); + return; + } + + // In case the autosens.min/max limits are reversed: + const autosens_min = Math.min(profile.autosens_min, profile.autosens_max); + const autosens_max = Math.max(profile.autosens_min, profile.autosens_max); + + // Turn off when autosens.min = autosens.max etc. + if (autosens_max == autosens_min || autosens_max < 1 || autosens_min > 1) { + console.log("Dynamic ISF disabled due to current autosens settings"); + return; + } + + // Insulin curve + const curve = profile.curve; + const ipt = profile.insulinPeakTime; + const ucpk = profile.useCustomPeakTime; + var insulinFactor = 55 // deafult (120-65) + var insulinPA = 65 // default (Novorapid/Novolog) + + switch (curve) { + case "rapid-acting": + insulinPA = 65; + break; + case "ultra-rapid": + insulinPA = 50; + break; + } + + if (ucpk) { + insulinFactor = 120 - ipt; + console.log("Custom insulinpeakTime set to: " + ipt + ", " + "insulinFactor: " + insulinFactor); + } else { + insulinFactor = 120 - insulinPA; + console.log("insulinFactor set to : " + insulinFactor); + } + + // Use a weighted TDD average + var tdd = 0; + const weighted_average = dynamicVariables.weightedAverage; + const weightPercentage = dynamicVariables.weigthPercentage; + const average14 = dynamicVariables.average_total_data; + + if (weightPercentage > 0 && weighted_average > 0) { + tdd = weighted_average; + console.log("Using a weighted TDD average: " + weighted_average); + } else { + console.log("Dynamic ISF disabled. Not enough TDD data."); + return; + } + + // Account for TDD of insulin. Compare last 2 hours with total data (up to 10 days) + var tdd_factor = weighted_average / average14; // weighted average TDD / total data average TDD + + const enable_sigmoid = profile.sigmoid; + var newRatio = 1; + + const sensitivity = profile.sens; + const adjustmentFactor = profile.adjustmentFactor; + + var BG = 100; + if (glucose.length > 0) { + BG = glucose[0].glucose; + } + + if (dynISFenabled && !(enable_sigmoid)) { + const power = BG / insulinFactor + 1; + newRatio = round(sensitivity * adjustmentFactor * tdd * Math.log(power) / 1800, 2); + console.log("Dynamic ISF enabled. Dynamic Ratio (Logarithmic formula): " + newRatio); + } + + // Sigmoid Function + if (dynISFenabled && enable_sigmoid) { + const autosens_interval = autosens_max - autosens_min; + // Blood glucose deviation from set target (the lower BG target) converted to mmol/l to fit current formula. + console.log("autosens_interval: " + autosens_interval); + const bg_dev = (BG - target) * 0.0555; + var max_minus_one = autosens_max - 1; + // Avoid division by 0 + if (autosens_max == 1) { + max_minus_one = autosens_max + 0.01 - 1; + } + // Makes sigmoid factor(y) = 1 when BG deviation(x) = 0. + const fix_offset = (Math.log10(1/max_minus_one-autosens_min/max_minus_one) / Math.log10(Math.E)); + //Exponent used in sigmoid formula + const exponent = bg_dev * adjustmentFactor * tdd_factor + fix_offset; + // The sigmoid function + const sigmoid_factor = autosens_interval / (1 + Math.exp(-exponent)) + autosens_min; + newRatio = round(sigmoid_factor, 2); + } + + // Respect autosens.max and autosens.min limitLogs + if (newRatio > autosens_max) { + console.log(", Dynamic ISF limited by autosens_max setting to: " + autosens_max + ", from: " + newRatio); + newRatio = autosens_max; + } else if (newRatio < autosens_min) { + console.log(", Dynamic ISF limited by autosens_min setting to: " + autosens_min + ", from: " + newRatio); + newRatio = autosens_min; + } + + // Dynamic CR + var cr = profile.carb_ratio; + if (profile.enableDynamicCR) { + cr /= newRatio; + profile.carb_ratio = round(cr, 1); + console.log(". Dynamic CR enabled, Dynamic CR: " + profile.carb_ratio + " g/U."); + } + + // Dyhamic ISF + const isf = round(profile.sens / newRatio, 1); + autosens_data.ratio = newRatio; + if (enable_sigmoid) { + console.log("Dynamic ISF enabled. Dynamic Ratio (Sigmoid function): " + newRatio + ". New ISF = " + isf + " mg/dl / " + round(0.0555 * isf, 1) + " mmol/l."); + } + + // Basal Adjustment + if (profile.tddAdjBasal && dynISFenabled) { + profile.current_basal *= tdd_factor; + console.log("Dynamic ISF. Basal adjusted with TDD factor: " + round(tdd_factor, 2)); + } +} + +function round(value, digits) { + if (! digits) { digits = 0; } + var scale = Math.pow(10, digits); + return Math.round(value * scale) / scale; +} + +function disableSMBs(dynamicVariables) { + if (dynamicVariables.smbIsOff) { + if (!dynamicVariables.smbIsAlwaysOff) { + return true; + } + const hour = new Date().getHours(); + if (dynamicVariables.end < dynamicVariables.start && hour < 24 && hour > dynamicVariables.start) { + dynamicVariables.end += 24; + } + if (hour >= dynamicVariables.start && hour <= dynamicVariables.end) { + return true; + } + if (dynamicVariables.end < dynamicVariables.start && hour < dynamicVariables.end) { + return true; + } + } + return false +} + + +describe('iob', () => { + it('should', () => { + const result = generate( + data.iob, + data.tempBasal, + data.glucose, + data.profile, + data.autosens, + data.meal, + true, + data.reservoir, + new Date(data.clock), + data.dynamicVariables, + ) + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual(JSON.parse(JSON.stringify(data.suggested))) + }) +}) diff --git a/tests/determine-basal.test.js b/tests/determine-basal.test.ts similarity index 99% rename from tests/determine-basal.test.js rename to tests/determine-basal.test.ts index 7c9edb918..342c416ad 100644 --- a/tests/determine-basal.test.js +++ b/tests/determine-basal.test.ts @@ -1,5 +1,3 @@ -'use strict'; - var should = require('should'); describe('round_basal', function ( ) { diff --git a/tests/get-last-glucose.test.js b/tests/get-last-glucose.test.ts similarity index 97% rename from tests/get-last-glucose.test.js rename to tests/get-last-glucose.test.ts index dff7436de..c7ec95651 100644 --- a/tests/get-last-glucose.test.js +++ b/tests/get-last-glucose.test.ts @@ -4,7 +4,7 @@ require('should'); describe('getLastGlucose', function ( ) { - var getLastGlucose = require('../lib/glucose-get-last.js'); + var getLastGlucose = require('../lib/glucose-get-last'); it('should handle NS sgv fields', function () { var glucose_status = getLastGlucose([{date: 1467942845000, sgv: 100}, {date: 1467942544500, sgv: 95}, {date: 1467942244000, sgv: 85}, {date: 1467941944000, sgv: 70}]); diff --git a/tests/glucose-noise.js b/tests/glucose-noise.test.ts similarity index 100% rename from tests/glucose-noise.js rename to tests/glucose-noise.test.ts diff --git a/tests/iob.data.json b/tests/iob.data.json new file mode 100644 index 000000000..699e7ca0a --- /dev/null +++ b/tests/iob.data.json @@ -0,0 +1,4026 @@ +{ + "pumpHistory": [ + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T19:56:00.953Z", + "duration (min)": 120, + "id": "74dce16c53a4a9bc67a740a3d29381ce" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "id": "_74dce16c53a4a9bc67a740a3d29381ce", + "timestamp": "2024-08-07T19:56:00.953Z", + "rate": 0 + }, + { + "id": "ece843aa27fffdb2f71afcf4ce3b6363", + "timestamp": "2024-08-07T19:46:23.861Z", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "id": "_ece843aa27fffdb2f71afcf4ce3b6363", + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T19:46:23.861Z", + "rate": 0 + }, + { + "isSMB": true, + "amount": 0.55, + "isExternal": false, + "id": "79f86ccf493ee3b807664bf18323cdfb", + "_type": "Bolus", + "duration": 0, + "timestamp": "2024-08-07T19:40:29.280Z" + }, + { + "id": "78f0773b1cee587f3803d5f9a36372fd", + "timestamp": "2024-08-07T19:40:26.017Z", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-07T19:40:26.017Z", + "id": "_78f0773b1cee587f3803d5f9a36372fd", + "rate": 0, + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T19:31:16.344Z", + "_type": "Bolus", + "duration": 0, + "amount": 0.8, + "isExternal": false, + "id": "4b80cdfa26cd6077cec73a20489a4c1e", + "isSMB": true + }, + { + "timestamp": "2024-08-07T19:31:13.848Z", + "duration (min)": 60, + "id": "683e837b672ae438bfea047a70d941a9", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "rate": 0, + "timestamp": "2024-08-07T19:31:13.848Z", + "id": "_683e837b672ae438bfea047a70d941a9" + }, + { + "isSMB": true, + "id": "3869b653beff45c54b0488e2f73711a2", + "_type": "Bolus", + "amount": 0.75, + "isExternal": false, + "duration": 0, + "timestamp": "2024-08-07T19:26:11.790Z" + }, + { + "_type": "TempBasalDuration", + "id": "b9b20de5946f9e76bb280ee47df220fd", + "timestamp": "2024-08-07T19:26:10.475Z", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "temp": "absolute", + "id": "_b9b20de5946f9e76bb280ee47df220fd", + "timestamp": "2024-08-07T19:26:10.475Z", + "rate": 0 + }, + { + "id": "341f6da4d1ecaba54a79317237d5a8c5", + "isExternal": false, + "timestamp": "2024-08-07T19:11:30.462Z", + "duration": 4, + "isSMB": false, + "_type": "Bolus", + "amount": 7.3 + }, + { + "timestamp": "2024-08-07T19:10:56.819Z", + "id": "88118ca6766bba102cffe37bdb146983", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T19:10:56.819Z", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0, + "id": "_88118ca6766bba102cffe37bdb146983" + }, + { + "isSMB": true, + "id": "72d0b9f842953fe127a6946e3111caeb", + "timestamp": "2024-08-07T19:05:58.563Z", + "duration": 0, + "_type": "Bolus", + "amount": 0.3, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T19:05:57.323Z", + "id": "9f0b411b1c11a67f5fde6e29934eb31b", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-07T19:05:57.323Z", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0, + "id": "_9f0b411b1c11a67f5fde6e29934eb31b" + }, + { + "timestamp": "2024-08-07T19:01:00.869Z", + "isExternal": false, + "id": "9c08a8eb908aa26edb6a5654789dfd09", + "_type": "Bolus", + "isSMB": true, + "amount": 0.6, + "duration": 0 + }, + { + "id": "e6ea68c340e66acdede056f8186bc35f", + "timestamp": "2024-08-07T19:00:58.899Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "id": "_e6ea68c340e66acdede056f8186bc35f", + "_type": "TempBasal", + "rate": 0.6, + "temp": "absolute", + "timestamp": "2024-08-07T19:00:58.899Z" + }, + { + "amount": 0.45, + "id": "0e1bd9066bff565348c138e41047a2a7", + "timestamp": "2024-08-07T18:56:05.549Z", + "duration": 0, + "isSMB": true, + "isExternal": false, + "_type": "Bolus" + }, + { + "timestamp": "2024-08-07T18:56:04.435Z", + "_type": "TempBasalDuration", + "id": "bb4fe0298a9b0ea6487b588df2588e27", + "duration (min)": 30 + }, + { + "temp": "absolute", + "_type": "TempBasal", + "id": "_bb4fe0298a9b0ea6487b588df2588e27", + "timestamp": "2024-08-07T18:56:04.435Z", + "rate": 0.1 + }, + { + "_type": "TempBasalDuration", + "id": "d235995386bd686375498b89964d397a", + "timestamp": "2024-08-07T18:51:02.168Z", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "id": "_d235995386bd686375498b89964d397a", + "timestamp": "2024-08-07T18:51:02.168Z", + "rate": 0, + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T18:46:02.246Z", + "id": "b15793917401d9f6da3db8d9ae1c5b4f", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-07T18:46:02.246Z", + "_type": "TempBasal", + "rate": 0, + "id": "_b15793917401d9f6da3db8d9ae1c5b4f", + "temp": "absolute" + }, + { + "amount": 0.1, + "duration": 0, + "id": "b1cc7ba43f534c189784258c25144e0c", + "timestamp": "2024-08-07T18:41:02.663Z", + "isExternal": false, + "isSMB": true, + "_type": "Bolus" + }, + { + "timestamp": "2024-08-07T18:41:01.467Z", + "_type": "TempBasalDuration", + "id": "486ef97e04d497339a89720bdd88004b", + "duration (min)": 60 + }, + { + "rate": 0, + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-07T18:41:01.467Z", + "id": "_486ef97e04d497339a89720bdd88004b" + }, + { + "id": "bb4244f760ca4c98c261074e6fdc0906", + "duration (min)": 60, + "timestamp": "2024-08-07T18:36:01.228Z", + "_type": "TempBasalDuration" + }, + { + "rate": 0, + "timestamp": "2024-08-07T18:36:01.228Z", + "id": "_bb4244f760ca4c98c261074e6fdc0906", + "_type": "TempBasal", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T18:30:57.708Z", + "_type": "TempBasalDuration", + "id": "0515383605e90cff11053ed4bfa756ff", + "duration (min)": 60 + }, + { + "temp": "absolute", + "_type": "TempBasal", + "id": "_0515383605e90cff11053ed4bfa756ff", + "timestamp": "2024-08-07T18:30:57.708Z", + "rate": 0 + }, + { + "id": "8e5df8c4ca2775f605f09115b496cb4a", + "duration (min)": 60, + "timestamp": "2024-08-07T18:25:56.773Z", + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T18:25:56.773Z", + "id": "_8e5df8c4ca2775f605f09115b496cb4a", + "_type": "TempBasal", + "rate": 0 + }, + { + "id": "c4cc0b49c0c7d4fa557a886a331a948b", + "_type": "TempBasalDuration", + "duration (min)": 120, + "timestamp": "2024-08-07T17:51:09.299Z" + }, + { + "id": "_c4cc0b49c0c7d4fa557a886a331a948b", + "timestamp": "2024-08-07T17:51:09.299Z", + "_type": "TempBasal", + "rate": 0, + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T17:45:50.691Z", + "_type": "Bolus", + "isExternal": false, + "isSMB": false, + "duration": 1, + "id": "b84d7af6531fa59fef6e8e7bfb8a2e1a", + "amount": 2.2 + }, + { + "_type": "TempBasalDuration", + "id": "cb3151c7dec339946c60cc258c580d9f", + "timestamp": "2024-08-07T17:40:56.183Z", + "duration (min)": 30 + }, + { + "id": "_cb3151c7dec339946c60cc258c580d9f", + "rate": 0.5, + "timestamp": "2024-08-07T17:40:56.183Z", + "temp": "absolute", + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T17:31:02.978Z", + "_type": "TempBasalDuration", + "id": "1fa1e6f8b51d054e920415f968dd4810", + "duration (min)": 30 + }, + { + "temp": "absolute", + "id": "_1fa1e6f8b51d054e920415f968dd4810", + "_type": "TempBasal", + "timestamp": "2024-08-07T17:31:02.978Z", + "rate": 0.9 + }, + { + "timestamp": "2024-08-07T17:25:56.613Z", + "duration (min)": 30, + "id": "af78f17981240e7fb3e27ed4c7fe2dc9", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T17:25:56.613Z", + "id": "_af78f17981240e7fb3e27ed4c7fe2dc9", + "_type": "TempBasal", + "rate": 0.1, + "temp": "absolute" + }, + { + "id": "8137e1725215c8424b0752cf6322c4cf", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T17:20:55.559Z" + }, + { + "timestamp": "2024-08-07T17:20:55.559Z", + "temp": "absolute", + "_type": "TempBasal", + "rate": 0.35, + "id": "_8137e1725215c8424b0752cf6322c4cf" + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "1f5ff6257fbff045d0144ce4f180970a", + "timestamp": "2024-08-07T17:10:59.860Z" + }, + { + "rate": 0.7, + "_type": "TempBasal", + "id": "_1f5ff6257fbff045d0144ce4f180970a", + "timestamp": "2024-08-07T17:10:59.860Z", + "temp": "absolute" + }, + { + "_type": "Bolus", + "isSMB": true, + "amount": 0.1, + "duration": 0, + "id": "1ecdf20b7880990542800c77423736cb", + "timestamp": "2024-08-07T17:05:55.811Z", + "isExternal": false + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T17:05:54.762Z", + "id": "7c59c7b3aafe8b2875827f9559a2cb79" + }, + { + "id": "_7c59c7b3aafe8b2875827f9559a2cb79", + "timestamp": "2024-08-07T17:05:54.762Z", + "rate": 1.15, + "temp": "absolute", + "_type": "TempBasal" + }, + { + "id": "30e456cc238693875aa21e0b761ec3f2", + "_type": "Bolus", + "isSMB": true, + "timestamp": "2024-08-07T16:16:05.242Z", + "duration": 0, + "amount": 0.1, + "isExternal": false + }, + { + "timestamp": "2024-08-07T16:16:03.638Z", + "duration (min)": 30, + "id": "6b2b0360c113a6bec489eb6fa1380a3f", + "_type": "TempBasalDuration" + }, + { + "rate": 0.35, + "_type": "TempBasal", + "id": "_6b2b0360c113a6bec489eb6fa1380a3f", + "temp": "absolute", + "timestamp": "2024-08-07T16:16:03.638Z" + }, + { + "id": "07a8e286998275c8c7e4994c93128679", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T16:06:05.233Z", + "duration (min)": 30 + }, + { + "id": "_07a8e286998275c8c7e4994c93128679", + "rate": 0, + "temp": "absolute", + "timestamp": "2024-08-07T16:06:05.233Z", + "_type": "TempBasal" + }, + { + "isSMB": true, + "_type": "Bolus", + "duration": 0, + "id": "06c31ac6b164e67653f7736e516eab3d", + "isExternal": false, + "amount": 0.1, + "timestamp": "2024-08-07T16:01:08.365Z" + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T16:01:05.405Z", + "id": "577427f6f91a01dabb9d4972d617cfe7" + }, + { + "id": "_577427f6f91a01dabb9d4972d617cfe7", + "temp": "absolute", + "rate": 0.55, + "_type": "TempBasal", + "timestamp": "2024-08-07T16:01:05.405Z" + }, + { + "duration (min)": 90, + "timestamp": "2024-08-07T15:56:08.342Z", + "_type": "TempBasalDuration", + "id": "d5fba099f152ab41880d02c47a42747b" + }, + { + "rate": 0, + "_type": "TempBasal", + "timestamp": "2024-08-07T15:56:08.342Z", + "temp": "absolute", + "id": "_d5fba099f152ab41880d02c47a42747b" + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T15:51:06.492Z", + "id": "fc9c3eeec271ed3cdea95035bf8f3f82", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "rate": 0.75, + "id": "_fc9c3eeec271ed3cdea95035bf8f3f82", + "timestamp": "2024-08-07T15:51:06.492Z", + "temp": "absolute" + }, + { + "isExternal": false, + "id": "75b847122e0d419c1044d7c4daf0befb", + "isSMB": true, + "timestamp": "2024-08-07T15:46:10.301Z", + "_type": "Bolus", + "amount": 0.2, + "duration": 0 + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T15:46:08.302Z", + "_type": "TempBasalDuration", + "id": "5d33394507285e4754446119c0322013" + }, + { + "rate": 0.2, + "_type": "TempBasal", + "timestamp": "2024-08-07T15:46:08.302Z", + "temp": "absolute", + "id": "_5d33394507285e4754446119c0322013" + }, + { + "id": "208cd9c5844642fd4ccc49587abcd240", + "duration": 0, + "_type": "Bolus", + "isExternal": false, + "isSMB": true, + "amount": 0.25, + "timestamp": "2024-08-07T15:41:36.107Z" + }, + { + "id": "8764cc3670b57fece736f7905d73cd06", + "timestamp": "2024-08-07T15:41:33.474Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "id": "_8764cc3670b57fece736f7905d73cd06", + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T15:41:33.474Z", + "rate": 0 + }, + { + "duration": 0, + "isExternal": false, + "amount": 0.15, + "_type": "Bolus", + "id": "a466cbcd3daa49b203dfa4b85152dd1a", + "isSMB": true, + "timestamp": "2024-08-07T15:36:44.623Z" + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "f782f4fda7500c453ddea73ab55ed867", + "timestamp": "2024-08-07T15:36:42.049Z" + }, + { + "timestamp": "2024-08-07T15:36:42.049Z", + "_type": "TempBasal", + "id": "_f782f4fda7500c453ddea73ab55ed867", + "temp": "absolute", + "rate": 0.05 + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T15:31:19.009Z", + "_type": "TempBasalDuration", + "id": "b7d1d744564638d96a2c98a8729aeca7" + }, + { + "temp": "absolute", + "id": "_b7d1d744564638d96a2c98a8729aeca7", + "_type": "TempBasal", + "timestamp": "2024-08-07T15:31:19.009Z", + "rate": 0.85 + }, + { + "id": "9654eec20b7034c15fb5156257c53b19", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T15:26:18.276Z" + }, + { + "_type": "TempBasal", + "rate": 0.2, + "timestamp": "2024-08-07T15:26:18.276Z", + "id": "_9654eec20b7034c15fb5156257c53b19", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T15:21:29.495Z", + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "eee30cb7931bfd19f088cb915147407c" + }, + { + "rate": 0.4, + "id": "_eee30cb7931bfd19f088cb915147407c", + "temp": "absolute", + "timestamp": "2024-08-07T15:21:29.495Z", + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T15:16:27.152Z", + "duration (min)": 30, + "id": "ef71820c550c86c149dbd0be3138cb6e" + }, + { + "_type": "TempBasal", + "id": "_ef71820c550c86c149dbd0be3138cb6e", + "timestamp": "2024-08-07T15:16:27.152Z", + "rate": 0.45, + "temp": "absolute" + }, + { + "isExternal": false, + "amount": 0.05, + "isSMB": true, + "timestamp": "2024-08-07T15:01:20.651Z", + "_type": "Bolus", + "id": "65425642b5732c7002e8e32fee7c1bda", + "duration": 0 + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "fd4dd5415c15fe278b03e91a040b1bcc", + "timestamp": "2024-08-07T15:01:18.609Z" + }, + { + "timestamp": "2024-08-07T15:01:18.609Z", + "temp": "absolute", + "_type": "TempBasal", + "rate": 0.35, + "id": "_fd4dd5415c15fe278b03e91a040b1bcc" + }, + { + "amount": 0.25, + "_type": "Bolus", + "timestamp": "2024-08-07T14:56:29.449Z", + "isSMB": true, + "id": "a773ee5ec09c2509b00073d4c2edd2c7", + "duration": 0, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T14:56:27.860Z", + "id": "a0e0aca81615ec11a0451ff7bdfa5718" + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T14:56:27.860Z", + "id": "_a0e0aca81615ec11a0451ff7bdfa5718", + "rate": 1.7, + "_type": "TempBasal" + }, + { + "_type": "Bolus", + "amount": 0.05, + "timestamp": "2024-08-07T14:52:31.678Z", + "isSMB": true, + "duration": 0, + "isExternal": false, + "id": "393d36d8b272bd53450ac3d528082b6c" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T14:52:30.143Z", + "id": "4233dd3a3cdc6deb32470cde32bdd93f" + }, + { + "id": "_4233dd3a3cdc6deb32470cde32bdd93f", + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T14:52:30.143Z", + "rate": 0.1 + }, + { + "id": "0cb3dade82b8078b9777bb52f3766c82", + "duration (min)": 30, + "timestamp": "2024-08-07T14:49:22.105Z", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T14:49:22.105Z", + "id": "_0cb3dade82b8078b9777bb52f3766c82", + "_type": "TempBasal", + "rate": 0.45, + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "id": "c4c075438b7adc042502a619008f850e", + "timestamp": "2024-08-07T14:36:07.637Z", + "duration (min)": 120 + }, + { + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T14:36:07.637Z", + "id": "_c4c075438b7adc042502a619008f850e", + "rate": 0 + }, + { + "id": "7607a0aefbd8eb3b53d0b4a8816ee8a5", + "timestamp": "2024-08-07T14:21:06.255Z", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "rate": 0.4, + "id": "_7607a0aefbd8eb3b53d0b4a8816ee8a5", + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-07T14:21:06.255Z" + }, + { + "duration": 0, + "id": "8f6a2c5712222ab3ef0d2a78b7fc167c", + "_type": "Bolus", + "amount": 0.2, + "timestamp": "2024-08-07T14:11:27.625Z", + "isExternal": false, + "isSMB": true + }, + { + "id": "103cc5f2f40b40ed64dae5d1704b4a99", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T14:11:25.951Z", + "duration (min)": 30 + }, + { + "timestamp": "2024-08-07T14:11:25.951Z", + "id": "_103cc5f2f40b40ed64dae5d1704b4a99", + "_type": "TempBasal", + "rate": 0.55, + "temp": "absolute" + }, + { + "_type": "Bolus", + "isSMB": true, + "duration": 0, + "isExternal": false, + "amount": 0.15, + "timestamp": "2024-08-07T14:06:05.613Z", + "id": "1b3001bc09227406f9e69aebc75093d6" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T14:06:04.013Z", + "duration (min)": 30, + "id": "8c72af87747f5e5be237c7d2d36ddddf" + }, + { + "timestamp": "2024-08-07T14:06:04.013Z", + "id": "_8c72af87747f5e5be237c7d2d36ddddf", + "rate": 0.5, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T14:01:10.165Z", + "amount": 0.3, + "isExternal": false, + "_type": "Bolus", + "isSMB": true, + "id": "038132a99ae618b06e32974d9824e447", + "duration": 0 + }, + { + "id": "15f0cbef90ca7494b2837b6c48990604", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T14:01:08.341Z" + }, + { + "timestamp": "2024-08-07T14:01:08.341Z", + "id": "_15f0cbef90ca7494b2837b6c48990604", + "rate": 1.95, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "id": "d624a6d657cdfbc7e5adb808235a629a", + "isSMB": true, + "_type": "Bolus", + "isExternal": false, + "amount": 0.2, + "duration": 0, + "timestamp": "2024-08-07T13:56:09.727Z" + }, + { + "id": "246f0c3e3bf5a3b9ad8a0487d5dea847", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T13:56:07.839Z" + }, + { + "_type": "TempBasal", + "id": "_246f0c3e3bf5a3b9ad8a0487d5dea847", + "rate": 1.5, + "temp": "absolute", + "timestamp": "2024-08-07T13:56:07.839Z" + }, + { + "timestamp": "2024-08-07T13:51:12.022Z", + "id": "fe394df9c8a82bdbd41f6e561468bc2b", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "id": "_fe394df9c8a82bdbd41f6e561468bc2b", + "rate": 0.45, + "timestamp": "2024-08-07T13:51:12.022Z", + "temp": "absolute", + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T13:36:16.047Z", + "id": "40b5a3bcdbbfeb650fad8dbba35d46c7", + "_type": "TempBasalDuration", + "duration (min)": 120 + }, + { + "rate": 0, + "timestamp": "2024-08-07T13:36:16.047Z", + "_type": "TempBasal", + "temp": "absolute", + "id": "_40b5a3bcdbbfeb650fad8dbba35d46c7" + }, + { + "duration (min)": 30, + "id": "0b2c41bb05eae156bdacf2de102ce94b", + "timestamp": "2024-08-07T13:26:07.809Z", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T13:26:07.809Z", + "_type": "TempBasal", + "rate": 0.75, + "temp": "absolute", + "id": "_0b2c41bb05eae156bdacf2de102ce94b" + }, + { + "_type": "TempBasalDuration", + "id": "6f20ff2cf789fdff1df771e3a797dff7", + "timestamp": "2024-08-07T13:16:20.274Z", + "duration (min)": 120 + }, + { + "id": "_6f20ff2cf789fdff1df771e3a797dff7", + "timestamp": "2024-08-07T13:16:20.274Z", + "rate": 0, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T13:01:18.022Z", + "duration (min)": 60, + "id": "e8da93f4db515dfafd7414e1849b5f56", + "_type": "TempBasalDuration" + }, + { + "id": "_e8da93f4db515dfafd7414e1849b5f56", + "timestamp": "2024-08-07T13:01:18.022Z", + "_type": "TempBasal", + "rate": 0, + "temp": "absolute" + }, + { + "id": "0c5d362d3884a487b4c2f640658e9cb7", + "isSMB": true, + "isExternal": false, + "timestamp": "2024-08-07T12:56:08.943Z", + "_type": "Bolus", + "duration": 0, + "amount": 0.6 + }, + { + "id": "fddedcfe2d5122f8892bd21f164ad01b", + "timestamp": "2024-08-07T12:56:07.232Z", + "_type": "TempBasalDuration", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-07T12:56:07.232Z", + "_type": "TempBasal", + "rate": 0, + "id": "_fddedcfe2d5122f8892bd21f164ad01b", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T12:46:28.992Z", + "amount": 0.4, + "_type": "Bolus", + "id": "f6d0b63f250c39cb8d7c4e1ccdcf3594", + "isSMB": true, + "duration": 0, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "id": "b581318a72f20c1d864b3e1f9a472508", + "timestamp": "2024-08-07T12:46:27.390Z", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "rate": 0, + "temp": "absolute", + "timestamp": "2024-08-07T12:46:27.390Z", + "id": "_b581318a72f20c1d864b3e1f9a472508" + }, + { + "amount": 0.6, + "id": "5969ffe950d59a2cee4b97db2bd2bb65", + "isExternal": false, + "timestamp": "2024-08-07T12:41:16.599Z", + "duration": 0, + "_type": "Bolus", + "isSMB": true + }, + { + "_type": "TempBasalDuration", + "id": "b6b0a7857afdcc3345e76bbd8f86ed8f", + "timestamp": "2024-08-07T12:41:14.764Z", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-07T12:41:14.764Z", + "rate": 0, + "id": "_b6b0a7857afdcc3345e76bbd8f86ed8f", + "_type": "TempBasal", + "temp": "absolute" + }, + { + "_type": "Bolus", + "duration": 0, + "timestamp": "2024-08-07T12:36:15.723Z", + "id": "332fed0c34268b9bad7946d044cd83cf", + "isSMB": true, + "isExternal": false, + "amount": 0.85 + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T12:36:13.857Z", + "duration (min)": 30, + "id": "11428704bfb97827e419c7033f09325f" + }, + { + "_type": "TempBasal", + "id": "_11428704bfb97827e419c7033f09325f", + "temp": "absolute", + "rate": 0.35, + "timestamp": "2024-08-07T12:36:13.857Z" + }, + { + "id": "3608452d6d1b211372debda6ac93b0fb", + "duration": 0, + "timestamp": "2024-08-07T12:31:30.362Z", + "isExternal": false, + "amount": 0.15, + "_type": "Bolus", + "isSMB": true + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T12:31:28.526Z", + "_type": "TempBasalDuration", + "id": "cecd31373c0f1dbfdea022c576f36e41" + }, + { + "rate": 0.65, + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-07T12:31:28.526Z", + "id": "_cecd31373c0f1dbfdea022c576f36e41" + }, + { + "id": "1b93247e2fdca7d6e04c5d287a02c612", + "timestamp": "2024-08-07T12:26:12.225Z", + "_type": "TempBasalDuration", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "id": "_1b93247e2fdca7d6e04c5d287a02c612", + "timestamp": "2024-08-07T12:26:12.225Z", + "temp": "absolute", + "rate": 0 + }, + { + "timestamp": "2024-08-07T12:21:06.716Z", + "duration (min)": 60, + "id": "8777874c8469a9d0225a8d9ac2b173f1", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T12:21:06.716Z", + "temp": "absolute", + "rate": 0, + "id": "_8777874c8469a9d0225a8d9ac2b173f1", + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "id": "50924b975c859a05872ceeeafa9c20b4", + "timestamp": "2024-08-07T12:16:05.192Z", + "duration (min)": 60 + }, + { + "id": "_50924b975c859a05872ceeeafa9c20b4", + "_type": "TempBasal", + "rate": 0, + "timestamp": "2024-08-07T12:16:05.192Z", + "temp": "absolute" + }, + { + "id": "e2c33fc4f1b95e197c78ce95f5964233", + "timestamp": "2024-08-07T12:06:08.343Z", + "_type": "TempBasalDuration", + "duration (min)": 60 + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T12:06:08.343Z", + "rate": 0, + "id": "_e2c33fc4f1b95e197c78ce95f5964233", + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T12:01:08.167Z", + "duration (min)": 60, + "id": "4e0ec82a4cbb54f3da33d6b218e2abea" + }, + { + "rate": 0, + "temp": "absolute", + "id": "_4e0ec82a4cbb54f3da33d6b218e2abea", + "timestamp": "2024-08-07T12:01:08.167Z", + "_type": "TempBasal" + }, + { + "isExternal": false, + "amount": 0.1, + "id": "eeb3e679876d8e1a46fc418b15d0be7d", + "timestamp": "2024-08-07T11:56:07.286Z", + "duration": 0, + "isSMB": true, + "_type": "Bolus" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T11:56:05.685Z", + "duration (min)": 60, + "id": "16a5f21ad23705a52a5797c7951428b1" + }, + { + "timestamp": "2024-08-07T11:56:05.685Z", + "temp": "absolute", + "rate": 0, + "_type": "TempBasal", + "id": "_16a5f21ad23705a52a5797c7951428b1" + }, + { + "id": "2c49cf532fc5961ae051b4991f126ff7", + "timestamp": "2024-08-07T11:51:08.585Z", + "isSMB": true, + "duration": 0, + "_type": "Bolus", + "amount": 0.05, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "id": "e2f8e84b3383a819540ca0d9922cda36", + "timestamp": "2024-08-07T11:51:07.056Z", + "duration (min)": 60 + }, + { + "id": "_e2f8e84b3383a819540ca0d9922cda36", + "temp": "absolute", + "timestamp": "2024-08-07T11:51:07.056Z", + "rate": 0, + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-07T11:41:10.396Z", + "id": "a5d2a76587f497dc0d56867284122b94" + }, + { + "temp": "absolute", + "_type": "TempBasal", + "id": "_a5d2a76587f497dc0d56867284122b94", + "timestamp": "2024-08-07T11:41:10.396Z", + "rate": 0 + }, + { + "duration (min)": 60, + "id": "5316feb8b8b477aa65e2f61efbc061e0", + "timestamp": "2024-08-07T11:31:36.482Z", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "id": "_5316feb8b8b477aa65e2f61efbc061e0", + "rate": 0, + "timestamp": "2024-08-07T11:31:36.482Z", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T11:29:05.405Z", + "isSMB": false, + "duration": 1, + "_type": "Bolus", + "id": "d1f40728435ce895b4645ec91843783e", + "amount": 2, + "isExternal": false + }, + { + "duration (min)": 60, + "timestamp": "2024-08-07T11:25:57.681Z", + "_type": "TempBasalDuration", + "id": "fe1e6a970ab969aea22d08126f906c21" + }, + { + "timestamp": "2024-08-07T11:25:57.681Z", + "id": "_fe1e6a970ab969aea22d08126f906c21", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0 + }, + { + "id": "a67eea1280953ba65ec9d1453cac656a", + "duration (min)": 30, + "timestamp": "2024-08-07T11:21:05.697Z", + "_type": "TempBasalDuration" + }, + { + "rate": 0.8, + "id": "_a67eea1280953ba65ec9d1453cac656a", + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T11:21:05.697Z" + }, + { + "isSMB": true, + "id": "a25ee82dfbed2bcfbd2af9e6902ad82c", + "timestamp": "2024-08-07T11:11:08.493Z", + "duration": 0, + "_type": "Bolus", + "amount": 0.15, + "isExternal": false + }, + { + "id": "13808739ca38f44f9caa28051c541159", + "timestamp": "2024-08-07T11:11:06.474Z", + "_type": "TempBasalDuration", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-07T11:11:06.474Z", + "_type": "TempBasal", + "temp": "absolute", + "id": "_13808739ca38f44f9caa28051c541159", + "rate": 0 + }, + { + "timestamp": "2024-08-07T11:06:17.548Z", + "duration": 0, + "_type": "Bolus", + "id": "ba3d8e475049672b3076b6f023d56191", + "isSMB": true, + "amount": 0.5, + "isExternal": false + }, + { + "id": "a7cb4f5324785d55331c4151801f5951", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T11:06:15.710Z", + "duration (min)": 30 + }, + { + "id": "_a7cb4f5324785d55331c4151801f5951", + "timestamp": "2024-08-07T11:06:15.710Z", + "rate": 0.25, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T11:01:07.304Z", + "duration": 0, + "isExternal": false, + "isSMB": true, + "_type": "Bolus", + "id": "b08b13447e512f8daf40adf3383e7cf6", + "amount": 0.15 + }, + { + "duration (min)": 60, + "_type": "TempBasalDuration", + "id": "6c4e146e8bcfa92386b360bc77047e32", + "timestamp": "2024-08-07T11:01:05.475Z" + }, + { + "temp": "absolute", + "rate": 0, + "_type": "TempBasal", + "id": "_6c4e146e8bcfa92386b360bc77047e32", + "timestamp": "2024-08-07T11:01:05.475Z" + }, + { + "_type": "Bolus", + "timestamp": "2024-08-07T10:56:19.210Z", + "isSMB": true, + "id": "d8781794833b03db4844c18875b51705", + "duration": 0, + "amount": 0.1, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "id": "a77ebafb3fd90ca549171ec94ce27b62", + "timestamp": "2024-08-07T10:56:17.726Z" + }, + { + "temp": "absolute", + "id": "_a77ebafb3fd90ca549171ec94ce27b62", + "timestamp": "2024-08-07T10:56:17.726Z", + "_type": "TempBasal", + "rate": 0 + }, + { + "timestamp": "2024-08-07T10:51:41.074Z", + "_type": "Bolus", + "isSMB": true, + "amount": 0.1, + "duration": 0, + "id": "7dee26a02e9b6fdc25476fb27b294c7d", + "isExternal": false + }, + { + "timestamp": "2024-08-07T10:51:38.681Z", + "_type": "TempBasalDuration", + "duration (min)": 60, + "id": "31ce2d969be4199ec047fada0340543c" + }, + { + "rate": 0, + "timestamp": "2024-08-07T10:51:38.681Z", + "id": "_31ce2d969be4199ec047fada0340543c", + "temp": "absolute", + "_type": "TempBasal" + }, + { + "_type": "Bolus", + "duration": 0, + "isExternal": false, + "timestamp": "2024-08-07T10:46:37.334Z", + "amount": 0.05, + "isSMB": true, + "id": "af0c4585c078b5cc5f71635384c70984" + }, + { + "_type": "TempBasalDuration", + "id": "a9eb03a20affbab3c79f30eff6913eee", + "timestamp": "2024-08-07T10:46:34.517Z", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "rate": 0, + "temp": "absolute", + "id": "_a9eb03a20affbab3c79f30eff6913eee", + "timestamp": "2024-08-07T10:46:34.517Z" + }, + { + "timestamp": "2024-08-07T10:31:19.632Z", + "_type": "TempBasalDuration", + "id": "45a4fbfaea23a98f68992b2e16b19fc6", + "duration (min)": 60 + }, + { + "temp": "absolute", + "id": "_45a4fbfaea23a98f68992b2e16b19fc6", + "rate": 0, + "timestamp": "2024-08-07T10:31:19.632Z", + "_type": "TempBasal" + }, + { + "isSMB": false, + "amount": 1.95, + "timestamp": "2024-08-07T10:29:27.809Z", + "id": "2d20a151aaec2decc90b8fc5220ec26b", + "_type": "Bolus", + "duration": 1, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "dffa55028951ec647c24950b900cadd2", + "timestamp": "2024-08-07T10:18:41.767Z" + }, + { + "id": "_dffa55028951ec647c24950b900cadd2", + "_type": "TempBasal", + "timestamp": "2024-08-07T10:18:41.767Z", + "rate": 0.4, + "temp": "absolute" + }, + { + "id": "f82445a3359dc3fafd942d6bdd2040cc", + "_type": "Bolus", + "amount": 0.05, + "isExternal": false, + "duration": 0, + "timestamp": "2024-08-07T10:15:39.119Z", + "isSMB": true + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "7bf16d0d9d74494b55773af27742f529", + "timestamp": "2024-08-07T10:15:37.423Z" + }, + { + "timestamp": "2024-08-07T10:15:37.423Z", + "temp": "absolute", + "rate": 0.9, + "id": "_7bf16d0d9d74494b55773af27742f529", + "_type": "TempBasal" + }, + { + "isSMB": true, + "timestamp": "2024-08-07T10:06:30.000Z", + "duration": 0, + "_type": "Bolus", + "isExternal": false, + "id": "db692435e33de4bf4e3fc34d4a1c8607", + "amount": 0.25 + }, + { + "id": "e166badcf1f6b9ae2b23a86048223b4f", + "timestamp": "2024-08-07T09:56:03.883Z", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "timestamp": "2024-08-07T09:56:03.883Z", + "id": "_e166badcf1f6b9ae2b23a86048223b4f", + "_type": "TempBasal", + "rate": 1.85, + "temp": "absolute" + }, + { + "duration (min)": 30, + "id": "1791a66476789f64a712ecce7abcb3bb", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T09:46:05.700Z" + }, + { + "temp": "absolute", + "id": "_1791a66476789f64a712ecce7abcb3bb", + "_type": "TempBasal", + "timestamp": "2024-08-07T09:46:05.700Z", + "rate": 0.65 + }, + { + "timestamp": "2024-08-07T09:36:08.735Z", + "duration (min)": 30, + "id": "294d10ee96b3db8d8503799e70142172", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T09:36:08.735Z", + "_type": "TempBasal", + "temp": "absolute", + "rate": 1.35, + "id": "_294d10ee96b3db8d8503799e70142172" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T09:31:08.149Z", + "id": "2565e8724ac40146ac74d34f57d001c8", + "duration (min)": 30 + }, + { + "timestamp": "2024-08-07T09:31:08.149Z", + "_type": "TempBasal", + "rate": 0.55, + "temp": "absolute", + "id": "_2565e8724ac40146ac74d34f57d001c8" + }, + { + "id": "3b2906df12ad5dfacf33b3557fcd2458", + "timestamp": "2024-08-07T09:16:07.542Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "rate": 0.45, + "id": "_3b2906df12ad5dfacf33b3557fcd2458", + "timestamp": "2024-08-07T09:16:07.542Z", + "_type": "TempBasal", + "temp": "absolute" + }, + { + "id": "ed3c24e10820b076715267d625a33845", + "duration (min)": 90, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T09:06:07.464Z" + }, + { + "timestamp": "2024-08-07T09:06:07.464Z", + "_type": "TempBasal", + "rate": 0, + "temp": "absolute", + "id": "_ed3c24e10820b076715267d625a33845" + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T09:01:08.405Z", + "id": "8461d1f58b883aced58cbef7d3b2081c", + "_type": "TempBasalDuration" + }, + { + "rate": 0.6, + "timestamp": "2024-08-07T09:01:08.405Z", + "_type": "TempBasal", + "id": "_8461d1f58b883aced58cbef7d3b2081c", + "temp": "absolute" + }, + { + "duration (min)": 30, + "id": "6b8188e9891cf6a57e11a3d820cf809b", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T08:51:03.893Z" + }, + { + "id": "_6b8188e9891cf6a57e11a3d820cf809b", + "rate": 1, + "_type": "TempBasal", + "timestamp": "2024-08-07T08:51:03.893Z", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T08:41:07.422Z", + "id": "1289686c0b128a537450b4a4ba0845d2", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "id": "_1289686c0b128a537450b4a4ba0845d2", + "timestamp": "2024-08-07T08:41:07.422Z", + "_type": "TempBasal", + "rate": 0.55 + }, + { + "_type": "TempBasalDuration", + "id": "944fb27e42988acf292e514ef3ef1995", + "duration (min)": 30, + "timestamp": "2024-08-07T08:36:08.827Z" + }, + { + "_type": "TempBasal", + "id": "_944fb27e42988acf292e514ef3ef1995", + "timestamp": "2024-08-07T08:36:08.827Z", + "rate": 1.7, + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "179741ddea04b4562a5331e9319640c7", + "timestamp": "2024-08-07T08:31:08.243Z" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-07T08:31:08.243Z", + "id": "_179741ddea04b4562a5331e9319640c7", + "rate": 0.55, + "temp": "absolute" + }, + { + "duration (min)": 120, + "timestamp": "2024-08-07T08:16:06.549Z", + "id": "4ce21b020744d9c15d56c0974cedb172", + "_type": "TempBasalDuration" + }, + { + "id": "_4ce21b020744d9c15d56c0974cedb172", + "timestamp": "2024-08-07T08:16:06.549Z", + "rate": 0, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "id": "f5613712ceee0ca0c3980a8c49cc29e9", + "timestamp": "2024-08-07T08:11:01.900Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "id": "_f5613712ceee0ca0c3980a8c49cc29e9", + "timestamp": "2024-08-07T08:11:01.900Z", + "rate": 0.75, + "temp": "absolute", + "_type": "TempBasal" + }, + { + "id": "f8e5ab3803cf260d5bce1ef827f2c3f3", + "duration (min)": 30, + "timestamp": "2024-08-07T08:01:10.160Z", + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T08:01:10.160Z", + "_type": "TempBasal", + "id": "_f8e5ab3803cf260d5bce1ef827f2c3f3", + "rate": 0.7, + "temp": "absolute" + }, + { + "id": "c3c7aecbb9034c8f4de0cca045c0e466", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T07:56:30.585Z" + }, + { + "timestamp": "2024-08-07T07:56:30.585Z", + "rate": 0.45, + "_type": "TempBasal", + "id": "_c3c7aecbb9034c8f4de0cca045c0e466", + "temp": "absolute" + }, + { + "id": "535a7f974ea3cf61a2ec693ad56aa3a4", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T07:41:18.349Z" + }, + { + "temp": "absolute", + "id": "_535a7f974ea3cf61a2ec693ad56aa3a4", + "timestamp": "2024-08-07T07:41:18.349Z", + "rate": 1.9, + "_type": "TempBasal" + }, + { + "id": "3613b68182bbe3df026914abeedb94ba", + "timestamp": "2024-08-07T07:36:21.416Z", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "rate": 1.1, + "timestamp": "2024-08-07T07:36:21.416Z", + "temp": "absolute", + "_type": "TempBasal", + "id": "_3613b68182bbe3df026914abeedb94ba" + }, + { + "id": "531201d255c31ddfb4b54920b5a33779", + "duration (min)": 30, + "timestamp": "2024-08-07T07:31:24.985Z", + "_type": "TempBasalDuration" + }, + { + "id": "_531201d255c31ddfb4b54920b5a33779", + "temp": "absolute", + "_type": "TempBasal", + "rate": 0.85, + "timestamp": "2024-08-07T07:31:24.985Z" + }, + { + "id": "e6d3806773255c009a5f26209f738497", + "timestamp": "2024-08-07T07:16:33.558Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-07T07:16:33.558Z", + "temp": "absolute", + "rate": 0.75, + "_type": "TempBasal", + "id": "_e6d3806773255c009a5f26209f738497" + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T07:11:36.099Z", + "id": "01931a03cd9ed92075e82bd83e0be336" + }, + { + "_type": "TempBasal", + "id": "_01931a03cd9ed92075e82bd83e0be336", + "temp": "absolute", + "timestamp": "2024-08-07T07:11:36.099Z", + "rate": 0.55 + }, + { + "_type": "TempBasalDuration", + "id": "69086bca3a8270d1a41c58f1666c4c98", + "timestamp": "2024-08-07T07:06:19.344Z", + "duration (min)": 30 + }, + { + "id": "_69086bca3a8270d1a41c58f1666c4c98", + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-07T07:06:19.344Z", + "rate": 0.45 + }, + { + "_type": "TempBasalDuration", + "id": "5d74ecf4a143dca151bbdb844af849fd", + "timestamp": "2024-08-07T07:01:37.668Z", + "duration (min)": 90 + }, + { + "timestamp": "2024-08-07T07:01:37.668Z", + "_type": "TempBasal", + "id": "_5d74ecf4a143dca151bbdb844af849fd", + "rate": 0, + "temp": "absolute" + }, + { + "id": "5c30da5d15aa7304c68e4ceaeb03f46d", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T06:56:11.739Z" + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T06:56:11.739Z", + "rate": 0, + "_type": "TempBasal", + "id": "_5c30da5d15aa7304c68e4ceaeb03f46d" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "4377ceb086ee23f4d484d46a4494753e", + "timestamp": "2024-08-07T06:46:03.724Z" + }, + { + "_type": "TempBasal", + "rate": 0.55, + "id": "_4377ceb086ee23f4d484d46a4494753e", + "temp": "absolute", + "timestamp": "2024-08-07T06:46:03.724Z" + }, + { + "timestamp": "2024-08-07T06:41:07.374Z", + "id": "5f0a4ab8182848d1de6c14a4c8e44351", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "id": "_5f0a4ab8182848d1de6c14a4c8e44351", + "timestamp": "2024-08-07T06:41:07.374Z", + "temp": "absolute", + "rate": 0.8 + }, + { + "duration (min)": 30, + "id": "2e066be80cc03ae5493336eda3c7d593", + "timestamp": "2024-08-07T06:36:14.218Z", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "rate": 0.45, + "temp": "absolute", + "id": "_2e066be80cc03ae5493336eda3c7d593", + "timestamp": "2024-08-07T06:36:14.218Z" + }, + { + "id": "408f7f91a7b160155a7ccef86b2bc260", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-07T06:21:12.862Z" + }, + { + "timestamp": "2024-08-07T06:21:12.862Z", + "id": "_408f7f91a7b160155a7ccef86b2bc260", + "_type": "TempBasal", + "rate": 0.45, + "temp": "absolute" + }, + { + "duration (min)": 30, + "timestamp": "2024-08-07T06:06:10.258Z", + "id": "84d26aa5678b5435ab85e57ee18a46af", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "rate": 0.45, + "id": "_84d26aa5678b5435ab85e57ee18a46af", + "timestamp": "2024-08-07T06:06:10.258Z" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T05:56:09.636Z", + "duration (min)": 30, + "id": "9ff981c46591c1414a4e0f364ec81609" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "id": "_9ff981c46591c1414a4e0f364ec81609", + "rate": 0, + "timestamp": "2024-08-07T05:56:09.636Z" + }, + { + "id": "8b9f3a1daa3f9b5946dc2252053becca", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T05:51:10.680Z" + }, + { + "temp": "absolute", + "rate": 0.15, + "id": "_8b9f3a1daa3f9b5946dc2252053becca", + "timestamp": "2024-08-07T05:51:10.680Z", + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "6dd6a5a3c246c3df61deb9677695fc7a", + "timestamp": "2024-08-07T05:46:11.064Z" + }, + { + "timestamp": "2024-08-07T05:46:11.064Z", + "id": "_6dd6a5a3c246c3df61deb9677695fc7a", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0.25 + }, + { + "timestamp": "2024-08-07T05:31:11.900Z", + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "850792b30317e7b4a15b252efb7e1adc" + }, + { + "_type": "TempBasal", + "rate": 1.5, + "temp": "absolute", + "timestamp": "2024-08-07T05:31:11.900Z", + "id": "_850792b30317e7b4a15b252efb7e1adc" + }, + { + "id": "ff6b0ec44ff8222581646c15ffef2712", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T05:21:11.402Z", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "rate": 1.2, + "temp": "absolute", + "id": "_ff6b0ec44ff8222581646c15ffef2712", + "timestamp": "2024-08-07T05:21:11.402Z" + }, + { + "timestamp": "2024-08-07T05:16:04.376Z", + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "aa6261600d46cb46fe66ef79c536fb32" + }, + { + "_type": "TempBasal", + "rate": 0.75, + "id": "_aa6261600d46cb46fe66ef79c536fb32", + "temp": "absolute", + "timestamp": "2024-08-07T05:16:04.376Z" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "31a74b8bca3607ccd81ea1cd974c5528", + "timestamp": "2024-08-07T05:06:31.686Z" + }, + { + "id": "_31a74b8bca3607ccd81ea1cd974c5528", + "timestamp": "2024-08-07T05:06:31.686Z", + "_type": "TempBasal", + "rate": 0.5, + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "id": "f8a16314d27b9b8a0dae6bae1b107aa8", + "duration (min)": 30, + "timestamp": "2024-08-07T05:01:10.579Z" + }, + { + "rate": 0.65, + "id": "_f8a16314d27b9b8a0dae6bae1b107aa8", + "timestamp": "2024-08-07T05:01:10.579Z", + "temp": "absolute", + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T04:47:01.292Z", + "id": "39d285e40d853aeb4fdaf823d1b1ef52", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "id": "_39d285e40d853aeb4fdaf823d1b1ef52", + "timestamp": "2024-08-07T04:47:01.292Z", + "rate": 1.55, + "temp": "absolute", + "_type": "TempBasal" + }, + { + "id": "197439a850423e52e9a8fe5674d9573a", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T04:40:59.292Z", + "duration (min)": 30 + }, + { + "temp": "absolute", + "rate": 0.9, + "_type": "TempBasal", + "timestamp": "2024-08-07T04:40:59.292Z", + "id": "_197439a850423e52e9a8fe5674d9573a" + }, + { + "id": "6979eebf6a63ba52e9590409c00997e3", + "timestamp": "2024-08-07T04:16:05.031Z", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "rate": 0.85, + "id": "_6979eebf6a63ba52e9590409c00997e3", + "timestamp": "2024-08-07T04:16:05.031Z", + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "id": "6ae315c4a544e2960aa660ed21082621", + "timestamp": "2024-08-07T04:10:58.507Z", + "duration (min)": 30 + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T04:10:58.507Z", + "_type": "TempBasal", + "rate": 0.4, + "id": "_6ae315c4a544e2960aa660ed21082621" + }, + { + "id": "b1a33727d796ecc75593010994c2134f", + "timestamp": "2024-08-07T04:07:00.236Z", + "_type": "PumpResume" + }, + { + "timestamp": "2024-08-07T04:06:59.622Z", + "_type": "PumpSuspend", + "id": "1eb6fd174830f67a461385dda7bb8755" + }, + { + "duration (min)": 60, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T04:06:11.774Z", + "id": "c8f95b793fc396728e0d1f448190bb79" + }, + { + "timestamp": "2024-08-07T04:06:11.774Z", + "id": "_c8f95b793fc396728e0d1f448190bb79", + "rate": 0, + "_type": "TempBasal", + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "id": "b3175564b37c150bbc1f1315ce688981", + "duration (min)": 30, + "timestamp": "2024-08-07T04:01:52.647Z" + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T04:01:52.647Z", + "rate": 0.35, + "_type": "TempBasal", + "id": "_b3175564b37c150bbc1f1315ce688981" + }, + { + "duration (min)": 30, + "id": "3edeb7ee5cddc64b2a1dfad043e7b20f", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T03:51:20.077Z" + }, + { + "rate": 0.55, + "temp": "absolute", + "id": "_3edeb7ee5cddc64b2a1dfad043e7b20f", + "_type": "TempBasal", + "timestamp": "2024-08-07T03:51:20.077Z" + }, + { + "id": "c8842aefe775b5e1015304d7ed6a5d3f", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T03:26:39.121Z", + "duration (min)": 30 + }, + { + "rate": 0, + "temp": "absolute", + "id": "_c8842aefe775b5e1015304d7ed6a5d3f", + "timestamp": "2024-08-07T03:26:39.121Z", + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T03:11:31.865Z", + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "b5dc753aab7bbb043fb7e4b878cec954" + }, + { + "temp": "absolute", + "id": "_b5dc753aab7bbb043fb7e4b878cec954", + "timestamp": "2024-08-07T03:11:31.865Z", + "_type": "TempBasal", + "rate": 0 + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T02:56:44.166Z", + "id": "415a91565d98eb4e40a49593d07092e2" + }, + { + "_type": "TempBasal", + "rate": 0.7, + "timestamp": "2024-08-07T02:56:44.166Z", + "id": "_415a91565d98eb4e40a49593d07092e2", + "temp": "absolute" + }, + { + "timestamp": "2024-08-07T02:52:13.289Z", + "id": "fedfb82325c511c4bc2b3965c392829a", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "id": "_fedfb82325c511c4bc2b3965c392829a", + "rate": 1.25, + "timestamp": "2024-08-07T02:52:13.289Z" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T02:49:10.378Z", + "duration (min)": 30, + "id": "8b685e0374905654cb297b26dcb28cd6" + }, + { + "timestamp": "2024-08-07T02:49:10.378Z", + "id": "_8b685e0374905654cb297b26dcb28cd6", + "temp": "absolute", + "rate": 0.6, + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T02:31:04.977Z", + "id": "63cf956641ce9bdc12a7200df3831144", + "duration": 0, + "isSMB": true, + "amount": 0.1, + "isExternal": false, + "_type": "Bolus" + }, + { + "timestamp": "2024-08-07T02:31:03.223Z", + "id": "6eac832d681cd8f6b799f1bb2c74ab6e", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "id": "_6eac832d681cd8f6b799f1bb2c74ab6e", + "rate": 0.25, + "temp": "absolute", + "timestamp": "2024-08-07T02:31:03.223Z" + }, + { + "amount": 0.15, + "id": "5a767e5a1395d8c412a07614169e2981", + "isExternal": false, + "isSMB": true, + "duration": 0, + "timestamp": "2024-08-07T02:26:06.079Z", + "_type": "Bolus" + }, + { + "timestamp": "2024-08-07T02:26:04.460Z", + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "cb0fcb334b5b317236c387f20d493f45" + }, + { + "_type": "TempBasal", + "id": "_cb0fcb334b5b317236c387f20d493f45", + "timestamp": "2024-08-07T02:26:04.460Z", + "rate": 0.1, + "temp": "absolute" + }, + { + "isSMB": true, + "timestamp": "2024-08-07T02:21:03.499Z", + "isExternal": false, + "id": "89f6fda168fd5418785bd194768afbb5", + "_type": "Bolus", + "amount": 0.2, + "duration": 0 + }, + { + "isSMB": true, + "amount": 0.15, + "isExternal": false, + "id": "4e7e7e17e8922b0ad7464263be8516d5", + "timestamp": "2024-08-07T02:16:08.215Z", + "_type": "Bolus", + "duration": 0 + }, + { + "id": "951f072934b14dff84facc71c50b2be1", + "duration (min)": 30, + "timestamp": "2024-08-07T02:16:06.322Z", + "_type": "TempBasalDuration" + }, + { + "rate": 1.25, + "temp": "absolute", + "timestamp": "2024-08-07T02:16:06.322Z", + "_type": "TempBasal", + "id": "_951f072934b14dff84facc71c50b2be1" + }, + { + "duration": 0, + "isSMB": true, + "isExternal": false, + "timestamp": "2024-08-07T02:11:08.318Z", + "amount": 0.1, + "_type": "Bolus", + "id": "f5747dcf3e6fbfc2e0d5ec0a55a271af" + }, + { + "id": "3f45ea5b7c78ee32cedd961ec41de663", + "timestamp": "2024-08-07T02:11:06.120Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "rate": 0.95, + "temp": "absolute", + "timestamp": "2024-08-07T02:11:06.120Z", + "id": "_3f45ea5b7c78ee32cedd961ec41de663" + }, + { + "timestamp": "2024-08-07T02:06:06.138Z", + "id": "1a4c202bbd913b4ee0f5cb189df0f892", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "timestamp": "2024-08-07T02:06:06.138Z", + "id": "_1a4c202bbd913b4ee0f5cb189df0f892", + "rate": 0.65 + }, + { + "id": "548b9d018f8f8cbdaafb23a8fea1e719", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T02:01:07.611Z" + }, + { + "_type": "TempBasal", + "id": "_548b9d018f8f8cbdaafb23a8fea1e719", + "timestamp": "2024-08-07T02:01:07.611Z", + "temp": "absolute", + "rate": 0.4 + }, + { + "timestamp": "2024-08-07T01:56:07.427Z", + "id": "93b28664f9a2e6d4398271344a3988ff", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "id": "_93b28664f9a2e6d4398271344a3988ff", + "rate": 0.55, + "timestamp": "2024-08-07T01:56:07.427Z", + "temp": "absolute" + }, + { + "duration (min)": 30, + "id": "70d35d671d254d7e015d4fcd56905ad4", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T01:51:04.391Z" + }, + { + "temp": "absolute", + "rate": 0.45, + "timestamp": "2024-08-07T01:51:04.391Z", + "_type": "TempBasal", + "id": "_70d35d671d254d7e015d4fcd56905ad4" + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T01:46:02.319Z", + "duration (min)": 30, + "id": "4881d6fbcc57b4404be75fa033dac23f" + }, + { + "id": "_4881d6fbcc57b4404be75fa033dac23f", + "_type": "TempBasal", + "timestamp": "2024-08-07T01:46:02.319Z", + "temp": "absolute", + "rate": 0.05 + }, + { + "id": "7d5607e085a125f44497b5f25699b96e", + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T01:36:09.038Z", + "duration (min)": 30 + }, + { + "id": "_7d5607e085a125f44497b5f25699b96e", + "timestamp": "2024-08-07T01:36:09.038Z", + "rate": 0.6, + "temp": "absolute", + "_type": "TempBasal" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "5e64191d7cebfdc494511ce103aafcc2", + "timestamp": "2024-08-07T01:26:05.956Z" + }, + { + "id": "_5e64191d7cebfdc494511ce103aafcc2", + "timestamp": "2024-08-07T01:26:05.956Z", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0.55 + }, + { + "duration (min)": 60, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T01:11:08.040Z", + "id": "5c49487baa42faf6175cdef47f8d0193" + }, + { + "id": "_5c49487baa42faf6175cdef47f8d0193", + "_type": "TempBasal", + "rate": 0, + "temp": "absolute", + "timestamp": "2024-08-07T01:11:08.040Z" + }, + { + "amount": 0.05, + "isExternal": false, + "_type": "Bolus", + "timestamp": "2024-08-07T01:06:25.097Z", + "isSMB": true, + "id": "ffe3227b1437005c0d535d507ad8a6f2", + "duration": 0 + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T01:06:23.484Z", + "duration (min)": 30, + "id": "07f2971f213a7eeae78cd3f158012519" + }, + { + "_type": "TempBasal", + "temp": "absolute", + "rate": 0.8, + "timestamp": "2024-08-07T01:06:23.484Z", + "id": "_07f2971f213a7eeae78cd3f158012519" + }, + { + "isExternal": false, + "isSMB": true, + "duration": 0, + "id": "110b6d131297852bbf57d7d5df499f90", + "timestamp": "2024-08-07T01:01:10.872Z", + "_type": "Bolus", + "amount": 0.05 + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "ef3a55edea143c11b7af4c3ad4fe5141", + "timestamp": "2024-08-07T01:01:09.231Z" + }, + { + "temp": "absolute", + "id": "_ef3a55edea143c11b7af4c3ad4fe5141", + "_type": "TempBasal", + "rate": 0, + "timestamp": "2024-08-07T01:01:09.231Z" + }, + { + "isExternal": false, + "duration": 0, + "isSMB": true, + "id": "bb0ac955ef884057915aae9f2d21f91d", + "_type": "Bolus", + "timestamp": "2024-08-07T00:56:07.691Z", + "amount": 0.05 + }, + { + "timestamp": "2024-08-07T00:56:05.907Z", + "id": "449af0ac2d69b8aaf0d310c62338489c", + "_type": "TempBasalDuration", + "duration (min)": 30 + }, + { + "_type": "TempBasal", + "id": "_449af0ac2d69b8aaf0d310c62338489c", + "rate": 0, + "timestamp": "2024-08-07T00:56:05.907Z", + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "id": "66ca4a8a7085407ba156733029c17a11", + "duration (min)": 30, + "timestamp": "2024-08-07T00:51:11.228Z" + }, + { + "timestamp": "2024-08-07T00:51:11.228Z", + "rate": 0.2, + "temp": "absolute", + "_type": "TempBasal", + "id": "_66ca4a8a7085407ba156733029c17a11" + }, + { + "id": "d7b2f79444699505b760bf2189723a19", + "isExternal": false, + "isSMB": true, + "timestamp": "2024-08-07T00:46:20.735Z", + "_type": "Bolus", + "amount": 0.05, + "duration": 0 + }, + { + "_type": "TempBasalDuration", + "id": "3a40e6698be0936fa65f2c9ece928e95", + "timestamp": "2024-08-07T00:46:19.226Z", + "duration (min)": 30 + }, + { + "timestamp": "2024-08-07T00:46:19.226Z", + "id": "_3a40e6698be0936fa65f2c9ece928e95", + "_type": "TempBasal", + "temp": "absolute", + "rate": 0.25 + }, + { + "id": "0394d00c5df80f4cfb6e33b6001783c6", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T00:41:06.181Z" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-07T00:41:06.181Z", + "id": "_0394d00c5df80f4cfb6e33b6001783c6", + "rate": 0.75, + "temp": "absolute" + }, + { + "amount": 0.05, + "isSMB": true, + "isExternal": false, + "timestamp": "2024-08-07T00:26:16.163Z", + "duration": 0, + "_type": "Bolus", + "id": "e178cd831caf7994b8aece43c2bf65eb" + }, + { + "timestamp": "2024-08-07T00:26:14.412Z", + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "ad9a0e9e5f5482744ee8f4ebd1e99bd9" + }, + { + "id": "_ad9a0e9e5f5482744ee8f4ebd1e99bd9", + "temp": "absolute", + "timestamp": "2024-08-07T00:26:14.412Z", + "rate": 0.25, + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-07T00:21:05.668Z", + "id": "e71af2f9170c08198df4df7b01de2427", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "timestamp": "2024-08-07T00:21:05.668Z", + "rate": 0.5, + "_type": "TempBasal", + "id": "_e71af2f9170c08198df4df7b01de2427" + }, + { + "duration (min)": 60, + "_type": "TempBasalDuration", + "timestamp": "2024-08-07T00:16:11.271Z", + "id": "eecf8ef914a0dc07e83b19024c32c619" + }, + { + "temp": "absolute", + "id": "_eecf8ef914a0dc07e83b19024c32c619", + "_type": "TempBasal", + "rate": 0, + "timestamp": "2024-08-07T00:16:11.271Z" + }, + { + "timestamp": "2024-08-07T00:01:15.831Z", + "_type": "TempBasalDuration", + "duration (min)": 60, + "id": "d95b954806b80bc49540931622365526" + }, + { + "id": "_d95b954806b80bc49540931622365526", + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-07T00:01:15.831Z", + "rate": 0 + }, + { + "duration (min)": 90, + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T23:51:05.417Z", + "id": "8ab115337a443264300ef1772b9ae4d8" + }, + { + "id": "_8ab115337a443264300ef1772b9ae4d8", + "timestamp": "2024-08-06T23:51:05.417Z", + "temp": "absolute", + "rate": 0, + "_type": "TempBasal" + }, + { + "timestamp": "2024-08-06T23:46:29.404Z", + "isExternal": false, + "id": "067ff7a6f5e2f0192fa4047ac65b6b83", + "duration": 0, + "amount": 0.1, + "isSMB": true, + "_type": "Bolus" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T23:46:26.028Z", + "id": "73e1c7261cb1da3f0f81ba3ea7758143" + }, + { + "timestamp": "2024-08-06T23:46:26.028Z", + "id": "_73e1c7261cb1da3f0f81ba3ea7758143", + "_type": "TempBasal", + "rate": 0, + "temp": "absolute" + }, + { + "amount": 0.05, + "_type": "Bolus", + "id": "e82d0b6a44b2ea87d011ed892eb9688b", + "isSMB": true, + "isExternal": false, + "timestamp": "2024-08-06T23:31:06.877Z", + "duration": 0 + }, + { + "_type": "TempBasalDuration", + "id": "ddc2ac820f43cc0718a0f5ff9fb012ba", + "timestamp": "2024-08-06T23:31:04.950Z", + "duration (min)": 60 + }, + { + "timestamp": "2024-08-06T23:31:04.950Z", + "temp": "absolute", + "id": "_ddc2ac820f43cc0718a0f5ff9fb012ba", + "rate": 0, + "_type": "TempBasal" + }, + { + "duration (min)": 30, + "_type": "TempBasalDuration", + "id": "393401564fa1dd18d947687558cf85d6", + "timestamp": "2024-08-06T23:21:38.658Z" + }, + { + "id": "_393401564fa1dd18d947687558cf85d6", + "_type": "TempBasal", + "timestamp": "2024-08-06T23:21:38.658Z", + "rate": 0.9, + "temp": "absolute" + }, + { + "_type": "Bolus", + "isExternal": false, + "isSMB": true, + "amount": 0.2, + "id": "22cccb38f44d3ee5d403250dd36693f7", + "timestamp": "2024-08-06T23:16:06.868Z", + "duration": 0 + }, + { + "_type": "TempBasalDuration", + "id": "2f3c4aaeb7660e6da9daae19abb6234e", + "timestamp": "2024-08-06T23:16:05.147Z", + "duration (min)": 60 + }, + { + "rate": 0, + "id": "_2f3c4aaeb7660e6da9daae19abb6234e", + "timestamp": "2024-08-06T23:16:05.147Z", + "_type": "TempBasal", + "temp": "absolute" + }, + { + "duration": 0, + "id": "b26c72657069b9046994e04373006d22", + "isExternal": false, + "amount": 0.3, + "timestamp": "2024-08-06T23:11:07.753Z", + "isSMB": true, + "_type": "Bolus" + }, + { + "id": "b289fed89f70db7eca588538c82b215b", + "timestamp": "2024-08-06T23:11:06.123Z", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "rate": 0, + "temp": "absolute", + "id": "_b289fed89f70db7eca588538c82b215b", + "timestamp": "2024-08-06T23:11:06.123Z", + "_type": "TempBasal" + }, + { + "isSMB": true, + "duration": 0, + "isExternal": false, + "_type": "Bolus", + "amount": 0.3, + "id": "7baedb0402c6257d0f5d55e61c387fc4", + "timestamp": "2024-08-06T23:06:06.647Z" + }, + { + "id": "055248d2fc27513d5b9bedaa41cdbc19", + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T23:06:03.954Z" + }, + { + "temp": "absolute", + "rate": 0, + "_type": "TempBasal", + "id": "_055248d2fc27513d5b9bedaa41cdbc19", + "timestamp": "2024-08-06T23:06:03.954Z" + }, + { + "timestamp": "2024-08-06T23:01:06.880Z", + "isSMB": true, + "isExternal": false, + "id": "2e93635783faa2f93f92fb38890f2b3f", + "duration": 0, + "_type": "Bolus", + "amount": 0.5 + }, + { + "duration (min)": 30, + "id": "5616787f02e1c94aa7e281aff04f72a3", + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T23:01:04.324Z" + }, + { + "rate": 0.25, + "temp": "absolute", + "_type": "TempBasal", + "timestamp": "2024-08-06T23:01:04.324Z", + "id": "_5616787f02e1c94aa7e281aff04f72a3" + }, + { + "id": "dabe3184281645f42548acba18ce9791", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-06T22:51:06.124Z" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-06T22:51:06.124Z", + "id": "_dabe3184281645f42548acba18ce9791", + "rate": 0.75, + "temp": "absolute" + }, + { + "timestamp": "2024-08-06T22:46:33.732Z", + "id": "d18c02217a8af8bf44f2b5a86c0ae4ad", + "_type": "Bolus", + "isExternal": false, + "duration": 0, + "isSMB": true, + "amount": 0.1 + }, + { + "timestamp": "2024-08-06T22:46:31.564Z", + "duration (min)": 30, + "id": "62bc928312df82211bfbd2d3db95ba3e", + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "timestamp": "2024-08-06T22:46:31.564Z", + "id": "_62bc928312df82211bfbd2d3db95ba3e", + "_type": "TempBasal", + "rate": 0 + }, + { + "id": "ca1908a4c41469b04c9e6edf999bd263", + "timestamp": "2024-08-06T22:41:04.149Z", + "duration (min)": 30, + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-06T22:41:04.149Z", + "rate": 0.75, + "id": "_ca1908a4c41469b04c9e6edf999bd263", + "temp": "absolute", + "_type": "TempBasal" + }, + { + "_type": "Bolus", + "isExternal": false, + "duration": 0, + "isSMB": true, + "amount": 0.1, + "timestamp": "2024-08-06T22:31:12.294Z", + "id": "56d997062f166f5a48dced8bbd029580" + }, + { + "_type": "TempBasalDuration", + "id": "1d70eefc40db53ca90dbf8db44f345e3", + "duration (min)": 60, + "timestamp": "2024-08-06T22:31:10.646Z" + }, + { + "rate": 0, + "timestamp": "2024-08-06T22:31:10.646Z", + "_type": "TempBasal", + "id": "_1d70eefc40db53ca90dbf8db44f345e3", + "temp": "absolute" + }, + { + "id": "7db20b3788203660397671d882263086", + "_type": "Bolus", + "timestamp": "2024-08-06T22:26:15.730Z", + "isSMB": true, + "amount": 0.3, + "duration": 0, + "isExternal": false + }, + { + "id": "80375585b33d91a48a0b7ab82ceb5fd0", + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T22:26:14.081Z", + "duration (min)": 30 + }, + { + "temp": "absolute", + "id": "_80375585b33d91a48a0b7ab82ceb5fd0", + "rate": 0, + "timestamp": "2024-08-06T22:26:14.081Z", + "_type": "TempBasal" + }, + { + "id": "714229a5152a6e99393f5c5452aed6fe", + "duration (min)": 120, + "timestamp": "2024-08-06T22:16:06.092Z", + "_type": "TempBasalDuration" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-06T22:16:06.092Z", + "rate": 0, + "temp": "absolute", + "id": "_714229a5152a6e99393f5c5452aed6fe" + }, + { + "id": "ea32388ab8ad9e4e5535637f7d436ecc", + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T22:11:15.501Z", + "duration (min)": 90 + }, + { + "timestamp": "2024-08-06T22:11:15.501Z", + "temp": "absolute", + "_type": "TempBasal", + "id": "_ea32388ab8ad9e4e5535637f7d436ecc", + "rate": 0 + }, + { + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T22:06:05.778Z", + "id": "97b66e68d4866722d96ef0f7020b2cab", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-06T22:06:05.778Z", + "id": "_97b66e68d4866722d96ef0f7020b2cab", + "temp": "absolute", + "rate": 0 + }, + { + "id": "79ebd78704508b6b584625b34ca94b89", + "isSMB": true, + "timestamp": "2024-08-06T22:01:13.830Z", + "duration": 0, + "_type": "Bolus", + "amount": 0.25, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T22:01:12.235Z", + "id": "28ec25256862fed73796e833f76533af" + }, + { + "_type": "TempBasal", + "rate": 0, + "timestamp": "2024-08-06T22:01:12.235Z", + "temp": "absolute", + "id": "_28ec25256862fed73796e833f76533af" + }, + { + "timestamp": "2024-08-06T21:56:08.577Z", + "amount": 0.05, + "duration": 0, + "isSMB": true, + "isExternal": false, + "_type": "Bolus", + "id": "f2dd3ed58f7a7994afc45f7e3cfa29ac" + }, + { + "timestamp": "2024-08-06T21:56:05.845Z", + "duration (min)": 60, + "_type": "TempBasalDuration", + "id": "4517297eb6f0569e3480ed94bbd4d980" + }, + { + "timestamp": "2024-08-06T21:56:05.845Z", + "rate": 0, + "id": "_4517297eb6f0569e3480ed94bbd4d980", + "_type": "TempBasal", + "temp": "absolute" + }, + { + "_type": "Bolus", + "isSMB": true, + "timestamp": "2024-08-06T21:51:04.444Z", + "isExternal": false, + "amount": 0.25, + "duration": 0, + "id": "c71f18335873eed28e8fbd3e16e8e4de" + }, + { + "timestamp": "2024-08-06T21:51:02.758Z", + "duration (min)": 60, + "_type": "TempBasalDuration", + "id": "467bd277c005ce52c9d32564904ff444" + }, + { + "rate": 0, + "id": "_467bd277c005ce52c9d32564904ff444", + "_type": "TempBasal", + "timestamp": "2024-08-06T21:51:02.758Z", + "temp": "absolute" + }, + { + "id": "a2e13b6d00aaf85826e4ea797ea9f0d7", + "timestamp": "2024-08-06T21:46:15.236Z", + "_type": "Bolus", + "isSMB": true, + "amount": 0.6, + "duration": 0, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "id": "f81e87c1a41e65d5e150732fe9923cd4", + "timestamp": "2024-08-06T21:46:13.785Z", + "duration (min)": 60 + }, + { + "rate": 0, + "timestamp": "2024-08-06T21:46:13.785Z", + "_type": "TempBasal", + "temp": "absolute", + "id": "_f81e87c1a41e65d5e150732fe9923cd4" + }, + { + "timestamp": "2024-08-06T21:41:08.955Z", + "isSMB": true, + "duration": 0, + "isExternal": false, + "_type": "Bolus", + "amount": 0.7, + "id": "9ea6d07e728a86946df7b21ba6917f8f" + }, + { + "id": "8ae5a77953e7fb92342fdea7db08771b", + "duration (min)": 30, + "timestamp": "2024-08-06T21:41:07.325Z", + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "timestamp": "2024-08-06T21:41:07.325Z", + "rate": 0.55, + "id": "_8ae5a77953e7fb92342fdea7db08771b", + "_type": "TempBasal" + }, + { + "isExternal": false, + "amount": 0.05, + "timestamp": "2024-08-06T21:36:10.869Z", + "isSMB": true, + "_type": "Bolus", + "duration": 0, + "id": "cdcc60817b1b3e9338f4fa3a011bcb44" + }, + { + "duration (min)": 60, + "id": "4688327cd01004c9d3041098ff83ddc5", + "timestamp": "2024-08-06T21:36:09.137Z", + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "_type": "TempBasal", + "rate": 0, + "id": "_4688327cd01004c9d3041098ff83ddc5", + "timestamp": "2024-08-06T21:36:09.137Z" + }, + { + "id": "e25c0b571d37c44dfc739b6bda0984ad", + "timestamp": "2024-08-06T21:31:03.885Z", + "_type": "TempBasalDuration", + "duration (min)": 60 + }, + { + "_type": "TempBasal", + "id": "_e25c0b571d37c44dfc739b6bda0984ad", + "timestamp": "2024-08-06T21:31:03.885Z", + "rate": 0, + "temp": "absolute" + }, + { + "timestamp": "2024-08-06T21:26:04.642Z", + "_type": "TempBasalDuration", + "duration (min)": 60, + "id": "d00fda1f64eaa69f3314db189317673b" + }, + { + "rate": 0, + "id": "_d00fda1f64eaa69f3314db189317673b", + "_type": "TempBasal", + "timestamp": "2024-08-06T21:26:04.642Z", + "temp": "absolute" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T21:21:04.329Z", + "id": "fa591d8726b8f3cead3935bdc9e3b402" + }, + { + "rate": 0, + "timestamp": "2024-08-06T21:21:04.329Z", + "temp": "absolute", + "id": "_fa591d8726b8f3cead3935bdc9e3b402", + "_type": "TempBasal" + }, + { + "id": "d21675e3d95997f21ed8e02235f8d5b5", + "duration (min)": 60, + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T21:16:07.190Z" + }, + { + "_type": "TempBasal", + "timestamp": "2024-08-06T21:16:07.190Z", + "temp": "absolute", + "id": "_d21675e3d95997f21ed8e02235f8d5b5", + "rate": 0 + }, + { + "timestamp": "2024-08-06T21:06:06.093Z", + "id": "9f2aed64958e443dd15769ec8f74e68e", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "_type": "TempBasal", + "id": "_9f2aed64958e443dd15769ec8f74e68e", + "timestamp": "2024-08-06T21:06:06.093Z", + "rate": 0 + }, + { + "id": "562e5c99264412c19dea5d9637126246", + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T21:01:12.797Z" + }, + { + "temp": "absolute", + "rate": 0, + "_type": "TempBasal", + "timestamp": "2024-08-06T21:01:12.797Z", + "id": "_562e5c99264412c19dea5d9637126246" + }, + { + "isSMB": true, + "id": "2defa21d18eecd8c43cb5231bdd815c5", + "_type": "Bolus", + "duration": 0, + "timestamp": "2024-08-06T20:56:12.802Z", + "amount": 0.1, + "isExternal": false + }, + { + "_type": "TempBasalDuration", + "duration (min)": 60, + "id": "05b9d8b26dc9474af3f89a9a86674c2c", + "timestamp": "2024-08-06T20:56:10.445Z" + }, + { + "timestamp": "2024-08-06T20:56:10.445Z", + "temp": "absolute", + "_type": "TempBasal", + "id": "_05b9d8b26dc9474af3f89a9a86674c2c", + "rate": 0 + }, + { + "timestamp": "2024-08-06T20:51:42.745Z", + "_type": "Bolus", + "duration": 0, + "amount": 0.25, + "isSMB": true, + "id": "e0bec0a44d3ebf819bb63b517daa4eb8", + "isExternal": false + }, + { + "id": "72979804f8da36c9768d8c18f419a292", + "_type": "TempBasalDuration", + "duration (min)": 60, + "timestamp": "2024-08-06T20:51:40.953Z" + }, + { + "rate": 0, + "_type": "TempBasal", + "temp": "absolute", + "id": "_72979804f8da36c9768d8c18f419a292", + "timestamp": "2024-08-06T20:51:40.953Z" + }, + { + "id": "bef39ad6d03f1218cf5ff55006c509d6", + "duration": 0, + "_type": "Bolus", + "isExternal": false, + "timestamp": "2024-08-06T20:46:12.215Z", + "amount": 0.35, + "isSMB": true + }, + { + "_type": "TempBasalDuration", + "id": "b3804e3c7f4d47eed158d8077897d7de", + "timestamp": "2024-08-06T20:46:10.056Z", + "duration (min)": 60 + }, + { + "rate": 0, + "timestamp": "2024-08-06T20:46:10.056Z", + "temp": "absolute", + "_type": "TempBasal", + "id": "_b3804e3c7f4d47eed158d8077897d7de" + }, + { + "timestamp": "2024-08-06T20:41:09.820Z", + "duration (min)": 60, + "id": "24f8578cc1e4e7ab5373c1eca080149d", + "_type": "TempBasalDuration" + }, + { + "temp": "absolute", + "_type": "TempBasal", + "id": "_24f8578cc1e4e7ab5373c1eca080149d", + "rate": 0, + "timestamp": "2024-08-06T20:41:09.820Z" + }, + { + "duration": 1, + "isExternal": false, + "id": "ffbdcf6f2580b435417c8097319eea19", + "timestamp": "2024-08-06T20:37:41.955Z", + "_type": "Bolus", + "amount": 1.5, + "isSMB": false + }, + { + "id": "173a8be6edbb04b344e1f1b03e087514", + "timestamp": "2024-08-06T20:36:33.971Z", + "duration (min)": 60, + "_type": "TempBasalDuration" + }, + { + "timestamp": "2024-08-06T20:36:33.971Z", + "temp": "absolute", + "rate": 0, + "_type": "TempBasal", + "id": "_173a8be6edbb04b344e1f1b03e087514" + }, + { + "duration": 1, + "isSMB": false, + "isExternal": false, + "amount": 1.5, + "id": "a9357867bde098367ef1416941f540ab", + "timestamp": "2024-08-06T20:31:55.760Z", + "_type": "Bolus" + }, + { + "amount": 0.25, + "isSMB": true, + "id": "c86a786ac9aa7afd8dc170b1a23c3a5b", + "timestamp": "2024-08-06T20:31:10.057Z", + "_type": "Bolus", + "duration": 0, + "isExternal": false + }, + { + "id": "b760c522ef6dd79b7099d73f0f769f44", + "duration (min)": 30, + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T20:31:07.600Z" + }, + { + "temp": "absolute", + "rate": 1.75, + "timestamp": "2024-08-06T20:31:07.600Z", + "_type": "TempBasal", + "id": "_b760c522ef6dd79b7099d73f0f769f44" + }, + { + "id": "a230fe4e1a2bb6f79344795fee697de4", + "amount": 0.4, + "duration": 0, + "isSMB": true, + "timestamp": "2024-08-06T20:21:25.275Z", + "isExternal": false, + "_type": "Bolus" + }, + { + "id": "3a70f1adbb40be3b7d0a83f3c8f05d09", + "_type": "TempBasalDuration", + "timestamp": "2024-08-06T20:21:21.896Z", + "duration (min)": 30 + }, + { + "temp": "absolute", + "id": "_3a70f1adbb40be3b7d0a83f3c8f05d09", + "_type": "TempBasal", + "timestamp": "2024-08-06T20:21:21.896Z", + "rate": 0.1 + }, + { + "duration": 0, + "id": "301da1b4f90bb137ceb75703bbcf7f7d", + "amount": 0.3, + "isExternal": false, + "_type": "Bolus", + "isSMB": true, + "timestamp": "2024-08-06T20:06:12.032Z" + }, + { + "_type": "TempBasalDuration", + "duration (min)": 30, + "id": "3bcdb0a99738fcc13a461abd405d1028", + "timestamp": "2024-08-06T20:06:10.229Z" + }, + { + "id": "_3bcdb0a99738fcc13a461abd405d1028", + "_type": "TempBasal", + "timestamp": "2024-08-06T20:06:10.229Z", + "rate": 0.15, + "temp": "absolute" + }, + { + "id": "4625af0e02fdd271396da8653d7fb74f", + "_type": "TempBasalDuration", + "duration (min)": 30, + "timestamp": "2024-08-06T20:01:02.713Z" + }, + { + "temp": "absolute", + "rate": 0, + "id": "_4625af0e02fdd271396da8653d7fb74f", + "_type": "TempBasal", + "timestamp": "2024-08-06T20:01:02.713Z" + } + ], + "profile": { + "max_iob": 14, + "max_daily_safety_multiplier": 3, + "current_basal_safety_multiplier": 4, + "autosens_max": 1.3, + "autosens_min": 0.7, + "rewind_resets_autosens": true, + "high_temptarget_raises_sensitivity": true, + "low_temptarget_lowers_sensitivity": true, + "sensitivity_raises_target": true, + "resistance_lowers_target": false, + "exercise_mode": false, + "half_basal_exercise_target": 160, + "maxCOB": 120, + "skip_neutral_temps": false, + "unsuspend_if_no_temp": false, + "min_5m_carbimpact": 8, + "autotune_isf_adjustmentFraction": 1, + "remainingCarbsFraction": 1, + "remainingCarbsCap": 90, + "enableUAM": true, + "A52_risk_enable": false, + "enableSMB_with_COB": true, + "enableSMB_with_temptarget": true, + "enableSMB_always": false, + "enableSMB_after_carbs": true, + "allowSMB_with_high_temptarget": false, + "maxSMBBasalMinutes": 90, + "maxUAMSMBBasalMinutes": 120, + "SMBInterval": 3, + "bolus_increment": 0.05, + "maxDelta_bg_threshold": 0.2, + "curve": "ultra-rapid", + "useCustomPeakTime": false, + "insulinPeakTime": 55, + "carbsReqThreshold": 1, + "offline_hotspot": false, + "noisyCGMTargetMultiplier": 1.3, + "suspend_zeros_iob": false, + "enableEnliteBgproxy": false, + "calc_glucose_noise": false, + "target_bg": false, + "smb_delivery_ratio": 0.7, + "adjustmentFactor": 0.7, + "useNewFormula": true, + "enableDynamicCR": true, + "sigmoid": true, + "weightPercentage": 0.65, + "tddAdjBasal": true, + "enableSMB_high_bg": true, + "enableSMB_high_bg_target": 110, + "threshold_setting": 65, + "dia": 9, + "model": "722", + "current_basal": 0.75, + "basalprofile": [ + { + "minutes": 0, + "start": "00:00:00", + "rate": 0.7 + }, + { + "start": "03:00:00", + "minutes": 180, + "rate": 0.7 + }, + { + "start": "09:00:00", + "rate": 0.75, + "minutes": 540 + } + ], + "max_daily_basal": 0.75, + "max_basal": 3, + "out_units": "mg/dL", + "min_bg": 98, + "max_bg": 98, + "bg_targets": { + "units": "mg/dL", + "user_preferred_units": "mg/dL", + "targets": [ + { + "start": "00:00:00", + "high": 98, + "offset": 0, + "low": 98, + "max_bg": 98, + "min_bg": 98 + } + ] + }, + "sens": 65, + "isfProfile": { + "user_preferred_units": "mg/dL", + "units": "mg/dL", + "sensitivities": [ + { + "start": "00:00:00", + "sensitivity": 65, + "offset": 0, + "endOffset": 1440 + } + ] + }, + "carb_ratio": 6.5, + "carb_ratios": { + "schedule": [ + { + "offset": 0, + "start": "00:00:00", + "ratio": 6 + }, + { + "start": "08:30:00", + "offset": 510, + "ratio": 5 + }, + { + "offset": 690, + "start": "11:30:00", + "ratio": 5.5 + }, + { + "start": "16:00:00", + "ratio": 6.5, + "offset": 960 + } + ], + "units": "grams" + } + }, + "clock": "2024-08-07T19:58:42.758Z", + "autosens": { + "ratio": 1, + "newisf": 65, + "timestamp": "2024-08-07T19:36:40.200Z" + }, + "iob": [ + { + "iob": 8.388, + "activity": 0.0725, + "basaliob": -0.954, + "bolusiob": 9.342, + "netbasalinsulin": -3.75, + "bolusinsulin": 20.55, + "time": "2024-08-07T19:58:42.758Z", + "iobWithZeroTemp": { + "iob": 8.388, + "activity": 0.0725, + "basaliob": -0.954, + "bolusiob": 9.342, + "netbasalinsulin": -3.75, + "bolusinsulin": 20.55, + "time": "2024-08-07T19:58:42.758Z" + }, + "lastBolusTime": 1723059629280, + "lastTemp": { + "rate": 0, + "timestamp": "2024-08-07T19:56:00.953Z", + "started_at": "2024-08-07T19:56:00.953Z", + "date": 1723060560953, + "duration": 3.7 + } + }, + { + "iob": 8.024, + "activity": 0.0732, + "basaliob": -0.912, + "bolusiob": 8.936, + "netbasalinsulin": -3.7, + "bolusinsulin": 20.4, + "time": "2024-08-07T20:03:42.758Z", + "iobWithZeroTemp": { + "iob": 7.974, + "activity": 0.0732, + "basaliob": -0.962, + "bolusiob": 8.936, + "netbasalinsulin": -3.75, + "bolusinsulin": 20.4, + "time": "2024-08-07T20:03:42.758Z" + } + }, + { + "iob": 7.657, + "activity": 0.0733, + "basaliob": -0.87, + "bolusiob": 8.527, + "netbasalinsulin": -3.65, + "bolusinsulin": 19.9, + "time": "2024-08-07T20:08:42.758Z", + "iobWithZeroTemp": { + "iob": 7.508, + "activity": 0.0731, + "basaliob": -1.019, + "bolusiob": 8.527, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.9, + "time": "2024-08-07T20:08:42.758Z" + } + }, + { + "iob": 7.291, + "activity": 0.0729, + "basaliob": -0.828, + "bolusiob": 8.12, + "netbasalinsulin": -3.6, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:13:42.758Z", + "iobWithZeroTemp": { + "iob": 7.093, + "activity": 0.0724, + "basaliob": -1.026, + "bolusiob": 8.12, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:13:42.758Z" + } + }, + { + "iob": 6.929, + "activity": 0.0719, + "basaliob": -0.787, + "bolusiob": 7.716, + "netbasalinsulin": -3.55, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:18:42.758Z", + "iobWithZeroTemp": { + "iob": 6.684, + "activity": 0.0712, + "basaliob": -1.032, + "bolusiob": 7.716, + "netbasalinsulin": -3.8, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:18:42.758Z" + } + }, + { + "iob": 6.573, + "activity": 0.0705, + "basaliob": -0.746, + "bolusiob": 7.319, + "netbasalinsulin": -3.55, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:23:42.758Z", + "iobWithZeroTemp": { + "iob": 6.282, + "activity": 0.0695, + "basaliob": -1.037, + "bolusiob": 7.319, + "netbasalinsulin": -3.85, + "bolusinsulin": 19.75, + "time": "2024-08-07T20:23:42.758Z" + } + }, + { + "iob": 6.224, + "activity": 0.0689, + "basaliob": -0.706, + "bolusiob": 6.931, + "netbasalinsulin": -3.5, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:28:42.758Z", + "iobWithZeroTemp": { + "iob": 5.889, + "activity": 0.0675, + "basaliob": -1.041, + "bolusiob": 6.931, + "netbasalinsulin": -3.85, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:28:42.758Z" + } + }, + { + "iob": 5.885, + "activity": 0.0669, + "basaliob": -0.668, + "bolusiob": 6.552, + "netbasalinsulin": -3.45, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:33:42.758Z", + "iobWithZeroTemp": { + "iob": 5.507, + "activity": 0.0652, + "basaliob": -1.045, + "bolusiob": 6.552, + "netbasalinsulin": -3.85, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:33:42.758Z" + } + }, + { + "iob": 5.555, + "activity": 0.0648, + "basaliob": -0.63, + "bolusiob": 6.185, + "netbasalinsulin": -3.4, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:38:42.758Z", + "iobWithZeroTemp": { + "iob": 5.087, + "activity": 0.0627, + "basaliob": -1.098, + "bolusiob": 6.185, + "netbasalinsulin": -3.9, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:38:42.758Z" + } + }, + { + "iob": 5.237, + "activity": 0.0625, + "basaliob": -0.594, + "bolusiob": 5.831, + "netbasalinsulin": -3.35, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:43:42.758Z", + "iobWithZeroTemp": { + "iob": 4.73, + "activity": 0.0601, + "basaliob": -1.101, + "bolusiob": 5.831, + "netbasalinsulin": -3.9, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:43:42.758Z" + } + }, + { + "iob": 4.93, + "activity": 0.0601, + "basaliob": -0.559, + "bolusiob": 5.489, + "netbasalinsulin": -3.3, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:48:42.758Z", + "iobWithZeroTemp": { + "iob": 4.387, + "activity": 0.0573, + "basaliob": -1.102, + "bolusiob": 5.489, + "netbasalinsulin": -3.9, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:48:42.758Z" + } + }, + { + "iob": 4.636, + "activity": 0.0577, + "basaliob": -0.525, + "bolusiob": 5.161, + "netbasalinsulin": -3.3, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:53:42.758Z", + "iobWithZeroTemp": { + "iob": 4.057, + "activity": 0.0544, + "basaliob": -1.104, + "bolusiob": 5.161, + "netbasalinsulin": -3.95, + "bolusinsulin": 17.75, + "time": "2024-08-07T20:53:42.758Z" + } + }, + { + "iob": 4.353, + "activity": 0.0552, + "basaliob": -0.493, + "bolusiob": 4.846, + "netbasalinsulin": -3.25, + "bolusinsulin": 17.65, + "time": "2024-08-07T20:58:42.758Z", + "iobWithZeroTemp": { + "iob": 3.742, + "activity": 0.0516, + "basaliob": -1.104, + "bolusiob": 4.846, + "netbasalinsulin": -3.95, + "bolusinsulin": 17.65, + "time": "2024-08-07T20:58:42.758Z" + } + }, + { + "iob": 4.084, + "activity": 0.0527, + "basaliob": -0.462, + "bolusiob": 4.546, + "netbasalinsulin": -3.2, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:03:42.758Z", + "iobWithZeroTemp": { + "iob": 3.442, + "activity": 0.0487, + "basaliob": -1.104, + "bolusiob": 4.546, + "netbasalinsulin": -3.95, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:03:42.758Z" + } + }, + { + "iob": 3.826, + "activity": 0.0502, + "basaliob": -0.433, + "bolusiob": 4.26, + "netbasalinsulin": -3.15, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:08:42.758Z", + "iobWithZeroTemp": { + "iob": 3.106, + "activity": 0.0458, + "basaliob": -1.154, + "bolusiob": 4.26, + "netbasalinsulin": -4, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:08:42.758Z" + } + }, + { + "iob": 3.582, + "activity": 0.0477, + "basaliob": -0.405, + "bolusiob": 3.987, + "netbasalinsulin": -3.1, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:13:42.758Z", + "iobWithZeroTemp": { + "iob": 2.834, + "activity": 0.0429, + "basaliob": -1.153, + "bolusiob": 3.987, + "netbasalinsulin": -4, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:13:42.758Z" + } + }, + { + "iob": 3.349, + "activity": 0.0453, + "basaliob": -0.379, + "bolusiob": 3.728, + "netbasalinsulin": -3.05, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:18:42.758Z", + "iobWithZeroTemp": { + "iob": 2.577, + "activity": 0.0401, + "basaliob": -1.151, + "bolusiob": 3.728, + "netbasalinsulin": -4, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:18:42.758Z" + } + }, + { + "iob": 3.129, + "activity": 0.0429, + "basaliob": -0.354, + "bolusiob": 3.483, + "netbasalinsulin": -3, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:23:42.758Z", + "iobWithZeroTemp": { + "iob": 2.333, + "activity": 0.0373, + "basaliob": -1.149, + "bolusiob": 3.483, + "netbasalinsulin": -4, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:23:42.758Z" + } + }, + { + "iob": 2.92, + "activity": 0.0406, + "basaliob": -0.33, + "bolusiob": 3.25, + "netbasalinsulin": -2.95, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:28:42.758Z", + "iobWithZeroTemp": { + "iob": 2.103, + "activity": 0.0347, + "basaliob": -1.147, + "bolusiob": 3.25, + "netbasalinsulin": -4, + "bolusinsulin": 17.65, + "time": "2024-08-07T21:28:42.758Z" + } + }, + { + "iob": 2.723, + "activity": 0.0383, + "basaliob": -0.308, + "bolusiob": 3.031, + "netbasalinsulin": -2.95, + "bolusinsulin": 17.5, + "time": "2024-08-07T21:33:42.758Z", + "iobWithZeroTemp": { + "iob": 1.886, + "activity": 0.0321, + "basaliob": -1.144, + "bolusiob": 3.031, + "netbasalinsulin": -4.05, + "bolusinsulin": 17.5, + "time": "2024-08-07T21:33:42.758Z" + } + }, + { + "iob": 2.537, + "activity": 0.0361, + "basaliob": -0.287, + "bolusiob": 2.823, + "netbasalinsulin": -2.9, + "bolusinsulin": 16.65, + "time": "2024-08-07T21:38:42.758Z", + "iobWithZeroTemp": { + "iob": 1.632, + "activity": 0.0296, + "basaliob": -1.191, + "bolusiob": 2.823, + "netbasalinsulin": -4.1, + "bolusinsulin": 16.65, + "time": "2024-08-07T21:38:42.758Z" + } + }, + { + "iob": 2.361, + "activity": 0.034, + "basaliob": -0.267, + "bolusiob": 2.628, + "netbasalinsulin": -2.85, + "bolusinsulin": 16.05, + "time": "2024-08-07T21:43:42.758Z", + "iobWithZeroTemp": { + "iob": 1.44, + "activity": 0.0272, + "basaliob": -1.188, + "bolusiob": 2.628, + "netbasalinsulin": -4.1, + "bolusinsulin": 16.05, + "time": "2024-08-07T21:43:42.758Z" + } + }, + { + "iob": 2.196, + "activity": 0.032, + "basaliob": -0.248, + "bolusiob": 2.444, + "netbasalinsulin": -2.8, + "bolusinsulin": 15.65, + "time": "2024-08-07T21:48:42.758Z", + "iobWithZeroTemp": { + "iob": 1.261, + "activity": 0.0248, + "basaliob": -1.184, + "bolusiob": 2.444, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.65, + "time": "2024-08-07T21:48:42.758Z" + } + }, + { + "iob": 2.041, + "activity": 0.0301, + "basaliob": -0.23, + "bolusiob": 2.272, + "netbasalinsulin": -2.75, + "bolusinsulin": 15.65, + "time": "2024-08-07T21:53:42.758Z", + "iobWithZeroTemp": { + "iob": 1.092, + "activity": 0.0226, + "basaliob": -1.18, + "bolusiob": 2.272, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.65, + "time": "2024-08-07T21:53:42.758Z" + } + }, + { + "iob": 1.896, + "activity": 0.0282, + "basaliob": -0.214, + "bolusiob": 2.109, + "netbasalinsulin": -2.7, + "bolusinsulin": 15.05, + "time": "2024-08-07T21:58:42.758Z", + "iobWithZeroTemp": { + "iob": 0.934, + "activity": 0.0205, + "basaliob": -1.175, + "bolusiob": 2.109, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.05, + "time": "2024-08-07T21:58:42.758Z" + } + }, + { + "iob": 1.759, + "activity": 0.0265, + "basaliob": -0.198, + "bolusiob": 1.957, + "netbasalinsulin": -2.65, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:03:42.758Z", + "iobWithZeroTemp": { + "iob": 0.787, + "activity": 0.0185, + "basaliob": -1.171, + "bolusiob": 1.957, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:03:42.758Z" + } + }, + { + "iob": 1.631, + "activity": 0.0248, + "basaliob": -0.184, + "bolusiob": 1.815, + "netbasalinsulin": -2.55, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:08:42.758Z", + "iobWithZeroTemp": { + "iob": 0.649, + "activity": 0.0166, + "basaliob": -1.166, + "bolusiob": 1.815, + "netbasalinsulin": -4.05, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:08:42.758Z" + } + }, + { + "iob": 1.511, + "activity": 0.0232, + "basaliob": -0.17, + "bolusiob": 1.681, + "netbasalinsulin": -2.5, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:13:42.758Z", + "iobWithZeroTemp": { + "iob": 0.471, + "activity": 0.0148, + "basaliob": -1.211, + "bolusiob": 1.681, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:13:42.758Z" + } + }, + { + "iob": 1.399, + "activity": 0.0217, + "basaliob": -0.158, + "bolusiob": 1.557, + "netbasalinsulin": -2.45, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:18:42.758Z", + "iobWithZeroTemp": { + "iob": 0.351, + "activity": 0.013, + "basaliob": -1.205, + "bolusiob": 1.557, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:18:42.758Z" + } + }, + { + "iob": 1.294, + "activity": 0.0202, + "basaliob": -0.146, + "bolusiob": 1.44, + "netbasalinsulin": -2.4, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:23:42.758Z", + "iobWithZeroTemp": { + "iob": 0.24, + "activity": 0.0114, + "basaliob": -1.2, + "bolusiob": 1.44, + "netbasalinsulin": -4.1, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:23:42.758Z" + } + }, + { + "iob": 1.197, + "activity": 0.0189, + "basaliob": -0.135, + "bolusiob": 1.331, + "netbasalinsulin": -2.4, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:28:42.758Z", + "iobWithZeroTemp": { + "iob": 0.137, + "activity": 0.0098, + "basaliob": -1.194, + "bolusiob": 1.331, + "netbasalinsulin": -4.15, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:28:42.758Z" + } + }, + { + "iob": 1.105, + "activity": 0.0176, + "basaliob": -0.124, + "bolusiob": 1.23, + "netbasalinsulin": -2.4, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:33:42.758Z", + "iobWithZeroTemp": { + "iob": 0.042, + "activity": 0.0084, + "basaliob": -1.188, + "bolusiob": 1.23, + "netbasalinsulin": -4.2, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:33:42.758Z" + } + }, + { + "iob": 1.02, + "activity": 0.0164, + "basaliob": -0.115, + "bolusiob": 1.135, + "netbasalinsulin": -2.35, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:38:42.758Z", + "iobWithZeroTemp": { + "iob": -0.097, + "activity": 0.007, + "basaliob": -1.232, + "bolusiob": 1.135, + "netbasalinsulin": -4.25, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:38:42.758Z" + } + }, + { + "iob": 0.941, + "activity": 0.0152, + "basaliob": -0.106, + "bolusiob": 1.047, + "netbasalinsulin": -2.25, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:43:42.758Z", + "iobWithZeroTemp": { + "iob": -0.178, + "activity": 0.0057, + "basaliob": -1.226, + "bolusiob": 1.047, + "netbasalinsulin": -4.2, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:43:42.758Z" + } + }, + { + "iob": 0.868, + "activity": 0.0142, + "basaliob": -0.098, + "bolusiob": 0.965, + "netbasalinsulin": -2.2, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:48:42.758Z", + "iobWithZeroTemp": { + "iob": -0.254, + "activity": 0.0045, + "basaliob": -1.219, + "bolusiob": 0.965, + "netbasalinsulin": -4.2, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:48:42.758Z" + } + }, + { + "iob": 0.8, + "activity": 0.0132, + "basaliob": -0.09, + "bolusiob": 0.889, + "netbasalinsulin": -2.2, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:53:42.758Z", + "iobWithZeroTemp": { + "iob": -0.323, + "activity": 0.0033, + "basaliob": -1.213, + "bolusiob": 0.889, + "netbasalinsulin": -4.25, + "bolusinsulin": 15.05, + "time": "2024-08-07T22:53:42.758Z" + } + }, + { + "iob": 0.736, + "activity": 0.0122, + "basaliob": -0.083, + "bolusiob": 0.819, + "netbasalinsulin": -2.25, + "bolusinsulin": 14.85, + "time": "2024-08-07T22:58:42.758Z", + "iobWithZeroTemp": { + "iob": -0.387, + "activity": 0.0022, + "basaliob": -1.206, + "bolusiob": 0.819, + "netbasalinsulin": -4.35, + "bolusinsulin": 14.85, + "time": "2024-08-07T22:58:42.758Z" + } + }, + { + "iob": 0.677, + "activity": 0.0113, + "basaliob": -0.076, + "bolusiob": 0.753, + "netbasalinsulin": -2.35, + "bolusinsulin": 14.55, + "time": "2024-08-07T23:03:42.758Z", + "iobWithZeroTemp": { + "iob": -0.446, + "activity": 0.0012, + "basaliob": -1.199, + "bolusiob": 0.753, + "netbasalinsulin": -4.5, + "bolusinsulin": 14.55, + "time": "2024-08-07T23:03:42.758Z" + } + }, + { + "iob": 0.622, + "activity": 0.0105, + "basaliob": -0.07, + "bolusiob": 0.692, + "netbasalinsulin": -2.35, + "bolusinsulin": 14.4, + "time": "2024-08-07T23:08:42.758Z", + "iobWithZeroTemp": { + "iob": -0.549, + "activity": 0.0003, + "basaliob": -1.242, + "bolusiob": 0.692, + "netbasalinsulin": -4.6, + "bolusinsulin": 14.4, + "time": "2024-08-07T23:08:42.758Z" + } + }, + { + "iob": 0.572, + "activity": 0.0097, + "basaliob": -0.064, + "bolusiob": 0.636, + "netbasalinsulin": -2.3, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:13:42.758Z", + "iobWithZeroTemp": { + "iob": -0.599, + "activity": -0.0006, + "basaliob": -1.235, + "bolusiob": 0.636, + "netbasalinsulin": -4.6, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:13:42.758Z" + } + }, + { + "iob": 0.525, + "activity": 0.009, + "basaliob": -0.059, + "bolusiob": 0.584, + "netbasalinsulin": -2.3, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:18:42.758Z", + "iobWithZeroTemp": { + "iob": -0.644, + "activity": -0.0014, + "basaliob": -1.227, + "bolusiob": 0.584, + "netbasalinsulin": -4.65, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:18:42.758Z" + } + }, + { + "iob": 0.481, + "activity": 0.0083, + "basaliob": -0.054, + "bolusiob": 0.535, + "netbasalinsulin": -2.25, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:23:42.758Z", + "iobWithZeroTemp": { + "iob": -0.685, + "activity": -0.0022, + "basaliob": -1.22, + "bolusiob": 0.535, + "netbasalinsulin": -4.65, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:23:42.758Z" + } + }, + { + "iob": 0.441, + "activity": 0.0077, + "basaliob": -0.049, + "bolusiob": 0.491, + "netbasalinsulin": -2.2, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:28:42.758Z", + "iobWithZeroTemp": { + "iob": -0.722, + "activity": -0.0029, + "basaliob": -1.212, + "bolusiob": 0.491, + "netbasalinsulin": -4.65, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:28:42.758Z" + } + }, + { + "iob": 0.404, + "activity": 0.0071, + "basaliob": -0.045, + "bolusiob": 0.449, + "netbasalinsulin": -2.2, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:33:42.758Z", + "iobWithZeroTemp": { + "iob": -0.755, + "activity": -0.0036, + "basaliob": -1.205, + "bolusiob": 0.449, + "netbasalinsulin": -4.7, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:33:42.758Z" + } + }, + { + "iob": 0.37, + "activity": 0.0066, + "basaliob": -0.041, + "bolusiob": 0.411, + "netbasalinsulin": -2.15, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:38:42.758Z", + "iobWithZeroTemp": { + "iob": -0.836, + "activity": -0.0042, + "basaliob": -1.247, + "bolusiob": 0.411, + "netbasalinsulin": -4.75, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:38:42.758Z" + } + }, + { + "iob": 0.338, + "activity": 0.0061, + "basaliob": -0.038, + "bolusiob": 0.376, + "netbasalinsulin": -2.1, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:43:42.758Z", + "iobWithZeroTemp": { + "iob": -0.863, + "activity": -0.0048, + "basaliob": -1.24, + "bolusiob": 0.376, + "netbasalinsulin": -4.75, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:43:42.758Z" + } + }, + { + "iob": 0.309, + "activity": 0.0056, + "basaliob": -0.035, + "bolusiob": 0.344, + "netbasalinsulin": -2.05, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:48:42.758Z", + "iobWithZeroTemp": { + "iob": -0.888, + "activity": -0.0053, + "basaliob": -1.232, + "bolusiob": 0.344, + "netbasalinsulin": -4.75, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:48:42.758Z" + } + }, + { + "iob": 0.282, + "activity": 0.0052, + "basaliob": -0.032, + "bolusiob": 0.314, + "netbasalinsulin": -2.05, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:53:42.758Z", + "iobWithZeroTemp": { + "iob": -0.91, + "activity": -0.0058, + "basaliob": -1.224, + "bolusiob": 0.314, + "netbasalinsulin": -4.8, + "bolusinsulin": 14.2, + "time": "2024-08-07T23:53:42.758Z" + } + } + ] +} diff --git a/tests/iob.data.test.ts b/tests/iob.data.test.ts new file mode 100644 index 000000000..6bff5a66e --- /dev/null +++ b/tests/iob.data.test.ts @@ -0,0 +1,17 @@ +import data from './iob.data.json' +import generate from '../lib/iob' +import * as fs from 'fs' + +describe('iob', () => { + it('should', () => { + const result = generate({ + history: data.pumpHistory, + history24: null, + profile: data.profile, + clock: data.clock, + autosens: data.autosens ? data.autosens : undefined, + }) + + expect(JSON.parse(JSON.stringify(result))).toStrictEqual(JSON.parse(JSON.stringify(data.iob))) + }) +}) diff --git a/tests/iob.test.js b/tests/iob.test.ts similarity index 99% rename from tests/iob.test.js rename to tests/iob.test.ts index 260924536..cce380be6 100644 --- a/tests/iob.test.js +++ b/tests/iob.test.ts @@ -477,7 +477,10 @@ describe('IOB', function() { }; var iobInputs = inputs; - var iobNow = iob(iobInputs)[0]; + var iobRes = iob(iobInputs) + var iobNow = iobRes[0]; + + console.log('iobRes', iobRes) //console.log(iobNow); iobNow.iob.should.be.lessThan(1); @@ -881,8 +884,8 @@ describe('IOB', function() { 'rate': 1, 'minutes': 0 }]; - var now = Date.now(), - timestamp = new Date(now).toISOString(), + var now = new Date('2024-08-08 15:00:00').getTime() + var timestamp = new Date(now).toISOString(), timestamp15mAgo = new Date(now - (15 * 60 * 1000)).toISOString(), timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString(), timestamp45mAgo = new Date(now - (45 * 60 * 1000)).toISOString(), @@ -982,7 +985,7 @@ describe('IOB', function() { 'rate': 1, 'minutes': 0 }]; - var now = Date.now(), + var now = new Date('2024-08-08 15:00:00').getTime(), timestamp = new Date(now).toISOString(), timestamp15mAgo = new Date(now - (15 * 60 * 1000)).toISOString(), timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString(), @@ -1104,7 +1107,7 @@ describe('IOB', function() { 'rate': 1, 'minutes': 0 }]; - var now = Date.now(), + var now = new Date('2024-08-08T15:00:00.000Z').getTime(), timestamp = new Date(now).toISOString(), timestamp15mAgo = new Date(now - (15 * 60 * 1000)).toISOString(), timestamp30mAgo = new Date(now - (30 * 60 * 1000)).toISOString(), @@ -1182,6 +1185,7 @@ describe('IOB', function() { var iobInputs = inputs; var iobNowWithSuspend = iob(iobInputs)[0]; + console.log('iobWithSuspend', iobNowWithSuspend) iobNowWithSuspend.iob.should.equal(iobNowWithoutSuspend.iob); }); diff --git a/tests/json-wf.sh b/tests/json-wf.sh index f87fe7fd7..549b3cd17 100755 --- a/tests/json-wf.sh +++ b/tests/json-wf.sh @@ -21,7 +21,7 @@ ret=$? rc=$((rc|ret)) #echo $rc if ! [ ${ret} -eq 0 ]; then - for jsonfile in $(find ${myloc}/.. -name '*.json'); do + for jsonfile in $jsonfiles; do realfile=$(readlink -f ${jsonfile}) output=$(jq . ${jsonfile} 2>&1) ret=$? diff --git a/tests/profile.test.js b/tests/profile.test.ts similarity index 68% rename from tests/profile.test.js rename to tests/profile.test.ts index c0c572d64..3272a8e22 100644 --- a/tests/profile.test.js +++ b/tests/profile.test.ts @@ -1,5 +1,7 @@ 'use strict'; +import { initFinalResults } from "../lib/bin/utils"; + require('should'); var _ = require('lodash'); @@ -33,7 +35,7 @@ describe('Profile', function ( ) { }; it('should should create a profile from inputs', function () { - var profile = require('../lib/profile')(baseInputs); + var profile = require('../lib/profile')(initFinalResults(), baseInputs); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -48,7 +50,7 @@ describe('Profile', function ( ) { var creationDate = new Date(currentTime.getTime() - (5 * 60 * 1000)); it('should create a profile with temptarget set', function() { - var profile = require('../lib/profile')(_.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': creationDate}]})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': creationDate}]})); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -62,7 +64,7 @@ describe('Profile', function ( ) { var pastDate = new Date(currentTime.getTime() - 90*60*1000); it('should create a profile ignoring an out of date temptarget', function() { - var profile = require('../lib/profile')(_.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': pastDate}]})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': pastDate}]})); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -73,7 +75,7 @@ describe('Profile', function ( ) { }); it('should create a profile ignoring a temptarget with 0 duration', function() { - var profile = require('../lib/profile')(_.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':0, 'created_at': creationDate}]})); + var profile = require('../lib/profile')({}, _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':0, 'created_at': creationDate}]})); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -85,18 +87,18 @@ describe('Profile', function ( ) { it('should error with invalid DIA', function () { - var profile = require('../lib/profile')(_.merge({}, baseInputs, {settings: {insulin_action_curve: 1}})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {settings: {insulin_action_curve: 1}})); profile.should.equal(-1); }); it('should error with a current basal of 0', function () { - var profile = require('../lib/profile')(_.merge({}, baseInputs, {basals: [{minutes: 0, rate: 0}]})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {basals: [{minutes: 0, rate: 0}]})); profile.should.equal(-1); }); it('should set the profile model from input', function () { - var profile = require('../lib/profile')(_.merge({}, baseInputs, {model: 554})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {model: 554})); profile.model.should.equal(554); }); diff --git a/tests/set-temp-basal.test.js b/tests/set-temp-basal.test.ts similarity index 100% rename from tests/set-temp-basal.test.js rename to tests/set-temp-basal.test.ts diff --git a/tests/tests-in-shell.test.js b/tests/tests-in-shell.test.ts similarity index 83% rename from tests/tests-in-shell.test.js rename to tests/tests-in-shell.test.ts index 17f88f988..4cdfba916 100644 --- a/tests/tests-in-shell.test.js +++ b/tests/tests-in-shell.test.ts @@ -1,20 +1,13 @@ // Runner for unit tests which are written in bash. For each file in the // oref0/tests directory whose name ends in .sh, generates a separate test // which runs it and asserts that it exits with status 0 (success). -"use strict" var should = require('should'); -var fs = require("fs"); -var path = require("path"); -var child_process = require("child_process"); - -before(function() { - this.timeout(120000); -}); +import * as fs from 'fs' +import * as path from 'path' +import * as child_process from 'child_process' describe("shell-script tests", function() { - this.timeout(120000); - var bashUnitTestFiles = []; fs.readdirSync("tests").forEach(function(filename) { if(filename.endsWith(".sh")) @@ -25,7 +18,7 @@ describe("shell-script tests", function() { it(testFile, function() { var utilProcess = child_process.spawnSync(testFile, [], { timeout: 120000, //milliseconds - encoding: "UTF-8", + encoding: "utf-8", }); //console.error("================="); @@ -35,6 +28,6 @@ describe("shell-script tests", function() { //console.error(testFile + "stderr: \n", utilProcess.stderr); //console.error(utilProcess.error); should.equal(utilProcess.status, 0, "Bash unit test returned failure: run " + testFile + " manually for details."); - }); + }, 120000); }); }); diff --git a/tests/with-raw-glucose.test.js b/tests/with-raw-glucose.test.ts similarity index 100% rename from tests/with-raw-glucose.test.js rename to tests/with-raw-glucose.test.ts diff --git a/tsconfig.json b/tsconfig.json index e7f51d49e..ad459f080 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,10 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "display": "Default", "compilerOptions": { - "outDir": "./dist", "module": "CommonJS", - "target": "ES2022", + "outDir": "dist", + "target": "ES2015", "allowJs": true, + "checkJs": false, "composite": false, "declaration": false, "declarationMap": false, @@ -20,22 +19,19 @@ "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, + "noUncheckedIndexedAccess": false, "noPropertyAccessFromIndexSignature": true, "preserveWatchOutput": true, "skipLibCheck": true, "strict": true, - "stripInternal": true + "stripInternal": true, + "importHelpers": true, + "resolveJsonModule": true }, - "include": [ - "lib", - "tests" - ], "exclude": [ "node_modules" ], - "ts-node": { - "swc": false, - "transpileOnly": true - } + "include": [ + "lib/" + ] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 000000000..128d072e7 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "test/**/*" + ] +} From 93efaf4f70f8329b3c9e50f5c9285a4a13d9ca24 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 01:19:12 +0200 Subject: [PATCH 06/15] refactor utils in typescript --- lib/bin/utils.js | 50 ----------------------------------------------- lib/bin/utils.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 50 deletions(-) delete mode 100644 lib/bin/utils.js create mode 100644 lib/bin/utils.ts diff --git a/lib/bin/utils.js b/lib/bin/utils.js deleted file mode 100644 index e73846552..000000000 --- a/lib/bin/utils.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -function console_both(final_result, theArgs) { - if(final_result.length > 0) { - final_result += '\n'; - } - var len = theArgs.length; - for (var i = 0 ; i < len; i++) { - if (typeof theArgs[i] != 'object') { - final_result += theArgs[i]; - } else { - final_result += JSON.stringify(theArgs[i]); - } - if(i != len -1 ) { - final_result += ' '; - } - - } - return final_result; -} - -var console_error = function console_error(final_result, ...theArgs) { - final_result.err = console_both(final_result.err, theArgs); -} - -var console_log = function console_log(final_result, ...theArgs) { - final_result.stdout = console_both(final_result.stdout, theArgs); -} - -var process_exit = function process_exit(final_result, ret) { - final_result.return_val = ret; -} - -var initFinalResults = function initFinalResults() { - var final_result = { - stdout: '' - , err: '' - , return_val : 0 - }; - return final_result; -} - - - -module.exports = { - console_log : console_log, - console_error : console_error, - process_exit : process_exit, - initFinalResults : initFinalResults -} \ No newline at end of file diff --git a/lib/bin/utils.ts b/lib/bin/utils.ts new file mode 100644 index 000000000..47d1fceb2 --- /dev/null +++ b/lib/bin/utils.ts @@ -0,0 +1,51 @@ +export interface FinalResult { + err: string + stdout: string + return_val: number +} + +function console_both(final_result: string, theArgs: unknown[]) { + let newResult = final_result + if (newResult.length > 0) { + newResult += '\n' + } + const len = theArgs.length + for (let i = 0; i < len; i++) { + if (typeof theArgs[i] !== 'object') { + newResult += theArgs[i] + } else { + newResult += JSON.stringify(theArgs[i]) + } + if (i !== len - 1) { + newResult += ' ' + } + } + return newResult +} + +export const console_error = function console_error(final_result: FinalResult, ...theArgs: unknown[]) { + final_result.err = console_both(final_result.err, theArgs) +} + +export const console_log = function console_log(final_result: FinalResult, ...theArgs: unknown[]) { + final_result.stdout = console_both(final_result.stdout, theArgs) +} + +export const process_exit = function process_exit(final_result: FinalResult, ret: number) { + final_result.return_val = ret +} + +export const initFinalResults = function initFinalResults(): FinalResult { + return { + stdout: '', + err: '', + return_val: 0, + } +} + +module.exports = { + console_log: console_log, + console_error: console_error, + process_exit: process_exit, + initFinalResults: initFinalResults, +} From 1c265f99b3c685d913dc642fa1c4cff5ac43e1b2 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 01:20:37 +0200 Subject: [PATCH 07/15] eslint --- .eslintrc.js | 10 +- .vscode/settings.json | 18 +- lib/autotune-prep/categorize.js | 618 +++++----- lib/autotune-prep/dosed.js | 29 +- lib/autotune-prep/index.js | 343 +++--- lib/autotune/index.ts | 793 ++++++++----- lib/basal-set-temp.ts | 90 +- lib/bolus.js | 327 +++--- lib/calc-glucose-stats.ts | 36 +- lib/date.ts | 13 +- lib/determine-basal/autosens.js | 560 ++++----- lib/determine-basal/cob.ts | 214 ++-- lib/determine-basal/determine-basal.ts | 1469 +++++++++++++----------- lib/glucose-get-last.ts | 103 +- lib/glucose-stats.ts | 382 +++--- lib/iob/InsulinTreatment.ts | 24 +- lib/iob/calculate.ts | 157 +-- lib/iob/history.ts | 508 ++++---- lib/iob/index.ts | 92 +- lib/iob/total.ts | 107 +- lib/meal/history.ts | 149 +-- lib/meal/index.ts | 40 +- lib/meal/total.ts | 191 +-- lib/medtronic-clock.ts | 15 +- lib/percentile.ts | 26 +- lib/profile/basal.ts | 41 +- lib/profile/carbs.js | 41 - lib/profile/carbs.ts | 52 + lib/profile/index.js | 187 --- lib/profile/index.ts | 180 +++ lib/profile/isf.ts | 50 +- lib/profile/targets.js | 87 -- lib/profile/targets.ts | 122 ++ lib/pump.js | 58 +- lib/require-utils.js | 81 -- lib/require-utils.ts | 78 ++ lib/round-basal.ts | 17 +- lib/temps.js | 86 +- lib/types/Autosens.ts | 6 +- lib/types/EventType.ts | 52 +- lib/types/GlucoseEntry.ts | 3 +- lib/types/LocalDateFromDate.ts | 12 +- lib/types/NightscoutTreatment.ts | 66 +- lib/types/Profile.ts | 251 ++-- lib/types/PumpHistoryEvent.ts | 56 +- lib/types/renameKey.ts | 55 +- lib/with-raw-glucose.js | 83 +- package.json | 311 ++--- 48 files changed, 4446 insertions(+), 3843 deletions(-) delete mode 100644 lib/profile/carbs.js create mode 100644 lib/profile/carbs.ts delete mode 100644 lib/profile/index.js create mode 100644 lib/profile/index.ts delete mode 100644 lib/profile/targets.js create mode 100644 lib/profile/targets.ts delete mode 100644 lib/require-utils.js create mode 100644 lib/require-utils.ts diff --git a/.eslintrc.js b/.eslintrc.js index a4e6fe5b7..9e02124e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,7 +38,7 @@ module.exports = { 'guard-for-in': 'error', 'id-match': 'error', 'no-bitwise': 'error', - 'no-console': 'error', + 'no-console': 'off', 'no-eq-null': 'error', 'no-extend-native': 'error', 'no-extra-bind': 'error', @@ -88,7 +88,13 @@ module.exports = { '@typescript-eslint/no-unused-vars': [ 'warn', { - argsIgnorePattern: '^_', + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true }, ], '@typescript-eslint/no-shadow': ['error', { hoist: 'all', ignoreTypeValueShadow: true }], diff --git a/.vscode/settings.json b/.vscode/settings.json index 72446f434..1390a1290 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,19 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "files.autoSave": "onFocusChange", + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnType": true, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.format.enable": true, + "prettier.enable": false, + "jest.coverageFormatter": "GutterFormatter", + "search.exclude": { + "dist/": true + } } diff --git a/lib/autotune-prep/categorize.js b/lib/autotune-prep/categorize.js index c6d7e6e01..458de591d 100644 --- a/lib/autotune-prep/categorize.js +++ b/lib/autotune-prep/categorize.js @@ -1,456 +1,488 @@ -'use strict'; +'use strict' -var basal = require('../profile/basal'); -var getIOB = require('../iob'); -var ISF = require('../profile/isf'); -var find_insulin = require('../iob/history'); -var dosed = require('./dosed'); -var date = require('../date'); -var tz = date.tz; +const date = require('../date') +const getIOB = require('../iob') +const find_insulin = require('../iob/history') +const basal = require('../profile/basal') +const ISF = require('../profile/isf') +const dosed = require('./dosed') +const tz = date.tz // main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals. function categorizeBGDatums(opts) { - var treatments = opts.treatments; + let treatments = opts.treatments // this sorts the treatments collection in order. - treatments.sort(function (a, b) { - var aDate = new Date(tz(a.timestamp)); - var bDate = new Date(tz(b.timestamp)); + treatments.sort((a, b) => { + const aDate = new Date(tz(a.timestamp)) + const bDate = new Date(tz(b.timestamp)) //console.error(aDate); - return bDate.getTime() - aDate.getTime(); - }); - var profileData = opts.profile; + return bDate.getTime() - aDate.getTime() + }) + const profileData = opts.profile - var glucoseData = [ ]; - if (typeof(opts.glucose) !== 'undefined') { + let glucoseData = [] + if (typeof opts.glucose !== 'undefined') { //var glucoseData = opts.glucose; - glucoseData = opts.glucose.map(function prepGlucose (obj) { - //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv; - - if (obj.date) { - //obj.BGTime = new Date(obj.date); - } else if (obj.displayTime) { - // Attempt to get date from displayTime - obj.date = new Date(obj.displayTime.replace('T', ' ')).getTime(); - } else if (obj.dateString) { - // Attempt to get date from dateString - obj.date = new Date(obj.dateString).getTime(); - }// else { console.error("Could not determine BG time"); } - - if (!obj.dateString) - { - obj.dateString = new Date(tz(obj.date)).toISOString(); - } - return obj; - }).filter(function filterRecords(obj) { - // Only take records with a valid date record - // and a glucose value, which is also above 39 - return (obj.date && obj.glucose && obj.glucose >=39); - }).sort(function (a, b) { - // sort the collection in order - return b.date - a.date; - }); + glucoseData = opts.glucose + .map(obj => { + //Support the NS sgv field to avoid having to convert in a custom way + obj.glucose = obj.glucose || obj.sgv + + if (obj.date) { + //obj.BGTime = new Date(obj.date); + } else if (obj.displayTime) { + // Attempt to get date from displayTime + obj.date = new Date(obj.displayTime.replace('T', ' ')).getTime() + } else if (obj.dateString) { + // Attempt to get date from dateString + obj.date = new Date(obj.dateString).getTime() + } // else { console.error("Could not determine BG time"); } + + if (!obj.dateString) { + obj.dateString = new Date(tz(obj.date)).toISOString() + } + return obj + }) + .filter(obj => { + // Only take records with a valid date record + // and a glucose value, which is also above 39 + return obj.date && obj.glucose && obj.glucose >= 39 + }) + .sort((a, b) => { + // sort the collection in order + return b.date - a.date + }) } // if (typeof(opts.preppedGlucose) !== 'undefined') { - // var preppedGlucoseData = opts.preppedGlucose; + // var preppedGlucoseData = opts.preppedGlucose; // } //starting variable at 0 - var boluses = 0; - var maxCarbs = 0; + const boluses = 0 + const maxCarbs = 0 //console.error(treatments); - if (!treatments) return {}; + if (!treatments) { + return {} + } //console.error(glucoseData); - var IOBInputs = { - profile: profileData - , history: opts.pumpHistory - }; - var CSFGlucoseData = []; - var ISFGlucoseData = []; - var basalGlucoseData = []; - var UAMGlucoseData = []; - var CRData = []; - - var bucketedData = []; - bucketedData[0] = JSON.parse(JSON.stringify(glucoseData[0])); - var j=0; - var k=0; // index of first value used by bucket + let IOBInputs = { + profile: profileData, + history: opts.pumpHistory, + } + let CSFGlucoseData = [] + let ISFGlucoseData = [] + let basalGlucoseData = [] + const UAMGlucoseData = [] + const CRData = [] + + const bucketedData = [] + bucketedData[0] = JSON.parse(JSON.stringify(glucoseData[0])) + let j = 0 + let k = 0 // index of first value used by bucket //for loop to validate and bucket the data - for (var i=1; i < glucoseData.length; ++i) { - var BGTime = glucoseData[i].date; - var lastBGTime = glucoseData[k].date; - var elapsedMinutes = (BGTime - lastBGTime)/(60*1000); - - if(Math.abs(elapsedMinutes) >= 2) { - j++; // move to next bucket - k=i; // store index of first value used by bucket - bucketedData[j]=JSON.parse(JSON.stringify(glucoseData[i])); + for (var i = 1; i < glucoseData.length; ++i) { + var BGTime = glucoseData[i].date + const lastBGTime = glucoseData[k].date + const elapsedMinutes = (BGTime - lastBGTime) / (60 * 1000) + + if (Math.abs(elapsedMinutes) >= 2) { + j++ // move to next bucket + k = i // store index of first value used by bucket + bucketedData[j] = JSON.parse(JSON.stringify(glucoseData[i])) } else { // average all readings within time deadband - var glucoseTotal = glucoseData.slice(k, i+1).reduce(function(total, entry) { - return total + entry.glucose; - }, 0); - bucketedData[j].glucose = glucoseTotal / (i-k+1); + const glucoseTotal = glucoseData.slice(k, i + 1).reduce((total, entry) => { + return total + entry.glucose + }, 0) + bucketedData[j].glucose = glucoseTotal / (i - k + 1) } } //console.error(bucketedData); //console.error(bucketedData[bucketedData.length-1]); // go through the treatments and remove any that are older than the oldest glucose value //console.error(treatments); - for (i=treatments.length-1; i>0; --i) { - var treatment = treatments[i]; + for (i = treatments.length - 1; i > 0; --i) { + var treatment = treatments[i] //console.error(treatment); if (treatment) { - var treatmentDate = new Date(tz(treatment.timestamp)); - var treatmentTime = treatmentDate.getTime(); - var glucoseDatum = bucketedData[bucketedData.length-1]; + var treatmentDate = new Date(tz(treatment.timestamp)) + var treatmentTime = treatmentDate.getTime() + var glucoseDatum = bucketedData[bucketedData.length - 1] //console.error(glucoseDatum); if (glucoseDatum) { - var BGDate = new Date(glucoseDatum.date); - BGTime = BGDate.getTime(); - if ( treatmentTime < BGTime ) { - treatments.splice(i,1); + var BGDate = new Date(glucoseDatum.date) + BGTime = BGDate.getTime() + if (treatmentTime < BGTime) { + treatments.splice(i, 1) } } } } //console.error(treatments); - var calculatingCR = false; - var absorbing = 0; - var uam = 0; // unannounced meal - var mealCOB = 0; - var mealCarbs = 0; - var CRCarbs = 0; - var type=""; + let calculatingCR = false + let absorbing = 0 + let uam = 0 // unannounced meal + let mealCOB = 0 + let mealCarbs = 0 + let CRCarbs = 0 + let type = '' // main for loop - var fullHistory = IOBInputs.history; - var lastIsfResult = null; - for (i=bucketedData.length-5; i > 0; --i) { - glucoseDatum = bucketedData[i]; + const fullHistory = IOBInputs.history + let lastIsfResult = null + for (i = bucketedData.length - 5; i > 0; --i) { + glucoseDatum = bucketedData[i] //console.error(glucoseDatum); - BGDate = new Date(glucoseDatum.date); - BGTime = BGDate.getTime(); + BGDate = new Date(glucoseDatum.date) + BGTime = BGDate.getTime() // As we're processing each data point, go through the treatment.carbs and see if any of them are older than // the current BG data point. If so, add those carbs to COB. - treatment = treatments[treatments.length-1]; - var myCarbs = 0; + treatment = treatments[treatments.length - 1] + let myCarbs = 0 if (treatment) { - treatmentDate = new Date(tz(treatment.timestamp)); - treatmentTime = treatmentDate.getTime(); + treatmentDate = new Date(tz(treatment.timestamp)) + treatmentTime = treatmentDate.getTime() //console.error(treatmentDate); - if ( treatmentTime < BGTime ) { + if (treatmentTime < BGTime) { if (treatment.carbs >= 1) { - mealCOB += parseFloat(treatment.carbs); - mealCarbs += parseFloat(treatment.carbs); - myCarbs = treatment.carbs; + mealCOB += parseFloat(treatment.carbs) + mealCarbs += parseFloat(treatment.carbs) + myCarbs = treatment.carbs } - treatments.pop(); + treatments.pop() } } - var BG; - var delta; - var avgDelta; + var BG + var delta + var avgDelta // TODO: re-implement interpolation to avoid issues here with gaps // calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero - if (typeof(bucketedData[i].glucose) !== 'undefined' && typeof(bucketedData[i+4].glucose) !== 'undefined') { + if (typeof bucketedData[i].glucose !== 'undefined' && typeof bucketedData[i + 4].glucose !== 'undefined') { //console.error(bucketedData[i]); - BG = bucketedData[i].glucose; - if ( BG < 40 || bucketedData[i+4].glucose < 40) { + BG = bucketedData[i].glucose + if (BG < 40 || bucketedData[i + 4].glucose < 40) { //process.stderr.write("!"); - continue; + continue } - delta = (BG - bucketedData[i+1].glucose); - avgDelta = (BG - bucketedData[i+4].glucose)/4; - } else { console.error("Could not find glucose data"); } + delta = BG - bucketedData[i + 1].glucose + avgDelta = (BG - bucketedData[i + 4].glucose) / 4 + } else { + console.error('Could not find glucose data') + } - avgDelta = avgDelta.toFixed(2); - glucoseDatum.avgDelta = avgDelta; + avgDelta = avgDelta.toFixed(2) + glucoseDatum.avgDelta = avgDelta //sens = ISF - var sens; - [sens, lastIsfResult] = ISF.isfLookup(IOBInputs.profile.isfProfile, BGDate, lastIsfResult); - IOBInputs.clock=BGDate.toISOString(); + var sens + ;[sens, lastIsfResult] = ISF.isfLookup(IOBInputs.profile.isfProfile, BGDate, lastIsfResult) + IOBInputs.clock = BGDate.toISOString() // trim down IOBInputs.history to just the data for 6h prior to BGDate //console.error(IOBInputs.history[0].created_at); - var newHistory = []; - for (var h=0; h 0) { + if (BGDate - hDate < 6 * 60 * 60 * 1000 && BGDate - hDate > 0) { //process.stderr.write("i"); //console.error(hDate); - newHistory.push(fullHistory[h]); + newHistory.push(fullHistory[h]) } } - IOBInputs.history = newHistory; + IOBInputs.history = newHistory // process.stderr.write("" + newHistory.length + " "); //console.error(newHistory[0].created_at,newHistory[newHistory.length-1].created_at,newHistory.length); - // for IOB calculations, use the average of the last 4 hours' basals to help convergence; // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge. // use the pumpbasalprofile to properly calculate IOB during periods where no temp basal is set - var currentPumpBasal = basal.basalLookup(opts.pumpbasalprofile, BGDate); - var BGDate1hAgo = new Date(BGTime-1*60*60*1000); - var BGDate2hAgo = new Date(BGTime-2*60*60*1000); - var BGDate3hAgo = new Date(BGTime-3*60*60*1000); - var basal1hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate1hAgo); - var basal2hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate2hAgo); - var basal3hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate3hAgo); - var sum = [currentPumpBasal,basal1hAgo,basal2hAgo,basal3hAgo].reduce(function(a, b) { return a + b; }); - IOBInputs.profile.currentBasal = Math.round((sum/4)*1000)/1000; + const currentPumpBasal = basal.basalLookup(opts.pumpbasalprofile, BGDate) + const BGDate1hAgo = new Date(BGTime - 1 * 60 * 60 * 1000) + const BGDate2hAgo = new Date(BGTime - 2 * 60 * 60 * 1000) + const BGDate3hAgo = new Date(BGTime - 3 * 60 * 60 * 1000) + const basal1hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate1hAgo) + const basal2hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate2hAgo) + const basal3hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate3hAgo) + const sum = [currentPumpBasal, basal1hAgo, basal2hAgo, basal3hAgo].reduce((a, b) => { + return a + b + }) + IOBInputs.profile.currentBasal = Math.round((sum / 4) * 1000) / 1000 // this is the current autotuned basal, used for everything else besides IOB calculations - var currentBasal = basal.basalLookup(opts.basalprofile, BGDate); + const currentBasal = basal.basalLookup(opts.basalprofile, BGDate) //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal); // basalBGI is BGI of basal insulin activity. - var basalBGI = Math.round(( currentBasal * sens / 60 * 5 )*100)/100; // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m + const basalBGI = Math.round(((currentBasal * sens) / 60) * 5 * 100) / 100 // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m //console.log(JSON.stringify(IOBInputs.profile)); // call iob since calculated elsewhere - var iob = getIOB(IOBInputs)[0]; + const iob = getIOB(IOBInputs)[0] //console.error(JSON.stringify(iob)); // activity times ISF times 5 minutes is BGI - var BGI = Math.round(( -iob.activity * sens * 5 )*100)/100; + const BGI = Math.round(-iob.activity * sens * 5 * 100) / 100 // datum = one glucose data point (being prepped to store in output) - glucoseDatum.BGI = BGI; + glucoseDatum.BGI = BGI // calculating deviation - var deviation = avgDelta-BGI; - var dev5m = delta-BGI; + let deviation = avgDelta - BGI + let dev5m = delta - BGI //console.error(deviation,avgDelta,BG,bucketedData[i].glucose); // set positive deviations to zero if BG is below 80 - if ( BG < 80 && deviation > 0 ) { - deviation = 0; + if (BG < 80 && deviation > 0) { + deviation = 0 } // rounding and storing deviation - deviation = deviation.toFixed(2); - dev5m = dev5m.toFixed(2); - glucoseDatum.deviation = deviation; - + deviation = deviation.toFixed(2) + dev5m = dev5m.toFixed(2) + glucoseDatum.deviation = deviation // Then, calculate carb absorption for that 5m interval using the deviation. - if ( mealCOB > 0 ) { - var profile = profileData; - var ci = Math.max(deviation, profile.min_5m_carbimpact); - var absorbed = ci * profile.carb_ratio / sens; + if (mealCOB > 0) { + const profile = profileData + const ci = Math.max(deviation, profile.min_5m_carbimpact) + const absorbed = (ci * profile.carb_ratio) / sens // Store the COB, and use it as the starting point for the next data point. - mealCOB = Math.max(0, mealCOB-absorbed); + mealCOB = Math.max(0, mealCOB - absorbed) } - // Calculate carb ratio (CR) independently of CSF and ISF // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2 // For now, if another meal IOB/COB stacks on top of it, consider them together // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR. - if (mealCOB > 0 || calculatingCR ) { + if (mealCOB > 0 || calculatingCR) { // set initial values when we first see COB - CRCarbs += myCarbs; + CRCarbs += myCarbs if (!calculatingCR) { - var CRInitialIOB = iob.iob; - var CRInitialBG = glucoseDatum.glucose; - var CRInitialCarbTime = new Date(glucoseDatum.date); - console.error("CRInitialIOB:",CRInitialIOB,"CRInitialBG:",CRInitialBG,"CRInitialCarbTime:",CRInitialCarbTime); + var CRInitialIOB = iob.iob + var CRInitialBG = glucoseDatum.glucose + var CRInitialCarbTime = new Date(glucoseDatum.date) + console.error( + 'CRInitialIOB:', + CRInitialIOB, + 'CRInitialBG:', + CRInitialBG, + 'CRInitialCarbTime:', + CRInitialCarbTime + ) } // keep calculatingCR as long as we have COB or enough IOB - if ( mealCOB > 0 && i>1 ) { - calculatingCR = true; - } else if ( iob.iob > currentBasal/2 && i>1 ) { - calculatingCR = true; - // when COB=0 and IOB drops low enough, record end values and be done calculatingCR + if (mealCOB > 0 && i > 1) { + calculatingCR = true + } else if (iob.iob > currentBasal / 2 && i > 1) { + calculatingCR = true + // when COB=0 and IOB drops low enough, record end values and be done calculatingCR } else { - var CREndIOB = iob.iob; - var CREndBG = glucoseDatum.glucose; - var CREndTime = new Date(glucoseDatum.date); - console.error("CREndIOB:",CREndIOB,"CREndBG:",CREndBG,"CREndTime:",CREndTime); - var CRDatum = { - CRInitialIOB: CRInitialIOB - , CRInitialBG: CRInitialBG - , CRInitialCarbTime: CRInitialCarbTime - , CREndIOB: CREndIOB - , CREndBG: CREndBG - , CREndTime: CREndTime - , CRCarbs: CRCarbs - }; + const CREndIOB = iob.iob + const CREndBG = glucoseDatum.glucose + const CREndTime = new Date(glucoseDatum.date) + console.error('CREndIOB:', CREndIOB, 'CREndBG:', CREndBG, 'CREndTime:', CREndTime) + const CRDatum = { + CRInitialIOB: CRInitialIOB, + CRInitialBG: CRInitialBG, + CRInitialCarbTime: CRInitialCarbTime, + CREndIOB: CREndIOB, + CREndBG: CREndBG, + CREndTime: CREndTime, + CRCarbs: CRCarbs, + } //console.error(CRDatum); - var CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60); + const CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60) //console.error(CREndTime - CRInitialCarbTime, CRElapsedMinutes); - if ( CRElapsedMinutes < 60 || ( i===1 && mealCOB > 0 ) ) { - console.error("Ignoring",CRElapsedMinutes,"m CR period."); + if (CRElapsedMinutes < 60 || (i === 1 && mealCOB > 0)) { + console.error('Ignoring', CRElapsedMinutes, 'm CR period.') } else { - CRData.push(CRDatum); + CRData.push(CRDatum) } - CRCarbs = 0; - calculatingCR = false; + CRCarbs = 0 + calculatingCR = false } } - // If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData // Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals if (mealCOB > 0 || absorbing || mealCarbs > 0) { // if meal IOB has decayed, then end absorption after this data point unless COB > 0 - if ( iob.iob < currentBasal/2 ) { - absorbing = 0; - // otherwise, as long as deviations are positive, keep tracking carb deviations + if (iob.iob < currentBasal / 2) { + absorbing = 0 + // otherwise, as long as deviations are positive, keep tracking carb deviations } else if (deviation > 0) { - absorbing = 1; + absorbing = 1 } else { - absorbing = 0; + absorbing = 0 } - if ( ! absorbing && ! mealCOB ) { - mealCarbs = 0; + if (!absorbing && !mealCOB) { + mealCarbs = 0 } // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag //console.error(type); - if ( type !== "csf" ) { - glucoseDatum.mealAbsorption = "start"; - console.error(glucoseDatum.mealAbsorption,"carb absorption"); + if (type !== 'csf') { + glucoseDatum.mealAbsorption = 'start' + console.error(glucoseDatum.mealAbsorption, 'carb absorption') } - type="csf"; - glucoseDatum.mealCarbs = mealCarbs; + type = 'csf' + glucoseDatum.mealCarbs = mealCarbs //if (i == 0) { glucoseDatum.mealAbsorption = "end"; } - CSFGlucoseData.push(glucoseDatum); + CSFGlucoseData.push(glucoseDatum) } else { - // check previous "type" value, and if it was csf, set a mealAbsorption end flag - if ( type === "csf" ) { - CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end"; - console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); - } - - if ((iob.iob > 2 * currentBasal || deviation > 6 || uam) ) { - if (deviation > 0) { - uam = 1; - } else { - uam = 0; + // check previous "type" value, and if it was csf, set a mealAbsorption end flag + if (type === 'csf') { + CSFGlucoseData[CSFGlucoseData.length - 1].mealAbsorption = 'end' + console.error(CSFGlucoseData[CSFGlucoseData.length - 1].mealAbsorption, 'carb absorption') } - if ( type !== "uam" ) { - glucoseDatum.uamAbsorption = "start"; - console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption"); - } - type="uam"; - UAMGlucoseData.push(glucoseDatum); - } else { - if ( type === "uam" ) { - console.error("end unannounced meal absorption"); - } - - // Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity. - // When BGI is positive (insulin activity is negative), we want to use that data to tune basals - // When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals - // When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF, - // unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals - if (basalBGI > -4 * BGI) { - type="basal"; - basalGlucoseData.push(glucoseDatum); + if (iob.iob > 2 * currentBasal || deviation > 6 || uam) { + if (deviation > 0) { + uam = 1 + } else { + uam = 0 + } + if (type !== 'uam') { + glucoseDatum.uamAbsorption = 'start' + console.error(glucoseDatum.uamAbsorption, 'uannnounced meal absorption') + } + type = 'uam' + UAMGlucoseData.push(glucoseDatum) } else { - if ( avgDelta > 0 && avgDelta > -2*BGI ) { - //type="unknown" - type="basal" - basalGlucoseData.push(glucoseDatum); + if (type === 'uam') { + console.error('end unannounced meal absorption') + } + + // Go through the remaining time periods and divide them into periods where scheduled basal insulin activity dominates. This would be determined by calculating the BG impact of scheduled basal insulin (for example 1U/hr * 48 mg/dL/U ISF = 48 mg/dL/hr = 5 mg/dL/5m), and comparing that to BGI from bolus and net basal insulin activity. + // When BGI is positive (insulin activity is negative), we want to use that data to tune basals + // When BGI is smaller than about 1/4 of basalBGI, we want to use that data to tune basals + // When BGI is negative and more than about 1/4 of basalBGI, we can use that data to tune ISF, + // unless avgDelta is positive: then that's some sort of unexplained rise we don't want to use for ISF, so that means basals + if (basalBGI > -4 * BGI) { + type = 'basal' + basalGlucoseData.push(glucoseDatum) } else { - type="ISF"; - ISFGlucoseData.push(glucoseDatum); + if (avgDelta > 0 && avgDelta > -2 * BGI) { + //type="unknown" + type = 'basal' + basalGlucoseData.push(glucoseDatum) + } else { + type = 'ISF' + ISFGlucoseData.push(glucoseDatum) + } } } - } } // debug line to print out all the things - var BGDateArray = BGDate.toString().split(" "); - BGTime = BGDateArray[4]; + const BGDateArray = BGDate.toString().split(' ') + BGTime = BGDateArray[4] // console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"basalBGI:",basalBGI.toFixed(1),"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",deviation,"avgDelta:",avgDelta,type); - console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",dev5m,"avgDev:",deviation,"avgDelta:",avgDelta,type,BG,myCarbs); + console.error( + absorbing.toString(), + 'mealCOB:', + mealCOB.toFixed(1), + 'mealCarbs:', + mealCarbs, + 'BGI:', + BGI.toFixed(1), + 'IOB:', + iob.iob.toFixed(1), + 'at', + BGTime, + 'dev:', + dev5m, + 'avgDev:', + deviation, + 'avgDelta:', + avgDelta, + type, + BG, + myCarbs + ) } IOBInputs = { - profile: profileData - , history: opts.pumpHistory - }; - treatments = find_insulin(IOBInputs); - CRData.forEach(function(CRDatum) { - var dosedOpts = { - treatments: treatments - , profile: opts.profile - , start: CRDatum.CRInitialCarbTime - , end: CRDatum.CREndTime - }; - var insulinDosed = dosed(dosedOpts); - CRDatum.CRInsulin = insulinDosed.insulin; + profile: profileData, + history: opts.pumpHistory, + } + treatments = find_insulin(IOBInputs) + CRData.forEach(CRDatum => { + const dosedOpts = { + treatments: treatments, + profile: opts.profile, + start: CRDatum.CRInitialCarbTime, + end: CRDatum.CREndTime, + } + const insulinDosed = dosed(dosedOpts) + CRDatum.CRInsulin = insulinDosed.insulin //console.error(CRDatum); - }); + }) - var CSFLength = CSFGlucoseData.length; - var ISFLength = ISFGlucoseData.length; - var UAMLength = UAMGlucoseData.length; - var basalLength = basalGlucoseData.length; + const CSFLength = CSFGlucoseData.length + let ISFLength = ISFGlucoseData.length + const UAMLength = UAMGlucoseData.length + let basalLength = basalGlucoseData.length if (opts.categorize_uam_as_basal) { - console.error("--categorize-uam-as-basal=true set: categorizing all UAM data as basal."); - basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData); + console.error('--categorize-uam-as-basal=true set: categorizing all UAM data as basal.') + basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData) } else if (CSFLength > 12) { - console.error("Found at least 1h of carb absorption: assuming all meals were announced, and categorizing UAM data as basal."); - basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData); + console.error( + 'Found at least 1h of carb absorption: assuming all meals were announced, and categorizing UAM data as basal.' + ) + basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData) } else { - if (2*basalLength < UAMLength) { + if (2 * basalLength < UAMLength) { //console.error(basalGlucoseData, UAMGlucoseData); - console.error("Warning: too many deviations categorized as UnAnnounced Meals"); - console.error("Adding",UAMLength,"UAM deviations to",basalLength,"basal ones"); - basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData); + console.error('Warning: too many deviations categorized as UnAnnounced Meals') + console.error('Adding', UAMLength, 'UAM deviations to', basalLength, 'basal ones') + basalGlucoseData = basalGlucoseData.concat(UAMGlucoseData) //console.error(basalGlucoseData); // if too much data is excluded as UAM, add in the UAM deviations to basal, but then discard the highest 50% - basalGlucoseData.sort(function (a, b) { - return a.deviation - b.deviation; - }); - var newBasalGlucose = basalGlucoseData.slice(0,basalGlucoseData.length/2); + basalGlucoseData.sort((a, b) => { + return a.deviation - b.deviation + }) + const newBasalGlucose = basalGlucoseData.slice(0, basalGlucoseData.length / 2) //console.error(newBasalGlucose); - basalGlucoseData = newBasalGlucose; - console.error("and selecting the lowest 50%, leaving", basalGlucoseData.length, "basal+UAM ones"); + basalGlucoseData = newBasalGlucose + console.error('and selecting the lowest 50%, leaving', basalGlucoseData.length, 'basal+UAM ones') } - if (2*ISFLength < UAMLength && ISFLength < 10) { - console.error("Adding",UAMLength,"UAM deviations to",ISFLength,"ISF ones"); - ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData); + if (2 * ISFLength < UAMLength && ISFLength < 10) { + console.error('Adding', UAMLength, 'UAM deviations to', ISFLength, 'ISF ones') + ISFGlucoseData = ISFGlucoseData.concat(UAMGlucoseData) // if too much data is excluded as UAM, add in the UAM deviations to ISF, but then discard the highest 50% - ISFGlucoseData.sort(function (a, b) { - return a.deviation - b.deviation; - }); - var newISFGlucose = ISFGlucoseData.slice(0,ISFGlucoseData.length/2); + ISFGlucoseData.sort((a, b) => { + return a.deviation - b.deviation + }) + const newISFGlucose = ISFGlucoseData.slice(0, ISFGlucoseData.length / 2) //console.error(newISFGlucose); - ISFGlucoseData = newISFGlucose; - console.error("and selecting the lowest 50%, leaving", ISFGlucoseData.length, "ISF+UAM ones"); + ISFGlucoseData = newISFGlucose + console.error('and selecting the lowest 50%, leaving', ISFGlucoseData.length, 'ISF+UAM ones') //console.error(ISFGlucoseData.length, UAMLength); } } - basalLength = basalGlucoseData.length; - ISFLength = ISFGlucoseData.length; - if ( 4*basalLength + ISFLength < CSFLength && ISFLength < 10 ) { - console.error("Warning: too many deviations categorized as meals"); + basalLength = basalGlucoseData.length + ISFLength = ISFGlucoseData.length + if (4 * basalLength + ISFLength < CSFLength && ISFLength < 10) { + console.error('Warning: too many deviations categorized as meals') //console.error("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones"); //var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData); - console.error("Adding",CSFLength,"CSF deviations to",ISFLength,"ISF ones"); - ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData); - CSFGlucoseData = []; + console.error('Adding', CSFLength, 'CSF deviations to', ISFLength, 'ISF ones') + ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData) + CSFGlucoseData = [] } - return { CRData: CRData, CSFGlucoseData: CSFGlucoseData, ISFGlucoseData: ISFGlucoseData, - basalGlucoseData: basalGlucoseData - }; + basalGlucoseData: basalGlucoseData, + } } -exports = module.exports = categorizeBGDatums; +exports = module.exports = categorizeBGDatums diff --git a/lib/autotune-prep/dosed.js b/lib/autotune-prep/dosed.js index f00ec59b4..4ece2f5ef 100644 --- a/lib/autotune-prep/dosed.js +++ b/lib/autotune-prep/dosed.js @@ -1,26 +1,25 @@ function insulinDosed(opts) { - - var start = opts.start.getTime(); - var end = opts.end.getTime(); - var treatments = opts.treatments; - var profile_data = opts.profile; - var insulinDosed = 0; + const start = opts.start.getTime() + const end = opts.end.getTime() + const treatments = opts.treatments + const profile_data = opts.profile + let insulinDosed = 0 if (!treatments) { - console.error("No treatments to process."); - return {}; + console.error('No treatments to process.') + return {} } - treatments.forEach(function(treatment) { + treatments.forEach(treatment => { //console.error(treatment); - if(treatment.insulin && treatment.date > start && treatment.date <= end) { - insulinDosed += treatment.insulin; + if (treatment.insulin && treatment.date > start && treatment.date <= end) { + insulinDosed += treatment.insulin } - }); + }) //console.error(insulinDosed); return { - insulin: Math.round( insulinDosed * 1000 ) / 1000 - }; + insulin: Math.round(insulinDosed * 1000) / 1000, + } } -exports = module.exports = insulinDosed; +exports = module.exports = insulinDosed diff --git a/lib/autotune-prep/index.js b/lib/autotune-prep/index.js index 122f780ff..476ea4bde 100644 --- a/lib/autotune-prep/index.js +++ b/lib/autotune-prep/index.js @@ -1,175 +1,192 @@ +// Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js + +const find_meals = require('../meal/history') +const categorize = require('./categorize') + +function generate(inputs) { + //console.error(inputs); + const treatments = find_meals(inputs) + + const opts = { + treatments: treatments, + profile: inputs.profile, + pumpHistory: inputs.history, + glucose: inputs.glucose, + //, prepped_glucose: inputs.prepped_glucose + basalprofile: inputs.profile.basalprofile, + pumpbasalprofile: inputs.pumpprofile.basalprofile, + categorize_uam_as_basal: inputs.categorize_uam_as_basal, + } -// Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js - -var find_meals = require('../meal/history'); -var categorize = require('./categorize'); - -function generate (inputs) { - - //console.error(inputs); - var treatments = find_meals(inputs); - - var opts = { - treatments: treatments - , profile: inputs.profile - , pumpHistory: inputs.history - , glucose: inputs.glucose - //, prepped_glucose: inputs.prepped_glucose - , basalprofile: inputs.profile.basalprofile - , pumpbasalprofile: inputs.pumpprofile.basalprofile - , categorize_uam_as_basal: inputs.categorize_uam_as_basal - }; - - var autotune_prep_output = categorize(opts); - - if (inputs.tune_insulin_curve) { - if (opts.profile.curve === 'bilinear') { - console.error('--tune-insulin-curve is set but only valid for exponential curves'); - } else { - var minDeviations = 1000000; - var newDIA = 0; - var diaDeviations = []; - var peakDeviations = []; - var currentDIA = opts.profile.dia; - var currentPeak = opts.profile.insulinPeakTime; - - var consoleError = console.error; - console.error = function() {}; - - var startDIA=currentDIA - 2; - var endDIA=currentDIA + 2; - for (var dia=startDIA; dia <= endDIA; ++dia) { - var sqrtDeviations = 0; - var deviations = 0; - var deviationsSq = 0; - - opts.profile.dia = dia; - - var curve_output = categorize(opts); - var basalGlucose = curve_output.basalGlucoseData; - - for (var hour=0; hour < 24; ++hour) { - for (var i=0; i < basalGlucose.length; ++i) { - var BGTime; - - if (basalGlucose[i].date) { - BGTime = new Date(basalGlucose[i].date); - } else if (basalGlucose[i].displayTime) { - BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')); - } else if (basalGlucose[i].dateString) { - BGTime = new Date(basalGlucose[i].dateString); - } else { - consoleError("Could not determine last BG time"); + const autotune_prep_output = categorize(opts) + + if (inputs.tune_insulin_curve) { + if (opts.profile.curve === 'bilinear') { + console.error('--tune-insulin-curve is set but only valid for exponential curves') + } else { + let minDeviations = 1000000 + let newDIA = 0 + const diaDeviations = [] + const peakDeviations = [] + const currentDIA = opts.profile.dia + const currentPeak = opts.profile.insulinPeakTime + + const consoleError = console.error + console.error = function () {} + + const startDIA = currentDIA - 2 + const endDIA = currentDIA + 2 + for (let dia = startDIA; dia <= endDIA; ++dia) { + var sqrtDeviations = 0 + var deviations = 0 + var deviationsSq = 0 + + opts.profile.dia = dia + + var curve_output = categorize(opts) + var basalGlucose = curve_output.basalGlucoseData + + for (var hour = 0; hour < 24; ++hour) { + for (var i = 0; i < basalGlucose.length; ++i) { + var BGTime + + if (basalGlucose[i].date) { + BGTime = new Date(basalGlucose[i].date) + } else if (basalGlucose[i].displayTime) { + BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')) + } else if (basalGlucose[i].dateString) { + BGTime = new Date(basalGlucose[i].dateString) + } else { + consoleError('Could not determine last BG time') + } + + var myHour = BGTime.getHours() + if (hour === myHour) { + //console.error(basalGlucose[i].deviation); + sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5) + deviations += Math.abs(parseFloat(basalGlucose[i].deviation)) + deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2) + } + } + } + + var meanDeviation = Math.round(Math.abs(deviations / basalGlucose.length) * 1000) / 1000 + var SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 + var RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 + consoleError( + 'insulinEndTime', + dia, + 'meanDeviation:', + meanDeviation, + 'SMRDeviation:', + SMRDeviation, + 'RMSDeviation:', + RMSDeviation, + '(mg/dL)' + ) + diaDeviations.push({ + dia: dia, + meanDeviation: meanDeviation, + SMRDeviation: SMRDeviation, + RMSDeviation: RMSDeviation, + }) + autotune_prep_output.diaDeviations = diaDeviations + + deviations = Math.round(deviations * 1000) / 1000 + if (deviations < minDeviations) { + minDeviations = Math.round(deviations * 1000) / 1000 + newDIA = dia + } } - var myHour = BGTime.getHours(); - if (hour === myHour) { - //console.error(basalGlucose[i].deviation); - sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5); - deviations += Math.abs(parseFloat(basalGlucose[i].deviation)); - deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2); - } - } - } + // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); + //consoleError(diaDeviations); - var meanDeviation = Math.round(Math.abs(deviations/basalGlucose.length)*1000)/1000; - var SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000; - var RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000; - consoleError('insulinEndTime', dia, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)'); - diaDeviations.push({ - dia: dia, - meanDeviation: meanDeviation, - SMRDeviation: SMRDeviation, - RMSDeviation: RMSDeviation, - }); - autotune_prep_output.diaDeviations = diaDeviations; - - deviations = Math.round(deviations*1000)/1000; - if (deviations < minDeviations) { - minDeviations = Math.round(deviations*1000)/1000; - newDIA = dia; - } - } - - // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); - //consoleError(diaDeviations); - - minDeviations = 1000000; - - var newPeak = 0; - opts.profile.dia = currentDIA; - //consoleError(opts.profile.useCustomPeakTime, opts.profile.insulinPeakTime); - if ( ! opts.profile.useCustomPeakTime === true && opts.profile.curve === "ultra-rapid" ) { - opts.profile.insulinPeakTime = 55; - } else if ( ! opts.profile.useCustomPeakTime === true ) { - opts.profile.insulinPeakTime = 75; - } - opts.profile.useCustomPeakTime = true; - - var startPeak=opts.profile.insulinPeakTime - 10; - var endPeak=opts.profile.insulinPeakTime + 10; - for (var peak=startPeak; peak <= endPeak; peak=(peak+5)) { - sqrtDeviations = 0; - deviations = 0; - deviationsSq = 0; - - opts.profile.insulinPeakTime = peak; - - - curve_output = categorize(opts); - basalGlucose = curve_output.basalGlucoseData; - - for (hour=0; hour < 24; ++hour) { - for (i=0; i < basalGlucose.length; ++i) { - if (basalGlucose[i].date) { - BGTime = new Date(basalGlucose[i].date); - } else if (basalGlucose[i].displayTime) { - BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')); - } else if (basalGlucose[i].dateString) { - BGTime = new Date(basalGlucose[i].dateString); - } else { - consoleError("Could not determine last BG time"); - } + minDeviations = 1000000 - myHour = BGTime.getHours(); - if (hour === myHour) { - //console.error(basalGlucose[i].deviation); - sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5); - deviations += Math.abs(parseFloat(basalGlucose[i].deviation)); - deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2); + let newPeak = 0 + opts.profile.dia = currentDIA + //consoleError(opts.profile.useCustomPeakTime, opts.profile.insulinPeakTime); + if (!opts.profile.useCustomPeakTime === true && opts.profile.curve === 'ultra-rapid') { + opts.profile.insulinPeakTime = 55 + } else if (!opts.profile.useCustomPeakTime === true) { + opts.profile.insulinPeakTime = 75 + } + opts.profile.useCustomPeakTime = true + + const startPeak = opts.profile.insulinPeakTime - 10 + const endPeak = opts.profile.insulinPeakTime + 10 + for (let peak = startPeak; peak <= endPeak; peak = peak + 5) { + sqrtDeviations = 0 + deviations = 0 + deviationsSq = 0 + + opts.profile.insulinPeakTime = peak + + curve_output = categorize(opts) + basalGlucose = curve_output.basalGlucoseData + + for (hour = 0; hour < 24; ++hour) { + for (i = 0; i < basalGlucose.length; ++i) { + if (basalGlucose[i].date) { + BGTime = new Date(basalGlucose[i].date) + } else if (basalGlucose[i].displayTime) { + BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')) + } else if (basalGlucose[i].dateString) { + BGTime = new Date(basalGlucose[i].dateString) + } else { + consoleError('Could not determine last BG time') + } + + myHour = BGTime.getHours() + if (hour === myHour) { + //console.error(basalGlucose[i].deviation); + sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5) + deviations += Math.abs(parseFloat(basalGlucose[i].deviation)) + deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2) + } + } + } + console.error(deviationsSq) + + meanDeviation = Math.round((deviations / basalGlucose.length) * 1000) / 1000 + SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 + RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 + consoleError( + 'insulinPeakTime', + peak, + 'meanDeviation:', + meanDeviation, + 'SMRDeviation:', + SMRDeviation, + 'RMSDeviation:', + RMSDeviation, + '(mg/dL)' + ) + peakDeviations.push({ + peak: peak, + meanDeviation: meanDeviation, + SMRDeviation: SMRDeviation, + RMSDeviation: RMSDeviation, + }) + autotune_prep_output.diaDeviations = diaDeviations + + deviations = Math.round(deviations * 1000) / 1000 + if (deviations < minDeviations) { + minDeviations = Math.round(deviations * 1000) / 1000 + newPeak = peak + } } - } - } - console.error(deviationsSq); - - meanDeviation = Math.round(deviations/basalGlucose.length*1000)/1000; - SMRDeviation = Math.round(Math.pow(sqrtDeviations/basalGlucose.length,2)*1000)/1000; - RMSDeviation = Math.round(Math.pow(deviationsSq/basalGlucose.length,0.5)*1000)/1000; - consoleError('insulinPeakTime', peak, 'meanDeviation:', meanDeviation, 'SMRDeviation:', SMRDeviation, 'RMSDeviation:',RMSDeviation, '(mg/dL)'); - peakDeviations.push({ - peak: peak, - meanDeviation: meanDeviation, - SMRDeviation: SMRDeviation, - RMSDeviation: RMSDeviation, - }); - autotune_prep_output.diaDeviations = diaDeviations; - - deviations = Math.round(deviations*1000)/1000; - if (deviations < minDeviations) { - minDeviations = Math.round(deviations*1000)/1000; - newPeak = peak; - } - } - //consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); - //consoleError(peakDeviations); - autotune_prep_output.peakDeviations = peakDeviations; + //consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); + //consoleError(peakDeviations); + autotune_prep_output.peakDeviations = peakDeviations - console.error = consoleError; + console.error = consoleError + } } - } - return autotune_prep_output; + return autotune_prep_output } -exports = module.exports = generate; +exports = module.exports = generate diff --git a/lib/autotune/index.ts b/lib/autotune/index.ts index 86e3e2b40..a0ffa14a3 100644 --- a/lib/autotune/index.ts +++ b/lib/autotune/index.ts @@ -1,300 +1,343 @@ -var percentile = require('../percentile') +const percentile = require('../percentile') // does three things - tunes basals, ISF, and CSF -function tuneAllTheThings (inputs: any) { - - var previousAutotune = inputs.previousAutotune; +function tuneAllTheThings(inputs: any) { + const previousAutotune = inputs.previousAutotune //console.error(previousAutotune); - var pumpProfile = inputs.pumpProfile; - var pumpBasalProfile = pumpProfile.basalprofile; + const pumpProfile = inputs.pumpProfile + const pumpBasalProfile = pumpProfile.basalprofile //console.error(pumpBasalProfile); - var basalProfile = previousAutotune.basalprofile; + let basalProfile = previousAutotune.basalprofile //console.error(basalProfile); - var isfProfile = previousAutotune.isfProfile; + const isfProfile = previousAutotune.isfProfile //console.error(isfProfile); - var ISF = isfProfile.sensitivities[0].sensitivity; + let ISF = isfProfile.sensitivities[0].sensitivity //console.error(ISF); - var carbRatio = previousAutotune.carb_ratio; + let carbRatio = previousAutotune.carb_ratio //console.error(carbRatio); - var CSF = ISF / carbRatio; - var DIA = previousAutotune.dia; - var peak = previousAutotune.insulinPeakTime; - if (! previousAutotune.useCustomPeakTime === true) { - if ( previousAutotune.curve === "ultra-rapid" ) { - peak = 55; + let CSF = ISF / carbRatio + const DIA = previousAutotune.dia + let peak = previousAutotune.insulinPeakTime + if (!previousAutotune.useCustomPeakTime === true) { + if (previousAutotune.curve === 'ultra-rapid') { + peak = 55 } else { - peak = 75; + peak = 75 } } //console.error(DIA, peak); - var pumpISF: any - var pumpCSF: any - var pumpCarbRatio: any + let pumpISF: any + let pumpCSF: any + let pumpCarbRatio: any if (pumpProfile && pumpProfile.isfProfile) { pumpISF = pumpProfile.isfProfile.sensitivities[0].sensitivity pumpCarbRatio = pumpProfile.carb_ratio pumpCSF = pumpISF / pumpCarbRatio - if (! carbRatio) { carbRatio = pumpCarbRatio; } - if (! CSF) { CSF = pumpCSF; } - if (! ISF) { ISF = pumpISF; } + if (!carbRatio) { + carbRatio = pumpCarbRatio + } + if (!CSF) { + CSF = pumpCSF + } + if (!ISF) { + ISF = pumpISF + } } //console.error(CSF); - var preppedGlucose = inputs.preppedGlucose; - var CSFGlucose = preppedGlucose.CSFGlucoseData; + const preppedGlucose = inputs.preppedGlucose + const CSFGlucose = preppedGlucose.CSFGlucoseData //console.error(CSFGlucose[0]); - var ISFGlucose = preppedGlucose.ISFGlucoseData; + const ISFGlucose = preppedGlucose.ISFGlucoseData //console.error(ISFGlucose[0]); - var basalGlucose = preppedGlucose.basalGlucoseData; + const basalGlucose = preppedGlucose.basalGlucoseData //console.error(basalGlucose[0]); - var CRData = preppedGlucose.CRData; + const CRData = preppedGlucose.CRData //console.error(CRData); - var diaDeviations = preppedGlucose.diaDeviations; + const diaDeviations = preppedGlucose.diaDeviations //console.error(diaDeviations); - var peakDeviations = preppedGlucose.peakDeviations; + const peakDeviations = preppedGlucose.peakDeviations //console.error(peakDeviations); // tune DIA - var newDIA = DIA; + let newDIA = DIA if (diaDeviations) { - var currentDIAMeanDev = diaDeviations[2].meanDeviation; - var currentDIARMSDev = diaDeviations[2].RMSDeviation; + const currentDIAMeanDev = diaDeviations[2].meanDeviation + const currentDIARMSDev = diaDeviations[2].RMSDeviation //console.error(DIA,currentDIAMeanDev,currentDIARMSDev); - var minMeanDeviations = 1000000; - var minRMSDeviations = 1000000; - var meanBest = 2; - var RMSBest = 2; - for (var i=0; i < diaDeviations.length; i++) { - var meanDeviations = diaDeviations[i].meanDeviation; - var RMSDeviations = diaDeviations[i].RMSDeviation; + var minMeanDeviations = 1000000 + var minRMSDeviations = 1000000 + var meanBest = 2 + var RMSBest = 2 + for (var i = 0; i < diaDeviations.length; i++) { + var meanDeviations = diaDeviations[i].meanDeviation + var RMSDeviations = diaDeviations[i].RMSDeviation if (meanDeviations < minMeanDeviations) { - minMeanDeviations = Math.round(meanDeviations*1000)/1000; - meanBest = i; + minMeanDeviations = Math.round(meanDeviations * 1000) / 1000 + meanBest = i } if (RMSDeviations < minRMSDeviations) { - minRMSDeviations = Math.round(RMSDeviations*1000)/1000; - RMSBest = i; + minRMSDeviations = Math.round(RMSDeviations * 1000) / 1000 + RMSBest = i } } - console.error("Best insulinEndTime for meanDeviations:",diaDeviations[meanBest].dia,"hours"); - console.error("Best insulinEndTime for RMSDeviations:",diaDeviations[RMSBest].dia,"hours"); - if ( meanBest < 2 && RMSBest < 2 ) { - if ( diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 ) { - newDIA = diaDeviations[1].dia; + console.error('Best insulinEndTime for meanDeviations:', diaDeviations[meanBest].dia, 'hours') + console.error('Best insulinEndTime for RMSDeviations:', diaDeviations[RMSBest].dia, 'hours') + if (meanBest < 2 && RMSBest < 2) { + if ( + diaDeviations[1].meanDeviation < currentDIAMeanDev * 0.99 && + diaDeviations[1].RMSDeviation < currentDIARMSDev * 0.99 + ) { + newDIA = diaDeviations[1].dia } - } else if ( meanBest > 2 && RMSBest > 2 ) { - if ( diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 ) { - newDIA = diaDeviations[3].dia; + } else if (meanBest > 2 && RMSBest > 2) { + if ( + diaDeviations[3].meanDeviation < currentDIAMeanDev * 0.99 && + diaDeviations[3].RMSDeviation < currentDIARMSDev * 0.99 + ) { + newDIA = diaDeviations[3].dia } } - if ( newDIA > 12 ) { - console.error("insulinEndTime maximum is 12h: not raising further"); - newDIA=12; + if (newDIA > 12) { + console.error('insulinEndTime maximum is 12h: not raising further') + newDIA = 12 } - if ( newDIA !== DIA ) { - console.error("Adjusting insulinEndTime from",DIA,"to",newDIA,"hours"); + if (newDIA !== DIA) { + console.error('Adjusting insulinEndTime from', DIA, 'to', newDIA, 'hours') } else { - console.error("Leaving insulinEndTime unchanged at",DIA,"hours"); + console.error('Leaving insulinEndTime unchanged at', DIA, 'hours') } } // tune insulinPeakTime - var newPeak = peak; + let newPeak = peak if (peakDeviations && peakDeviations[2]) { - var currentPeakMeanDev = peakDeviations[2].meanDeviation; - var currentPeakRMSDev = peakDeviations[2].RMSDeviation; + const currentPeakMeanDev = peakDeviations[2].meanDeviation + const currentPeakRMSDev = peakDeviations[2].RMSDeviation //console.error(currentPeakMeanDev); - minMeanDeviations = 1000000; - minRMSDeviations = 1000000; - meanBest = 2; - RMSBest = 2; - for (i=0; i < peakDeviations.length; i++) { - meanDeviations = peakDeviations[i].meanDeviation; - RMSDeviations = peakDeviations[i].RMSDeviation; + minMeanDeviations = 1000000 + minRMSDeviations = 1000000 + meanBest = 2 + RMSBest = 2 + for (i = 0; i < peakDeviations.length; i++) { + meanDeviations = peakDeviations[i].meanDeviation + RMSDeviations = peakDeviations[i].RMSDeviation if (meanDeviations < minMeanDeviations) { - minMeanDeviations = Math.round(meanDeviations*1000)/1000; - meanBest = i; + minMeanDeviations = Math.round(meanDeviations * 1000) / 1000 + meanBest = i } if (RMSDeviations < minRMSDeviations) { - minRMSDeviations = Math.round(RMSDeviations*1000)/1000; - RMSBest = i; + minRMSDeviations = Math.round(RMSDeviations * 1000) / 1000 + RMSBest = i } } - console.error("Best insulinPeakTime for meanDeviations:",peakDeviations[meanBest].peak,"minutes"); - console.error("Best insulinPeakTime for RMSDeviations:",peakDeviations[RMSBest].peak,"minutes"); - if ( meanBest < 2 && RMSBest < 2 ) { - if ( peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 ) { - newPeak = peakDeviations[1].peak; + console.error('Best insulinPeakTime for meanDeviations:', peakDeviations[meanBest].peak, 'minutes') + console.error('Best insulinPeakTime for RMSDeviations:', peakDeviations[RMSBest].peak, 'minutes') + if (meanBest < 2 && RMSBest < 2) { + if ( + peakDeviations[1].meanDeviation < currentPeakMeanDev * 0.99 && + peakDeviations[1].RMSDeviation < currentPeakRMSDev * 0.99 + ) { + newPeak = peakDeviations[1].peak } - } else if ( meanBest > 2 && RMSBest > 2 ) { - if ( peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 ) { - newPeak = peakDeviations[3].peak; + } else if (meanBest > 2 && RMSBest > 2) { + if ( + peakDeviations[3].meanDeviation < currentPeakMeanDev * 0.99 && + peakDeviations[3].RMSDeviation < currentPeakRMSDev * 0.99 + ) { + newPeak = peakDeviations[3].peak } } - if ( newPeak !== peak ) { - console.error("Adjusting insulinPeakTime from",peak,"to",newPeak,"minutes"); + if (newPeak !== peak) { + console.error('Adjusting insulinPeakTime from', peak, 'to', newPeak, 'minutes') } else { - console.error("Leaving insulinPeakTime unchanged at",peak); + console.error('Leaving insulinPeakTime unchanged at', peak) } } - - // Calculate carb ratio (CR) independently of CSF and ISF // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2 // For now, if another meal IOB/COB stacks on top of it, consider them together // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR. - var CRTotalCarbs = 0; - var CRTotalInsulin = 0; - CRData.forEach(function(CRDatum: any) { - var CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG; - var CRInsulinReq = CRBGChange / ISF; + let CRTotalCarbs = 0 + let CRTotalInsulin = 0 + CRData.forEach((CRDatum: any) => { + const CRBGChange = CRDatum.CREndBG - CRDatum.CRInitialBG + const CRInsulinReq = CRBGChange / ISF //var CRIOBChange = CRDatum.CREndIOB - CRDatum.CRInitialIOB; - CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq; + CRDatum.CRInsulinTotal = CRDatum.CRInitialIOB + CRDatum.CRInsulin + CRInsulinReq //console.error(CRDatum.CRInitialIOB, CRDatum.CRInsulin, CRInsulinReq, CRDatum.CRInsulinTotal); //var CR = Math.round( CRDatum.CRCarbs / CRDatum.CRInsulinTotal * 1000 )/1000; //console.error(CRBGChange, CRInsulinReq, CRIOBChange, CRDatum.CRInsulinTotal); //console.error("CRCarbs:",CRDatum.CRCarbs,"CRInsulin:",CRDatum.CRInsulin,"CRDatum.CRInsulinTotal:",CRDatum.CRInsulinTotal,"CR:",CR); if (CRDatum.CRInsulinTotal > 0) { - CRTotalCarbs += CRDatum.CRCarbs; - CRTotalInsulin += CRDatum.CRInsulinTotal; + CRTotalCarbs += CRDatum.CRCarbs + CRTotalInsulin += CRDatum.CRInsulinTotal //console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin); } - }); - CRTotalInsulin = Math.round(CRTotalInsulin*1000)/1000; - var totalCR = Math.round( CRTotalCarbs / CRTotalInsulin * 1000 )/1000; - console.error("CRTotalCarbs:",CRTotalCarbs,"CRTotalInsulin:",CRTotalInsulin,"totalCR:",totalCR); + }) + CRTotalInsulin = Math.round(CRTotalInsulin * 1000) / 1000 + const totalCR = Math.round((CRTotalCarbs / CRTotalInsulin) * 1000) / 1000 + console.error('CRTotalCarbs:', CRTotalCarbs, 'CRTotalInsulin:', CRTotalInsulin, 'totalCR:', totalCR) // convert the basal profile to hourly if it isn't already - var hourlyBasalProfile = []; - var hourlyPumpProfile = []; - for (i=0; i < 24; i++) { + const hourlyBasalProfile = [] + const hourlyPumpProfile = [] + for (i = 0; i < 24; i++) { // autotuned basal profile - for (var j=0; j < basalProfile.length; ++j) { + for (var j = 0; j < basalProfile.length; ++j) { if (basalProfile[j].minutes <= i * 60) { if (basalProfile[j].rate === 0) { - console.error("ERROR: bad basalProfile",basalProfile[j]); - return; + console.error('ERROR: bad basalProfile', basalProfile[j]) + return } - hourlyBasalProfile[i] = JSON.parse(JSON.stringify(basalProfile[j])); + hourlyBasalProfile[i] = JSON.parse(JSON.stringify(basalProfile[j])) } } - hourlyBasalProfile[i].i=i; - hourlyBasalProfile[i].minutes=i*60; - var zeroPadHour = ("000"+i).slice(-2); - hourlyBasalProfile[i].start=zeroPadHour + ":00:00"; - hourlyBasalProfile[i].rate=Math.round(hourlyBasalProfile[i].rate*1000)/1000 + hourlyBasalProfile[i].i = i + hourlyBasalProfile[i].minutes = i * 60 + const zeroPadHour = `000${i}`.slice(-2) + hourlyBasalProfile[i].start = `${zeroPadHour}:00:00` + hourlyBasalProfile[i].rate = Math.round(hourlyBasalProfile[i].rate * 1000) / 1000 // pump basal profile if (pumpBasalProfile && pumpBasalProfile[0]) { - for (j=0; j < pumpBasalProfile.length; ++j) { + for (j = 0; j < pumpBasalProfile.length; ++j) { //console.error(pumpBasalProfile[j]); if (pumpBasalProfile[j].rate === 0) { - console.error("ERROR: bad pumpBasalProfile",pumpBasalProfile[j]); - return; + console.error('ERROR: bad pumpBasalProfile', pumpBasalProfile[j]) + return } if (pumpBasalProfile[j].minutes <= i * 60) { - hourlyPumpProfile[i] = JSON.parse(JSON.stringify(pumpBasalProfile[j])); + hourlyPumpProfile[i] = JSON.parse(JSON.stringify(pumpBasalProfile[j])) } } - hourlyPumpProfile[i].i=i; - hourlyPumpProfile[i].minutes=i*60; - hourlyPumpProfile[i].rate=Math.round(hourlyPumpProfile[i].rate*1000)/1000 + hourlyPumpProfile[i].i = i + hourlyPumpProfile[i].minutes = i * 60 + hourlyPumpProfile[i].rate = Math.round(hourlyPumpProfile[i].rate * 1000) / 1000 } } //console.error(hourlyPumpProfile); //console.error(hourlyBasalProfile); - var newHourlyBasalProfile = JSON.parse(JSON.stringify(hourlyBasalProfile)); + const newHourlyBasalProfile = JSON.parse(JSON.stringify(hourlyBasalProfile)) // look at net deviations for each hour - for (var hour=0; hour < 24; hour++) { - let deviations = 0; - for (i=0; i < basalGlucose.length; ++i) { - var BGTime; + for (var hour = 0; hour < 24; hour++) { + let deviations = 0 + for (i = 0; i < basalGlucose.length; ++i) { + var BGTime if (basalGlucose[i].date) { - BGTime = new Date(basalGlucose[i].date); + BGTime = new Date(basalGlucose[i].date) } else if (basalGlucose[i].displayTime) { - BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')); + BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')) } else if (basalGlucose[i].dateString) { - BGTime = new Date(basalGlucose[i].dateString); + BGTime = new Date(basalGlucose[i].dateString) } else { - console.error("Could not determine last BG time"); - continue; + console.error('Could not determine last BG time') + continue } - var myHour = BGTime.getHours(); + const myHour = BGTime.getHours() if (hour === myHour) { //console.error(basalGlucose[i].deviation); - deviations += parseFloat(basalGlucose[i].deviation); + deviations += parseFloat(basalGlucose[i].deviation) } } - deviations = Math.round( deviations * 1000 ) / 1000 - console.error("Hour",hour.toString(),"total deviations:",deviations,"mg/dL"); + deviations = Math.round(deviations * 1000) / 1000 + console.error('Hour', hour.toString(), 'total deviations:', deviations, 'mg/dL') // calculate how much less or additional basal insulin would have been required to eliminate the deviations // only apply 20% of the needed adjustment to keep things relatively stable - var basalNeeded = 0.2 * deviations / ISF; - basalNeeded = Math.round( basalNeeded * 100 ) / 100 + let basalNeeded = (0.2 * deviations) / ISF + basalNeeded = Math.round(basalNeeded * 100) / 100 // if basalNeeded is positive, adjust each of the 1-3 hour prior basals by 10% of the needed adjustment - console.error("Hour",hour,"basal adjustment needed:",basalNeeded,"U/hr"); - if (basalNeeded > 0 ) { - for (var offset=-3; offset < 0; offset++) { - var offsetHour = hour + offset; - if (offsetHour < 0) { offsetHour += 24; } + console.error('Hour', hour, 'basal adjustment needed:', basalNeeded, 'U/hr') + if (basalNeeded > 0) { + for (var offset = -3; offset < 0; offset++) { + var offsetHour = hour + offset + if (offsetHour < 0) { + offsetHour += 24 + } //console.error(offsetHour); - newHourlyBasalProfile[offsetHour].rate += basalNeeded / 3; - newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000 + newHourlyBasalProfile[offsetHour].rate += basalNeeded / 3 + newHourlyBasalProfile[offsetHour].rate = + Math.round(newHourlyBasalProfile[offsetHour].rate * 1000) / 1000 } - // otherwise, figure out the percentage reduction required to the 1-3 hour prior basals - // and adjust all of them downward proportionally + // otherwise, figure out the percentage reduction required to the 1-3 hour prior basals + // and adjust all of them downward proportionally } else if (basalNeeded < 0) { - var threeHourBasal = 0; - for (offset=-3; offset < 0; offset++) { - offsetHour = hour + offset; - if (offsetHour < 0) { offsetHour += 24; } - threeHourBasal += newHourlyBasalProfile[offsetHour].rate; + let threeHourBasal = 0 + for (offset = -3; offset < 0; offset++) { + offsetHour = hour + offset + if (offsetHour < 0) { + offsetHour += 24 + } + threeHourBasal += newHourlyBasalProfile[offsetHour].rate } - var adjustmentRatio = 1.0 + basalNeeded / threeHourBasal; + const adjustmentRatio = 1.0 + basalNeeded / threeHourBasal //console.error(adjustmentRatio); - for (offset=-3; offset < 0; offset++) { - offsetHour = hour + offset; - if (offsetHour < 0) { offsetHour += 24; } - newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio; - newHourlyBasalProfile[offsetHour].rate=Math.round(newHourlyBasalProfile[offsetHour].rate*1000)/1000 + for (offset = -3; offset < 0; offset++) { + offsetHour = hour + offset + if (offsetHour < 0) { + offsetHour += 24 + } + newHourlyBasalProfile[offsetHour].rate = newHourlyBasalProfile[offsetHour].rate * adjustmentRatio + newHourlyBasalProfile[offsetHour].rate = + Math.round(newHourlyBasalProfile[offsetHour].rate * 1000) / 1000 } } } - var autotuneMin: number = 0.7 - var autotuneMax: number = 1.2 - + let autotuneMin: number = 0.7 + let autotuneMax: number = 1.2 + if (pumpBasalProfile && pumpBasalProfile[0]) { - for (hour=0; hour < 24; hour++) { + for (hour = 0; hour < 24; hour++) { //console.error(newHourlyBasalProfile[hour],hourlyPumpProfile[hour].rate*1.2); // cap adjustments at autosens_max and autosens_min - + if (typeof pumpProfile.autosens_max !== 'undefined') { - autotuneMax = pumpProfile.autosens_max; + autotuneMax = pumpProfile.autosens_max } if (typeof pumpProfile.autosens_min !== 'undefined') { - autotuneMin = pumpProfile.autosens_min; + autotuneMin = pumpProfile.autosens_min } - var maxRate = hourlyPumpProfile[hour].rate * autotuneMax; - var minRate = hourlyPumpProfile[hour].rate * autotuneMin; - if (newHourlyBasalProfile[hour].rate > maxRate ) { - console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is",autotuneMax,"* pump basal of",hourlyPumpProfile[hour].rate,")"); + const maxRate = hourlyPumpProfile[hour].rate * autotuneMax + const minRate = hourlyPumpProfile[hour].rate * autotuneMin + if (newHourlyBasalProfile[hour].rate > maxRate) { + console.error( + 'Limiting hour', + hour, + 'basal to', + maxRate.toFixed(2), + '(which is', + autotuneMax, + '* pump basal of', + hourlyPumpProfile[hour].rate, + ')' + ) //console.error("Limiting hour",hour,"basal to",maxRate.toFixed(2),"(which is 20% above pump basal of",hourlyPumpProfile[hour].rate,")"); - newHourlyBasalProfile[hour].rate = maxRate; - } else if (newHourlyBasalProfile[hour].rate < minRate ) { - console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is",autotuneMin,"* pump basal of",hourlyPumpProfile[hour].rate,")"); + newHourlyBasalProfile[hour].rate = maxRate + } else if (newHourlyBasalProfile[hour].rate < minRate) { + console.error( + 'Limiting hour', + hour, + 'basal to', + minRate.toFixed(2), + '(which is', + autotuneMin, + '* pump basal of', + hourlyPumpProfile[hour].rate, + ')' + ) //console.error("Limiting hour",hour,"basal to",minRate.toFixed(2),"(which is 20% below pump basal of",hourlyPumpProfile[hour].rate,")"); - newHourlyBasalProfile[hour].rate = minRate; + newHourlyBasalProfile[hour].rate = minRate } - newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate*1000)/1000; + newHourlyBasalProfile[hour].rate = Math.round(newHourlyBasalProfile[hour].rate * 1000) / 1000 } } @@ -302,33 +345,55 @@ function tuneAllTheThings (inputs: any) { // when no adjustments are needed to a particular hour, we should adjust it toward the average of the // periods before and after it that do have data to be tuned - var lastAdjustedHour = 0; + let lastAdjustedHour = 0 // scan through newHourlyBasalProfile and find hours where the rate is unchanged - for (hour=0; hour < 24; hour++) { + for (hour = 0; hour < 24; hour++) { if (hourlyBasalProfile[hour].rate === newHourlyBasalProfile[hour].rate) { - var nextAdjustedHour = 23; - for (var nextHour = hour; nextHour < 24; nextHour++) { - if (! (hourlyBasalProfile[nextHour].rate === newHourlyBasalProfile[nextHour].rate)) { - nextAdjustedHour = nextHour; - break; - //} else { + let nextAdjustedHour = 23 + for (let nextHour = hour; nextHour < 24; nextHour++) { + if (!(hourlyBasalProfile[nextHour].rate === newHourlyBasalProfile[nextHour].rate)) { + nextAdjustedHour = nextHour + break + //} else { //console.error(nextHour, hourlyBasalProfile[nextHour].rate, newHourlyBasalProfile[nextHour].rate); } } //console.error(hour, newHourlyBasalProfile); - newHourlyBasalProfile[hour].rate = Math.round( (0.8*hourlyBasalProfile[hour].rate + 0.1*newHourlyBasalProfile[lastAdjustedHour].rate + 0.1*newHourlyBasalProfile[nextAdjustedHour].rate)*1000 )/1000; - if (newHourlyBasalProfile[hour].untuned) - newHourlyBasalProfile[hour].untuned++; - else - newHourlyBasalProfile[hour].untuned = 1; - console.error("Adjusting hour",hour,"basal from",hourlyBasalProfile[hour].rate,"to",newHourlyBasalProfile[hour].rate,"based on hour",lastAdjustedHour,"=",newHourlyBasalProfile[lastAdjustedHour].rate,"and hour",nextAdjustedHour,"=",newHourlyBasalProfile[nextAdjustedHour].rate); + newHourlyBasalProfile[hour].rate = + Math.round( + (0.8 * hourlyBasalProfile[hour].rate + + 0.1 * newHourlyBasalProfile[lastAdjustedHour].rate + + 0.1 * newHourlyBasalProfile[nextAdjustedHour].rate) * + 1000 + ) / 1000 + if (newHourlyBasalProfile[hour].untuned) { + newHourlyBasalProfile[hour].untuned++ + } else { + newHourlyBasalProfile[hour].untuned = 1 + } + console.error( + 'Adjusting hour', + hour, + 'basal from', + hourlyBasalProfile[hour].rate, + 'to', + newHourlyBasalProfile[hour].rate, + 'based on hour', + lastAdjustedHour, + '=', + newHourlyBasalProfile[lastAdjustedHour].rate, + 'and hour', + nextAdjustedHour, + '=', + newHourlyBasalProfile[nextAdjustedHour].rate + ) } else { - lastAdjustedHour = hour; + lastAdjustedHour = hour } } - console.error(newHourlyBasalProfile); - basalProfile = newHourlyBasalProfile; + console.error(newHourlyBasalProfile) + basalProfile = newHourlyBasalProfile // Calculate carb ratio (CR) independently of CSF and ISF // Use the time period from meal bolus/carbs until COB is zero and IOB is < currentBasal/2 @@ -336,222 +401,338 @@ function tuneAllTheThings (inputs: any) { // Compare beginning and ending BGs, and calculate how much more/less insulin is needed to neutralize // Use entered carbs vs. starting IOB + delivered insulin + needed-at-end insulin to directly calculate CR. - - // calculate net deviations while carbs are absorbing // measured from carb entry until COB and deviations both drop to zero - var deviations = 0; - var mealCarbs = 0; - var totalMealCarbs = 0; - var totalDeviations = 0; - var fullNewCSF; + let deviations = 0 + let mealCarbs = 0 + let totalMealCarbs = 0 + let totalDeviations = 0 + let fullNewCSF //console.error(CSFGlucose[0].mealAbsorption); //console.error(CSFGlucose[0]); - for (i=0; i < CSFGlucose.length; ++i) { + for (i = 0; i < CSFGlucose.length; ++i) { //console.error(CSFGlucose[i].mealAbsorption, i); - if ( CSFGlucose[i].mealAbsorption === "start" ) { - deviations = 0; - mealCarbs = parseInt(CSFGlucose[i].mealCarbs); - } else if (CSFGlucose[i].mealAbsorption === "end") { - deviations += parseFloat(CSFGlucose[i].deviation); + if (CSFGlucose[i].mealAbsorption === 'start') { + deviations = 0 + mealCarbs = parseInt(CSFGlucose[i].mealCarbs) + } else if (CSFGlucose[i].mealAbsorption === 'end') { + deviations += parseFloat(CSFGlucose[i].deviation) // compare the sum of deviations from start to end vs. current CSF * mealCarbs //console.error(CSF,mealCarbs); //var csfRise = CSF * mealCarbs; //console.error(deviations,ISF); //console.error("csfRise:",csfRise,"deviations:",deviations); - totalMealCarbs += mealCarbs; - totalDeviations += deviations; - + totalMealCarbs += mealCarbs + totalDeviations += deviations } else { - deviations += Math.max(0*previousAutotune.min_5m_carbimpact,parseFloat(CSFGlucose[i].deviation)); - mealCarbs = Math.max(mealCarbs, parseInt(CSFGlucose[i].mealCarbs)); + deviations += Math.max(0 * previousAutotune.min_5m_carbimpact, parseFloat(CSFGlucose[i].deviation)) + mealCarbs = Math.max(mealCarbs, parseInt(CSFGlucose[i].mealCarbs)) } } // at midnight, write down the mealcarbs as total meal carbs (to prevent special case of when only one meal and it not finishing absorbing by midnight) // TODO: figure out what to do with dinner carbs that don't finish absorbing by midnight - if (totalMealCarbs === 0) { totalMealCarbs += mealCarbs; } - if (totalDeviations === 0) { totalDeviations += deviations; } + if (totalMealCarbs === 0) { + totalMealCarbs += mealCarbs + } + if (totalDeviations === 0) { + totalDeviations += deviations + } //console.error(totalDeviations, totalMealCarbs); if (totalMealCarbs === 0) { // if no meals today, CSF is unchanged - fullNewCSF = CSF; + fullNewCSF = CSF } else { // how much change would be required to account for all of the deviations - fullNewCSF = Math.round( (totalDeviations / totalMealCarbs)*100 )/100; + fullNewCSF = Math.round((totalDeviations / totalMealCarbs) * 100) / 100 } // only adjust by 20% - var newCSF = ( 0.8 * CSF ) + ( 0.2 * fullNewCSF ); + let newCSF = 0.8 * CSF + 0.2 * fullNewCSF // safety cap CSF - if (typeof(pumpCSF) !== 'undefined') { - var maxCSF = pumpCSF * autotuneMax; - var minCSF = pumpCSF * autotuneMin; + if (typeof pumpCSF !== 'undefined') { + const maxCSF = pumpCSF * autotuneMax + const minCSF = pumpCSF * autotuneMin if (newCSF > maxCSF) { - console.error("Limiting CSF to",maxCSF.toFixed(2),"(which is",autotuneMax,"* pump CSF of",pumpCSF,")"); - newCSF = maxCSF; + console.error('Limiting CSF to', maxCSF.toFixed(2), '(which is', autotuneMax, '* pump CSF of', pumpCSF, ')') + newCSF = maxCSF } else if (newCSF < minCSF) { - console.error("Limiting CSF to",minCSF.toFixed(2),"(which is",autotuneMin,"* pump CSF of",pumpCSF,")"); - newCSF = minCSF; + console.error('Limiting CSF to', minCSF.toFixed(2), '(which is', autotuneMin, '* pump CSF of', pumpCSF, ')') + newCSF = minCSF } //else { console.error("newCSF",newCSF,"is close enough to",pumpCSF); } } - var oldCSF = Math.round( CSF * 1000 ) / 1000; - newCSF = Math.round( newCSF * 1000 ) / 1000; - totalDeviations = Math.round ( totalDeviations * 1000 )/1000; - console.error("totalMealCarbs:",totalMealCarbs,"totalDeviations:",totalDeviations,"oldCSF",oldCSF,"fullNewCSF:",fullNewCSF,"newCSF:",newCSF); + const oldCSF = Math.round(CSF * 1000) / 1000 + newCSF = Math.round(newCSF * 1000) / 1000 + totalDeviations = Math.round(totalDeviations * 1000) / 1000 + console.error( + 'totalMealCarbs:', + totalMealCarbs, + 'totalDeviations:', + totalDeviations, + 'oldCSF', + oldCSF, + 'fullNewCSF:', + fullNewCSF, + 'newCSF:', + newCSF + ) // this is where CSF is set based on the outputs if (newCSF) { - CSF = newCSF; + CSF = newCSF } if (totalCR === 0) { // if no meals today, CR is unchanged - var fullNewCR = carbRatio; + var fullNewCR = carbRatio } else { // how much change would be required to account for all of the deviations - fullNewCR = totalCR; + fullNewCR = totalCR } // don't tune CR out of bounds - var maxCR = pumpCarbRatio * autotuneMax; - if (maxCR > 150) { maxCR = 150 } - var minCR = pumpCarbRatio * autotuneMin; - if (minCR < 3) { minCR = 3 } + let maxCR = pumpCarbRatio * autotuneMax + if (maxCR > 150) { + maxCR = 150 + } + let minCR = pumpCarbRatio * autotuneMin + if (minCR < 3) { + minCR = 3 + } // safety cap fullNewCR - if (typeof(pumpCarbRatio) !== 'undefined') { + if (typeof pumpCarbRatio !== 'undefined') { if (fullNewCR > maxCR) { - console.error("Limiting fullNewCR from",fullNewCR,"to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")"); - fullNewCR = maxCR; + console.error( + 'Limiting fullNewCR from', + fullNewCR, + 'to', + maxCR.toFixed(2), + '(which is', + autotuneMax, + '* pump CR of', + pumpCarbRatio, + ')' + ) + fullNewCR = maxCR } else if (fullNewCR < minCR) { - console.error("Limiting fullNewCR from",fullNewCR,"to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")"); - fullNewCR = minCR; + console.error( + 'Limiting fullNewCR from', + fullNewCR, + 'to', + minCR.toFixed(2), + '(which is', + autotuneMin, + '* pump CR of', + pumpCarbRatio, + ')' + ) + fullNewCR = minCR } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); } } // only adjust by 20% - var newCR = ( 0.8 * carbRatio ) + ( 0.2 * fullNewCR ); + let newCR = 0.8 * carbRatio + 0.2 * fullNewCR // safety cap newCR - if (typeof(pumpCarbRatio) !== 'undefined') { + if (typeof pumpCarbRatio !== 'undefined') { if (newCR > maxCR) { - console.error("Limiting CR to",maxCR.toFixed(2),"(which is",autotuneMax,"* pump CR of",pumpCarbRatio,")"); - newCR = maxCR; + console.error( + 'Limiting CR to', + maxCR.toFixed(2), + '(which is', + autotuneMax, + '* pump CR of', + pumpCarbRatio, + ')' + ) + newCR = maxCR } else if (newCR < minCR) { - console.error("Limiting CR to",minCR.toFixed(2),"(which is",autotuneMin,"* pump CR of",pumpCarbRatio,")"); - newCR = minCR; + console.error( + 'Limiting CR to', + minCR.toFixed(2), + '(which is', + autotuneMin, + '* pump CR of', + pumpCarbRatio, + ')' + ) + newCR = minCR } //else { console.error("newCR",newCR,"is close enough to",pumpCarbRatio); } } - newCR = Math.round( newCR * 1000 ) / 1000; - console.error("oldCR:",carbRatio,"fullNewCR:",fullNewCR,"newCR:",newCR); + newCR = Math.round(newCR * 1000) / 1000 + console.error('oldCR:', carbRatio, 'fullNewCR:', fullNewCR, 'newCR:', newCR) // this is where CR is set based on the outputs //var ISFFromCRAndCSF = ISF; if (newCR) { - carbRatio = newCR; + carbRatio = newCR //ISFFromCRAndCSF = Math.round( carbRatio * CSF * 1000)/1000; } - - // calculate median deviation and bgi in data attributable to ISF - let deviationsArray: number[] = []; - var BGIs = []; - var avgDeltas = []; - var ratios = []; - for (i=0; i < ISFGlucose.length; ++i) { - var deviation = parseFloat(ISFGlucose[i].deviation); - deviationsArray.push(deviation); - var BGI = parseFloat(ISFGlucose[i].BGI); - BGIs.push(BGI); - var avgDelta = parseFloat(ISFGlucose[i].avgDelta); - avgDeltas.push(avgDelta); - var ratio = 1 + deviation / BGI; + const deviationsArray: number[] = [] + const BGIs = [] + const avgDeltas = [] + const ratios = [] + for (i = 0; i < ISFGlucose.length; ++i) { + const deviation = parseFloat(ISFGlucose[i].deviation) + deviationsArray.push(deviation) + const BGI = parseFloat(ISFGlucose[i].BGI) + BGIs.push(BGI) + const avgDelta = parseFloat(ISFGlucose[i].avgDelta) + avgDeltas.push(avgDelta) + const ratio = 1 + deviation / BGI //console.error("Deviation:",deviation,"BGI:",BGI,"avgDelta:",avgDelta,"ratio:",ratio); - ratios.push(ratio); + ratios.push(ratio) } - avgDeltas.sort(function(a: number, b: number){return a-b}); - BGIs.sort(function(a: number, b: number){return a-b}); - deviationsArray.sort(function(a: number, b: number){return a-b}); - ratios.sort(function(a: number, b: number){return a-b}); - var p50deviation = percentile(deviationsArray, 0.50); - var p50BGI = percentile(BGIs, 0.50); - var p50ratios = Math.round( percentile(ratios, 0.50) * 1000)/1000; - var fullNewISF = ISF; + avgDeltas.sort((a: number, b: number) => { + return a - b + }) + BGIs.sort((a: number, b: number) => { + return a - b + }) + deviationsArray.sort((a: number, b: number) => { + return a - b + }) + ratios.sort((a: number, b: number) => { + return a - b + }) + let p50deviation = percentile(deviationsArray, 0.5) + let p50BGI = percentile(BGIs, 0.5) + const p50ratios = Math.round(percentile(ratios, 0.5) * 1000) / 1000 + let fullNewISF = ISF if (ISFGlucose.length < 10) { // leave ISF unchanged if fewer than 10 ISF data points - console.error ("Only found",ISFGlucose.length,"ISF data points, leaving ISF unchanged at",ISF); + console.error('Only found', ISFGlucose.length, 'ISF data points, leaving ISF unchanged at', ISF) } else { // calculate what adjustments to ISF would have been necessary to bring median deviation to zero - fullNewISF = ISF * p50ratios; + fullNewISF = ISF * p50ratios } - fullNewISF = Math.round( fullNewISF * 1000 ) / 1000; + fullNewISF = Math.round(fullNewISF * 1000) / 1000 //console.error("p50ratios:",p50ratios,"fullNewISF:",fullNewISF,ratios[Math.floor(ratios.length/2)]); // adjust the target ISF to be a weighted average of fullNewISF and pumpISF - var adjustmentFraction; + let adjustmentFraction - if (typeof(pumpProfile.autotune_isf_adjustmentFraction) !== 'undefined') { - adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction; + if (typeof pumpProfile.autotune_isf_adjustmentFraction !== 'undefined') { + adjustmentFraction = pumpProfile.autotune_isf_adjustmentFraction } else { - adjustmentFraction = 1.0; + adjustmentFraction = 1.0 } // low autosens ratio = high ISF - var maxISF = pumpISF / autotuneMin; + const maxISF = pumpISF / autotuneMin // high autosens ratio = low ISF - var minISF = pumpISF / autotuneMax; - var newISF = ISF; - if (typeof(pumpISF) !== 'undefined') { - if ( fullNewISF < 0 ) { - var adjustedISF = ISF; + const minISF = pumpISF / autotuneMax + let newISF = ISF + if (typeof pumpISF !== 'undefined') { + if (fullNewISF < 0) { + var adjustedISF = ISF } else { - adjustedISF = adjustmentFraction*fullNewISF + (1-adjustmentFraction)*pumpISF; + adjustedISF = adjustmentFraction * fullNewISF + (1 - adjustmentFraction) * pumpISF } // cap adjustedISF before applying 10% //console.error(adjustedISF, maxISF, minISF); if (adjustedISF > maxISF) { - console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")"); - adjustedISF = maxISF; + console.error( + 'Limiting adjusted ISF of', + adjustedISF.toFixed(2), + 'to', + maxISF.toFixed(2), + '(which is pump ISF of', + pumpISF, + '/', + autotuneMin, + ')' + ) + adjustedISF = maxISF } else if (adjustedISF < minISF) { - console.error("Limiting adjusted ISF of",adjustedISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")"); - adjustedISF = minISF; + console.error( + 'Limiting adjusted ISF of', + adjustedISF.toFixed(2), + 'to', + minISF.toFixed(2), + '(which is pump ISF of', + pumpISF, + '/', + autotuneMax, + ')' + ) + adjustedISF = minISF } // and apply 20% of that adjustment - newISF = ( 0.8 * ISF ) + ( 0.2 * adjustedISF ); + newISF = 0.8 * ISF + 0.2 * adjustedISF if (newISF > maxISF) { - console.error("Limiting ISF of",newISF.toFixed(2),"to",maxISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMin,")"); - newISF = maxISF; + console.error( + 'Limiting ISF of', + newISF.toFixed(2), + 'to', + maxISF.toFixed(2), + '(which is pump ISF of', + pumpISF, + '/', + autotuneMin, + ')' + ) + newISF = maxISF } else if (newISF < minISF) { - console.error("Limiting ISF of",newISF.toFixed(2),"to",minISF.toFixed(2),"(which is pump ISF of",pumpISF,"/",autotuneMax,")"); - newISF = minISF; + console.error( + 'Limiting ISF of', + newISF.toFixed(2), + 'to', + minISF.toFixed(2), + '(which is pump ISF of', + pumpISF, + '/', + autotuneMax, + ')' + ) + newISF = minISF } } - newISF = Math.round( newISF * 1000 ) / 1000; + newISF = Math.round(newISF * 1000) / 1000 //console.error(avgRatio); //console.error(newISF); - p50deviation = Math.round( p50deviation * 1000 ) / 1000; - p50BGI = Math.round( p50BGI * 1000 ) / 1000; - adjustedISF = Math.round( adjustedISF * 1000 ) / 1000; - console.error("p50deviation:",p50deviation,"p50BGI",p50BGI,"p50ratios:",p50ratios,"Old ISF:",ISF,"fullNewISF:",fullNewISF,"adjustedISF:",adjustedISF,"newISF:",newISF,"newDIA:",newDIA,"newPeak:",newPeak); + p50deviation = Math.round(p50deviation * 1000) / 1000 + p50BGI = Math.round(p50BGI * 1000) / 1000 + adjustedISF = Math.round(adjustedISF * 1000) / 1000 + console.error( + 'p50deviation:', + p50deviation, + 'p50BGI', + p50BGI, + 'p50ratios:', + p50ratios, + 'Old ISF:', + ISF, + 'fullNewISF:', + fullNewISF, + 'adjustedISF:', + adjustedISF, + 'newISF:', + newISF, + 'newDIA:', + newDIA, + 'newPeak:', + newPeak + ) if (newISF) { - ISF = newISF; + ISF = newISF } - // reconstruct updated version of previousAutotune as autotuneOutput - var autotuneOutput = previousAutotune; - autotuneOutput.basalprofile = basalProfile; - isfProfile.sensitivities[0].sensitivity = ISF; - autotuneOutput.isfProfile = isfProfile; - autotuneOutput.sens = ISF; - autotuneOutput.csf = CSF; + const autotuneOutput = previousAutotune + autotuneOutput.basalprofile = basalProfile + isfProfile.sensitivities[0].sensitivity = ISF + autotuneOutput.isfProfile = isfProfile + autotuneOutput.sens = ISF + autotuneOutput.csf = CSF //carbRatio = ISF / CSF; - carbRatio = Math.round( carbRatio * 1000 ) / 1000; - autotuneOutput.carb_ratio = carbRatio; - autotuneOutput.dia = newDIA; - autotuneOutput.insulinPeakTime = newPeak; + carbRatio = Math.round(carbRatio * 1000) / 1000 + autotuneOutput.carb_ratio = carbRatio + autotuneOutput.dia = newDIA + autotuneOutput.insulinPeakTime = newPeak if (diaDeviations || peakDeviations) { - autotuneOutput.useCustomPeakTime = true; + autotuneOutput.useCustomPeakTime = true } - return autotuneOutput; + return autotuneOutput } -exports = module.exports = tuneAllTheThings; +exports = module.exports = tuneAllTheThings diff --git a/lib/basal-set-temp.ts b/lib/basal-set-temp.ts index fd38cb7a2..94184308a 100644 --- a/lib/basal-set-temp.ts +++ b/lib/basal-set-temp.ts @@ -1,4 +1,4 @@ -import { Profile } from "./types/Profile"; +import type { Profile } from './types/Profile' interface RT { reason?: string @@ -12,61 +12,79 @@ interface Temp { } function reason(rT: RT, msg: string) { - rT.reason = (rT.reason ? rT.reason + '. ' : '') + msg; - console.error(msg); + rT.reason = (rT.reason ? `${rT.reason}. ` : '') + msg + console.error(msg) } export function getMaxSafeBasal(profile: Profile) { + const max_daily_safety_multiplier = profile.max_daily_safety_multiplier || 3 + const current_basal_safety_multiplier = profile.current_basal_safety_multiplier || 4 - var max_daily_safety_multiplier = profile.max_daily_safety_multiplier || 3; - var current_basal_safety_multiplier = profile.current_basal_safety_multiplier || 4; - - return Math.min(profile.max_basal || 0, max_daily_safety_multiplier * (profile.max_daily_basal || 0), current_basal_safety_multiplier * (profile.current_basal || 0)); -}; + return Math.min( + profile.max_basal || 0, + max_daily_safety_multiplier * (profile.max_daily_basal || 0), + current_basal_safety_multiplier * (profile.current_basal || 0) + ) +} export function setTempBasal(rate: number, duration: number, profile: Profile, rT: RT, currenttemp?: Temp) { //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); - var maxSafeBasal = getMaxSafeBasal(profile); - var round_basal = require('./round-basal'); + const maxSafeBasal = getMaxSafeBasal(profile) + const round_basal = require('./round-basal') if (rate < 0) { - rate = 0; + rate = 0 } else if (rate > maxSafeBasal) { - rate = maxSafeBasal; + rate = maxSafeBasal } - var suggestedRate = round_basal(rate, profile); - if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && typeof(currenttemp.rate) !== 'undefined' && currenttemp.duration > (duration-10) && currenttemp.duration <= 120 && suggestedRate <= currenttemp.rate * 1.2 && suggestedRate >= currenttemp.rate * 0.8 && duration > 0 ) { - rT.reason += " "+currenttemp.duration+"m left and " + currenttemp.rate + " ~ req " + suggestedRate + "U/hr: no temp required"; - return rT; + const suggestedRate = round_basal(rate, profile) + if ( + typeof currenttemp !== 'undefined' && + typeof currenttemp.duration !== 'undefined' && + typeof currenttemp.rate !== 'undefined' && + currenttemp.duration > duration - 10 && + currenttemp.duration <= 120 && + suggestedRate <= currenttemp.rate * 1.2 && + suggestedRate >= currenttemp.rate * 0.8 && + duration > 0 + ) { + rT.reason += ` ${currenttemp.duration}m left and ${currenttemp.rate} ~ req ${ + suggestedRate + }U/hr: no temp required` + return rT } if (suggestedRate === profile.current_basal) { - if (profile.skip_neutral_temps === true) { - if (typeof(currenttemp) !== 'undefined' && typeof(currenttemp.duration) !== 'undefined' && currenttemp.duration > 0) { - reason(rT, 'Suggested rate is same as profile rate, a temp basal is active, canceling current temp'); - rT.duration = 0; - rT.rate = 0; - return rT; + if (profile.skip_neutral_temps === true) { + if ( + typeof currenttemp !== 'undefined' && + typeof currenttemp.duration !== 'undefined' && + currenttemp.duration > 0 + ) { + reason(rT, 'Suggested rate is same as profile rate, a temp basal is active, canceling current temp') + rT.duration = 0 + rT.rate = 0 + return rT + } else { + reason(rT, 'Suggested rate is same as profile rate, no temp basal is active, doing nothing') + return rT + } } else { - reason(rT, 'Suggested rate is same as profile rate, no temp basal is active, doing nothing'); - return rT; + reason(rT, `Setting neutral temp basal of ${profile.current_basal}U/hr`) + rT.duration = duration + rT.rate = suggestedRate + return rT } - } else { - reason(rT, 'Setting neutral temp basal of ' + profile.current_basal + 'U/hr'); - rT.duration = duration; - rT.rate = suggestedRate; - return rT; - } } else { - rT.duration = duration; - rT.rate = suggestedRate; - return rT; + rT.duration = duration + rT.rate = suggestedRate + return rT } -}; +} module.exports = { getMaxSafeBasal, - setTempBasal -}; + setTempBasal, +} diff --git a/lib/bolus.js b/lib/bolus.js index b46d3fc5c..46f94f53b 100644 --- a/lib/bolus.js +++ b/lib/bolus.js @@ -1,181 +1,186 @@ -'use strict'; +'use strict' -function reduce (treatments) { +function reduce(treatments) { + const results = [] - var results = [ ]; + let state = {} + const previous = [] - var state = { }; - var previous = [ ]; - - function in_previous (ev) { - var found = false; - previous.forEach(function (elem) { - if (elem.timestamp === ev.timestamp && ev._type === elem._type) { - found = true; - } - }); - - return found; - } - - function within_minutes_from (origin, tail, minutes) { - var ms = minutes * 1000 * 60; - var ts = Date.parse(origin.timestamp); - return /* candidates */ tail.slice( ).filter(function (elem) { - var dt = Date.parse(elem.timestamp); - return ts - dt <= ms; - }); - } + function in_previous(ev) { + let found = false + previous.forEach(elem => { + if (elem.timestamp === ev.timestamp && ev._type === elem._type) { + found = true + } + }) - function bolus (ev, remaining) { - if (!ev) { console.error('XXX', ev, remaining); return; } - if (ev._type === 'BolusWizard') { - state.carbs = ev.carb_input.toString( ); - state.ratio = ev.carb_ratio.toString( ); - if (ev.bg) { - state.bg = ev.bg.toString( ); - state.glucose = ev.bg.toString( ); - state.glucoseType = ev._type; - } - state.wizard = ev; - state.created_at = state.timestamp = ev.timestamp; - previous.push(ev); + return found } - if (ev._type === 'Bolus') { - state.duration = ev.duration.toString( ); - // if (state.square || state.bolus) { } - // state.insulin = (state.insulin ? state.insulin : 0) + ev.amount; - if (ev.duration && ev.duration > 0) { - state.square = ev; - } else { - if (state.bolus) { - state.bolus.amount = state.bolus.amount + ev.amount; - } else - state.bolus = ev; - } - state.created_at = state.timestamp = ev.timestamp; - previous.push(ev); + function within_minutes_from(origin, tail, minutes) { + const ms = minutes * 1000 * 60 + const ts = Date.parse(origin.timestamp) + return /* candidates */ tail.slice().filter(elem => { + const dt = Date.parse(elem.timestamp) + return ts - dt <= ms + }) } - if (remaining && remaining.length > 0) { - if (state.bolus && state.wizard) { - // skip to end - return bolus({}, []); - } - // keep recursing - return bolus(remaining[0], remaining.slice(1)); - } else { - // console.error("state", state); - // console.error("remaining", remaining); - state.eventType = ''; - - state.insulin = (state.insulin ? state.insulin : 0) + (state.square ? state.square.amount : 0) + - (state.bolus ? state.bolus.amount : 0); - var has_insulin = state.insulin && state.insulin > 0; - var has_carbs = state.carbs && state.carbs > 0; - if (state.square && state.bolus) { - annotate("DualWave bolus for", state.square.duration, "minutes"); - } else if (state.square && state.wizard) { - annotate("Square wave bolus for", state.square.duration, "minutes"); - } else if (state.square) { - annotate("Solo Square wave bolus for", state.square.duration, "minutes"); - annotate("No bolus wizard used."); - } else if (state.bolus && state.wizard) { - annotate("Normal bolus with wizard."); - } else if (state.bolus) { - annotate("Normal bolus (solo, no bolus wizard)."); - } - - if (has_insulin) { - var iobFile = "./monitor/iob.json"; - var fs = require('fs'); - if (fs.existsSync(iobFile)) { - var iob = JSON.parse(fs.readFileSync(iobFile)); - if (iob && Array.isArray(iob) && iob.length) { - annotate("Calculated IOB:", iob[0].iob); + function bolus(ev, remaining) { + if (!ev) { + console.error('XXX', ev, remaining) + return + } + if (ev._type === 'BolusWizard') { + state.carbs = ev.carb_input.toString() + state.ratio = ev.carb_ratio.toString() + if (ev.bg) { + state.bg = ev.bg.toString() + state.glucose = ev.bg.toString() + state.glucoseType = ev._type } + state.wizard = ev + state.created_at = state.timestamp = ev.timestamp + previous.push(ev) } - } - if (state.bolus) { - annotate("Programmed bolus", state.bolus.programmed); - annotate("Delivered bolus", state.bolus.amount); - annotate("Percent delivered: ", (state.bolus.amount/state.bolus.programmed * 100).toString( ) + '%'); - } - if (state.square) { - annotate("Programmed square", state.square.programmed); - annotate("Delivered square", state.square.amount); - annotate("Success: ", (state.square.amount/state.square.programmed * 100).toString( ) + '%'); - } - if (state.wizard) { - state.created_at = state.wizard.timestamp; - annotate("Food estimate", state.wizard.food_estimate); - annotate("Correction estimate", state.wizard.correction_estimate); - annotate("Bolus estimate", state.wizard.bolus_estimate); - annotate("Target low", state.wizard.bg_target_low); - annotate("Target high", state.wizard.bg_target_high); - var delta = state.wizard.sensitivity * state.insulin * -1; - annotate("Hypothetical glucose delta", delta); - if (state.bg && state.bg > 0) { - annotate('Glucose was:', state.bg); - // state.glucose = state.bg; - // TODO: annotate prediction - } - } - if (has_carbs && has_insulin) { - state.eventType = 'Meal Bolus'; - } else { - if (has_carbs && !has_insulin) { - state.eventType = 'Carb Correction'; - } - if (!has_carbs && has_insulin) { - state.eventType = 'Correction Bolus'; + if (ev._type === 'Bolus') { + state.duration = ev.duration.toString() + // if (state.square || state.bolus) { } + // state.insulin = (state.insulin ? state.insulin : 0) + ev.amount; + if (ev.duration && ev.duration > 0) { + state.square = ev + } else { + if (state.bolus) { + state.bolus.amount = state.bolus.amount + ev.amount + } else { + state.bolus = ev + } + } + state.created_at = state.timestamp = ev.timestamp + previous.push(ev) } - } - if (state.notes && state.notes.length > 0) { - state.notes = state.notes.join("\n"); - } - if (state.insulin) { - state.insulin = state.insulin.toString( ); - } - results.push(state); - state = { }; - } - } + if (remaining && remaining.length > 0) { + if (state.bolus && state.wizard) { + // skip to end + return bolus({}, []) + } + // keep recursing + return bolus(remaining[0], remaining.slice(1)) + } else { + // console.error("state", state); + // console.error("remaining", remaining); + state.eventType = '' + + state.insulin = + (state.insulin ? state.insulin : 0) + + (state.square ? state.square.amount : 0) + + (state.bolus ? state.bolus.amount : 0) + const has_insulin = state.insulin && state.insulin > 0 + const has_carbs = state.carbs && state.carbs > 0 + if (state.square && state.bolus) { + annotate('DualWave bolus for', state.square.duration, 'minutes') + } else if (state.square && state.wizard) { + annotate('Square wave bolus for', state.square.duration, 'minutes') + } else if (state.square) { + annotate('Solo Square wave bolus for', state.square.duration, 'minutes') + annotate('No bolus wizard used.') + } else if (state.bolus && state.wizard) { + annotate('Normal bolus with wizard.') + } else if (state.bolus) { + annotate('Normal bolus (solo, no bolus wizard).') + } - function annotate (msg) { - var args = [ ].slice.apply(arguments); - msg = args.join(' '); - if (!state.notes) { - state.notes = [ ]; + if (has_insulin) { + const iobFile = './monitor/iob.json' + const fs = require('fs') + if (fs.existsSync(iobFile)) { + const iob = JSON.parse(fs.readFileSync(iobFile)) + if (iob && Array.isArray(iob) && iob.length) { + annotate('Calculated IOB:', iob[0].iob) + } + } + } + + if (state.bolus) { + annotate('Programmed bolus', state.bolus.programmed) + annotate('Delivered bolus', state.bolus.amount) + annotate('Percent delivered: ', `${((state.bolus.amount / state.bolus.programmed) * 100).toString()}%`) + } + if (state.square) { + annotate('Programmed square', state.square.programmed) + annotate('Delivered square', state.square.amount) + annotate('Success: ', `${((state.square.amount / state.square.programmed) * 100).toString()}%`) + } + if (state.wizard) { + state.created_at = state.wizard.timestamp + annotate('Food estimate', state.wizard.food_estimate) + annotate('Correction estimate', state.wizard.correction_estimate) + annotate('Bolus estimate', state.wizard.bolus_estimate) + annotate('Target low', state.wizard.bg_target_low) + annotate('Target high', state.wizard.bg_target_high) + const delta = state.wizard.sensitivity * state.insulin * -1 + annotate('Hypothetical glucose delta', delta) + if (state.bg && state.bg > 0) { + annotate('Glucose was:', state.bg) + // state.glucose = state.bg; + // TODO: annotate prediction + } + } + if (has_carbs && has_insulin) { + state.eventType = 'Meal Bolus' + } else { + if (has_carbs && !has_insulin) { + state.eventType = 'Carb Correction' + } + if (!has_carbs && has_insulin) { + state.eventType = 'Correction Bolus' + } + } + if (state.notes && state.notes.length > 0) { + state.notes = state.notes.join('\n') + } + if (state.insulin) { + state.insulin = state.insulin.toString() + } + + results.push(state) + state = {} + } } - state.notes.push(msg); - } - function step (current, index) { - if (in_previous(current)) { - return; + function annotate(msg) { + const args = [].slice.apply(arguments) + msg = args.join(' ') + if (!state.notes) { + state.notes = [] + } + state.notes.push(msg) } - switch (current._type) { - case 'Bolus': - case 'BolusWizard': - var tail = within_minutes_from(current, treatments.slice(index+1), 2); - bolus(current, tail); - break; - case 'JournalEntryMealMarker': - current.carbs = current.carb_input; - current.eventType = 'Carb Correction'; - results.push(current); - break; - default: - results.push(current); - break; + + function step(current, index) { + if (in_previous(current)) { + return + } + switch (current._type) { + case 'Bolus': + case 'BolusWizard': + var tail = within_minutes_from(current, treatments.slice(index + 1), 2) + bolus(current, tail) + break + case 'JournalEntryMealMarker': + current.carbs = current.carb_input + current.eventType = 'Carb Correction' + results.push(current) + break + default: + results.push(current) + break + } } - } - treatments.forEach(step); - return results; + treatments.forEach(step) + return results } -exports = module.exports = reduce; +exports = module.exports = reduce diff --git a/lib/calc-glucose-stats.ts b/lib/calc-glucose-stats.ts index fcc6b754d..a18417fc4 100644 --- a/lib/calc-glucose-stats.ts +++ b/lib/calc-glucose-stats.ts @@ -6,30 +6,30 @@ interface Options { export function updateGlucoseStats(options: Options) { const hist = (options.glucose_hist || []) - .map(value => ({ - ...value, - // @todo: in tests, dateString is undefined - readDateMills: value.dateString ? new Date(value.dateString).getTime() : Date.now(), - })) - .sort((a, b) => a.readDateMills - b.readDateMills) + .map(value => ({ + ...value, + // @todo: in tests, dateString is undefined + readDateMills: value.dateString ? new Date(value.dateString).getTime() : Date.now(), + })) + .sort((a, b) => a.readDateMills - b.readDateMills) - if (hist && hist.length > 0) { - var noise_val = stats.calcSensorNoise(null, hist, null, null); + if (hist && hist.length > 0) { + const noise_val = stats.calcSensorNoise(null, hist, null, null) - var ns_noise_val = stats.calcNSNoise(noise_val, hist); + let ns_noise_val = stats.calcNSNoise(noise_val, hist) - if ('noise' in options.glucose_hist[0]) { - console.error("Glucose noise CGM reported level: ", options.glucose_hist[0].noise); - ns_noise_val = Math.max(ns_noise_val, options.glucose_hist[0].noise); - } + if ('noise' in options.glucose_hist[0]) { + console.error('Glucose noise CGM reported level: ', options.glucose_hist[0].noise) + ns_noise_val = Math.max(ns_noise_val, options.glucose_hist[0].noise) + } - console.error("Glucose noise calculated: ", noise_val, " setting noise level to ", ns_noise_val); + console.error('Glucose noise calculated: ', noise_val, ' setting noise level to ', ns_noise_val) - options.glucose_hist[0].noise = ns_noise_val; - } + options.glucose_hist[0].noise = ns_noise_val + } - return options.glucose_hist; -}; + return options.glucose_hist +} exports = module.exports = { updateGlucoseStats, diff --git a/lib/date.ts b/lib/date.ts index e8ee53648..3af3f5fa8 100644 --- a/lib/date.ts +++ b/lib/date.ts @@ -1,21 +1,18 @@ export const tz = (a: Date | string): Date => { - return new Date(new Date(a).toISOString()); + return new Date(new Date(a).toISOString()) } - export const format = (a: Date) => { const newDate = new Date(a) const absOffset = Math.abs(a.getTimezoneOffset()) const hours = Math.ceil(absOffset / 60) - const minutes = absOffset - (hours * 60) + const minutes = absOffset - hours * 60 const sign = a.getTimezoneOffset() < 0 ? '+' : '-' newDate.setMinutes(a.getMinutes() - a.getTimezoneOffset()) - return newDate.toISOString().substring(0, 19) - + sign - + hours.toString().padStart(2, '0') - + ':' - + minutes.toString().padStart(2, '0') + return `${newDate.toISOString().substring(0, 19) + sign + hours.toString().padStart(2, '0')}:${minutes + .toString() + .padStart(2, '0')}` } exports = exports.default = { diff --git a/lib/determine-basal/autosens.js b/lib/determine-basal/autosens.js index 10f31c087..05676d0cd 100644 --- a/lib/determine-basal/autosens.js +++ b/lib/determine-basal/autosens.js @@ -1,455 +1,483 @@ -'use strict'; +'use strict' -var basal = require('../profile/basal'); -var get_iob = require('../iob'); -var find_insulin = require('../iob/history'); -var isf = require('../profile/isf'); -var find_meals = require('../meal/history'); -var percentile = require('../percentile'); -var date = require('../date'); -var tz = date.tz; +const date = require('../date') +const get_iob = require('../iob') +const find_insulin = require('../iob/history') +const find_meals = require('../meal/history') +const percentile = require('../percentile') +const basal = require('../profile/basal') +const isf = require('../profile/isf') +const tz = date.tz function detectSensitivity(inputs) { - //console.error(inputs.glucose_data[0]); - var glucose_data = inputs.glucose_data.map(function prepGlucose (obj) { + const glucose_data = inputs.glucose_data.map(obj => { //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv; - return obj; - }); + obj.glucose = obj.glucose || obj.sgv + return obj + }) //console.error(glucose_data[0]); - var iob_inputs = inputs.iob_inputs; - var basalprofile = inputs.basalprofile; - var profile = inputs.iob_inputs.profile; + const iob_inputs = inputs.iob_inputs + const basalprofile = inputs.basalprofile + const profile = inputs.iob_inputs.profile // use last 24h worth of data by default if (inputs.retrospective) { //console.error(glucose_data[0]); - var lastSiteChange = new Date(new Date(glucose_data[0].date).getTime() - (24 * 60 * 60 * 1000)); + var lastSiteChange = new Date(new Date(glucose_data[0].date).getTime() - 24 * 60 * 60 * 1000) } else { - lastSiteChange = new Date(new Date().getTime() - (24 * 60 * 60 * 1000)); + lastSiteChange = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) } - if (inputs.iob_inputs.profile.rewind_resets_autosens === true ) { + if (inputs.iob_inputs.profile.rewind_resets_autosens === true) { // scan through pumphistory and set lastSiteChange to the time of the last pump rewind event // if not present, leave lastSiteChange unchanged at 24h ago. - var history = inputs.iob_inputs.history; - for (var h=1; h < history.length; ++h) { - if ( ! history[h]._type || history[h]._type !== "Rewind" ) { + const history = inputs.iob_inputs.history + for (let h = 1; h < history.length; ++h) { + if (!history[h]._type || history[h]._type !== 'Rewind') { //process.stderr.write("-"); - continue; + continue } - if ( history[h].timestamp ) { - lastSiteChange = new Date( history[h].timestamp ); - console.error("Setting lastSiteChange to",lastSiteChange,"using timestamp",history[h].timestamp); - break; + if (history[h].timestamp) { + lastSiteChange = new Date(history[h].timestamp) + console.error('Setting lastSiteChange to', lastSiteChange, 'using timestamp', history[h].timestamp) + break } } } // get treatments from pumphistory once, not every time we get_iob() - var treatments = find_insulin(inputs.iob_inputs); + const treatments = find_insulin(inputs.iob_inputs) - var mealinputs = { - history: inputs.iob_inputs.history - , profile: profile - , carbs: inputs.carbs - , glucose: inputs.glucose_data - //, prepped_glucose: prepped_glucose_data - }; - var meals = find_meals(mealinputs); - meals.sort(function (a, b) { - var aDate = new Date(tz(a.timestamp)); - var bDate = new Date(tz(b.timestamp)); + const mealinputs = { + history: inputs.iob_inputs.history, + profile: profile, + carbs: inputs.carbs, + glucose: inputs.glucose_data, + //, prepped_glucose: prepped_glucose_data + } + const meals = find_meals(mealinputs) + meals.sort((a, b) => { + const aDate = new Date(tz(a.timestamp)) + const bDate = new Date(tz(b.timestamp)) //console.error(aDate); - return bDate.getTime() - aDate.getTime(); - }); + return bDate.getTime() - aDate.getTime() + }) //console.error(meals); - var avgDeltas = []; - var bgis = []; - var deviations = []; - var deviationSum = 0; - var bucketed_data = []; - glucose_data.reverse(); - bucketed_data[0] = glucose_data[0]; + const avgDeltas = [] + const bgis = [] + const deviations = [] + let deviationSum = 0 + const bucketed_data = [] + glucose_data.reverse() + bucketed_data[0] = glucose_data[0] //console.error(bucketed_data[0]); - var j=0; + let j = 0 // go through the meal treatments and remove any that are older than the oldest glucose value //console.error(meals); - for (var i=1; i < glucose_data.length; ++i) { - var bgTime; - var lastbgTime; + for (var i = 1; i < glucose_data.length; ++i) { + var bgTime + var lastbgTime if (glucose_data[i].display_time) { - bgTime = new Date(glucose_data[i].display_time.replace('T', ' ')); + bgTime = new Date(glucose_data[i].display_time.replace('T', ' ')) } else if (glucose_data[i].dateString) { - bgTime = new Date(glucose_data[i].dateString); + bgTime = new Date(glucose_data[i].dateString) } else if (glucose_data[i].xDrip_started_at) { - continue; - } else { console.error("Could not determine BG time"); } - if (glucose_data[i-1].display_time) { - lastbgTime = new Date(glucose_data[i-1].display_time.replace('T', ' ')); - } else if (glucose_data[i-1].dateString) { - lastbgTime = new Date(glucose_data[i-1].dateString); + continue + } else { + console.error('Could not determine BG time') + } + if (glucose_data[i - 1].display_time) { + lastbgTime = new Date(glucose_data[i - 1].display_time.replace('T', ' ')) + } else if (glucose_data[i - 1].dateString) { + lastbgTime = new Date(glucose_data[i - 1].dateString) } else if (bucketed_data[0].display_time) { - lastbgTime = new Date(bucketed_data[0].display_time.replace('T', ' ')); - } else if (glucose_data[i-1].xDrip_started_at) { - continue; - } else { console.error("Could not determine last BG time"); } - if (glucose_data[i].glucose < 39 || glucose_data[i-1].glucose < 39) { -//console.error("skipping:",glucose_data[i].glucose,glucose_data[i-1].glucose); - continue; + lastbgTime = new Date(bucketed_data[0].display_time.replace('T', ' ')) + } else if (glucose_data[i - 1].xDrip_started_at) { + continue + } else { + console.error('Could not determine last BG time') + } + if (glucose_data[i].glucose < 39 || glucose_data[i - 1].glucose < 39) { + //console.error("skipping:",glucose_data[i].glucose,glucose_data[i-1].glucose); + continue } // only consider BGs since lastSiteChange if (lastSiteChange) { - var hoursSinceSiteChange = (bgTime-lastSiteChange)/(60*60*1000); + const hoursSinceSiteChange = (bgTime - lastSiteChange) / (60 * 60 * 1000) if (hoursSinceSiteChange < 0) { //console.error(hoursSinceSiteChange, bgTime, lastSiteChange); - continue; + continue } } - var elapsed_minutes = (bgTime - lastbgTime)/(60*1000); - if(Math.abs(elapsed_minutes) > 2) { - j++; - bucketed_data[j]=glucose_data[i]; - bucketed_data[j].date = bgTime.getTime(); + const elapsed_minutes = (bgTime - lastbgTime) / (60 * 1000) + if (Math.abs(elapsed_minutes) > 2) { + j++ + bucketed_data[j] = glucose_data[i] + bucketed_data[j].date = bgTime.getTime() //console.error(elapsed_minutes, bucketed_data[j].glucose, glucose_data[i].glucose); } else { - bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose)/2; + bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose) / 2 //console.error(bucketed_data[j].glucose, glucose_data[i].glucose); } } - bucketed_data.shift(); + bucketed_data.shift() //console.error(bucketed_data[0]); - for (i=meals.length-1; i>0; --i) { - var treatment = meals[i]; + for (i = meals.length - 1; i > 0; --i) { + var treatment = meals[i] //console.error(treatment); if (treatment) { - var treatmentDate = new Date(tz(treatment.timestamp)); - var treatmentTime = treatmentDate.getTime(); - var glucoseDatum = bucketed_data[0]; + var treatmentDate = new Date(tz(treatment.timestamp)) + var treatmentTime = treatmentDate.getTime() + var glucoseDatum = bucketed_data[0] //console.error(glucoseDatum); - if (! glucoseDatum || ! glucoseDatum.date) { - //console.error("No date found on: ",glucoseDatum); - continue; + if (!glucoseDatum || !glucoseDatum.date) { + //console.error("No date found on: ",glucoseDatum); + continue } - var BGDate = new Date(glucoseDatum.date); - var BGTime = BGDate.getTime(); - if ( treatmentTime < BGTime ) { + var BGDate = new Date(glucoseDatum.date) + var BGTime = BGDate.getTime() + if (treatmentTime < BGTime) { //console.error("Removing old meal: ",treatmentDate); - meals.splice(i,1); + meals.splice(i, 1) } } } - var absorbing = 0; - var uam = 0; // unannounced meal - var mealCOB = 0; - var mealCarbs = 0; - var mealStartCounter = 999; - var type=""; - var lastIsfResult = null; + let absorbing = 0 + let uam = 0 // unannounced meal + let mealCOB = 0 + let mealCarbs = 0 + let mealStartCounter = 999 + let type = '' + let lastIsfResult = null //console.error(bucketed_data); - for (i=3; i < bucketed_data.length; ++i) { - bgTime = new Date(bucketed_data[i].date); - var sens; - [sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult); + for (i = 3; i < bucketed_data.length; ++i) { + bgTime = new Date(bucketed_data[i].date) + var sens + ;[sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult) //console.error(bgTime , bucketed_data[i].glucose); - var bg; - var avgDelta; - var delta; - if (typeof(bucketed_data[i].glucose) !== 'undefined') { - bg = bucketed_data[i].glucose; - var last_bg = bucketed_data[i-1].glucose; - var old_bg = bucketed_data[i-3].glucose; - if ( isNaN(bg) || !bg || bg < 40 || isNaN(old_bg) || !old_bg || old_bg < 40 || isNaN(last_bg) || !last_bg || last_bg < 40) { - process.stderr.write("!"); - continue; + var bg + var avgDelta + var delta + if (typeof bucketed_data[i].glucose !== 'undefined') { + bg = bucketed_data[i].glucose + var last_bg = bucketed_data[i - 1].glucose + var old_bg = bucketed_data[i - 3].glucose + if ( + isNaN(bg) || + !bg || + bg < 40 || + isNaN(old_bg) || + !old_bg || + old_bg < 40 || + isNaN(last_bg) || + !last_bg || + last_bg < 40 + ) { + process.stderr.write('!') + continue } - avgDelta = (bg - old_bg)/3; - delta = (bg - last_bg); + avgDelta = (bg - old_bg) / 3 + delta = bg - last_bg } else { - console.error("Could not find glucose data"); - continue; + console.error('Could not find glucose data') + continue } - avgDelta = avgDelta.toFixed(2); - iob_inputs.clock=bgTime; - iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime); + avgDelta = avgDelta.toFixed(2) + iob_inputs.clock = bgTime + iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime) // make sure autosens doesn't use temptarget-adjusted insulin calculations - iob_inputs.profile.temptargetSet = false; + iob_inputs.profile.temptargetSet = false //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); - var iob = get_iob(iob_inputs, true, treatments)[0]; + const iob = get_iob(iob_inputs, true, treatments)[0] //console.error("After: ", new Date().getTime()); //console.log(JSON.stringify(iob)); - var bgi = Math.round(( -iob.activity * sens * 5 )*100)/100; - bgi = bgi.toFixed(2); + let bgi = Math.round(-iob.activity * sens * 5 * 100) / 100 + bgi = bgi.toFixed(2) //console.error(delta); - var deviation; - if (isNaN(delta) ) { - console.error("Bad delta: ",delta, bg, last_bg, old_bg); + var deviation + if (isNaN(delta)) { + console.error('Bad delta: ', delta, bg, last_bg, old_bg) } else { - deviation = delta-bgi; + deviation = delta - bgi } //if (!deviation) { console.error(deviation, delta, bgi); } // set positive deviations to zero if BG is below 80 - if ( bg < 80 && deviation > 0 ) { - deviation = 0; + if (bg < 80 && deviation > 0) { + deviation = 0 } - deviation = deviation.toFixed(2); + deviation = deviation.toFixed(2) - glucoseDatum = bucketed_data[i]; + glucoseDatum = bucketed_data[i] //console.error(glucoseDatum); - BGDate = new Date(glucoseDatum.date); - BGTime = BGDate.getTime(); + BGDate = new Date(glucoseDatum.date) + BGTime = BGDate.getTime() // As we're processing each data point, go through the treatment.carbs and see if any of them are older than // the current BG data point. If so, add those carbs to COB. - treatment = meals[meals.length-1]; + treatment = meals[meals.length - 1] if (treatment) { - treatmentDate = new Date(tz(treatment.timestamp)); - treatmentTime = treatmentDate.getTime(); - if ( treatmentTime < BGTime ) { + treatmentDate = new Date(tz(treatment.timestamp)) + treatmentTime = treatmentDate.getTime() + if (treatmentTime < BGTime) { if (treatment.carbs >= 1) { - //console.error(treatmentDate, treatmentTime, BGTime, BGTime-treatmentTime); - mealCOB += parseFloat(treatment.carbs); - mealCarbs += parseFloat(treatment.carbs); - var displayCOB = Math.round(mealCOB); + //console.error(treatmentDate, treatmentTime, BGTime, BGTime-treatmentTime); + mealCOB += parseFloat(treatment.carbs) + mealCarbs += parseFloat(treatment.carbs) + var displayCOB = Math.round(mealCOB) //console.error(displayCOB, mealCOB, treatment.carbs); - process.stderr.write(displayCOB.toString()+"g"); + process.stderr.write(`${displayCOB.toString()}g`) } - meals.pop(); + meals.pop() } } // calculate carb absorption for that 5m interval using the deviation. - if ( mealCOB > 0 ) { + if (mealCOB > 0) { //var profile = profileData; - var ci = Math.max(deviation, profile.min_5m_carbimpact); - var absorbed = ci * profile.carb_ratio / sens; + const ci = Math.max(deviation, profile.min_5m_carbimpact) + const absorbed = (ci * profile.carb_ratio) / sens if (absorbed) { - mealCOB = Math.max(0, mealCOB-absorbed); + mealCOB = Math.max(0, mealCOB - absorbed) } else { - console.error(absorbed, ci, profile.carb_ratio, sens, deviation, profile.min_5m_carbimpact); + console.error(absorbed, ci, profile.carb_ratio, sens, deviation, profile.min_5m_carbimpact) } } // If mealCOB is zero but all deviations since hitting COB=0 are positive, exclude from autosens //console.error(mealCOB, absorbing, mealCarbs); if (mealCOB > 0 || absorbing || mealCarbs > 0) { - if (deviation > 0 ) { - absorbing = 1; + if (deviation > 0) { + absorbing = 1 } else { - absorbing = 0; + absorbing = 0 } // stop excluding positive deviations as soon as mealCOB=0 if meal has been absorbing for >5h - if ( mealStartCounter > 60 && mealCOB < 0.5 ) { - displayCOB = Math.round(mealCOB); - process.stderr.write(displayCOB.toString()+"g"); - absorbing = 0; + if (mealStartCounter > 60 && mealCOB < 0.5) { + displayCOB = Math.round(mealCOB) + process.stderr.write(`${displayCOB.toString()}g`) + absorbing = 0 } - if ( ! absorbing && mealCOB < 0.5 ) { - mealCarbs = 0; + if (!absorbing && mealCOB < 0.5) { + mealCarbs = 0 } // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag //console.error(type); - if ( type !== "csf" ) { - process.stderr.write("("); - mealStartCounter = 0; + if (type !== 'csf') { + process.stderr.write('(') + mealStartCounter = 0 //glucoseDatum.mealAbsorption = "start"; //console.error(glucoseDatum.mealAbsorption,"carb absorption"); } - mealStartCounter++; - type="csf"; - glucoseDatum.mealCarbs = mealCarbs; + mealStartCounter++ + type = 'csf' + glucoseDatum.mealCarbs = mealCarbs //if (i == 0) { glucoseDatum.mealAbsorption = "end"; } //CSFGlucoseData.push(glucoseDatum); } else { - // check previous "type" value, and if it was csf, set a mealAbsorption end flag - if ( type === "csf" ) { - process.stderr.write(")"); - //CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end"; - //console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); - } + // check previous "type" value, and if it was csf, set a mealAbsorption end flag + if (type === 'csf') { + process.stderr.write(')') + //CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption = "end"; + //console.error(CSFGlucoseData[CSFGlucoseData.length-1].mealAbsorption,"carb absorption"); + } - var currentBasal = iob_inputs.profile.current_basal; - // always exclude the first 45m after each carb entry using mealStartCounter - //if (iob.iob > currentBasal || uam ) { - if ((!inputs.retrospective && iob.iob > 2 * currentBasal) || uam || mealStartCounter < 9 ) { - mealStartCounter++; - if (deviation > 0) { - uam = 1; + const currentBasal = iob_inputs.profile.current_basal + // always exclude the first 45m after each carb entry using mealStartCounter + //if (iob.iob > currentBasal || uam ) { + if ((!inputs.retrospective && iob.iob > 2 * currentBasal) || uam || mealStartCounter < 9) { + mealStartCounter++ + if (deviation > 0) { + uam = 1 + } else { + uam = 0 + } + if (type !== 'uam') { + process.stderr.write('u(') + //glucoseDatum.uamAbsorption = "start"; + //console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption"); + } + //console.error(mealStartCounter); + type = 'uam' } else { - uam = 0; - } - if ( type !== "uam" ) { - process.stderr.write("u("); - //glucoseDatum.uamAbsorption = "start"; - //console.error(glucoseDatum.uamAbsorption,"uannnounced meal absorption"); - } - //console.error(mealStartCounter); - type="uam"; - } else { - if ( type === "uam" ) { - process.stderr.write(")"); - //console.error("end unannounced meal absorption"); + if (type === 'uam') { + process.stderr.write(')') + //console.error("end unannounced meal absorption"); + } + type = 'non-meal' } - type = "non-meal" - } } // Exclude meal-related deviations (carb absorption) from autosens - if ( type === "non-meal" ) { - if ( deviation > 0 ) { + if (type === 'non-meal') { + if (deviation > 0) { //process.stderr.write(" "+bg.toString()); - process.stderr.write("+"); - } else if ( deviation === 0 ) { - process.stderr.write("="); + process.stderr.write('+') + } else if (deviation === 0) { + process.stderr.write('=') } else { //process.stderr.write(" "+bg.toString()); - process.stderr.write("-"); + process.stderr.write('-') } - avgDeltas.push(avgDelta); - bgis.push(bgi); - deviations.push(deviation); - deviationSum += parseFloat(deviation); + avgDeltas.push(avgDelta) + bgis.push(bgi) + deviations.push(deviation) + deviationSum += parseFloat(deviation) } else { - process.stderr.write("x"); + process.stderr.write('x') } // add an extra negative deviation if a high temptarget is running and exercise mode is set if (profile.high_temptarget_raises_sensitivity === true || profile.exercise_mode === true) { - var tempTarget = tempTargetRunning(inputs.temptargets, bgTime) + const tempTarget = tempTargetRunning(inputs.temptargets, bgTime) if (tempTarget) { //console.error(tempTarget) } - if ( tempTarget > 100 ) { + if (tempTarget > 100) { // for a 110 temptarget, add a -0.5 deviation, for 160 add -3 - var tempDeviation=-(tempTarget-100)/20; - process.stderr.write("-"); + const tempDeviation = -(tempTarget - 100) / 20 + process.stderr.write('-') //console.error(tempDeviation) - deviations.push(tempDeviation); + deviations.push(tempDeviation) } } - var minutes = bgTime.getMinutes(); - var hours = bgTime.getHours(); - if ( minutes >= 0 && minutes < 5 ) { + const minutes = bgTime.getMinutes() + const hours = bgTime.getHours() + if (minutes >= 0 && minutes < 5) { //console.error(bgTime); - process.stderr.write(hours.toString()+"h"); + process.stderr.write(`${hours.toString()}h`) // add one neutral deviation every 2 hours to help decay over long exclusion periods - if ( hours % 2 === 0 ) { - deviations.push(0); - process.stderr.write("="); + if (hours % 2 === 0) { + deviations.push(0) + process.stderr.write('=') } } - var lookback = inputs.deviations; - if (!lookback) { lookback = 96; } + let lookback = inputs.deviations + if (!lookback) { + lookback = 96 + } // only keep the last 96 non-excluded data points (8h+ for any exclusions) if (deviations.length > lookback) { - deviations.shift(); + deviations.shift() } } //console.error(""); - process.stderr.write(" "); + process.stderr.write(' ') //console.log(JSON.stringify(avgDeltas)); //console.log(JSON.stringify(bgis)); // when we have less than 8h worth of deviation data, add up to 90m of zero deviations // this dampens any large sensitivity changes detected based on too little data, without ignoring them completely - console.error(""); - console.error("Using most recent",deviations.length,"deviations since",lastSiteChange); + console.error('') + console.error('Using most recent', deviations.length, 'deviations since', lastSiteChange) if (deviations.length < 96) { - var pad = Math.round((1 - deviations.length/96) * 18); - console.error("Adding",pad,"more zero deviations"); - for (var d=0; d 0.1; i = i - 0.01) { + avgDeltas.sort((a, b) => { + return a - b + }) + bgis.sort((a, b) => { + return a - b + }) + deviations.sort((a, b) => { + return a - b + }) + for (i = 0.9; i > 0.1; i = i - 0.01) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); - if ( percentile(deviations, (i+0.01)) >= 0 && percentile(deviations, i) < 0 ) { + if (percentile(deviations, i + 0.01) >= 0 && percentile(deviations, i) < 0) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); - var lessThanZero = Math.round(100*i); - console.error(lessThanZero+"% of non-meal deviations negative (>50% = sensitivity)"); + const lessThanZero = Math.round(100 * i) + console.error(`${lessThanZero}% of non-meal deviations negative (>50% = sensitivity)`) } - if ( percentile(deviations, (i+0.01)) > 0 && percentile(deviations, i) <= 0 ) { + if (percentile(deviations, i + 0.01) > 0 && percentile(deviations, i) <= 0) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); - var greaterThanZero = 100-Math.round(100*i); - console.error(greaterThanZero+"% of non-meal deviations positive (>50% = resistance)"); + const greaterThanZero = 100 - Math.round(100 * i) + console.error(`${greaterThanZero}% of non-meal deviations positive (>50% = resistance)`) } } - var pSensitive = percentile(deviations, 0.50); - var pResistant = percentile(deviations, 0.50); + const pSensitive = percentile(deviations, 0.5) + const pResistant = percentile(deviations, 0.5) - var average = deviationSum / deviations.length; + const average = deviationSum / deviations.length //console.error("Mean deviation: "+average.toFixed(2)); - - var squareDeviations = deviations.reduce(function(acc, dev){var dev_f = parseFloat(dev); return acc + dev_f * dev_f}, 0); - var rmsDev = Math.sqrt(squareDeviations / deviations.length); - console.error("RMS deviation: "+rmsDev.toFixed(2)); - var basalOff = 0; + const squareDeviations = deviations.reduce((acc, dev) => { + const dev_f = parseFloat(dev) + return acc + dev_f * dev_f + }, 0) + const rmsDev = Math.sqrt(squareDeviations / deviations.length) + console.error(`RMS deviation: ${rmsDev.toFixed(2)}`) + + let basalOff = 0 - if(pSensitive < 0) { // sensitive - basalOff = pSensitive * (60/5) / profile.sens; - process.stderr.write("Insulin sensitivity detected: "); - } else if (pResistant > 0) { // resistant - basalOff = pResistant * (60/5) / profile.sens; - process.stderr.write("Insulin resistance detected: "); + if (pSensitive < 0) { + // sensitive + basalOff = (pSensitive * (60 / 5)) / profile.sens + process.stderr.write('Insulin sensitivity detected: ') + } else if (pResistant > 0) { + // resistant + basalOff = (pResistant * (60 / 5)) / profile.sens + process.stderr.write('Insulin resistance detected: ') } else { - console.error("Sensitivity normal."); + console.error('Sensitivity normal.') } - var ratio = 1 + (basalOff / profile.max_daily_basal); + let ratio = 1 + basalOff / profile.max_daily_basal //console.error(basalOff, profile.max_daily_basal, ratio); // don't adjust more than 1.2x by default (set in preferences.json) - var rawRatio = ratio; - ratio = Math.max(ratio, profile.autosens_min); - ratio = Math.min(ratio, profile.autosens_max); + const rawRatio = ratio + ratio = Math.max(ratio, profile.autosens_min) + ratio = Math.min(ratio, profile.autosens_max) if (ratio !== rawRatio) { - console.error('Ratio limited from ' + rawRatio + ' to ' + ratio); + console.error(`Ratio limited from ${rawRatio} to ${ratio}`) } - ratio = Math.round(ratio*100)/100; - var newisf = Math.round(profile.sens / ratio); + ratio = Math.round(ratio * 100) / 100 + const newisf = Math.round(profile.sens / ratio) //console.error(profile, newisf, ratio); - console.error("ISF adjusted from "+profile.sens+" to "+newisf); + console.error(`ISF adjusted from ${profile.sens} to ${newisf}`) //console.error("Basal adjustment "+basalOff.toFixed(2)+"U/hr"); //console.error("Ratio: "+ratio*100+"%: new ISF: "+newisf.toFixed(1)+"mg/dL/U"); return { - "ratio": ratio, - "newisf": newisf + ratio: ratio, + newisf: newisf, } } -module.exports = detectSensitivity; +module.exports = detectSensitivity function tempTargetRunning(temptargets_data, time) { // sort tempTargets by date so we can process most recent first try { - temptargets_data.sort(function (a, b) { return new Date(a.created_at) < new Date(b.created_at) }); + temptargets_data.sort((a, b) => { + return new Date(a.created_at) < new Date(b.created_at) + }) } catch (e) { //console.error("Could not sort temptargets_data. Optional feature temporary targets disabled."); } //console.error(temptargets_data); //console.error(time); - for (var i = 0; i < temptargets_data.length; i++) { - var start = new Date(temptargets_data[i].created_at); + for (let i = 0; i < temptargets_data.length; i++) { + const start = new Date(temptargets_data[i].created_at) //console.error(start); - var expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000); + const expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000) //console.error(expires); if (time >= new Date(temptargets_data[i].created_at) && temptargets_data[i].duration === 0) { // cancel temp targets //console.error(temptargets_data[i]); - return 0; - } else if (time >= new Date(temptargets_data[i].created_at) && time < expires ) { + return 0 + } else if (time >= new Date(temptargets_data[i].created_at) && time < expires) { //console.error(temptargets_data[i]); - var tempTarget = ( temptargets_data[i].targetTop + temptargets_data[i].targetBottom ) / 2; + const tempTarget = (temptargets_data[i].targetTop + temptargets_data[i].targetBottom) / 2 //console.error(tempTarget); - return tempTarget; + return tempTarget } } } diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index 2bd18b07c..229c17709 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -1,9 +1,10 @@ -import * as basal from '../profile/basal' import get_iob from '../iob' -import find_insulin, { Input as IOBInput } from '../iob/history' +import type { Input as IOBInput } from '../iob/history' +import find_insulin from '../iob/history' +import * as basal from '../profile/basal' import isf from '../profile/isf' -import { GlucoseEntry } from '../types/GlucoseEntry'; -import { BasalSchedule } from '../types/Profile'; +import type { GlucoseEntry } from '../types/GlucoseEntry' +import type { BasalSchedule } from '../types/Profile' export interface DetectCOBInput { glucose_data: GlucoseEntry[] @@ -26,202 +27,201 @@ function getDateFromEntry(entry: GlucoseEntry) { } export default function detectCarbAbsorption(inputs: DetectCOBInput) { - const glucose_data = inputs.glucose_data.reduce( (b, a) => { const glucose = a.glucose || a.sgv return glucose ? [...b, { ...a, glucose, date: getDateFromEntry(a) }] : b }, - [] as (GlucoseEntry & { glucose: number, date: number })[] + [] as (GlucoseEntry & { glucose: number; date: number })[] ) - var iob_inputs = inputs.iob_inputs; - var basalprofile = inputs.basalprofile; - /* TODO why does declaring profile break tests-command-behavior.tests.sh? - because it is a global variable used in other places.*/ - var profile = inputs.iob_inputs.profile; - var mealTime = new Date(inputs.mealTime); - var ciTime = inputs.ciTime ? new Date(inputs.ciTime) : undefined; + const iob_inputs = inputs.iob_inputs + const basalprofile = inputs.basalprofile + /* TODO why does declaring profile break tests-command-behavior.tests.sh? + because it is a global variable used in other places.*/ + const profile = inputs.iob_inputs.profile + const mealTime = new Date(inputs.mealTime) + const ciTime = inputs.ciTime ? new Date(inputs.ciTime) : undefined //console.error(mealTime, ciTime); // get treatments from pumphistory once, not every time we get_iob() - var treatments = find_insulin(inputs.iob_inputs); + const treatments = find_insulin(inputs.iob_inputs) - if (! glucose_data.length) { + if (!glucose_data.length) { // @todo: return something empty } - var carbsAbsorbed = 0; - var bucketed_data = glucose_data.slice(0, 1) - var j=0; - var foundPreMealBG = false; - var lastbgi = 0; + let carbsAbsorbed = 0 + const bucketed_data = glucose_data.slice(0, 1) + let j = 0 + let foundPreMealBG = false + let lastbgi = 0 - if (! glucose_data[0].glucose || glucose_data[0].glucose < 39) { - lastbgi = -1; + if (!glucose_data[0].glucose || glucose_data[0].glucose < 39) { + lastbgi = -1 } - for (var i=1; i < glucose_data.length; ++i) { + for (let i = 1; i < glucose_data.length; ++i) { const currentGlucose = glucose_data[i] - var bgTime = new Date(currentGlucose.date) - var lastbgTime; + const bgTime = new Date(currentGlucose.date) + let lastbgTime if (currentGlucose.glucose < 39) { //console.error("skipping:",glucose_data[i].glucose); - continue; + continue } // only consider BGs for 6h after a meal for calculating COB - var hoursAfterMeal = (bgTime.getTime() - mealTime.getTime())/(60*60*1000); + const hoursAfterMeal = (bgTime.getTime() - mealTime.getTime()) / (60 * 60 * 1000) if (hoursAfterMeal > 6 || foundPreMealBG) { - continue; + continue } else if (hoursAfterMeal < 0) { //console.error("Found pre-meal BG:",glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100); - foundPreMealBG = true; + foundPreMealBG = true } //console.error(glucose_data[i].glucose, bgTime, Math.round(hoursAfterMeal*100)/100, bucketed_data[bucketed_data.length-1].display_time); // only consider last ~45m of data in CI mode // this allows us to calculate deviations for the last ~30m if (typeof ciTime !== 'undefined') { - var hoursAgo = (ciTime.getTime() - bgTime.getTime())/(45*60*1000); + const hoursAgo = (ciTime.getTime() - bgTime.getTime()) / (45 * 60 * 1000) if (hoursAgo > 1 || hoursAgo < 0) { - continue; + continue } } - const lastBucketedData = bucketed_data[bucketed_data.length-1] - lastbgTime = new Date(lastBucketedData.date); - var elapsed_minutes = (bgTime.getTime() - lastbgTime.getTime())/(60*1000); - //console.error(bgTime, lastbgTime, elapsed_minutes); - if(Math.abs(elapsed_minutes) > 8) { + const lastBucketedData = bucketed_data[bucketed_data.length - 1] + lastbgTime = new Date(lastBucketedData.date) + let elapsed_minutes = (bgTime.getTime() - lastbgTime.getTime()) / (60 * 1000) + //console.error(bgTime, lastbgTime, elapsed_minutes); + if (Math.abs(elapsed_minutes) > 8) { // interpolate missing data points - var lastbg = glucose_data[lastbgi].glucose; + let lastbg = glucose_data[lastbgi].glucose // cap interpolation at a maximum of 4h - elapsed_minutes = Math.min(240,Math.abs(elapsed_minutes)); + elapsed_minutes = Math.min(240, Math.abs(elapsed_minutes)) //console.error(elapsed_minutes); while (elapsed_minutes > 5) { - var previousbgTime: Date = new Date(lastbgTime.getTime() - 5 * 60*1000); - j++; + const previousbgTime: Date = new Date(lastbgTime.getTime() - 5 * 60 * 1000) + j++ - var gapDelta = glucose_data[i].glucose - lastbg; - var previousbg = lastbg + (5/elapsed_minutes * gapDelta); + const gapDelta = glucose_data[i].glucose - lastbg + const previousbg = lastbg + (5 / elapsed_minutes) * gapDelta bucketed_data[j] = { date: previousbgTime.getTime(), glucose: Math.round(previousbg), - }; + } - elapsed_minutes = elapsed_minutes - 5; - lastbg = previousbg; - lastbgTime = new Date(previousbgTime); + elapsed_minutes = elapsed_minutes - 5 + lastbg = previousbg + lastbgTime = new Date(previousbgTime) } - - } else if(Math.abs(elapsed_minutes) > 2) { - j++; - bucketed_data[j]=glucose_data[i]; - bucketed_data[j].date = bgTime.getTime(); + } else if (Math.abs(elapsed_minutes) > 2) { + j++ + bucketed_data[j] = glucose_data[i] + bucketed_data[j].date = bgTime.getTime() } else { - bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose)/2; + bucketed_data[j].glucose = (bucketed_data[j].glucose + glucose_data[i].glucose) / 2 } - lastbgi = i; + lastbgi = i //console.error(bucketed_data[j].date) } - var currentDeviation = 0; - var slopeFromMaxDeviation = 0; - var slopeFromMinDeviation = 999; - var maxDeviation = 0; - var minDeviation = 999; - var allDeviations = []; + let currentDeviation = 0 + let slopeFromMaxDeviation = 0 + let slopeFromMinDeviation = 999 + let maxDeviation = 0 + let minDeviation = 999 + const allDeviations = [] //console.error(bucketed_data); - var lastIsfResult = undefined; + let lastIsfResult = null if (!profile.isfProfile) { - console.error("No isfProfile found in Profile"); - throw new TypeError("No isfProfile found in Profile") + console.error('No isfProfile found in Profile') + throw new TypeError('No isfProfile found in Profile') } - for (i=0; i < bucketed_data.length-3; ++i) { - bgTime = new Date(bucketed_data[i].date); + for (let i = 0; i < bucketed_data.length - 3; ++i) { + const bgTime = new Date(bucketed_data[i].date) - var sens; - [sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult); + let sens = null + ;[sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult) //console.error(bgTime , bucketed_data[i].glucose, bucketed_data[i].date); - var bg; - let avgDelta; - var delta; - if (typeof(bucketed_data[i].glucose) !== 'undefined') { - bg = bucketed_data[i].glucose; - if ( bg < 39 || bucketed_data[i+3].glucose < 39) { - process.stderr.write("!"); - continue; + let bg + let avgDelta + let delta + if (typeof bucketed_data[i].glucose !== 'undefined') { + bg = bucketed_data[i].glucose + if (bg < 39 || bucketed_data[i + 3].glucose < 39) { + process.stderr.write('!') + continue } - avgDelta = Math.round(((bg - bucketed_data[i+3].glucose)/3) * 100) / 100; - delta = (bg - bucketed_data[i+1].glucose); + avgDelta = Math.round(((bg - bucketed_data[i + 3].glucose) / 3) * 100) / 100 + delta = bg - bucketed_data[i + 1].glucose } else { - console.error("Could not find glucose data"); - continue; + console.error('Could not find glucose data') + continue } - iob_inputs.clock=bgTime.toISOString(); - iob_inputs.profile.current_basal = basal.basalLookup(basalprofile || [], bgTime); + iob_inputs.clock = bgTime.toISOString() + iob_inputs.profile.current_basal = basal.basalLookup(basalprofile || [], bgTime) //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); - var iob = get_iob(iob_inputs, true, treatments)[0]; + const iob = get_iob(iob_inputs, true, treatments)[0] //console.error("After: ", new Date().getTime()); //console.error(JSON.stringify(iob)); - var bgi = Math.round(( -iob.activity * sens * 5 ) * 100) / 100; + const bgi = Math.round(-iob.activity * sens * 5 * 100) / 100 //console.error(delta); - var deviation = Math.round((delta-bgi) * 100) / 100; + const deviation = Math.round((delta - bgi) * 100) / 100 //if (deviation < 0 && deviation > -2) { console.error("BG: "+bg+", avgDelta: "+avgDelta+", BGI: "+bgi+", deviation: "+deviation); } // calculate the deviation right now, for use in min_5m - if (i===0) { - currentDeviation = Math.round((avgDelta-bgi)*1000)/1000; + if (i === 0) { + currentDeviation = Math.round((avgDelta - bgi) * 1000) / 1000 if (ciTime && ciTime > bgTime) { //console.error("currentDeviation:",currentDeviation,avgDelta,bgi); - allDeviations.push(Math.round(currentDeviation)); + allDeviations.push(Math.round(currentDeviation)) } - if (currentDeviation/2 > profile.min_5m_carbimpact) { + if (currentDeviation / 2 > profile.min_5m_carbimpact) { //console.error("currentDeviation",currentDeviation,"/2 > min_5m_carbimpact",profile.min_5m_carbimpact); } } else if (ciTime && ciTime > bgTime) { - var avgDeviation = Math.round((avgDelta-bgi)*1000)/1000; - var deviationSlope = (avgDeviation - currentDeviation)/(bgTime.getTime()-ciTime.getTime())*1000*60*5; + const avgDeviation = Math.round((avgDelta - bgi) * 1000) / 1000 + const deviationSlope = + ((avgDeviation - currentDeviation) / (bgTime.getTime() - ciTime.getTime())) * 1000 * 60 * 5 //console.error(avgDeviation,currentDeviation,bgTime,ciTime) if (avgDeviation > maxDeviation) { - slopeFromMaxDeviation = Math.min(0, deviationSlope); - maxDeviation = avgDeviation; + slopeFromMaxDeviation = Math.min(0, deviationSlope) + maxDeviation = avgDeviation } if (avgDeviation < minDeviation) { - slopeFromMinDeviation = Math.max(0, deviationSlope); - minDeviation = avgDeviation; + slopeFromMinDeviation = Math.max(0, deviationSlope) + minDeviation = avgDeviation } //console.error("Deviations:",avgDeviation, avgDelta,bgi,bgTime); - allDeviations.push(Math.round(avgDeviation)); + allDeviations.push(Math.round(avgDeviation)) //console.error(allDeviations); } // if bgTime is more recent than mealTime - if(bgTime > mealTime) { + if (bgTime > mealTime) { // figure out how many carbs that represents // if currentDeviation is > 2 * min_5m_carbimpact, assume currentDeviation/2 worth of carbs were absorbed // but always assume at least profile.min_5m_carbimpact (3mg/dL/5m by default) absorption - var ci = Math.max(deviation, currentDeviation/2, profile.min_5m_carbimpact); - var absorbed = ci * profile.carb_ratio / sens; + const ci = Math.max(deviation, currentDeviation / 2, profile.min_5m_carbimpact) + const absorbed = (ci * profile.carb_ratio) / sens // and add that to the running total carbsAbsorbed //console.error("carbsAbsorbed:",carbsAbsorbed,"absorbed:",absorbed,"bgTime:",bgTime,"BG:",bucketed_data[i].glucose) - carbsAbsorbed += absorbed; + carbsAbsorbed += absorbed } } - if(maxDeviation>0) { + if (maxDeviation > 0) { //console.error("currentDeviation:",currentDeviation,"maxDeviation:",maxDeviation,"slopeFromMaxDeviation:",slopeFromMaxDeviation); } return { - "carbsAbsorbed": carbsAbsorbed - , "currentDeviation": currentDeviation - , "maxDeviation": maxDeviation - , "minDeviation": minDeviation - , "slopeFromMaxDeviation": slopeFromMaxDeviation - , "slopeFromMinDeviation": slopeFromMinDeviation - , "allDeviations": allDeviations + carbsAbsorbed: carbsAbsorbed, + currentDeviation: currentDeviation, + maxDeviation: maxDeviation, + minDeviation: minDeviation, + slopeFromMaxDeviation: slopeFromMaxDeviation, + slopeFromMinDeviation: slopeFromMinDeviation, + allDeviations: allDeviations, } } -module.exports = detectCarbAbsorption; +module.exports = detectCarbAbsorption diff --git a/lib/determine-basal/determine-basal.ts b/lib/determine-basal/determine-basal.ts index bcdabead4..86ab57623 100644 --- a/lib/determine-basal/determine-basal.ts +++ b/lib/determine-basal/determine-basal.ts @@ -13,41 +13,34 @@ THE SOFTWARE. */ -import { Profile } from "../types/Profile"; - // Define various functions used later on, in the main function determine_basal() below import round_basal from '../round-basal' -import { Autosens } from "../types/Autosens"; +import type { Autosens } from '../types/Autosens' +import type { Profile } from '../types/Profile' // Rounds value to 'digits' decimal places -function round(value: number, digits?: number) -{ - var scale = Math.pow(10, digits || 0); +function round(value: number, digits?: number) { + const scale = Math.pow(10, digits || 0) - return Math.round(value * scale) / scale; + return Math.round(value * scale) / scale } // we expect BG to rise or fall at the rate of BGI, // adjusted by the rate at which BG would need to rise / // fall to get eventualBG to target over 2 hours -function calculate_expected_delta(target_bg: number, eventual_bg: number, bgi: number ) { +function calculate_expected_delta(target_bg: number, eventual_bg: number, bgi: number) { // (hours * mins_per_hour) / 5 = how many 5 minute periods in 2h = 24 - var five_min_blocks = (2 * 60) / 5; - var target_delta = target_bg - eventual_bg; - return /* expectedDelta */ round(bgi + (target_delta / five_min_blocks), 1); + const five_min_blocks = (2 * 60) / 5 + const target_delta = target_bg - eventual_bg + return /* expectedDelta */ round(bgi + target_delta / five_min_blocks, 1) } - -function convert_bg(value: number, profile: Profile) -{ - if (profile.out_units === "mmol/L") - { - return round(value / 18, 1); - } - else - { - return Math.round(value); +function convert_bg(value: number, profile: Profile) { + if (profile.out_units === 'mmol/L') { + return round(value / 18, 1) + } else { + return Math.round(value) } } @@ -71,72 +64,82 @@ function enable_smb( high_bg: number | undefined ) { // disable SMB when a high temptarget is set - if (! microBolusAllowed) { - console.error("SMB disabled (!microBolusAllowed)"); - return false; - } else if (! profile.allowSMB_with_high_temptarget && profile.temptargetSet && target_bg > 100) { - console.error("SMB disabled due to high temptarget of",target_bg); - return false; + if (!microBolusAllowed) { + console.error('SMB disabled (!microBolusAllowed)') + return false + } else if (!profile.allowSMB_with_high_temptarget && profile.temptargetSet && target_bg > 100) { + console.error('SMB disabled due to high temptarget of', target_bg) + return false } else if (meal_data.bwFound === true && profile.A52_risk_enable === false) { - console.error("SMB disabled due to Bolus Wizard activity in the last 6 hours."); - return false; + console.error('SMB disabled due to Bolus Wizard activity in the last 6 hours.') + return false } // enable SMB/UAM if always-on (unless previously disabled for high temptarget) if (profile.enableSMB_always === true) { if (meal_data.bwFound) { - console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard"); + console.error( + 'Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard' + ) } else { - console.error("SMB enabled due to enableSMB_always"); + console.error('SMB enabled due to enableSMB_always') } - return true; + return true } // enable SMB/UAM (if enabled in preferences) while we have COB if (profile.enableSMB_with_COB === true && meal_data.mealCOB) { if (meal_data.bwCarbs) { - console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard"); + console.error( + 'Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard' + ) } else { - console.error("SMB enabled for COB of",meal_data.mealCOB); + console.error('SMB enabled for COB of', meal_data.mealCOB) } - return true; + return true } // enable SMB/UAM (if enabled in preferences) for a full 6 hours after any carb entry // (6 hours is defined in carbWindow in lib/meal/total.js) - if (profile.enableSMB_after_carbs === true && meal_data.carbs ) { + if (profile.enableSMB_after_carbs === true && meal_data.carbs) { if (meal_data.bwCarbs) { - console.error("Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard"); + console.error( + 'Warning: SMB enabled with Bolus Wizard carbs: be sure to easy bolus 30s before using Bolus Wizard' + ) } else { - console.error("SMB enabled for 6h after carb entry"); + console.error('SMB enabled for 6h after carb entry') } - return true; + return true } // enable SMB/UAM (if enabled in preferences) if a low temptarget is set - if (profile.enableSMB_with_temptarget === true && (profile.temptargetSet && target_bg < 100)) { + if (profile.enableSMB_with_temptarget === true && profile.temptargetSet && target_bg < 100) { if (meal_data.bwFound) { - console.error("Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard"); + console.error( + 'Warning: SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard' + ) } else { - console.error("SMB enabled for temptarget of",convert_bg(target_bg, profile)); + console.error('SMB enabled for temptarget of', convert_bg(target_bg, profile)) } - return true; + return true } // enable SMB if high bg is found if (profile.enableSMB_high_bg === true && high_bg && bg >= high_bg) { - console.error("Checking BG to see if High for SMB enablement."); - console.error("Current BG", bg, " | High BG ", high_bg); + console.error('Checking BG to see if High for SMB enablement.') + console.error('Current BG', bg, ' | High BG ', high_bg) if (meal_data.bwFound) { - console.error("Warning: High BG SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard"); + console.error( + 'Warning: High BG SMB enabled within 6h of using Bolus Wizard: be sure to easy bolus 30s before using Bolus Wizard' + ) } else { - console.error("High BG detected. Enabling SMB."); + console.error('High BG detected. Enabling SMB.') } - return true; + return true } - console.error("SMB disabled (no enableSMB preferences active or no condition satisfied)"); - return false; + console.error('SMB disabled (no enableSMB preferences active or no condition satisfied)') + return false } interface GlucoseStatus { @@ -157,7 +160,7 @@ interface IOBTick { date: number duration: number rate: number - }, + } iobWithZeroTemp: { activity: number } @@ -165,13 +168,13 @@ interface IOBTick { } interface CurrentTemp { - timestamp: string, - temp: "absolute" | string, - rate: number, + timestamp: string + temp: 'absolute' | string + rate: number duration: number } -var determine_basal = function determine_basal( +const determine_basal = function determine_basal( glucose_status: GlucoseStatus, currenttemp: CurrentTemp, iobArray: IOBTick | IOBTick[], @@ -183,9 +186,8 @@ var determine_basal = function determine_basal( reservoir_data: number, currentTime?: Date ) { - -// Set variables required for evaluating error conditions - var rT: { + // Set variables required for evaluating error conditions + let rT: { error?: string reason?: string deliverAt?: Date @@ -208,403 +210,439 @@ var determine_basal = function determine_basal( target_bg?: number carbsReq?: number insulinReq?: number - units?: number, + units?: number [k: string]: unknown - } = {}; //short for requestedTemp + } = {} //short for requestedTemp - var deliverAt = new Date(); + let deliverAt = new Date() if (currentTime) { - deliverAt = currentTime; + deliverAt = currentTime } if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { - rT.error ='Error: could not get current basal rate'; - return rT; + rT.error = 'Error: could not get current basal rate' + return rT } - var profile_current_basal = round_basal(profile.current_basal, profile); - var basal = profile_current_basal; + const profile_current_basal = round_basal(profile.current_basal, profile) + let basal = profile_current_basal - var systemTime = new Date(); + let systemTime = new Date() if (currentTime) { - systemTime = currentTime; + systemTime = currentTime } - var bgTime = new Date(glucose_status.date); - var minAgo = round( (systemTime.getTime() - bgTime.getTime()) / 60 / 1000, 1); + const bgTime = new Date(glucose_status.date) + const minAgo = round((systemTime.getTime() - bgTime.getTime()) / 60 / 1000, 1) - var bg = glucose_status.glucose; - var noise = glucose_status.noise; + const bg = glucose_status.glucose + const noise = glucose_status.noise -// Prep various delta variables. - var tick = glucose_status.delta > -0.5 ? "+" + round(glucose_status.delta,0) : round(glucose_status.delta,0) + // Prep various delta variables. + const tick = glucose_status.delta > -0.5 ? `+${round(glucose_status.delta, 0)}` : round(glucose_status.delta, 0) //var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); - var minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta); - var minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta); - var maxDelta = Math.max(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta); + const minDelta = Math.min(glucose_status.delta, glucose_status.short_avgdelta) + const minAvgDelta = Math.min(glucose_status.short_avgdelta, glucose_status.long_avgdelta) + const maxDelta = Math.max(glucose_status.delta, glucose_status.short_avgdelta, glucose_status.long_avgdelta) - -// Cancel high temps (and replace with neutral) or shorten long zero temps for various error conditions + // Cancel high temps (and replace with neutral) or shorten long zero temps for various error conditions // 38 is an xDrip error state that usually indicates sensor failure // all other BG values between 11 and 37 mg/dL reflect non-error-code BG values, so we should zero temp for those -// First, print out different explanations for each different error condition - if (bg <= 10 || bg === 38 || noise >= 3) { //Dexcom is in ??? mode or calibrating, or xDrip reports high noise - rT.reason = "CGM is calibrating, in ??? state, or noise is high"; - } - var tooflat=false; - if (bg > 60 && glucose_status.delta == 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1) { - if (glucose_status.device == "fakecgm") { - console.error("CGM data is unchanged ("+bg+"+"+glucose_status.delta+") for 5m w/ "+glucose_status.short_avgdelta+" mg/dL ~15m change & "+glucose_status.long_avgdelta+" mg/dL ~45m change"); - console.error("Simulator mode detected (",glucose_status.device,"): continuing anyway"); + // First, print out different explanations for each different error condition + if (bg <= 10 || bg === 38 || noise >= 3) { + //Dexcom is in ??? mode or calibrating, or xDrip reports high noise + rT.reason = 'CGM is calibrating, in ??? state, or noise is high' + } + let tooflat = false + if ( + bg > 60 && + glucose_status.delta == 0 && + glucose_status.short_avgdelta > -1 && + glucose_status.short_avgdelta < 1 && + glucose_status.long_avgdelta > -1 && + glucose_status.long_avgdelta < 1 + ) { + if (glucose_status.device == 'fakecgm') { + console.error( + `CGM data is unchanged (${bg}+${glucose_status.delta}) for 5m w/ ${glucose_status.short_avgdelta} mg/dL ~15m change & ${glucose_status.long_avgdelta} mg/dL ~45m change` + ) + console.error('Simulator mode detected (', glucose_status.device, '): continuing anyway') } else { - tooflat=true; + tooflat = true } } - if (minAgo > 12 || minAgo < -5) { // Dexcom data is too old, or way in the future - rT.reason = "If current system time "+systemTime+" is correct, then BG data is too old. The last BG data was read "+minAgo+"m ago at "+bgTime; - // if BG is too old/noisy, or is changing less than 1 mg/dL/5m for 45m, cancel any high temps and shorten any long zero temps - } else if ( tooflat ) { - if ( glucose_status.last_cal && glucose_status.last_cal < 3 ) { - rT.reason = "CGM was just calibrated"; + if (minAgo > 12 || minAgo < -5) { + // Dexcom data is too old, or way in the future + rT.reason = `If current system time ${ + systemTime + } is correct, then BG data is too old. The last BG data was read ${minAgo}m ago at ${bgTime}` + // if BG is too old/noisy, or is changing less than 1 mg/dL/5m for 45m, cancel any high temps and shorten any long zero temps + } else if (tooflat) { + if (glucose_status.last_cal && glucose_status.last_cal < 3) { + rT.reason = 'CGM was just calibrated' } else { - rT.reason = "CGM data is unchanged ("+bg+"+"+glucose_status.delta+") for 5m w/ "+glucose_status.short_avgdelta+" mg/dL ~15m change & "+glucose_status.long_avgdelta+" mg/dL ~45m change"; + rT.reason = `CGM data is unchanged (${bg}+${glucose_status.delta}) for 5m w/ ${ + glucose_status.short_avgdelta + } mg/dL ~15m change & ${glucose_status.long_avgdelta} mg/dL ~45m change` } } -// Then, for all such error conditions, cancel any running high temp or shorten any long zero temp, and return. - if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || tooflat ) { - if (currenttemp.rate > basal) { // high temp is running - rT.reason += ". Replacing high temp basal of "+currenttemp.rate+" with neutral temp of "+basal; - rT.deliverAt = deliverAt; - rT.temp = 'absolute'; - rT.duration = 30; - rT.rate = basal; - return rT; + // Then, for all such error conditions, cancel any running high temp or shorten any long zero temp, and return. + if (bg <= 10 || bg === 38 || noise >= 3 || minAgo > 12 || minAgo < -5 || tooflat) { + if (currenttemp.rate > basal) { + // high temp is running + rT.reason += `. Replacing high temp basal of ${currenttemp.rate} with neutral temp of ${basal}` + rT.deliverAt = deliverAt + rT.temp = 'absolute' + rT.duration = 30 + rT.rate = basal + return rT // don't use setTempBasal(), as it has logic that allows <120% high temps to continue running //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); - } else if ( currenttemp.rate === 0 && currenttemp.duration > 30 ) { //shorten long zero temps to 30m - rT.reason += ". Shortening " + currenttemp.duration + "m long zero temp to 30m. "; - rT.deliverAt = deliverAt; - rT.temp = 'absolute'; - rT.duration = 30; - rT.rate = 0; - return rT; + } else if (currenttemp.rate === 0 && currenttemp.duration > 30) { + //shorten long zero temps to 30m + rT.reason += `. Shortening ${currenttemp.duration}m long zero temp to 30m. ` + rT.deliverAt = deliverAt + rT.temp = 'absolute' + rT.duration = 30 + rT.rate = 0 + return rT // don't use setTempBasal(), as it has logic that allows long zero temps to continue running //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); - } else { //do nothing. - rT.reason += ". Temp " + currenttemp.rate + " <= current basal " + basal + "U/hr; doing nothing. "; - return rT; + } else { + //do nothing. + rT.reason += `. Temp ${currenttemp.rate} <= current basal ${basal}U/hr; doing nothing. ` + return rT } } -// Get configured target, and return if unable to do so. -// This should occur after checking that we're not in one of the CGM-data-related error conditions handled above, -// and before using target_bg to adjust sensitivityRatio below. - var max_iob = profile.max_iob; // maximum amount of non-bolus IOB OpenAPS will ever deliver + // Get configured target, and return if unable to do so. + // This should occur after checking that we're not in one of the CGM-data-related error conditions handled above, + // and before using target_bg to adjust sensitivityRatio below. + const max_iob = profile.max_iob // maximum amount of non-bolus IOB OpenAPS will ever deliver // if min and max are set, then set target to their average - var target_bg: number; - var min_bg = profile.min_bg as number; - var max_bg = profile.max_bg as number; - var high_bg = profile.enableSMB_high_bg_target + let target_bg: number + let min_bg = profile.min_bg as number + let max_bg = profile.max_bg as number + const high_bg = profile.enableSMB_high_bg_target if (min_bg === undefined || max_bg === undefined) { - rT.error ='Error: could not determine target_bg. '; - return rT; + rT.error = 'Error: could not determine target_bg. ' + return rT } - target_bg = (min_bg + max_bg) / 2; + target_bg = (min_bg + max_bg) / 2 -// Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens - var sensitivityRatio: number | undefined; - var high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity; - var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled target (which might change) - var halfBasalTarget; - if ( profile.half_basal_exercise_target ) { - halfBasalTarget = profile.half_basal_exercise_target; + // Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens + let sensitivityRatio: number | undefined + const high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity + const normalTarget = 100 // evaluate high/low temptarget against 100, not scheduled target (which might change) + let halfBasalTarget + if (profile.half_basal_exercise_target) { + halfBasalTarget = profile.half_basal_exercise_target } else { - halfBasalTarget = 160; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + halfBasalTarget = 160 // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limited to autosens_max (1.2x by default) } - if (profile.autosens_max !== undefined && high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget - || profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget ) { + if ( + (profile.autosens_max !== undefined && + high_temptarget_raises_sensitivity && + profile.temptargetSet && + target_bg > normalTarget) || + (profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget) + ) { // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 // 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 //sensitivityRatio = 2/(2+(target_bg-normalTarget)/40); - var c = halfBasalTarget - normalTarget; + const c = halfBasalTarget - normalTarget // getting multiplication less or equal to 0 means that we have a really low target with a really low halfBasalTarget // with low TT and lowTTlowersSensitivity we need autosens_max as a value // we use multiplication instead of the division to avoid "division by zero error" - if (c * (c + target_bg-normalTarget) <= 0.0) { - sensitivityRatio = profile.autosens_max!; + if (c * (c + target_bg - normalTarget) <= 0.0) { + sensitivityRatio = profile.autosens_max! } else { - sensitivityRatio = c/(c+target_bg-normalTarget); + sensitivityRatio = c / (c + target_bg - normalTarget) } // limit sensitivityRatio to profile.autosens_max (1.2x by default) - sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max!); - sensitivityRatio = round(sensitivityRatio,2); - process.stderr.write("Sensitivity ratio set to "+sensitivityRatio+" based on temp target of "+target_bg+"; "); + sensitivityRatio = Math.min(sensitivityRatio, profile.autosens_max!) + sensitivityRatio = round(sensitivityRatio, 2) + process.stderr.write(`Sensitivity ratio set to ${sensitivityRatio} based on temp target of ${target_bg}; `) } else if (typeof autosens_data !== 'undefined' && autosens_data) { - sensitivityRatio = autosens_data.ratio; - process.stderr.write("Autosens ratio: "+sensitivityRatio+"; "); + sensitivityRatio = autosens_data.ratio + process.stderr.write(`Autosens ratio: ${sensitivityRatio}; `) } if (sensitivityRatio) { - basal = profile.current_basal * sensitivityRatio; - basal = round_basal(basal, profile); + basal = profile.current_basal * sensitivityRatio + basal = round_basal(basal, profile) if (basal !== profile_current_basal) { - process.stderr.write("Adjusting basal from "+profile_current_basal+" to "+basal+"; "); + process.stderr.write(`Adjusting basal from ${profile_current_basal} to ${basal}; `) } else { - process.stderr.write("Basal unchanged: "+basal+"; "); + process.stderr.write(`Basal unchanged: ${basal}; `) } } -// Conversely, adjust BG target based on autosens ratio if no temp target is running + // Conversely, adjust BG target based on autosens ratio if no temp target is running // adjust min, max, and target BG for sensitivity, such that 50% increase in ISF raises target from 100 to 120 if (profile.temptargetSet) { //process.stderr.write("Temp Target set, not adjusting with autosens; "); } else if (typeof autosens_data !== 'undefined' && autosens_data) { - if ( profile.sensitivity_raises_target && autosens_data.ratio < 1 || profile.resistance_lowers_target && autosens_data.ratio > 1 ) { + if ( + (profile.sensitivity_raises_target && autosens_data.ratio < 1) || + (profile.resistance_lowers_target && autosens_data.ratio > 1) + ) { // with a target of 100, default 0.7-1.2 autosens min/max range would allow a 93-117 target range - min_bg = round((min_bg - 60) / autosens_data.ratio) + 60; - max_bg = round((max_bg - 60) / autosens_data.ratio) + 60; - var new_target_bg = round((target_bg - 60) / autosens_data.ratio) + 60; + min_bg = round((min_bg - 60) / autosens_data.ratio) + 60 + max_bg = round((max_bg - 60) / autosens_data.ratio) + 60 + let new_target_bg = round((target_bg - 60) / autosens_data.ratio) + 60 // don't allow target_bg below 80 - new_target_bg = Math.max(80, new_target_bg); + new_target_bg = Math.max(80, new_target_bg) if (target_bg === new_target_bg) { - process.stderr.write("target_bg unchanged: "+new_target_bg+"; "); + process.stderr.write(`target_bg unchanged: ${new_target_bg}; `) } else { - process.stderr.write("target_bg from "+target_bg+" to "+new_target_bg+"; "); + process.stderr.write(`target_bg from ${target_bg} to ${new_target_bg}; `) } - target_bg = new_target_bg; + target_bg = new_target_bg } } // Raise target for noisy / raw CGM data. if (glucose_status.noise >= 2) { // increase target at least 10% (default 30%) for raw / noisy data - var noisyCGMTargetMultiplier = Math.max( 1.1, profile.noisyCGMTargetMultiplier || 0); + const noisyCGMTargetMultiplier = Math.max(1.1, profile.noisyCGMTargetMultiplier || 0) // don't allow maxRaw above 250 //var maxRaw = Math.min( 250, profile.maxRaw ); - var adjustedMinBG = round(Math.min(200, min_bg * noisyCGMTargetMultiplier )); - var adjustedTargetBG = round(Math.min(200, target_bg * noisyCGMTargetMultiplier )); - var adjustedMaxBG = round(Math.min(200, max_bg * noisyCGMTargetMultiplier )); - process.stderr.write("Raising target_bg for noisy / raw CGM data, from "+target_bg+" to "+adjustedTargetBG+"; "); - min_bg = adjustedMinBG; - target_bg = adjustedTargetBG; - max_bg = adjustedMaxBG; + const adjustedMinBG = round(Math.min(200, min_bg * noisyCGMTargetMultiplier)) + const adjustedTargetBG = round(Math.min(200, target_bg * noisyCGMTargetMultiplier)) + const adjustedMaxBG = round(Math.min(200, max_bg * noisyCGMTargetMultiplier)) + process.stderr.write(`Raising target_bg for noisy / raw CGM data, from ${target_bg} to ${adjustedTargetBG}; `) + min_bg = adjustedMinBG + target_bg = adjustedTargetBG + max_bg = adjustedMaxBG } // min_bg of 90 -> threshold of 65, 100 -> 70 110 -> 75, and 130 -> 85 - var threshold = min_bg - 0.5*(min_bg-40); + const threshold = min_bg - 0.5 * (min_bg - 40) // If iob_data or its required properties are missing, return. // This has to be checked after checking that we're not in one of the CGM-data-related error conditions handled above, // and before attempting to use iob_data below. // Adjust ISF based on sensitivityRatio - var profile_sens = round(profile.sens, 1) - var sens = profile.sens; + const profile_sens = round(profile.sens, 1) + let sens = profile.sens if (typeof autosens_data !== 'undefined' && autosens_data && sensitivityRatio) { - sens = profile.sens / sensitivityRatio; - sens = round(sens, 1); + sens = profile.sens / sensitivityRatio + sens = round(sens, 1) if (sens !== profile_sens) { - process.stderr.write("ISF from "+profile_sens+" to "+sens); + process.stderr.write(`ISF from ${profile_sens} to ${sens}`) } else { - process.stderr.write("ISF unchanged: "+sens); + process.stderr.write(`ISF unchanged: ${sens}`) } //process.stderr.write(" (autosens ratio "+sensitivityRatio+")"); } - console.error("; CR:",profile.carb_ratio); + console.error('; CR:', profile.carb_ratio) - let iob_data = Array.isArray(iobArray) ? iobArray[0] : iobArray + const iob_data = Array.isArray(iobArray) ? iobArray[0] : iobArray - if (! iob_data) { - rT.error ='Error: iob_data undefined. '; - return rT; + if (!iob_data) { + rT.error = 'Error: iob_data undefined. ' + return rT } - if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined' ) { - rT.error ='Error: iob_data missing some property. '; - return rT; + if (typeof iob_data.activity === 'undefined' || typeof iob_data.iob === 'undefined') { + rT.error = 'Error: iob_data missing some property. ' + return rT } -// Compare currenttemp to iob_data.lastTemp and cancel temp if they don't match, as a safety check -// This should occur after checking that we're not in one of the CGM-data-related error conditions handled above, -// and before returning (doing nothing) below if eventualBG is undefined. - var lastTempAge; - if (typeof iob_data.lastTemp !== 'undefined' ) { - lastTempAge = round(( new Date(systemTime).getTime() - iob_data.lastTemp.date ) / 60000); // in minutes + // Compare currenttemp to iob_data.lastTemp and cancel temp if they don't match, as a safety check + // This should occur after checking that we're not in one of the CGM-data-related error conditions handled above, + // and before returning (doing nothing) below if eventualBG is undefined. + let lastTempAge + if (typeof iob_data.lastTemp !== 'undefined') { + lastTempAge = round((new Date(systemTime).getTime() - iob_data.lastTemp.date) / 60000) // in minutes } else { - lastTempAge = 0; + lastTempAge = 0 } //console.error("currenttemp:",currenttemp,"lastTemp:",JSON.stringify(iob_data.lastTemp),"lastTempAge:",lastTempAge,"m"); - var tempModulus = (lastTempAge + currenttemp.duration) % 30; - console.error("currenttemp:",currenttemp,"lastTempAge:",lastTempAge,"m","tempModulus:",tempModulus,"m"); + const tempModulus = (lastTempAge + currenttemp.duration) % 30 + console.error('currenttemp:', currenttemp, 'lastTempAge:', lastTempAge, 'm', 'tempModulus:', tempModulus, 'm') const rTDeliveredAt = deliverAt - rT.temp = 'absolute'; - rT.deliverAt = rTDeliveredAt; - if ( microBolusAllowed && currenttemp && iob_data.lastTemp && currenttemp.rate !== iob_data.lastTemp.rate && lastTempAge > 10 && currenttemp.duration ) { - rT.reason = "Warning: currenttemp rate "+currenttemp.rate+" != lastTemp rate "+iob_data.lastTemp.rate+" from pumphistory; canceling temp"; - return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); - } - if ( currenttemp && iob_data.lastTemp && currenttemp.duration > 0 ) { + rT.temp = 'absolute' + rT.deliverAt = rTDeliveredAt + if ( + microBolusAllowed && + currenttemp && + iob_data.lastTemp && + currenttemp.rate !== iob_data.lastTemp.rate && + lastTempAge > 10 && + currenttemp.duration + ) { + rT.reason = `Warning: currenttemp rate ${currenttemp.rate} != lastTemp rate ${ + iob_data.lastTemp.rate + } from pumphistory; canceling temp` + return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp) + } + if (currenttemp && iob_data.lastTemp && currenttemp.duration > 0) { //console.error(lastTempAge, round(iob_data.lastTemp.duration,1), round(lastTempAge - iob_data.lastTemp.duration,1)); - var lastTempEnded = lastTempAge - iob_data.lastTemp.duration - if ( lastTempEnded > 5 && lastTempAge > 10 ) { - rT.reason = "Warning: currenttemp running but lastTemp from pumphistory ended "+lastTempEnded+"m ago; canceling temp"; + const lastTempEnded = lastTempAge - iob_data.lastTemp.duration + if (lastTempEnded > 5 && lastTempAge > 10) { + rT.reason = `Warning: currenttemp running but lastTemp from pumphistory ended ${ + lastTempEnded + }m ago; canceling temp` //console.error(currenttemp, round(iob_data.lastTemp,1), round(lastTempAge,1)); - return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp); + return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp) } } -// Calculate BGI, deviation, and eventualBG. -// This has to happen after we obtain iob_data + // Calculate BGI, deviation, and eventualBG. + // This has to happen after we obtain iob_data //calculate BG impact: the amount BG "should" be rising or falling based on insulin activity alone - var bgi = round(( -iob_data.activity * sens * 5 ), 2); + const bgi = round(-iob_data.activity * sens * 5, 2) // project deviations for 30 minutes - var deviation = round( 30 / 5 * ( minDelta - bgi ) ); + let deviation = round((30 / 5) * (minDelta - bgi)) // don't overreact to a big negative delta: use minAvgDelta if deviation is negative if (deviation < 0) { - deviation = round( (30 / 5) * ( minAvgDelta - bgi ) ); + deviation = round((30 / 5) * (minAvgDelta - bgi)) // and if deviation is still negative, use long_avgdelta if (deviation < 0) { - deviation = round( (30 / 5) * ( glucose_status.long_avgdelta - bgi ) ); + deviation = round((30 / 5) * (glucose_status.long_avgdelta - bgi)) } } // calculate the naive (bolus calculator math) eventual BG based on net IOB and sensitivity if (iob_data.iob > 0) { - var naive_eventualBG = round( bg - (iob_data.iob * sens) ); - } else { // if IOB is negative, be more conservative and use the lower of sens, profile.sens - naive_eventualBG = round( bg - (iob_data.iob * Math.min(sens, profile.sens) ) ); + var naive_eventualBG = round(bg - iob_data.iob * sens) + } else { + // if IOB is negative, be more conservative and use the lower of sens, profile.sens + naive_eventualBG = round(bg - iob_data.iob * Math.min(sens, profile.sens)) } // and adjust it for the deviation above - var eventualBG = naive_eventualBG + deviation; + let eventualBG = naive_eventualBG + deviation if (typeof eventualBG === 'undefined' || isNaN(eventualBG)) { - rT.error ='Error: could not calculate eventualBG. '; - return rT; + rT.error = 'Error: could not calculate eventualBG. ' + return rT } - var expectedDelta = calculate_expected_delta(target_bg, eventualBG, bgi); + const expectedDelta = calculate_expected_delta(target_bg, eventualBG, bgi) //console.error(reservoir_data); -// Initialize rT (requestedTemp) object. Has to be done after eventualBG is calculated. + // Initialize rT (requestedTemp) object. Has to be done after eventualBG is calculated. rT = { - 'temp': 'absolute' - , 'bg': bg - , 'tick': tick - , 'eventualBG': eventualBG - , 'insulinReq': 0 - , 'reservoir' : reservoir_data // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from right before the last pumphistory run) - , 'deliverAt' : deliverAt // The time at which the microbolus should be delivered - , 'sensitivityRatio' : sensitivityRatio // autosens ratio (fraction of normal basal) - }; - -// Generate predicted future BGs based on IOB, COB, and current absorption rate - -// Initialize and calculate variables used for predicting BGs - var COBpredBGs: number[] = []; - var IOBpredBGs: number[] = []; - var UAMpredBGs: number[] = []; - var ZTpredBGs: number[] = []; - COBpredBGs.push(bg); - IOBpredBGs.push(bg); - ZTpredBGs.push(bg); - UAMpredBGs.push(bg); - - var enableSMB = enable_smb( - profile, - microBolusAllowed, - meal_data, - bg, - target_bg, - high_bg - ); + temp: 'absolute', + bg: bg, + tick: tick, + eventualBG: eventualBG, + insulinReq: 0, + reservoir: reservoir_data, // The expected reservoir volume at which to deliver the microbolus (the reservoir volume from right before the last pumphistory run) + deliverAt: deliverAt, // The time at which the microbolus should be delivered + sensitivityRatio: sensitivityRatio, // autosens ratio (fraction of normal basal) + } + + // Generate predicted future BGs based on IOB, COB, and current absorption rate + + // Initialize and calculate variables used for predicting BGs + const COBpredBGs: number[] = [] + const IOBpredBGs: number[] = [] + const UAMpredBGs: number[] = [] + const ZTpredBGs: number[] = [] + COBpredBGs.push(bg) + IOBpredBGs.push(bg) + ZTpredBGs.push(bg) + UAMpredBGs.push(bg) + + let enableSMB = enable_smb(profile, microBolusAllowed, meal_data, bg, target_bg, high_bg) // enable UAM (if enabled in preferences) - var enableUAM = profile.enableUAM; - + const enableUAM = profile.enableUAM //console.error(meal_data); // carb impact and duration are 0 unless changed below - var ci = 0; - var cid = 0; + let ci = 0 + let cid = 0 // calculate current carb absorption rate, and how long to absorb all carbs // CI = current carb impact on BG in mg/dL/5m - ci = round((minDelta - bgi),1); - var uci = round((minDelta - bgi),1); + ci = round(minDelta - bgi, 1) + const uci = round(minDelta - bgi, 1) // ISF (mg/dL/U) / CR (g/U) = CSF (mg/dL/g) // use autosens-adjusted sens to counteract autosens meal insulin dosing adjustments so that // autotuned CR is still in effect even when basals and ISF are being adjusted by TT or autosens // this avoids overdosing insulin for large meals when low temp targets are active - var csf = sens / profile.carb_ratio; - console.error("profile.sens:",profile.sens,"sens:",sens,"CSF:",csf); + const csf = sens / profile.carb_ratio + console.error('profile.sens:', profile.sens, 'sens:', sens, 'CSF:', csf) - var maxCarbAbsorptionRate = 30; // g/h; maximum rate to assume carbs will absorb if no CI observed + const maxCarbAbsorptionRate = 30 // g/h; maximum rate to assume carbs will absorb if no CI observed // limit Carb Impact to maxCarbAbsorptionRate * csf in mg/dL per 5m - var maxCI = round(maxCarbAbsorptionRate*csf*5/60,1) + const maxCI = round((maxCarbAbsorptionRate * csf * 5) / 60, 1) if (ci > maxCI) { - console.error("Limiting carb impact from",ci,"to",maxCI,"mg/dL/5m (",maxCarbAbsorptionRate,"g/h )"); - ci = maxCI; + console.error('Limiting carb impact from', ci, 'to', maxCI, 'mg/dL/5m (', maxCarbAbsorptionRate, 'g/h )') + ci = maxCI } - var remainingCATimeMin = 3; // h; minimum duration of expected not-yet-observed carb absorption + let remainingCATimeMin = 3 // h; minimum duration of expected not-yet-observed carb absorption // adjust remainingCATime (instead of CR) for autosens if sensitivityRatio defined - if (sensitivityRatio){ - remainingCATimeMin = remainingCATimeMin / sensitivityRatio; + if (sensitivityRatio) { + remainingCATimeMin = remainingCATimeMin / sensitivityRatio } // 20 g/h means that anything <= 60g will get a remainingCATimeMin, 80g will get 4h, and 120g 6h // when actual absorption ramps up it will take over from remainingCATime - var assumedCarbAbsorptionRate = 20; // g/h; maximum rate to assume carbs will absorb if no CI observed - var remainingCATime = remainingCATimeMin; + const assumedCarbAbsorptionRate = 20 // g/h; maximum rate to assume carbs will absorb if no CI observed + let remainingCATime = remainingCATimeMin if (meal_data.carbs) { // if carbs * assumedCarbAbsorptionRate > remainingCATimeMin, raise it // so <= 90g is assumed to take 3h, and 120g=4h - remainingCATimeMin = Math.max(remainingCATimeMin, meal_data.mealCOB/assumedCarbAbsorptionRate); - var lastCarbAge = round(( new Date(systemTime).getTime() - meal_data.lastCarbTime ) / 60000); + remainingCATimeMin = Math.max(remainingCATimeMin, meal_data.mealCOB / assumedCarbAbsorptionRate) + const lastCarbAge = round((new Date(systemTime).getTime() - meal_data.lastCarbTime) / 60000) //console.error(meal_data.lastCarbTime, lastCarbAge); - var fractionCOBAbsorbed = ( meal_data.carbs - meal_data.mealCOB ) / meal_data.carbs; + const fractionCOBAbsorbed = (meal_data.carbs - meal_data.mealCOB) / meal_data.carbs // if the lastCarbTime was 1h ago, increase remainingCATime by 1.5 hours - remainingCATime = remainingCATimeMin + 1.5 * lastCarbAge/60; - remainingCATime = round(remainingCATime,1); + remainingCATime = remainingCATimeMin + (1.5 * lastCarbAge) / 60 + remainingCATime = round(remainingCATime, 1) //console.error(fractionCOBAbsorbed, remainingCATimeAdjustment, remainingCATime) - console.error("Last carbs",lastCarbAge,"minutes ago; remainingCATime:",remainingCATime,"hours;",round(fractionCOBAbsorbed*100)+"% carbs absorbed"); + console.error( + 'Last carbs', + lastCarbAge, + 'minutes ago; remainingCATime:', + remainingCATime, + 'hours;', + `${round(fractionCOBAbsorbed * 100)}% carbs absorbed` + ) } // calculate the number of carbs absorbed over remainingCATime hours at current CI // CI (mg/dL/5m) * (5m)/5 (m) * 60 (min/hr) * 4 (h) / 2 (linear decay factor) = total carb impact (mg/dL) - var totalCI = Math.max(0, ci / 5 * 60 * remainingCATime / 2); + const totalCI = Math.max(0, ((ci / 5) * 60 * remainingCATime) / 2) // totalCI (mg/dL) / CSF (mg/dL/g) = total carbs absorbed (g) - var totalCA = totalCI / csf; - var remainingCarbsCap = 90; // default to 90 - var remainingCarbsFraction = 1; + const totalCA = totalCI / csf + let remainingCarbsCap = 90 // default to 90 + let remainingCarbsFraction = 1 if (profile.remainingCarbsCap) { - remainingCarbsCap = Math.min(90,profile.remainingCarbsCap); + remainingCarbsCap = Math.min(90, profile.remainingCarbsCap) } if (profile.remainingCarbsFraction) { - remainingCarbsFraction = Math.min(1,profile.remainingCarbsFraction); + remainingCarbsFraction = Math.min(1, profile.remainingCarbsFraction) } - var remainingCarbsIgnore = 1 - remainingCarbsFraction; - var remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs*remainingCarbsIgnore); - remainingCarbs = Math.min(remainingCarbsCap,remainingCarbs); + const remainingCarbsIgnore = 1 - remainingCarbsFraction + let remainingCarbs = Math.max(0, meal_data.mealCOB - totalCA - meal_data.carbs * remainingCarbsIgnore) + remainingCarbs = Math.min(remainingCarbsCap, remainingCarbs) // assume remainingCarbs will absorb in a /\ shaped bilinear curve // peaking at remainingCATime / 2 and ending at remainingCATime hours // area of the /\ triangle is the same as a remainingCIpeak-height rectangle out to remainingCATime/2 // remainingCIpeak (mg/dL/5m) = remainingCarbs (g) * CSF (mg/dL/g) * 5 (m/5m) * 1h/60m / (remainingCATime/2) (h) - var remainingCIpeak = remainingCarbs * csf * 5 / 60 / (remainingCATime/2); + const remainingCIpeak = (remainingCarbs * csf * 5) / 60 / (remainingCATime / 2) //console.error(profile.min_5m_carbimpact,ci,totalCI,totalCA,remainingCarbs,remainingCI,remainingCATime); // calculate peak deviation in last hour, and slope from that to current deviation - var slopeFromMaxDeviation = round(meal_data.slopeFromMaxDeviation,2); + const slopeFromMaxDeviation = round(meal_data.slopeFromMaxDeviation, 2) // calculate lowest deviation in last hour, and slope from that to current deviation - var slopeFromMinDeviation = round(meal_data.slopeFromMinDeviation,2); + const slopeFromMinDeviation = round(meal_data.slopeFromMinDeviation, 2) // assume deviations will drop back down at least at 1/3 the rate they ramped up - var slopeFromDeviations = Math.min(slopeFromMaxDeviation,-slopeFromMinDeviation/3); + const slopeFromDeviations = Math.min(slopeFromMaxDeviation, -slopeFromMinDeviation / 3) //console.error(slopeFromMaxDeviation); //5m data points = g * (1U/10g) * (40mg/dL/1U) / (mg/dL/5m) @@ -612,654 +650,763 @@ var determine_basal = function determine_basal( // limit cid to remainingCATime hours: the reset goes to remainingCI if (ci === 0) { // avoid divide by zero - cid = 0; + cid = 0 } else { - cid = Math.min(remainingCATime*60/5/2,Math.max(0, meal_data.mealCOB * csf / ci )); + cid = Math.min((remainingCATime * 60) / 5 / 2, Math.max(0, (meal_data.mealCOB * csf) / ci)) } // duration (hours) = duration (5m) * 5 / 60 * 2 (to account for linear decay) - console.error("Carb Impact:",ci,"mg/dL per 5m; CI Duration:",round(cid*5/60*2,1),"hours; remaining CI (",remainingCATime," peak):",round(remainingCIpeak,1),"mg/dL per 5m"); - - var minIOBPredBG = 999; - var minCOBPredBG = 999; - var minUAMPredBG = 999; - var minGuardBG = bg; - var minCOBGuardBG = 999; - var minUAMGuardBG = 999; - var minIOBGuardBG = 999; - var minZTGuardBG = 999; - var minPredBG; - var avgPredBG; - var IOBpredBG = eventualBG; - var maxIOBPredBG = bg; - var maxCOBPredBG = bg; + console.error( + 'Carb Impact:', + ci, + 'mg/dL per 5m; CI Duration:', + round(((cid * 5) / 60) * 2, 1), + 'hours; remaining CI (', + remainingCATime, + ' peak):', + round(remainingCIpeak, 1), + 'mg/dL per 5m' + ) + + let minIOBPredBG = 999 + let minCOBPredBG = 999 + let minUAMPredBG = 999 + let minGuardBG = bg + let minCOBGuardBG = 999 + let minUAMGuardBG = 999 + let minIOBGuardBG = 999 + let minZTGuardBG = 999 + let minPredBG + let avgPredBG + let IOBpredBG = eventualBG + let maxIOBPredBG = bg + let maxCOBPredBG = bg //var maxUAMPredBG = bg; //var eventualPredBG = bg; - var lastIOBpredBG; - var lastCOBpredBG; - var lastUAMpredBG; + let lastIOBpredBG + let lastCOBpredBG + let lastUAMpredBG //var lastZTpredBG; - var UAMduration = 0; - var remainingCItotal = 0; - var remainingCIs: number[] = []; - var predCIs: number[] = []; - var COBpredBG: number | undefined; - var UAMpredBG: number | undefined; + let UAMduration = 0 + let remainingCItotal = 0 + const remainingCIs: number[] = [] + const predCIs: number[] = [] + let COBpredBG: number | undefined + let UAMpredBG: number | undefined try { - (Array.isArray(iobArray) ? iobArray : []).forEach(function(iobTick) { + ;(Array.isArray(iobArray) ? iobArray : []).forEach(iobTick => { //console.error(iobTick); - var predBGI = round(( -iobTick.activity * sens * 5 ), 2); - var predZTBGI = round(( -iobTick.iobWithZeroTemp.activity * sens * 5 ), 2); + const predBGI = round(-iobTick.activity * sens * 5, 2) + const predZTBGI = round(-iobTick.iobWithZeroTemp.activity * sens * 5, 2) // for IOBpredBGs, predicted deviation impact drops linearly from current deviation down to zero // over 60 minutes (data points every 5m) - var predDev = ci * ( 1 - Math.min(1,IOBpredBGs.length/(60/5)) ); - IOBpredBG = IOBpredBGs[IOBpredBGs.length-1] + predBGI + predDev; + const predDev = ci * (1 - Math.min(1, IOBpredBGs.length / (60 / 5))) + IOBpredBG = IOBpredBGs[IOBpredBGs.length - 1] + predBGI + predDev // calculate predBGs with long zero temp without deviations - var ZTpredBG = ZTpredBGs[ZTpredBGs.length-1] + predZTBGI; + const ZTpredBG = ZTpredBGs[ZTpredBGs.length - 1] + predZTBGI // for COBpredBGs, predicted carb impact drops linearly from current carb impact down to zero // eventually accounting for all carbs (if they can be absorbed over DIA) - var predCI = Math.max(0, Math.max(0,ci) * ( 1 - COBpredBGs.length/Math.max(cid*2,1) ) ); + const predCI = Math.max(0, Math.max(0, ci) * (1 - COBpredBGs.length / Math.max(cid * 2, 1))) // if any carbs aren't absorbed after remainingCATime hours, assume they'll absorb in a /\ shaped // bilinear curve peaking at remainingCIpeak at remainingCATime/2 hours (remainingCATime/2*12 * 5m) // and ending at remainingCATime h (remainingCATime*12 * 5m intervals) - var intervals = Math.min( COBpredBGs.length, (remainingCATime*12)-COBpredBGs.length ); - var remainingCI = Math.max(0, intervals / (remainingCATime/2*12) * remainingCIpeak ); - remainingCItotal += predCI+remainingCI; - remainingCIs.push(round(remainingCI,0)); - predCIs.push(round(predCI,0)); + const intervals = Math.min(COBpredBGs.length, remainingCATime * 12 - COBpredBGs.length) + const remainingCI = Math.max(0, (intervals / ((remainingCATime / 2) * 12)) * remainingCIpeak) + remainingCItotal += predCI + remainingCI + remainingCIs.push(round(remainingCI, 0)) + predCIs.push(round(predCI, 0)) //process.stderr.write(round(predCI,1)+"+"+round(remainingCI,1)+" "); - COBpredBG = COBpredBGs[COBpredBGs.length-1] + predBGI + Math.min(0,predDev) + predCI + remainingCI; + COBpredBG = COBpredBGs[COBpredBGs.length - 1] + predBGI + Math.min(0, predDev) + predCI + remainingCI // for UAMpredBGs, predicted carb impact drops at slopeFromDeviations // calculate predicted CI from UAM based on slopeFromDeviations - var predUCIslope = Math.max(0, uci + ( UAMpredBGs.length*slopeFromDeviations ) ); + const predUCIslope = Math.max(0, uci + UAMpredBGs.length * slopeFromDeviations) // if slopeFromDeviations is too flat, predicted deviation impact drops linearly from // current deviation down to zero over 3h (data points every 5m) - var predUCImax = Math.max(0, uci * ( 1 - UAMpredBGs.length/Math.max(3*60/5,1) ) ); + const predUCImax = Math.max(0, uci * (1 - UAMpredBGs.length / Math.max((3 * 60) / 5, 1))) //console.error(predUCIslope, predUCImax); // predicted CI from UAM is the lesser of CI based on deviationSlope or DIA - var predUCI = Math.min(predUCIslope, predUCImax); - if(predUCI>0) { + const predUCI = Math.min(predUCIslope, predUCImax) + if (predUCI > 0) { //console.error(UAMpredBGs.length,slopeFromDeviations, predUCI); - UAMduration=round((UAMpredBGs.length+1)*5/60,1); + UAMduration = round(((UAMpredBGs.length + 1) * 5) / 60, 1) } - UAMpredBG = UAMpredBGs[UAMpredBGs.length-1] + predBGI + Math.min(0, predDev) + predUCI; + UAMpredBG = UAMpredBGs[UAMpredBGs.length - 1] + predBGI + Math.min(0, predDev) + predUCI //console.error(predBGI, predCI, predUCI); // truncate all BG predictions at 4 hours - if ( IOBpredBGs.length < 48) { IOBpredBGs.push(IOBpredBG); } - if ( COBpredBGs.length < 48) { COBpredBGs.push(COBpredBG); } - if ( UAMpredBGs.length < 48) { UAMpredBGs.push(UAMpredBG); } - if ( ZTpredBGs.length < 48) { ZTpredBGs.push(ZTpredBG); } + if (IOBpredBGs.length < 48) { + IOBpredBGs.push(IOBpredBG) + } + if (COBpredBGs.length < 48) { + COBpredBGs.push(COBpredBG) + } + if (UAMpredBGs.length < 48) { + UAMpredBGs.push(UAMpredBG) + } + if (ZTpredBGs.length < 48) { + ZTpredBGs.push(ZTpredBG) + } // calculate minGuardBGs without a wait from COB, UAM, IOB predBGs - if ( COBpredBG < minCOBGuardBG ) { minCOBGuardBG = round(COBpredBG); } - if ( UAMpredBG < minUAMGuardBG ) { minUAMGuardBG = round(UAMpredBG); } - if ( IOBpredBG < minIOBGuardBG ) { minIOBGuardBG = round(IOBpredBG); } - if ( ZTpredBG < minZTGuardBG ) { minZTGuardBG = round(ZTpredBG); } + if (COBpredBG < minCOBGuardBG) { + minCOBGuardBG = round(COBpredBG) + } + if (UAMpredBG < minUAMGuardBG) { + minUAMGuardBG = round(UAMpredBG) + } + if (IOBpredBG < minIOBGuardBG) { + minIOBGuardBG = round(IOBpredBG) + } + if (ZTpredBG < minZTGuardBG) { + minZTGuardBG = round(ZTpredBG) + } // set minPredBGs starting when currently-dosed insulin activity will peak // look ahead 60m (regardless of insulin type) so as to be less aggressive on slower insulins - var insulinPeakTime = 60; + let insulinPeakTime = 60 // add 30m to allow for insulin delivery (SMBs or temps) - insulinPeakTime = 90; - var insulinPeak5m = (insulinPeakTime/60)*12; + insulinPeakTime = 90 + const insulinPeak5m = (insulinPeakTime / 60) * 12 //console.error(insulinPeakTime, insulinPeak5m, profile.insulinPeakTime, profile.curve); // wait 90m before setting minIOBPredBG - if ( IOBpredBGs.length > insulinPeak5m && (IOBpredBG < minIOBPredBG) ) { minIOBPredBG = round(IOBpredBG); } - if ( IOBpredBG > maxIOBPredBG ) { maxIOBPredBG = IOBpredBG; } + if (IOBpredBGs.length > insulinPeak5m && IOBpredBG < minIOBPredBG) { + minIOBPredBG = round(IOBpredBG) + } + if (IOBpredBG > maxIOBPredBG) { + maxIOBPredBG = IOBpredBG + } // wait 85-105m before setting COB and 60m for UAM minPredBGs - if ( (cid || remainingCIpeak > 0) && COBpredBGs.length > insulinPeak5m && (COBpredBG < minCOBPredBG) ) { minCOBPredBG = round(COBpredBG); } - if ( (cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG ) { maxCOBPredBG = COBpredBG; } - if ( enableUAM && UAMpredBGs.length > 12 && (UAMpredBG < minUAMPredBG) ) { minUAMPredBG = round(UAMpredBG); } + if ((cid || remainingCIpeak > 0) && COBpredBGs.length > insulinPeak5m && COBpredBG < minCOBPredBG) { + minCOBPredBG = round(COBpredBG) + } + if ((cid || remainingCIpeak > 0) && COBpredBG > maxIOBPredBG) { + maxCOBPredBG = COBpredBG + } + if (enableUAM && UAMpredBGs.length > 12 && UAMpredBG < minUAMPredBG) { + minUAMPredBG = round(UAMpredBG) + } //if ( enableUAM && UAMpredBG > maxIOBPredBG ) { maxUAMPredBG = UAMpredBG; } - }); + }) // set eventualBG to include effect of carbs //console.error("PredBGs:",JSON.stringify(predBGs)); } catch (e) { console.error(e) - console.error("Problem with iobArray. Optional feature Advanced Meal Assist disabled"); + console.error('Problem with iobArray. Optional feature Advanced Meal Assist disabled') } if (meal_data.mealCOB) { - console.error("predCIs (mg/dL/5m):",predCIs.join(" ")); - console.error("remainingCIs: ",remainingCIs.join(" ")); - } - rT.predBGs = {}; - IOBpredBGs.forEach(function(p, i, theArray) { - theArray[i] = round(Math.min(401,Math.max(39,p))); - }); - for (var i=IOBpredBGs.length-1; i > 12; i--) { - if (IOBpredBGs[i-1] !== IOBpredBGs[i]) { break; } - else { IOBpredBGs.pop(); } - } - rT.predBGs.IOB = IOBpredBGs; - lastIOBpredBG=round(IOBpredBGs[IOBpredBGs.length-1]); - ZTpredBGs.forEach(function(p, i, theArray) { - theArray[i] = round(Math.min(401,Math.max(39,p))); - }); - for (i=ZTpredBGs.length-1; i > 6; i--) { + console.error('predCIs (mg/dL/5m):', predCIs.join(' ')) + console.error('remainingCIs: ', remainingCIs.join(' ')) + } + rT.predBGs = {} + IOBpredBGs.forEach((p, i, theArray) => { + theArray[i] = round(Math.min(401, Math.max(39, p))) + }) + for (var i = IOBpredBGs.length - 1; i > 12; i--) { + if (IOBpredBGs[i - 1] !== IOBpredBGs[i]) { + break + } else { + IOBpredBGs.pop() + } + } + rT.predBGs.IOB = IOBpredBGs + lastIOBpredBG = round(IOBpredBGs[IOBpredBGs.length - 1]) + ZTpredBGs.forEach((p, i, theArray) => { + theArray[i] = round(Math.min(401, Math.max(39, p))) + }) + for (i = ZTpredBGs.length - 1; i > 6; i--) { // stop displaying ZTpredBGs once they're rising and above target - if (ZTpredBGs[i-1] >= ZTpredBGs[i] || ZTpredBGs[i] <= target_bg) { break; } - else { ZTpredBGs.pop(); } + if (ZTpredBGs[i - 1] >= ZTpredBGs[i] || ZTpredBGs[i] <= target_bg) { + break + } else { + ZTpredBGs.pop() + } } - rT.predBGs.ZT = ZTpredBGs; + rT.predBGs.ZT = ZTpredBGs //lastZTpredBG=round(ZTpredBGs[ZTpredBGs.length-1]); - if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { - COBpredBGs.forEach(function(p, i, theArray) { - theArray[i] = round(Math.min(401,Math.max(39,p))); - }); - for (i=COBpredBGs.length-1; i > 12; i--) { - if (COBpredBGs[i-1] !== COBpredBGs[i]) { break; } - else { COBpredBGs.pop(); } + if (meal_data.mealCOB > 0 && (ci > 0 || remainingCIpeak > 0)) { + COBpredBGs.forEach((p, i, theArray) => { + theArray[i] = round(Math.min(401, Math.max(39, p))) + }) + for (i = COBpredBGs.length - 1; i > 12; i--) { + if (COBpredBGs[i - 1] !== COBpredBGs[i]) { + break + } else { + COBpredBGs.pop() + } } - rT.predBGs.COB = COBpredBGs; - lastCOBpredBG=round(COBpredBGs[COBpredBGs.length-1]); - eventualBG = Math.max(eventualBG, round(COBpredBGs[COBpredBGs.length-1]) ); + rT.predBGs.COB = COBpredBGs + lastCOBpredBG = round(COBpredBGs[COBpredBGs.length - 1]) + eventualBG = Math.max(eventualBG, round(COBpredBGs[COBpredBGs.length - 1])) } if (ci > 0 || remainingCIpeak > 0) { if (enableUAM) { - UAMpredBGs.forEach(function(p, i, theArray) { - theArray[i] = round(Math.min(401,Math.max(39,p))); - }); - for (i=UAMpredBGs.length-1; i > 12; i--) { - if (UAMpredBGs[i-1] !== UAMpredBGs[i]) { break; } - else { UAMpredBGs.pop(); } + UAMpredBGs.forEach((p, i, theArray) => { + theArray[i] = round(Math.min(401, Math.max(39, p))) + }) + for (i = UAMpredBGs.length - 1; i > 12; i--) { + if (UAMpredBGs[i - 1] !== UAMpredBGs[i]) { + break + } else { + UAMpredBGs.pop() + } } - rT.predBGs.UAM = UAMpredBGs; - lastUAMpredBG=round(UAMpredBGs[UAMpredBGs.length-1]); - if (UAMpredBGs[UAMpredBGs.length-1]) { - eventualBG = Math.max(eventualBG, round(UAMpredBGs[UAMpredBGs.length-1]) ); + rT.predBGs.UAM = UAMpredBGs + lastUAMpredBG = round(UAMpredBGs[UAMpredBGs.length - 1]) + if (UAMpredBGs[UAMpredBGs.length - 1]) { + eventualBG = Math.max(eventualBG, round(UAMpredBGs[UAMpredBGs.length - 1])) } } // set eventualBG based on COB or UAM predBGs - rT.eventualBG = eventualBG; // for FreeAPS-X needs to be in mg/dL + rT.eventualBG = eventualBG // for FreeAPS-X needs to be in mg/dL } - console.error("UAM Impact:",uci,"mg/dL per 5m; UAM Duration:",UAMduration,"hours"); - + console.error('UAM Impact:', uci, 'mg/dL per 5m; UAM Duration:', UAMduration, 'hours') - minIOBPredBG = Math.max(39,minIOBPredBG); - minCOBPredBG = Math.max(39,minCOBPredBG); - minUAMPredBG = Math.max(39,minUAMPredBG); - minPredBG = round(minIOBPredBG); + minIOBPredBG = Math.max(39, minIOBPredBG) + minCOBPredBG = Math.max(39, minCOBPredBG) + minUAMPredBG = Math.max(39, minUAMPredBG) + minPredBG = round(minIOBPredBG) - var fractionCarbsLeft = meal_data.mealCOB/meal_data.carbs; + const fractionCarbsLeft = meal_data.mealCOB / meal_data.carbs // if we have COB and UAM is enabled, average both - if (UAMpredBG && COBpredBG && minUAMPredBG < 999 && minCOBPredBG < 999 ) { + if (UAMpredBG && COBpredBG && minUAMPredBG < 999 && minCOBPredBG < 999) { // weight COBpredBG vs. UAMpredBG based on how many carbs remain as COB - avgPredBG = round( (1-fractionCarbsLeft)*UAMpredBG + fractionCarbsLeft*COBpredBG ); - // if UAM is disabled, average IOB and COB - } else if (COBpredBG && minCOBPredBG < 999 ) { - avgPredBG = round( (IOBpredBG + COBpredBG)/2 ); - // if we have UAM but no COB, average IOB and UAM - } else if (UAMpredBG && minUAMPredBG < 999 ) { - avgPredBG = round( (IOBpredBG + UAMpredBG)/2 ); + avgPredBG = round((1 - fractionCarbsLeft) * UAMpredBG + fractionCarbsLeft * COBpredBG) + // if UAM is disabled, average IOB and COB + } else if (COBpredBG && minCOBPredBG < 999) { + avgPredBG = round((IOBpredBG + COBpredBG) / 2) + // if we have UAM but no COB, average IOB and UAM + } else if (UAMpredBG && minUAMPredBG < 999) { + avgPredBG = round((IOBpredBG + UAMpredBG) / 2) } else { - avgPredBG = round( IOBpredBG ); + avgPredBG = round(IOBpredBG) } // if avgPredBG is below minZTGuardBG, bring it up to that level - if ( minZTGuardBG > avgPredBG ) { - avgPredBG = minZTGuardBG; + if (minZTGuardBG > avgPredBG) { + avgPredBG = minZTGuardBG } // if we have both minCOBGuardBG and minUAMGuardBG, blend according to fractionCarbsLeft - if ( (cid || remainingCIpeak > 0) ) { - if ( enableUAM ) { - minGuardBG = fractionCarbsLeft*minCOBGuardBG + (1-fractionCarbsLeft)*minUAMGuardBG; + if (cid || remainingCIpeak > 0) { + if (enableUAM) { + minGuardBG = fractionCarbsLeft * minCOBGuardBG + (1 - fractionCarbsLeft) * minUAMGuardBG } else { - minGuardBG = minCOBGuardBG; + minGuardBG = minCOBGuardBG } - } else if ( enableUAM ) { - minGuardBG = minUAMGuardBG; + } else if (enableUAM) { + minGuardBG = minUAMGuardBG } else { - minGuardBG = minIOBGuardBG; + minGuardBG = minIOBGuardBG } - minGuardBG = round(minGuardBG); + minGuardBG = round(minGuardBG) //console.error(minCOBGuardBG, minUAMGuardBG, minIOBGuardBG, minGuardBG); - var minZTUAMPredBG = minUAMPredBG; + let minZTUAMPredBG = minUAMPredBG // if minZTGuardBG is below threshold, bring down any super-high minUAMPredBG by averaging // this helps prevent UAM from giving too much insulin in case absorption falls off suddenly - if ( minZTGuardBG < threshold ) { - minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2; - // if minZTGuardBG is between threshold and target, blend in the averaging - } else if ( minZTGuardBG < target_bg ) { + if (minZTGuardBG < threshold) { + minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2 + // if minZTGuardBG is between threshold and target, blend in the averaging + } else if (minZTGuardBG < target_bg) { // target 100, threshold 70, minZTGuardBG 85 gives 50%: (85-70) / (100-70) - var blendPct = (minZTGuardBG-threshold) / (target_bg-threshold); - var blendedMinZTGuardBG = minUAMPredBG*blendPct + minZTGuardBG*(1-blendPct); - minZTUAMPredBG = (minUAMPredBG + blendedMinZTGuardBG) / 2; + const blendPct = (minZTGuardBG - threshold) / (target_bg - threshold) + const blendedMinZTGuardBG = minUAMPredBG * blendPct + minZTGuardBG * (1 - blendPct) + minZTUAMPredBG = (minUAMPredBG + blendedMinZTGuardBG) / 2 //minZTUAMPredBG = minUAMPredBG - target_bg + minZTGuardBG; - // if minUAMPredBG is below minZTGuardBG, bring minUAMPredBG up by averaging - // this allows more insulin if lastUAMPredBG is below target, but minZTGuardBG is still high - } else if ( minZTGuardBG > minUAMPredBG ) { - minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2; + // if minUAMPredBG is below minZTGuardBG, bring minUAMPredBG up by averaging + // this allows more insulin if lastUAMPredBG is below target, but minZTGuardBG is still high + } else if (minZTGuardBG > minUAMPredBG) { + minZTUAMPredBG = (minUAMPredBG + minZTGuardBG) / 2 } - minZTUAMPredBG = round(minZTUAMPredBG); + minZTUAMPredBG = round(minZTUAMPredBG) //console.error("minUAMPredBG:",minUAMPredBG,"minZTGuardBG:",minZTGuardBG,"minZTUAMPredBG:",minZTUAMPredBG); // if any carbs have been entered recently if (meal_data.carbs) { - // if UAM is disabled, use max of minIOBPredBG, minCOBPredBG - if ( ! enableUAM && minCOBPredBG < 999 ) { - minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG)); - // if we have COB, use minCOBPredBG, or blendedMinPredBG if it's higher - } else if ( minCOBPredBG < 999 ) { + if (!enableUAM && minCOBPredBG < 999) { + minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG)) + // if we have COB, use minCOBPredBG, or blendedMinPredBG if it's higher + } else if (minCOBPredBG < 999) { // calculate blendedMinPredBG based on how many carbs remain as COB - var blendedMinPredBG = fractionCarbsLeft*minCOBPredBG + (1-fractionCarbsLeft)*minZTUAMPredBG; + const blendedMinPredBG = fractionCarbsLeft * minCOBPredBG + (1 - fractionCarbsLeft) * minZTUAMPredBG // if blendedMinPredBG > minCOBPredBG, use that instead - minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG, blendedMinPredBG)); - // if carbs have been entered, but have expired, use minUAMPredBG - } else if ( enableUAM ) { - minPredBG = minZTUAMPredBG; + minPredBG = round(Math.max(minIOBPredBG, minCOBPredBG, blendedMinPredBG)) + // if carbs have been entered, but have expired, use minUAMPredBG + } else if (enableUAM) { + minPredBG = minZTUAMPredBG } else { - minPredBG = minGuardBG; + minPredBG = minGuardBG } - // in pure UAM mode, use the higher of minIOBPredBG,minUAMPredBG - } else if ( enableUAM ) { - minPredBG = round(Math.max(minIOBPredBG,minZTUAMPredBG)); + // in pure UAM mode, use the higher of minIOBPredBG,minUAMPredBG + } else if (enableUAM) { + minPredBG = round(Math.max(minIOBPredBG, minZTUAMPredBG)) } // make sure minPredBG isn't higher than avgPredBG - minPredBG = Math.min( minPredBG, avgPredBG ); + minPredBG = Math.min(minPredBG, avgPredBG) -// Print summary variables based on predBGs etc. + // Print summary variables based on predBGs etc. - process.stderr.write("minPredBG: "+minPredBG+" minIOBPredBG: "+minIOBPredBG+" minZTGuardBG: "+minZTGuardBG); + process.stderr.write(`minPredBG: ${minPredBG} minIOBPredBG: ${minIOBPredBG} minZTGuardBG: ${minZTGuardBG}`) if (minCOBPredBG < 999) { - process.stderr.write(" minCOBPredBG: "+minCOBPredBG); + process.stderr.write(` minCOBPredBG: ${minCOBPredBG}`) } if (minUAMPredBG < 999) { - process.stderr.write(" minUAMPredBG: "+minUAMPredBG); + process.stderr.write(` minUAMPredBG: ${minUAMPredBG}`) } - console.error(" avgPredBG:",avgPredBG,"COB:",meal_data.mealCOB,"/",meal_data.carbs); + console.error(' avgPredBG:', avgPredBG, 'COB:', meal_data.mealCOB, '/', meal_data.carbs) // But if the COB line falls off a cliff, don't trust UAM too much: // use maxCOBPredBG if it's been set and lower than minPredBG - if ( maxCOBPredBG > bg ) { - minPredBG = Math.min(minPredBG, maxCOBPredBG); - } - - rT.COB=meal_data.mealCOB; - rT.IOB=iob_data.iob; - rT.BGI=convert_bg(bgi,profile); - rT.deviation=convert_bg(deviation, profile); - rT.ISF=convert_bg(sens, profile); - rT.CR=round(profile.carb_ratio, 2); - rT.target_bg=convert_bg(target_bg, profile); - rT.reason="COB: " + rT.COB + ", Dev: " + rT.deviation + ", BGI: " + rT.BGI+ ", ISF: " + rT.ISF + ", CR: " + rT.CR + ", minPredBG: " + convert_bg(minPredBG, profile) + ", minGuardBG: " + convert_bg(minGuardBG, profile) + ", IOBpredBG: " + convert_bg(lastIOBpredBG, profile); + if (maxCOBPredBG > bg) { + minPredBG = Math.min(minPredBG, maxCOBPredBG) + } + + rT.COB = meal_data.mealCOB + rT.IOB = iob_data.iob + rT.BGI = convert_bg(bgi, profile) + rT.deviation = convert_bg(deviation, profile) + rT.ISF = convert_bg(sens, profile) + rT.CR = round(profile.carb_ratio, 2) + rT.target_bg = convert_bg(target_bg, profile) + rT.reason = `COB: ${rT.COB}, Dev: ${rT.deviation}, BGI: ${rT.BGI}, ISF: ${rT.ISF}, CR: ${ + rT.CR + }, minPredBG: ${convert_bg(minPredBG, profile)}, minGuardBG: ${convert_bg( + minGuardBG, + profile + )}, IOBpredBG: ${convert_bg(lastIOBpredBG, profile)}` if (lastCOBpredBG && lastCOBpredBG > 0) { - rT.reason += ", COBpredBG: " + convert_bg(lastCOBpredBG, profile); + rT.reason += `, COBpredBG: ${convert_bg(lastCOBpredBG, profile)}` } if (lastUAMpredBG && lastUAMpredBG > 0) { - rT.reason += ", UAMpredBG: " + convert_bg(lastUAMpredBG, profile) + rT.reason += `, UAMpredBG: ${convert_bg(lastUAMpredBG, profile)}` } - rT.reason += "; "; + rT.reason += '; ' -// Use minGuardBG to prevent overdosing in hypo-risk situations + // Use minGuardBG to prevent overdosing in hypo-risk situations // use naive_eventualBG if above 40, but switch to minGuardBG if both eventualBGs hit floor of 39 - var carbsReqBG = naive_eventualBG; - if ( carbsReqBG < 40 ) { - carbsReqBG = Math.min( minGuardBG, carbsReqBG ); + let carbsReqBG = naive_eventualBG + if (carbsReqBG < 40) { + carbsReqBG = Math.min(minGuardBG, carbsReqBG) } - var bgUndershoot = threshold - carbsReqBG; + let bgUndershoot = threshold - carbsReqBG // calculate how long until COB (or IOB) predBGs drop below min_bg - var minutesAboveMinBG = 240; - var minutesAboveThreshold = 240; - if (meal_data.mealCOB > 0 && ( ci > 0 || remainingCIpeak > 0 )) { - for (i=0; i 0 && (ci > 0 || remainingCIpeak > 0)) { + for (i = 0; i < COBpredBGs.length; i++) { //console.error(COBpredBGs[i], min_bg); - if ( COBpredBGs[i] < min_bg ) { - minutesAboveMinBG = 5*i; - break; + if (COBpredBGs[i] < min_bg) { + minutesAboveMinBG = 5 * i + break } } - for (i=0; i(treatment: A): treatment is A & BasalTreatment => Object.prototype.hasOwnProperty.call(treatment, 'rate') -export const isBolusTreatment = (treatment: A): treatment is A & BolusTreatment => Object.prototype.hasOwnProperty.call(treatment, 'insulin') +export const isBasalTreatment = (treatment: A): treatment is A & BasalTreatment => + Object.prototype.hasOwnProperty.call(treatment, 'rate') +export const isBolusTreatment = (treatment: A): treatment is A & BolusTreatment => + Object.prototype.hasOwnProperty.call(treatment, 'insulin') diff --git a/lib/iob/calculate.ts b/lib/iob/calculate.ts index b0228ba49..fe0d58d4d 100644 --- a/lib/iob/calculate.ts +++ b/lib/iob/calculate.ts @@ -1,12 +1,20 @@ -import { Profile } from "../types/Profile"; -import { BolusTreatment, InsulinTreatment, isBolusTreatment } from "./InsulinTreatment"; +import type { Profile } from '../types/Profile' +import type { BolusTreatment, InsulinTreatment } from './InsulinTreatment' +import { isBolusTreatment } from './InsulinTreatment' interface IobCalcResult { - activityContrib?: number; - iobContrib?: number; + activityContrib?: number + iobContrib?: number } -export default function iobCalc(treatment: InsulinTreatment, time: Date | undefined, curve: 'bilinear' | string, dia: number, peak: number, profile: Profile): IobCalcResult { +export default function iobCalc( + treatment: InsulinTreatment, + time: Date | undefined, + curve: 'bilinear' | string, + dia: number, + peak: number, + profile: Profile +): IobCalcResult { // iobCalc returns two variables: // activityContrib = units of treatment.insulin used in previous minute // iobContrib = units of treatment.insulin still remaining at a given point in time @@ -18,132 +26,125 @@ export default function iobCalc(treatment: InsulinTreatment, time: Date | undefi // (which functional form to use is specified in the user's profile) if (isBolusTreatment(treatment)) { - time = time || new Date() - var bolusTime = new Date(treatment.date); - var minsAgo = Math.round((time.getTime() - bolusTime.getTime()) / 1000 / 60); - + const bolusTime = new Date(treatment.date) + const minsAgo = Math.round((time.getTime() - bolusTime.getTime()) / 1000 / 60) if (curve === 'bilinear') { - return iobCalcBilinear(treatment, minsAgo, dia); // no user-specified peak with this model + return iobCalcBilinear(treatment, minsAgo, dia) // no user-specified peak with this model } else { - return iobCalcExponential(treatment, minsAgo, dia, peak, profile); + return iobCalcExponential(treatment, minsAgo, dia, peak, profile) } - - } else { // empty return if (treatment.insulin) == False - return {}; - } + } else { + // empty return if (treatment.insulin) == False + return {} + } } - function iobCalcBilinear(treatment: BolusTreatment, minsAgo: number, dia: number) { - - var default_dia = 3.0 // assumed duration of insulin activity, in hours - var peak = 75; // assumed peak insulin activity, in minutes - var end = 180; // assumed end of insulin activity, in minutes + const default_dia = 3.0 // assumed duration of insulin activity, in hours + const peak = 75 // assumed peak insulin activity, in minutes + const end = 180 // assumed end of insulin activity, in minutes - // Scale minsAgo by the ratio of the default dia / the user's dia - // so the calculations for activityContrib and iobContrib work for + // Scale minsAgo by the ratio of the default dia / the user's dia + // so the calculations for activityContrib and iobContrib work for // other dia values (while using the constants specified above) - var timeScalar = default_dia / dia; - var scaled_minsAgo = timeScalar * minsAgo; + const timeScalar = default_dia / dia + const scaled_minsAgo = timeScalar * minsAgo - - var activityContrib = 0; - var iobContrib = 0; + let activityContrib = 0 + let iobContrib = 0 // Calc percent of insulin activity at peak, and slopes up to and down from peak // Based on area of triangle, because area under the insulin action "curve" must sum to 1 // (length * height) / 2 = area of triangle (1), therefore height (activityPeak) = 2 / length (which in this case is dia, in minutes) // activityPeak scales based on user's dia even though peak and end remain fixed - var activityPeak = 2 / (dia * 60) - var slopeUp = activityPeak / peak - var slopeDown = -1 * (activityPeak / (end - peak)) + const activityPeak = 2 / (dia * 60) + const slopeUp = activityPeak / peak + const slopeDown = -1 * (activityPeak / (end - peak)) if (scaled_minsAgo < peak) { + activityContrib = treatment.insulin * (slopeUp * scaled_minsAgo) - activityContrib = treatment.insulin * (slopeUp * scaled_minsAgo); - - var x1 = (scaled_minsAgo / 5) + 1; // scaled minutes since bolus, pre-peak; divided by 5 to work with coefficients estimated based on 5 minute increments - iobContrib = treatment.insulin * ( (-0.001852*x1*x1) + (0.001852*x1) + 1.000000 ); - + const x1 = scaled_minsAgo / 5 + 1 // scaled minutes since bolus, pre-peak; divided by 5 to work with coefficients estimated based on 5 minute increments + iobContrib = treatment.insulin * (-0.001852 * x1 * x1 + 0.001852 * x1 + 1.0) } else if (scaled_minsAgo < end) { - - var minsPastPeak = scaled_minsAgo - peak - activityContrib = treatment.insulin * (activityPeak + (slopeDown * minsPastPeak)); + const minsPastPeak = scaled_minsAgo - peak + activityContrib = treatment.insulin * (activityPeak + slopeDown * minsPastPeak) - var x2 = ((scaled_minsAgo - peak) / 5); // scaled minutes past peak; divided by 5 to work with coefficients estimated based on 5 minute increments - iobContrib = treatment.insulin * ( (0.001323*x2*x2) + (-0.054233*x2) + 0.555560 ); + const x2 = (scaled_minsAgo - peak) / 5 // scaled minutes past peak; divided by 5 to work with coefficients estimated based on 5 minute increments + iobContrib = treatment.insulin * (0.001323 * x2 * x2 + -0.054233 * x2 + 0.55556) } return { activityContrib: activityContrib, - iobContrib: iobContrib - }; + iobContrib: iobContrib, + } } - function iobCalcExponential(treatment: BolusTreatment, minsAgo: number, dia: number, peak: number, profile: Profile) { - // Use custom peak time (in minutes) if value is valid - if ( profile.curve === "rapid-acting" ) { + if (profile.curve === 'rapid-acting') { if (profile.useCustomPeakTime === true && profile.insulinPeakTime !== undefined) { - if ( profile.insulinPeakTime > 120 ) { - console.error('Setting maximum Insulin Peak Time of 120m for',profile.curve,'insulin'); - peak = 120; - } else if ( profile.insulinPeakTime < 50 ) { - console.error('Setting minimum Insulin Peak Time of 50m for',profile.curve,'insulin'); - peak = 50; + if (profile.insulinPeakTime > 120) { + console.error('Setting maximum Insulin Peak Time of 120m for', profile.curve, 'insulin') + peak = 120 + } else if (profile.insulinPeakTime < 50) { + console.error('Setting minimum Insulin Peak Time of 50m for', profile.curve, 'insulin') + peak = 50 } else { - peak = profile.insulinPeakTime; + peak = profile.insulinPeakTime } } else { - peak = 75; + peak = 75 } - } else if ( profile.curve === "ultra-rapid" ) { + } else if (profile.curve === 'ultra-rapid') { if (profile.useCustomPeakTime === true && profile.insulinPeakTime !== undefined) { - if ( profile.insulinPeakTime > 100 ) { - console.error('Setting maximum Insulin Peak Time of 100m for',profile.curve,'insulin'); - peak = 100; - } else if ( profile.insulinPeakTime < 35 ) { - console.error('Setting minimum Insulin Peak Time of 35m for',profile.curve,'insulin'); - peak = 35; + if (profile.insulinPeakTime > 100) { + console.error('Setting maximum Insulin Peak Time of 100m for', profile.curve, 'insulin') + peak = 100 + } else if (profile.insulinPeakTime < 35) { + console.error('Setting minimum Insulin Peak Time of 35m for', profile.curve, 'insulin') + peak = 35 } else { - peak = profile.insulinPeakTime; + peak = profile.insulinPeakTime } } else { - peak = 55; + peak = 55 } } else { - console.error('Curve of',profile.curve,'is not supported.'); + console.error('Curve of', profile.curve, 'is not supported.') } - var end = dia * 60; // end of insulin activity, in minutes + const end = dia * 60 // end of insulin activity, in minutes - - var activityContrib = 0; - var iobContrib = 0; + let activityContrib = 0 + let iobContrib = 0 if (minsAgo < end) { - // Formula source: https://github.com/LoopKit/Loop/issues/388#issuecomment-317938473 // Mapping of original source variable names to those used here: // td = end // tp = peak // t = minsAgo - var tau = peak * (1 - peak / end) / (1 - 2 * peak / end); // time constant of exponential decay - var a = 2 * tau / end; // rise time factor - var S = 1 / (1 - a + (1 + a) * Math.exp(-end / tau)); // auxiliary scale factor - - activityContrib = treatment.insulin * (S / Math.pow(tau, 2)) * minsAgo * (1 - minsAgo / end) * Math.exp(-minsAgo / tau); - iobContrib = treatment.insulin * (1 - S * (1 - a) * ((Math.pow(minsAgo, 2) / (tau * end * (1 - a)) - minsAgo / tau - 1) * Math.exp(-minsAgo / tau) + 1)); + const tau = (peak * (1 - peak / end)) / (1 - (2 * peak) / end) // time constant of exponential decay + const a = (2 * tau) / end // rise time factor + const S = 1 / (1 - a + (1 + a) * Math.exp(-end / tau)) // auxiliary scale factor + + activityContrib = + treatment.insulin * (S / Math.pow(tau, 2)) * minsAgo * (1 - minsAgo / end) * Math.exp(-minsAgo / tau) + iobContrib = + treatment.insulin * + (1 - + S * + (1 - a) * + ((Math.pow(minsAgo, 2) / (tau * end * (1 - a)) - minsAgo / tau - 1) * Math.exp(-minsAgo / tau) + 1)) //console.error('DIA: ' + dia + ' minsAgo: ' + minsAgo + ' end: ' + end + ' peak: ' + peak + ' tau: ' + tau + ' a: ' + a + ' S: ' + S + ' activityContrib: ' + activityContrib + ' iobContrib: ' + iobContrib); } return { activityContrib: activityContrib, - iobContrib: iobContrib - }; + iobContrib: iobContrib, + } } - -exports = module.exports = iobCalc; +exports = module.exports = iobCalc diff --git a/lib/iob/history.ts b/lib/iob/history.ts index 834e7b41b..bcdb95b9c 100644 --- a/lib/iob/history.ts +++ b/lib/iob/history.ts @@ -1,12 +1,12 @@ -import * as basalprofile from '../profile/basal' -import { Profile } from '../types/Profile'; -import { tz } from '../date'; -import { PumpHistoryEvent } from '../types/PumpHistoryEvent'; -import { NightscoutTreatment } from '../types/NightscoutTreatment'; import * as t from 'io-ts' -import { Autosens } from '../types/Autosens'; -import { BasalTreatment, BolusTreatment, InsulinTreatment } from "./InsulinTreatment"; +import { tz } from '../date' import * as date from '../date' +import * as basalprofile from '../profile/basal' +import { Autosens } from '../types/Autosens' +import { NightscoutTreatment } from '../types/NightscoutTreatment' +import { Profile } from '../types/Profile' +import { PumpHistoryEvent } from '../types/PumpHistoryEvent' +import type { BasalTreatment, BolusTreatment, InsulinTreatment } from './InsulinTreatment' interface Splitter { type: 'recurring' @@ -22,60 +22,59 @@ interface PumpSuspendResume { const Input = t.intersection([ t.type({ - history: t.array(t.union([ - NightscoutTreatment, - PumpHistoryEvent - ])), + history: t.array(t.union([NightscoutTreatment, PumpHistoryEvent])), profile: Profile, }), t.partial({ - history24: t.array(t.union([ - NightscoutTreatment, - PumpHistoryEvent - ])), + history24: t.array(t.union([NightscoutTreatment, PumpHistoryEvent])), autosens: Autosens, clock: t.string, - }) + }), ]) export type Input = t.TypeOf function splitTimespanWithOneSplitter(event: BasalTreatment, splitter: Splitter) { - if (splitter.type !== 'recurring') { return [event] } - var startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes(); - var endMinutes = startMinutes + event.duration; + const startMinutes = event.started_at.getHours() * 60 + event.started_at.getMinutes() + const endMinutes = startMinutes + event.duration - if (! (event.duration > 30 || (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || (endMinutes > 1440 && splitter.minutes < (endMinutes - 1440)))) { + if ( + !( + event.duration > 30 || + (startMinutes < splitter.minutes && endMinutes > splitter.minutes) || + (endMinutes > 1440 && splitter.minutes < endMinutes - 1440) + ) + ) { return [event] } // 1440 = one day; no clean way to check if the event overlaps midnight // so checking if end of event in minutes is past midnight - var event1Duration = 0; + let event1Duration = 0 if (event.duration > 30) { - event1Duration = 30; + event1Duration = 30 } else { - var splitPoint = splitter.minutes; + let splitPoint = splitter.minutes if (endMinutes > 1440) { - splitPoint = 1440; + splitPoint = 1440 } - event1Duration = splitPoint - startMinutes; + event1Duration = splitPoint - startMinutes } const event1EndDate = new Date(event.started_at) event1EndDate.setMinutes(event1EndDate.getMinutes() + event1Duration) - var event1 = { + const event1 = { ...event, - duration: event1Duration - }; - var event2 = { + duration: event1Duration, + } + const event2 = { ...event, duration: event.duration - event1Duration, timestamp: date.format(event1EndDate), @@ -83,149 +82,160 @@ function splitTimespanWithOneSplitter(event: BasalTreatment, splitter: Splitter) date: event1EndDate.getTime(), } - return [event1, event2]; + return [event1, event2] } function splitTimespan(event: BasalTreatment, splitterMoments: Splitter[]) { + let results = [event] - var results = [event]; - - var splitFound = true; + let splitFound = true - while(splitFound) { - - let resultArray: BasalTreatment[] = []; - splitFound = false; + while (splitFound) { + let resultArray: BasalTreatment[] = [] + splitFound = false for (let i = 0; i < results.length; i++) { const o = results[i] for (let j = 0; j < splitterMoments.length; j++) { const p = splitterMoments[j] - var splitResult = splitTimespanWithOneSplitter(o,p); + const splitResult = splitTimespanWithOneSplitter(o, p) if (splitResult.length > 1) { - resultArray = resultArray.concat(splitResult); - splitFound = true; - break; + resultArray = resultArray.concat(splitResult) + splitFound = true + break } } if (!splitFound) { - resultArray = resultArray.concat([o]); - }; + resultArray = resultArray.concat([o]) + } } - results = resultArray; + results = resultArray } - return results; + return results } // Split currentEvent around any conflicting suspends // by removing the time period from the event that // overlaps with any suspend. -function splitAroundSuspends (currentEvent: BasalTreatment, pumpSuspends: PumpSuspendResume[], firstResumeTime: string | undefined, suspendedPrior: boolean, lastSuspendTime: string | undefined, currentlySuspended: boolean): BasalTreatment[] { - var events = []; +function splitAroundSuspends( + currentEvent: BasalTreatment, + pumpSuspends: PumpSuspendResume[], + firstResumeTime: string | undefined, + suspendedPrior: boolean, + lastSuspendTime: string | undefined, + currentlySuspended: boolean +): BasalTreatment[] { + const events = [] // @todo: check why it can be undefined - var firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date(0); - var firstResumeDate = firstResumeStarted.getTime() + const firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date(0) + const firstResumeDate = firstResumeStarted.getTime() // @todo: check why it can be undefined - var lastSuspendStarted = lastSuspendTime ? new Date(lastSuspendTime) : new Date(); - var lastSuspendDate = lastSuspendStarted.getTime(); + const lastSuspendStarted = lastSuspendTime ? new Date(lastSuspendTime) : new Date() + const lastSuspendDate = lastSuspendStarted.getTime() - if (suspendedPrior && (currentEvent.date < firstResumeDate)) { - if ((currentEvent.date+currentEvent.duration*60*1000) < firstResumeDate) { - currentEvent.duration = 0; + if (suspendedPrior && currentEvent.date < firstResumeDate) { + if (currentEvent.date + currentEvent.duration * 60 * 1000 < firstResumeDate) { + currentEvent.duration = 0 } else { - currentEvent.duration = ((currentEvent.date+currentEvent.duration*60*1000)-firstResumeDate)/60/1000; + currentEvent.duration = + (currentEvent.date + currentEvent.duration * 60 * 1000 - firstResumeDate) / 60 / 1000 - currentEvent.started_at = tz(firstResumeStarted); + currentEvent.started_at = tz(firstResumeStarted) currentEvent.date = firstResumeDate } } - if (currentlySuspended && ((currentEvent.date+currentEvent.duration*60*1000) > lastSuspendDate)) { + if (currentlySuspended && currentEvent.date + currentEvent.duration * 60 * 1000 > lastSuspendDate) { if (currentEvent.date > lastSuspendDate) { - currentEvent.duration = 0; + currentEvent.duration = 0 } else { - currentEvent.duration = (firstResumeDate - currentEvent.date)/60/1000; + currentEvent.duration = (firstResumeDate - currentEvent.date) / 60 / 1000 } } - events.push(currentEvent); + events.push(currentEvent) if (currentEvent.duration === 0) { // bail out rather than wasting time going through the rest of the suspend events - return events; + return events } - for (var i=0; i < pumpSuspends.length; i++) { - var suspend = pumpSuspends[i]; + for (let i = 0; i < pumpSuspends.length; i++) { + const suspend = pumpSuspends[i] - for (var j=0; j < events.length; j++) { - - if ((events[j].date <= suspend.date) && (events[j].date+events[j].duration*60*1000) > suspend.date) { + for (let j = 0; j < events.length; j++) { + if (events[j].date <= suspend.date && events[j].date + events[j].duration * 60 * 1000 > suspend.date) { // event started before the suspend, but finished after the suspend started - if ((events[j].date+events[j].duration*60*1000) > (suspend.date+suspend.duration*60*1000)) { - var event2StartDate = new Date(suspend.started_at) + if (events[j].date + events[j].duration * 60 * 1000 > suspend.date + suspend.duration * 60 * 1000) { + const event2StartDate = new Date(suspend.started_at) event2StartDate.setMinutes(event2StartDate.getMinutes() + suspend.duration) events.push({ ...events[j], timestamp: date.format(event2StartDate), started_at: tz(event2StartDate), - date: suspend.date+suspend.duration*60*1000, - duration: ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000, - }); + date: suspend.date + suspend.duration * 60 * 1000, + duration: + (events[j].date + + events[j].duration * 60 * 1000 - + (suspend.date + suspend.duration * 60 * 1000)) / + 60 / + 1000, + }) } - events[j].duration = (suspend.date-events[j].date)/60/1000; - - } else if ((suspend.date <= events[j].date) && (suspend.date+suspend.duration*60*1000 > events[j].date)) { + events[j].duration = (suspend.date - events[j].date) / 60 / 1000 + } else if (suspend.date <= events[j].date && suspend.date + suspend.duration * 60 * 1000 > events[j].date) { // suspend started before the event, but finished after the event started - - events[j].duration = ((events[j].date+events[j].duration*60*1000) - (suspend.date+suspend.duration*60*1000))/60/1000; + + events[j].duration = + (events[j].date + events[j].duration * 60 * 1000 - (suspend.date + suspend.duration * 60 * 1000)) / + 60 / + 1000 const eventStartDate = new Date(suspend.started_at) - eventStartDate.setMinutes(eventStartDate.getMinutes() + suspend.duration); + eventStartDate.setMinutes(eventStartDate.getMinutes() + suspend.duration) - events[j].timestamp = date.format(eventStartDate); - events[j].started_at = tz(new Date(events[j].timestamp)); - events[j].date = suspend.date + suspend.duration*60*1000; + events[j].timestamp = date.format(eventStartDate) + events[j].started_at = tz(new Date(events[j].timestamp)) + events[j].date = suspend.date + suspend.duration * 60 * 1000 } } } - return events; + return events } - -export default function calcTempTreatments (inputs: Input, zeroTempDuration?: number): InsulinTreatment[] { - let pumpHistory = [...inputs.history, ...(inputs.history24 || [])] - var profile_data = inputs.profile; - var autosens_data = inputs.autosens; - var tempHistory: BasalTreatment[] = []; - var tempBoluses: BolusTreatment[] = []; - var pumpSuspends: PumpSuspendResume[] = []; - var pumpResumes: PumpSuspendResume[] = []; - var suspendedPrior = false; - var firstResumeTime: string | undefined - let lastSuspendTime: string | undefined; - var currentlySuspended = false; +export default function calcTempTreatments(inputs: Input, zeroTempDuration?: number): InsulinTreatment[] { + const pumpHistory = [...inputs.history, ...(inputs.history24 || [])] + const profile_data = inputs.profile + const autosens_data = inputs.autosens + let tempHistory: BasalTreatment[] = [] + const tempBoluses: BolusTreatment[] = [] + let pumpSuspends: PumpSuspendResume[] = [] + let pumpResumes: PumpSuspendResume[] = [] + let suspendedPrior = false + let firstResumeTime: string | undefined + let lastSuspendTime: string | undefined + let currentlySuspended = false // @todo: check if clock can be undefined - var now = tz(inputs.clock ? new Date(inputs.clock) : new Date()); + const now = tz(inputs.clock ? new Date(inputs.clock) : new Date()) - var lastRecordTime = now; + let lastRecordTime = now // Gather the times the pump was suspended and resumed - for (var i=0; i < pumpHistory.length; i++) { - const current = pumpHistory[i]; + for (var i = 0; i < pumpHistory.length; i++) { + const current = pumpHistory[i] - if (! PumpHistoryEvent.is(current) || (current._type !== 'PumpSuspend' && current._type !== 'PumpResume')) { - continue; + if (!PumpHistoryEvent.is(current) || (current._type !== 'PumpSuspend' && current._type !== 'PumpResume')) { + continue } const started_at = tz(new Date(current.timestamp)) @@ -236,96 +246,100 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu duration: 0, } - if (current._type === "PumpSuspend") { - pumpSuspends.push(temp); - } else if (current._type === "PumpResume") { - pumpResumes.push(temp); + if (current._type === 'PumpSuspend') { + pumpSuspends.push(temp) + } else if (current._type === 'PumpResume') { + pumpResumes.push(temp) } } pumpSuspends = pumpSuspends.sort((a, b) => a.date - b.date) - pumpResumes = pumpResumes.sort((a, b) => a.date - b.date) + pumpResumes = pumpResumes.sort((a, b) => a.date - b.date) if (pumpResumes.length > 0) { - firstResumeTime = pumpResumes[0].timestamp; + firstResumeTime = pumpResumes[0].timestamp // Check to see if our first resume was prior to our first suspend // indicating suspend was prior to our first event. - if (pumpSuspends.length === 0 || (pumpResumes[0].date < pumpSuspends[0].date)) { - suspendedPrior = true; + if (pumpSuspends.length === 0 || pumpResumes[0].date < pumpSuspends[0].date) { + suspendedPrior = true } - } - var j=0; // matching pumpResumes entry; + let j = 0 // matching pumpResumes entry; // Match the resumes with the suspends to get durations - for (i=0; i < pumpSuspends.length; i++) { + for (i = 0; i < pumpSuspends.length; i++) { for (; j < pumpResumes.length; j++) { if (pumpResumes[j].date > pumpSuspends[i].date) { - break; + break } } - if ((j >= pumpResumes.length) && !currentlySuspended) { + if (j >= pumpResumes.length && !currentlySuspended) { // even though it isn't the last suspend, we have reached // the final suspend. Set resume last so the // algorithm knows to suspend all the way // through the last record beginning at the last suspend // since we don't have a matching resume. - currentlySuspended = true; - lastSuspendTime = pumpSuspends[i].timestamp; + currentlySuspended = true + lastSuspendTime = pumpSuspends[i].timestamp - break; + break } - pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date)/60/1000; - + pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date) / 60 / 1000 } // These checks indicate something isn't quite aligned. // Perhaps more resumes that suspends or vice versa... - if (!suspendedPrior && !currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+")!"); - } else if (suspendedPrior && !currentlySuspended && ((pumpResumes.length-1) !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to history block!"); - } else if (!suspendedPrior && currentlySuspended && (pumpResumes.length !== (pumpSuspends.length-1))) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended past end of history block!"); - } else if (suspendedPrior && currentlySuspended && (pumpResumes.length !== pumpSuspends.length)) { - console.error("Mismatched number of resumes("+pumpResumes.length+") and suspends("+pumpSuspends.length+") assuming suspended prior to and past end of history block!"); + if (!suspendedPrior && !currentlySuspended && pumpResumes.length !== pumpSuspends.length) { + console.error(`Mismatched number of resumes(${pumpResumes.length}) and suspends(${pumpSuspends.length})!`) + } else if (suspendedPrior && !currentlySuspended && pumpResumes.length - 1 !== pumpSuspends.length) { + console.error( + `Mismatched number of resumes(${pumpResumes.length}) and suspends(${pumpSuspends.length}) assuming suspended prior to history block!` + ) + } else if (!suspendedPrior && currentlySuspended && pumpResumes.length !== pumpSuspends.length - 1) { + console.error( + `Mismatched number of resumes(${pumpResumes.length}) and suspends(${pumpSuspends.length}) assuming suspended past end of history block!` + ) + } else if (suspendedPrior && currentlySuspended && pumpResumes.length !== pumpSuspends.length) { + console.error( + `Mismatched number of resumes(${pumpResumes.length}) and suspends(${pumpSuspends.length}) assuming suspended prior to and past end of history block!` + ) } - if (i < (pumpSuspends.length-1)) { + if (i < pumpSuspends.length - 1) { // truncate any extra suspends. if we had any extras // the error checks above would have issued a error log message - pumpSuspends.splice(i+1, pumpSuspends.length-i-1); + pumpSuspends.splice(i + 1, pumpSuspends.length - i - 1) } // Pick relevant events for processing and clean the data - for (i=0; i < pumpHistory.length; i++) { - let current: NightscoutTreatment | PumpHistoryEvent = pumpHistory[i]; - if (NightscoutTreatment.is(current) && current.bolus && current.bolus._type === "Bolus") { - current = current.bolus; + for (i = 0; i < pumpHistory.length; i++) { + let current: NightscoutTreatment | PumpHistoryEvent = pumpHistory[i] + if (NightscoutTreatment.is(current) && current.bolus && current.bolus._type === 'Bolus') { + current = current.bolus } const timestamp = NightscoutTreatment.is(current) ? current.created_at : current.timestamp - var currentRecordTime = tz(new Date(timestamp)) + const currentRecordTime = tz(new Date(timestamp)) //console.error(current); //console.error(currentRecordTime,lastRecordTime); // ignore duplicate or out-of-order records (due to 1h and 24h overlap, or timezone changes) if (currentRecordTime > lastRecordTime) { //console.error("",currentRecordTime," > ",lastRecordTime); //process.stderr.write("."); - continue; + continue } else { - lastRecordTime = currentRecordTime; + lastRecordTime = currentRecordTime } - if (PumpHistoryEvent.is(current) && current._type === "Bolus") { - const started_at = tz(new Date(current.timestamp)); + if (PumpHistoryEvent.is(current) && current._type === 'Bolus') { + const started_at = tz(new Date(current.timestamp)) if (started_at > now) { //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at); - process.stderr.write(" "+current.amount+"U @ "+started_at); + process.stderr.write(` ${current.amount}U @ ${started_at}`) } else { // @todo check for undefined insulin tempBoluses.push({ @@ -333,9 +347,15 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu started_at, date: started_at.getTime(), insulin: current.amount!, - }); + }) } - } else if (NightscoutTreatment.is(current) && (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard")) { + } else if ( + NightscoutTreatment.is(current) && + (current.eventType === 'Meal Bolus' || + current.eventType === 'Correction Bolus' || + current.eventType === 'Snack Bolus' || + current.eventType === 'Bolus Wizard') + ) { //imports treatments entered through Nightscout Care Portal //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard const started_at = tz(new Date(current.created_at)) @@ -344,27 +364,31 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu timestamp: current.created_at, started_at, date: started_at.getTime(), - insulin: current.insulin! - }); - } else if (NightscoutTreatment.is(current) && current.enteredBy === "xdrip") { + insulin: current.insulin!, + }) + } else if (NightscoutTreatment.is(current) && current.enteredBy === 'xdrip') { const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ timestamp: current.created_at, started_at, date: started_at.getTime(), - insulin: current.insulin! - }); - } else if (NightscoutTreatment.is(current) && current.enteredBy ==="HAPP_App" && current.insulin) { + insulin: current.insulin!, + }) + } else if (NightscoutTreatment.is(current) && current.enteredBy === 'HAPP_App' && current.insulin) { const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ timestamp: current.created_at, started_at, date: started_at.getTime(), - insulin: current.insulin! - }); - } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal" && (current.enteredBy === "HAPP_App" || current.enteredBy === "openaps://AndroidAPS")) { + insulin: current.insulin!, + }) + } else if ( + NightscoutTreatment.is(current) && + current.eventType === 'Temp Basal' && + (current.enteredBy === 'HAPP_App' || current.enteredBy === 'openaps://AndroidAPS') + ) { const started_at = tz(new Date(current.created_at)) // @todo check for undefined rate and duration tempHistory.push({ @@ -373,15 +397,15 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu date: started_at.getTime(), rate: current.absolute!, duration: current.duration!, - }); - } else if (NightscoutTreatment.is(current) && current.eventType === "Temp Basal") { + }) + } else if (NightscoutTreatment.is(current) && current.eventType === 'Temp Basal') { const started_at = tz(new Date(current.created_at)) let rate = current.rate // Loop reports the amount of insulin actually delivered while the temp basal was running // use that to calculate the effective temp basal rate if (typeof current.amount !== 'undefined') { // @todo: fix type for duration possibly undefined - rate = current.amount / current.duration! * 60; + rate = (current.amount / current.duration!) * 60 } // @todo check for undefined rate and duration tempHistory.push({ @@ -390,27 +414,40 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu date: started_at.getTime(), rate: rate!, duration: current.duration!, - }); - } else if (PumpHistoryEvent.is(current) && current._type === "TempBasal") { + }) + } else if (PumpHistoryEvent.is(current) && current._type === 'TempBasal') { if (current.temp === 'percent') { - continue; + continue } - var rate = current.rate; - let duration; - const previous = i > 0 ? pumpHistory[i-1] : undefined - if (PumpHistoryEvent.is(previous) && previous.timestamp === timestamp && previous._type === "TempBasalDuration") { - duration = previous['duration (min)']; + const rate = current.rate + let duration + const previous = i > 0 ? pumpHistory[i - 1] : undefined + if ( + PumpHistoryEvent.is(previous) && + previous.timestamp === timestamp && + previous._type === 'TempBasalDuration' + ) { + duration = previous['duration (min)'] } else { - for (var iter=0; iter < pumpHistory.length; iter++) { + for (let iter = 0; iter < pumpHistory.length; iter++) { const item = pumpHistory[iter] - if (PumpHistoryEvent.is(item) && item.timestamp === timestamp && item._type === "TempBasalDuration") { - duration = item['duration (min)']; - break; + if ( + PumpHistoryEvent.is(item) && + item.timestamp === timestamp && + item._type === 'TempBasalDuration' + ) { + duration = item['duration (min)'] + break } } if (duration === undefined) { - console.error("No duration found for "+rate+" U/hr basal "+timestamp, pumpHistory[i - 1], current, pumpHistory[i + 1]); + console.error( + `No duration found for ${rate} U/hr basal ${timestamp}`, + pumpHistory[i - 1], + current, + pumpHistory[i + 1] + ) } } @@ -422,34 +459,34 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu date: started_at.getTime(), rate: rate!, duration: duration!, - }); + }) } // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation // start the zero temp 1m in the future to avoid clock skew - const started_at = new Date(now.getTime() + (1 * 60 * 1000)); + const started_at = new Date(now.getTime() + 1 * 60 * 1000) tempHistory.push({ timestamp: started_at.toISOString(), started_at, date: started_at.getTime(), rate: 0, - duration: zeroTempDuration || 0 - }); + duration: zeroTempDuration || 0, + }) } // Check for overlapping events and adjust event lengths in case of overlap tempHistory = tempHistory.sort((a, b) => a.date - b.date) - for (i=0; i < tempHistory.length -1; i++) { + for (i = 0; i < tempHistory.length - 1; i++) { const item = tempHistory[i] - const next = tempHistory[i+1] + const next = tempHistory[i + 1] // @todo: check duration when undefined (or null) - if (item.date + (item.duration || 0) * 60*1000 > next.date) { - tempHistory[i].duration = (next.date - item.date)/60/1000; + if (item.date + (item.duration || 0) * 60 * 1000 > next.date) { + tempHistory[i].duration = (next.date - item.date) / 60 / 1000 // Delete AndroidAPS "Cancel TBR records" in which duration is not populated if (next.duration === null || next.duration === undefined) { - tempHistory.splice(i+1, 1); + tempHistory.splice(i + 1, 1) } } } @@ -464,69 +501,78 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu // iterate through the events and split at basal break points if needed const splitHistoryByBasal = tempHistory.reduce( - (b, o) => ([...b, ...splitTimespan(o, splitterEvents)]), + (b, o) => [...b, ...splitTimespan(o, splitterEvents)], [] as BasalTreatment[] ) // @todo: not necessary: remove tempHistory = tempHistory.sort((a, b) => a.date - b.date) - var suspend_zeros_iob = profile_data.suspend_zeros_iob || false; - let splitHistory = splitHistoryByBasal; + const suspend_zeros_iob = profile_data.suspend_zeros_iob || false + let splitHistory = splitHistoryByBasal if (suspend_zeros_iob) { - // iterate through the events and adjust their + // iterate through the events and adjust their // times as required to account for pump suspends splitHistory = splitHistoryByBasal.reduce( - (b, a) => [...b, ...splitAroundSuspends(a, pumpSuspends, firstResumeTime, suspendedPrior, lastSuspendTime, currentlySuspended)], + (b, a) => [ + ...b, + ...splitAroundSuspends( + a, + pumpSuspends, + firstResumeTime, + suspendedPrior, + lastSuspendTime, + currentlySuspended + ), + ], [] as BasalTreatment[] ) // Any existing temp basals during times the pump was suspended are now deleted // Add 0 temp basals to negate the profile basal rates during times pump is suspended - let zTempSuspendBasals = pumpSuspends.reduce( - (b, a) => [...b, { ...a, rate: 0 }], + const zTempSuspendBasals = pumpSuspends.reduce( + (b, a) => [...b, { ...a, rate: 0 }], [] as (PumpSuspendResume & { rate: number })[] ) // Add temp suspend basal for maximum DIA (8) up to the resume time // if there is no matching suspend in the history before the first // resume - var max_dia_ago = now.getTime() - 8*60*60*1000; + const max_dia_ago = now.getTime() - 8 * 60 * 60 * 1000 // @todo check why firstResumeStarted can be undefined - var firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date(); - var firstResumeDate = firstResumeStarted.getTime() + const firstResumeStarted = firstResumeTime ? new Date(firstResumeTime) : new Date() + const firstResumeDate = firstResumeStarted.getTime() // impact on IOB only matters if the resume occurred // after DIA hours before now. // otherwise, first resume date can be ignored. Whatever // insulin is present prior to resume will be aged // out due to DIA. - if (suspendedPrior && (max_dia_ago < firstResumeDate)) { - - var suspendStart = new Date(max_dia_ago); + if (suspendedPrior && max_dia_ago < firstResumeDate) { + var suspendStart = new Date(max_dia_ago) var suspendStartDate = suspendStart.getTime() - var started_at = tz(suspendStart); + var started_at = tz(suspendStart) zTempSuspendBasals.push({ rate: 0, - duration: (firstResumeDate - max_dia_ago)/60/1000, + duration: (firstResumeDate - max_dia_ago) / 60 / 1000, date: suspendStartDate, started_at, - timestamp: suspendStart.toISOString() + timestamp: suspendStart.toISOString(), }) } if (currentlySuspended) { // @todo check why lastSuspendTime can be undefined - var suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date(); + var suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date() var suspendStartDate = suspendStart.getTime() - var started_at = tz(suspendStart); + var started_at = tz(suspendStart) // @todo check why lastSuspendTime can be undefined zTempSuspendBasals.push({ rate: 0, - duration: (now.getTime() - suspendStartDate)/60/1000, + duration: (now.getTime() - suspendStartDate) / 60 / 1000, date: suspendStartDate, started_at, timestamp: lastSuspendTime!, @@ -536,10 +582,7 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu // Add the new 0 temp basals to the splitHistory. // We have to split the new zero temp basals by the profile // basals just like the other temp basals. - splitHistory = zTempSuspendBasals.reduce( - (b, a) => [...b, ...splitTimespan(a, splitterEvents)], - splitHistory - ) + splitHistory = zTempSuspendBasals.reduce((b, a) => [...b, ...splitTimespan(a, splitterEvents)], splitHistory) } splitHistory = splitHistory.sort((a, b) => a.date - b.date) @@ -548,75 +591,68 @@ export default function calcTempTreatments (inputs: Input, zeroTempDuration?: nu // iterate through the temp basals and create bolus events from temps that affect IOB - for (i=0; i < splitHistory.length; i++) { - - var currentItem = splitHistory[i]; + for (i = 0; i < splitHistory.length; i++) { + const currentItem = splitHistory[i] if (currentItem.duration > 0) { - var target_bg; + var target_bg - var currentRate = profile_data.current_basal; + let currentRate = profile_data.current_basal if (profile_data.basalprofile && profile_data.basalprofile.length > 0) { - currentRate = basalprofile.basalLookup( - profile_data.basalprofile, - new Date(currentItem.timestamp) - ); + currentRate = basalprofile.basalLookup(profile_data.basalprofile, new Date(currentItem.timestamp)) } if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') { - target_bg = (profile_data.min_bg + profile_data.max_bg) / 2; + target_bg = (profile_data.min_bg + profile_data.max_bg) / 2 } //if (profile_data.temptargetSet && target_bg > 110) { - //sensitivityRatio = 2/(2+(target_bg-100)/40); - //currentRate = profile_data.current_basal * sensitivityRatio; + //sensitivityRatio = 2/(2+(target_bg-100)/40); + //currentRate = profile_data.current_basal * sensitivityRatio; //} - var sensitivityRatio; - var profile = profile_data; - var normalTarget = 100; // evaluate high/low temptarget against 100, not scheduled basal (which might change) - if ( profile.half_basal_exercise_target ) { - var halfBasalTarget = profile.half_basal_exercise_target; + var sensitivityRatio + const profile = profile_data + const normalTarget = 100 // evaluate high/low temptarget against 100, not scheduled basal (which might change) + if (profile.half_basal_exercise_target) { + var halfBasalTarget = profile.half_basal_exercise_target } else { - halfBasalTarget = 160 as t.Int; // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + halfBasalTarget = 160 as t.Int // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) } - if ( profile.exercise_mode && profile.temptargetSet && target_bg && target_bg >= normalTarget + 5 ) { + if (profile.exercise_mode && profile.temptargetSet && target_bg && target_bg >= normalTarget + 5) { // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 // 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 - var c = halfBasalTarget - normalTarget; - sensitivityRatio = c/(c+target_bg-normalTarget); - } else if (typeof autosens_data !== 'undefined' ) { - sensitivityRatio = autosens_data.ratio; + const c = halfBasalTarget - normalTarget + sensitivityRatio = c / (c + target_bg - normalTarget) + } else if (typeof autosens_data !== 'undefined') { + sensitivityRatio = autosens_data.ratio //process.stderr.write("Autosens ratio: "+sensitivityRatio+"; "); } // @check why currentRate can be undefined currentRate = currentRate || 0 - if ( sensitivityRatio ) { - currentRate = currentRate * sensitivityRatio; + if (sensitivityRatio) { + currentRate = currentRate * sensitivityRatio } - var netBasalRate = currentItem.rate - currentRate; - const tempBolusSize = netBasalRate < 0 ? -0.05 : 0.05; - var netBasalAmount = Math.round(netBasalRate*currentItem.duration*10/6)/100 - var tempBolusCount = Math.round(netBasalAmount / tempBolusSize); - var tempBolusSpacing = currentItem.duration / tempBolusCount; - for (j=0; j < tempBolusCount; j++) { - const tempBolusDate = currentItem.date + j * tempBolusSpacing*60*1000 + const netBasalRate = currentItem.rate - currentRate + const tempBolusSize = netBasalRate < 0 ? -0.05 : 0.05 + const netBasalAmount = Math.round((netBasalRate * currentItem.duration * 10) / 6) / 100 + const tempBolusCount = Math.round(netBasalAmount / tempBolusSize) + const tempBolusSpacing = currentItem.duration / tempBolusCount + for (j = 0; j < tempBolusCount; j++) { + const tempBolusDate = currentItem.date + j * tempBolusSpacing * 60 * 1000 tempBoluses.push({ insulin: tempBolusSize, date: tempBolusDate, started_at: new Date(tempBolusDate), - timestamp: new Date(tempBolusDate).toISOString() - }); + timestamp: new Date(tempBolusDate).toISOString(), + }) } } } - const all_data = [ - ...tempBoluses, - ...tempHistory - ] + const all_data = [...tempBoluses, ...tempHistory] return all_data.sort((a, b) => a.date - b.date) } -exports = module.exports = calcTempTreatments; +exports = module.exports = calcTempTreatments diff --git a/lib/iob/index.ts b/lib/iob/index.ts index a6073a8fe..40882d472 100644 --- a/lib/iob/index.ts +++ b/lib/iob/index.ts @@ -1,17 +1,18 @@ +import { tz } from '../date' +import type { InsulinTreatment } from './InsulinTreatment' +import { isBasalTreatment, isBolusTreatment } from './InsulinTreatment' import find_insulin from './history' import type { Input } from './history' import sum from './total' -import { tz } from '../date'; -import { InsulinTreatment, isBasalTreatment, isBolusTreatment } from './InsulinTreatment'; interface IOB { - iob: number; - activity: number; - basaliob: number; - bolusiob: number; - netbasalinsulin: number; - bolusinsulin: number; - time: Date; + iob: number + activity: number + basaliob: number + bolusiob: number + netbasalinsulin: number + bolusinsulin: number + time: Date } interface IOBItem extends IOB { @@ -23,46 +24,45 @@ interface IOBItem extends IOB { } } -export default function generate (inputs: Input, currentIOBOnly: boolean = false, treatments?: InsulinTreatment[]) { - +export default function generate(inputs: Input, currentIOBOnly: boolean = false, treatments?: InsulinTreatment[]) { let treatmentsWithZeroTemp: InsulinTreatment[] = [] if (!treatments) { - treatments = find_insulin(inputs); + treatments = find_insulin(inputs) // calculate IOB based on continuous future zero temping as well - treatmentsWithZeroTemp = find_insulin(inputs, 240); + treatmentsWithZeroTemp = find_insulin(inputs, 240) } //console.error(treatments.length, treatmentsWithZeroTemp.length); //console.error(treatments[treatments.length-1], treatmentsWithZeroTemp[treatmentsWithZeroTemp.length-1]) - var opts = { + const opts = { treatments: treatments, profile: inputs.profile, - autosens: inputs.autosens - }; - var optsWithZeroTemp = { + autosens: inputs.autosens, + } + const optsWithZeroTemp = { treatments: treatmentsWithZeroTemp, profile: inputs.profile, - }; + } if (!inputs.clock) { - console.error("Clock is not defined"); + console.error('Clock is not defined') return [] } - var iobArray: IOBItem[] = []; + const iobArray: IOBItem[] = [] //console.error(inputs.clock); - if (! /(Z|[+-][0-2][0-9]:?[034][05])+/.test(inputs.clock) ) { - console.error("Warning: clock input " + inputs.clock + " is unzoned; please pass clock-zoned.json instead"); + if (!/(Z|[+-][0-2][0-9]:?[034][05])+/.test(inputs.clock)) { + console.error(`Warning: clock input ${inputs.clock} is unzoned; please pass clock-zoned.json instead`) } - var clock = tz(new Date(inputs.clock)); + const clock = tz(new Date(inputs.clock)) - var lastBolusTime = new Date(0).getTime(); //clock.getTime()); - var lastTemp = { + let lastBolusTime = new Date(0).getTime() //clock.getTime()); + let lastTemp = { date: new Date(0).getTime(), //clock.getTime()); duration: 0, - }; + } //console.error(treatments[treatments.length-1]); - treatments.forEach(function(treatment) { + treatments.forEach(treatment => { if (isBolusTreatment(treatment) && treatment.insulin > 0) { if (treatment.started_at.getTime() > lastBolusTime) { lastBolusTime = treatment.started_at.getTime() @@ -70,44 +70,44 @@ export default function generate (inputs: Input, currentIOBOnly: boolean = false //lastBolusTime = Math.max(lastBolusTime, treatment.started_at.getTime()); //console.error(treatment.insulin,treatment.started_at,lastBolusTime); } else if (isBasalTreatment(treatment) && treatment.duration > 0) { - if ( treatment.date > lastTemp.date ) { - lastTemp = treatment; - lastTemp.duration = Math.round(lastTemp.duration*100)/100; + if (treatment.date > lastTemp.date) { + lastTemp = treatment + lastTemp.duration = Math.round(lastTemp.duration * 100) / 100 } //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at) } //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at) //if (treatment.insulin && treatment.started_at) { console.error(treatment.insulin,treatment.started_at,lastBolusTime); } - }); + }) - var iStop; + let iStop if (currentIOBOnly) { // for COB calculation, we only need the zeroth element of iobArray - iStop=1 + iStop = 1 } else { // predict IOB out to 4h, regardless of DIA - iStop=4*60; + iStop = 4 * 60 } - for (var i=0; i dia_ago ) { + treatments.forEach(treatment => { + if (treatment.date <= now) { + const dia_ago = now - dia * 60 * 60 * 1000 + if (treatment.date > dia_ago) { // tIOB = total IOB - var tIOB = iobCalc(treatment, time, curve, dia, peak, profile_data); - if (tIOB && tIOB.iobContrib) { iob += tIOB.iobContrib; } - if (tIOB && tIOB.activityContrib) { activity += tIOB.activityContrib; } + const tIOB = iobCalc(treatment, time, curve, dia, peak, profile_data) + if (tIOB && tIOB.iobContrib) { + iob += tIOB.iobContrib + } + if (tIOB && tIOB.activityContrib) { + activity += tIOB.activityContrib + } // basals look like either of these: // {"insulin":-0.05,"date":1507265512363.6365,"created_at":"2017-10-06T04:51:52.363Z"} // {"insulin":0.05,"date":1507266530000,"created_at":"2017-10-06T05:08:50.000Z"} @@ -93,17 +100,17 @@ export default function iobTotal(opts: Options, time: Date) { // {"timestamp":"2017-10-05T22:06:31-07:00","started_at":"2017-10-06T05:06:31.000Z","date":1507266391000,"insulin":0.5} if (isBolusTreatment(treatment) && treatment.insulin && tIOB && tIOB.iobContrib) { if (treatment.insulin < 0.1) { - basaliob += tIOB.iobContrib; - netbasalinsulin += treatment.insulin; + basaliob += tIOB.iobContrib + netbasalinsulin += treatment.insulin } else { - bolusiob += tIOB.iobContrib; - bolusinsulin += treatment.insulin; + bolusiob += tIOB.iobContrib + bolusinsulin += treatment.insulin } } //console.error(JSON.stringify(treatment)); } } // else { console.error("ignoring future treatment:",treatment); } - }); + }) return { iob: Math.round(iob * 1000) / 1000, @@ -112,8 +119,8 @@ export default function iobTotal(opts: Options, time: Date) { bolusiob: Math.round(bolusiob * 1000) / 1000, netbasalinsulin: Math.round(netbasalinsulin * 1000) / 1000, bolusinsulin: Math.round(bolusinsulin * 1000) / 1000, - time: time - }; + time: time, + } } -exports = module.exports = iobTotal; +exports = module.exports = iobTotal diff --git a/lib/meal/history.ts b/lib/meal/history.ts index d9fd041ec..0b8d3967c 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -1,10 +1,10 @@ //import { PumpEntry, PumpEntryBolusWizard } from "../types/PumpEntry.ts.bak"; -import { NightscoutTreatment } from "../types/NightscoutTreatment"; -import { PumpHistoryEvent } from "../types/PumpHistoryEvent"; -import { MealTreatment } from "./MealTreatment"; -import { struct, eqStrict, fromEquals } from 'fp-ts/Eq' import { uniq } from 'fp-ts/Array' +import { struct, eqStrict, fromEquals } from 'fp-ts/Eq' +import { NightscoutTreatment } from '../types/NightscoutTreatment' +import { PumpHistoryEvent } from '../types/PumpHistoryEvent' +import type { MealTreatment } from './MealTreatment' export interface CarbEntry { carbs?: number @@ -17,8 +17,8 @@ export interface Input { } interface TempMealTreatment extends MealTreatment { - hasBolus: boolean, - hasCarbs: boolean, + hasBolus: boolean + hasCarbs: boolean } const createMeal = (timestamp: string, partial: Partial): TempMealTreatment => ({ @@ -32,101 +32,114 @@ const createMeal = (timestamp: string, partial: Partial): TempMea hasCarbs: partial.carbs !== undefined, }) -export default function findMealInputs (inputs: Input): MealTreatment[] { - var pumpHistory = inputs.history; - var carbHistory = inputs.carbs; - let mealInputs: TempMealTreatment[] = []; - var bolusWizardInputs: PumpHistoryEvent[] = []; +export default function findMealInputs(inputs: Input): MealTreatment[] { + const pumpHistory = inputs.history + const carbHistory = inputs.carbs + const mealInputs: TempMealTreatment[] = [] + const bolusWizardInputs: PumpHistoryEvent[] = [] - const timestampEq = fromEquals((a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000) + const timestampEq = fromEquals( + (a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000 + ) - for (var i=0; i < carbHistory.length; i++) { - const current = carbHistory[i]; + for (var i = 0; i < carbHistory.length; i++) { + const current = carbHistory[i] if (current.carbs && current.created_at) { - mealInputs.push(createMeal( - current.created_at, - { + mealInputs.push( + createMeal(current.created_at, { carbs: current.carbs !== null ? current.carbs : undefined, nsCarbs: current.carbs !== null ? current.carbs : undefined, - } - )) + }) + ) } } - for (i=0; i < pumpHistory.length; i++) { - const current = pumpHistory[i]; - if (PumpHistoryEvent.is(current) && current._type === "Bolus" && current.timestamp) { + for (i = 0; i < pumpHistory.length; i++) { + const current = pumpHistory[i] + if (PumpHistoryEvent.is(current) && current._type === 'Bolus' && current.timestamp) { //console.log(pumpHistory[i]); mealInputs.push(createMeal(current.timestamp, { bolus: current.amount })) - } else if (PumpHistoryEvent.is(current) && current._type === "BolusWizard" && current.timestamp) { + } else if (PumpHistoryEvent.is(current) && current._type === 'BolusWizard' && current.timestamp) { // Delay process the BolusWizard entries to make sure we've seen all possible that correspond to the bolus wizard. // More specifically, we need to make sure we process the corresponding bolus entry first. - bolusWizardInputs.push(current); - - } else if (NightscoutTreatment.is(current) && current.created_at && (current.eventType === "Meal Bolus" || current.eventType === "Correction Bolus" || current.eventType === "Snack Bolus" || current.eventType === "Bolus Wizard" || current.eventType === "Carb Correction")) { + bolusWizardInputs.push(current) + } else if ( + NightscoutTreatment.is(current) && + current.created_at && + (current.eventType === 'Meal Bolus' || + current.eventType === 'Correction Bolus' || + current.eventType === 'Snack Bolus' || + current.eventType === 'Bolus Wizard' || + current.eventType === 'Carb Correction') + ) { //imports carbs entered through Nightscout Care Portal //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard // don't enter the treatment if there's another treatment with the same exact timestamp // to prevent duped carb entries from multiple sources - mealInputs.push(createMeal( - current.created_at, { + mealInputs.push( + createMeal(current.created_at, { carbs: current.carbs !== null ? current.carbs : undefined, nsCarbs: current.carbs !== null ? current.carbs : undefined, - } - )) - } else if (NightscoutTreatment.is(current) && current.enteredBy === "xdrip" && current.created_at) { - mealInputs.push(createMeal( - current.created_at, - { + }) + ) + } else if (NightscoutTreatment.is(current) && current.enteredBy === 'xdrip' && current.created_at) { + mealInputs.push( + createMeal(current.created_at, { carbs: current.carbs !== null ? current.carbs : undefined, nsCarbs: current.carbs !== null ? current.carbs : undefined, bolus: current.insulin !== null ? current.insulin : undefined, - } - )) + }) + ) } else if (NightscoutTreatment.is(current) && current.carbs && current.carbs > 0 && current.created_at) { - mealInputs.push(createMeal( - current.created_at, - { + mealInputs.push( + createMeal(current.created_at, { carbs: current.carbs !== null ? current.carbs : undefined, nsCarbs: current.carbs !== null ? current.carbs : undefined, bolus: current.insulin !== null ? current.insulin : undefined, - } - )) - } else if (PumpHistoryEvent.is(current) && current._type === "JournalEntryMealMarker" && current.carb_input && current.carb_input > 0) { - mealInputs.push(createMeal( - current.timestamp, - { + }) + ) + } else if ( + PumpHistoryEvent.is(current) && + current._type === 'JournalEntryMealMarker' && + current.carb_input && + current.carb_input > 0 + ) { + mealInputs.push( + createMeal(current.timestamp, { carbs: current.carb_input, journalCarbs: current.carb_input, - } - )) + }) + ) } } - for(i=0; i < bolusWizardInputs.length; i++) { - const current = bolusWizardInputs[i]; - //console.log(bolusWizardInputs[i]); - const temp = createMeal( - current.timestamp, - { + for (i = 0; i < bolusWizardInputs.length; i++) { + const current = bolusWizardInputs[i] + //console.log(bolusWizardInputs[i]); + const temp = createMeal(current.timestamp, { carbs: current.carb_input, bwCarbs: current.carb_input, - } - ) + }) - // don't enter the treatment if there's another treatment with the same exact timestamp - // to prevent duped carb entries from multiple sources - if (mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasCarbs)) { - continue; - } + // don't enter the treatment if there's another treatment with the same exact timestamp + // to prevent duped carb entries from multiple sources + if (mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasCarbs)) { + continue + } - if (!mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasBolus)) { - console.error("Skipping bolus wizard entry", i, "in the pump history with",current.carb_input,"g carbs and no insulin."); - continue; - } + if (!mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasBolus)) { + console.error( + 'Skipping bolus wizard entry', + i, + 'in the pump history with', + current.carb_input, + 'g carbs and no insulin.' + ) + continue + } - mealInputs.push(temp); + mealInputs.push(temp) } const eq = struct({ @@ -134,7 +147,9 @@ export default function findMealInputs (inputs: Input): MealTreatment[] { carbs: eqStrict, bolus: eqStrict, }) - return uniq(eq)(mealInputs).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()) + return uniq(eq)(mealInputs).sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ) } -exports = module.exports = findMealInputs; +exports = module.exports = findMealInputs diff --git a/lib/meal/index.ts b/lib/meal/index.ts index 8484e9a31..70ca127ab 100644 --- a/lib/meal/index.ts +++ b/lib/meal/index.ts @@ -1,9 +1,10 @@ -import { GlucoseEntry } from '../types/GlucoseEntry'; -import { tz } from '../date'; -import { NightscoutTreatment } from '../types/NightscoutTreatment'; -import { BasalSchedule, Profile } from '../types/Profile'; -import { PumpHistoryEvent } from '../types/PumpHistoryEvent'; -import find_meals, { CarbEntry } from './history' +import { tz } from '../date' +import type { GlucoseEntry } from '../types/GlucoseEntry' +import type { NightscoutTreatment } from '../types/NightscoutTreatment' +import type { BasalSchedule, Profile } from '../types/Profile' +import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' +import type { CarbEntry } from './history' +import find_meals from './history' import sum from './total' interface Input { @@ -15,22 +16,21 @@ interface Input { clock: string } -export default function generate (inputs: Input) { +export default function generate(inputs: Input) { + const treatments = find_meals(inputs) - var treatments = find_meals(inputs); + const opts = { + treatments: treatments, + profile: inputs.profile, + pumphistory: inputs.history, + glucose: inputs.glucose, + basalprofile: inputs.basalprofile, + clock: inputs.clock, + } - var opts = { - treatments: treatments, - profile: inputs.profile, - pumphistory: inputs.history, - glucose: inputs.glucose, - basalprofile: inputs.basalprofile, - clock: inputs.clock - }; + const clock = tz(new Date(inputs.clock)) - var clock = tz(new Date(inputs.clock)); - - return /* meal_data */ sum(opts, clock); + return /* meal_data */ sum(opts, clock) } -exports = module.exports = generate; +exports = module.exports = generate diff --git a/lib/meal/total.ts b/lib/meal/total.ts index 9b4d0023f..54d7cf289 100644 --- a/lib/meal/total.ts +++ b/lib/meal/total.ts @@ -1,10 +1,11 @@ -import detectCarbAbsorption, { DetectCOBInput } from '../determine-basal/cob' -import { GlucoseEntry } from "../types/GlucoseEntry"; -import { tz } from '../date'; -import { NightscoutTreatment } from "../types/NightscoutTreatment"; -import { BasalSchedule, Profile } from "../types/Profile"; -import { PumpHistoryEvent } from "../types/PumpHistoryEvent"; -import { MealTreatment } from "./MealTreatment"; +import { tz } from '../date' +import type { DetectCOBInput } from '../determine-basal/cob' +import detectCarbAbsorption from '../determine-basal/cob' +import type { GlucoseEntry } from '../types/GlucoseEntry' +import type { NightscoutTreatment } from '../types/NightscoutTreatment' +import type { BasalSchedule, Profile } from '../types/Profile' +import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' +import type { MealTreatment } from './MealTreatment' export interface Options { treatments?: Array @@ -16,142 +17,150 @@ export interface Options { } export default function recentCarbs(opts: Options, time: Date) { - var treatments = opts.treatments; - var profile_data = opts.profile; - var glucose_data = opts.glucose; - var carbs = 0; - var nsCarbs = 0; - var bwCarbs = 0; - var journalCarbs = 0; - var bwFound = false; - var mealCarbTime = time.getTime(); - var lastCarbTime = 0; + const treatments = opts.treatments + const profile_data = opts.profile + const glucose_data = opts.glucose + let carbs = 0 + let nsCarbs = 0 + let bwCarbs = 0 + let journalCarbs = 0 + let bwFound = false + const mealCarbTime = time.getTime() + let lastCarbTime = 0 if (!treatments) { - return {}; + return {} } //console.error(glucose_data); - var iob_inputs = { + const iob_inputs = { profile: profile_data, history: opts.pumphistory, - }; - var COB_inputs: DetectCOBInput = { + } + const COB_inputs: DetectCOBInput = { glucose_data: glucose_data || [], iob_inputs: iob_inputs, basalprofile: opts.basalprofile, mealTime: mealCarbTime, - }; - var mealCOB = 0; + } + let mealCOB = 0 // this sorts the treatments collection in order. - treatments.sort(function (a, b) { - var aDate = new Date(a.timestamp); - var bDate = new Date(b.timestamp); + treatments.sort((a, b) => { + const aDate = new Date(a.timestamp) + const bDate = new Date(b.timestamp) //console.error(aDate); - return bDate.getTime() - aDate.getTime(); - }); + return bDate.getTime() - aDate.getTime() + }) - var carbsToRemove = 0; - var nsCarbsToRemove = 0; - var bwCarbsToRemove = 0; - var journalCarbsToRemove = 0; - treatments.forEach(function(treatment) { - var now = time.getTime(); + let carbsToRemove = 0 + let nsCarbsToRemove = 0 + let bwCarbsToRemove = 0 + let journalCarbsToRemove = 0 + treatments.forEach(treatment => { + const now = time.getTime() // consider carbs from up to 6 hours ago in calculating COB - var carbWindow = now - 6 * 60*60*1000; - var treatmentDate = tz(new Date(treatment.timestamp)); - var treatmentTime = treatmentDate.getTime(); + const carbWindow = now - 6 * 60 * 60 * 1000 + const treatmentDate = tz(new Date(treatment.timestamp)) + const treatmentTime = treatmentDate.getTime() if (treatmentTime > carbWindow && treatmentTime <= now) { if (treatment.carbs >= 1) { if (treatment.nsCarbs >= 1) { - nsCarbs += treatment.nsCarbs; + nsCarbs += treatment.nsCarbs } else if (treatment.bwCarbs >= 1) { - bwCarbs += treatment.bwCarbs; - bwFound = true; + bwCarbs += treatment.bwCarbs + bwFound = true } else if (treatment.journalCarbs >= 1) { - journalCarbs += treatment.journalCarbs; + journalCarbs += treatment.journalCarbs } else { - console.error("Treatment carbs unclassified:",treatment); + console.error('Treatment carbs unclassified:', treatment) } //console.error(treatment.carbs, maxCarbs, treatmentDate); - carbs += treatment.carbs; - COB_inputs.mealTime = treatmentTime; - lastCarbTime = Math.max(lastCarbTime,treatmentTime); - var myCarbsAbsorbed = detectCarbAbsorption(COB_inputs).carbsAbsorbed; //?????????????????????????????? here prfile was defined - var myMealCOB = Math.max(0, carbs - myCarbsAbsorbed); - if (typeof(myMealCOB) === 'number' && ! isNaN(myMealCOB)) { - mealCOB = Math.max(mealCOB, myMealCOB); + carbs += treatment.carbs + COB_inputs.mealTime = treatmentTime + lastCarbTime = Math.max(lastCarbTime, treatmentTime) + const myCarbsAbsorbed = detectCarbAbsorption(COB_inputs).carbsAbsorbed //?????????????????????????????? here prfile was defined + const myMealCOB = Math.max(0, carbs - myCarbsAbsorbed) + if (typeof myMealCOB === 'number' && !isNaN(myMealCOB)) { + mealCOB = Math.max(mealCOB, myMealCOB) } else { - console.error("Bad myMealCOB:",myMealCOB, "mealCOB:",mealCOB, "carbs:",carbs,"myCarbsAbsorbed:",myCarbsAbsorbed); + console.error( + 'Bad myMealCOB:', + myMealCOB, + 'mealCOB:', + mealCOB, + 'carbs:', + carbs, + 'myCarbsAbsorbed:', + myCarbsAbsorbed + ) } if (myMealCOB < mealCOB) { - carbsToRemove += treatment.carbs; + carbsToRemove += treatment.carbs if (treatment.nsCarbs >= 1) { - nsCarbsToRemove += treatment.nsCarbs; + nsCarbsToRemove += treatment.nsCarbs } else if (treatment.bwCarbs >= 1) { - bwCarbsToRemove += treatment.bwCarbs; + bwCarbsToRemove += treatment.bwCarbs } else if (treatment.journalCarbs >= 1) { - journalCarbsToRemove += treatment.journalCarbs; + journalCarbsToRemove += treatment.journalCarbs } } else { - carbsToRemove = 0; - nsCarbsToRemove = 0; - bwCarbsToRemove = 0; + carbsToRemove = 0 + nsCarbsToRemove = 0 + bwCarbsToRemove = 0 } //console.error(carbs, carbsToRemove); //console.error("COB:",mealCOB); } } - }); + }) // only include carbs actually used in calculating COB - carbs -= carbsToRemove; - nsCarbs -= nsCarbsToRemove; - bwCarbs -= bwCarbsToRemove; - journalCarbs -= journalCarbsToRemove; + carbs -= carbsToRemove + nsCarbs -= nsCarbsToRemove + bwCarbs -= bwCarbsToRemove + journalCarbs -= journalCarbsToRemove // calculate the current deviation and steepest deviation downslope over the last hour - COB_inputs.ciTime = time.getTime(); + COB_inputs.ciTime = time.getTime() // set mealTime to 6h ago for Deviation calculations - COB_inputs.mealTime = time.getTime() - 6 * 60 * 60 * 1000; - var c = detectCarbAbsorption(COB_inputs); + COB_inputs.mealTime = time.getTime() - 6 * 60 * 60 * 1000 + const c = detectCarbAbsorption(COB_inputs) //console.error(c.currentDeviation, c.slopeFromMaxDeviation); // set a hard upper limit on COB to mitigate impact of erroneous or malicious carb entry if (profile_data.maxCOB !== undefined) { - mealCOB = Math.min( profile_data.maxCOB, mealCOB ); + mealCOB = Math.min(profile_data.maxCOB, mealCOB) } else { - console.error("Bad profile.maxCOB:",profile_data.maxCOB); + console.error('Bad profile.maxCOB:', profile_data.maxCOB) } // if currentDeviation is null or maxDeviation is 0, set mealCOB to 0 for zombie-carb safety - if (typeof(c.currentDeviation) === 'undefined' || c.currentDeviation === null) { - console.error(""); - console.error("Warning: setting mealCOB to 0 because currentDeviation is null/undefined"); - mealCOB = 0; + if (typeof c.currentDeviation === 'undefined' || c.currentDeviation === null) { + console.error('') + console.error('Warning: setting mealCOB to 0 because currentDeviation is null/undefined') + mealCOB = 0 } - if (typeof(c.maxDeviation) === 'undefined' || c.maxDeviation === null) { - console.error(""); - console.error("Warning: setting mealCOB to 0 because maxDeviation is 0 or undefined"); - mealCOB = 0; + if (typeof c.maxDeviation === 'undefined' || c.maxDeviation === null) { + console.error('') + console.error('Warning: setting mealCOB to 0 because maxDeviation is 0 or undefined') + mealCOB = 0 } return { - carbs: Math.round( carbs * 1000 ) / 1000 - , nsCarbs: Math.round( nsCarbs * 1000 ) / 1000 - , bwCarbs: Math.round( bwCarbs * 1000 ) / 1000 - , journalCarbs: Math.round( journalCarbs * 1000 ) / 1000 - , mealCOB: Math.round( mealCOB ) - , currentDeviation: Math.round( c.currentDeviation * 100 ) / 100 - , maxDeviation: Math.round( c.maxDeviation * 100 ) / 100 - , minDeviation: Math.round( c.minDeviation * 100 ) / 100 - , slopeFromMaxDeviation: Math.round( c.slopeFromMaxDeviation * 1000 ) / 1000 - , slopeFromMinDeviation: Math.round( c.slopeFromMinDeviation * 1000 ) / 1000 - , allDeviations: c.allDeviations - , lastCarbTime: lastCarbTime - , bwFound: bwFound - }; + carbs: Math.round(carbs * 1000) / 1000, + nsCarbs: Math.round(nsCarbs * 1000) / 1000, + bwCarbs: Math.round(bwCarbs * 1000) / 1000, + journalCarbs: Math.round(journalCarbs * 1000) / 1000, + mealCOB: Math.round(mealCOB), + currentDeviation: Math.round(c.currentDeviation * 100) / 100, + maxDeviation: Math.round(c.maxDeviation * 100) / 100, + minDeviation: Math.round(c.minDeviation * 100) / 100, + slopeFromMaxDeviation: Math.round(c.slopeFromMaxDeviation * 1000) / 1000, + slopeFromMinDeviation: Math.round(c.slopeFromMinDeviation * 1000) / 1000, + allDeviations: c.allDeviations, + lastCarbTime: lastCarbTime, + bwFound: bwFound, + } } -exports = module.exports = recentCarbs; - +exports = module.exports = recentCarbs diff --git a/lib/medtronic-clock.ts b/lib/medtronic-clock.ts index e55f57c13..9c0a7006c 100644 --- a/lib/medtronic-clock.ts +++ b/lib/medtronic-clock.ts @@ -1,11 +1,10 @@ export default function getTime(minutes: number) { - var baseTime = new Date(); - baseTime.setHours(0); - baseTime.setMinutes(0); - baseTime.setSeconds(0); - - return baseTime.getTime() + minutes * 60 * 1000; -} + const baseTime = new Date() + baseTime.setHours(0) + baseTime.setMinutes(0) + baseTime.setSeconds(0) -exports = module.exports = getTime; + return baseTime.getTime() + minutes * 60 * 1000 +} +exports = module.exports = getTime diff --git a/lib/percentile.ts b/lib/percentile.ts index 74a03565c..b923800cc 100644 --- a/lib/percentile.ts +++ b/lib/percentile.ts @@ -2,18 +2,28 @@ // Returns the value at a given percentile in a sorted numeric array. // "Linear interpolation between closest ranks" method export default function percentile(arr: number[], p: number) { - if (arr.length === 0) return 0; - if (typeof p !== 'number') throw new TypeError('p must be a number'); - if (p <= 0) return arr[0]; - if (p >= 1) return arr[arr.length - 1]; + if (arr.length === 0) { + return 0 + } + if (typeof p !== 'number') { + throw new TypeError('p must be a number') + } + if (p <= 0) { + return arr[0] + } + if (p >= 1) { + return arr[arr.length - 1] + } - var index = arr.length * p, + const index = arr.length * p, lower = Math.floor(index), upper = lower + 1, - weight = index % 1; + weight = index % 1 - if (upper >= arr.length) return arr[lower]; - return arr[lower] * (1 - weight) + arr[upper] * weight; + if (upper >= arr.length) { + return arr[lower] + } + return arr[lower] * (1 - weight) + arr[upper] * weight } exports = module.exports = percentile diff --git a/lib/profile/basal.ts b/lib/profile/basal.ts index 5781fe054..0da6e4ede 100644 --- a/lib/profile/basal.ts +++ b/lib/profile/basal.ts @@ -1,40 +1,39 @@ -import type { BasalSchedule } from "../types/Profile"; +import type { BasalSchedule } from '../types/Profile' /* Return basal rate(U / hr) at the provided timeOfDay */ -export function basalLookup (schedules: BasalSchedule[], now?: Date) { - - var nowDate = now || new Date(); +export function basalLookup(schedules: BasalSchedule[], now?: Date) { + const nowDate = now || new Date() // @todo: check `i` because it can be undefined const basalprofile_data = schedules.sort((a, b) => Number(a.i) - Number(b.i)) - var basalRate = basalprofile_data[basalprofile_data.length-1].rate + let basalRate = basalprofile_data[basalprofile_data.length - 1].rate if (basalRate === 0) { // TODO - shared node - move this print to shared object. - console.error("ERROR: bad basal schedule",schedules); - return; + console.error('ERROR: bad basal schedule', schedules) + return } - var nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes(); + const nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes() - for (var i = 0; i < basalprofile_data.length - 1; i++) { - if ((nowMinutes >= basalprofile_data[i].minutes) && (nowMinutes < basalprofile_data[i + 1].minutes)) { - basalRate = basalprofile_data[i].rate; - break; + for (let i = 0; i < basalprofile_data.length - 1; i++) { + if (nowMinutes >= basalprofile_data[i].minutes && nowMinutes < basalprofile_data[i + 1].minutes) { + basalRate = basalprofile_data[i].rate + break } } - return Math.round(basalRate*1000)/1000; + return Math.round(basalRate * 1000) / 1000 } -export function maxDailyBasal (inputs: { basals: { rate: string | number}[] }): number { - const max = inputs.basals.reduce((b, a) => Number(a.rate) > b ? Number(a.rate) : b, 0) - return (Number(max) *1000)/1000 +export function maxDailyBasal(inputs: { basals: { rate: string | number }[] }): number { + const max = inputs.basals.reduce((b, a) => (Number(a.rate) > b ? Number(a.rate) : b), 0) + return (Number(max) * 1000) / 1000 } /*Return maximum daily basal rate(U / hr) from profile.basals */ -export function maxBasalLookup (inputs: { settings: { maxBasal: number }}): number { - return inputs.settings.maxBasal; +export function maxBasalLookup(inputs: { settings: { maxBasal: number } }): number { + return inputs.settings.maxBasal } -exports.maxDailyBasal = maxDailyBasal; -exports.maxBasalLookup = maxBasalLookup; -exports.basalLookup = basalLookup; +exports.maxDailyBasal = maxDailyBasal +exports.maxBasalLookup = maxBasalLookup +exports.basalLookup = basalLookup diff --git a/lib/profile/carbs.js b/lib/profile/carbs.js deleted file mode 100644 index ad43b5fae..000000000 --- a/lib/profile/carbs.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -var getTime = require('../medtronic-clock'); -var shared_node_utils = require('../bin/utils'); -var console_error = shared_node_utils.console_error; - -function carbRatioLookup (final_result, inputs, profile) { - var now = new Date(); - var carbratio_data = inputs.carbratio; - if (typeof(carbratio_data) !== "undefined" && typeof(carbratio_data.schedule) !== "undefined") { - var carbRatio; - if ((carbratio_data.units === "grams") || (carbratio_data.units === "exchanges")) { - //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset }); - carbRatio = carbratio_data.schedule[carbratio_data.schedule.length - 1]; - - for (var i = 0; i < carbratio_data.schedule.length - 1; i++) { - if ((now >= getTime(carbratio_data.schedule[i].offset)) && (now < getTime(carbratio_data.schedule[i + 1].offset))) { - carbRatio = carbratio_data.schedule[i]; - // disallow impossibly high/low carbRatios due to bad decoding - if (carbRatio < 3 || carbRatio > 150) { - console_error(final_result, "Error: carbRatio of " + carbRatio + " out of bounds."); - return; - } - break; - } - } - if (carbratio_data.units === "exchanges") { - carbRatio.ratio = 12 / carbRatio.ratio - } - return carbRatio.ratio; - } else { - console_error(final_result, "Error: Unsupported carb_ratio units " + carbratio_data.units); - return; - } - //return carbRatio.ratio; - //profile.carbratio = carbRatio.ratio; - } else { return; } -} - -carbRatioLookup.carbRatioLookup = carbRatioLookup; -exports = module.exports = carbRatioLookup; diff --git a/lib/profile/carbs.ts b/lib/profile/carbs.ts new file mode 100644 index 000000000..d0f0c2a49 --- /dev/null +++ b/lib/profile/carbs.ts @@ -0,0 +1,52 @@ +import type { FinalResult } from '../bin/utils' +import { console_error } from '../bin/utils' +import getTime from '../medtronic-clock' +import type { CarbRatioSchedule, Profile } from '../types/Profile' + +interface Inputs { + carbratio?: { + schedule?: CarbRatioSchedule[] + units: 'grams' | 'exchanges' | string + } +} + +export default function carbRatioLookup(final_result: FinalResult, inputs: Inputs, _profile?: Profile) { + const now = new Date() + const carbratio_data = inputs.carbratio + const carbratio_schedule = carbratio_data?.schedule + if (typeof carbratio_data !== 'undefined' && carbratio_schedule) { + if (carbratio_data.units === 'grams' || carbratio_data.units === 'exchanges') { + //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset }); + let carbRatio = carbratio_schedule[carbratio_schedule.length - 1] + + for (let i = 0; i < carbratio_schedule.length - 1; i++) { + if ( + now.getTime() >= getTime(carbratio_schedule[i].offset) && + now.getTime() < getTime(carbratio_schedule[i + 1].offset) + ) { + carbRatio = carbratio_schedule[i] + // disallow impossibly high/low carbRatios due to bad decoding + if (carbRatio.ratio < 3 || carbRatio.ratio > 150) { + console_error(final_result, `Error: carbRatio of ${carbRatio} out of bounds.`) + return + } + break + } + } + if (carbratio_data.units === 'exchanges') { + carbRatio.ratio = 12 / carbRatio.ratio + } + return carbRatio.ratio + } else { + console_error(final_result, `Error: Unsupported carb_ratio units ${carbratio_data.units}`) + return + } + //return carbRatio.ratio; + //profile.carbratio = carbRatio.ratio; + } else { + return + } +} + +carbRatioLookup.carbRatioLookup = carbRatioLookup +exports = module.exports = carbRatioLookup diff --git a/lib/profile/index.js b/lib/profile/index.js deleted file mode 100644 index 79b07d25b..000000000 --- a/lib/profile/index.js +++ /dev/null @@ -1,187 +0,0 @@ -'use strict'; - -var basal = require('./basal'); -var targets = require('./targets'); -var isf = require('./isf'); -var carb_ratios = require('./carbs'); - -var shared_node_utils = require('../bin/utils'); -var console_error = shared_node_utils.console_error; - -function defaults ( ) { - return /* profile */ { - max_iob: 0 // if max_iob is not provided, will default to zero - , max_daily_safety_multiplier: 3 - , current_basal_safety_multiplier: 4 - , autosens_max: 1.2 - , autosens_min: 0.7 - , rewind_resets_autosens: true // reset autosensitivity to neutral for awhile after each pump rewind - // , autosens_adjust_targets: false // when autosens detects sensitivity/resistance, also adjust BG target accordingly - , high_temptarget_raises_sensitivity: false // raise sensitivity for temptargets >= 101. synonym for exercise_mode - , low_temptarget_lowers_sensitivity: false // lower sensitivity for temptargets <= 99. - , sensitivity_raises_target: true // raise BG target when autosens detects sensitivity - , resistance_lowers_target: false // lower BG target when autosens detects resistance - , exercise_mode: false // when true, > 100 mg/dL high temp target adjusts sensitivityRatio for exercise_mode. This majorly changes the behavior of high temp targets from before. synonmym for high_temptarget_raises_sensitivity - , half_basal_exercise_target: 160 // when temptarget is 160 mg/dL *and* exercise_mode=true, run 50% basal at this level (120 = 75%; 140 = 60%) - // create maxCOB and default it to 120 because that's the most a typical body can absorb over 4 hours. - // (If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120. - // Essentially, this just limits AMA/SMB as a safety cap against excessive COB entry) - , maxCOB: 120 - , skip_neutral_temps: false // if true, don't set neutral temps - , unsuspend_if_no_temp: false // if true, pump will un-suspend after a zero temp finishes - , bolussnooze_dia_divisor: 2 // bolus snooze decays after 1/2 of DIA - , min_5m_carbimpact: 8 // mg/dL per 5m (8 mg/dL/5m corresponds to 24g/hr at a CSF of 4 mg/dL/g (x/5*60/4)) - , autotune_isf_adjustmentFraction: 1.0 // keep autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF. 1.0 allows full adjustment, 0 is no adjustment from pump ISF. - , remainingCarbsFraction: 1.0 // fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption - , remainingCarbsCap: 90 // max carbs we'll assume will absorb over 4h if we don't yet see carb absorption - // WARNING: use SMB with caution: it can and will automatically bolus up to max_iob worth of extra insulin - , enableUAM: true // enable detection of unannounced meal carb absorption - , A52_risk_enable: false - , enableSMB_with_COB: false // enable supermicrobolus while COB is positive - , enableSMB_with_temptarget: false // enable supermicrobolus for eating soon temp targets - // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar - // LimiTTer, etc. do not properly filter out high-noise SGVs. xDrip+ builds greater than or equal to - // version number d8e-7097-2018-01-22 provide proper noise values, so that oref0 can ignore high noise - // readings, and can temporarily raise the BG target when sensor readings have medium noise, - // resulting in appropriate SMB behaviour. Older versions of xDrip+ should not be used with enableSMB_always. - // Using SMB overnight with such data sources risks causing a dangerous overdose of insulin - // if the CGM sensor reads falsely high and doesn't come down as actual BG does - , enableSMB_always: false // always enable supermicrobolus (unless disabled by high temptarget) - , enableSMB_after_carbs: false // enable supermicrobolus for 6h after carbs, even with 0 COB - , enableSMB_high_bg: false // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile) - , enableSMB_high_bg_target: 110 // set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable. - // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar. - , allowSMB_with_high_temptarget: false // allow supermicrobolus (if otherwise enabled) even with high temp targets - , maxSMBBasalMinutes: 30 // maximum minutes of basal that can be delivered as a single SMB with uncovered COB - , maxUAMSMBBasalMinutes: 30 // maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB - , SMBInterval: 3 // minimum interval between SMBs, in minutes. - , bolus_increment: 0.1 // minimum bolus that can be delivered as an SMB - , maxDelta_bg_threshold: 0.2 // maximum change in bg to use SMB, above that will disable SMB - , curve: "rapid-acting" // change this to "ultra-rapid" for Fiasp, or "bilinear" for old curve - , useCustomPeakTime: false // allows changing insulinPeakTime - , insulinPeakTime: 75 // number of minutes after a bolus activity peaks. defaults to 55m for Fiasp if useCustomPeakTime: false - , carbsReqThreshold: 1 // grams of carbsReq to trigger a pushover - , offline_hotspot: false // enabled an offline-only local wifi hotspot if no Internet available - , noisyCGMTargetMultiplier: 1.3 // increase target by this amount when looping off raw/noisy CGM data - , suspend_zeros_iob: true // recognize pump suspends as non insulin delivery events - // send the glucose data to the pump emulating an enlite sensor. This allows to setup high / low warnings when offline and see trend. - // To enable this feature, enable the sensor, set a sensor with id 0000000, go to start sensor and press find lost sensor. - , enableEnliteBgproxy: false - // TODO: make maxRaw a preference here usable by oref0-raw in myopenaps-cgm-loop - //, maxRaw: 200 // highest raw/noisy CGM value considered safe to use for looping - , calc_glucose_noise: false - , target_bg: false // set to an integer value in mg/dL to override pump min_bg - , edison_battery_shutdown_voltage: 3050 - , pi_battery_shutdown_percent: 2 - } -} - -function displayedDefaults (final_result) { - var allDefaults = defaults(); - var profile = { }; - - profile.max_iob = allDefaults.max_iob; - profile.max_daily_safety_multiplier = allDefaults.max_daily_safety_multiplier; - profile.current_basal_safety_multiplier= allDefaults.current_basal_safety_multiplier; - profile.autosens_max = allDefaults.autosens_max; - profile.autosens_min = allDefaults.autosens_min; - profile.rewind_resets_autosens = allDefaults.rewind_resets_autosens; - profile.exercise_mode = allDefaults.exercise_mode; - profile.sensitivity_raises_target = allDefaults.sensitivity_raises_target; - profile.unsuspend_if_no_temp = allDefaults.unsuspend_if_no_temp; - profile.enableSMB_with_COB = allDefaults.enableSMB_with_COB; - profile.enableSMB_with_temptarget = allDefaults.enableSMB_with_temptarget; - profile.enableUAM = allDefaults.enableUAM; - profile.curve = allDefaults.curve; - profile.offline_hotspot = allDefaults.offline_hotspot; - profile.edison_battery_shutdown_voltage = allDefaults.edison_battery_shutdown_voltage; - profile.pi_battery_shutdown_percent = allDefaults.pi_battery_shutdown_percent; - - console_error(final_result, profile); - return profile -} - -function generate (final_result, inputs, opts) { - var profile = opts && opts.type ? opts : defaults( ); - - // check if inputs has overrides for any of the default prefs - // and apply if applicable - for (var pref in profile) { - if (inputs.hasOwnProperty(pref)) { - profile[pref] = inputs[pref]; - } - } - - var pumpsettings_data = inputs.settings; - if (inputs.settings.insulin_action_curve > 1) { - profile.dia = pumpsettings_data.insulin_action_curve; - } else { - console_error(final_result, 'DIA of', profile.dia, 'is not supported'); - return -1; - } - - if (inputs.model) { - profile.model = inputs.model; - } - profile.skip_neutral_temps = inputs.skip_neutral_temps; - - profile.current_basal = basal.basalLookup(inputs.basals); - profile.basalprofile = inputs.basals; - - (profile.basalprofile || []).forEach(function(basalentry) { - basalentry.rate = +(Math.round(basalentry.rate + "e+3") + "e-3"); - }); - - profile.max_daily_basal = basal.maxDailyBasal(inputs); - profile.max_basal = basal.maxBasalLookup(inputs); - if (profile.current_basal === 0) { - console_error(final_result, "current_basal of",profile.current_basal,"is not supported"); - return -1; - } - if (profile.max_daily_basal === 0) { - console_error(final_result, "max_daily_basal of",profile.max_daily_basal,"is not supported"); - return -1; - } - if (profile.max_basal < 0.1) { - console_error(final_result, "max_basal of",profile.max_basal,"is not supported"); - return -1; - } - - var range = targets.bgTargetsLookup(final_result, inputs, profile); - profile.out_units = inputs.targets.user_preferred_units; - profile.min_bg = Math.round(range.min_bg); - profile.max_bg = Math.round(range.max_bg); - profile.bg_targets = inputs.targets; - - (profile.bg_targets.targets || []).forEach(function(bg_entry) { - bg_entry.high = Math.round(bg_entry.high); - bg_entry.low = Math.round(bg_entry.low); - bg_entry.min_bg = Math.round(bg_entry.min_bg); - bg_entry.max_bg = Math.round(bg_entry.max_bg); - }); - - delete profile.bg_targets.raw; - - profile.temptargetSet = range.temptargetSet; - var lastResult = null; - [profile.sens, lastResult] = isf.isfLookup(inputs.isf, undefined, lastResult); - profile.isfProfile = inputs.isf; - if (profile.sens < 5) { - console_error(final_result, "ISF of",profile.sens,"is not supported"); - return -1; - } - if (typeof(inputs.carbratio) !== "undefined") { - profile.carb_ratio = carb_ratios.carbRatioLookup(final_result, inputs, profile); - profile.carb_ratios = inputs.carbratio; - } else { - console_error(final_result, "Profile wasn't given carb ratio data, cannot calculate carb_ratio"); - } - - return profile; -} - - -generate.defaults = defaults; -generate.displayedDefaults = displayedDefaults; -exports = module.exports = generate; - diff --git a/lib/profile/index.ts b/lib/profile/index.ts new file mode 100644 index 000000000..a22198bc0 --- /dev/null +++ b/lib/profile/index.ts @@ -0,0 +1,180 @@ +import type { FinalResult } from '../bin/utils' +import { console_error } from '../bin/utils' +import { maxDailyBasal, basalLookup, maxBasalLookup } from './basal' +import carb_ratios from './carbs' +import isf from './isf' +import * as targets from './targets' + +export function defaults() { + return /* profile */ { + max_iob: 0, // if max_iob is not provided, will default to zero + max_daily_safety_multiplier: 3, + current_basal_safety_multiplier: 4, + autosens_max: 1.2, + autosens_min: 0.7, + rewind_resets_autosens: true, // reset autosensitivity to neutral for awhile after each pump rewind + // , autosens_adjust_targets: false // when autosens detects sensitivity/resistance, also adjust BG target accordingly + high_temptarget_raises_sensitivity: false, // raise sensitivity for temptargets >= 101. synonym for exercise_mode + low_temptarget_lowers_sensitivity: false, // lower sensitivity for temptargets <= 99. + sensitivity_raises_target: true, // raise BG target when autosens detects sensitivity + resistance_lowers_target: false, // lower BG target when autosens detects resistance + exercise_mode: false, // when true, > 100 mg/dL high temp target adjusts sensitivityRatio for exercise_mode. This majorly changes the behavior of high temp targets from before. synonmym for high_temptarget_raises_sensitivity + half_basal_exercise_target: 160, // when temptarget is 160 mg/dL *and* exercise_mode=true, run 50% basal at this level (120 = 75%; 140 = 60%) + // create maxCOB and default it to 120 because that's the most a typical body can absorb over 4 hours. + // (If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120. + // Essentially, this just limits AMA/SMB as a safety cap against excessive COB entry) + maxCOB: 120, + skip_neutral_temps: false, // if true, don't set neutral temps + unsuspend_if_no_temp: false, // if true, pump will un-suspend after a zero temp finishes + bolussnooze_dia_divisor: 2, // bolus snooze decays after 1/2 of DIA + min_5m_carbimpact: 8, // mg/dL per 5m (8 mg/dL/5m corresponds to 24g/hr at a CSF of 4 mg/dL/g (x/5*60/4)) + autotune_isf_adjustmentFraction: 1.0, // keep autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF. 1.0 allows full adjustment, 0 is no adjustment from pump ISF. + remainingCarbsFraction: 1.0, // fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption + remainingCarbsCap: 90, // max carbs we'll assume will absorb over 4h if we don't yet see carb absorption + // WARNING: use SMB with caution: it can and will automatically bolus up to max_iob worth of extra insulin + enableUAM: true, // enable detection of unannounced meal carb absorption + A52_risk_enable: false, + enableSMB_with_COB: false, // enable supermicrobolus while COB is positive + enableSMB_with_temptarget: false, // enable supermicrobolus for eating soon temp targets + // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar + // LimiTTer, etc. do not properly filter out high-noise SGVs. xDrip+ builds greater than or equal to + // version number d8e-7097-2018-01-22 provide proper noise values, so that oref0 can ignore high noise + // readings, and can temporarily raise the BG target when sensor readings have medium noise, + // resulting in appropriate SMB behaviour. Older versions of xDrip+ should not be used with enableSMB_always. + // Using SMB overnight with such data sources risks causing a dangerous overdose of insulin + // if the CGM sensor reads falsely high and doesn't come down as actual BG does + enableSMB_always: false, // always enable supermicrobolus (unless disabled by high temptarget) + enableSMB_after_carbs: false, // enable supermicrobolus for 6h after carbs, even with 0 COB + enableSMB_high_bg: false, // enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile) + enableSMB_high_bg_target: 110, // set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable. + // *** WARNING *** DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar. + allowSMB_with_high_temptarget: false, // allow supermicrobolus (if otherwise enabled) even with high temp targets + maxSMBBasalMinutes: 30, // maximum minutes of basal that can be delivered as a single SMB with uncovered COB + maxUAMSMBBasalMinutes: 30, // maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB + SMBInterval: 3, // minimum interval between SMBs, in minutes. + bolus_increment: 0.1, // minimum bolus that can be delivered as an SMB + maxDelta_bg_threshold: 0.2, // maximum change in bg to use SMB, above that will disable SMB + curve: 'rapid-acting', // change this to "ultra-rapid" for Fiasp, or "bilinear" for old curve + useCustomPeakTime: false, // allows changing insulinPeakTime + insulinPeakTime: 75, // number of minutes after a bolus activity peaks. defaults to 55m for Fiasp if useCustomPeakTime: false + carbsReqThreshold: 1, // grams of carbsReq to trigger a pushover + offline_hotspot: false, // enabled an offline-only local wifi hotspot if no Internet available + noisyCGMTargetMultiplier: 1.3, // increase target by this amount when looping off raw/noisy CGM data + suspend_zeros_iob: true, // recognize pump suspends as non insulin delivery events + // send the glucose data to the pump emulating an enlite sensor. This allows to setup high / low warnings when offline and see trend. + // To enable this feature, enable the sensor, set a sensor with id 0000000, go to start sensor and press find lost sensor. + enableEnliteBgproxy: false, + // TODO: make maxRaw a preference here usable by oref0-raw in myopenaps-cgm-loop + //, maxRaw: 200 // highest raw/noisy CGM value considered safe to use for looping + calc_glucose_noise: false, + target_bg: false, // set to an integer value in mg/dL to override pump min_bg + edison_battery_shutdown_voltage: 3050, + pi_battery_shutdown_percent: 2, + } +} + +export function displayedDefaults(final_result: FinalResult) { + const allDefaults = defaults() + const profile = { + max_iob: allDefaults.max_iob, + max_daily_safety_multiplier: allDefaults.max_daily_safety_multiplier, + current_basal_safety_multiplier: allDefaults.current_basal_safety_multiplier, + autosens_max: allDefaults.autosens_max, + autosens_min: allDefaults.autosens_min, + rewind_resets_autosens: allDefaults.rewind_resets_autosens, + exercise_mode: allDefaults.exercise_mode, + sensitivity_raises_target: allDefaults.sensitivity_raises_target, + unsuspend_if_no_temp: allDefaults.unsuspend_if_no_temp, + enableSMB_with_COB: allDefaults.enableSMB_with_COB, + enableSMB_with_temptarget: allDefaults.enableSMB_with_temptarget, + enableUAM: allDefaults.enableUAM, + curve: allDefaults.curve, + offline_hotspot: allDefaults.offline_hotspot, + edison_battery_shutdown_voltage: allDefaults.edison_battery_shutdown_voltage, + pi_battery_shutdown_percent: allDefaults.pi_battery_shutdown_percent, + } + + console_error(final_result, profile) + return profile +} + +export default function generate(final_result: FinalResult, inputs: any, opts: any) { + const profile = opts && opts.type ? opts : defaults() + + // check if inputs has overrides for any of the default prefs + // and apply if applicable + for (const pref in profile) { + if (Object.prototype.hasOwnProperty.call(inputs, pref)) { + profile[pref] = inputs[pref] + } + } + + const pumpsettings_data = inputs.settings + if (inputs.settings.insulin_action_curve > 1) { + profile.dia = pumpsettings_data.insulin_action_curve + } else { + console_error(final_result, 'DIA of', profile.dia, 'is not supported') + return -1 + } + + if (inputs.model) { + profile.model = inputs.model + } + profile.skip_neutral_temps = inputs.skip_neutral_temps + + profile.current_basal = basalLookup(inputs.basals) + profile.basalprofile = inputs.basals + ;[...profile.basalprofile].forEach(basalentry => { + basalentry.rate = Number(`${Math.round(Number(`${basalentry.rate}e+3`))}e-3`) + }) + + profile.max_daily_basal = maxDailyBasal(inputs) + profile.max_basal = maxBasalLookup(inputs) + if (profile.current_basal === 0) { + console_error(final_result, 'current_basal of', profile.current_basal, 'is not supported') + return -1 + } + if (profile.max_daily_basal === 0) { + console_error(final_result, 'max_daily_basal of', profile.max_daily_basal, 'is not supported') + return -1 + } + if (profile.max_basal < 0.1) { + console_error(final_result, 'max_basal of', profile.max_basal, 'is not supported') + return -1 + } + + const range = targets.bgTargetsLookup(final_result, inputs, profile) + profile.out_units = inputs.targets.user_preferred_units + profile.min_bg = Math.round(range.min_bg) + profile.max_bg = Math.round(range.max_bg) + profile.bg_targets = inputs.targets + ;(profile.bg_targets.targets || []).forEach((bg_entry: any) => { + bg_entry.high = Math.round(bg_entry.high) + bg_entry.low = Math.round(bg_entry.low) + bg_entry.min_bg = Math.round(bg_entry.min_bg) + bg_entry.max_bg = Math.round(bg_entry.max_bg) + }) + + delete profile.bg_targets.raw + + profile.temptargetSet = range.temptargetSet + let lastResult = null + ;[profile.sens, lastResult] = isf.isfLookup(inputs.isf, undefined, lastResult) + profile.isfProfile = inputs.isf + if (profile.sens < 5) { + console_error(final_result, 'ISF of', profile.sens, 'is not supported') + return -1 + } + if (typeof inputs.carbratio !== 'undefined') { + profile.carb_ratio = carb_ratios.carbRatioLookup(final_result, inputs, profile) + profile.carb_ratios = inputs.carbratio + } else { + console_error(final_result, "Profile wasn't given carb ratio data, cannot calculate carb_ratio") + } + + return profile +} + +generate.defaults = defaults +generate.displayedDefaults = displayedDefaults +exports = module.exports = generate diff --git a/lib/profile/isf.ts b/lib/profile/isf.ts index 1cffcbb2a..9e7e9e8ed 100644 --- a/lib/profile/isf.ts +++ b/lib/profile/isf.ts @@ -1,40 +1,46 @@ -import { ISFProfile, ISFSensitivity } from "../types/Profile"; +import type { ISFProfile, ISFSensitivity } from '../types/Profile' -export default function isfLookup(isf_profile: ISFProfile, timestamp: Date | undefined, lastResult?: ISFSensitivity): [number, ISFSensitivity | undefined] { +export default function isfLookup( + isf_profile: ISFProfile, + timestamp: Date | undefined, + lastResult: ISFSensitivity | null +): [number, ISFSensitivity | null] { + const nowDate = timestamp || new Date() - var nowDate = timestamp || new Date(); - - var nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes(); + const nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes() if (lastResult && nowMinutes >= lastResult.offset && nowMinutes < lastResult.endOffset) { - return [lastResult.sensitivity, lastResult]; + return [lastResult.sensitivity, lastResult] } - let isf_data = isf_profile.sensitivities.sort((a, b) => a.offset - b.offset) + const isf_data = isf_profile.sensitivities.sort((a, b) => a.offset - b.offset) - var isfSchedule = isf_data[isf_data.length - 1]; + let isfSchedule = isf_data[isf_data.length - 1] if (isf_data[0].offset !== 0) { - return [-1, lastResult]; + return [-1, lastResult] } - var endMinutes = 1440; + let endMinutes = 1440 - for (var i = 0; i < isf_data.length - 1; i++) { - var currentISF = isf_data[i]; - var nextISF = isf_data[i+1]; + for (let i = 0; i < isf_data.length - 1; i++) { + const currentISF = isf_data[i] + const nextISF = isf_data[i + 1] if (nowMinutes >= currentISF.offset && nowMinutes < nextISF.offset) { - endMinutes = nextISF.offset; - isfSchedule = isf_data[i]; - break; + endMinutes = nextISF.offset + isfSchedule = isf_data[i] + break } } - lastResult = isfSchedule; - lastResult.endOffset = endMinutes; - - return [isfSchedule.sensitivity, lastResult]; + return [ + isfSchedule.sensitivity, + { + ...isfSchedule, + endOffset: endMinutes, + }, + ] } -isfLookup.isfLookup = isfLookup; -exports = module.exports = isfLookup; +isfLookup.isfLookup = isfLookup +exports = module.exports = isfLookup diff --git a/lib/profile/targets.js b/lib/profile/targets.js deleted file mode 100644 index a91fc23b6..000000000 --- a/lib/profile/targets.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict'; - -var getTime = require('../medtronic-clock'); -var shared_node_utils = require('../bin/utils'); -var console_error = shared_node_utils.console_error; - -function bgTargetsLookup (final_result, inputs, profile) { - return bound_target_range(lookup(final_result, inputs, profile)); -} - -function lookup (final_result, inputs, profile) { - var bgtargets_data = inputs.targets; - var temptargets_data = inputs.temptargets; - var now = new Date(); - - //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset }); - - var bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1]; - - for (var i = 0; i < bgtargets_data.targets.length - 1; i++) { - if ((now >= getTime(bgtargets_data.targets[i].offset)) && (now < getTime(bgtargets_data.targets[i + 1].offset))) { - bgTargets = bgtargets_data.targets[i]; - break; - } - } - - if (profile.target_bg) { - bgTargets.low = profile.target_bg; - } - - bgTargets.high = bgTargets.low; - - var tempTargets = bgTargets; - - // sort tempTargets by date so we can process most recent first - try { - temptargets_data.sort(function (a, b) { return new Date(b.created_at) - new Date(a.created_at) }); - } catch (e) { - console_error(final_result, "No temptargets found."); - } - //console.error(temptargets_data); - //console.error(now); - for (i = 0; i < temptargets_data.length; i++) { - var start = new Date(temptargets_data[i].created_at); - //console.error(start); - var expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000); - //console.error(expires); - if (now >= start && temptargets_data[i].duration === 0) { - // cancel temp targets - //console.error(temptargets_data[i]); - tempTargets = bgTargets; - break; - } else if (! temptargets_data[i].targetBottom || ! temptargets_data[i].targetTop) { - console_error(final_result, "eventualBG target range invalid: " + temptargets_data[i].targetBottom + "-" + temptargets_data[i].targetTop); - break; - } else if (now >= start && now < expires ) { - //console.error(temptargets_data[i]); - tempTargets.high = temptargets_data[i].targetTop; - tempTargets.low = temptargets_data[i].targetBottom; - tempTargets.temptargetSet = true; - break; - } - } - bgTargets = tempTargets; - //console.error(bgTargets); - - return bgTargets; -} - -function bound_target_range (target) { - // if targets are < 20, assume for safety that they're intended to be mmol/L, and convert to mg/dL - if ( target.high < 20 ) { target.high = target.high * 18; } - if ( target.low < 20 ) { target.low = target.low * 18; } - // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong - target.max_bg = Math.max(80, target.high); - target.min_bg = Math.max(80, target.low); - // hard-code upper bound for min_bg in case pump is set too high - target.min_bg = Math.min(200, target.min_bg); - target.max_bg = Math.min(200, target.max_bg); - return target -} - -bgTargetsLookup.bgTargetsLookup = bgTargetsLookup; // does use log -bgTargetsLookup.lookup = lookup; // not used outside -bgTargetsLookup.bound_target_range = bound_target_range; // does not log -exports = module.exports = bgTargetsLookup; - diff --git a/lib/profile/targets.ts b/lib/profile/targets.ts new file mode 100644 index 000000000..94fc203c9 --- /dev/null +++ b/lib/profile/targets.ts @@ -0,0 +1,122 @@ +import type { FinalResult } from '../bin/utils' +import { console_error } from '../bin/utils' +import getTime from '../medtronic-clock' +import type { Profile } from '../types/Profile' + +interface BgTarget { + offset: number + low: number + high: number + min_bg?: number + max_bg?: number + temptargetSet?: boolean +} + +interface TempTarget { + created_at: string + duration: number + targetTop: number + targetBottom: number +} + +interface LookupInputs { + targets: { + targets: BgTarget[] + } + temptargets: TempTarget[] +} + +export function bgTargetsLookup(final_result: FinalResult, inputs: LookupInputs, profile: Profile) { + return bound_target_range(lookup(final_result, inputs, profile)) +} + +export function lookup(final_result: FinalResult, inputs: LookupInputs, profile: Profile) { + const bgtargets_data = inputs.targets + let temptargets_data = inputs.temptargets + const now = new Date() + + //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset }); + + let bgTargets = bgtargets_data.targets[bgtargets_data.targets.length - 1] + + for (let i = 0; i < bgtargets_data.targets.length - 1; i++) { + if ( + now.getTime() >= getTime(bgtargets_data.targets[i].offset) && + now.getTime() < getTime(bgtargets_data.targets[i + 1].offset) + ) { + bgTargets = bgtargets_data.targets[i] + break + } + } + + if (profile.target_bg) { + bgTargets.low = profile.target_bg + } + + bgTargets.high = bgTargets.low + + let tempTargets = bgTargets + + if (!Array.isArray(temptargets_data)) { + console_error(final_result, 'No temptargets found.') + return bgTargets + } else { + // sort tempTargets by date so we can process most recent first + temptargets_data = [...temptargets_data].sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + } + + //console.error(temptargets_data); + //console.error(now); + for (let i = 0; i < temptargets_data.length; i++) { + const start = new Date(temptargets_data[i].created_at) + //console.error(start); + const expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000) + //console.error(expires); + if (now >= start && temptargets_data[i].duration === 0) { + // cancel temp targets + //console.error(temptargets_data[i]); + tempTargets = bgTargets + break + } else if (!temptargets_data[i].targetBottom || !temptargets_data[i].targetTop) { + console_error( + final_result, + `eventualBG target range invalid: ${temptargets_data[i].targetBottom}-${temptargets_data[i].targetTop}` + ) + break + } else if (now >= start && now < expires) { + //console.error(temptargets_data[i]); + tempTargets.high = temptargets_data[i].targetTop + tempTargets.low = temptargets_data[i].targetBottom + tempTargets.temptargetSet = true + break + } + } + bgTargets = tempTargets + //console.error(bgTargets); + + return bgTargets +} + +export function bound_target_range(target: BgTarget) { + // if targets are < 20, assume for safety that they're intended to be mmol/L, and convert to mg/dL + if (target.high < 20) { + target.high = target.high * 18 + } + if (target.low < 20) { + target.low = target.low * 18 + } + return { + ...target, + // hard-code lower bounds for min_bg and max_bg in case pump is set too low, or units are wrong + // hard-code upper bound for min_bg in case pump is set too high + max_bg: Math.min(200, Math.max(80, target.high)), + min_bg: Math.min(200, Math.max(80, target.low)), + } +} + +bgTargetsLookup.bgTargetsLookup = bgTargetsLookup // does use log +bgTargetsLookup.lookup = lookup // not used outside +bgTargetsLookup.bound_target_range = bound_target_range // does not log +exports = module.exports = bgTargetsLookup diff --git a/lib/pump.js b/lib/pump.js index 838fea172..6c61959b7 100644 --- a/lib/pump.js +++ b/lib/pump.js @@ -1,36 +1,34 @@ -'use strict'; +'use strict' -function translate (treatments) { +function translate(treatments) { + const results = [] - var results = [ ]; - - function step (current) { - var invalid = false; - switch (current._type) { - case 'CalBGForPH': - current.eventType = 'BG Check'; - current.glucose = current.amount; - current.glucoseType = 'Finger'; - break; - case 'BasalProfileStart': - case 'ResultDailyTotal': - case 'BGReceived': - case 'Sara6E': - case 'Model522ResultTotals': - case 'Model722ResultTotals': - invalid = true; - break; - default: - break; - } + function step(current) { + let invalid = false + switch (current._type) { + case 'CalBGForPH': + current.eventType = 'BG Check' + current.glucose = current.amount + current.glucoseType = 'Finger' + break + case 'BasalProfileStart': + case 'ResultDailyTotal': + case 'BGReceived': + case 'Sara6E': + case 'Model522ResultTotals': + case 'Model722ResultTotals': + invalid = true + break + default: + break + } - if (!invalid) { - results.push(current); + if (!invalid) { + results.push(current) + } } - - } - treatments.forEach(step); - return results; + treatments.forEach(step) + return results } -exports = module.exports = translate; +exports = module.exports = translate diff --git a/lib/require-utils.js b/lib/require-utils.js deleted file mode 100644 index c17f3e82b..000000000 --- a/lib/require-utils.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict'; - -var fs = require('fs'); - -function safeRequire (path) { - var resolved; - - try { - resolved = require(path); - } catch (e) { - console.error("Could not require: " + path, e); - } - - return resolved; -} - -function safeLoadFile(path) { - - var resolved; - - try { - resolved = JSON.parse(fs.readFileSync(path, 'utf8')); - //console.log('content = ' , resolved); - } catch (e) { - console.error("Could not require: " + path, e); - } - return resolved; -} - -function requireWithTimestamp (path) { - var resolved = safeLoadFile(path); - - if (resolved) { - resolved.timestamp = fs.statSync(path).mtime; - } - return resolved; -} - -// Functions that are needed in order to test the module. Can be removed in the future. - -function compareMethods(path) { - var new_data = safeLoadFile(path); - var old_data = safeRequire(path); - if (JSON.stringify(new_data) === JSON.stringify(old_data) ) { - console.log("test passed", new_data, old_data); - } else { - console.log("test failed"); - } -} - -// Module tests. -if (!module.parent) { - // Write the first file: and test it. - var obj = {x: "x", y: 1} - fs.writeFileSync('/tmp/file1.json', JSON.stringify(obj)); - compareMethods('/tmp/file1.json'); - - // Check a non existing object. - compareMethods('/tmp/not_exist.json'); - - // check a file that is not formated well. - fs.writeFileSync('/tmp/bad.json', '{"x":"x","y":1'); - compareMethods('/tmp/bad.json'); - - // Rewrite the file and reread it. - var new_obj = {x: "x", y: 2} - fs.writeFileSync('/tmp/file1.json', JSON.stringify(new_obj)); - var obj_read = safeLoadFile('/tmp/file1.json'); - if (JSON.stringify(new_obj) === JSON.stringify(obj_read) ) { - console.log("test passed"); - } else { - console.log("test failed"); - } - -} - -module.exports = { - safeRequire: safeRequire - , requireWithTimestamp: requireWithTimestamp - , safeLoadFile: safeLoadFile -}; diff --git a/lib/require-utils.ts b/lib/require-utils.ts new file mode 100644 index 000000000..525e06e71 --- /dev/null +++ b/lib/require-utils.ts @@ -0,0 +1,78 @@ +import * as fs from 'fs' + +function safeRequire(path: string) { + let resolved + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + resolved = require(path) + } catch (e) { + console.error(`Could not require: ${path}`, e) + } + + return resolved +} + +function safeLoadFile(path: string) { + let resolved + + try { + resolved = JSON.parse(fs.readFileSync(path, 'utf8')) + //console.log('content = ' , resolved); + } catch (e) { + console.error(`Could not require: ${path}`, e) + } + return resolved +} + +function requireWithTimestamp(path: string) { + const resolved = safeLoadFile(path) + + if (resolved) { + resolved.timestamp = fs.statSync(path).mtime + } + return resolved +} + +// Functions that are needed in order to test the module. Can be removed in the future. + +function compareMethods(path: string) { + const new_data = safeLoadFile(path) + const old_data = safeRequire(path) + if (JSON.stringify(new_data) === JSON.stringify(old_data)) { + console.log('test passed', new_data, old_data) + } else { + console.log('test failed') + } +} + +// Module tests. +if (!module.parent) { + // Write the first file: and test it. + const obj = { x: 'x', y: 1 } + fs.writeFileSync('/tmp/file1.json', JSON.stringify(obj)) + compareMethods('/tmp/file1.json') + + // Check a non existing object. + compareMethods('/tmp/not_exist.json') + + // check a file that is not formated well. + fs.writeFileSync('/tmp/bad.json', '{"x":"x","y":1') + compareMethods('/tmp/bad.json') + + // Rewrite the file and reread it. + const new_obj = { x: 'x', y: 2 } + fs.writeFileSync('/tmp/file1.json', JSON.stringify(new_obj)) + const obj_read = safeLoadFile('/tmp/file1.json') + if (JSON.stringify(new_obj) === JSON.stringify(obj_read)) { + console.log('test passed') + } else { + console.log('test failed') + } +} + +module.exports = { + safeRequire: safeRequire, + requireWithTimestamp: requireWithTimestamp, + safeLoadFile: safeLoadFile, +} diff --git a/lib/round-basal.ts b/lib/round-basal.ts index 9952ae9e9..b62c95f0c 100644 --- a/lib/round-basal.ts +++ b/lib/round-basal.ts @@ -1,31 +1,30 @@ -import { Profile } from "./types/Profile"; +import type { Profile } from './types/Profile' const round_basal = (basal: number, profile?: Profile) => { - /* x23 and x54 pumps change basal increment depending on how much basal is being delivered: 0.025u for 0.025 < x < 0.975 0.05u for 1 < x < 9.95 0.1u for 10 < x To round numbers nicely for the pump, use a scale factor of (1 / increment). */ - let lowest_rate_scale = 20; + let lowest_rate_scale = 20 // Make sure optional model has been set if (profile?.model?.endsWith('54') || profile?.model?.endsWith('23')) { - lowest_rate_scale = 40; + lowest_rate_scale = 40 } - let rounded_basal = basal; + let rounded_basal = basal // Shouldn't need to check against 0 as pumps can't deliver negative basal anyway? if (basal < 1) { - rounded_basal = Math.round(basal * lowest_rate_scale) / lowest_rate_scale; + rounded_basal = Math.round(basal * lowest_rate_scale) / lowest_rate_scale } else if (basal < 10) { - rounded_basal = Math.round(basal * 20) / 20; + rounded_basal = Math.round(basal * 20) / 20 } else { - rounded_basal = Math.round(basal * 10) / 10; + rounded_basal = Math.round(basal * 10) / 10 } - return rounded_basal; + return rounded_basal } export default round_basal diff --git a/lib/temps.js b/lib/temps.js index 9ac6918cc..e7f06c7f1 100644 --- a/lib/temps.js +++ b/lib/temps.js @@ -1,49 +1,47 @@ -'use strict'; - -function filter (treatments) { - - var results = [ ]; - - var state = { }; - - function temp (ev) { - if ('duration (min)' in ev) { - state.duration = ev['duration (min)'].toString( ); - state.raw_duration = ev; - } - - if ('rate' in ev) { - state[ev.temp] = ev.rate.toString( ); - state.rate = ev['rate']; - state.raw_rate = ev; - } - - if ('timestamp' in state && ev.timestamp !== state.timestamp) { - state.invalid = true; - } else { - state.timestamp = ev.timestamp; - } - - if ('duration' in state && ('percent' in state || 'absolute' in state)) { - state.eventType = 'Temp Basal'; - results.push(state); - state = { }; +'use strict' + +function filter(treatments) { + const results = [] + + let state = {} + function temp(ev) { + if ('duration (min)' in ev) { + state.duration = ev['duration (min)'].toString() + state.raw_duration = ev + } + + if ('rate' in ev) { + state[ev.temp] = ev.rate.toString() + state.rate = ev.rate + state.raw_rate = ev + } + + if ('timestamp' in state && ev.timestamp !== state.timestamp) { + state.invalid = true + } else { + state.timestamp = ev.timestamp + } + + if ('duration' in state && ('percent' in state || 'absolute' in state)) { + state.eventType = 'Temp Basal' + results.push(state) + state = {} + } } - } - function step (current) { - switch (current._type) { - case 'TempBasalDuration': - case 'TempBasal': - temp(current); - break; - default: - results.push(current); - break; + function step(current) { + switch (current._type) { + case 'TempBasalDuration': + case 'TempBasal': + temp(current) + break + default: + results.push(current) + break + } } - } - treatments.forEach(step); - return results; + treatments.forEach(step) + return results } -exports = module.exports = filter; +exports = module.exports = filter diff --git a/lib/types/Autosens.ts b/lib/types/Autosens.ts index f611334dd..04a2406df 100644 --- a/lib/types/Autosens.ts +++ b/lib/types/Autosens.ts @@ -6,8 +6,8 @@ export const Autosens = t.intersection([ ratio: t.number, }), t.partial({ - newisf: t.number - }) -]); + newisf: t.number, + }), +]) export type Autosens = t.TypeOf diff --git a/lib/types/EventType.ts b/lib/types/EventType.ts index f1099b24e..ba01ebf97 100644 --- a/lib/types/EventType.ts +++ b/lib/types/EventType.ts @@ -1,28 +1,28 @@ export enum EventType { - bolus = "Bolus", - smb = "SMB", - isExternal = "External Insulin", - mealBolus = "Meal Bolus", - correctionBolus = "Correction Bolus", - snackBolus = "Snack Bolus", - bolusWizard = "BolusWizard", - tempBasal = "TempBasal", - tempBasalDuration = "TempBasalDuration", - pumpSuspend = "PumpSuspend", - pumpResume = "PumpResume", - pumpAlarm = "PumpAlarm", - pumpBattery = "PumpBattery", - rewind = "Rewind", - prime = "Prime", - journalCarbs = "JournalEntryMealMarker", - nsTempBasal = "Temp Basal", - nsCarbCorrection = "Carb Correction", - nsTempTarget = "Temporary Target", - nsInsulinChange = "Insulin Change", - nsSiteChange = "Site Change", - nsBatteryChange = "Pump Battery Change", - nsAnnouncement = "Announcement", - nsSensorChange = "Sensor Start", - capillaryGlucose = "BG Check", - nsExercise = "Exercise", + bolus = 'Bolus', + smb = 'SMB', + isExternal = 'External Insulin', + mealBolus = 'Meal Bolus', + correctionBolus = 'Correction Bolus', + snackBolus = 'Snack Bolus', + bolusWizard = 'BolusWizard', + tempBasal = 'TempBasal', + tempBasalDuration = 'TempBasalDuration', + pumpSuspend = 'PumpSuspend', + pumpResume = 'PumpResume', + pumpAlarm = 'PumpAlarm', + pumpBattery = 'PumpBattery', + rewind = 'Rewind', + prime = 'Prime', + journalCarbs = 'JournalEntryMealMarker', + nsTempBasal = 'Temp Basal', + nsCarbCorrection = 'Carb Correction', + nsTempTarget = 'Temporary Target', + nsInsulinChange = 'Insulin Change', + nsSiteChange = 'Site Change', + nsBatteryChange = 'Pump Battery Change', + nsAnnouncement = 'Announcement', + nsSensorChange = 'Sensor Start', + capillaryGlucose = 'BG Check', + nsExercise = 'Exercise', } diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts index b60efb0e8..efb5273f4 100644 --- a/lib/types/GlucoseEntry.ts +++ b/lib/types/GlucoseEntry.ts @@ -1,5 +1,4 @@ - -export type GlucoseEntry = { +export interface GlucoseEntry { date?: number display_time?: string dateString?: string diff --git a/lib/types/LocalDateFromDate.ts b/lib/types/LocalDateFromDate.ts index 359c4906c..3d097e505 100644 --- a/lib/types/LocalDateFromDate.ts +++ b/lib/types/LocalDateFromDate.ts @@ -1,11 +1,11 @@ -import { date } from 'io-ts-types/date' -import * as t from 'io-ts' import { right } from 'fp-ts/Either' -import { tz } from '../date'; +import * as t from 'io-ts' +import { date } from 'io-ts-types/date' +import { tz } from '../date' export const LocalDateFromDate = new t.Type( 'LocalDateFromDate', - (a) => date.is(a), - (i) => right(tz(i)), - a => a, + a => date.is(a), + i => right(tz(i)), + a => a ) diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts index 3243b90d6..abdd364ce 100644 --- a/lib/types/NightscoutTreatment.ts +++ b/lib/types/NightscoutTreatment.ts @@ -28,36 +28,36 @@ export interface NightscoutTreatment { amount?: number } -export const NightscoutTreatment: t.Type = t.intersection([ - t.type({ - eventType: EventType, - created_at: t.string, - }), - t.partial({ - id: t.string, - duration: t.number, - rawDuration: PumpHistoryEvent, - rawRate: PumpHistoryEvent, - absolute: t.number, - rate: t.number, - enteredBy: t.string, - bolus: PumpHistoryEvent, - insulin: t.union([t.number, t.null]), - notes: t.string, - carbs: t.union([t.number, t.null]), - fat: t.number, - protein: t.number, - foodType: t.string, - targetTop: t.number, - targetBottom: t.number, - glucoseType: t.union([ - t.literal('Finger'), - t.string, - ]), - glucose: t.number, - units: t.string, - fpuID: t.string, - // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running - amount: t.number, - }) -], 'NightscoutTreatment') +export const NightscoutTreatment: t.Type = t.intersection( + [ + t.type({ + eventType: EventType, + created_at: t.string, + }), + t.partial({ + id: t.string, + duration: t.number, + rawDuration: PumpHistoryEvent, + rawRate: PumpHistoryEvent, + absolute: t.number, + rate: t.number, + enteredBy: t.string, + bolus: PumpHistoryEvent, + insulin: t.union([t.number, t.null]), + notes: t.string, + carbs: t.union([t.number, t.null]), + fat: t.number, + protein: t.number, + foodType: t.string, + targetTop: t.number, + targetBottom: t.number, + glucoseType: t.union([t.literal('Finger'), t.string]), + glucose: t.number, + units: t.string, + fpuID: t.string, + // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running + amount: t.number, + }), + ], + 'NightscoutTreatment' +) diff --git a/lib/types/Profile.ts b/lib/types/Profile.ts index d7146525d..6b6193ed5 100644 --- a/lib/types/Profile.ts +++ b/lib/types/Profile.ts @@ -2,22 +2,24 @@ import * as t from 'io-ts' export interface BasalSchedule { i?: number - start: string, - minutes: number, + start: string + minutes: number rate: number } -export const BasalSchedule: t.Type = t.intersection([ - t.type({ - start: t.string, - minutes: t.number, - rate: t.number - }), - t.partial({ - i: t.number - }) -], 'BasalSchedule') - +export const BasalSchedule: t.Type = t.intersection( + [ + t.type({ + start: t.string, + minutes: t.number, + rate: t.number, + }), + t.partial({ + i: t.number, + }), + ], + 'BasalSchedule' +) /** * { @@ -155,33 +157,41 @@ export const BasalSchedule: t.Type = t.intersection([ export interface CarbRatioSchedule { start: string - offsset: number + offset: number ratio: number } -export const CarbRatioSchedule: t.Type = t.type({ - start: t.string, - offsset: t.number, - ratio: t.number, -}, 'CarbRatioSchedule') +export const CarbRatioSchedule: t.Type = t.type( + { + start: t.string, + offset: t.number, + ratio: t.number, + }, + 'CarbRatioSchedule' +) export interface CarbRatios { units: string - schedule: Array, + schedule: Array } -export const CarbRatios: t.Type = t.type({ - units: t.string, - schedule: t.array(CarbRatioSchedule), -}, 'CarbRatios') +export const CarbRatios: t.Type = t.type( + { + units: t.string, + schedule: t.array(CarbRatioSchedule), + }, + 'CarbRatios' +) export type InsulineCurve = 'bilinear' | 'rapid-acting' | 'ultra-rapid' -export const InsulineCurve: t.Type = t.keyof({ - 'bilinear': null, - 'rapid-acting': null, - 'ultra-rapid': null, -}, 'InsulineCurve') - +export const InsulineCurve: t.Type = t.keyof( + { + bilinear: null, + 'rapid-acting': null, + 'ultra-rapid': null, + }, + 'InsulineCurve' +) export interface ISFSensitivity { i?: number @@ -192,18 +202,21 @@ export interface ISFSensitivity { x?: number } -export const ISFSensitivity: t.Type = t.intersection([ - t.type({ - offset: t.number, - endOffset: t.number, - sensitivity: t.number, - }), - t.partial({ - i: t.number, - start: t.string, - x: t.number - }) -], 'ISFSensitivity') +export const ISFSensitivity: t.Type = t.intersection( + [ + t.type({ + offset: t.number, + endOffset: t.number, + sensitivity: t.number, + }), + t.partial({ + i: t.number, + start: t.string, + x: t.number, + }), + ], + 'ISFSensitivity' +) export interface ISFProfile { sensitivities: Array @@ -211,21 +224,27 @@ export interface ISFProfile { user_preferred_units?: string } -export const ISFProfile: t.Type = t.intersection([ - t.type({ - sensitivities: t.array(ISFSensitivity), - }), - t.partial({ - units: t.string, - user_preferred_units: t.string - }) -], 'ISFProfile') +export const ISFProfile: t.Type = t.intersection( + [ + t.type({ + sensitivities: t.array(ISFSensitivity), + }), + t.partial({ + units: t.string, + user_preferred_units: t.string, + }), + ], + 'ISFProfile' +) export type GlucoseUnits = 'mg/dL' | 'mmol/L' -export const GlucoseUnits = t.keyof({ - 'mg/dL': null, - 'mmol/L': null, -}, 'GlucoseUnits') +export const GlucoseUnits = t.keyof( + { + 'mg/dL': null, + 'mmol/L': null, + }, + 'GlucoseUnits' +) export interface Profile { basalprofile: Array @@ -246,6 +265,7 @@ export interface Profile { max_iob?: number min_bg?: number max_bg?: number + target_bg?: number A52_risk_enable?: boolean noisyCGMTargetMultiplier?: number maxRaw?: number @@ -281,63 +301,66 @@ export interface Profile { SMBInterval?: number } -export const Profile: t.Type = t.intersection([ - t.type({ - basalprofile: t.array(BasalSchedule), - sens: t.number, - carb_ratio: t.number, - min_5m_carbimpact: t.number, - }), - t.partial({ - out_units: t.keyof({ - 'mg/dL': null, - 'mmol/L': null, +export const Profile: t.Type = t.intersection( + [ + t.type({ + basalprofile: t.array(BasalSchedule), + sens: t.number, + carb_ratio: t.number, + min_5m_carbimpact: t.number, + }), + t.partial({ + out_units: t.keyof({ + 'mg/dL': null, + 'mmol/L': null, + }), + max_daily_safety_multiplier: t.number, + current_basal_safety_multiplier: t.number, + model: t.string, + curve: InsulineCurve, + dia: t.number, + useCustomPeakTime: t.boolean, + insulinPeakTime: t.number, + remainingCarbsCap: t.number, + remainingCarbsFraction: t.number, + maxCOB: t.number, + max_iob: t.number, + min_bg: t.number, + max_bg: t.number, + A52_risk_enable: t.boolean, + noisyCGMTargetMultiplier: t.number, + maxRaw: t.number, + low_temptarget_lowers_sensitivity: t.boolean, + high_temptarget_raises_sensitivity: t.boolean, + sensitivity_raises_target: t.boolean, + resistance_lowers_target: t.boolean, + autosens_max: t.number, + allowSMB_with_high_temptarget: t.boolean, + enableSMB_high_bg_target: t.number, + enableSMB_with_temptarget: t.boolean, + enableSMB_after_carbs: t.boolean, + enableSMB_with_COB: t.boolean, + enableSMB_high_bg: t.boolean, + enableSMB_always: t.boolean, + enableUAM: t.boolean, + suspend_zeros_iob: t.boolean, + current_basal: t.number, + half_basal_exercise_target: t.number, + exercise_mode: t.boolean, + temptargetSet: t.unknown, + max_daily_basal: t.number, + max_basal: t.number, + maxDelta_bg_threshold: t.number, + bg_targets: t.unknown, + isfProfile: ISFProfile, + carb_ratios: CarbRatios, + carbsReqThreshold: t.number, + skip_neutral_temps: t.boolean, + maxSMBBasalMinutes: t.number, + maxUAMSMBBasalMinutes: t.number, + bolus_increment: t.number, + SMBInterval: t.number, }), - max_daily_safety_multiplier: t.number, - current_basal_safety_multiplier: t.number, - model: t.string, - curve: InsulineCurve, - dia: t.number, - useCustomPeakTime: t.boolean, - insulinPeakTime: t.number, - remainingCarbsCap: t.number, - remainingCarbsFraction: t.number, - maxCOB: t.number, - max_iob: t.number, - min_bg: t.number, - max_bg: t.number, - A52_risk_enable: t.boolean, - noisyCGMTargetMultiplier: t.number, - maxRaw: t.number, - low_temptarget_lowers_sensitivity: t.boolean, - high_temptarget_raises_sensitivity: t.boolean, - sensitivity_raises_target: t.boolean, - resistance_lowers_target: t.boolean, - autosens_max: t.number, - allowSMB_with_high_temptarget: t.boolean, - enableSMB_high_bg_target: t.number, - enableSMB_with_temptarget: t.boolean, - enableSMB_after_carbs: t.boolean, - enableSMB_with_COB: t.boolean, - enableSMB_high_bg: t.boolean, - enableSMB_always: t.boolean, - enableUAM: t.boolean, - suspend_zeros_iob: t.boolean, - current_basal: t.number, - half_basal_exercise_target: t.number, - exercise_mode: t.boolean, - temptargetSet: t.unknown, - max_daily_basal: t.number, - max_basal: t.number, - maxDelta_bg_threshold: t.number, - bg_targets: t.unknown, - isfProfile: ISFProfile, - carb_ratios: CarbRatios, - carbsReqThreshold: t.number, - skip_neutral_temps: t.boolean, - maxSMBBasalMinutes: t.number, - maxUAMSMBBasalMinutes: t.number, - bolus_increment: t.number, - SMBInterval: t.number, - }) -], 'Profile'); + ], + 'Profile' +) diff --git a/lib/types/PumpHistoryEvent.ts b/lib/types/PumpHistoryEvent.ts index 0c009eac7..f0f454724 100644 --- a/lib/types/PumpHistoryEvent.ts +++ b/lib/types/PumpHistoryEvent.ts @@ -7,33 +7,33 @@ export const NightscoutEventType = t.keyof({ 'Insulin Change': null, 'Site Change': null, 'Pump Battery Change': null, - 'Announcement': null, + Announcement: null, 'Sensor Start': null, 'BG Check': null, - 'Exercise': null, + Exercise: null, 'Bolus Wizard': null, }) export const EventType = t.union([ NightscoutEventType, t.keyof({ - 'Bolus': null, - 'SMB': null, + Bolus: null, + SMB: null, 'External Insulin': null, 'Meal Bolus': null, 'Correction Bolus': null, 'Snack Bolus': null, - 'BolusWizard': null, - 'TempBasal': null, - 'TempBasalDuration': null, - 'PumpSuspend': null, - 'PumpResume': null, - 'PumpAlarm': null, - 'PumpBattery': null, - 'Rewind': null, - 'Prime': null, - 'JournalEntryMealMarker': null, - 'SuspendBasal': null, + BolusWizard: null, + TempBasal: null, + TempBasalDuration: null, + PumpSuspend: null, + PumpResume: null, + PumpAlarm: null, + PumpBattery: null, + Rewind: null, + Prime: null, + JournalEntryMealMarker: null, + SuspendBasal: null, }), t.string, ]) @@ -67,22 +67,22 @@ export const PumpHistoryEvent = t.intersection([ isExternal: t.boolean, // @todo: check: used in iob/history //date: t.number, - }) + }), ]) export interface PumpHistoryEvent { - _type: EventType, - timestamp: string, - id?: string, - amount?: number, - duration?: number, - 'duration (min)'?: number, - rate?: number, - temp?: TempType, - carb_input?: number, - note?: string, - isSMB?: boolean, - isExternal?: boolean, + _type: EventType + timestamp: string + id?: string + amount?: number + duration?: number + 'duration (min)'?: number + rate?: number + temp?: TempType + carb_input?: number + note?: string + isSMB?: boolean + isExternal?: boolean // @todo: check: used in iob/history //date?: number, } diff --git a/lib/types/renameKey.ts b/lib/types/renameKey.ts index 1b1cdf679..993d1db32 100644 --- a/lib/types/renameKey.ts +++ b/lib/types/renameKey.ts @@ -1,29 +1,30 @@ -import { flow, pipe } from 'fp-ts/function'; -import * as t from 'io-ts' import { map, chain } from 'fp-ts/Either' +import { flow, pipe } from 'fp-ts/function' +import * as t from 'io-ts' -export const renameKey = (from: KA, to: KO) => (a: A) => new t.Type, Record>( - a.name, - a.is, - (i, c) => pipe( - t.record(t.string, t.unknown).validate(i, c), - map(b => { - if (Object.prototype.hasOwnProperty.call(b, from)) { - b[to] = b[from] - delete b[from] - } - return b - }), - chain(b => a.validate(b, c)) - ), - flow( - a.encode, - b => { - if (Object.prototype.hasOwnProperty.call(b, to)) { - b[from] = b[to] - delete b[to] - } - return b - } - ) -) +export const renameKey = + (from: KA, to: KO) => + (a: A) => + new t.Type, Record>( + a.name, + a.is, + (i, c) => + pipe( + t.record(t.string, t.unknown).validate(i, c), + map(b => { + if (Object.prototype.hasOwnProperty.call(b, from)) { + b[to] = b[from] + delete b[from] + } + return b + }), + chain(b => a.validate(b, c)) + ), + flow(a.encode, b => { + if (Object.prototype.hasOwnProperty.call(b, to)) { + b[from] = b[to] + delete b[to] + } + return b + }) + ) diff --git a/lib/with-raw-glucose.js b/lib/with-raw-glucose.js index 0f784adf7..c0589d625 100644 --- a/lib/with-raw-glucose.js +++ b/lib/with-raw-glucose.js @@ -1,56 +1,55 @@ -'use strict'; +'use strict' -function cleanCal (cal) { - var clean = { - scale: parseFloat(cal.scale) || 0 - , intercept: parseFloat(cal.intercept) || 0 - , slope: parseFloat(cal.slope) || 0 - }; +function cleanCal(cal) { + const clean = { + scale: parseFloat(cal.scale) || 0, + intercept: parseFloat(cal.intercept) || 0, + slope: parseFloat(cal.slope) || 0, + } - clean.valid = ! (clean.slope === 0 || clean.unfiltered === 0 || clean.scale === 0); + clean.valid = !(clean.slope === 0 || clean.unfiltered === 0 || clean.scale === 0) - return clean; + return clean } -module.exports = function withRawGlucose (entry, cals, maxRaw) { - maxRaw = maxRaw || 200; +module.exports = function withRawGlucose(entry, cals, maxRaw) { + maxRaw = maxRaw || 200 - if ( entry.type === "mbg" || entry.type === "cal" ) { - return entry; - } - var egv = entry.glucose || entry.sgv || 0; + if (entry.type === 'mbg' || entry.type === 'cal') { + return entry + } + const egv = entry.glucose || entry.sgv || 0 - entry.unfiltered = parseInt(entry.unfiltered) || 0; - entry.filtered = parseInt(entry.filtered) || 0; + entry.unfiltered = parseInt(entry.unfiltered) || 0 + entry.filtered = parseInt(entry.filtered) || 0 - //TODO: add time check, but how recent should it be? - //TODO: currently assuming the first is the best (and that there is probably just 1 cal) - var cal = cals && cals.length > 0 && cleanCal(cals[0]); + //TODO: add time check, but how recent should it be? + //TODO: currently assuming the first is the best (and that there is probably just 1 cal) + const cal = cals && cals.length > 0 && cleanCal(cals[0]) - if (cal && cal.valid) { - if (cal.filtered === 0 || egv < 40) { - entry.raw = Math.round(cal.scale * (entry.unfiltered - cal.intercept) / cal.slope); - } else { - var ratio = cal.scale * (entry.filtered - cal.intercept) / cal.slope / egv; - entry.raw = Math.round(cal.scale * (entry.unfiltered - cal.intercept) / cal.slope / ratio); - } + if (cal && cal.valid) { + if (cal.filtered === 0 || egv < 40) { + entry.raw = Math.round((cal.scale * (entry.unfiltered - cal.intercept)) / cal.slope) + } else { + const ratio = (cal.scale * (entry.filtered - cal.intercept)) / cal.slope / egv + entry.raw = Math.round((cal.scale * (entry.unfiltered - cal.intercept)) / cal.slope / ratio) + } - if ( egv < 40 ) { - if (entry.raw) { - entry.glucose = entry.raw; - entry.fromRaw = true; - if (entry.raw <= maxRaw) { - entry.noise = 2; + if (egv < 40) { + if (entry.raw) { + entry.glucose = entry.raw + entry.fromRaw = true + if (entry.raw <= maxRaw) { + entry.noise = 2 + } else { + entry.noise = 3 + } } else { - entry.noise = 3; + entry.noise = 3 } - } else { - entry.noise = 3; + } else if (!entry.noise) { + entry.noise = 0 } - } else if (! entry.noise) { - entry.noise = 0; } - - } - return entry; -}; + return entry +} diff --git a/package.json b/package.json index 7cbb2fbc6..1dea2b2ad 100644 --- a/package.json +++ b/package.json @@ -1,157 +1,160 @@ { - "name": "oref0", - "version": "0.7.1", - "description": "openaps oref0 reference implementation of the reference design", - "scripts": { - "test": "jest", - "global-install": "npm install && npm run build && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", - "build": "tsc", - "lint": "eslint lib/" - }, - "repository": { - "type": "git", - "url": "https://github.com/openaps/oref0.git" - }, - "keywords": [ - "openaps" - ], - "author": "Scott Leibrand, Dana Lewis, OpenAPS contributors", - "license": "MIT", - "bugs": { - "url": "https://github.com/openaps/oref0/issues" - }, - "bin": { - "bt-pan": "./bin/bt-pan", - "killall-g": "./bin/killall-g.sh", - "l": "./bin/oref0-tail-log.sh", - "mm-format-ns-glucose": "./bin/mm-format-ns-glucose.sh", - "mm-format-ns-profile": "./bin/mm-format-ns-profile.sh", - "mm-format-ns-pump-history": "./bin/mm-format-ns-pump-history.sh", - "mm-format-ns-treatments": "./bin/mm-format-ns-treatments.sh", - "mm-stick": "./bin/mm-stick.sh", - "monitor-xdrip": "./bin/monitor-xdrip.sh", - "nightscout": "./bin/nightscout.sh", - "ns-dedupe-treatments": "./bin/ns-dedupe-treatments.sh", - "ns-get": "./bin/ns-get.sh", - "ns-status": "./bin/ns-status.js", - "ns-upload": "./bin/ns-upload.sh", - "ns-upload-entries": "./bin/ns-upload-entries.sh", - "oref0": "./bin/oref0.sh", - "oref0-append-local-temptarget": "./bin/oref0-append-local-temptarget.sh", - "oref0-autosens-loop": "./bin/oref0-autosens-loop.sh", - "oref0-autotune": "./bin/oref0-autotune.sh", - "oref0-autotune-core": "./bin/oref0-autotune-core.js", - "oref0-autotune-export-to-xlsx": "./bin/oref0-autotune-export-to-xlsx.py", - "oref0-autotune-prep": "./bin/oref0-autotune-prep.js", - "oref0-autotune-recommends-report": "./bin/oref0-autotune-recommends-report.sh", - "oref0-backtest": "./bin/oref0-backtest.sh", - "oref0-bash-common-functions.sh": "./bin/oref0-bash-common-functions.sh", - "oref0-bluetoothup": "./bin/oref0-bluetoothup.sh", - "oref0-calculate-iob": "./bin/oref0-calculate-iob.js", - "oref0-calculate-glucose-noise": "./bin/oref0-calculate-glucose-noise.js", - "oref0-copy-fresher": "./bin/oref0-copy-fresher", - "oref0-crun": "./bin/oref0-conditional-run.sh", - "oref0-cron-every-minute": "./bin/oref0-cron-every-minute.sh", - "oref0-cron-every-15min": "./bin/oref0-cron-every-15min.sh", - "oref0-cron-post-reboot": "./bin/oref0-cron-post-reboot.sh", - "oref0-cron-nightly": "./bin/oref0-cron-nightly.sh", - "oref0-delete-future-entries": "./bin/oref0-delete-future-entries.sh", - "oref0-detect-sensitivity": "./bin/oref0-detect-sensitivity.js", - "oref0-determine-basal": "./bin/oref0-determine-basal.js", - "oref0-dex-is-fresh": "./bin/oref0-dex-is-fresh.sh", - "oref0-dex-time-since": "./bin/oref0-dex-time-since.sh", - "oref0-dex-wait-until-expected": "./bin/oref0-dex-wait-until-expected.sh", - "oref0-find-insulin-uses": "./bin/oref0-find-insulin-uses.js", - "oref0-fix-git-corruption": "./bin/oref0-fix-git-corruption.sh", - "oref0-g4-loop": "./bin/oref0-g4-loop.sh", - "oref0-get-bg": "./bin/oref0-get-bg.sh", - "oref0-get-profile": "./bin/oref0-get-profile.js", - "oref0-html": "./bin/oref0-html.js", - "oref0-ifttt-notify": "./bin/oref0-ifttt-notify", - "oref0-log-shortcuts": "./bin/oref0-log-shortcuts.sh", - "oref0-mdt-update": "./bin/oref0-mdt-update.sh", - "oref0-meal": "./bin/oref0-meal.js", - "oref0-monitor-cgm": "./bin/oref0-monitor-cgm.sh", - "oref0-mraa-install": "./bin/oref0-mraa-install.sh", - "oref0-ns-loop": "./bin/oref0-ns-loop.sh", - "oref0-normalize-temps": "./bin/oref0-normalize-temps.js", - "oref0_nightscout_check": "./bin/oref0_nightscout_check.py", - "oref0-online": "./bin/oref0-online.sh", - "oref0-pebble": "./bin/oref0-pebble.js", - "oref0-pump-loop": "./bin/oref0-pump-loop.sh", - "oref0-pushover": "./bin/oref0-pushover.sh", - "oref0-radio-reboot": "./bin/oref0-radio-reboot.sh", - "oref0-mmtune": "./bin/oref0-mmtune.sh", - "oref0-raw": "./bin/oref0-raw.js", - "oref0-reset-git": "./bin/oref0-reset-git.sh", - "oref0-reset-usb": "./bin/oref0-reset-usb.sh", - "oref0-set-device-clocks": "./bin/oref0-set-device-clocks.sh", - "oref0-set-system-clock": "./bin/oref0-set-system-clock.sh", - "oref0-set-local-temptarget": "./bin/oref0-set-local-temptarget.js", - "oref0-setup": "./bin/oref0-setup.sh", - "oref0-simulator": "./bin/oref0-simulator.sh", - "oref0-truncate-git-history": "./bin/oref0-truncate-git-history.sh", - "oref0-upload-entries": "./bin/oref0-upload-entries.sh", - "oref0-upload-profile": "./bin/oref0-upload-profile.js", - "oref0-version": "./bin/oref0-version.sh", - "oref0-get-ns-entries": "./bin/oref0-get-ns-entries.js", - "oref0-shared-node-loop": "./bin/oref0-shared-node-loop.sh", - "peb-urchin-status": "./bin/peb-urchin-status.sh", - "wifi": "./bin/oref0-tail-wifi.sh" - }, - "homepage": "https://github.com/openaps/oref0", - "dependencies": { - "fp-ts": "^2.16.9", - "io-ts": "^2.2.21", - "io-ts-types": "^0.5.19", - "json": "^9.0.6", - "json-stable-stringify": "^1.0.1", - "lodash": "^4.17.15", - "moment": "^2.24.0", - "moment-timezone": "^0.5.45", - "network": "^0.4.1", - "request": "^2.88.0", - "share2nightscout-bridge": "^0.2.1", - "tslib": "^2.6.3", - "yargs": "^13.2.2" - }, - "devDependencies": { - "@types/jest": "^29.5.12", - "@types/lodash": "^4.17.0", - "@types/mocha": "^10.0.6", - "@types/node": "^16.0", - "@types/should": "^13.0.0", - "@typescript-eslint/eslint-plugin": "^8.0.1", - "@typescript-eslint/parser": "^8.0.1", - "coveralls": "^3.0.3", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-fp-ts": "^0.3.2", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^5.2.1", - "istanbul": "^0.4.5", - "jest": "^29.7.0", - "mocha": "^5.2.0", - "mocha-lcov-reporter": "^1.3.0", - "prettier": "^3.3.3", - "should": "^13.2.3", - "ts-jest": "^29.2.4", - "ts-node": "^10.9.2", - "ts-node-dev": "^2.0.0", - "typescript": "^5.4.5" - }, - "config": { - "blanket": { - "pattern": [ - "bin", - "lib", - "tests" - ], - "data-cover-never": [ - "node_modules" - ] + "name": "oref0", + "version": "0.7.1", + "description": "openaps oref0 reference implementation of the reference design", + "scripts": { + "test": "jest", + "global-install": "npm install && npm run build && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", + "build": "tsc", + "lint": "eslint lib/" + }, + "repository": { + "type": "git", + "url": "https://github.com/openaps/oref0.git" + }, + "keywords": [ + "openaps" + ], + "author": "Scott Leibrand, Dana Lewis, OpenAPS contributors", + "license": "MIT", + "bugs": { + "url": "https://github.com/openaps/oref0/issues" + }, + "bin": { + "bt-pan": "./bin/bt-pan", + "killall-g": "./bin/killall-g.sh", + "l": "./bin/oref0-tail-log.sh", + "mm-format-ns-glucose": "./bin/mm-format-ns-glucose.sh", + "mm-format-ns-profile": "./bin/mm-format-ns-profile.sh", + "mm-format-ns-pump-history": "./bin/mm-format-ns-pump-history.sh", + "mm-format-ns-treatments": "./bin/mm-format-ns-treatments.sh", + "mm-stick": "./bin/mm-stick.sh", + "monitor-xdrip": "./bin/monitor-xdrip.sh", + "nightscout": "./bin/nightscout.sh", + "ns-dedupe-treatments": "./bin/ns-dedupe-treatments.sh", + "ns-get": "./bin/ns-get.sh", + "ns-status": "./bin/ns-status.js", + "ns-upload": "./bin/ns-upload.sh", + "ns-upload-entries": "./bin/ns-upload-entries.sh", + "oref0": "./bin/oref0.sh", + "oref0-append-local-temptarget": "./bin/oref0-append-local-temptarget.sh", + "oref0-autosens-loop": "./bin/oref0-autosens-loop.sh", + "oref0-autotune": "./bin/oref0-autotune.sh", + "oref0-autotune-core": "./bin/oref0-autotune-core.js", + "oref0-autotune-export-to-xlsx": "./bin/oref0-autotune-export-to-xlsx.py", + "oref0-autotune-prep": "./bin/oref0-autotune-prep.js", + "oref0-autotune-recommends-report": "./bin/oref0-autotune-recommends-report.sh", + "oref0-backtest": "./bin/oref0-backtest.sh", + "oref0-bash-common-functions.sh": "./bin/oref0-bash-common-functions.sh", + "oref0-bluetoothup": "./bin/oref0-bluetoothup.sh", + "oref0-calculate-iob": "./bin/oref0-calculate-iob.js", + "oref0-calculate-glucose-noise": "./bin/oref0-calculate-glucose-noise.js", + "oref0-copy-fresher": "./bin/oref0-copy-fresher", + "oref0-crun": "./bin/oref0-conditional-run.sh", + "oref0-cron-every-minute": "./bin/oref0-cron-every-minute.sh", + "oref0-cron-every-15min": "./bin/oref0-cron-every-15min.sh", + "oref0-cron-post-reboot": "./bin/oref0-cron-post-reboot.sh", + "oref0-cron-nightly": "./bin/oref0-cron-nightly.sh", + "oref0-delete-future-entries": "./bin/oref0-delete-future-entries.sh", + "oref0-detect-sensitivity": "./bin/oref0-detect-sensitivity.js", + "oref0-determine-basal": "./bin/oref0-determine-basal.js", + "oref0-dex-is-fresh": "./bin/oref0-dex-is-fresh.sh", + "oref0-dex-time-since": "./bin/oref0-dex-time-since.sh", + "oref0-dex-wait-until-expected": "./bin/oref0-dex-wait-until-expected.sh", + "oref0-find-insulin-uses": "./bin/oref0-find-insulin-uses.js", + "oref0-fix-git-corruption": "./bin/oref0-fix-git-corruption.sh", + "oref0-g4-loop": "./bin/oref0-g4-loop.sh", + "oref0-get-bg": "./bin/oref0-get-bg.sh", + "oref0-get-profile": "./bin/oref0-get-profile.js", + "oref0-html": "./bin/oref0-html.js", + "oref0-ifttt-notify": "./bin/oref0-ifttt-notify", + "oref0-log-shortcuts": "./bin/oref0-log-shortcuts.sh", + "oref0-mdt-update": "./bin/oref0-mdt-update.sh", + "oref0-meal": "./bin/oref0-meal.js", + "oref0-monitor-cgm": "./bin/oref0-monitor-cgm.sh", + "oref0-mraa-install": "./bin/oref0-mraa-install.sh", + "oref0-ns-loop": "./bin/oref0-ns-loop.sh", + "oref0-normalize-temps": "./bin/oref0-normalize-temps.js", + "oref0_nightscout_check": "./bin/oref0_nightscout_check.py", + "oref0-online": "./bin/oref0-online.sh", + "oref0-pebble": "./bin/oref0-pebble.js", + "oref0-pump-loop": "./bin/oref0-pump-loop.sh", + "oref0-pushover": "./bin/oref0-pushover.sh", + "oref0-radio-reboot": "./bin/oref0-radio-reboot.sh", + "oref0-mmtune": "./bin/oref0-mmtune.sh", + "oref0-raw": "./bin/oref0-raw.js", + "oref0-reset-git": "./bin/oref0-reset-git.sh", + "oref0-reset-usb": "./bin/oref0-reset-usb.sh", + "oref0-set-device-clocks": "./bin/oref0-set-device-clocks.sh", + "oref0-set-system-clock": "./bin/oref0-set-system-clock.sh", + "oref0-set-local-temptarget": "./bin/oref0-set-local-temptarget.js", + "oref0-setup": "./bin/oref0-setup.sh", + "oref0-simulator": "./bin/oref0-simulator.sh", + "oref0-truncate-git-history": "./bin/oref0-truncate-git-history.sh", + "oref0-upload-entries": "./bin/oref0-upload-entries.sh", + "oref0-upload-profile": "./bin/oref0-upload-profile.js", + "oref0-version": "./bin/oref0-version.sh", + "oref0-get-ns-entries": "./bin/oref0-get-ns-entries.js", + "oref0-shared-node-loop": "./bin/oref0-shared-node-loop.sh", + "peb-urchin-status": "./bin/peb-urchin-status.sh", + "wifi": "./bin/oref0-tail-wifi.sh" + }, + "homepage": "https://github.com/openaps/oref0", + "dependencies": { + "fp-ts": "^2.16.9", + "io-ts": "^2.2.21", + "io-ts-types": "^0.5.19", + "json": "^9.0.6", + "json-stable-stringify": "^1.0.1", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "moment-timezone": "^0.5.45", + "network": "^0.4.1", + "request": "^2.88.0", + "share2nightscout-bridge": "^0.2.1", + "tslib": "^2.6.3", + "yargs": "^13.2.2" + }, + "devDependencies": { + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.0", + "@types/mocha": "^10.0.6", + "@types/node": "^16.0", + "@types/should": "^13.0.0", + "@typescript-eslint/eslint-plugin": "^8.0.1", + "@typescript-eslint/parser": "^8.0.1", + "coveralls": "^3.0.3", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-fp-ts": "^0.3.2", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^5.2.1", + "istanbul": "^0.4.5", + "jest": "^29.7.0", + "mocha": "^5.2.0", + "mocha-lcov-reporter": "^1.3.0", + "prettier": "^3.3.3", + "should": "^13.2.3", + "ts-jest": "^29.2.4", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.4.5" + }, + "engines": { + "node": ">= 16.0" + }, + "config": { + "blanket": { + "pattern": [ + "bin", + "lib", + "tests" + ], + "data-cover-never": [ + "node_modules" + ] + } } - } } From fa99dc7c8b946660a098af1513cd2842ca53888a Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 03:22:24 +0200 Subject: [PATCH 08/15] refactoring --- .../{autosens.js => autosens.ts} | 182 ++++++++++-------- lib/determine-basal/cob.ts | 4 +- lib/glucose-get-last.ts | 18 +- lib/meal/history.ts | 6 +- lib/profile/index.ts | 4 +- lib/profile/isf.ts | 3 +- lib/profile/targets.ts | 8 +- lib/types/GlucoseEntry.ts | 13 ++ lib/types/TempTarget.ts | 6 + 9 files changed, 144 insertions(+), 100 deletions(-) rename lib/determine-basal/{autosens.js => autosens.ts} (78%) create mode 100644 lib/types/TempTarget.ts diff --git a/lib/determine-basal/autosens.js b/lib/determine-basal/autosens.ts similarity index 78% rename from lib/determine-basal/autosens.js rename to lib/determine-basal/autosens.ts index 05676d0cd..b3ebc3a92 100644 --- a/lib/determine-basal/autosens.js +++ b/lib/determine-basal/autosens.ts @@ -1,33 +1,50 @@ -'use strict' +import { tz } from '../date' +import get_iob from '../iob' +import find_insulin from '../iob/history' +import type { CarbEntry } from '../meal/history' +import find_meals from '../meal/history' +import percentile from '../percentile' +import { basalLookup } from '../profile/basal' +import { isfLookup } from '../profile/isf' +import type { GlucoseEntry } from '../types/GlucoseEntry' +import { getGlucoseEntryDate } from '../types/GlucoseEntry' +import type { BasalSchedule, ISFSensitivity } from '../types/Profile' +import type { TempTarget } from '../types/TempTarget' -const date = require('../date') -const get_iob = require('../iob') -const find_insulin = require('../iob/history') -const find_meals = require('../meal/history') -const percentile = require('../percentile') -const basal = require('../profile/basal') -const isf = require('../profile/isf') -const tz = date.tz +interface Inputs { + glucose_data: GlucoseEntry[] + iob_inputs: any + basalprofile: BasalSchedule[] + retrospective?: boolean + carbs: CarbEntry[] + temptargets: TempTarget[] + deviations?: number +} -function detectSensitivity(inputs) { +function detectSensitivity(inputs: Inputs) { //console.error(inputs.glucose_data[0]); const glucose_data = inputs.glucose_data.map(obj => { //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv - return obj + return { + ...obj, + glucose: obj.glucose || obj.sgv!, + } }) //console.error(glucose_data[0]); const iob_inputs = inputs.iob_inputs const basalprofile = inputs.basalprofile const profile = inputs.iob_inputs.profile + let lastSiteChange = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) // use last 24h worth of data by default if (inputs.retrospective) { - //console.error(glucose_data[0]); - var lastSiteChange = new Date(new Date(glucose_data[0].date).getTime() - 24 * 60 * 60 * 1000) - } else { - lastSiteChange = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) + const firstDate = getGlucoseEntryDate(glucose_data[0]) + if (!firstDate) { + throw new Error('Unable to find glucose date for first item') + } + lastSiteChange = new Date(firstDate.getTime() - 24 * 60 * 60 * 1000) } + if (inputs.iob_inputs.profile.rewind_resets_autosens === true) { // scan through pumphistory and set lastSiteChange to the time of the last pump rewind event // if not present, leave lastSiteChange unchanged at 24h ago. @@ -67,7 +84,7 @@ function detectSensitivity(inputs) { const avgDeltas = [] const bgis = [] const deviations = [] - let deviationSum = 0 + //let deviationSum = 0 const bucketed_data = [] glucose_data.reverse() bucketed_data[0] = glucose_data[0] @@ -75,45 +92,49 @@ function detectSensitivity(inputs) { let j = 0 // go through the meal treatments and remove any that are older than the oldest glucose value //console.error(meals); - for (var i = 1; i < glucose_data.length; ++i) { - var bgTime - var lastbgTime - if (glucose_data[i].display_time) { - bgTime = new Date(glucose_data[i].display_time.replace('T', ' ')) - } else if (glucose_data[i].dateString) { - bgTime = new Date(glucose_data[i].dateString) - } else if (glucose_data[i].xDrip_started_at) { + for (let i = 1; i < glucose_data.length; ++i) { + let bgTime + let lastbgTime + const entry = glucose_data[i] + const previous = glucose_data[i - 1] + if (entry.display_time) { + bgTime = new Date(entry.display_time.replace('T', ' ')) + } else if (entry.dateString) { + bgTime = new Date(entry.dateString) + } else if (entry.xDrip_started_at) { continue } else { console.error('Could not determine BG time') + continue } - if (glucose_data[i - 1].display_time) { - lastbgTime = new Date(glucose_data[i - 1].display_time.replace('T', ' ')) - } else if (glucose_data[i - 1].dateString) { - lastbgTime = new Date(glucose_data[i - 1].dateString) + if (previous.display_time) { + lastbgTime = new Date(previous.display_time.replace('T', ' ')) + } else if (previous.dateString) { + lastbgTime = new Date(previous.dateString) } else if (bucketed_data[0].display_time) { lastbgTime = new Date(bucketed_data[0].display_time.replace('T', ' ')) } else if (glucose_data[i - 1].xDrip_started_at) { continue } else { console.error('Could not determine last BG time') + continue } - if (glucose_data[i].glucose < 39 || glucose_data[i - 1].glucose < 39) { + if (entry.glucose < 39 || glucose_data[i - 1].glucose < 39) { //console.error("skipping:",glucose_data[i].glucose,glucose_data[i-1].glucose); continue } // only consider BGs since lastSiteChange if (lastSiteChange) { - const hoursSinceSiteChange = (bgTime - lastSiteChange) / (60 * 60 * 1000) + const hoursSinceSiteChange = (bgTime.getTime() - lastSiteChange.getTime()) / (60 * 60 * 1000) if (hoursSinceSiteChange < 0) { //console.error(hoursSinceSiteChange, bgTime, lastSiteChange); continue } } - const elapsed_minutes = (bgTime - lastbgTime) / (60 * 1000) + const elapsed_minutes = (bgTime.getTime() - lastbgTime.getTime()) / (60 * 1000) if (Math.abs(elapsed_minutes) > 2) { j++ - bucketed_data[j] = glucose_data[i] + bucketed_data[j] = entry bucketed_data[j].date = bgTime.getTime() //console.error(elapsed_minutes, bucketed_data[j].glucose, glucose_data[i].glucose); } else { @@ -123,20 +144,20 @@ function detectSensitivity(inputs) { } bucketed_data.shift() //console.error(bucketed_data[0]); - for (i = meals.length - 1; i > 0; --i) { - var treatment = meals[i] + for (let i = meals.length - 1; i > 0; --i) { + const treatment = meals[i] //console.error(treatment); if (treatment) { - var treatmentDate = new Date(tz(treatment.timestamp)) - var treatmentTime = treatmentDate.getTime() - var glucoseDatum = bucketed_data[0] + const treatmentDate = new Date(tz(treatment.timestamp)) + const treatmentTime = treatmentDate.getTime() + const glucoseDatum = bucketed_data[0] //console.error(glucoseDatum); if (!glucoseDatum || !glucoseDatum.date) { //console.error("No date found on: ",glucoseDatum); continue } - var BGDate = new Date(glucoseDatum.date) - var BGTime = BGDate.getTime() + const BGDate = new Date(glucoseDatum.date) + const BGTime = BGDate.getTime() if (treatmentTime < BGTime) { //console.error("Removing old meal: ",treatmentDate); meals.splice(i, 1) @@ -149,21 +170,23 @@ function detectSensitivity(inputs) { let mealCarbs = 0 let mealStartCounter = 999 let type = '' - let lastIsfResult = null + let lastIsfResult: ISFSensitivity | null = null //console.error(bucketed_data); - for (i = 3; i < bucketed_data.length; ++i) { - bgTime = new Date(bucketed_data[i].date) - var sens - ;[sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult) - + for (let i = 3; i < bucketed_data.length; ++i) { + const bucketed = bucketed_data[i] + const bgTime = new Date(bucketed.date!) + const [sens, isf_res] = isfLookup(profile.isfProfile, bgTime, lastIsfResult) + lastIsfResult = isf_res as ISFSensitivity | null //console.error(bgTime , bucketed_data[i].glucose); - var bg - var avgDelta - var delta + let bg + let avgDelta + let delta + let last_bg + let old_bg if (typeof bucketed_data[i].glucose !== 'undefined') { bg = bucketed_data[i].glucose - var last_bg = bucketed_data[i - 1].glucose - var old_bg = bucketed_data[i - 3].glucose + last_bg = bucketed_data[i - 1].glucose + old_bg = bucketed_data[i - 3].glucose if ( isNaN(bg) || !bg || @@ -185,9 +208,9 @@ function detectSensitivity(inputs) { continue } - avgDelta = avgDelta.toFixed(2) + avgDelta = Math.round(avgDelta * 100) / 100 iob_inputs.clock = bgTime - iob_inputs.profile.current_basal = basal.basalLookup(basalprofile, bgTime) + iob_inputs.profile.current_basal = basalLookup(basalprofile, bgTime) // make sure autosens doesn't use temptarget-adjusted insulin calculations iob_inputs.profile.temptargetSet = false //console.log(JSON.stringify(iob_inputs.profile)); @@ -196,12 +219,13 @@ function detectSensitivity(inputs) { //console.error("After: ", new Date().getTime()); //console.log(JSON.stringify(iob)); - let bgi = Math.round(-iob.activity * sens * 5 * 100) / 100 - bgi = bgi.toFixed(2) + const bgi = Math.round(-iob.activity * sens * 5 * 100) / 100 + //bgi = bgi.toFixed(2) //console.error(delta); - var deviation + let deviation if (isNaN(delta)) { console.error('Bad delta: ', delta, bg, last_bg, old_bg) + continue } else { deviation = delta - bgi } @@ -210,24 +234,27 @@ function detectSensitivity(inputs) { if (bg < 80 && deviation > 0) { deviation = 0 } - deviation = deviation.toFixed(2) + deviation = Math.round(deviation * 100) / 100 - glucoseDatum = bucketed_data[i] + let glucoseDatum: GlucoseEntry & { + glucose: number + mealCarbs?: number + } = bucketed_data[i] //console.error(glucoseDatum); - BGDate = new Date(glucoseDatum.date) - BGTime = BGDate.getTime() + const BGDate = new Date(glucoseDatum.date!) + const BGTime = BGDate.getTime() // As we're processing each data point, go through the treatment.carbs and see if any of them are older than // the current BG data point. If so, add those carbs to COB. - treatment = meals[meals.length - 1] + const treatment = meals[meals.length - 1] if (treatment) { - treatmentDate = new Date(tz(treatment.timestamp)) - treatmentTime = treatmentDate.getTime() + const treatmentDate = new Date(tz(treatment.timestamp)) + const treatmentTime = treatmentDate.getTime() if (treatmentTime < BGTime) { if (treatment.carbs >= 1) { //console.error(treatmentDate, treatmentTime, BGTime, BGTime-treatmentTime); - mealCOB += parseFloat(treatment.carbs) - mealCarbs += parseFloat(treatment.carbs) - var displayCOB = Math.round(mealCOB) + mealCOB += treatment.carbs + mealCarbs += treatment.carbs + const displayCOB = Math.round(mealCOB) //console.error(displayCOB, mealCOB, treatment.carbs); process.stderr.write(`${displayCOB.toString()}g`) } @@ -257,7 +284,7 @@ function detectSensitivity(inputs) { } // stop excluding positive deviations as soon as mealCOB=0 if meal has been absorbing for >5h if (mealStartCounter > 60 && mealCOB < 0.5) { - displayCOB = Math.round(mealCOB) + const displayCOB = Math.round(mealCOB) process.stderr.write(`${displayCOB.toString()}g`) absorbing = 0 } @@ -274,7 +301,10 @@ function detectSensitivity(inputs) { } mealStartCounter++ type = 'csf' - glucoseDatum.mealCarbs = mealCarbs + glucoseDatum = { + ...glucoseDatum, + mealCarbs, + } //if (i == 0) { glucoseDatum.mealAbsorption = "end"; } //CSFGlucoseData.push(glucoseDatum); } else { @@ -325,7 +355,7 @@ function detectSensitivity(inputs) { avgDeltas.push(avgDelta) bgis.push(bgi) deviations.push(deviation) - deviationSum += parseFloat(deviation) + //deviationSum += parseFloat(deviation) } else { process.stderr.write('x') } @@ -389,7 +419,7 @@ function detectSensitivity(inputs) { deviations.sort((a, b) => { return a - b }) - for (i = 0.9; i > 0.1; i = i - 0.01) { + for (let i = 0.9; i > 0.1; i = i - 0.01) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); if (percentile(deviations, i + 0.01) >= 0 && percentile(deviations, i) < 0) { //console.error("p="+i.toFixed(2)+": "+percentile(avgDeltas, i).toFixed(2)+", "+percentile(bgis, i).toFixed(2)+", "+percentile(deviations, i).toFixed(2)); @@ -405,11 +435,11 @@ function detectSensitivity(inputs) { const pSensitive = percentile(deviations, 0.5) const pResistant = percentile(deviations, 0.5) - const average = deviationSum / deviations.length + //const average = deviationSum / deviations.length //console.error("Mean deviation: "+average.toFixed(2)); const squareDeviations = deviations.reduce((acc, dev) => { - const dev_f = parseFloat(dev) + const dev_f = dev return acc + dev_f * dev_f }, 0) const rmsDev = Math.sqrt(squareDeviations / deviations.length) @@ -453,13 +483,13 @@ function detectSensitivity(inputs) { } module.exports = detectSensitivity -function tempTargetRunning(temptargets_data, time) { +function tempTargetRunning(temptargets_data: TempTarget[], time: Date) { // sort tempTargets by date so we can process most recent first try { temptargets_data.sort((a, b) => { - return new Date(a.created_at) < new Date(b.created_at) + return new Date(a.created_at).getTime() + new Date(b.created_at).getTime() }) - } catch (e) { + } catch (_e) { //console.error("Could not sort temptargets_data. Optional feature temporary targets disabled."); } //console.error(temptargets_data); @@ -480,4 +510,6 @@ function tempTargetRunning(temptargets_data, time) { return tempTarget } } + + return 0 } diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index 229c17709..bd195ad71 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -2,7 +2,7 @@ import get_iob from '../iob' import type { Input as IOBInput } from '../iob/history' import find_insulin from '../iob/history' import * as basal from '../profile/basal' -import isf from '../profile/isf' +import { isfLookup } from '../profile/isf' import type { GlucoseEntry } from '../types/GlucoseEntry' import type { BasalSchedule } from '../types/Profile' @@ -138,7 +138,7 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { const bgTime = new Date(bucketed_data[i].date) let sens = null - ;[sens, lastIsfResult] = isf.isfLookup(profile.isfProfile, bgTime, lastIsfResult) + ;[sens, lastIsfResult] = isfLookup(profile.isfProfile, bgTime, lastIsfResult) //console.error(bgTime , bucketed_data[i].glucose, bucketed_data[i].date); let bg diff --git a/lib/glucose-get-last.ts b/lib/glucose-get-last.ts index 594513fcd..ab24e4f22 100644 --- a/lib/glucose-get-last.ts +++ b/lib/glucose-get-last.ts @@ -1,15 +1,13 @@ -import type { GlucoseEntry } from './types/GlucoseEntry' +import { getGlucoseEntryDate, type GlucoseEntry } from './types/GlucoseEntry' function getDateFromEntry(entry: GlucoseEntry) { - if (entry.date) { - return entry.date - } else if (entry.display_time) { - return Date.parse(entry.display_time) - } else if (entry.dateString) { - return Date.parse(entry.dateString) + const date = getGlucoseEntryDate(entry) + + if (!date) { + throw new TypeError('Unable to find a date in GlucoseEntry') } - throw new TypeError('Unable to find a date in GlucoseEntry') + return date } const getLastGlucose = function (input: GlucoseEntry[]) { @@ -22,7 +20,7 @@ const getLastGlucose = function (input: GlucoseEntry[]) { ) const now = data[0] - let now_date = getDateFromEntry(now) + let now_date = getDateFromEntry(now).getTime() let change const last_deltas = [] const short_deltas = [] @@ -39,7 +37,7 @@ const getLastGlucose = function (input: GlucoseEntry[]) { // only use data from the same device as the most recent BG data point if (typeof data[i] !== 'undefined' && data[i].glucose > 38 && data[i].device === now.device) { const then = data[i] - const then_date = getDateFromEntry(then) + const then_date = getDateFromEntry(then).getTime() let avgdelta = 0 let minutesago if (typeof then_date !== 'undefined' && typeof now_date !== 'undefined') { diff --git a/lib/meal/history.ts b/lib/meal/history.ts index 0b8d3967c..ea9192bf6 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -42,7 +42,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { (a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000 ) - for (var i = 0; i < carbHistory.length; i++) { + for (let i = 0; i < carbHistory.length; i++) { const current = carbHistory[i] if (current.carbs && current.created_at) { mealInputs.push( @@ -54,7 +54,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { } } - for (i = 0; i < pumpHistory.length; i++) { + for (let i = 0; i < pumpHistory.length; i++) { const current = pumpHistory[i] if (PumpHistoryEvent.is(current) && current._type === 'Bolus' && current.timestamp) { //console.log(pumpHistory[i]); @@ -114,7 +114,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { } } - for (i = 0; i < bolusWizardInputs.length; i++) { + for (let i = 0; i < bolusWizardInputs.length; i++) { const current = bolusWizardInputs[i] //console.log(bolusWizardInputs[i]); const temp = createMeal(current.timestamp, { diff --git a/lib/profile/index.ts b/lib/profile/index.ts index a22198bc0..21f8f7063 100644 --- a/lib/profile/index.ts +++ b/lib/profile/index.ts @@ -2,7 +2,7 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' import { maxDailyBasal, basalLookup, maxBasalLookup } from './basal' import carb_ratios from './carbs' -import isf from './isf' +import { isfLookup } from './isf' import * as targets from './targets' export function defaults() { @@ -159,7 +159,7 @@ export default function generate(final_result: FinalResult, inputs: any, opts: a profile.temptargetSet = range.temptargetSet let lastResult = null - ;[profile.sens, lastResult] = isf.isfLookup(inputs.isf, undefined, lastResult) + ;[profile.sens, lastResult] = isfLookup(inputs.isf, undefined, lastResult) profile.isfProfile = inputs.isf if (profile.sens < 5) { console_error(final_result, 'ISF of', profile.sens, 'is not supported') diff --git a/lib/profile/isf.ts b/lib/profile/isf.ts index 9e7e9e8ed..933371930 100644 --- a/lib/profile/isf.ts +++ b/lib/profile/isf.ts @@ -1,6 +1,6 @@ import type { ISFProfile, ISFSensitivity } from '../types/Profile' -export default function isfLookup( +export function isfLookup( isf_profile: ISFProfile, timestamp: Date | undefined, lastResult: ISFSensitivity | null @@ -42,5 +42,6 @@ export default function isfLookup( ] } +export default isfLookup isfLookup.isfLookup = isfLookup exports = module.exports = isfLookup diff --git a/lib/profile/targets.ts b/lib/profile/targets.ts index 94fc203c9..db37df17f 100644 --- a/lib/profile/targets.ts +++ b/lib/profile/targets.ts @@ -2,6 +2,7 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' import getTime from '../medtronic-clock' import type { Profile } from '../types/Profile' +import type { TempTarget } from '../types/TempTarget' interface BgTarget { offset: number @@ -12,13 +13,6 @@ interface BgTarget { temptargetSet?: boolean } -interface TempTarget { - created_at: string - duration: number - targetTop: number - targetBottom: number -} - interface LookupInputs { targets: { targets: BgTarget[] diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts index efb5273f4..c9b31c475 100644 --- a/lib/types/GlucoseEntry.ts +++ b/lib/types/GlucoseEntry.ts @@ -7,4 +7,17 @@ export interface GlucoseEntry { type?: 'sgv' | 'cal' | string device?: string noise?: number + xDrip_started_at?: unknown +} + +export const getGlucoseEntryDate = (entry: GlucoseEntry): Date | undefined => { + if (entry.date) { + return new Date(entry.date) + } else if (entry.dateString) { + return new Date(entry.dateString) + } else if (entry.display_time) { + return new Date(entry.display_time.replace('T', ' ')) + } + + return undefined } diff --git a/lib/types/TempTarget.ts b/lib/types/TempTarget.ts new file mode 100644 index 000000000..33d86dfae --- /dev/null +++ b/lib/types/TempTarget.ts @@ -0,0 +1,6 @@ +export interface TempTarget { + created_at: string + duration: number + targetTop: number + targetBottom: number +} From 86822fbd04d08953c5931f0375a77d937cc15e86 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 14:03:29 +0200 Subject: [PATCH 09/15] meal total refactoring --- lib/meal/total.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/meal/total.ts b/lib/meal/total.ts index 54d7cf289..f2edc2b24 100644 --- a/lib/meal/total.ts +++ b/lib/meal/total.ts @@ -16,7 +16,23 @@ export interface Options { clock: string } -export default function recentCarbs(opts: Options, time: Date) { +export interface RecentCarbs { + carbs: number + nsCarbs: number + bwCarbs: number + journalCarbs: number + mealCOB: number + currentDeviation: number + maxDeviation: number + minDeviation: number + slopeFromMaxDeviation: number + slopeFromMinDeviation: number + allDeviations: number[] + lastCarbTime: number + bwFound: boolean +} + +export default function recentCarbs(opts: Options, time: Date): Partial { const treatments = opts.treatments const profile_data = opts.profile const glucose_data = opts.glucose From da365acfa47cabbf2469514ad2b51b150bdefb57 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 19:24:54 +0200 Subject: [PATCH 10/15] types refactoring --- lib/determine-basal/autosens.ts | 3 +- lib/determine-basal/cob.ts | 8 +- lib/iob/index.ts | 3 +- lib/iob/total.ts | 4 +- lib/meal/history.ts | 29 +-- lib/meal/index.ts | 7 +- lib/meal/total.ts | 5 +- lib/profile/basal.ts | 6 +- lib/types/BasalSchedule.ts | 25 +++ lib/types/GlucoseUnit.ts | 10 + lib/types/InsulineCurve.ts | 18 ++ lib/types/PositiveInt.ts | 9 + lib/types/PositiveNumber.ts | 8 + lib/types/Preferences.ts | 347 ++++++++++++++++++++++++++++++++ lib/types/Profile.ts | 52 +---- lib/types/ScheduleStart.ts | 7 + 16 files changed, 467 insertions(+), 74 deletions(-) create mode 100644 lib/types/BasalSchedule.ts create mode 100644 lib/types/GlucoseUnit.ts create mode 100644 lib/types/InsulineCurve.ts create mode 100644 lib/types/PositiveInt.ts create mode 100644 lib/types/PositiveNumber.ts create mode 100644 lib/types/Preferences.ts create mode 100644 lib/types/ScheduleStart.ts diff --git a/lib/determine-basal/autosens.ts b/lib/determine-basal/autosens.ts index b3ebc3a92..03b65a86f 100644 --- a/lib/determine-basal/autosens.ts +++ b/lib/determine-basal/autosens.ts @@ -6,9 +6,10 @@ import find_meals from '../meal/history' import percentile from '../percentile' import { basalLookup } from '../profile/basal' import { isfLookup } from '../profile/isf' +import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' import { getGlucoseEntryDate } from '../types/GlucoseEntry' -import type { BasalSchedule, ISFSensitivity } from '../types/Profile' +import type { ISFSensitivity } from '../types/Profile' import type { TempTarget } from '../types/TempTarget' interface Inputs { diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index bd195ad71..bc032e6d7 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -3,8 +3,8 @@ import type { Input as IOBInput } from '../iob/history' import find_insulin from '../iob/history' import * as basal from '../profile/basal' import { isfLookup } from '../profile/isf' +import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' -import type { BasalSchedule } from '../types/Profile' export interface DetectCOBInput { glucose_data: GlucoseEntry[] @@ -158,7 +158,11 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { } iob_inputs.clock = bgTime.toISOString() - iob_inputs.profile.current_basal = basal.basalLookup(basalprofile || [], bgTime) + const current_basal = basal.basalLookup(basalprofile || [], bgTime) + if (!current_basal) { + continue + } + iob_inputs.profile.current_basal = current_basal //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); const iob = get_iob(iob_inputs, true, treatments)[0] diff --git a/lib/iob/index.ts b/lib/iob/index.ts index 40882d472..5b04ea085 100644 --- a/lib/iob/index.ts +++ b/lib/iob/index.ts @@ -24,8 +24,9 @@ interface IOBItem extends IOB { } } -export default function generate(inputs: Input, currentIOBOnly: boolean = false, treatments?: InsulinTreatment[]) { +export default function generate(inputs: Input, currentIOBOnly: boolean = false, inputTreatments?: InsulinTreatment[]) { let treatmentsWithZeroTemp: InsulinTreatment[] = [] + let treatments = inputTreatments if (!treatments) { treatments = find_insulin(inputs) // calculate IOB based on continuous future zero temping as well diff --git a/lib/iob/total.ts b/lib/iob/total.ts index d11b05802..bb65712d3 100644 --- a/lib/iob/total.ts +++ b/lib/iob/total.ts @@ -1,6 +1,6 @@ import type { Autosens } from '../types/Autosens' +import { InsulineCurve } from '../types/InsulineCurve' import type { Profile } from '../types/Profile' -import { InsulineCurve } from '../types/Profile' import type { InsulinTreatment } from './InsulinTreatment' import { isBolusTreatment } from './InsulinTreatment' import calculate from './calculate' @@ -8,7 +8,7 @@ import calculate from './calculate' interface Options { treatments: InsulinTreatment[] profile: Profile - autosens?: Autosens + autosens?: Autosens | undefined } export default function iobTotal(opts: Options, time: Date) { diff --git a/lib/meal/history.ts b/lib/meal/history.ts index ea9192bf6..478a965e0 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -47,8 +47,8 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { if (current.carbs && current.created_at) { mealInputs.push( createMeal(current.created_at, { - carbs: current.carbs !== null ? current.carbs : undefined, - nsCarbs: current.carbs !== null ? current.carbs : undefined, + carbs: current.carbs, + nsCarbs: current.carbs, }) ) } @@ -56,7 +56,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { for (let i = 0; i < pumpHistory.length; i++) { const current = pumpHistory[i] - if (PumpHistoryEvent.is(current) && current._type === 'Bolus' && current.timestamp) { + if (PumpHistoryEvent.is(current) && current._type === 'Bolus' && current.timestamp && current.amount) { //console.log(pumpHistory[i]); mealInputs.push(createMeal(current.timestamp, { bolus: current.amount })) } else if (PumpHistoryEvent.is(current) && current._type === 'BolusWizard' && current.timestamp) { @@ -70,7 +70,8 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { current.eventType === 'Correction Bolus' || current.eventType === 'Snack Bolus' || current.eventType === 'Bolus Wizard' || - current.eventType === 'Carb Correction') + current.eventType === 'Carb Correction') && + current.carbs ) { //imports carbs entered through Nightscout Care Portal //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard @@ -79,24 +80,24 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { // to prevent duped carb entries from multiple sources mealInputs.push( createMeal(current.created_at, { - carbs: current.carbs !== null ? current.carbs : undefined, - nsCarbs: current.carbs !== null ? current.carbs : undefined, + carbs: current.carbs, + nsCarbs: current.carbs, }) ) } else if (NightscoutTreatment.is(current) && current.enteredBy === 'xdrip' && current.created_at) { mealInputs.push( createMeal(current.created_at, { - carbs: current.carbs !== null ? current.carbs : undefined, - nsCarbs: current.carbs !== null ? current.carbs : undefined, - bolus: current.insulin !== null ? current.insulin : undefined, + carbs: current.carbs || 0, + nsCarbs: current.carbs || 0, + bolus: current.insulin || 0, }) ) } else if (NightscoutTreatment.is(current) && current.carbs && current.carbs > 0 && current.created_at) { mealInputs.push( createMeal(current.created_at, { - carbs: current.carbs !== null ? current.carbs : undefined, - nsCarbs: current.carbs !== null ? current.carbs : undefined, - bolus: current.insulin !== null ? current.insulin : undefined, + carbs: current.carbs || 0, + nsCarbs: current.carbs || 0, + bolus: current.insulin || 0, }) ) } else if ( @@ -118,8 +119,8 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { const current = bolusWizardInputs[i] //console.log(bolusWizardInputs[i]); const temp = createMeal(current.timestamp, { - carbs: current.carb_input, - bwCarbs: current.carb_input, + carbs: current.carb_input || 0, + bwCarbs: current.carb_input || 0, }) // don't enter the treatment if there's another treatment with the same exact timestamp diff --git a/lib/meal/index.ts b/lib/meal/index.ts index 70ca127ab..d2e957cce 100644 --- a/lib/meal/index.ts +++ b/lib/meal/index.ts @@ -1,7 +1,8 @@ import { tz } from '../date' +import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' import type { NightscoutTreatment } from '../types/NightscoutTreatment' -import type { BasalSchedule, Profile } from '../types/Profile' +import type { Profile } from '../types/Profile' import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' import type { CarbEntry } from './history' import find_meals from './history' @@ -23,8 +24,8 @@ export default function generate(inputs: Input) { treatments: treatments, profile: inputs.profile, pumphistory: inputs.history, - glucose: inputs.glucose, - basalprofile: inputs.basalprofile, + basalprofile: inputs.basalprofile || [], + glucose: inputs.glucose || [], clock: inputs.clock, } diff --git a/lib/meal/total.ts b/lib/meal/total.ts index f2edc2b24..0223f6cb7 100644 --- a/lib/meal/total.ts +++ b/lib/meal/total.ts @@ -1,9 +1,10 @@ import { tz } from '../date' import type { DetectCOBInput } from '../determine-basal/cob' import detectCarbAbsorption from '../determine-basal/cob' +import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' import type { NightscoutTreatment } from '../types/NightscoutTreatment' -import type { BasalSchedule, Profile } from '../types/Profile' +import type { Profile } from '../types/Profile' import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' import type { MealTreatment } from './MealTreatment' @@ -56,7 +57,7 @@ export default function recentCarbs(opts: Options, time: Date): Partial (Number(a.rate) > b ? Number(a.rate) : b), 0) +export function maxDailyBasal(inputs: { basals: { rate: number }[] }): number { + const max = inputs.basals.reduce((b, a) => (a.rate > b ? a.rate : b), 0) return (Number(max) * 1000) / 1000 } diff --git a/lib/types/BasalSchedule.ts b/lib/types/BasalSchedule.ts new file mode 100644 index 000000000..3a108e741 --- /dev/null +++ b/lib/types/BasalSchedule.ts @@ -0,0 +1,25 @@ +import * as t from 'io-ts' +import { PositiveInt } from './PositiveInt' +import { PositiveNumber } from './PositiveNumber' +import { ScheduleStart } from './ScheduleStart' + +export interface BasalSchedule { + i?: t.Int + start: ScheduleStart + minutes: PositiveInt + rate: PositiveNumber +} + +export const BasalSchedule: t.Type = t.intersection( + [ + t.type({ + start: ScheduleStart, + minutes: PositiveInt, + rate: PositiveNumber, + }), + t.partial({ + i: t.Int, + }), + ], + 'BasalSchedule' +) diff --git a/lib/types/GlucoseUnit.ts b/lib/types/GlucoseUnit.ts new file mode 100644 index 000000000..554820149 --- /dev/null +++ b/lib/types/GlucoseUnit.ts @@ -0,0 +1,10 @@ +import * as t from 'io-ts' + +export type GlucoseUnit = 'mg/dL' | 'mmol/L' +export const GlucoseUnit: t.Type = t.keyof( + { + 'mg/dL': null, + 'mmol/L': null, + }, + 'GlucoseUnit' +) diff --git a/lib/types/InsulineCurve.ts b/lib/types/InsulineCurve.ts new file mode 100644 index 000000000..8a6eabc8e --- /dev/null +++ b/lib/types/InsulineCurve.ts @@ -0,0 +1,18 @@ +import * as t from 'io-ts' + +/** + * Insulin curve. + * + * - `ultra-rapid`: Fiasp + * - `rapid-acting`: Humalog + * - `bilinear`: old curve + */ +export type InsulineCurve = 'bilinear' | 'rapid-acting' | 'ultra-rapid' +export const InsulineCurve: t.Type = t.keyof( + { + bilinear: null, + 'rapid-acting': null, + 'ultra-rapid': null, + }, + 'InsulineCurve' +) diff --git a/lib/types/PositiveInt.ts b/lib/types/PositiveInt.ts new file mode 100644 index 000000000..923eb9208 --- /dev/null +++ b/lib/types/PositiveInt.ts @@ -0,0 +1,9 @@ +import * as t from 'io-ts' +import type { PositiveNumberBrand } from './PositiveNumber' + +export interface PositiveIntBrand extends PositiveNumberBrand { + readonly PositiveInt: unique symbol +} + +export type PositiveInt = t.Branded +export const PositiveInt = t.brand(t.Int, (n): n is PositiveInt => n >= 0, 'PositiveInt') diff --git a/lib/types/PositiveNumber.ts b/lib/types/PositiveNumber.ts new file mode 100644 index 000000000..bf7144831 --- /dev/null +++ b/lib/types/PositiveNumber.ts @@ -0,0 +1,8 @@ +import * as t from 'io-ts' + +export interface PositiveNumberBrand { + readonly PositiveNumber: unique symbol +} + +export type PositiveNumber = t.Branded +export const PositiveNumber = t.brand(t.number, (n): n is PositiveNumber => n >= 0, 'PositiveNumber') diff --git a/lib/types/Preferences.ts b/lib/types/Preferences.ts new file mode 100644 index 000000000..257460f34 --- /dev/null +++ b/lib/types/Preferences.ts @@ -0,0 +1,347 @@ +import type { NonEmptyArray } from 'fp-ts/NonEmptyArray' +import * as t from 'io-ts' +import { fromNullable } from 'io-ts-types/fromNullable' +import { nonEmptyArray } from 'io-ts-types/nonEmptyArray' +import { BasalSchedule } from './BasalSchedule' +import { GlucoseUnit } from './GlucoseUnit' +import { InsulineCurve } from './InsulineCurve' +import { PositiveInt } from './PositiveInt' +import { PositiveNumber } from './PositiveNumber' + +interface PumpSettings { + insulin_action_curve?: PositiveNumber +} + +const PumpSettings = t.partial({ + insulin_action_curve: t.refinement(PositiveNumber, a => a > 1, 'PositiveNumber(> 1)'), +}) + +interface Targets { + user_preferred_units: GlucoseUnit +} + +const Targets = t.type({ + user_preferred_units: GlucoseUnit, +}) + +export interface Preferences { + [k: string]: unknown + max_iob: PositiveInt + max_daily_safety_multiplier: PositiveNumber + current_basal_safety_multiplier: PositiveNumber + autosens_max: PositiveNumber + autosens_min: PositiveNumber + rewind_resets_autosens: boolean + /** raise sensitivity for temptargets >= 101. synonym for exercise_mode */ + high_temptarget_raises_sensitivity: boolean + /** lower sensitivity for temptargets <= 99. */ + low_temptarget_lowers_sensitivity: boolean + /** raise BG target when autosens detects sensitivity */ + sensitivity_raises_target: boolean + /** lower BG target when autosens detects resistance */ + resistance_lowers_target: boolean + /** when true, > 100 mg/dL high temp target adjusts sensitivityRatio for exercise_mode. This majorly changes the behavior of high temp targets from before. synonmym for high_temptarget_raises_sensitivity */ + exercise_mode: boolean + half_basal_exercise_target: PositiveInt + /** + * Max carbs absorbed in 4 hours. + * + * Default to 120 because that's the most a typical body can absorb over 4 hours. + * + * If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120. + * Essentially, this just limits AMA/SMB as a safety cap against excessive COB entry. + */ + maxCOB: PositiveInt + /** if true, don't set neutral temps */ + skip_neutral_temps: boolean + /** if true, pump will un-suspend after a zero temp finishes */ + unsuspend_if_no_temp: boolean + /** bolus snooze decays after 1/2 of DIA */ + bolussnooze_dia_divisor: PositiveInt + /** mg/dL per 5m (8 mg/dL/5m corresponds to 24g/hr at a CSF of 4 mg/dL/g (x/5*60/4)) */ + min_5m_carbimpact: PositiveInt + /** keep autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF. 1.0 allows full adjustment, 0 is no adjustment from pump ISF. */ + autotune_isf_adjustmentFraction: PositiveNumber + /** fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption */ + remainingCarbsFraction: PositiveNumber + /** max carbs we'll assume will absorb over 4h if we don't yet see carb absorption */ + remainingCarbsCap: PositiveInt + /** Enable detection of unannounced meal carb absorption */ + enableUAM: boolean + A52_risk_enable: boolean + /** Enable supermicrobolus while COB is positive */ + enableSMB_with_COB: boolean + /** Enable supermicrobolus for eating soon temp targets */ + enableSMB_with_temptarget: boolean + /** + * Always enable supermicrobolus (unless disabled by high temptarget). + * + * **WARNING** + * DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar + * + * LimiTTer, etc. do not properly filter out high-noise SGVs. xDrip+ builds greater than or equal to + * version number d8e-7097-2018-01-22 provide proper noise values, so that oref0 can ignore high noise + * readings, and can temporarily raise the BG target when sensor readings have medium noise, + * resulting in appropriate SMB behaviour. Older versions of xDrip+ should not be used with enableSMB_always. + * Using SMB overnight with such data sources risks causing a dangerous overdose of insulin + * if the CGM sensor reads falsely high and doesn't come down as actual BG does + */ + enableSMB_always: boolean + /** + * Enable supermicrobolus for 6h after carbs, even with 0 COB. + * + * **WARNING** + * DO NOT USE enableSMB_always or enableSMB_after_carbs with Libre or similar. + */ + enableSMB_after_carbs: boolean + /** Enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile) */ + enableSMB_high_bg: boolean + /** Set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable. */ + enableSMB_high_bg_target: PositiveInt + /** Allow supermicrobolus (if otherwise enabled) even with high temp targets */ + allowSMB_with_high_temptarget: boolean + /** maximum minutes of basal that can be delivered as a single SMB with uncovered COB */ + maxSMBBasalMinutes: PositiveInt + /** maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB */ + maxUAMSMBBasalMinutes: PositiveInt + /** minimum interval between SMBs, in minutes. */ + SMBInterval: PositiveInt + /** minimum bolus that can be delivered as an SMB */ + bolus_increment: PositiveNumber + /** maximum change in bg to use SMB, above that will disable SMB */ + maxDelta_bg_threshold: PositiveNumber + /** Insulin curve. */ + curve: InsulineCurve + /** allows changing insulinPeakTime */ + useCustomPeakTime: boolean + /** + * Number of minutes after a bolus activity peaks. + * Defaults to 55m for Fiasp if useCustomPeakTime: boolean + */ + insulinPeakTime: PositiveInt + /** grams of carbsReq to trigger a pushover */ + carbsReqThreshold: PositiveInt + /** enabled an offline-only local wifi hotspot if no Internet available */ + offline_hotspot: boolean + /** increase target by this amount when looping off raw/noisy CGM data */ + noisyCGMTargetMultiplier: PositiveNumber + /** recognize pump suspends as non insulin delivery events */ + suspend_zeros_iob: boolean + /** + * Send the glucose data to the pump emulating an enlite sensor. + * This allows to setup high / low warnings when offline and see trend. + * + * To enable this feature, enable the sensor, set a sensor with id 0000000, + * go to start sensor and press find lost sensor. + */ + enableEnliteBgproxy: boolean + calc_glucose_noise: boolean + /** set to an integer value in mg/dL to override pump min_bg */ + target_bg: boolean + edison_battery_shutdown_voltage: PositiveInt + pi_battery_shutdown_percent: PositiveInt + // no defaults + model?: string + settings: PumpSettings + basals: NonEmptyArray + targets: Targets +} + +/** + * @todo: should we parse numbers from strings? + */ +export const Preferences: t.Type = t.intersection( + [ + t.record(t.string, t.unknown), + t.type({ + max_iob: fromNullable(PositiveInt, 0 as PositiveInt), + max_daily_safety_multiplier: fromNullable(PositiveNumber, 3 as PositiveNumber), + current_basal_safety_multiplier: fromNullable(PositiveNumber, 3 as PositiveNumber), + autosens_max: fromNullable(PositiveNumber, 1.2 as PositiveNumber), + autosens_min: fromNullable(PositiveNumber, 0.7 as PositiveNumber), + rewind_resets_autosens: fromNullable(t.boolean, true), + high_temptarget_raises_sensitivity: fromNullable(t.boolean, false), + low_temptarget_lowers_sensitivity: fromNullable(t.boolean, false), + sensitivity_raises_target: fromNullable(t.boolean, true), + resistance_lowers_target: fromNullable(t.boolean, false), + exercise_mode: fromNullable(t.boolean, false), + half_basal_exercise_target: fromNullable(PositiveInt, 160 as PositiveInt), + maxCOB: fromNullable(PositiveInt, 120 as PositiveInt), + skip_neutral_temps: fromNullable(t.boolean, false), + unsuspend_if_no_temp: fromNullable(t.boolean, false), + bolussnooze_dia_divisor: fromNullable(PositiveInt, 2 as PositiveInt), + min_5m_carbimpact: fromNullable(PositiveInt, 8 as PositiveInt), + autotune_isf_adjustmentFraction: fromNullable(PositiveNumber, 1.0 as PositiveNumber), + remainingCarbsFraction: fromNullable(PositiveNumber, 1.0 as PositiveNumber), + remainingCarbsCap: fromNullable(PositiveInt, 90 as PositiveInt), + enableUAM: fromNullable(t.boolean, true), + A52_risk_enable: fromNullable(t.boolean, false), + enableSMB_with_COB: fromNullable(t.boolean, false), + enableSMB_with_temptarget: fromNullable(t.boolean, false), + enableSMB_always: fromNullable(t.boolean, false), + enableSMB_after_carbs: fromNullable(t.boolean, false), + enableSMB_high_bg: fromNullable(t.boolean, false), + enableSMB_high_bg_target: fromNullable(PositiveInt, 110 as PositiveInt), + allowSMB_with_high_temptarget: fromNullable(t.boolean, false), + maxSMBBasalMinutes: fromNullable(PositiveInt, 30 as PositiveInt), + maxUAMSMBBasalMinutes: fromNullable(PositiveInt, 30 as PositiveInt), + SMBInterval: fromNullable(PositiveInt, 3 as PositiveInt), + bolus_increment: fromNullable(PositiveNumber, 0.1 as PositiveNumber), + maxDelta_bg_threshold: fromNullable(PositiveNumber, 0.2 as PositiveNumber), + curve: fromNullable(InsulineCurve, 'rapid-acting'), + useCustomPeakTime: fromNullable(t.boolean, false), + insulinPeakTime: fromNullable(PositiveInt, 75 as PositiveInt), + carbsReqThreshold: fromNullable(PositiveInt, 1 as PositiveInt), + offline_hotspot: fromNullable(t.boolean, false), + noisyCGMTargetMultiplier: fromNullable(PositiveNumber, 1.3 as PositiveNumber), + suspend_zeros_iob: fromNullable(t.boolean, true), + enableEnliteBgproxy: fromNullable(t.boolean, false), + calc_glucose_noise: fromNullable(t.boolean, false), + target_bg: fromNullable(t.boolean, false), + edison_battery_shutdown_voltage: fromNullable(PositiveInt, 3050 as PositiveInt), + pi_battery_shutdown_percent: fromNullable(PositiveInt, 2 as PositiveInt), + // no defaults + basals: nonEmptyArray(BasalSchedule), + settings: PumpSettings, + targets: Targets, + }), + t.partial({ + model: t.string, + }), + ], + 'Preferences' +) + +/** + * { + "max_iob": 14, + "max_daily_safety_multiplier": 3, + "current_basal_safety_multiplier": 4, + "autosens_max": 1.3, + "autosens_min": 0.7, + "rewind_resets_autosens": true, + "high_temptarget_raises_sensitivity": true, + "low_temptarget_lowers_sensitivity": true, + "sensitivity_raises_target": true, + "resistance_lowers_target": false, + "exercise_mode": false, + "half_basal_exercise_target": 160, + "maxCOB": 120, + "skip_neutral_temps": false, + "unsuspend_if_no_temp": false, + "min_5m_carbimpact": 8, + "autotune_isf_adjustmentFraction": 1, + "remainingCarbsFraction": 1, + "remainingCarbsCap": 90, + "enableUAM": true, + "A52_risk_enable": false, + "enableSMB_with_COB": true, + "enableSMB_with_temptarget": true, + "enableSMB_always": false, + "enableSMB_after_carbs": true, + "allowSMB_with_high_temptarget": false, + "maxSMBBasalMinutes": 90, + "maxUAMSMBBasalMinutes": 120, + "SMBInterval": 3, + "bolus_increment": 0.05, + "maxDelta_bg_threshold": 0.2, + "curve": "ultra-rapid", + "useCustomPeakTime": false, + "insulinPeakTime": 55, + "carbsReqThreshold": 1, + "offline_hotspot": false, + "noisyCGMTargetMultiplier": 1.3, + "suspend_zeros_iob": false, + "enableEnliteBgproxy": false, + "calc_glucose_noise": false, + "target_bg": false, + "smb_delivery_ratio": 0.7, + "adjustmentFactor": 0.7, + "useNewFormula": true, + "enableDynamicCR": true, + "sigmoid": true, + "weightPercentage": 0.65, + "tddAdjBasal": true, + "enableSMB_high_bg": true, + "enableSMB_high_bg_target": 110, + "threshold_setting": 65, + "dia": 9, + "model": "722", + "current_basal": 0.7, + "basalprofile": [ + { + "rate": 0.7, + "start": "00:00:00", + "minutes": 0 + }, + { + "rate": 0.7, + "minutes": 180, + "start": "03:00:00" + }, + { + "start": "09:00:00", + "minutes": 540, + "rate": 0.7 + } + ], + "max_daily_basal": 0.7, + "max_basal": 3, + "out_units": "mg/dL", + "min_bg": 98, + "max_bg": 98, + "bg_targets": { + "units": "mg/dL", + "user_preferred_units": "mg/dL", + "targets": [ + { + "start": "00:00:00", + "high": 98, + "offset": 0, + "low": 98, + "max_bg": 98, + "min_bg": 98 + } + ] + }, + "sens": 65, + "isfProfile": { + "user_preferred_units": "mg/dL", + "units": "mg/dL", + "sensitivities": [ + { + "start": "00:00:00", + "sensitivity": 65, + "offset": 0, + "endOffset": 1440 + } + ] + }, + "carb_ratio": 6.5, + "carb_ratios": { + "units": "grams", + "schedule": [ + { + "start": "00:00:00", + "offset": 0, + "ratio": 6 + }, + { + "ratio": 5, + "start": "08:30:00", + "offset": 510 + }, + { + "start": "11:30:00", + "ratio": 6, + "offset": 690 + }, + { + "offset": 960, + "ratio": 6.5, + "start": "16:00:00" + } + ] + } +} + */ diff --git a/lib/types/Profile.ts b/lib/types/Profile.ts index 6b6193ed5..966c4e9e7 100644 --- a/lib/types/Profile.ts +++ b/lib/types/Profile.ts @@ -1,25 +1,7 @@ import * as t from 'io-ts' - -export interface BasalSchedule { - i?: number - start: string - minutes: number - rate: number -} - -export const BasalSchedule: t.Type = t.intersection( - [ - t.type({ - start: t.string, - minutes: t.number, - rate: t.number, - }), - t.partial({ - i: t.number, - }), - ], - 'BasalSchedule' -) +import { BasalSchedule } from './BasalSchedule' +import { GlucoseUnit } from './GlucoseUnit' +import { InsulineCurve } from './InsulineCurve' /** * { @@ -183,16 +165,6 @@ export const CarbRatios: t.Type = t.type( 'CarbRatios' ) -export type InsulineCurve = 'bilinear' | 'rapid-acting' | 'ultra-rapid' -export const InsulineCurve: t.Type = t.keyof( - { - bilinear: null, - 'rapid-acting': null, - 'ultra-rapid': null, - }, - 'InsulineCurve' -) - export interface ISFSensitivity { i?: number offset: number @@ -237,21 +209,12 @@ export const ISFProfile: t.Type = t.intersection( 'ISFProfile' ) -export type GlucoseUnits = 'mg/dL' | 'mmol/L' -export const GlucoseUnits = t.keyof( - { - 'mg/dL': null, - 'mmol/L': null, - }, - 'GlucoseUnits' -) - export interface Profile { basalprofile: Array sens: number carb_ratio: number min_5m_carbimpact: number - out_units?: GlucoseUnits + out_units?: GlucoseUnit max_daily_safety_multiplier?: number current_basal_safety_multiplier?: number model?: string @@ -301,7 +264,7 @@ export interface Profile { SMBInterval?: number } -export const Profile: t.Type = t.intersection( +export const Profile: t.Type = t.intersection( [ t.type({ basalprofile: t.array(BasalSchedule), @@ -310,10 +273,7 @@ export const Profile: t.Type = t.intersection( min_5m_carbimpact: t.number, }), t.partial({ - out_units: t.keyof({ - 'mg/dL': null, - 'mmol/L': null, - }), + out_units: GlucoseUnit, max_daily_safety_multiplier: t.number, current_basal_safety_multiplier: t.number, model: t.string, diff --git a/lib/types/ScheduleStart.ts b/lib/types/ScheduleStart.ts new file mode 100644 index 000000000..e54e1ce60 --- /dev/null +++ b/lib/types/ScheduleStart.ts @@ -0,0 +1,7 @@ +import * as t from 'io-ts' + +export const ScheduleStart = t.refinement(t.string, s => /^([01][0-9]|2[0-3]):([0-5][0-9])$/.test(s), 'ScheduleStart') +/** + * Time in HH:MM + */ +export type ScheduleStart = t.TypeOf From d475c97a62327a182e74d16909c8c5b30756ec53 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Fri, 9 Aug 2024 19:31:12 +0200 Subject: [PATCH 11/15] refactor EventType --- lib/types/EventType.ts | 101 ++++++++++++++++++++++--------- lib/types/NightscoutTreatment.ts | 3 +- lib/types/PumpHistoryEvent.ts | 41 +------------ 3 files changed, 76 insertions(+), 69 deletions(-) diff --git a/lib/types/EventType.ts b/lib/types/EventType.ts index ba01ebf97..1d1c75bfe 100644 --- a/lib/types/EventType.ts +++ b/lib/types/EventType.ts @@ -1,28 +1,73 @@ -export enum EventType { - bolus = 'Bolus', - smb = 'SMB', - isExternal = 'External Insulin', - mealBolus = 'Meal Bolus', - correctionBolus = 'Correction Bolus', - snackBolus = 'Snack Bolus', - bolusWizard = 'BolusWizard', - tempBasal = 'TempBasal', - tempBasalDuration = 'TempBasalDuration', - pumpSuspend = 'PumpSuspend', - pumpResume = 'PumpResume', - pumpAlarm = 'PumpAlarm', - pumpBattery = 'PumpBattery', - rewind = 'Rewind', - prime = 'Prime', - journalCarbs = 'JournalEntryMealMarker', - nsTempBasal = 'Temp Basal', - nsCarbCorrection = 'Carb Correction', - nsTempTarget = 'Temporary Target', - nsInsulinChange = 'Insulin Change', - nsSiteChange = 'Site Change', - nsBatteryChange = 'Pump Battery Change', - nsAnnouncement = 'Announcement', - nsSensorChange = 'Sensor Start', - capillaryGlucose = 'BG Check', - nsExercise = 'Exercise', -} +import * as t from 'io-ts' + +export type NightscoutEventType = + | 'Temp Basal' + | 'Carb Correction' + | 'Temporary Target' + | 'Insulin Change' + | 'Site Change' + | 'Pump Battery Change' + | 'Announcement' + | 'Sensor Start' + | 'BG Check' + | 'Exercise' + | 'Bolus Wizard' + +export type EventType = + | NightscoutEventType + | 'Bolus' + | 'SMB' + | 'External Insulin' + | 'Meal Bolus' + | 'Correction Bolus' + | 'Snack Bolus' + | 'BolusWizard' + | 'TempBasal' + | 'TempBasalDuration' + | 'PumpSuspend' + | 'PumpResume' + | 'PumpAlarm' + | 'PumpBattery' + | 'Rewind' + | 'Prime' + | 'JournalEntryMealMarker' + | 'SuspendBasal' + | string + +export const NightscoutEventType: t.Type = t.keyof({ + 'Temp Basal': null, + 'Carb Correction': null, + 'Temporary Target': null, + 'Insulin Change': null, + 'Site Change': null, + 'Pump Battery Change': null, + Announcement: null, + 'Sensor Start': null, + 'BG Check': null, + Exercise: null, + 'Bolus Wizard': null, +}) + +export const EventType: t.Type = t.union([ + NightscoutEventType, + t.keyof({ + Bolus: null, + SMB: null, + 'External Insulin': null, + 'Meal Bolus': null, + 'Correction Bolus': null, + 'Snack Bolus': null, + BolusWizard: null, + TempBasal: null, + TempBasalDuration: null, + PumpSuspend: null, + PumpResume: null, + PumpAlarm: null, + PumpBattery: null, + Rewind: null, + Prime: null, + JournalEntryMealMarker: null, + SuspendBasal: null, + }), + t.string, +]) diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts index abdd364ce..bb53383fa 100644 --- a/lib/types/NightscoutTreatment.ts +++ b/lib/types/NightscoutTreatment.ts @@ -1,5 +1,6 @@ import * as t from 'io-ts' -import { EventType, PumpHistoryEvent } from './PumpHistoryEvent' +import { EventType } from './EventType' +import { PumpHistoryEvent } from './PumpHistoryEvent' export interface NightscoutTreatment { eventType: EventType diff --git a/lib/types/PumpHistoryEvent.ts b/lib/types/PumpHistoryEvent.ts index f0f454724..5ee2f06b5 100644 --- a/lib/types/PumpHistoryEvent.ts +++ b/lib/types/PumpHistoryEvent.ts @@ -1,44 +1,5 @@ import * as t from 'io-ts' - -export const NightscoutEventType = t.keyof({ - 'Temp Basal': null, - 'Carb Correction': null, - 'Temporary Target': null, - 'Insulin Change': null, - 'Site Change': null, - 'Pump Battery Change': null, - Announcement: null, - 'Sensor Start': null, - 'BG Check': null, - Exercise: null, - 'Bolus Wizard': null, -}) - -export const EventType = t.union([ - NightscoutEventType, - t.keyof({ - Bolus: null, - SMB: null, - 'External Insulin': null, - 'Meal Bolus': null, - 'Correction Bolus': null, - 'Snack Bolus': null, - BolusWizard: null, - TempBasal: null, - TempBasalDuration: null, - PumpSuspend: null, - PumpResume: null, - PumpAlarm: null, - PumpBattery: null, - Rewind: null, - Prime: null, - JournalEntryMealMarker: null, - SuspendBasal: null, - }), - t.string, -]) - -export type EventType = t.TypeOf +import { EventType } from './EventType' export const TempType = t.keyof({ absolute: null, From 5611a1c0e25ba2c8a3d5ac57c2a5f107eadbec79 Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Sun, 11 Aug 2024 17:19:35 +0200 Subject: [PATCH 12/15] migrate from fp-ts to effect --- .eslintrc.js | 1 + bin/oref0-get-profile.js | 16 +- lib/determine-basal/autosens.ts | 6 +- lib/determine-basal/cob.ts | 16 +- lib/determine-basal/determine-basal.ts | 16 +- lib/glucose-get-last.ts | 4 +- lib/iob/history.ts | 113 +++--- lib/iob/total.ts | 3 +- lib/meal/MealTreatment.ts | 6 + lib/meal/history.ts | 39 +- lib/profile/basal.ts | 14 +- lib/profile/carbs.ts | 16 +- lib/profile/index.ts | 56 +-- lib/profile/isf.ts | 12 +- lib/profile/targets.ts | 64 ++-- lib/round-basal.ts | 2 +- lib/types/Autosens.ts | 18 +- lib/types/BasalSchedule.ts | 37 +- lib/types/CarbRatioSchedule.ts | 14 + lib/types/CarbRatios.ts | 12 + lib/types/EventType.ts | 100 ++---- lib/types/GlucoseEntry.ts | 54 ++- lib/types/GlucoseUnit.ts | 16 +- lib/types/ISFSensitivity.ts | 21 ++ lib/types/InsulineCurve.ts | 17 +- lib/types/LocalDateFromDate.ts | 11 - lib/types/NightscoutTreatment.ts | 90 ++--- lib/types/PositiveInt.ts | 16 +- lib/types/PositiveNumber.ts | 16 +- lib/types/Preferences.ts | 324 +++++++++++------ lib/types/Profile.ts | 475 +++++++++---------------- lib/types/PumpHistoryEvent.ts | 60 +--- lib/types/ScheduleStart.ts | 17 +- lib/types/TempTarget.ts | 21 +- lib/types/renameKey.ts | 30 -- package.json | 7 +- tests/command-behavior.tests.sh | 4 +- tests/determine-basal.test.ts | 111 ++++-- tests/profile.test.ts | 7 +- tsconfig.json | 1 + 40 files changed, 914 insertions(+), 949 deletions(-) create mode 100644 lib/types/CarbRatioSchedule.ts create mode 100644 lib/types/CarbRatios.ts create mode 100644 lib/types/ISFSensitivity.ts delete mode 100644 lib/types/LocalDateFromDate.ts delete mode 100644 lib/types/renameKey.ts diff --git a/.eslintrc.js b/.eslintrc.js index 9e02124e2..fc41ecea5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -76,6 +76,7 @@ module.exports = { 'node/no-missing-import': 'off', 'import/no-unresolved': 'error', + '@typescript-eslint/no-empty-object-type': 'off', '@typescript-eslint/dot-notation': 'error', '@typescript-eslint/no-namespace': 'warn', '@typescript-eslint/consistent-type-definitions': 'error', diff --git a/bin/oref0-get-profile.js b/bin/oref0-get-profile.js index 82f8e57e8..1cab20626 100755 --- a/bin/oref0-get-profile.js +++ b/bin/oref0-get-profile.js @@ -32,9 +32,9 @@ function exportDefaults (final_result) { function updatePreferences (final_result, prefs) { var defaults = generate.displayedDefaults(final_result); - + // check for any displayedDefaults missing from current prefs and add from defaults - + for (var pref in defaults) { if (defaults.hasOwnProperty(pref) && !prefs.hasOwnProperty(pref)) { prefs[pref] = defaults[pref]; @@ -44,7 +44,7 @@ function updatePreferences (final_result, prefs) { console_log(final_result, JSON.stringify(prefs, null, '\t')); } -var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { +var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { var argv = require('yargs')(argv_params) .usage("$0 [] [] [] [--model ] [--autotune ] [--exportDefaults] [--updatePreferences ]") .option('model', { @@ -122,7 +122,7 @@ var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { return; } } - + var isf_data = JSON.parse(fs.readFileSync(cwd + '/' + isf_input)); if (isf_data.units !== 'mg/dL') { if (isf_data.units === 'mmol/L') { @@ -144,7 +144,7 @@ var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { preferences = JSON.parse(fs.readFileSync(cwd + '/' + preferences_input)); } - var model_data = { } + var model_data = undefined if (params.model) { try { var model_string = fs.readFileSync(model_input, 'utf8'); @@ -171,7 +171,7 @@ var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { } } - var carbratio_data = { }; + var carbratio_data = undefined; //console.log("carbratio_input",carbratio_input); if (typeof carbratio_input !== 'undefined') { try { @@ -200,11 +200,11 @@ var oref0_get_profile = function oref0_get_profile(final_result, argv_params) { }); console_log(final_result, JSON.stringify(errors)); process_exit(final_result, 1); - + return; } } - var temptargets_data = { }; + var temptargets_data = []; if (typeof temptargets_input !== 'undefined') { try { temptargets_data = JSON.parse(fs.readFileSync(temptargets_input, 'utf8')); diff --git a/lib/determine-basal/autosens.ts b/lib/determine-basal/autosens.ts index 03b65a86f..49bfe26f1 100644 --- a/lib/determine-basal/autosens.ts +++ b/lib/determine-basal/autosens.ts @@ -8,8 +8,8 @@ import { basalLookup } from '../profile/basal' import { isfLookup } from '../profile/isf' import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' -import { getGlucoseEntryDate } from '../types/GlucoseEntry' -import type { ISFSensitivity } from '../types/Profile' +import { getDate } from '../types/GlucoseEntry' +import type { ISFSensitivity } from '../types/ISFSensitivity' import type { TempTarget } from '../types/TempTarget' interface Inputs { @@ -39,7 +39,7 @@ function detectSensitivity(inputs: Inputs) { let lastSiteChange = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) // use last 24h worth of data by default if (inputs.retrospective) { - const firstDate = getGlucoseEntryDate(glucose_data[0]) + const firstDate = getDate(glucose_data[0]) if (!firstDate) { throw new Error('Unable to find glucose date for first item') } diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index bc032e6d7..75f6cef9b 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -26,6 +26,9 @@ function getDateFromEntry(entry: GlucoseEntry) { throw new TypeError('Unable to find a date in GlucoseEntry') } +/** + * @todo: does it works with profile.carb_ratio === undefined? + */ export default function detectCarbAbsorption(inputs: DetectCOBInput) { const glucose_data = inputs.glucose_data.reduce( (b, a) => { @@ -162,10 +165,17 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { if (!current_basal) { continue } - iob_inputs.profile.current_basal = current_basal + const newIobInputs = { + ...iob_inputs, + profile: { + ...iob_inputs.profile, + current_basal, + }, + } + //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); - const iob = get_iob(iob_inputs, true, treatments)[0] + const iob = get_iob(newIobInputs, true, treatments)[0] //console.error("After: ", new Date().getTime()); //console.error(JSON.stringify(iob)); @@ -208,7 +218,7 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { // if currentDeviation is > 2 * min_5m_carbimpact, assume currentDeviation/2 worth of carbs were absorbed // but always assume at least profile.min_5m_carbimpact (3mg/dL/5m by default) absorption const ci = Math.max(deviation, currentDeviation / 2, profile.min_5m_carbimpact) - const absorbed = (ci * profile.carb_ratio) / sens + const absorbed = profile.carb_ratio ? (ci * profile.carb_ratio) / sens : 0 // and add that to the running total carbsAbsorbed //console.error("carbsAbsorbed:",carbsAbsorbed,"absorbed:",absorbed,"bgTime:",bgTime,"BG:",bucketed_data[i].glucose) carbsAbsorbed += absorbed diff --git a/lib/determine-basal/determine-basal.ts b/lib/determine-basal/determine-basal.ts index 86ab57623..531ebad0b 100644 --- a/lib/determine-basal/determine-basal.ts +++ b/lib/determine-basal/determine-basal.ts @@ -321,6 +321,8 @@ const determine_basal = function determine_basal( // and before using target_bg to adjust sensitivityRatio below. const max_iob = profile.max_iob // maximum amount of non-bolus IOB OpenAPS will ever deliver + const carb_ratio = profile.carb_ratio !== undefined ? round(profile.carb_ratio, 2) : 0 + // if min and max are set, then set target to their average let target_bg: number let min_bg = profile.min_bg as number @@ -443,7 +445,7 @@ const determine_basal = function determine_basal( } //process.stderr.write(" (autosens ratio "+sensitivityRatio+")"); } - console.error('; CR:', profile.carb_ratio) + console.error('; CR:', carb_ratio) const iob_data = Array.isArray(iobArray) ? iobArray[0] : iobArray @@ -573,7 +575,7 @@ const determine_basal = function determine_basal( // use autosens-adjusted sens to counteract autosens meal insulin dosing adjustments so that // autotuned CR is still in effect even when basals and ISF are being adjusted by TT or autosens // this avoids overdosing insulin for large meals when low temp targets are active - const csf = sens / profile.carb_ratio + const csf = carb_ratio ? sens / carb_ratio : 0 console.error('profile.sens:', profile.sens, 'sens:', sens, 'CSF:', csf) const maxCarbAbsorptionRate = 30 // g/h; maximum rate to assume carbs will absorb if no CI observed @@ -682,7 +684,6 @@ const determine_basal = function determine_basal( let maxCOBPredBG = bg //var maxUAMPredBG = bg; //var eventualPredBG = bg; - let lastIOBpredBG let lastCOBpredBG let lastUAMpredBG //var lastZTpredBG; @@ -807,7 +808,7 @@ const determine_basal = function determine_basal( } } rT.predBGs.IOB = IOBpredBGs - lastIOBpredBG = round(IOBpredBGs[IOBpredBGs.length - 1]) + const lastIOBpredBG = round(IOBpredBGs[IOBpredBGs.length - 1]) ZTpredBGs.forEach((p, i, theArray) => { theArray[i] = round(Math.min(401, Math.max(39, p))) }) @@ -965,7 +966,7 @@ const determine_basal = function determine_basal( rT.BGI = convert_bg(bgi, profile) rT.deviation = convert_bg(deviation, profile) rT.ISF = convert_bg(sens, profile) - rT.CR = round(profile.carb_ratio, 2) + rT.CR = carb_ratio rT.target_bg = convert_bg(target_bg, profile) rT.reason = `COB: ${rT.COB}, Dev: ${rT.deviation}, BGI: ${rT.BGI}, ISF: ${rT.ISF}, CR: ${ rT.CR @@ -1277,9 +1278,10 @@ const determine_basal = function determine_basal( // only allow microboluses with COB or low temp targets, or within DIA hours of a bolus if (microBolusAllowed && enableSMB && bg > threshold) { // never bolus more than maxSMBBasalMinutes worth of basal - const mealInsulinReq = round(meal_data.mealCOB / profile.carb_ratio, 3) + const mealInsulinReq = carb_ratio ? round(meal_data.mealCOB / carb_ratio, 3) : 0 + let maxBolus if (typeof profile.maxSMBBasalMinutes === 'undefined') { - var maxBolus = round((profile.current_basal * 30) / 60, 1) + maxBolus = round((profile.current_basal * 30) / 60, 1) console.error('profile.maxSMBBasalMinutes undefined: defaulting to 30m') // if IOB covers more than COB, limit maxBolus to 30m of basal } else if (iob_data.iob > mealInsulinReq && iob_data.iob > 0) { diff --git a/lib/glucose-get-last.ts b/lib/glucose-get-last.ts index ab24e4f22..0bd0d47cf 100644 --- a/lib/glucose-get-last.ts +++ b/lib/glucose-get-last.ts @@ -1,7 +1,7 @@ -import { getGlucoseEntryDate, type GlucoseEntry } from './types/GlucoseEntry' +import { getDate, type GlucoseEntry } from './types/GlucoseEntry' function getDateFromEntry(entry: GlucoseEntry) { - const date = getGlucoseEntryDate(entry) + const date = getDate(entry) if (!date) { throw new TypeError('Unable to find a date in GlucoseEntry') diff --git a/lib/iob/history.ts b/lib/iob/history.ts index bcdb95b9c..5d0356d29 100644 --- a/lib/iob/history.ts +++ b/lib/iob/history.ts @@ -1,10 +1,10 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' import { tz } from '../date' import * as date from '../date' import * as basalprofile from '../profile/basal' -import { Autosens } from '../types/Autosens' +import type { Autosens } from '../types/Autosens' import { NightscoutTreatment } from '../types/NightscoutTreatment' -import { Profile } from '../types/Profile' +import type { Profile } from '../types/Profile' import { PumpHistoryEvent } from '../types/PumpHistoryEvent' import type { BasalTreatment, BolusTreatment, InsulinTreatment } from './InsulinTreatment' @@ -20,19 +20,13 @@ interface PumpSuspendResume { duration: number } -const Input = t.intersection([ - t.type({ - history: t.array(t.union([NightscoutTreatment, PumpHistoryEvent])), - profile: Profile, - }), - t.partial({ - history24: t.array(t.union([NightscoutTreatment, PumpHistoryEvent])), - autosens: Autosens, - clock: t.string, - }), -]) - -export type Input = t.TypeOf +export interface Input { + history: Array + history24?: Array + profile: Profile + autosens?: Autosens + clock?: string +} function splitTimespanWithOneSplitter(event: BasalTreatment, splitter: Splitter) { if (splitter.type !== 'recurring') { @@ -231,10 +225,13 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num let lastRecordTime = now // Gather the times the pump was suspended and resumed - for (var i = 0; i < pumpHistory.length; i++) { + for (let i = 0; i < pumpHistory.length; i++) { const current = pumpHistory[i] - if (!PumpHistoryEvent.is(current) || (current._type !== 'PumpSuspend' && current._type !== 'PumpResume')) { + if ( + !Schema.is(PumpHistoryEvent)(current) || + (current._type !== 'PumpSuspend' && current._type !== 'PumpResume') + ) { continue } @@ -268,10 +265,11 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num let j = 0 // matching pumpResumes entry; + let iSuspends = 0 // Match the resumes with the suspends to get durations - for (i = 0; i < pumpSuspends.length; i++) { + for (iSuspends = 0; iSuspends < pumpSuspends.length; iSuspends++) { for (; j < pumpResumes.length; j++) { - if (pumpResumes[j].date > pumpSuspends[i].date) { + if (pumpResumes[j].date > pumpSuspends[iSuspends].date) { break } } @@ -283,12 +281,12 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num // through the last record beginning at the last suspend // since we don't have a matching resume. currentlySuspended = true - lastSuspendTime = pumpSuspends[i].timestamp + lastSuspendTime = pumpSuspends[iSuspends].timestamp break } - pumpSuspends[i].duration = (pumpResumes[j].date - pumpSuspends[i].date) / 60 / 1000 + pumpSuspends[iSuspends].duration = (pumpResumes[j].date - pumpSuspends[iSuspends].date) / 60 / 1000 } // These checks indicate something isn't quite aligned. @@ -309,21 +307,21 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num ) } - if (i < pumpSuspends.length - 1) { + if (iSuspends < pumpSuspends.length - 1) { // truncate any extra suspends. if we had any extras // the error checks above would have issued a error log message - pumpSuspends.splice(i + 1, pumpSuspends.length - i - 1) + pumpSuspends.splice(iSuspends + 1, pumpSuspends.length - iSuspends - 1) } // Pick relevant events for processing and clean the data - for (i = 0; i < pumpHistory.length; i++) { + for (let i = 0; i < pumpHistory.length; i++) { let current: NightscoutTreatment | PumpHistoryEvent = pumpHistory[i] - if (NightscoutTreatment.is(current) && current.bolus && current.bolus._type === 'Bolus') { + if (Schema.is(NightscoutTreatment)(current) && current.bolus && current.bolus._type === 'Bolus') { current = current.bolus } - const timestamp = NightscoutTreatment.is(current) ? current.created_at : current.timestamp + const timestamp = Schema.is(NightscoutTreatment)(current) ? current.created_at : current.timestamp const currentRecordTime = tz(new Date(timestamp)) //console.error(current); //console.error(currentRecordTime,lastRecordTime); @@ -335,7 +333,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num } else { lastRecordTime = currentRecordTime } - if (PumpHistoryEvent.is(current) && current._type === 'Bolus') { + if (Schema.is(PumpHistoryEvent)(current) && current._type === 'Bolus') { const started_at = tz(new Date(current.timestamp)) if (started_at > now) { //console.error("Warning: ignoring",current.amount,"U bolus in the future at",temp.started_at); @@ -350,7 +348,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num }) } } else if ( - NightscoutTreatment.is(current) && + Schema.is(NightscoutTreatment)(current) && (current.eventType === 'Meal Bolus' || current.eventType === 'Correction Bolus' || current.eventType === 'Snack Bolus' || @@ -366,7 +364,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num date: started_at.getTime(), insulin: current.insulin!, }) - } else if (NightscoutTreatment.is(current) && current.enteredBy === 'xdrip') { + } else if (Schema.is(NightscoutTreatment)(current) && current.enteredBy === 'xdrip') { const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ @@ -375,7 +373,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num date: started_at.getTime(), insulin: current.insulin!, }) - } else if (NightscoutTreatment.is(current) && current.enteredBy === 'HAPP_App' && current.insulin) { + } else if (Schema.is(NightscoutTreatment)(current) && current.enteredBy === 'HAPP_App' && current.insulin) { const started_at = tz(new Date(current.created_at)) // @todo check for undefined insulin tempBoluses.push({ @@ -385,7 +383,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num insulin: current.insulin!, }) } else if ( - NightscoutTreatment.is(current) && + Schema.is(NightscoutTreatment)(current) && current.eventType === 'Temp Basal' && (current.enteredBy === 'HAPP_App' || current.enteredBy === 'openaps://AndroidAPS') ) { @@ -398,7 +396,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num rate: current.absolute!, duration: current.duration!, }) - } else if (NightscoutTreatment.is(current) && current.eventType === 'Temp Basal') { + } else if (Schema.is(NightscoutTreatment)(current) && current.eventType === 'Temp Basal') { const started_at = tz(new Date(current.created_at)) let rate = current.rate // Loop reports the amount of insulin actually delivered while the temp basal was running @@ -415,7 +413,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num rate: rate!, duration: current.duration!, }) - } else if (PumpHistoryEvent.is(current) && current._type === 'TempBasal') { + } else if (Schema.is(PumpHistoryEvent)(current) && current._type === 'TempBasal') { if (current.temp === 'percent') { continue } @@ -423,7 +421,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num let duration const previous = i > 0 ? pumpHistory[i - 1] : undefined if ( - PumpHistoryEvent.is(previous) && + Schema.is(PumpHistoryEvent)(previous) && previous.timestamp === timestamp && previous._type === 'TempBasalDuration' ) { @@ -432,7 +430,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num for (let iter = 0; iter < pumpHistory.length; iter++) { const item = pumpHistory[iter] if ( - PumpHistoryEvent.is(item) && + Schema.is(PumpHistoryEvent)(item) && item.timestamp === timestamp && item._type === 'TempBasalDuration' ) { @@ -464,11 +462,11 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num // Add a temp basal cancel event to ignore future temps and reduce predBG oscillation // start the zero temp 1m in the future to avoid clock skew - const started_at = new Date(now.getTime() + 1 * 60 * 1000) + const started_atTemp = new Date(now.getTime() + 1 * 60 * 1000) tempHistory.push({ - timestamp: started_at.toISOString(), - started_at, - date: started_at.getTime(), + timestamp: started_atTemp.toISOString(), + started_at: started_atTemp, + date: started_atTemp.getTime(), rate: 0, duration: zeroTempDuration || 0, }) @@ -478,7 +476,7 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num tempHistory = tempHistory.sort((a, b) => a.date - b.date) - for (i = 0; i < tempHistory.length - 1; i++) { + for (let i = 0; i < tempHistory.length - 1; i++) { const item = tempHistory[i] const next = tempHistory[i + 1] // @todo: check duration when undefined (or null) @@ -550,9 +548,9 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num // insulin is present prior to resume will be aged // out due to DIA. if (suspendedPrior && max_dia_ago < firstResumeDate) { - var suspendStart = new Date(max_dia_ago) - var suspendStartDate = suspendStart.getTime() - var started_at = tz(suspendStart) + const suspendStart = new Date(max_dia_ago) + const suspendStartDate = suspendStart.getTime() + const started_at = tz(suspendStart) zTempSuspendBasals.push({ rate: 0, @@ -565,9 +563,9 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num if (currentlySuspended) { // @todo check why lastSuspendTime can be undefined - var suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date() - var suspendStartDate = suspendStart.getTime() - var started_at = tz(suspendStart) + const suspendStart = lastSuspendTime ? new Date(lastSuspendTime) : new Date() + const suspendStartDate = suspendStart.getTime() + const started_at = tz(suspendStart) // @todo check why lastSuspendTime can be undefined zTempSuspendBasals.push({ @@ -591,15 +589,23 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num // iterate through the temp basals and create bolus events from temps that affect IOB - for (i = 0; i < splitHistory.length; i++) { + for (let i = 0; i < splitHistory.length; i++) { const currentItem = splitHistory[i] if (currentItem.duration > 0) { - var target_bg + let target_bg let currentRate = profile_data.current_basal if (profile_data.basalprofile && profile_data.basalprofile.length > 0) { - currentRate = basalprofile.basalLookup(profile_data.basalprofile, new Date(currentItem.timestamp)) + const newCurrentRate = basalprofile.basalLookup( + profile_data.basalprofile, + new Date(currentItem.timestamp) + ) + if (!newCurrentRate) { + // @todo: handle errors + throw new Error('Unable to find basal rate for iob time') + } + currentRate = newCurrentRate } if (typeof profile_data.min_bg !== 'undefined' && typeof profile_data.max_bg !== 'undefined') { @@ -609,13 +615,12 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num //sensitivityRatio = 2/(2+(target_bg-100)/40); //currentRate = profile_data.current_basal * sensitivityRatio; //} - var sensitivityRatio + let sensitivityRatio const profile = profile_data const normalTarget = 100 // evaluate high/low temptarget against 100, not scheduled basal (which might change) + let halfBasalTarget = 160 // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) if (profile.half_basal_exercise_target) { - var halfBasalTarget = profile.half_basal_exercise_target - } else { - halfBasalTarget = 160 as t.Int // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + halfBasalTarget = profile.half_basal_exercise_target } if (profile.exercise_mode && profile.temptargetSet && target_bg && target_bg >= normalTarget + 5) { // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 diff --git a/lib/iob/total.ts b/lib/iob/total.ts index bb65712d3..0457b96de 100644 --- a/lib/iob/total.ts +++ b/lib/iob/total.ts @@ -1,3 +1,4 @@ +import { Schema } from '@effect/schema' import type { Autosens } from '../types/Autosens' import { InsulineCurve } from '../types/InsulineCurve' import type { Profile } from '../types/Profile' @@ -64,7 +65,7 @@ export default function iobTotal(opts: Options, time: Date) { let curve = profile_data.curve || 'bilinear' // @todo: remove when decoding - if (!InsulineCurve.is(curve)) { + if (!Schema.is(InsulineCurve)(curve)) { console.error( `Unsupported curve function: "${curve}". Supported curves: "bilinear", "rapid-acting" (Novolog, Novorapid, Humalog, Apidra) and "ultra-rapid" (Fiasp). Defaulting to "rapid-acting".` ) diff --git a/lib/meal/MealTreatment.ts b/lib/meal/MealTreatment.ts index 53e687d82..6e6e225e3 100644 --- a/lib/meal/MealTreatment.ts +++ b/lib/meal/MealTreatment.ts @@ -1,3 +1,5 @@ +import * as O from 'effect/Order' + export interface MealTreatment { timestamp: string carbs: number @@ -6,3 +8,7 @@ export interface MealTreatment { bolus: number journalCarbs: number } + +export const Order: O.Order = O.make((a, b) => + O.Date(new Date(a.timestamp), new Date(b.timestamp)) +) diff --git a/lib/meal/history.ts b/lib/meal/history.ts index 478a965e0..e2ae49d42 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -1,10 +1,12 @@ //import { PumpEntry, PumpEntryBolusWizard } from "../types/PumpEntry.ts.bak"; -import { uniq } from 'fp-ts/Array' -import { struct, eqStrict, fromEquals } from 'fp-ts/Eq' +import { Schema } from '@effect/schema' +import { dedupeWith, sort } from 'effect/Array' +import { struct, strict } from 'effect/Equivalence' import { NightscoutTreatment } from '../types/NightscoutTreatment' import { PumpHistoryEvent } from '../types/PumpHistoryEvent' import type { MealTreatment } from './MealTreatment' +import { Order } from './MealTreatment' export interface CarbEntry { carbs?: number @@ -38,9 +40,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { const mealInputs: TempMealTreatment[] = [] const bolusWizardInputs: PumpHistoryEvent[] = [] - const timestampEq = fromEquals( - (a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000 - ) + const timestampEq = (a: string, b: string) => Math.abs(new Date(a).getTime() - new Date(b).getTime()) < 2000 for (let i = 0; i < carbHistory.length; i++) { const current = carbHistory[i] @@ -56,15 +56,15 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { for (let i = 0; i < pumpHistory.length; i++) { const current = pumpHistory[i] - if (PumpHistoryEvent.is(current) && current._type === 'Bolus' && current.timestamp && current.amount) { + if (Schema.is(PumpHistoryEvent)(current) && current._type === 'Bolus' && current.timestamp && current.amount) { //console.log(pumpHistory[i]); mealInputs.push(createMeal(current.timestamp, { bolus: current.amount })) - } else if (PumpHistoryEvent.is(current) && current._type === 'BolusWizard' && current.timestamp) { + } else if (Schema.is(PumpHistoryEvent)(current) && current._type === 'BolusWizard' && current.timestamp) { // Delay process the BolusWizard entries to make sure we've seen all possible that correspond to the bolus wizard. // More specifically, we need to make sure we process the corresponding bolus entry first. bolusWizardInputs.push(current) } else if ( - NightscoutTreatment.is(current) && + Schema.is(NightscoutTreatment)(current) && current.created_at && (current.eventType === 'Meal Bolus' || current.eventType === 'Correction Bolus' || @@ -84,7 +84,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { nsCarbs: current.carbs, }) ) - } else if (NightscoutTreatment.is(current) && current.enteredBy === 'xdrip' && current.created_at) { + } else if (Schema.is(NightscoutTreatment)(current) && current.enteredBy === 'xdrip' && current.created_at) { mealInputs.push( createMeal(current.created_at, { carbs: current.carbs || 0, @@ -92,7 +92,12 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { bolus: current.insulin || 0, }) ) - } else if (NightscoutTreatment.is(current) && current.carbs && current.carbs > 0 && current.created_at) { + } else if ( + Schema.is(NightscoutTreatment)(current) && + current.carbs && + current.carbs > 0 && + current.created_at + ) { mealInputs.push( createMeal(current.created_at, { carbs: current.carbs || 0, @@ -101,7 +106,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { }) ) } else if ( - PumpHistoryEvent.is(current) && + Schema.is(PumpHistoryEvent)(current) && current._type === 'JournalEntryMealMarker' && current.carb_input && current.carb_input > 0 @@ -125,11 +130,11 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { // don't enter the treatment if there's another treatment with the same exact timestamp // to prevent duped carb entries from multiple sources - if (mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasCarbs)) { + if (mealInputs.some(a => timestampEq(a.timestamp, current.timestamp) && a.hasCarbs)) { continue } - if (!mealInputs.some(a => timestampEq.equals(a.timestamp, current.timestamp) && a.hasBolus)) { + if (!mealInputs.some(a => timestampEq(a.timestamp, current.timestamp) && a.hasBolus)) { console.error( 'Skipping bolus wizard entry', i, @@ -145,12 +150,10 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { const eq = struct({ timestamp: timestampEq, - carbs: eqStrict, - bolus: eqStrict, + carbs: strict(), + bolus: strict(), }) - return uniq(eq)(mealInputs).sort( - (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() - ) + return sort(dedupeWith(mealInputs, eq), Order) } exports = module.exports = findMealInputs diff --git a/lib/profile/basal.ts b/lib/profile/basal.ts index 34141c139..0fcbe8e79 100644 --- a/lib/profile/basal.ts +++ b/lib/profile/basal.ts @@ -1,11 +1,13 @@ -import type { BasalSchedule } from '../types/BasalSchedule' +import { sort } from 'effect/Array' +import * as BasalSchedule from '../types/BasalSchedule' +import type { Preferences } from '../types/Preferences' /* Return basal rate(U / hr) at the provided timeOfDay */ -export function basalLookup(schedules: BasalSchedule[], now?: Date) { +export function basalLookup(schedules: readonly BasalSchedule.BasalSchedule[], now?: Date) { const nowDate = now || new Date() - // @todo: check `i` because it can be undefined - const basalprofile_data = schedules.sort((a, b) => Number(a.i) - Number(b.i)) + const basalprofile_data = sort(BasalSchedule.Order)(schedules) + let basalRate = basalprofile_data[basalprofile_data.length - 1].rate if (basalRate === 0) { // TODO - shared node - move this print to shared object. @@ -23,14 +25,14 @@ export function basalLookup(schedules: BasalSchedule[], now?: Date) { return Math.round(basalRate * 1000) / 1000 } -export function maxDailyBasal(inputs: { basals: { rate: number }[] }): number { +export function maxDailyBasal(inputs: Preferences): number { const max = inputs.basals.reduce((b, a) => (a.rate > b ? a.rate : b), 0) return (Number(max) * 1000) / 1000 } /*Return maximum daily basal rate(U / hr) from profile.basals */ -export function maxBasalLookup(inputs: { settings: { maxBasal: number } }): number { +export function maxBasalLookup(inputs: Preferences): number | undefined { return inputs.settings.maxBasal } diff --git a/lib/profile/carbs.ts b/lib/profile/carbs.ts index d0f0c2a49..43d5037a1 100644 --- a/lib/profile/carbs.ts +++ b/lib/profile/carbs.ts @@ -1,20 +1,13 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' import getTime from '../medtronic-clock' -import type { CarbRatioSchedule, Profile } from '../types/Profile' +import type { Preferences } from '../types/Preferences' -interface Inputs { - carbratio?: { - schedule?: CarbRatioSchedule[] - units: 'grams' | 'exchanges' | string - } -} - -export default function carbRatioLookup(final_result: FinalResult, inputs: Inputs, _profile?: Profile) { +export function carbRatioLookup(final_result: FinalResult, inputs: Preferences) { const now = new Date() const carbratio_data = inputs.carbratio const carbratio_schedule = carbratio_data?.schedule - if (typeof carbratio_data !== 'undefined' && carbratio_schedule) { + if (carbratio_data && carbratio_schedule) { if (carbratio_data.units === 'grams' || carbratio_data.units === 'exchanges') { //carbratio_data.schedule.sort(function (a, b) { return a.offset > b.offset }); let carbRatio = carbratio_schedule[carbratio_schedule.length - 1] @@ -34,7 +27,7 @@ export default function carbRatioLookup(final_result: FinalResult, inputs: Input } } if (carbratio_data.units === 'exchanges') { - carbRatio.ratio = 12 / carbRatio.ratio + return 12 / carbRatio.ratio } return carbRatio.ratio } else { @@ -48,5 +41,6 @@ export default function carbRatioLookup(final_result: FinalResult, inputs: Input } } +export default carbRatioLookup carbRatioLookup.carbRatioLookup = carbRatioLookup exports = module.exports = carbRatioLookup diff --git a/lib/profile/index.ts b/lib/profile/index.ts index 21f8f7063..4095a5974 100644 --- a/lib/profile/index.ts +++ b/lib/profile/index.ts @@ -1,7 +1,8 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' import { maxDailyBasal, basalLookup, maxBasalLookup } from './basal' -import carb_ratios from './carbs' +//import { carbRatioLookup } from './carbs' +import carbRatioLookup from './carbs' import { isfLookup } from './isf' import * as targets from './targets' @@ -100,36 +101,41 @@ export function displayedDefaults(final_result: FinalResult) { export default function generate(final_result: FinalResult, inputs: any, opts: any) { const profile = opts && opts.type ? opts : defaults() + const preferences = inputs - // check if inputs has overrides for any of the default prefs + // check if preferences has overrides for any of the default prefs // and apply if applicable for (const pref in profile) { - if (Object.prototype.hasOwnProperty.call(inputs, pref)) { - profile[pref] = inputs[pref] + if (Object.prototype.hasOwnProperty.call(preferences, pref)) { + profile[pref] = preferences[pref] } } - const pumpsettings_data = inputs.settings - if (inputs.settings.insulin_action_curve > 1) { + const pumpsettings_data = preferences.settings + if (preferences.settings.insulin_action_curve > 1) { profile.dia = pumpsettings_data.insulin_action_curve } else { console_error(final_result, 'DIA of', profile.dia, 'is not supported') return -1 } - if (inputs.model) { - profile.model = inputs.model + if (preferences.model) { + profile.model = preferences.model } - profile.skip_neutral_temps = inputs.skip_neutral_temps + profile.skip_neutral_temps = preferences.skip_neutral_temps - profile.current_basal = basalLookup(inputs.basals) - profile.basalprofile = inputs.basals - ;[...profile.basalprofile].forEach(basalentry => { - basalentry.rate = Number(`${Math.round(Number(`${basalentry.rate}e+3`))}e-3`) - }) + profile.current_basal = basalLookup(preferences.basals) + if (!profile.current_basal) { + console.error('ERROR: bad basal schedule', profile.current_basal) + return -1 + } + profile.basalprofile = preferences.basals.map((basalentry: any) => ({ + ...basalentry, + rate: Math.round(basalentry.rate * 100) / 100, + })) - profile.max_daily_basal = maxDailyBasal(inputs) - profile.max_basal = maxBasalLookup(inputs) + profile.max_daily_basal = maxDailyBasal(preferences) + profile.max_basal = maxBasalLookup(preferences) if (profile.current_basal === 0) { console_error(final_result, 'current_basal of', profile.current_basal, 'is not supported') return -1 @@ -143,11 +149,11 @@ export default function generate(final_result: FinalResult, inputs: any, opts: a return -1 } - const range = targets.bgTargetsLookup(final_result, inputs, profile) - profile.out_units = inputs.targets.user_preferred_units + const range = targets.bgTargetsLookup(final_result, preferences) + profile.out_units = preferences.targets.user_preferred_units profile.min_bg = Math.round(range.min_bg) profile.max_bg = Math.round(range.max_bg) - profile.bg_targets = inputs.targets + profile.bg_targets = preferences.targets ;(profile.bg_targets.targets || []).forEach((bg_entry: any) => { bg_entry.high = Math.round(bg_entry.high) bg_entry.low = Math.round(bg_entry.low) @@ -158,16 +164,16 @@ export default function generate(final_result: FinalResult, inputs: any, opts: a delete profile.bg_targets.raw profile.temptargetSet = range.temptargetSet - let lastResult = null - ;[profile.sens, lastResult] = isfLookup(inputs.isf, undefined, lastResult) - profile.isfProfile = inputs.isf + const [sens] = isfLookup(preferences.isf, undefined, null) + profile.sens = sens + profile.isfProfile = preferences.isf if (profile.sens < 5) { console_error(final_result, 'ISF of', profile.sens, 'is not supported') return -1 } - if (typeof inputs.carbratio !== 'undefined') { - profile.carb_ratio = carb_ratios.carbRatioLookup(final_result, inputs, profile) - profile.carb_ratios = inputs.carbratio + if (typeof preferences.carbratio !== 'undefined') { + profile.carb_ratio = carbRatioLookup(final_result, preferences) + profile.carb_ratios = preferences.carbratio } else { console_error(final_result, "Profile wasn't given carb ratio data, cannot calculate carb_ratio") } diff --git a/lib/profile/isf.ts b/lib/profile/isf.ts index 933371930..df397b228 100644 --- a/lib/profile/isf.ts +++ b/lib/profile/isf.ts @@ -1,19 +1,21 @@ -import type { ISFProfile, ISFSensitivity } from '../types/Profile' +import { sort } from 'effect/Array' +import * as ISFSensitivity from '../types/ISFSensitivity' +import type { ISFProfile } from '../types/Profile' export function isfLookup( isf_profile: ISFProfile, timestamp: Date | undefined, - lastResult: ISFSensitivity | null -): [number, ISFSensitivity | null] { + lastResult: ISFSensitivity.ISFSensitivity | null +): [number, ISFSensitivity.ISFSensitivity | null] { const nowDate = timestamp || new Date() const nowMinutes = nowDate.getHours() * 60 + nowDate.getMinutes() - if (lastResult && nowMinutes >= lastResult.offset && nowMinutes < lastResult.endOffset) { + if (lastResult && nowMinutes >= lastResult.offset && nowMinutes < (lastResult.endOffset || 0)) { return [lastResult.sensitivity, lastResult] } - const isf_data = isf_profile.sensitivities.sort((a, b) => a.offset - b.offset) + const isf_data = sort(ISFSensitivity.Order)(isf_profile.sensitivities) let isfSchedule = isf_data[isf_data.length - 1] diff --git a/lib/profile/targets.ts b/lib/profile/targets.ts index db37df17f..c0918b00b 100644 --- a/lib/profile/targets.ts +++ b/lib/profile/targets.ts @@ -1,32 +1,22 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' import getTime from '../medtronic-clock' -import type { Profile } from '../types/Profile' -import type { TempTarget } from '../types/TempTarget' +import type { Preferences } from '../types/Preferences' interface BgTarget { offset: number low: number high: number - min_bg?: number - max_bg?: number temptargetSet?: boolean } -interface LookupInputs { - targets: { - targets: BgTarget[] - } - temptargets: TempTarget[] -} - -export function bgTargetsLookup(final_result: FinalResult, inputs: LookupInputs, profile: Profile) { - return bound_target_range(lookup(final_result, inputs, profile)) +export function bgTargetsLookup(final_result: FinalResult, inputs: Preferences) { + return bound_target_range(lookup(final_result, inputs)) } -export function lookup(final_result: FinalResult, inputs: LookupInputs, profile: Profile) { - const bgtargets_data = inputs.targets - let temptargets_data = inputs.temptargets +export function lookup(final_result: FinalResult, preferences: Preferences): BgTarget { + const bgtargets_data = preferences.targets + let temptargets_data = preferences.temptargets const now = new Date() //bgtargets_data.targets.sort(function (a, b) { return a.offset > b.offset }); @@ -43,35 +33,33 @@ export function lookup(final_result: FinalResult, inputs: LookupInputs, profile: } } - if (profile.target_bg) { - bgTargets.low = profile.target_bg - } - - bgTargets.high = bgTargets.low + const target_bg = preferences.target_bg || bgTargets.low - let tempTargets = bgTargets + let tempTargets: BgTarget = { + ...bgTargets, + low: target_bg, + high: target_bg, + } + bgTargets = tempTargets - if (!Array.isArray(temptargets_data)) { + if (temptargets_data.length === 0) { console_error(final_result, 'No temptargets found.') return bgTargets - } else { - // sort tempTargets by date so we can process most recent first - temptargets_data = [...temptargets_data].sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ) } - //console.error(temptargets_data); - //console.error(now); + // sort tempTargets by date so we can process most recent first + temptargets_data = [...temptargets_data].sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + for (let i = 0; i < temptargets_data.length; i++) { const start = new Date(temptargets_data[i].created_at) - //console.error(start); const expires = new Date(start.getTime() + temptargets_data[i].duration * 60 * 1000) - //console.error(expires); if (now >= start && temptargets_data[i].duration === 0) { // cancel temp targets - //console.error(temptargets_data[i]); - tempTargets = bgTargets + tempTargets = { + ...bgTargets, + } break } else if (!temptargets_data[i].targetBottom || !temptargets_data[i].targetTop) { console_error( @@ -80,7 +68,12 @@ export function lookup(final_result: FinalResult, inputs: LookupInputs, profile: ) break } else if (now >= start && now < expires) { - //console.error(temptargets_data[i]); + tempTargets = { + ...tempTargets, + high: temptargets_data[i].targetTop, + low: temptargets_data[i].targetBottom, + temptargetSet: true, + } tempTargets.high = temptargets_data[i].targetTop tempTargets.low = temptargets_data[i].targetBottom tempTargets.temptargetSet = true @@ -88,7 +81,6 @@ export function lookup(final_result: FinalResult, inputs: LookupInputs, profile: } } bgTargets = tempTargets - //console.error(bgTargets); return bgTargets } diff --git a/lib/round-basal.ts b/lib/round-basal.ts index b62c95f0c..3a65eb147 100644 --- a/lib/round-basal.ts +++ b/lib/round-basal.ts @@ -10,7 +10,7 @@ const round_basal = (basal: number, profile?: Profile) => { let lowest_rate_scale = 20 // Make sure optional model has been set - if (profile?.model?.endsWith('54') || profile?.model?.endsWith('23')) { + if (profile?.model?.toString().endsWith('54') || profile?.model?.toString().endsWith('23')) { lowest_rate_scale = 40 } diff --git a/lib/types/Autosens.ts b/lib/types/Autosens.ts index 04a2406df..1d3169ed9 100644 --- a/lib/types/Autosens.ts +++ b/lib/types/Autosens.ts @@ -1,13 +1,9 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' -export const Autosens = t.intersection([ - t.type({ - timestamp: t.string, - ratio: t.number, - }), - t.partial({ - newisf: t.number, - }), -]) +export const Autosens = Schema.Struct({ + timestamp: Schema.String, + ratio: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), + newisf: Schema.optional(Schema.Number), +}) -export type Autosens = t.TypeOf +export type Autosens = typeof Autosens.Type diff --git a/lib/types/BasalSchedule.ts b/lib/types/BasalSchedule.ts index 3a108e741..dc71d5b4b 100644 --- a/lib/types/BasalSchedule.ts +++ b/lib/types/BasalSchedule.ts @@ -1,25 +1,20 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' +import * as O from 'effect/Order' import { PositiveInt } from './PositiveInt' -import { PositiveNumber } from './PositiveNumber' import { ScheduleStart } from './ScheduleStart' -export interface BasalSchedule { - i?: t.Int - start: ScheduleStart - minutes: PositiveInt - rate: PositiveNumber -} +export const BasalSchedule = Schema.Struct({ + i: Schema.optionalWith(Schema.Int, { exact: true }), + start: Schema.optional(ScheduleStart), + minutes: PositiveInt, + rate: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), +}) -export const BasalSchedule: t.Type = t.intersection( - [ - t.type({ - start: ScheduleStart, - minutes: PositiveInt, - rate: PositiveNumber, - }), - t.partial({ - i: t.Int, - }), - ], - 'BasalSchedule' -) +export type BasalSchedule = typeof BasalSchedule.Type + +export const Order: O.Order = O.combineAll([ + O.make((a, b) => O.number(Number(a.i), Number(b.i))), + O.struct({ + //start: ScheduleStartOrder, + }), +]) diff --git a/lib/types/CarbRatioSchedule.ts b/lib/types/CarbRatioSchedule.ts new file mode 100644 index 000000000..9fb29824b --- /dev/null +++ b/lib/types/CarbRatioSchedule.ts @@ -0,0 +1,14 @@ +import { Schema } from '@effect/schema' +import * as O from 'effect/Order' +import { ScheduleStart } from './ScheduleStart' + +export const CarbRatioSchedule = Schema.Struct({ + i: Schema.optionalWith(Schema.Int, { exact: true }), + start: Schema.optional(ScheduleStart), + offset: Schema.Number, + ratio: Schema.Number, +}) + +export type CarbRatioSchedule = typeof CarbRatioSchedule.Type + +export const Order: O.Order = O.make((a, b) => O.number(Number(a.i), Number(b.i))) diff --git a/lib/types/CarbRatios.ts b/lib/types/CarbRatios.ts new file mode 100644 index 000000000..5121003c7 --- /dev/null +++ b/lib/types/CarbRatios.ts @@ -0,0 +1,12 @@ +import { Schema } from '@effect/schema' +import { CarbRatioSchedule } from './CarbRatioSchedule' + +export const CarbRatioUnit = Schema.Literal('grams', 'exchanges') +export type CarbRatioUnit = typeof CarbRatioUnit.Type + +export const CarbRatios = Schema.Struct({ + units: CarbRatioUnit, + schedule: Schema.Array(CarbRatioSchedule), +}) + +export type CarbRatios = typeof CarbRatios.Type diff --git a/lib/types/EventType.ts b/lib/types/EventType.ts index 1d1c75bfe..4f5a719b2 100644 --- a/lib/types/EventType.ts +++ b/lib/types/EventType.ts @@ -1,73 +1,31 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' -export type NightscoutEventType = - | 'Temp Basal' - | 'Carb Correction' - | 'Temporary Target' - | 'Insulin Change' - | 'Site Change' - | 'Pump Battery Change' - | 'Announcement' - | 'Sensor Start' - | 'BG Check' - | 'Exercise' - | 'Bolus Wizard' +const NightscoutEventType = Schema.Literal( + 'Temp Basal', + 'Carb Correction', + 'Temporary Target', + 'Insulin Change', + 'Site Change', + 'Pump Battery Change', + 'Announcement', + 'Sensor Start', + 'BG Check', + 'Exercise', + 'Bolus Wizard' +) -export type EventType = - | NightscoutEventType - | 'Bolus' - | 'SMB' - | 'External Insulin' - | 'Meal Bolus' - | 'Correction Bolus' - | 'Snack Bolus' - | 'BolusWizard' - | 'TempBasal' - | 'TempBasalDuration' - | 'PumpSuspend' - | 'PumpResume' - | 'PumpAlarm' - | 'PumpBattery' - | 'Rewind' - | 'Prime' - | 'JournalEntryMealMarker' - | 'SuspendBasal' - | string - -export const NightscoutEventType: t.Type = t.keyof({ - 'Temp Basal': null, - 'Carb Correction': null, - 'Temporary Target': null, - 'Insulin Change': null, - 'Site Change': null, - 'Pump Battery Change': null, - Announcement: null, - 'Sensor Start': null, - 'BG Check': null, - Exercise: null, - 'Bolus Wizard': null, -}) - -export const EventType: t.Type = t.union([ - NightscoutEventType, - t.keyof({ - Bolus: null, - SMB: null, - 'External Insulin': null, - 'Meal Bolus': null, - 'Correction Bolus': null, - 'Snack Bolus': null, - BolusWizard: null, - TempBasal: null, - TempBasalDuration: null, - PumpSuspend: null, - PumpResume: null, - PumpAlarm: null, - PumpBattery: null, - Rewind: null, - Prime: null, - JournalEntryMealMarker: null, - SuspendBasal: null, - }), - t.string, -]) +const PumpHistoryEvent = Schema.Literal( + 'Temp Basal', + 'Carb Correction', + 'Temporary Target', + 'Insulin Change', + 'Site Change', + 'Pump Battery Change', + 'Announcement', + 'Sensor Start', + 'BG Check', + 'Exercise', + 'Bolus Wizard' +) +export const EventType = Schema.Union(NightscoutEventType, PumpHistoryEvent, Schema.String) +export type EventType = typeof EventType.Type diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts index c9b31c475..77e78518d 100644 --- a/lib/types/GlucoseEntry.ts +++ b/lib/types/GlucoseEntry.ts @@ -1,22 +1,46 @@ -export interface GlucoseEntry { - date?: number - display_time?: string - dateString?: string - sgv?: number - glucose?: number - type?: 'sgv' | 'cal' | string - device?: string - noise?: number - xDrip_started_at?: unknown -} +import { Schema } from '@effect/schema' +import { Positive } from '@effect/schema/Schema' +import { identity, String } from 'effect' +import { PositiveInt } from './PositiveInt' + +export const GlucoseType = Schema.Union(Schema.Literal('sgv', 'cal'), Schema.String) +export type GlucoseType = typeof GlucoseType.Type + +const DateFromDisplayTime = Schema.String.pipe( + Schema.transform(Schema.DateFromString, { + strict: true, + decode: String.replace('T', ''), + encode: identity, + }) +) +const DateNumber = Schema.Number.pipe(Schema.filter(s => Schema.is(Schema.DateFromNumber)(s) || 'invalid Date number')) +const DisplayTime = Schema.String.pipe(Schema.filter(s => Schema.is(DateFromDisplayTime)(s) || 'invalid Display Time')) +const DateString = Schema.String.pipe(Schema.filter(s => Schema.is(Schema.DateFromString)(s) || 'invalid Date string')) + +export const GlucoseEntry = Schema.Struct({ + date: Schema.optional(DateNumber), + display_time: Schema.optional(DisplayTime), + dateString: Schema.optional(DateString), + sgv: Schema.optional(PositiveInt), + glucose: Schema.optional(Positive), + type: Schema.optional(GlucoseType), + device: Schema.optional(Schema.String), + noise: Schema.optional(Schema.Number), + xDrip_started_at: Schema.optional(Schema.Unknown), +}).annotations({ + title: 'GlucoseEntry', + description: 'Glucose Entry', +}) + +export type GlucoseEntry = typeof GlucoseEntry.Type -export const getGlucoseEntryDate = (entry: GlucoseEntry): Date | undefined => { +export const getDate = (entry: GlucoseEntry): Date | undefined => { if (entry.date) { - return new Date(entry.date) + return Schema.decodeSync(Schema.DateFromNumber)(entry.date) } else if (entry.dateString) { - return new Date(entry.dateString) + return Schema.decodeSync(Schema.DateFromString)(entry.dateString) } else if (entry.display_time) { - return new Date(entry.display_time.replace('T', ' ')) + return Schema.decodeSync(DateFromDisplayTime)(entry.display_time) } return undefined diff --git a/lib/types/GlucoseUnit.ts b/lib/types/GlucoseUnit.ts index 554820149..0a5c9f335 100644 --- a/lib/types/GlucoseUnit.ts +++ b/lib/types/GlucoseUnit.ts @@ -1,10 +1,8 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' -export type GlucoseUnit = 'mg/dL' | 'mmol/L' -export const GlucoseUnit: t.Type = t.keyof( - { - 'mg/dL': null, - 'mmol/L': null, - }, - 'GlucoseUnit' -) +export const GlucoseUnit = Schema.Literal('mg/dL', 'mmol/L').annotations({ + identifier: 'GlucoseUnit', + title: 'Glucose Unit', +}) + +export type GlucoseUnit = typeof GlucoseUnit.Type diff --git a/lib/types/ISFSensitivity.ts b/lib/types/ISFSensitivity.ts new file mode 100644 index 000000000..32161c19f --- /dev/null +++ b/lib/types/ISFSensitivity.ts @@ -0,0 +1,21 @@ +import { Schema } from '@effect/schema' +import * as O from 'effect/Order' +import { ScheduleStart } from './ScheduleStart' + +export const ISFSensitivity = Schema.Struct({ + offset: Schema.Number, + endOffset: Schema.optional(Schema.Number), + sensitivity: Schema.Number, + i: Schema.optional(Schema.Number), + start: Schema.optional(ScheduleStart), + x: Schema.optional(Schema.Number), +}) + +export type ISFSensitivity = typeof ISFSensitivity.Type + +export const Order: O.Order = O.combineAll([ + O.make((a, b) => O.number(Number(a), Number(b))), + O.struct({ + //start: ScheduleStartOrder, + }), +]) diff --git a/lib/types/InsulineCurve.ts b/lib/types/InsulineCurve.ts index 8a6eabc8e..641b24aea 100644 --- a/lib/types/InsulineCurve.ts +++ b/lib/types/InsulineCurve.ts @@ -1,4 +1,9 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' + +export const InsulineCurve = Schema.Literal('bilinear', 'rapid-acting', 'ultra-rapid').annotations({ + identifier: 'InsulineCurve', + title: 'Insuline Curve', +}) /** * Insulin curve. @@ -7,12 +12,4 @@ import * as t from 'io-ts' * - `rapid-acting`: Humalog * - `bilinear`: old curve */ -export type InsulineCurve = 'bilinear' | 'rapid-acting' | 'ultra-rapid' -export const InsulineCurve: t.Type = t.keyof( - { - bilinear: null, - 'rapid-acting': null, - 'ultra-rapid': null, - }, - 'InsulineCurve' -) +export type InsulineCurve = typeof InsulineCurve.Type diff --git a/lib/types/LocalDateFromDate.ts b/lib/types/LocalDateFromDate.ts deleted file mode 100644 index 3d097e505..000000000 --- a/lib/types/LocalDateFromDate.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { right } from 'fp-ts/Either' -import * as t from 'io-ts' -import { date } from 'io-ts-types/date' -import { tz } from '../date' - -export const LocalDateFromDate = new t.Type( - 'LocalDateFromDate', - a => date.is(a), - i => right(tz(i)), - a => a -) diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts index bb53383fa..3b84be2c3 100644 --- a/lib/types/NightscoutTreatment.ts +++ b/lib/types/NightscoutTreatment.ts @@ -1,64 +1,34 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' import { EventType } from './EventType' import { PumpHistoryEvent } from './PumpHistoryEvent' -export interface NightscoutTreatment { - eventType: EventType - created_at: string - id?: string - duration?: number - rawDuration?: PumpHistoryEvent - rawRate?: PumpHistoryEvent - absolute?: number - rate?: number - enteredBy?: string - bolus?: PumpHistoryEvent - insulin?: number | null - notes?: string - carbs?: number | null - fat?: number - protein?: number - foodType?: string - targetTop?: number - targetBottom?: number - glucoseType?: string - glucose?: number - units?: string - fpuID?: string - // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running - amount?: number -} +export const NightscoutTreatment = Schema.Struct({ + eventType: EventType, + created_at: Schema.String, + id: Schema.optional(Schema.String), + duration: Schema.optional(Schema.Number), + rawDuration: Schema.optional(PumpHistoryEvent), + rawRate: Schema.optional(PumpHistoryEvent), + absolute: Schema.optional(Schema.Number), + rate: Schema.optional(Schema.Number), + enteredBy: Schema.optional(Schema.String), + bolus: Schema.optional(PumpHistoryEvent), + insulin: Schema.optionalWith(Schema.Number, { nullable: true }), + notes: Schema.optional(Schema.String), + carbs: Schema.optionalWith(Schema.Number, { nullable: true }), + fat: Schema.optional(Schema.Number), + protein: Schema.optional(Schema.Number), + foodType: Schema.optional(Schema.String), + targetTop: Schema.optional(Schema.Number), + targetBottom: Schema.optional(Schema.Number), + glucoseType: Schema.optional(Schema.String), + glucose: Schema.optional(Schema.Number), + units: Schema.optional(Schema.String), + fpuID: Schema.optional(Schema.String), + amount: Schema.optional(Schema.Number), +}).annotations({ + identifier: 'NightscoutTreatment', + title: 'Nightscout Treatment', +}) -export const NightscoutTreatment: t.Type = t.intersection( - [ - t.type({ - eventType: EventType, - created_at: t.string, - }), - t.partial({ - id: t.string, - duration: t.number, - rawDuration: PumpHistoryEvent, - rawRate: PumpHistoryEvent, - absolute: t.number, - rate: t.number, - enteredBy: t.string, - bolus: PumpHistoryEvent, - insulin: t.union([t.number, t.null]), - notes: t.string, - carbs: t.union([t.number, t.null]), - fat: t.number, - protein: t.number, - foodType: t.string, - targetTop: t.number, - targetBottom: t.number, - glucoseType: t.union([t.literal('Finger'), t.string]), - glucose: t.number, - units: t.string, - fpuID: t.string, - // Loop Temp Basal: Loop reports the amount of insulin actually delivered while the temp basal was running - amount: t.number, - }), - ], - 'NightscoutTreatment' -) +export type NightscoutTreatment = typeof NightscoutTreatment.Type diff --git a/lib/types/PositiveInt.ts b/lib/types/PositiveInt.ts index 923eb9208..e96894f55 100644 --- a/lib/types/PositiveInt.ts +++ b/lib/types/PositiveInt.ts @@ -1,9 +1,9 @@ -import * as t from 'io-ts' -import type { PositiveNumberBrand } from './PositiveNumber' +import { Schema } from '@effect/schema' +import { PositiveNumber } from './PositiveNumber' -export interface PositiveIntBrand extends PositiveNumberBrand { - readonly PositiveInt: unique symbol -} - -export type PositiveInt = t.Branded -export const PositiveInt = t.brand(t.Int, (n): n is PositiveInt => n >= 0, 'PositiveInt') +export const PositiveIntBrand = Symbol.for('PositiveInt') +export const PositiveInt = PositiveNumber.pipe(Schema.brand(PositiveIntBrand)).annotations({ + identifier: 'PositiveInt', + title: 'PositiveInt', +}) +export type PositiveInt = typeof PositiveInt.Type diff --git a/lib/types/PositiveNumber.ts b/lib/types/PositiveNumber.ts index bf7144831..3cb688d5d 100644 --- a/lib/types/PositiveNumber.ts +++ b/lib/types/PositiveNumber.ts @@ -1,8 +1,10 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' -export interface PositiveNumberBrand { - readonly PositiveNumber: unique symbol -} - -export type PositiveNumber = t.Branded -export const PositiveNumber = t.brand(t.number, (n): n is PositiveNumber => n >= 0, 'PositiveNumber') +export const PositiveNumberBrand = Symbol.for('PositiveNumber') +export const PositiveNumber = Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)) + .pipe(Schema.brand(PositiveNumberBrand)) + .annotations({ + identifier: 'PositiveInt', + title: 'PositiveInt', + }) +export type PositiveNumber = typeof PositiveNumber.Type diff --git a/lib/types/Preferences.ts b/lib/types/Preferences.ts index 257460f34..167e3bf10 100644 --- a/lib/types/Preferences.ts +++ b/lib/types/Preferences.ts @@ -1,36 +1,208 @@ -import type { NonEmptyArray } from 'fp-ts/NonEmptyArray' -import * as t from 'io-ts' -import { fromNullable } from 'io-ts-types/fromNullable' -import { nonEmptyArray } from 'io-ts-types/nonEmptyArray' +import { Schema } from '@effect/schema' + +import { constant } from 'effect/Function' import { BasalSchedule } from './BasalSchedule' +import { CarbRatios } from './CarbRatios' import { GlucoseUnit } from './GlucoseUnit' import { InsulineCurve } from './InsulineCurve' -import { PositiveInt } from './PositiveInt' -import { PositiveNumber } from './PositiveNumber' +import { ISFProfile } from './Profile' +import { ScheduleStart } from './ScheduleStart' +import { TempTarget } from './TempTarget' -interface PumpSettings { - insulin_action_curve?: PositiveNumber -} +const PumpSettings = Schema.Struct({ + // maxBolus: Schema.Number, + maxBasal: Schema.optional(Schema.Number), + insulin_action_curve: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThan(1)), { + default: constant(2), + }), +}).annotations({ + identifier: 'PumpSettings', + title: 'Pump Settings', +}) -const PumpSettings = t.partial({ - insulin_action_curve: t.refinement(PositiveNumber, a => a > 1, 'PositiveNumber(> 1)'), +type PumpSettings = typeof PumpSettings.Type + +const BGTarget = Schema.Struct({ + offset: Schema.Number, + start: Schema.optionalWith(ScheduleStart, { exact: true }), + low: Schema.Number, + high: Schema.Number, }) -interface Targets { - user_preferred_units: GlucoseUnit -} +export type BgTarget = typeof BGTarget.Type + +const Targets = Schema.Struct({ + user_preferred_units: Schema.optional(GlucoseUnit), + targets: Schema.NonEmptyArray(BGTarget), +}) + +type Targets = typeof Targets.Type + +const Basals = Schema.NonEmptyArray(BasalSchedule) +type Basals = typeof Basals.Type -const Targets = t.type({ - user_preferred_units: GlucoseUnit, +export const Preferences = Schema.Struct({ + max_iob: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 0, + }), + max_daily_safety_multiplier: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(3), + }), + current_basal_safety_multiplier: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(4), + }), + autosens_max: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(1.2), + }), + autosens_min: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(0.7), + }), + rewind_resets_autosens: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => true }), + high_temptarget_raises_sensitivity: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => false, + }), + low_temptarget_lowers_sensitivity: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => false, + }), + sensitivity_raises_target: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => true, + }), + resistance_lowers_target: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => false, + }), + exercise_mode: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + half_basal_exercise_target: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 160, + }), + maxCOB: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 120, + }), + skip_neutral_temps: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + unsuspend_if_no_temp: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + bolussnooze_dia_divisor: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 2, + }), + min_5m_carbimpact: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 8, + }), + autotune_isf_adjustmentFraction: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(1.0), + }), + remainingCarbsFraction: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(1.0), + }), + remainingCarbsCap: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 90, + }), + enableUAM: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => true }), + A52_risk_enable: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + enableSMB_with_COB: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + enableSMB_with_temptarget: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => false, + }), + enableSMB_always: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + enableSMB_after_carbs: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + enableSMB_high_bg: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + enableSMB_high_bg_target: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 110, + }), + allowSMB_with_high_temptarget: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: () => false, + }), + maxSMBBasalMinutes: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 30, + }), + maxUAMSMBBasalMinutes: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 30, + }), + SMBInterval: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 3, + }), + bolus_increment: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(0.1), + }), + maxDelta_bg_threshold: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(0.2), + }), + curve: Schema.optionalWith(InsulineCurve, { nullable: true, default: () => 'rapid-acting' as const }), + useCustomPeakTime: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + insulinPeakTime: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 75, + }), + carbsReqThreshold: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 1, + }), + offline_hotspot: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + noisyCGMTargetMultiplier: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: constant(1.3), + }), + suspend_zeros_iob: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => true }), + enableEnliteBgproxy: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + calc_glucose_noise: Schema.optionalWith(Schema.Boolean, { nullable: true, default: () => false }), + target_bg: Schema.optionalWith( + Schema.Union(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), Schema.Literal(false)), + { + nullable: true, + default: () => false as const, + } + ), + edison_battery_shutdown_voltage: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 3050, + }), + pi_battery_shutdown_percent: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 2, + }), + isf: ISFProfile, + // no defaults + basals: Basals, + settings: PumpSettings, + targets: Targets, + temptargets: Schema.Array(TempTarget), + // @todo: can we make it just a string? + model: Schema.optionalWith(Schema.Union(Schema.NonEmptyString, Schema.Int), { nullable: true }), + carbratio: Schema.optionalWith(CarbRatios, { strict: true, nullable: true }), +}).annotations({ + identifier: 'Preferences', + title: 'Preferences', }) -export interface Preferences { - [k: string]: unknown - max_iob: PositiveInt - max_daily_safety_multiplier: PositiveNumber - current_basal_safety_multiplier: PositiveNumber - autosens_max: PositiveNumber - autosens_min: PositiveNumber +//export interface Preferences extends Schema.Schema.Type {} + +export interface Preferences extends Schema.Schema.Type { + max_iob: number + max_daily_safety_multiplier: number + current_basal_safety_multiplier: number + autosens_max: number + autosens_min: number rewind_resets_autosens: boolean /** raise sensitivity for temptargets >= 101. synonym for exercise_mode */ high_temptarget_raises_sensitivity: boolean @@ -42,7 +214,7 @@ export interface Preferences { resistance_lowers_target: boolean /** when true, > 100 mg/dL high temp target adjusts sensitivityRatio for exercise_mode. This majorly changes the behavior of high temp targets from before. synonmym for high_temptarget_raises_sensitivity */ exercise_mode: boolean - half_basal_exercise_target: PositiveInt + half_basal_exercise_target: number /** * Max carbs absorbed in 4 hours. * @@ -51,21 +223,21 @@ export interface Preferences { * If someone enters more carbs or stacks more; OpenAPS will just truncate dosing based on 120. * Essentially, this just limits AMA/SMB as a safety cap against excessive COB entry. */ - maxCOB: PositiveInt + maxCOB: number /** if true, don't set neutral temps */ skip_neutral_temps: boolean /** if true, pump will un-suspend after a zero temp finishes */ unsuspend_if_no_temp: boolean /** bolus snooze decays after 1/2 of DIA */ - bolussnooze_dia_divisor: PositiveInt + bolussnooze_dia_divisor: number /** mg/dL per 5m (8 mg/dL/5m corresponds to 24g/hr at a CSF of 4 mg/dL/g (x/5*60/4)) */ - min_5m_carbimpact: PositiveInt + min_5m_carbimpact: number /** keep autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF. 1.0 allows full adjustment, 0 is no adjustment from pump ISF. */ - autotune_isf_adjustmentFraction: PositiveNumber + autotune_isf_adjustmentFraction: number /** fraction of carbs we'll assume will absorb over 4h if we don't yet see carb absorption */ - remainingCarbsFraction: PositiveNumber + remainingCarbsFraction: number /** max carbs we'll assume will absorb over 4h if we don't yet see carb absorption */ - remainingCarbsCap: PositiveInt + remainingCarbsCap: number /** Enable detection of unannounced meal carb absorption */ enableUAM: boolean A52_risk_enable: boolean @@ -97,19 +269,19 @@ export interface Preferences { /** Enable SMBs when a high BG is detected, based on the high BG target (adjusted or profile) */ enableSMB_high_bg: boolean /** Set the value enableSMB_high_bg will compare against to enable SMB. If BG > than this value, SMBs should enable. */ - enableSMB_high_bg_target: PositiveInt + enableSMB_high_bg_target: number /** Allow supermicrobolus (if otherwise enabled) even with high temp targets */ allowSMB_with_high_temptarget: boolean /** maximum minutes of basal that can be delivered as a single SMB with uncovered COB */ - maxSMBBasalMinutes: PositiveInt + maxSMBBasalMinutes: number /** maximum minutes of basal that can be delivered as a single SMB when IOB exceeds COB */ - maxUAMSMBBasalMinutes: PositiveInt + maxUAMSMBBasalMinutes: number /** minimum interval between SMBs, in minutes. */ - SMBInterval: PositiveInt + SMBInterval: number /** minimum bolus that can be delivered as an SMB */ - bolus_increment: PositiveNumber + bolus_increment: number /** maximum change in bg to use SMB, above that will disable SMB */ - maxDelta_bg_threshold: PositiveNumber + maxDelta_bg_threshold: number /** Insulin curve. */ curve: InsulineCurve /** allows changing insulinPeakTime */ @@ -118,13 +290,13 @@ export interface Preferences { * Number of minutes after a bolus activity peaks. * Defaults to 55m for Fiasp if useCustomPeakTime: boolean */ - insulinPeakTime: PositiveInt + insulinPeakTime: number /** grams of carbsReq to trigger a pushover */ - carbsReqThreshold: PositiveInt + carbsReqThreshold: number /** enabled an offline-only local wifi hotspot if no Internet available */ offline_hotspot: boolean /** increase target by this amount when looping off raw/noisy CGM data */ - noisyCGMTargetMultiplier: PositiveNumber + noisyCGMTargetMultiplier: number /** recognize pump suspends as non insulin delivery events */ suspend_zeros_iob: boolean /** @@ -137,81 +309,9 @@ export interface Preferences { enableEnliteBgproxy: boolean calc_glucose_noise: boolean /** set to an integer value in mg/dL to override pump min_bg */ - target_bg: boolean - edison_battery_shutdown_voltage: PositiveInt - pi_battery_shutdown_percent: PositiveInt - // no defaults - model?: string - settings: PumpSettings - basals: NonEmptyArray - targets: Targets + target_bg: number | false } -/** - * @todo: should we parse numbers from strings? - */ -export const Preferences: t.Type = t.intersection( - [ - t.record(t.string, t.unknown), - t.type({ - max_iob: fromNullable(PositiveInt, 0 as PositiveInt), - max_daily_safety_multiplier: fromNullable(PositiveNumber, 3 as PositiveNumber), - current_basal_safety_multiplier: fromNullable(PositiveNumber, 3 as PositiveNumber), - autosens_max: fromNullable(PositiveNumber, 1.2 as PositiveNumber), - autosens_min: fromNullable(PositiveNumber, 0.7 as PositiveNumber), - rewind_resets_autosens: fromNullable(t.boolean, true), - high_temptarget_raises_sensitivity: fromNullable(t.boolean, false), - low_temptarget_lowers_sensitivity: fromNullable(t.boolean, false), - sensitivity_raises_target: fromNullable(t.boolean, true), - resistance_lowers_target: fromNullable(t.boolean, false), - exercise_mode: fromNullable(t.boolean, false), - half_basal_exercise_target: fromNullable(PositiveInt, 160 as PositiveInt), - maxCOB: fromNullable(PositiveInt, 120 as PositiveInt), - skip_neutral_temps: fromNullable(t.boolean, false), - unsuspend_if_no_temp: fromNullable(t.boolean, false), - bolussnooze_dia_divisor: fromNullable(PositiveInt, 2 as PositiveInt), - min_5m_carbimpact: fromNullable(PositiveInt, 8 as PositiveInt), - autotune_isf_adjustmentFraction: fromNullable(PositiveNumber, 1.0 as PositiveNumber), - remainingCarbsFraction: fromNullable(PositiveNumber, 1.0 as PositiveNumber), - remainingCarbsCap: fromNullable(PositiveInt, 90 as PositiveInt), - enableUAM: fromNullable(t.boolean, true), - A52_risk_enable: fromNullable(t.boolean, false), - enableSMB_with_COB: fromNullable(t.boolean, false), - enableSMB_with_temptarget: fromNullable(t.boolean, false), - enableSMB_always: fromNullable(t.boolean, false), - enableSMB_after_carbs: fromNullable(t.boolean, false), - enableSMB_high_bg: fromNullable(t.boolean, false), - enableSMB_high_bg_target: fromNullable(PositiveInt, 110 as PositiveInt), - allowSMB_with_high_temptarget: fromNullable(t.boolean, false), - maxSMBBasalMinutes: fromNullable(PositiveInt, 30 as PositiveInt), - maxUAMSMBBasalMinutes: fromNullable(PositiveInt, 30 as PositiveInt), - SMBInterval: fromNullable(PositiveInt, 3 as PositiveInt), - bolus_increment: fromNullable(PositiveNumber, 0.1 as PositiveNumber), - maxDelta_bg_threshold: fromNullable(PositiveNumber, 0.2 as PositiveNumber), - curve: fromNullable(InsulineCurve, 'rapid-acting'), - useCustomPeakTime: fromNullable(t.boolean, false), - insulinPeakTime: fromNullable(PositiveInt, 75 as PositiveInt), - carbsReqThreshold: fromNullable(PositiveInt, 1 as PositiveInt), - offline_hotspot: fromNullable(t.boolean, false), - noisyCGMTargetMultiplier: fromNullable(PositiveNumber, 1.3 as PositiveNumber), - suspend_zeros_iob: fromNullable(t.boolean, true), - enableEnliteBgproxy: fromNullable(t.boolean, false), - calc_glucose_noise: fromNullable(t.boolean, false), - target_bg: fromNullable(t.boolean, false), - edison_battery_shutdown_voltage: fromNullable(PositiveInt, 3050 as PositiveInt), - pi_battery_shutdown_percent: fromNullable(PositiveInt, 2 as PositiveInt), - // no defaults - basals: nonEmptyArray(BasalSchedule), - settings: PumpSettings, - targets: Targets, - }), - t.partial({ - model: t.string, - }), - ], - 'Preferences' -) - /** * { "max_iob": 14, diff --git a/lib/types/Profile.ts b/lib/types/Profile.ts index 966c4e9e7..ac2bdd686 100644 --- a/lib/types/Profile.ts +++ b/lib/types/Profile.ts @@ -1,326 +1,169 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' +import { constant } from 'effect/Function' import { BasalSchedule } from './BasalSchedule' +import { CarbRatios } from './CarbRatios' import { GlucoseUnit } from './GlucoseUnit' +import { ISFSensitivity } from './ISFSensitivity' import { InsulineCurve } from './InsulineCurve' -/** - * { - "max_iob": 14, - "max_daily_safety_multiplier": 3, - "current_basal_safety_multiplier": 4, - "autosens_max": 1.3, - "autosens_min": 0.7, - "rewind_resets_autosens": true, - "high_temptarget_raises_sensitivity": true, - "low_temptarget_lowers_sensitivity": true, - "sensitivity_raises_target": true, - "resistance_lowers_target": false, - "exercise_mode": false, - "half_basal_exercise_target": 160, - "maxCOB": 120, - "skip_neutral_temps": false, - "unsuspend_if_no_temp": false, - "min_5m_carbimpact": 8, - "autotune_isf_adjustmentFraction": 1, - "remainingCarbsFraction": 1, - "remainingCarbsCap": 90, - "enableUAM": true, - "A52_risk_enable": false, - "enableSMB_with_COB": true, - "enableSMB_with_temptarget": true, - "enableSMB_always": false, - "enableSMB_after_carbs": true, - "allowSMB_with_high_temptarget": false, - "maxSMBBasalMinutes": 90, - "maxUAMSMBBasalMinutes": 120, - "SMBInterval": 3, - "bolus_increment": 0.05, - "maxDelta_bg_threshold": 0.2, - "curve": "ultra-rapid", - "useCustomPeakTime": false, - "insulinPeakTime": 55, - "carbsReqThreshold": 1, - "offline_hotspot": false, - "noisyCGMTargetMultiplier": 1.3, - "suspend_zeros_iob": false, - "enableEnliteBgproxy": false, - "calc_glucose_noise": false, - "target_bg": false, - "smb_delivery_ratio": 0.7, - "adjustmentFactor": 0.7, - "useNewFormula": true, - "enableDynamicCR": true, - "sigmoid": true, - "weightPercentage": 0.65, - "tddAdjBasal": true, - "enableSMB_high_bg": true, - "enableSMB_high_bg_target": 110, - "threshold_setting": 65, - "dia": 9, - "model": "722", - "current_basal": 0.7, - "basalprofile": [ - { - "rate": 0.7, - "start": "00:00:00", - "minutes": 0 - }, - { - "rate": 0.7, - "minutes": 180, - "start": "03:00:00" - }, - { - "start": "09:00:00", - "minutes": 540, - "rate": 0.7 - } - ], - "max_daily_basal": 0.7, - "max_basal": 3, - "out_units": "mg/dL", - "min_bg": 98, - "max_bg": 98, - "bg_targets": { - "units": "mg/dL", - "user_preferred_units": "mg/dL", - "targets": [ - { - "start": "00:00:00", - "high": 98, - "offset": 0, - "low": 98, - "max_bg": 98, - "min_bg": 98 - } - ] - }, - "sens": 65, - "isfProfile": { - "user_preferred_units": "mg/dL", - "units": "mg/dL", - "sensitivities": [ - { - "start": "00:00:00", - "sensitivity": 65, - "offset": 0, - "endOffset": 1440 - } - ] - }, - "carb_ratio": 6.5, - "carb_ratios": { - "units": "grams", - "schedule": [ - { - "start": "00:00:00", - "offset": 0, - "ratio": 6 - }, - { - "ratio": 5, - "start": "08:30:00", - "offset": 510 - }, - { - "start": "11:30:00", - "ratio": 6, - "offset": 690 - }, - { - "offset": 960, - "ratio": 6.5, - "start": "16:00:00" - } - ] - } -} - */ - -export interface CarbRatioSchedule { - start: string - offset: number - ratio: number -} +export const ISFProfile = Schema.Struct({ + sensitivities: Schema.Array(ISFSensitivity), + units: Schema.optional(Schema.String), + user_preferred_units: Schema.optional(Schema.String), +}) -export const CarbRatioSchedule: t.Type = t.type( - { - start: t.string, - offset: t.number, - ratio: t.number, - }, - 'CarbRatioSchedule' -) +export type ISFProfile = typeof ISFProfile.Type -export interface CarbRatios { - units: string - schedule: Array -} +export const ProfileDefaults = Schema.Struct({ + max_iob: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 0, + }), + max_daily_safety_multiplier: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(3), + }), + current_basal_safety_multiplier: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(4), + }), + autosens_max: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 1.2, + }), + autosens_min: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 0.7, + }), + rewind_resets_autosens: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(true), + }), + high_temptarget_raises_sensitivity: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + low_temptarget_lowers_sensitivity: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + sensitivity_raises_target: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(true), + }), + resistance_lowers_target: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + exercise_mode: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + half_basal_exercise_target: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(160), + }), + maxCOB: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(120) }), + skip_neutral_temps: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + unsuspend_if_no_temp: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + bolussnooze_dia_divisor: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThan(0)), { + nullable: true, + default: constant(2), + }), + min_5m_carbimpact: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 8, + }), + autotune_isf_adjustmentFraction: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 1.0, + }), + remainingCarbsFraction: Schema.optionalWith(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 1.0, + }), + remainingCarbsCap: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { + nullable: true, + default: () => 90, + }), + enableUAM: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(true) }), + A52_risk_enable: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + enableSMB_with_COB: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + enableSMB_with_temptarget: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), -export const CarbRatios: t.Type = t.type( - { - units: t.string, - schedule: t.array(CarbRatioSchedule), - }, - 'CarbRatios' -) - -export interface ISFSensitivity { - i?: number - offset: number - endOffset: number - sensitivity: number - start?: string - x?: number -} - -export const ISFSensitivity: t.Type = t.intersection( - [ - t.type({ - offset: t.number, - endOffset: t.number, - sensitivity: t.number, - }), - t.partial({ - i: t.number, - start: t.string, - x: t.number, - }), - ], - 'ISFSensitivity' -) - -export interface ISFProfile { - sensitivities: Array - units?: string - user_preferred_units?: string -} + enableSMB_always: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + enableSMB_after_carbs: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + enableSMB_high_bg: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + enableSMB_high_bg_target: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(110), + }), + allowSMB_with_high_temptarget: Schema.optionalWith(Schema.Boolean, { + nullable: true, + default: constant(false), + }), + maxSMBBasalMinutes: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(30) }), + maxUAMSMBBasalMinutes: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(30) }), + SMBInterval: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(3) }), + bolus_increment: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(0.1) }), + maxDelta_bg_threshold: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(0.2) }), + curve: Schema.optionalWith(InsulineCurve, { + nullable: true, + default: constant('rapid-acting' as const), + }), + useCustomPeakTime: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + insulinPeakTime: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(75) }), + carbsReqThreshold: Schema.optionalWith(Schema.Number, { nullable: true, default: constant(1) }), + offline_hotspot: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + noisyCGMTargetMultiplier: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(1.3), + }), + suspend_zeros_iob: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(true) }), + enableEnliteBgproxy: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + calc_glucose_noise: Schema.optionalWith(Schema.Boolean, { nullable: true, default: constant(false) }), + target_bg: Schema.optionalWith( + Schema.Union(Schema.Literal(false), Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + { + nullable: true, + default: constant(false as const), + } + ), + edison_battery_shutdown_voltage: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(3050), + }), + pi_battery_shutdown_percent: Schema.optionalWith(Schema.Number, { + nullable: true, + default: constant(2), + }), + model: Schema.optionalWith(Schema.Union(Schema.NonEmptyString, Schema.Int), { nullable: true }), +}) -export const ISFProfile: t.Type = t.intersection( - [ - t.type({ - sensitivities: t.array(ISFSensitivity), - }), - t.partial({ - units: t.string, - user_preferred_units: t.string, - }), - ], - 'ISFProfile' -) +export interface ProfileDefaults extends Schema.Schema.Type {} -export interface Profile { - basalprofile: Array - sens: number - carb_ratio: number - min_5m_carbimpact: number - out_units?: GlucoseUnit - max_daily_safety_multiplier?: number - current_basal_safety_multiplier?: number - model?: string - curve?: InsulineCurve - dia?: number - useCustomPeakTime?: boolean - insulinPeakTime?: number - remainingCarbsCap?: number - remainingCarbsFraction?: number - maxCOB?: number - max_iob?: number - min_bg?: number - max_bg?: number - target_bg?: number - A52_risk_enable?: boolean - noisyCGMTargetMultiplier?: number - maxRaw?: number - low_temptarget_lowers_sensitivity?: boolean - high_temptarget_raises_sensitivity?: boolean - sensitivity_raises_target?: boolean - resistance_lowers_target?: boolean - autosens_max?: number - allowSMB_with_high_temptarget?: boolean - enableSMB_high_bg_target?: number - enableSMB_with_temptarget?: boolean - enableSMB_after_carbs?: boolean - enableSMB_with_COB?: boolean - enableSMB_high_bg?: boolean - enableSMB_always?: boolean - enableUAM?: boolean - suspend_zeros_iob?: boolean - current_basal?: number - half_basal_exercise_target?: number - exercise_mode?: boolean - temptargetSet?: unknown - max_daily_basal?: number - max_basal?: number - maxDelta_bg_threshold?: number - bg_targets?: unknown - isfProfile?: ISFProfile - carb_ratios?: CarbRatios - carbsReqThreshold?: number - skip_neutral_temps?: boolean - maxSMBBasalMinutes?: number - maxUAMSMBBasalMinutes?: number - bolus_increment?: number - SMBInterval?: number -} +export const Profile = Schema.Struct({ + ...ProfileDefaults.fields, + basalprofile: Schema.Array(BasalSchedule), + sens: Schema.Number.pipe(Schema.greaterThanOrEqualTo(5)), + carb_ratio: Schema.optional(Schema.Number), + carb_ratios: Schema.optional(CarbRatios), + out_units: Schema.optional(GlucoseUnit), + dia: Schema.Number.pipe(Schema.greaterThan(1)), + current_basal: Schema.Number.pipe(Schema.greaterThan(0)), + max_daily_basal: Schema.Number.pipe(Schema.greaterThan(0)), + max_basal: Schema.optional(Schema.Number.pipe(Schema.greaterThanOrEqualTo(0.1))), + min_bg: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), + max_bg: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), + temptargetSet: Schema.optionalWith(Schema.Boolean, { nullable: true }), + bg_targets: Schema.optionalWith(Schema.Unknown, { nullable: true }), + isfProfile: Schema.optionalWith(ISFProfile, { nullable: true }), +}) -export const Profile: t.Type = t.intersection( - [ - t.type({ - basalprofile: t.array(BasalSchedule), - sens: t.number, - carb_ratio: t.number, - min_5m_carbimpact: t.number, - }), - t.partial({ - out_units: GlucoseUnit, - max_daily_safety_multiplier: t.number, - current_basal_safety_multiplier: t.number, - model: t.string, - curve: InsulineCurve, - dia: t.number, - useCustomPeakTime: t.boolean, - insulinPeakTime: t.number, - remainingCarbsCap: t.number, - remainingCarbsFraction: t.number, - maxCOB: t.number, - max_iob: t.number, - min_bg: t.number, - max_bg: t.number, - A52_risk_enable: t.boolean, - noisyCGMTargetMultiplier: t.number, - maxRaw: t.number, - low_temptarget_lowers_sensitivity: t.boolean, - high_temptarget_raises_sensitivity: t.boolean, - sensitivity_raises_target: t.boolean, - resistance_lowers_target: t.boolean, - autosens_max: t.number, - allowSMB_with_high_temptarget: t.boolean, - enableSMB_high_bg_target: t.number, - enableSMB_with_temptarget: t.boolean, - enableSMB_after_carbs: t.boolean, - enableSMB_with_COB: t.boolean, - enableSMB_high_bg: t.boolean, - enableSMB_always: t.boolean, - enableUAM: t.boolean, - suspend_zeros_iob: t.boolean, - current_basal: t.number, - half_basal_exercise_target: t.number, - exercise_mode: t.boolean, - temptargetSet: t.unknown, - max_daily_basal: t.number, - max_basal: t.number, - maxDelta_bg_threshold: t.number, - bg_targets: t.unknown, - isfProfile: ISFProfile, - carb_ratios: CarbRatios, - carbsReqThreshold: t.number, - skip_neutral_temps: t.boolean, - maxSMBBasalMinutes: t.number, - maxUAMSMBBasalMinutes: t.number, - bolus_increment: t.number, - SMBInterval: t.number, - }), - ], - 'Profile' -) +export interface Profile extends Schema.Schema.Type {} diff --git a/lib/types/PumpHistoryEvent.ts b/lib/types/PumpHistoryEvent.ts index 5ee2f06b5..941bacc53 100644 --- a/lib/types/PumpHistoryEvent.ts +++ b/lib/types/PumpHistoryEvent.ts @@ -1,51 +1,23 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' import { EventType } from './EventType' -export const TempType = t.keyof({ - absolute: null, - percent: null, -}) +export const TempType = Schema.Literal('absolute', 'percent') -export type TempType = t.TypeOf +export type TempType = typeof TempType.Type -const PumpEventBase = t.type({ +export const PumpHistoryEvent = Schema.Struct({ _type: EventType, - timestamp: t.string, + timestamp: Schema.String, + id: Schema.optional(Schema.String), + amount: Schema.optional(Schema.Number), + duration: Schema.optional(Schema.Number), + 'duration (min)': Schema.optional(Schema.Number), + rate: Schema.optional(Schema.Number), + temp: Schema.optional(TempType), + carb_input: Schema.optional(Schema.Number), + note: Schema.optional(Schema.String), + isSMB: Schema.optional(Schema.Boolean), + isExternal: Schema.optional(Schema.Boolean), }) -export const PumpHistoryEvent = t.intersection([ - PumpEventBase, - t.partial({ - id: t.string, - amount: t.number, - duration: t.number, - 'duration (min)': t.number, - rate: t.number, - temp: TempType, - carb_input: t.number, - note: t.string, - isSMB: t.boolean, - isExternal: t.boolean, - // @todo: check: used in iob/history - //date: t.number, - }), -]) - -export interface PumpHistoryEvent { - _type: EventType - timestamp: string - id?: string - amount?: number - duration?: number - 'duration (min)'?: number - rate?: number - temp?: TempType - carb_input?: number - note?: string - isSMB?: boolean - isExternal?: boolean - // @todo: check: used in iob/history - //date?: number, -} - -export type PumpHistoryEvent2 = t.TypeOf +export type PumpHistoryEvent = typeof PumpHistoryEvent.Type diff --git a/lib/types/ScheduleStart.ts b/lib/types/ScheduleStart.ts index e54e1ce60..86e51de45 100644 --- a/lib/types/ScheduleStart.ts +++ b/lib/types/ScheduleStart.ts @@ -1,7 +1,18 @@ -import * as t from 'io-ts' +import { Schema } from '@effect/schema' +import * as O from 'effect/Order' + +export const ScheduleStartBrand = Symbol.for('ScheduleStart') + +const pattern = '^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$' +export const ScheduleStart = Schema.String.pipe(Schema.pattern(new RegExp(pattern))) + .pipe(Schema.brand(ScheduleStartBrand)) + .annotations({ + description: 'Time in HH:MM format', + }) -export const ScheduleStart = t.refinement(t.string, s => /^([01][0-9]|2[0-3]):([0-5][0-9])$/.test(s), 'ScheduleStart') /** * Time in HH:MM */ -export type ScheduleStart = t.TypeOf +export type ScheduleStart = typeof ScheduleStart.Type + +export const Order: O.Order = O.string diff --git a/lib/types/TempTarget.ts b/lib/types/TempTarget.ts index 33d86dfae..c54c60e57 100644 --- a/lib/types/TempTarget.ts +++ b/lib/types/TempTarget.ts @@ -1,6 +1,15 @@ -export interface TempTarget { - created_at: string - duration: number - targetTop: number - targetBottom: number -} +import { Schema } from '@effect/schema' + +export const TempTarget = Schema.Struct({ + created_at: Schema.Union(Schema.DateFromSelf, Schema.String.pipe(Schema.nonEmptyString())), + duration: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), + targetTop: Schema.Int, + targetBottom: Schema.Int, +}).annotations({ + title: 'TempTarget', + description: 'Temp Target', +}) + +export type TempTarget = typeof TempTarget.Type + +export const decode = Schema.decodeUnknownSync(TempTarget) diff --git a/lib/types/renameKey.ts b/lib/types/renameKey.ts deleted file mode 100644 index 993d1db32..000000000 --- a/lib/types/renameKey.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { map, chain } from 'fp-ts/Either' -import { flow, pipe } from 'fp-ts/function' -import * as t from 'io-ts' - -export const renameKey = - (from: KA, to: KO) => - (a: A) => - new t.Type, Record>( - a.name, - a.is, - (i, c) => - pipe( - t.record(t.string, t.unknown).validate(i, c), - map(b => { - if (Object.prototype.hasOwnProperty.call(b, from)) { - b[to] = b[from] - delete b[from] - } - return b - }), - chain(b => a.validate(b, c)) - ), - flow(a.encode, b => { - if (Object.prototype.hasOwnProperty.call(b, to)) { - b[from] = b[to] - delete b[to] - } - return b - }) - ) diff --git a/package.json b/package.json index 1dea2b2ad..7a71030f6 100644 --- a/package.json +++ b/package.json @@ -101,9 +101,10 @@ }, "homepage": "https://github.com/openaps/oref0", "dependencies": { - "fp-ts": "^2.16.9", - "io-ts": "^2.2.21", - "io-ts-types": "^0.5.19", + "@effect/data": "^0.18.7", + "@effect/io": "^0.41.2", + "@effect/schema": "^0.70.3", + "effect": "^3.6.2", "json": "^9.0.6", "json-stable-stringify": "^1.0.1", "lodash": "^4.17.15", diff --git a/tests/command-behavior.tests.sh b/tests/command-behavior.tests.sh index b14cd3da7..6af334c91 100755 --- a/tests/command-behavior.tests.sh +++ b/tests/command-behavior.tests.sh @@ -26,7 +26,7 @@ main () { test-autotune-core - test-autotune-prep + #test-autotune-prep test-calculate-iob @@ -126,7 +126,7 @@ test-autotune-prep () { ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) ERROR_LINES=$( cat stderr_output ) - + [[ $(cat stderr_output | grep mealCOB | wc -l) -eq 82 ]] || fail_test "oref0-autotune-prep didn't contain expected stderr output" # Make sure output has expected data diff --git a/tests/determine-basal.test.ts b/tests/determine-basal.test.ts index 342c416ad..bfbdda2db 100644 --- a/tests/determine-basal.test.ts +++ b/tests/determine-basal.test.ts @@ -17,18 +17,24 @@ describe('round_basal', function ( ) { }); - it('should round correctly with a new pump model', function() { + it('should round correctly with a new pump model', function () { + const testProfile = { + ...profile, + model: '554', + } var basal = 0.025; - profile.model = "554"; - var output = round_basal(basal, profile); + var output = round_basal(basal, testProfile); output.should.equal(0.025); //console.error(output); }); - it('should round correctly with an invalid pump model', function() { + it('should round correctly with an invalid pump model', function () { + const testProfile = { + ...profile, + model: 'HelloThisIsntAPumpModel', + } var basal = 0.025; - profile.model = "HelloThisIsntAPumpModel"; - var output = round_basal(basal, profile); + var output = round_basal(basal, testProfile); output.should.equal(0.05); }); @@ -662,30 +668,41 @@ describe('determine-basal', function ( ) { it('maxSafeBasal current_basal_safety_multiplier of 1 should cause the current rate to be set, even if higher is needed', function () { var glucose_status = {"delta":5,"glucose":185,"long_avgdelta":5,"short_avgdelta":5}; - var iob_data = {"iob":0,"activity":-0.01,"bolussnooze":0}; - profile.current_basal_safety_multiplier = 1; - var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); + var iob_data = { "iob": 0, "activity": -0.01, "bolussnooze": 0 }; + const testProfile = { + ...profile, + current_basal_safety_multiplier: 1, + } + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); output.rate.should.equal(0.9); output.reason.should.match(/.*, adj. req. rate:.* to maxSafeBasal:.*, no temp, setting/); }); it('maxSafeBasal max_daily_safety_multiplier of 1 should cause the max daily rate to be set, even if higher is needed', function () { var glucose_status = {"delta":5,"glucose":185,"long_avgdelta":5,"short_avgdelta":5}; - var iob_data = {"iob":0,"activity":-0.01,"bolussnooze":0}; - profile.current_basal_safety_multiplier = null; - profile.max_daily_safety_multiplier = 1; - var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); + var iob_data = { "iob": 0, "activity": -0.01, "bolussnooze": 0 }; + const testProfile = { + ...profile, + current_basal_safety_multiplier: null, + max_daily_safety_multiplier: 1, + } + + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); output.rate.should.equal(1.3); output.reason.should.match(/.*, adj. req. rate:.* to maxSafeBasal:.*, no temp, setting/); }); it('overriding maxSafeBasal multipliers to 10 should increase temp', function () { var glucose_status = {"delta":5,"glucose":285,"long_avgdelta":5,"short_avgdelta":5}; - var iob_data = {"iob":0,"activity":-0.01,"bolussnooze":0}; - profile.max_basal = 5; - profile.current_basal_safety_multiplier = 10; - profile.max_daily_safety_multiplier = 10; - var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); + var iob_data = { "iob": 0, "activity": -0.01, "bolussnooze": 0 }; + const testProfile = { + ...profile, + max_basal: 5, + current_basal_safety_multiplier: 10, + max_daily_safety_multiplier: 10, + } + + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); output.rate.should.equal(5); output.reason.should.match(/.*, adj. req. rate:.* to maxSafeBasal:.*, no temp, setting/); }); @@ -699,13 +716,17 @@ describe('determine-basal', function ( ) { output.duration.should.equal(30); output.reason.should.match(/.*, adj. req. rate:.* to maxSafeBasal: 0.05, no temp, setting 0.05/); }); - + it('should match the basal rate precision available on a 523', function () { //var currenttemp = {"duration":30,"rate":0,"temp":"absolute"}; - var currenttemp = {"duration":0,"rate":0,"temp":"absolute"}; - profile.current_basal = 0.825; - profile.model = "523"; - var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); + var currenttemp = { "duration": 0, "rate": 0, "temp": "absolute" }; + const testProfile = { + ...profile, + current_basal: 0.825, + model: "523", + } + + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); //console.log(output); //output.rate.should.equal(0); //output.duration.should.equal(0); @@ -716,10 +737,14 @@ describe('determine-basal', function ( ) { it('should match the basal rate precision available on a 522', function () { //var currenttemp = {"duration":30,"rate":0,"temp":"absolute"}; - var currenttemp = {"duration":0,"rate":0,"temp":"absolute"}; - profile.current_basal = 0.875; - profile.model = "522"; - var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); + var currenttemp = { "duration": 0, "rate": 0, "temp": "absolute" }; + const testProfile = { + ...profile, + current_basal: 0.875, + model: "522", + } + + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); //console.log(output); //output.rate.should.equal(0); //output.duration.should.equal(0); @@ -728,4 +753,36 @@ describe('determine-basal', function ( ) { output.reason.should.match(/in range.*/); }); + it('should match using profile without carb_ratio', function () { + //var currenttemp = {"duration":30,"rate":0,"temp":"absolute"}; + var currenttemp = { "duration": 0, "rate": 0, "temp": "absolute" }; + const testProfile = { + ...profile, + carb_ratio: undefined, + } + + var output = determine_basal(glucose_status, currenttemp, iob_data, testProfile, autosens, meal_data, tempBasalFunctions); + + expect(output).toMatchObject({ + temp: 'absolute', + bg: 115, + tick: '+0', + eventualBG: 115, + insulinReq: 0, + reservoir: undefined, + sensitivityRatio: 1, + predBGs: { IOB: [ 115 ], ZT: [ 115 ] }, + COB: 0, + IOB: 0, + BGI: -0, + deviation: 0, + ISF: 40, + CR: 0, + target_bg: 115, + reason: 'COB: 0, Dev: 0, BGI: 0, ISF: 40, CR: 0, minPredBG: 999, minGuardBG: 999, IOBpredBG: 115; 115-999 in range: no temp required; setting current basal of 0.9 as temp. . Setting neutral temp basal of 0.9U/hr', + duration: 30, + rate: 0.9 + }) + }); + }); diff --git a/tests/profile.test.ts b/tests/profile.test.ts index 3272a8e22..83b5e6d7e 100644 --- a/tests/profile.test.ts +++ b/tests/profile.test.ts @@ -35,7 +35,8 @@ describe('Profile', function ( ) { }; it('should should create a profile from inputs', function () { - var profile = require('../lib/profile')(initFinalResults(), baseInputs); + const finalResult = initFinalResults() + var profile = require('../lib/profile')(finalResult, baseInputs); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -75,7 +76,7 @@ describe('Profile', function ( ) { }); it('should create a profile ignoring a temptarget with 0 duration', function() { - var profile = require('../lib/profile')({}, _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':0, 'created_at': creationDate}]})); + var profile = require('../lib/profile')({}, _.merge({}, baseInputs, { temptargets: [{ 'eventType': 'Temporary Target', 'reason': 'Eating Soon', 'targetTop': 80, 'targetBottom': 80, 'duration': 0, 'created_at': creationDate }] })); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -98,7 +99,7 @@ describe('Profile', function ( ) { it('should set the profile model from input', function () { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {model: 554})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { model: 554 })); profile.model.should.equal(554); }); diff --git a/tsconfig.json b/tsconfig.json index ad459f080..12d557b29 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "preserveWatchOutput": true, "skipLibCheck": true, "strict": true, + "exactOptionalPropertyTypes": true, "stripInternal": true, "importHelpers": true, "resolveJsonModule": true From 9ab2638ed6f4eff3493d438f30e3950fd384a63c Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Mon, 12 Aug 2024 19:55:01 +0200 Subject: [PATCH 13/15] refactor --- TODO.md | 2 + bin/oref0-autotune-prep.js | 5 +- bin/oref0-detect-sensitivity.js | 6 +- bin/oref0-determine-basal.js | 14 +- bin/oref0-meal.js | 8 +- .../{categorize.js => categorize.ts} | 365 +++++++++--------- lib/autotune-prep/dosed.js | 25 -- lib/autotune-prep/dosed.ts | 28 ++ lib/autotune-prep/index.js | 192 --------- lib/autotune-prep/index.ts | 194 ++++++++++ lib/autotune/index.ts | 4 +- lib/basal-set-temp.ts | 5 - lib/bin/utils.ts | 15 +- lib/calc-glucose-stats.ts | 4 - lib/date.ts | 5 - lib/determine-basal/autosens.ts | 41 +- lib/determine-basal/cob.ts | 30 +- lib/determine-basal/determine-basal.ts | 181 ++++----- lib/glucose-get-last.ts | 18 +- lib/glucose-stats.ts | 10 - lib/iob/Input.ts | 17 + lib/iob/InsulinTreatment.ts | 10 + lib/iob/calculate.ts | 2 - lib/iob/history.ts | 20 +- lib/iob/index.ts | 16 +- lib/iob/total.ts | 2 - lib/meal/RecentCarbs.ts | 19 + lib/meal/history.ts | 24 +- lib/meal/index.ts | 34 +- lib/meal/total.ts | 57 ++- lib/medtronic-clock.ts | 2 - lib/percentile.ts | 2 - lib/profile/basal.ts | 4 +- lib/profile/carbs.ts | 2 - lib/profile/index.ts | 18 +- lib/profile/isf.ts | 2 - lib/profile/targets.ts | 11 +- lib/require-utils.ts | 12 +- lib/round-basal.ts | 3 +- lib/types/Autosens.ts | 5 + lib/types/BasalSchedule.ts | 29 +- lib/types/CarbEntry.ts | 12 + lib/types/CarbRatioSchedule.ts | 18 +- lib/types/GlucoseEntry.ts | 74 +++- lib/types/IOB.ts | 28 ++ lib/types/ISFSensitivity.ts | 25 +- lib/types/LastGlucose.ts | 14 + lib/types/NightscoutTreatment.ts | 41 +- lib/types/PositiveInt.ts | 9 - lib/types/PositiveNumber.ts | 10 - lib/types/PumpHistoryEvent.ts | 3 + lib/types/ScheduleStart.ts | 10 +- lib/types/TempBasal.ts | 9 + lib/types/TempTarget.ts | 6 + package.json | 3 +- tests/command-behavior.tests.sh | 47 +-- tests/profile.test.ts | 2 +- 57 files changed, 918 insertions(+), 836 deletions(-) create mode 100644 TODO.md rename lib/autotune-prep/{categorize.js => categorize.ts} (64%) delete mode 100644 lib/autotune-prep/dosed.js create mode 100644 lib/autotune-prep/dosed.ts delete mode 100644 lib/autotune-prep/index.js create mode 100644 lib/autotune-prep/index.ts create mode 100644 lib/iob/Input.ts create mode 100644 lib/meal/RecentCarbs.ts create mode 100644 lib/types/CarbEntry.ts create mode 100644 lib/types/IOB.ts create mode 100644 lib/types/LastGlucose.ts delete mode 100644 lib/types/PositiveInt.ts delete mode 100644 lib/types/PositiveNumber.ts create mode 100644 lib/types/TempBasal.ts diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..2585cc4f5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,2 @@ +- check code for `Array.sort()`. We should do it in a non-mutable way +- activate test-autotune-prep in command-behavior.tests.sh diff --git a/bin/oref0-autotune-prep.js b/bin/oref0-autotune-prep.js index cf506ba3a..ad43d6c0f 100755 --- a/bin/oref0-autotune-prep.js +++ b/bin/oref0-autotune-prep.js @@ -104,13 +104,14 @@ if (!module.parent) { // Always keep the curve value up to date with what's in the user preferences profile_data.curve = pumpprofile_data.curve; + let glucose_data = [] try { - var glucose_data = JSON.parse(fs.readFileSync(glucose_input, 'utf8')); + glucose_data = JSON.parse(fs.readFileSync(glucose_input, 'utf8')); } catch (e) { return console.error("Warning: could not parse "+glucose_input, e); } - var carb_data = { }; + var carb_data = []; if (typeof carb_input !== 'undefined') { try { carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')); diff --git a/bin/oref0-detect-sensitivity.js b/bin/oref0-detect-sensitivity.js index 1d72065dd..a983ae278 100755 --- a/bin/oref0-detect-sensitivity.js +++ b/bin/oref0-detect-sensitivity.js @@ -38,7 +38,7 @@ if (!module.parent) { console.error('Incorrect number of arguments'); process.exit(1); } - + var fs = require('fs'); try { var cwd = process.cwd(); @@ -68,7 +68,7 @@ if (!module.parent) { } var basalprofile = require(cwd + '/' + basalprofile_input); - var carb_data = { }; + var carb_data = []; if (typeof carb_input !== 'undefined') { try { carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')); @@ -79,7 +79,7 @@ if (!module.parent) { // TODO: add support for a proper --retrospective flag if anything besides oref0-simulator needs this var retrospective = false; - var temptarget_data = { }; + var temptarget_data = []; if (typeof temptarget_input !== 'undefined') { try { if (temptarget_input == "retrospective") { diff --git a/bin/oref0-determine-basal.js b/bin/oref0-determine-basal.js index a1d8e31ec..f38fec9d4 100755 --- a/bin/oref0-determine-basal.js +++ b/bin/oref0-determine-basal.js @@ -116,7 +116,7 @@ if (!module.parent) { } //console.log(carbratio_data); - var meal_data = { }; + var meal_data = []; //console.error("meal_input",meal_input); if (meal_input && typeof meal_input !== 'undefined') { try { @@ -210,7 +210,17 @@ if (!module.parent) { //console.error(JSON.stringify(currenttemp)); //console.error(JSON.stringify(profile)); - var tempBasalFunctions = require('../dist/basal-set-temp'); + const input = { + glucose: glucose_status, + currenttemp: currenttemp, + iobTicks: iob_data, + profile: profile, + autosens: autosens_data, + meal: meal_data, + microBolusAllowed: params['microbolus'], + reservoir: reservoir_data, + currentTime: currentTime + } var rT = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, params['microbolus'], reservoir_data, currentTime); diff --git a/bin/oref0-meal.js b/bin/oref0-meal.js index 79f9dbeb9..fc7606d3a 100755 --- a/bin/oref0-meal.js +++ b/bin/oref0-meal.js @@ -20,14 +20,14 @@ */ -var generate = require('../dist/meal'); +var generate = require('../dist/meal') var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; var process_exit = shared_node_utils.process_exit; var initFinalResults = shared_node_utils.initFinalResults; -var oref0_meal = function oref0_meal(final_result, argv_params) { +var oref0_meal = function oref0_meal(final_result, argv_params) { var argv = require('yargs')(argv_params) .usage('$0 []') // error and show help if some other args given @@ -56,7 +56,7 @@ var oref0_meal = function oref0_meal(final_result, argv_params) { var profile_data; var clock_data; var basalprofile_data; - + try { pumphistory_data = JSON.parse(fs.readFileSync(pumphistory_input, 'utf8')); } catch (e) { @@ -97,7 +97,7 @@ var oref0_meal = function oref0_meal(final_result, argv_params) { console_error(final_result, "Warning: could not parse "+glucose_input); } - var carb_data = { }; + var carb_data = []; if (typeof carb_input !== 'undefined') { try { carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')); diff --git a/lib/autotune-prep/categorize.js b/lib/autotune-prep/categorize.ts similarity index 64% rename from lib/autotune-prep/categorize.js rename to lib/autotune-prep/categorize.ts index 458de591d..60533c4f2 100644 --- a/lib/autotune-prep/categorize.js +++ b/lib/autotune-prep/categorize.ts @@ -1,123 +1,117 @@ -'use strict' - -const date = require('../date') -const getIOB = require('../iob') -const find_insulin = require('../iob/history') -const basal = require('../profile/basal') -const ISF = require('../profile/isf') -const dosed = require('./dosed') -const tz = date.tz +import { pipe } from 'effect' +import * as A from 'effect/Array' +import * as Option from 'effect/Option' +import * as O from 'effect/Order' +import { getIob } from '../iob' +import { findInsulin } from '../iob/history' +import type { MealTreatment } from '../meal/MealTreatment' +import { basalLookup } from '../profile/basal' +import { isfLookup } from '../profile/isf' +import type { BasalSchedule } from '../types/BasalSchedule' +import * as GlucoseEntry from '../types/GlucoseEntry' +import type { ISFSensitivity } from '../types/ISFSensitivity' +import type { NightscoutTreatment } from '../types/NightscoutTreatment' +import type { Profile } from '../types/Profile' +import dosed from './dosed' + +interface Input { + treatments: ReadonlyArray + profile: Profile + pumpHistory: ReadonlyArray + glucose: ReadonlyArray + basalprofile: ReadonlyArray + pumpbasalprofile: ReadonlyArray + categorize_uam_as_basal: boolean +} -// main function categorizeBGDatums. ;) categorize to ISF, CSF, or basals. +type CSFUAMGlucoseData = GlucoseEntry.GlucoseEntry & { + glucose: number + dateString: string + avgDelta: number + BGI: number + deviation: number + mealCarbs: number + uamAbsorption?: string | undefined + mealAbsorption?: string | undefined +} -function categorizeBGDatums(opts) { - let treatments = opts.treatments +function categorizeBGDatums(opts: Input) { // this sorts the treatments collection in order. - treatments.sort((a, b) => { - const aDate = new Date(tz(a.timestamp)) - const bDate = new Date(tz(b.timestamp)) - //console.error(aDate); - return bDate.getTime() - aDate.getTime() - }) - const profileData = opts.profile - - let glucoseData = [] - if (typeof opts.glucose !== 'undefined') { - //var glucoseData = opts.glucose; - glucoseData = opts.glucose - .map(obj => { - //Support the NS sgv field to avoid having to convert in a custom way - obj.glucose = obj.glucose || obj.sgv - - if (obj.date) { - //obj.BGTime = new Date(obj.date); - } else if (obj.displayTime) { - // Attempt to get date from displayTime - obj.date = new Date(obj.displayTime.replace('T', ' ')).getTime() - } else if (obj.dateString) { - // Attempt to get date from dateString - obj.date = new Date(obj.dateString).getTime() - } // else { console.error("Could not determine BG time"); } - - if (!obj.dateString) { - obj.dateString = new Date(tz(obj.date)).toISOString() - } - return obj - }) - .filter(obj => { - // Only take records with a valid date record - // and a glucose value, which is also above 39 - return obj.date && obj.glucose && obj.glucose >= 39 - }) - .sort((a, b) => { - // sort the collection in order - return b.date - a.date - }) + let treatments = A.sort( + opts.treatments, + O.mapInput(O.Date, a => new Date(a.timestamp)) + ) + const profile = opts.profile + const pumpHistory = opts.pumpHistory + + if (!profile.isfProfile) { + throw new Error('ISF profile not set') } - // if (typeof(opts.preppedGlucose) !== 'undefined') { - // var preppedGlucoseData = opts.preppedGlucose; - // } - //starting variable at 0 - const boluses = 0 - const maxCarbs = 0 - //console.error(treatments); - if (!treatments) { - return {} + + if (!profile.carb_ratio) { + throw new Error('Carb ration not set') } - //console.error(glucoseData); - let IOBInputs = { - profile: profileData, - history: opts.pumpHistory, + const glucoseData = pipe( + A.filterMap(opts.glucose || [], a => + pipe( + Option.some({ + ...a, + glucose: GlucoseEntry.getGlucose(a), + dateString: GlucoseEntry.getDate(a).toISOString(), + }), + Option.filter(b => b.glucose >= 39) + ) + ), + A.sort(O.reverse(GlucoseEntry.Order)) + ) + + if (!treatments.length) { + return undefined } - let CSFGlucoseData = [] - let ISFGlucoseData = [] + + //console.error(glucoseData); + let CSFGlucoseData: CSFUAMGlucoseData[] = [] + let ISFGlucoseData: CSFUAMGlucoseData[] = [] let basalGlucoseData = [] - const UAMGlucoseData = [] - const CRData = [] + const UAMGlucoseData: CSFUAMGlucoseData[] = [] + let CRData = [] - const bucketedData = [] - bucketedData[0] = JSON.parse(JSON.stringify(glucoseData[0])) + const bucketedData: Array = [] + // why this? just to handle dates to strings? + bucketedData[0] = glucoseData[0] let j = 0 let k = 0 // index of first value used by bucket //for loop to validate and bucket the data - for (var i = 1; i < glucoseData.length; ++i) { - var BGTime = glucoseData[i].date - const lastBGTime = glucoseData[k].date - const elapsedMinutes = (BGTime - lastBGTime) / (60 * 1000) + for (let i = 1; i < glucoseData.length; ++i) { + const current = glucoseData[i] + const BGTime = new Date(current.dateString) + const lastBGTime = new Date(glucoseData[k].dateString) + const elapsedMinutes = (BGTime.getTime() - lastBGTime.getTime()) / (60 * 1000) if (Math.abs(elapsedMinutes) >= 2) { j++ // move to next bucket k = i // store index of first value used by bucket - bucketedData[j] = JSON.parse(JSON.stringify(glucoseData[i])) + bucketedData[j] = glucoseData[i] } else { // average all readings within time deadband - const glucoseTotal = glucoseData.slice(k, i + 1).reduce((total, entry) => { - return total + entry.glucose - }, 0) - bucketedData[j].glucose = glucoseTotal / (i - k + 1) + const glucoseTotal = glucoseData.slice(k, i + 1).reduce((total, entry) => total + entry.glucose, 0) + bucketedData[j] = { + ...bucketedData[j], + glucose: glucoseTotal / (i - k + 1), + } } } //console.error(bucketedData); //console.error(bucketedData[bucketedData.length-1]); // go through the treatments and remove any that are older than the oldest glucose value //console.error(treatments); - for (i = treatments.length - 1; i > 0; --i) { - var treatment = treatments[i] - //console.error(treatment); - if (treatment) { - var treatmentDate = new Date(tz(treatment.timestamp)) - var treatmentTime = treatmentDate.getTime() - var glucoseDatum = bucketedData[bucketedData.length - 1] - //console.error(glucoseDatum); - if (glucoseDatum) { - var BGDate = new Date(glucoseDatum.date) - BGTime = BGDate.getTime() - if (treatmentTime < BGTime) { - treatments.splice(i, 1) - } - } - } + const lastBucked = bucketedData[bucketedData.length - 1] + if (lastBucked) { + treatments = A.filter( + treatments, + treatment => new Date(treatment.timestamp).getTime() >= new Date(lastBucked.dateString).getTime() + ) } //console.error(treatments); let calculatingCR = false @@ -128,56 +122,51 @@ function categorizeBGDatums(opts) { let CRCarbs = 0 let type = '' // main for loop - const fullHistory = IOBInputs.history - let lastIsfResult = null - for (i = bucketedData.length - 5; i > 0; --i) { - glucoseDatum = bucketedData[i] + const fullHistory = pumpHistory + let newProfile = { + ...profile, + } + let lastIsfResult: ISFSensitivity | null = null + let CRInitialCarbTime + let CRInitialIOB + let CRInitialBG + for (let i = bucketedData.length - 5; i > 0; --i) { + const current = bucketedData[i] + const glucose = current.glucose //console.error(glucoseDatum); - BGDate = new Date(glucoseDatum.date) - BGTime = BGDate.getTime() + const BGDate = GlucoseEntry.getDate(current) + const BGTime = BGDate.getTime() // As we're processing each data point, go through the treatment.carbs and see if any of them are older than // the current BG data point. If so, add those carbs to COB. - treatment = treatments[treatments.length - 1] + const treatment = treatments[treatments.length - 1] let myCarbs = 0 if (treatment) { - treatmentDate = new Date(tz(treatment.timestamp)) - treatmentTime = treatmentDate.getTime() + const treatmentDate = new Date(treatment.timestamp) + const treatmentTime = treatmentDate.getTime() //console.error(treatmentDate); if (treatmentTime < BGTime) { if (treatment.carbs >= 1) { - mealCOB += parseFloat(treatment.carbs) - mealCarbs += parseFloat(treatment.carbs) + mealCOB += treatment.carbs + mealCarbs += treatment.carbs myCarbs = treatment.carbs } - treatments.pop() + treatments = A.remove(treatments, treatments.length - 1) } } - var BG - var delta - var avgDelta // TODO: re-implement interpolation to avoid issues here with gaps // calculate avgDelta as last 4 datapoints to better catch more rises after COB hits zero - if (typeof bucketedData[i].glucose !== 'undefined' && typeof bucketedData[i + 4].glucose !== 'undefined') { - //console.error(bucketedData[i]); - BG = bucketedData[i].glucose - if (BG < 40 || bucketedData[i + 4].glucose < 40) { - //process.stderr.write("!"); - continue - } - delta = BG - bucketedData[i + 1].glucose - avgDelta = (BG - bucketedData[i + 4].glucose) / 4 - } else { - console.error('Could not find glucose data') + const BG = glucose + if (BG < 40 || bucketedData[i + 4].glucose < 40) { + //process.stderr.write("!"); + continue } - - avgDelta = avgDelta.toFixed(2) - glucoseDatum.avgDelta = avgDelta + const delta = BG - bucketedData[i + 1].glucose + const avgDelta = Math.round(((BG - bucketedData[i + 4].glucose) / 4) * 100) / 100 //sens = ISF - var sens - ;[sens, lastIsfResult] = ISF.isfLookup(IOBInputs.profile.isfProfile, BGDate, lastIsfResult) - IOBInputs.clock = BGDate.toISOString() + let sens + ;[sens, lastIsfResult] = isfLookup(profile.isfProfile, BGDate, lastIsfResult) // trim down IOBInputs.history to just the data for 6h prior to BGDate //console.error(IOBInputs.history[0].created_at); const newHistory = [] @@ -187,49 +176,55 @@ function categorizeBGDatums(opts) { //if (h == 0 || h == fullHistory.length - 1) { //console.error(hDate, BGDate, hDate-BGDate) //} - if (BGDate - hDate < 6 * 60 * 60 * 1000 && BGDate - hDate > 0) { + if (BGDate.getTime() - hDate.getTime() < 6 * 60 * 60 * 1000 && BGDate.getTime() - hDate.getTime() > 0) { //process.stderr.write("i"); //console.error(hDate); newHistory.push(fullHistory[h]) } } - IOBInputs.history = newHistory // process.stderr.write("" + newHistory.length + " "); //console.error(newHistory[0].created_at,newHistory[newHistory.length-1].created_at,newHistory.length); // for IOB calculations, use the average of the last 4 hours' basals to help convergence; // this helps since the basal this hour could be different from previous, especially if with autotune they start to diverge. // use the pumpbasalprofile to properly calculate IOB during periods where no temp basal is set - const currentPumpBasal = basal.basalLookup(opts.pumpbasalprofile, BGDate) + const currentPumpBasal = basalLookup(opts.pumpbasalprofile, BGDate) const BGDate1hAgo = new Date(BGTime - 1 * 60 * 60 * 1000) const BGDate2hAgo = new Date(BGTime - 2 * 60 * 60 * 1000) const BGDate3hAgo = new Date(BGTime - 3 * 60 * 60 * 1000) - const basal1hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate1hAgo) - const basal2hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate2hAgo) - const basal3hAgo = basal.basalLookup(opts.pumpbasalprofile, BGDate3hAgo) + const basal1hAgo = basalLookup(opts.pumpbasalprofile, BGDate1hAgo) + const basal2hAgo = basalLookup(opts.pumpbasalprofile, BGDate2hAgo) + const basal3hAgo = basalLookup(opts.pumpbasalprofile, BGDate3hAgo) const sum = [currentPumpBasal, basal1hAgo, basal2hAgo, basal3hAgo].reduce((a, b) => { return a + b }) - IOBInputs.profile.currentBasal = Math.round((sum / 4) * 1000) / 1000 + newProfile = { + ...newProfile, + current_basal: Math.round((sum / 4) * 1000) / 1000, + } // this is the current autotuned basal, used for everything else besides IOB calculations - const currentBasal = basal.basalLookup(opts.basalprofile, BGDate) + const currentBasal = basalLookup(opts.basalprofile, BGDate) //console.error(currentBasal,basal1hAgo,basal2hAgo,basal3hAgo,IOBInputs.profile.currentBasal); // basalBGI is BGI of basal insulin activity. const basalBGI = Math.round(((currentBasal * sens) / 60) * 5 * 100) / 100 // U/hr * mg/dL/U * 1 hr / 60 minutes * 5 = mg/dL/5m //console.log(JSON.stringify(IOBInputs.profile)); // call iob since calculated elsewhere - const iob = getIOB(IOBInputs)[0] + const iob = getIob({ + profile: newProfile, + history: newHistory, + clock: BGDate.toISOString(), + })[0] //console.error(JSON.stringify(iob)); // activity times ISF times 5 minutes is BGI const BGI = Math.round(-iob.activity * sens * 5 * 100) / 100 // datum = one glucose data point (being prepped to store in output) - glucoseDatum.BGI = BGI + // calculating deviation - let deviation = avgDelta - BGI - let dev5m = delta - BGI + let deviation = Math.round((avgDelta - BGI) * 100) / 100 + const dev5m = Math.round((delta - BGI) * 100) / 100 //console.error(deviation,avgDelta,BG,bucketedData[i].glucose); // set positive deviations to zero if BG is below 80 @@ -237,14 +232,8 @@ function categorizeBGDatums(opts) { deviation = 0 } - // rounding and storing deviation - deviation = deviation.toFixed(2) - dev5m = dev5m.toFixed(2) - glucoseDatum.deviation = deviation - // Then, calculate carb absorption for that 5m interval using the deviation. if (mealCOB > 0) { - const profile = profileData const ci = Math.max(deviation, profile.min_5m_carbimpact) const absorbed = (ci * profile.carb_ratio) / sens // Store the COB, and use it as the starting point for the next data point. @@ -260,10 +249,11 @@ function categorizeBGDatums(opts) { if (mealCOB > 0 || calculatingCR) { // set initial values when we first see COB CRCarbs += myCarbs + if (!calculatingCR) { - var CRInitialIOB = iob.iob - var CRInitialBG = glucoseDatum.glucose - var CRInitialCarbTime = new Date(glucoseDatum.date) + CRInitialIOB = iob.iob + CRInitialBG = glucose + CRInitialCarbTime = GlucoseEntry.getDate(current) console.error( 'CRInitialIOB:', CRInitialIOB, @@ -281,8 +271,8 @@ function categorizeBGDatums(opts) { // when COB=0 and IOB drops low enough, record end values and be done calculatingCR } else { const CREndIOB = iob.iob - const CREndBG = glucoseDatum.glucose - const CREndTime = new Date(glucoseDatum.date) + const CREndBG = glucose + const CREndTime = GlucoseEntry.getDate(current) console.error('CREndIOB:', CREndIOB, 'CREndBG:', CREndBG, 'CREndTime:', CREndTime) const CRDatum = { CRInitialIOB: CRInitialIOB, @@ -295,7 +285,9 @@ function categorizeBGDatums(opts) { } //console.error(CRDatum); - const CRElapsedMinutes = Math.round((CREndTime - CRInitialCarbTime) / 1000 / 60) + const CRElapsedMinutes = CRInitialCarbTime + ? Math.round((CREndTime.getTime() - CRInitialCarbTime.getTime()) / 1000 / 60) + : 0 //console.error(CREndTime - CRInitialCarbTime, CRElapsedMinutes); if (CRElapsedMinutes < 60 || (i === 1 && mealCOB > 0)) { console.error('Ignoring', CRElapsedMinutes, 'm CR period.') @@ -308,6 +300,14 @@ function categorizeBGDatums(opts) { } } + const glucoseDatum = { + ...current, + avgDelta, + BGI, + deviation, + mealCarbs, + } + // If mealCOB is zero but all deviations since hitting COB=0 are positive, assign those data points to CSFGlucoseData // Once deviations go negative for at least one data point after COB=0, we can use the rest of the data to tune ISF or basals if (mealCOB > 0 || absorbing || mealCarbs > 0) { @@ -325,14 +325,18 @@ function categorizeBGDatums(opts) { } // check previous "type" value, and if it wasn't csf, set a mealAbsorption start flag //console.error(type); + let mealAbsorption if (type !== 'csf') { - glucoseDatum.mealAbsorption = 'start' - console.error(glucoseDatum.mealAbsorption, 'carb absorption') + mealAbsorption = 'start' + console.error('start', 'carb absorption') } type = 'csf' - glucoseDatum.mealCarbs = mealCarbs //if (i == 0) { glucoseDatum.mealAbsorption = "end"; } - CSFGlucoseData.push(glucoseDatum) + CSFGlucoseData.push({ + ...glucoseDatum, + mealCarbs, + mealAbsorption, + }) } else { // check previous "type" value, and if it was csf, set a mealAbsorption end flag if (type === 'csf') { @@ -346,12 +350,16 @@ function categorizeBGDatums(opts) { } else { uam = 0 } + let uamAbsorption if (type !== 'uam') { - glucoseDatum.uamAbsorption = 'start' - console.error(glucoseDatum.uamAbsorption, 'uannnounced meal absorption') + uamAbsorption = 'start' + console.error('start', 'uannnounced meal absorption') } type = 'uam' - UAMGlucoseData.push(glucoseDatum) + UAMGlucoseData.push({ + ...glucoseDatum, + uamAbsorption, + }) } else { if (type === 'uam') { console.error('end unannounced meal absorption') @@ -378,8 +386,9 @@ function categorizeBGDatums(opts) { } } // debug line to print out all the things + // get the time in HH:MM:SS const BGDateArray = BGDate.toString().split(' ') - BGTime = BGDateArray[4] + const BGTimeString = BGDateArray[4] // console.error(absorbing.toString(),"mealCOB:",mealCOB.toFixed(1),"mealCarbs:",mealCarbs,"basalBGI:",basalBGI.toFixed(1),"BGI:",BGI.toFixed(1),"IOB:",iob.iob.toFixed(1),"at",BGTime,"dev:",deviation,"avgDelta:",avgDelta,type); console.error( absorbing.toString(), @@ -392,7 +401,7 @@ function categorizeBGDatums(opts) { 'IOB:', iob.iob.toFixed(1), 'at', - BGTime, + BGTimeString, 'dev:', dev5m, 'avgDev:', @@ -405,22 +414,18 @@ function categorizeBGDatums(opts) { ) } - IOBInputs = { - profile: profileData, + const insulinTreatments = findInsulin({ + profile, history: opts.pumpHistory, - } - treatments = find_insulin(IOBInputs) - CRData.forEach(CRDatum => { - const dosedOpts = { - treatments: treatments, - profile: opts.profile, - start: CRDatum.CRInitialCarbTime, - end: CRDatum.CREndTime, - } - const insulinDosed = dosed(dosedOpts) - CRDatum.CRInsulin = insulinDosed.insulin - //console.error(CRDatum); }) + CRData = CRData.map(CRDatum => ({ + ...CRDatum, + CRInsulin: dosed({ + treatments: insulinTreatments, + start: CRDatum.CRInitialCarbTime!, + end: CRDatum.CREndTime, + }), + })) const CSFLength = CSFGlucoseData.length let ISFLength = ISFGlucoseData.length @@ -446,9 +451,8 @@ function categorizeBGDatums(opts) { basalGlucoseData.sort((a, b) => { return a.deviation - b.deviation }) - const newBasalGlucose = basalGlucoseData.slice(0, basalGlucoseData.length / 2) + basalGlucoseData = basalGlucoseData.slice(0, basalGlucoseData.length / 2) //console.error(newBasalGlucose); - basalGlucoseData = newBasalGlucose console.error('and selecting the lowest 50%, leaving', basalGlucoseData.length, 'basal+UAM ones') } @@ -459,9 +463,8 @@ function categorizeBGDatums(opts) { ISFGlucoseData.sort((a, b) => { return a.deviation - b.deviation }) - const newISFGlucose = ISFGlucoseData.slice(0, ISFGlucoseData.length / 2) + ISFGlucoseData = ISFGlucoseData.slice(0, ISFGlucoseData.length / 2) //console.error(newISFGlucose); - ISFGlucoseData = newISFGlucose console.error('and selecting the lowest 50%, leaving', ISFGlucoseData.length, 'ISF+UAM ones') //console.error(ISFGlucoseData.length, UAMLength); } @@ -473,7 +476,7 @@ function categorizeBGDatums(opts) { //console.error("Adding",CSFLength,"CSF deviations to",basalLength,"basal ones"); //var basalGlucoseData = basalGlucoseData.concat(CSFGlucoseData); console.error('Adding', CSFLength, 'CSF deviations to', ISFLength, 'ISF ones') - ISFGlucoseData = ISFGlucoseData.concat(CSFGlucoseData) + ISFGlucoseData = [...ISFGlucoseData, ...CSFGlucoseData] CSFGlucoseData = [] } @@ -485,4 +488,4 @@ function categorizeBGDatums(opts) { } } -exports = module.exports = categorizeBGDatums +export default categorizeBGDatums diff --git a/lib/autotune-prep/dosed.js b/lib/autotune-prep/dosed.js deleted file mode 100644 index 4ece2f5ef..000000000 --- a/lib/autotune-prep/dosed.js +++ /dev/null @@ -1,25 +0,0 @@ -function insulinDosed(opts) { - const start = opts.start.getTime() - const end = opts.end.getTime() - const treatments = opts.treatments - const profile_data = opts.profile - let insulinDosed = 0 - if (!treatments) { - console.error('No treatments to process.') - return {} - } - - treatments.forEach(treatment => { - //console.error(treatment); - if (treatment.insulin && treatment.date > start && treatment.date <= end) { - insulinDosed += treatment.insulin - } - }) - //console.error(insulinDosed); - - return { - insulin: Math.round(insulinDosed * 1000) / 1000, - } -} - -exports = module.exports = insulinDosed diff --git a/lib/autotune-prep/dosed.ts b/lib/autotune-prep/dosed.ts new file mode 100644 index 000000000..a0d2af53c --- /dev/null +++ b/lib/autotune-prep/dosed.ts @@ -0,0 +1,28 @@ +import { isBolusTreatment, type InsulinTreatment } from '../iob/InsulinTreatment' + +interface Input { + treatments: ReadonlyArray + start: Date + end: Date +} + +export function insulinDosed(opts: Input): { insulin?: number } { + const start = opts.start.getTime() + const end = opts.end.getTime() + const treatments = opts.treatments + if (!treatments) { + console.error('No treatments to process.') + return {} + } + + const insulin = treatments.reduce( + (b, a) => (isBolusTreatment(a) && a.date > start && a.date <= end ? b + a.insulin : b), + 0 + ) + + return { + insulin: Math.round(insulin * 1000) / 1000, + } +} + +export default insulinDosed diff --git a/lib/autotune-prep/index.js b/lib/autotune-prep/index.js deleted file mode 100644 index 476ea4bde..000000000 --- a/lib/autotune-prep/index.js +++ /dev/null @@ -1,192 +0,0 @@ -// Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js - -const find_meals = require('../meal/history') -const categorize = require('./categorize') - -function generate(inputs) { - //console.error(inputs); - const treatments = find_meals(inputs) - - const opts = { - treatments: treatments, - profile: inputs.profile, - pumpHistory: inputs.history, - glucose: inputs.glucose, - //, prepped_glucose: inputs.prepped_glucose - basalprofile: inputs.profile.basalprofile, - pumpbasalprofile: inputs.pumpprofile.basalprofile, - categorize_uam_as_basal: inputs.categorize_uam_as_basal, - } - - const autotune_prep_output = categorize(opts) - - if (inputs.tune_insulin_curve) { - if (opts.profile.curve === 'bilinear') { - console.error('--tune-insulin-curve is set but only valid for exponential curves') - } else { - let minDeviations = 1000000 - let newDIA = 0 - const diaDeviations = [] - const peakDeviations = [] - const currentDIA = opts.profile.dia - const currentPeak = opts.profile.insulinPeakTime - - const consoleError = console.error - console.error = function () {} - - const startDIA = currentDIA - 2 - const endDIA = currentDIA + 2 - for (let dia = startDIA; dia <= endDIA; ++dia) { - var sqrtDeviations = 0 - var deviations = 0 - var deviationsSq = 0 - - opts.profile.dia = dia - - var curve_output = categorize(opts) - var basalGlucose = curve_output.basalGlucoseData - - for (var hour = 0; hour < 24; ++hour) { - for (var i = 0; i < basalGlucose.length; ++i) { - var BGTime - - if (basalGlucose[i].date) { - BGTime = new Date(basalGlucose[i].date) - } else if (basalGlucose[i].displayTime) { - BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')) - } else if (basalGlucose[i].dateString) { - BGTime = new Date(basalGlucose[i].dateString) - } else { - consoleError('Could not determine last BG time') - } - - var myHour = BGTime.getHours() - if (hour === myHour) { - //console.error(basalGlucose[i].deviation); - sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5) - deviations += Math.abs(parseFloat(basalGlucose[i].deviation)) - deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2) - } - } - } - - var meanDeviation = Math.round(Math.abs(deviations / basalGlucose.length) * 1000) / 1000 - var SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 - var RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 - consoleError( - 'insulinEndTime', - dia, - 'meanDeviation:', - meanDeviation, - 'SMRDeviation:', - SMRDeviation, - 'RMSDeviation:', - RMSDeviation, - '(mg/dL)' - ) - diaDeviations.push({ - dia: dia, - meanDeviation: meanDeviation, - SMRDeviation: SMRDeviation, - RMSDeviation: RMSDeviation, - }) - autotune_prep_output.diaDeviations = diaDeviations - - deviations = Math.round(deviations * 1000) / 1000 - if (deviations < minDeviations) { - minDeviations = Math.round(deviations * 1000) / 1000 - newDIA = dia - } - } - - // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); - //consoleError(diaDeviations); - - minDeviations = 1000000 - - let newPeak = 0 - opts.profile.dia = currentDIA - //consoleError(opts.profile.useCustomPeakTime, opts.profile.insulinPeakTime); - if (!opts.profile.useCustomPeakTime === true && opts.profile.curve === 'ultra-rapid') { - opts.profile.insulinPeakTime = 55 - } else if (!opts.profile.useCustomPeakTime === true) { - opts.profile.insulinPeakTime = 75 - } - opts.profile.useCustomPeakTime = true - - const startPeak = opts.profile.insulinPeakTime - 10 - const endPeak = opts.profile.insulinPeakTime + 10 - for (let peak = startPeak; peak <= endPeak; peak = peak + 5) { - sqrtDeviations = 0 - deviations = 0 - deviationsSq = 0 - - opts.profile.insulinPeakTime = peak - - curve_output = categorize(opts) - basalGlucose = curve_output.basalGlucoseData - - for (hour = 0; hour < 24; ++hour) { - for (i = 0; i < basalGlucose.length; ++i) { - if (basalGlucose[i].date) { - BGTime = new Date(basalGlucose[i].date) - } else if (basalGlucose[i].displayTime) { - BGTime = new Date(basalGlucose[i].displayTime.replace('T', ' ')) - } else if (basalGlucose[i].dateString) { - BGTime = new Date(basalGlucose[i].dateString) - } else { - consoleError('Could not determine last BG time') - } - - myHour = BGTime.getHours() - if (hour === myHour) { - //console.error(basalGlucose[i].deviation); - sqrtDeviations += Math.pow(parseFloat(Math.abs(basalGlucose[i].deviation)), 0.5) - deviations += Math.abs(parseFloat(basalGlucose[i].deviation)) - deviationsSq += Math.pow(parseFloat(basalGlucose[i].deviation), 2) - } - } - } - console.error(deviationsSq) - - meanDeviation = Math.round((deviations / basalGlucose.length) * 1000) / 1000 - SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 - RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 - consoleError( - 'insulinPeakTime', - peak, - 'meanDeviation:', - meanDeviation, - 'SMRDeviation:', - SMRDeviation, - 'RMSDeviation:', - RMSDeviation, - '(mg/dL)' - ) - peakDeviations.push({ - peak: peak, - meanDeviation: meanDeviation, - SMRDeviation: SMRDeviation, - RMSDeviation: RMSDeviation, - }) - autotune_prep_output.diaDeviations = diaDeviations - - deviations = Math.round(deviations * 1000) / 1000 - if (deviations < minDeviations) { - minDeviations = Math.round(deviations * 1000) / 1000 - newPeak = peak - } - } - - //consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); - //consoleError(peakDeviations); - autotune_prep_output.peakDeviations = peakDeviations - - console.error = consoleError - } - } - - return autotune_prep_output -} - -exports = module.exports = generate diff --git a/lib/autotune-prep/index.ts b/lib/autotune-prep/index.ts new file mode 100644 index 000000000..4e2e4ae6f --- /dev/null +++ b/lib/autotune-prep/index.ts @@ -0,0 +1,194 @@ +// Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js + +import { Schema } from '@effect/schema' +import find_meals from '../meal/history' +import { CarbEntry } from '../types/CarbEntry' +import * as GlucoseEntry from '../types/GlucoseEntry' +import { NightscoutTreatment } from '../types/NightscoutTreatment' +import { Profile } from '../types/Profile' +import categorize from './categorize' + +const Input = Schema.Struct({ + history: Schema.Array(NightscoutTreatment), + profile: Profile, + pumpprofile: Profile, + carbs: Schema.Array(CarbEntry), + glucose: Schema.optionalWith(Schema.Array(GlucoseEntry.GlucoseEntry), { nullable: true }), + categorize_uam_as_basal: Schema.optionalWith(Schema.Boolean, { default: () => false }), + tune_insulin_curve: Schema.optionalWith(Schema.Boolean, { default: () => false }), +}) + +function generate(input: unknown) { + //console.error(inputs); + const inputs = Schema.decodeUnknownSync(Input)(input) + const treatments = find_meals(inputs) + const profile = inputs.profile + + const opts = { + treatments: treatments, + profile: inputs.profile, + pumpHistory: inputs.history, + glucose: inputs.glucose || [], + //, prepped_glucose: inputs.prepped_glucose + basalprofile: inputs.profile.basalprofile, + pumpbasalprofile: inputs.pumpprofile.basalprofile || false, + categorize_uam_as_basal: inputs.categorize_uam_as_basal || false, + } + + const diaDeviations = [] + const peakDeviations = [] + + if (inputs.tune_insulin_curve) { + if (profile.curve === 'bilinear') { + console.error('--tune-insulin-curve is set but only valid for exponential curves') + } else { + let minDeviations = 1000000 + //let newDIA = 0 + const currentDIA = profile.dia + + const consoleError = console.error + //console.error = function () {} + + const startDIA = currentDIA - 2 + const endDIA = currentDIA + 2 + for (let dia = startDIA; dia <= endDIA; ++dia) { + let sqrtDeviations = 0 + let deviations = 0 + let deviationsSq = 0 + + const curve_output = categorize({ + ...opts, + profile: { + ...profile, + dia, + }, + }) + const basalGlucose = curve_output?.basalGlucoseData || [] + + for (let hour = 0; hour < 24; ++hour) { + for (let i = 0; i < basalGlucose.length; ++i) { + const current = basalGlucose[i] + const BGTime = GlucoseEntry.getDate(current) + + if (hour === BGTime.getHours()) { + //console.error(basalGlucose[i].deviation); + sqrtDeviations += Math.pow(Math.abs(basalGlucose[i].deviation), 0.5) + deviations += Math.abs(basalGlucose[i].deviation) + deviationsSq += Math.pow(basalGlucose[i].deviation, 2) + } + } + } + + const meanDeviation = Math.round(Math.abs(deviations / basalGlucose.length) * 1000) / 1000 + const SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 + const RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 + consoleError( + 'insulinEndTime', + dia, + 'meanDeviation:', + meanDeviation, + 'SMRDeviation:', + SMRDeviation, + 'RMSDeviation:', + RMSDeviation, + '(mg/dL)' + ) + diaDeviations.push({ + dia: dia, + meanDeviation: meanDeviation, + SMRDeviation: SMRDeviation, + RMSDeviation: RMSDeviation, + }) + + deviations = Math.round(deviations * 1000) / 1000 + if (deviations < minDeviations) { + minDeviations = Math.round(deviations * 1000) / 1000 + //newDIA = dia + } + } + + // consoleError('Optimum insulinEndTime', newDIA, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); + //consoleError(diaDeviations); + + minDeviations = 1000000 + + //let newPeak = 0 + //consoleError(profile.useCustomPeakTime, profile.insulinPeakTime); + let insulinPeakTime = profile.insulinPeakTime + if (!profile.useCustomPeakTime === true && profile.curve === 'ultra-rapid') { + insulinPeakTime = 55 + } else if (!profile.useCustomPeakTime === true) { + insulinPeakTime = 75 + } + + const startPeak = insulinPeakTime - 10 + const endPeak = insulinPeakTime + 10 + for (let peak = startPeak; peak <= endPeak; peak = peak + 5) { + let sqrtDeviations = 0 + let deviations = 0 + let deviationsSq = 0 + + const curve_output = categorize({ + ...opts, + profile: { + ...profile, + insulinPeakTime: peak, + useCustomPeakTime: true, + }, + }) + const basalGlucose = curve_output?.basalGlucoseData || [] + + for (let hour = 0; hour < 24; ++hour) { + for (let i = 0; i < basalGlucose.length; ++i) { + const currentBasalGlucose = basalGlucose[i] + const BGTime = GlucoseEntry.getDate(currentBasalGlucose) + + if (hour === BGTime.getHours()) { + //console.error(basalGlucose[i].deviation); + sqrtDeviations += Math.pow(Math.abs(currentBasalGlucose.deviation), 0.5) + deviations += Math.abs(currentBasalGlucose.deviation) + deviationsSq += Math.pow(currentBasalGlucose.deviation, 2) + } + } + } + console.error(deviationsSq) + + const meanDeviation = Math.round((deviations / basalGlucose.length) * 1000) / 1000 + const SMRDeviation = Math.round(Math.pow(sqrtDeviations / basalGlucose.length, 2) * 1000) / 1000 + const RMSDeviation = Math.round(Math.pow(deviationsSq / basalGlucose.length, 0.5) * 1000) / 1000 + consoleError( + 'insulinPeakTime', + peak, + 'meanDeviation:', + meanDeviation, + 'SMRDeviation:', + SMRDeviation, + 'RMSDeviation:', + RMSDeviation, + '(mg/dL)' + ) + peakDeviations.push({ + peak: peak, + meanDeviation: meanDeviation, + SMRDeviation: SMRDeviation, + RMSDeviation: RMSDeviation, + }) + + deviations = Math.round(deviations * 1000) / 1000 + if (deviations < minDeviations) { + minDeviations = Math.round(deviations * 1000) / 1000 + //newPeak = peak + } + } + + //consoleError('Optimum insulinPeakTime', newPeak, 'mean deviation:', Math.round(minDeviations/basalGlucose.length*1000)/1000, '(mg/dL)'); + //consoleError(peakDeviations); + + console.error = consoleError + } + } + + return { ...categorize(opts), diaDeviations, peakDeviations } +} + +export default module.exports = generate diff --git a/lib/autotune/index.ts b/lib/autotune/index.ts index a0ffa14a3..3306be9b6 100644 --- a/lib/autotune/index.ts +++ b/lib/autotune/index.ts @@ -1,4 +1,4 @@ -const percentile = require('../percentile') +import percentile from '../percentile' // does three things - tunes basals, ISF, and CSF @@ -735,4 +735,4 @@ function tuneAllTheThings(inputs: any) { return autotuneOutput } -exports = module.exports = tuneAllTheThings +export default module.exports = tuneAllTheThings diff --git a/lib/basal-set-temp.ts b/lib/basal-set-temp.ts index 94184308a..a8cc01514 100644 --- a/lib/basal-set-temp.ts +++ b/lib/basal-set-temp.ts @@ -83,8 +83,3 @@ export function setTempBasal(rate: number, duration: number, profile: Profile, r return rT } } - -module.exports = { - getMaxSafeBasal, - setTempBasal, -} diff --git a/lib/bin/utils.ts b/lib/bin/utils.ts index 47d1fceb2..24143539f 100644 --- a/lib/bin/utils.ts +++ b/lib/bin/utils.ts @@ -23,29 +23,22 @@ function console_both(final_result: string, theArgs: unknown[]) { return newResult } -export const console_error = function console_error(final_result: FinalResult, ...theArgs: unknown[]) { +export const console_error = (final_result: FinalResult, ...theArgs: unknown[]) => { final_result.err = console_both(final_result.err, theArgs) } -export const console_log = function console_log(final_result: FinalResult, ...theArgs: unknown[]) { +export const console_log = (final_result: FinalResult, ...theArgs: unknown[]) => { final_result.stdout = console_both(final_result.stdout, theArgs) } -export const process_exit = function process_exit(final_result: FinalResult, ret: number) { +export const process_exit = (final_result: FinalResult, ret: number) => { final_result.return_val = ret } -export const initFinalResults = function initFinalResults(): FinalResult { +export const initFinalResults = (): FinalResult => { return { stdout: '', err: '', return_val: 0, } } - -module.exports = { - console_log: console_log, - console_error: console_error, - process_exit: process_exit, - initFinalResults: initFinalResults, -} diff --git a/lib/calc-glucose-stats.ts b/lib/calc-glucose-stats.ts index a18417fc4..869a3fc12 100644 --- a/lib/calc-glucose-stats.ts +++ b/lib/calc-glucose-stats.ts @@ -30,7 +30,3 @@ export function updateGlucoseStats(options: Options) { return options.glucose_hist } - -exports = module.exports = { - updateGlucoseStats, -} diff --git a/lib/date.ts b/lib/date.ts index 3af3f5fa8..1b3ba3730 100644 --- a/lib/date.ts +++ b/lib/date.ts @@ -14,8 +14,3 @@ export const format = (a: Date) => { .toString() .padStart(2, '0')}` } - -exports = exports.default = { - tz: tz, - format, -} diff --git a/lib/determine-basal/autosens.ts b/lib/determine-basal/autosens.ts index 49bfe26f1..d12039d13 100644 --- a/lib/determine-basal/autosens.ts +++ b/lib/determine-basal/autosens.ts @@ -1,16 +1,18 @@ +import * as A from 'effect/Array' import { tz } from '../date' -import get_iob from '../iob' -import find_insulin from '../iob/history' -import type { CarbEntry } from '../meal/history' +import { getIob } from '../iob' +import { findInsulin } from '../iob/history' +import * as MealTreatment from '../meal/MealTreatment' import find_meals from '../meal/history' import percentile from '../percentile' import { basalLookup } from '../profile/basal' import { isfLookup } from '../profile/isf' import type { BasalSchedule } from '../types/BasalSchedule' +import type { CarbEntry } from '../types/CarbEntry' import type { GlucoseEntry } from '../types/GlucoseEntry' -import { getDate } from '../types/GlucoseEntry' +import { getDate, getGlucose } from '../types/GlucoseEntry' import type { ISFSensitivity } from '../types/ISFSensitivity' -import type { TempTarget } from '../types/TempTarget' +import * as TempTarget from '../types/TempTarget' interface Inputs { glucose_data: GlucoseEntry[] @@ -18,7 +20,7 @@ interface Inputs { basalprofile: BasalSchedule[] retrospective?: boolean carbs: CarbEntry[] - temptargets: TempTarget[] + temptargets: TempTarget.TempTarget[] deviations?: number } @@ -28,7 +30,7 @@ function detectSensitivity(inputs: Inputs) { //Support the NS sgv field to avoid having to convert in a custom way return { ...obj, - glucose: obj.glucose || obj.sgv!, + glucose: getGlucose(obj), } }) //console.error(glucose_data[0]); @@ -64,7 +66,7 @@ function detectSensitivity(inputs: Inputs) { } // get treatments from pumphistory once, not every time we get_iob() - const treatments = find_insulin(inputs.iob_inputs) + const treatments = findInsulin(inputs.iob_inputs) const mealinputs = { history: inputs.iob_inputs.history, @@ -73,13 +75,7 @@ function detectSensitivity(inputs: Inputs) { glucose: inputs.glucose_data, //, prepped_glucose: prepped_glucose_data } - const meals = find_meals(mealinputs) - meals.sort((a, b) => { - const aDate = new Date(tz(a.timestamp)) - const bDate = new Date(tz(b.timestamp)) - //console.error(aDate); - return bDate.getTime() - aDate.getTime() - }) + const meals = A.sort(find_meals(mealinputs), MealTreatment.Order) //console.error(meals); const avgDeltas = [] @@ -216,7 +212,7 @@ function detectSensitivity(inputs: Inputs) { iob_inputs.profile.temptargetSet = false //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); - const iob = get_iob(iob_inputs, true, treatments)[0] + const iob = getIob(iob_inputs, true, treatments)[0] //console.error("After: ", new Date().getTime()); //console.log(JSON.stringify(iob)); @@ -482,17 +478,10 @@ function detectSensitivity(inputs: Inputs) { newisf: newisf, } } -module.exports = detectSensitivity -function tempTargetRunning(temptargets_data: TempTarget[], time: Date) { +function tempTargetRunning(temptargets: TempTarget.TempTarget[], time: Date) { // sort tempTargets by date so we can process most recent first - try { - temptargets_data.sort((a, b) => { - return new Date(a.created_at).getTime() + new Date(b.created_at).getTime() - }) - } catch (_e) { - //console.error("Could not sort temptargets_data. Optional feature temporary targets disabled."); - } + const temptargets_data = A.sort(temptargets, TempTarget.Order) //console.error(temptargets_data); //console.error(time); for (let i = 0; i < temptargets_data.length; i++) { @@ -514,3 +503,5 @@ function tempTargetRunning(temptargets_data: TempTarget[], time: Date) { return 0 } + +export default exports.module = detectSensitivity diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index 75f6cef9b..8bba6d483 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -1,15 +1,15 @@ -import get_iob from '../iob' -import type { Input as IOBInput } from '../iob/history' -import find_insulin from '../iob/history' +import { getIob } from '../iob' +import type { Input as IOBInput } from '../iob/Input' +import { findInsulin } from '../iob/history' import * as basal from '../profile/basal' import { isfLookup } from '../profile/isf' import type { BasalSchedule } from '../types/BasalSchedule' -import type { GlucoseEntry } from '../types/GlucoseEntry' +import { getGlucose, type GlucoseEntry } from '../types/GlucoseEntry' export interface DetectCOBInput { - glucose_data: GlucoseEntry[] + glucose_data: ReadonlyArray iob_inputs: IOBInput - basalprofile?: BasalSchedule[] + basalprofile?: ReadonlyArray mealTime: number ciTime?: number } @@ -29,15 +29,15 @@ function getDateFromEntry(entry: GlucoseEntry) { /** * @todo: does it works with profile.carb_ratio === undefined? */ -export default function detectCarbAbsorption(inputs: DetectCOBInput) { +function detectCarbAbsorption(inputs: DetectCOBInput) { const glucose_data = inputs.glucose_data.reduce( (b, a) => { - const glucose = a.glucose || a.sgv + const glucose = getGlucose(a) return glucose ? [...b, { ...a, glucose, date: getDateFromEntry(a) }] : b }, [] as (GlucoseEntry & { glucose: number; date: number })[] ) - const iob_inputs = inputs.iob_inputs + let iob_inputs = inputs.iob_inputs const basalprofile = inputs.basalprofile /* TODO why does declaring profile break tests-command-behavior.tests.sh? because it is a global variable used in other places.*/ @@ -48,7 +48,7 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { //console.error(mealTime, ciTime); // get treatments from pumphistory once, not every time we get_iob() - const treatments = find_insulin(inputs.iob_inputs) + const treatments = findInsulin(inputs.iob_inputs) if (!glucose_data.length) { // @todo: return something empty @@ -160,7 +160,10 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { continue } - iob_inputs.clock = bgTime.toISOString() + iob_inputs = { + ...iob_inputs, + clock: bgTime.toISOString(), + } const current_basal = basal.basalLookup(basalprofile || [], bgTime) if (!current_basal) { continue @@ -175,7 +178,7 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { //console.log(JSON.stringify(iob_inputs.profile)); //console.error("Before: ", new Date().getTime()); - const iob = get_iob(newIobInputs, true, treatments)[0] + const iob = getIob(newIobInputs, true, treatments)[0] //console.error("After: ", new Date().getTime()); //console.error(JSON.stringify(iob)); @@ -238,4 +241,5 @@ export default function detectCarbAbsorption(inputs: DetectCOBInput) { allDeviations: allDeviations, } } -module.exports = detectCarbAbsorption + +export default module.exports = detectCarbAbsorption diff --git a/lib/determine-basal/determine-basal.ts b/lib/determine-basal/determine-basal.ts index 531ebad0b..be28aa696 100644 --- a/lib/determine-basal/determine-basal.ts +++ b/lib/determine-basal/determine-basal.ts @@ -15,9 +15,15 @@ // Define various functions used later on, in the main function determine_basal() below +import { Schema } from '@effect/schema' +import { getMaxSafeBasal, setTempBasal } from '../basal-set-temp' +import { RecentCarbs } from '../meal/RecentCarbs' import round_basal from '../round-basal' -import type { Autosens } from '../types/Autosens' -import type { Profile } from '../types/Profile' +import { Autosens } from '../types/Autosens' +import { IOB } from '../types/IOB' +import { LastGlucose } from '../types/LastGlucose' +import { Profile } from '../types/Profile' +import { TempBasal } from '../types/TempBasal' // Rounds value to 'digits' decimal places function round(value: number, digits?: number) { @@ -44,21 +50,10 @@ function convert_bg(value: number, profile: Profile) { } } -interface MealData { - bwFound?: boolean - mealCOB: number - carbs: number - bwCarbs?: number - lastCarbTime: number - slopeFromMaxDeviation: number - slopeFromMinDeviation: number - reason?: string -} - function enable_smb( profile: Profile, microBolusAllowed: boolean, - meal_data: MealData, + meal_data: RecentCarbs, bg: number, target_bg: number, high_bg: number | undefined @@ -142,46 +137,40 @@ function enable_smb( return false } -interface GlucoseStatus { - glucose: number - delta: number - noise: number - date: string | number - short_avgdelta: number - long_avgdelta: number - device?: string - last_cal?: number -} - -interface IOBTick { - activity: number - iob: number - lastTemp?: { - date: number - duration: number - rate: number - } - iobWithZeroTemp: { - activity: number - } - lastBolusTime: number -} - -interface CurrentTemp { - timestamp: string - temp: 'absolute' | string - rate: number - duration: number +const Input = Schema.Struct({ + glucose: LastGlucose, + currenttemp: TempBasal, + iobTicks: Schema.Array(IOB), + profile: Profile, + autosens: Schema.optional(Autosens), + meal: RecentCarbs, + microBolusAllowed: Schema.Boolean, + reservoir: Schema.Number, + currentTime: Schema.optional(Schema.Union(Schema.DateFromSelf, Schema.DateFromString).pipe(Schema.validDate())), +}) + +function generate(input: unknown) { + const r = Schema.decodeUnknownSync(Input)(input) + return determine_basal( + r.glucose, + r.currenttemp, + r.iobTicks, + r.profile, + r.autosens, + r.meal, + r.microBolusAllowed, + r.reservoir, + r.currentTime + ) } -const determine_basal = function determine_basal( - glucose_status: GlucoseStatus, - currenttemp: CurrentTemp, - iobArray: IOBTick | IOBTick[], +export const determine_basal = function determine_basal( + glucose_status: LastGlucose, + currenttemp: TempBasal, + iob_ticks: ReadonlyArray, profile: Profile, autosens_data: Autosens | undefined, - meal_data: MealData, - tempBasalFunctions: any, + meal_data: RecentCarbs, microBolusAllowed: boolean, reservoir_data: number, currentTime?: Date @@ -214,22 +203,12 @@ const determine_basal = function determine_basal( [k: string]: unknown } = {} //short for requestedTemp - let deliverAt = new Date() - if (currentTime) { - deliverAt = currentTime - } + const deliverAt = currentTime || new Date() - if (typeof profile === 'undefined' || typeof profile.current_basal === 'undefined') { - rT.error = 'Error: could not get current basal rate' - return rT - } const profile_current_basal = round_basal(profile.current_basal, profile) let basal = profile_current_basal - let systemTime = new Date() - if (currentTime) { - systemTime = currentTime - } + const systemTime = currentTime || new Date() const bgTime = new Date(glucose_status.date) const minAgo = round((systemTime.getTime() - bgTime.getTime()) / 60 / 1000, 1) @@ -256,13 +235,13 @@ const determine_basal = function determine_basal( let tooflat = false if ( bg > 60 && - glucose_status.delta == 0 && + glucose_status.delta === 0 && glucose_status.short_avgdelta > -1 && glucose_status.short_avgdelta < 1 && glucose_status.long_avgdelta > -1 && glucose_status.long_avgdelta < 1 ) { - if (glucose_status.device == 'fakecgm') { + if (glucose_status.device === 'fakecgm') { console.error( `CGM data is unchanged (${bg}+${glucose_status.delta}) for 5m w/ ${glucose_status.short_avgdelta} mg/dL ~15m change & ${glucose_status.long_avgdelta} mg/dL ~45m change` ) @@ -298,7 +277,7 @@ const determine_basal = function determine_basal( rT.rate = basal return rT // don't use setTempBasal(), as it has logic that allows <120% high temps to continue running - //return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp); + //return setTempBasal(basal, 30, profile, rT, currenttemp); } else if (currenttemp.rate === 0 && currenttemp.duration > 30) { //shorten long zero temps to 30m rT.reason += `. Shortening ${currenttemp.duration}m long zero temp to 30m. ` @@ -308,7 +287,7 @@ const determine_basal = function determine_basal( rT.rate = 0 return rT // don't use setTempBasal(), as it has logic that allows long zero temps to continue running - //return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp); + //return setTempBasal(0, 30, profile, rT, currenttemp); } else { //do nothing. rT.reason += `. Temp ${currenttemp.rate} <= current basal ${basal}U/hr; doing nothing. ` @@ -325,34 +304,23 @@ const determine_basal = function determine_basal( // if min and max are set, then set target to their average let target_bg: number - let min_bg = profile.min_bg as number - let max_bg = profile.max_bg as number + let min_bg = profile.min_bg + let max_bg = profile.max_bg const high_bg = profile.enableSMB_high_bg_target - if (min_bg === undefined || max_bg === undefined) { - rT.error = 'Error: could not determine target_bg. ' - return rT - } - target_bg = (min_bg + max_bg) / 2 // Calculate sensitivityRatio based on temp targets, if applicable, or using the value calculated by autosens let sensitivityRatio: number | undefined const high_temptarget_raises_sensitivity = profile.exercise_mode || profile.high_temptarget_raises_sensitivity const normalTarget = 100 // evaluate high/low temptarget against 100, not scheduled target (which might change) - let halfBasalTarget - if (profile.half_basal_exercise_target) { - halfBasalTarget = profile.half_basal_exercise_target - } else { - halfBasalTarget = 160 // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) - // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limited to autosens_max (1.2x by default) - } + + // when temptarget is 160 mg/dL, run 50% basal (120 = 75%; 140 = 60%) + // 80 mg/dL with low_temptarget_lowers_sensitivity would give 1.5x basal, but is limited to autosens_max (1.2x by default) + const halfBasalTarget = profile.half_basal_exercise_target || 160 if ( - (profile.autosens_max !== undefined && - high_temptarget_raises_sensitivity && - profile.temptargetSet && - target_bg > normalTarget) || + (high_temptarget_raises_sensitivity && profile.temptargetSet && target_bg > normalTarget) || (profile.low_temptarget_lowers_sensitivity && profile.temptargetSet && target_bg < normalTarget) ) { // w/ target 100, temp target 110 = .89, 120 = 0.8, 140 = 0.67, 160 = .57, and 200 = .44 @@ -435,7 +403,7 @@ const determine_basal = function determine_basal( // Adjust ISF based on sensitivityRatio const profile_sens = round(profile.sens, 1) let sens = profile.sens - if (typeof autosens_data !== 'undefined' && autosens_data && sensitivityRatio) { + if (autosens_data && sensitivityRatio) { sens = profile.sens / sensitivityRatio sens = round(sens, 1) if (sens !== profile_sens) { @@ -447,7 +415,7 @@ const determine_basal = function determine_basal( } console.error('; CR:', carb_ratio) - const iob_data = Array.isArray(iobArray) ? iobArray[0] : iobArray + const iob_data = Array.isArray(iob_ticks) ? iob_ticks[0] : iob_ticks if (!iob_data) { rT.error = 'Error: iob_data undefined. ' @@ -485,7 +453,7 @@ const determine_basal = function determine_basal( rT.reason = `Warning: currenttemp rate ${currenttemp.rate} != lastTemp rate ${ iob_data.lastTemp.rate } from pumphistory; canceling temp` - return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp) + return setTempBasal(0, 0, profile, rT, currenttemp) } if (currenttemp && iob_data.lastTemp && currenttemp.duration > 0) { //console.error(lastTempAge, round(iob_data.lastTemp.duration,1), round(lastTempAge - iob_data.lastTemp.duration,1)); @@ -495,7 +463,7 @@ const determine_basal = function determine_basal( lastTempEnded }m ago; canceling temp` //console.error(currenttemp, round(iob_data.lastTemp,1), round(lastTempAge,1)); - return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp) + return setTempBasal(0, 0, profile, rT, currenttemp) } } @@ -694,7 +662,7 @@ const determine_basal = function determine_basal( let COBpredBG: number | undefined let UAMpredBG: number | undefined try { - ;(Array.isArray(iobArray) ? iobArray : []).forEach(iobTick => { + iob_ticks.forEach(iobTick => { //console.error(iobTick); const predBGI = round(-iobTick.activity * sens * 5, 2) const predZTBGI = round(-iobTick.iobWithZeroTemp.activity * sens * 5, 2) @@ -1076,9 +1044,7 @@ const determine_basal = function determine_basal( console.error( `naive_eventualBG: ${convert_bg(naive_eventualBG, profile)}, bgUndershoot: ${convert_bg(bgUndershoot, profile)}, zeroTempDuration: ${zeroTempDuration}, zeroTempEffect: ${zeroTempEffect}, carbsReq: ${carbsReq}` ) - if (meal_data.reason == 'Could not parse clock data') { - console.error('carbsReq unknown: Could not parse clock data') - } else if ( + if ( profile.carbsReqThreshold !== undefined && carbsReq >= profile.carbsReqThreshold && minutesAboveThreshold <= 45 @@ -1109,14 +1075,14 @@ const determine_basal = function determine_basal( durationReq = round(durationReq / 30) * 30 // always set a 30-120m zero temp (oref0-pump-loop will let any longer SMB zero temp run) durationReq = Math.min(120, Math.max(30, durationReq)) - return tempBasalFunctions.setTempBasal(0, durationReq, profile, rT, currenttemp) + return setTempBasal(0, durationReq, profile, rT, currenttemp) } // if not in LGS mode, cancel temps before the top of the hour to reduce beeping/vibration // console.error(profile.skip_neutral_temps, rT.deliverAt.getMinutes()); if (profile.skip_neutral_temps && rTDeliveredAt.getMinutes() >= 55) { rT.reason += `; Canceling temp at ${rTDeliveredAt.getMinutes()}m past the hour. ` - return tempBasalFunctions.setTempBasal(0, 0, profile, rT, currenttemp) + return setTempBasal(0, 0, profile, rT, currenttemp) } if (eventualBG < min_bg) { @@ -1127,7 +1093,7 @@ const determine_basal = function determine_basal( // if naive_eventualBG < 40, set a 30m zero temp (oref0-pump-loop will let any longer SMB zero temp run) if (naive_eventualBG < 40) { rT.reason += ', naive_eventualBG < 40. ' - return tempBasalFunctions.setTempBasal(0, 30, profile, rT, currenttemp) + return setTempBasal(0, 30, profile, rT, currenttemp) } if (glucose_status.delta > minDelta) { rT.reason += `, but Delta ${convert_bg(Number(tick), profile)} > expectedDelta ${convert_bg( @@ -1142,7 +1108,7 @@ const determine_basal = function determine_basal( return rT } else { rT.reason += `; setting current basal of ${basal} as temp. ` - return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp) + return setTempBasal(basal, 30, profile, rT, currenttemp) } } @@ -1170,7 +1136,7 @@ const determine_basal = function determine_basal( const minInsulinReq = Math.min(insulinReq, naiveInsulinReq) if (insulinScheduled < minInsulinReq - basal * 0.3) { rT.reason += `, ${currenttemp.duration}m@${currenttemp.rate.toFixed(2)} is a lot less than needed. ` - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp) + return setTempBasal(rate, 30, profile, rT, currenttemp) } if (typeof currenttemp.rate !== 'undefined' && currenttemp.duration > 5 && rate >= currenttemp.rate * 0.8) { rT.reason += `, temp ${currenttemp.rate} ~< req ${rate}U/hr. ` @@ -1191,12 +1157,12 @@ const determine_basal = function determine_basal( //console.error(durationReq); if (durationReq > 0) { rT.reason += `, setting ${durationReq}m zero temp. ` - return tempBasalFunctions.setTempBasal(rate, durationReq, profile, rT, currenttemp) + return setTempBasal(rate, durationReq, profile, rT, currenttemp) } } else { rT.reason += `, setting ${rate}U/hr. ` } - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp) + return setTempBasal(rate, 30, profile, rT, currenttemp) } } @@ -1220,7 +1186,7 @@ const determine_basal = function determine_basal( return rT } else { rT.reason += `; setting current basal of ${basal} as temp. ` - return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp) + return setTempBasal(basal, 30, profile, rT, currenttemp) } } } @@ -1234,7 +1200,7 @@ const determine_basal = function determine_basal( return rT } else { rT.reason += `; setting current basal of ${basal} as temp. ` - return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp) + return setTempBasal(basal, 30, profile, rT, currenttemp) } } } @@ -1251,7 +1217,7 @@ const determine_basal = function determine_basal( return rT } else { rT.reason += `; setting current basal of ${basal} as temp. ` - return tempBasalFunctions.setTempBasal(basal, 30, profile, rT, currenttemp) + return setTempBasal(basal, 30, profile, rT, currenttemp) } } else { // otherwise, calculate 30m high-temp required to get projected BG down to target @@ -1376,7 +1342,7 @@ const determine_basal = function determine_basal( } } - const maxSafeBasal = tempBasalFunctions.getMaxSafeBasal(profile) + const maxSafeBasal = getMaxSafeBasal(profile) if (rate > maxSafeBasal) { rT.reason += `adj. req. rate: ${rate} to maxSafeBasal: ${maxSafeBasal}, ` @@ -1389,13 +1355,13 @@ const determine_basal = function determine_basal( rT.reason += `${currenttemp.duration}m@${currenttemp.rate.toFixed( 2 )} > 2 * insulinReq. Setting temp basal of ${rate}U/hr. ` - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp) + return setTempBasal(rate, 30, profile, rT, currenttemp) } if (typeof currenttemp.duration === 'undefined' || currenttemp.duration === 0) { // no temp is set rT.reason += `no temp, setting ${rate}U/hr. ` - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp) + return setTempBasal(rate, 30, profile, rT, currenttemp) } if (currenttemp.duration > 5 && round_basal(rate, profile) <= round_basal(currenttemp.rate, profile)) { @@ -1406,9 +1372,8 @@ const determine_basal = function determine_basal( // required temp > existing temp basal rT.reason += `temp ${currenttemp.rate}<${rate}U/hr. ` - return tempBasalFunctions.setTempBasal(rate, 30, profile, rT, currenttemp) + return setTempBasal(rate, 30, profile, rT, currenttemp) } } -export default determine_basal -module.exports = determine_basal +export default module.exports = generate diff --git a/lib/glucose-get-last.ts b/lib/glucose-get-last.ts index 0bd0d47cf..d0b623bc3 100644 --- a/lib/glucose-get-last.ts +++ b/lib/glucose-get-last.ts @@ -1,4 +1,6 @@ -import { getDate, type GlucoseEntry } from './types/GlucoseEntry' +import { Schema } from '@effect/schema' +import { getDate, getGlucose, GlucoseEntry } from './types/GlucoseEntry' +import type { LastGlucose } from './types/LastGlucose' function getDateFromEntry(entry: GlucoseEntry) { const date = getDate(entry) @@ -10,11 +12,16 @@ function getDateFromEntry(entry: GlucoseEntry) { return date } -const getLastGlucose = function (input: GlucoseEntry[]) { +function generate(input: unknown) { + const glucoseData = Schema.decodeUnknownSync(Schema.Array(GlucoseEntry))(input) + return getLastGlucose(glucoseData) +} + +export const getLastGlucose = function (input: ReadonlyArray): LastGlucose { const data = input.reduce( (b, a) => { - const glucose = a.glucose || a.sgv - return glucose ? [...b, { ...a, glucose }] : b + const glucose = getGlucose(a) + return [...b, { ...a, glucose }] }, [] as (GlucoseEntry & { glucose: number })[] ) @@ -106,5 +113,4 @@ const getLastGlucose = function (input: GlucoseEntry[]) { } } -export default getLastGlucose -module.exports = getLastGlucose +export default module.exports = generate diff --git a/lib/glucose-stats.ts b/lib/glucose-stats.ts index bc83164c4..bc0ec73fa 100644 --- a/lib/glucose-stats.ts +++ b/lib/glucose-stats.ts @@ -14,8 +14,6 @@ // real readDate -- milliseconds since Epoch // },... -import { updateGlucoseStats } from './calc-glucose-stats' - // ] const calcNoise = (sgvArr: any) => { let noise = 0 @@ -243,11 +241,3 @@ export function NSNoiseString(nsNoise: any) { return 'Unknown' } } - -exports = module.exports = { - updateGlucoseStats, - calcSensorNoise, - calcTrend, - calcNSNoise, - NSNoiseString, -} diff --git a/lib/iob/Input.ts b/lib/iob/Input.ts new file mode 100644 index 000000000..726fba80a --- /dev/null +++ b/lib/iob/Input.ts @@ -0,0 +1,17 @@ +import { Schema } from '@effect/schema' +import { Autosens } from '../types/Autosens' +import { NightscoutTreatment } from '../types/NightscoutTreatment' +import { Profile } from '../types/Profile' +import { PumpHistoryEvent } from '../types/PumpHistoryEvent' + +export const Input = Schema.Struct({ + history: Schema.Array(Schema.Union(NightscoutTreatment, PumpHistoryEvent)), + history24: Schema.optionalWith(Schema.Array(Schema.Union(NightscoutTreatment, PumpHistoryEvent)), { + nullable: true, + }), + profile: Profile, + autosens: Schema.optionalWith(Autosens, { nullable: true }), + clock: Schema.optionalWith(Schema.String, { nullable: true }), +}) + +export type Input = typeof Input.Type diff --git a/lib/iob/InsulinTreatment.ts b/lib/iob/InsulinTreatment.ts index f5736b8b4..25ddfaa24 100644 --- a/lib/iob/InsulinTreatment.ts +++ b/lib/iob/InsulinTreatment.ts @@ -1,3 +1,13 @@ +import { Schema } from '@effect/schema' + +const BasalTreatment = Schema.Struct({ + timestamp: Schema.String, + started_at: Schema.ValidDateFromSelf, + date: Schema.Number, + rate: Schema.Number, + duration: Schema.Number, +}) + export interface BasalTreatment { timestamp: string started_at: Date diff --git a/lib/iob/calculate.ts b/lib/iob/calculate.ts index fe0d58d4d..99f288e78 100644 --- a/lib/iob/calculate.ts +++ b/lib/iob/calculate.ts @@ -146,5 +146,3 @@ function iobCalcExponential(treatment: BolusTreatment, minsAgo: number, dia: num iobContrib: iobContrib, } } - -exports = module.exports = iobCalc diff --git a/lib/iob/history.ts b/lib/iob/history.ts index 5d0356d29..6f80899a8 100644 --- a/lib/iob/history.ts +++ b/lib/iob/history.ts @@ -2,10 +2,9 @@ import { Schema } from '@effect/schema' import { tz } from '../date' import * as date from '../date' import * as basalprofile from '../profile/basal' -import type { Autosens } from '../types/Autosens' import { NightscoutTreatment } from '../types/NightscoutTreatment' -import type { Profile } from '../types/Profile' import { PumpHistoryEvent } from '../types/PumpHistoryEvent' +import { Input } from './Input' import type { BasalTreatment, BolusTreatment, InsulinTreatment } from './InsulinTreatment' interface Splitter { @@ -20,14 +19,6 @@ interface PumpSuspendResume { duration: number } -export interface Input { - history: Array - history24?: Array - profile: Profile - autosens?: Autosens - clock?: string -} - function splitTimespanWithOneSplitter(event: BasalTreatment, splitter: Splitter) { if (splitter.type !== 'recurring') { return [event] @@ -206,7 +197,12 @@ function splitAroundSuspends( return events } -export default function calcTempTreatments(inputs: Input, zeroTempDuration?: number): InsulinTreatment[] { +export default function generate(input: unknown, zeroTempDuration?: number) { + const inputs = Schema.decodeUnknownSync(Input)(input) + return findInsulin(inputs, zeroTempDuration) +} + +export function findInsulin(inputs: Input, zeroTempDuration?: number): InsulinTreatment[] { const pumpHistory = [...inputs.history, ...(inputs.history24 || [])] const profile_data = inputs.profile const autosens_data = inputs.autosens @@ -660,4 +656,4 @@ export default function calcTempTreatments(inputs: Input, zeroTempDuration?: num return all_data.sort((a, b) => a.date - b.date) } -exports = module.exports = calcTempTreatments +//exports = module.exports = generate diff --git a/lib/iob/index.ts b/lib/iob/index.ts index 5b04ea085..7542a1551 100644 --- a/lib/iob/index.ts +++ b/lib/iob/index.ts @@ -1,8 +1,9 @@ +import { Schema } from '@effect/schema' import { tz } from '../date' +import { Input } from './Input' import type { InsulinTreatment } from './InsulinTreatment' import { isBasalTreatment, isBolusTreatment } from './InsulinTreatment' -import find_insulin from './history' -import type { Input } from './history' +import { findInsulin } from './history' import sum from './total' interface IOB { @@ -24,13 +25,13 @@ interface IOBItem extends IOB { } } -export default function generate(inputs: Input, currentIOBOnly: boolean = false, inputTreatments?: InsulinTreatment[]) { +export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTreatments?: InsulinTreatment[]) => { let treatmentsWithZeroTemp: InsulinTreatment[] = [] let treatments = inputTreatments if (!treatments) { - treatments = find_insulin(inputs) + treatments = findInsulin(inputs) // calculate IOB based on continuous future zero temping as well - treatmentsWithZeroTemp = find_insulin(inputs, 240) + treatmentsWithZeroTemp = findInsulin(inputs, 240) } //console.error(treatments.length, treatmentsWithZeroTemp.length); //console.error(treatments[treatments.length-1], treatmentsWithZeroTemp[treatmentsWithZeroTemp.length-1]) @@ -111,4 +112,7 @@ export default function generate(inputs: Input, currentIOBOnly: boolean = false, return iobArray } -exports = module.exports = generate +export default function generate(input: unknown) { + const inputs = Schema.decodeUnknownSync(Input)(input) + return getIob(inputs) +} diff --git a/lib/iob/total.ts b/lib/iob/total.ts index 0457b96de..08daf8c73 100644 --- a/lib/iob/total.ts +++ b/lib/iob/total.ts @@ -123,5 +123,3 @@ export default function iobTotal(opts: Options, time: Date) { time: time, } } - -exports = module.exports = iobTotal diff --git a/lib/meal/RecentCarbs.ts b/lib/meal/RecentCarbs.ts new file mode 100644 index 000000000..6e01a92a6 --- /dev/null +++ b/lib/meal/RecentCarbs.ts @@ -0,0 +1,19 @@ +import { Schema } from '@effect/schema' + +export const RecentCarbs = Schema.Struct({ + carbs: Schema.optionalWith(Schema.Number, { default: () => 0 }), + nsCarbs: Schema.optionalWith(Schema.Number, { default: () => 0 }), + bwCarbs: Schema.optionalWith(Schema.Number, { default: () => 0 }), + journalCarbs: Schema.optionalWith(Schema.Number, { default: () => 0 }), + mealCOB: Schema.optionalWith(Schema.Number, { default: () => 0 }), + currentDeviation: Schema.optionalWith(Schema.Number, { default: () => 0 }), + maxDeviation: Schema.optionalWith(Schema.Number, { default: () => 0 }), + minDeviation: Schema.optionalWith(Schema.Number, { default: () => 0 }), + slopeFromMaxDeviation: Schema.optionalWith(Schema.Number, { default: () => 0 }), + slopeFromMinDeviation: Schema.optionalWith(Schema.Number, { default: () => 0 }), + allDeviations: Schema.optionalWith(Schema.Array(Schema.Number), { default: () => [] }), + lastCarbTime: Schema.optionalWith(Schema.Number, { default: () => 0 }), + bwFound: Schema.optionalWith(Schema.Boolean, { default: () => false }), +}) + +export type RecentCarbs = typeof RecentCarbs.Type diff --git a/lib/meal/history.ts b/lib/meal/history.ts index e2ae49d42..23e199eaf 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -3,19 +3,15 @@ import { Schema } from '@effect/schema' import { dedupeWith, sort } from 'effect/Array' import { struct, strict } from 'effect/Equivalence' +import type { CarbEntry } from '../types/CarbEntry' import { NightscoutTreatment } from '../types/NightscoutTreatment' import { PumpHistoryEvent } from '../types/PumpHistoryEvent' import type { MealTreatment } from './MealTreatment' import { Order } from './MealTreatment' -export interface CarbEntry { - carbs?: number - created_at?: string -} - export interface Input { - history: Array - carbs: CarbEntry[] + history: ReadonlyArray + carbs: ReadonlyArray } interface TempMealTreatment extends MealTreatment { @@ -65,13 +61,12 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { bolusWizardInputs.push(current) } else if ( Schema.is(NightscoutTreatment)(current) && - current.created_at && (current.eventType === 'Meal Bolus' || current.eventType === 'Correction Bolus' || current.eventType === 'Snack Bolus' || current.eventType === 'Bolus Wizard' || current.eventType === 'Carb Correction') && - current.carbs + current.carbs !== undefined ) { //imports carbs entered through Nightscout Care Portal //"Bolus Wizard" refers to the Nightscout Bolus Wizard, not the Medtronic Bolus Wizard @@ -84,7 +79,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { nsCarbs: current.carbs, }) ) - } else if (Schema.is(NightscoutTreatment)(current) && current.enteredBy === 'xdrip' && current.created_at) { + } else if (Schema.is(NightscoutTreatment)(current) && current.enteredBy === 'xdrip') { mealInputs.push( createMeal(current.created_at, { carbs: current.carbs || 0, @@ -92,12 +87,7 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { bolus: current.insulin || 0, }) ) - } else if ( - Schema.is(NightscoutTreatment)(current) && - current.carbs && - current.carbs > 0 && - current.created_at - ) { + } else if (Schema.is(NightscoutTreatment)(current) && current.carbs && current.carbs > 0) { mealInputs.push( createMeal(current.created_at, { carbs: current.carbs || 0, @@ -155,5 +145,3 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { }) return sort(dedupeWith(mealInputs, eq), Order) } - -exports = module.exports = findMealInputs diff --git a/lib/meal/index.ts b/lib/meal/index.ts index d2e957cce..b69aefe4c 100644 --- a/lib/meal/index.ts +++ b/lib/meal/index.ts @@ -1,23 +1,25 @@ +import { Schema } from '@effect/schema' import { tz } from '../date' -import type { BasalSchedule } from '../types/BasalSchedule' -import type { GlucoseEntry } from '../types/GlucoseEntry' -import type { NightscoutTreatment } from '../types/NightscoutTreatment' -import type { Profile } from '../types/Profile' -import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' -import type { CarbEntry } from './history' +import { BasalSchedule } from '../types/BasalSchedule' +import { CarbEntry } from '../types/CarbEntry' +import { GlucoseEntry } from '../types/GlucoseEntry' +import { NightscoutTreatment } from '../types/NightscoutTreatment' +import { Profile } from '../types/Profile' +import { PumpHistoryEvent } from '../types/PumpHistoryEvent' import find_meals from './history' import sum from './total' -interface Input { - history: Array - carbs: CarbEntry[] - profile: Profile - basalprofile?: BasalSchedule[] - glucose?: GlucoseEntry[] - clock: string -} +const Input = Schema.Struct({ + history: Schema.Array(Schema.Union(NightscoutTreatment, PumpHistoryEvent)), + carbs: Schema.Array(CarbEntry), + profile: Profile, + basalprofile: Schema.optionalWith(Schema.Array(BasalSchedule), { nullable: true }), + glucose: Schema.optionalWith(Schema.Array(GlucoseEntry), { nullable: true }), + clock: Schema.String, +}) -export default function generate(inputs: Input) { +function generate(input: unknown) { + const inputs = Schema.decodeUnknownSync(Input)(input) const treatments = find_meals(inputs) const opts = { @@ -34,4 +36,4 @@ export default function generate(inputs: Input) { return /* meal_data */ sum(opts, clock) } -exports = module.exports = generate +export default module.exports = generate diff --git a/lib/meal/total.ts b/lib/meal/total.ts index 0223f6cb7..44d8ab33b 100644 --- a/lib/meal/total.ts +++ b/lib/meal/total.ts @@ -1,3 +1,4 @@ +import * as A from 'effect/Array' import { tz } from '../date' import type { DetectCOBInput } from '../determine-basal/cob' import detectCarbAbsorption from '../determine-basal/cob' @@ -6,35 +7,20 @@ import type { GlucoseEntry } from '../types/GlucoseEntry' import type { NightscoutTreatment } from '../types/NightscoutTreatment' import type { Profile } from '../types/Profile' import type { PumpHistoryEvent } from '../types/PumpHistoryEvent' -import type { MealTreatment } from './MealTreatment' +import * as MealTreatment from './MealTreatment' +import type { RecentCarbs } from './RecentCarbs' export interface Options { - treatments?: Array - pumphistory: Array + treatments?: ReadonlyArray + pumphistory: ReadonlyArray profile: Profile - basalprofile?: BasalSchedule[] - glucose?: GlucoseEntry[] + basalprofile?: ReadonlyArray + glucose?: ReadonlyArray clock: string } -export interface RecentCarbs { - carbs: number - nsCarbs: number - bwCarbs: number - journalCarbs: number - mealCOB: number - currentDeviation: number - maxDeviation: number - minDeviation: number - slopeFromMaxDeviation: number - slopeFromMinDeviation: number - allDeviations: number[] - lastCarbTime: number - bwFound: boolean -} - -export default function recentCarbs(opts: Options, time: Date): Partial { - const treatments = opts.treatments +export default function recentCarbs(opts: Options, time: Date): RecentCarbs { + let treatments = opts.treatments const profile_data = opts.profile const glucose_data = opts.glucose let carbs = 0 @@ -46,7 +32,21 @@ export default function recentCarbs(opts: Options, time: Date): Partial { - const aDate = new Date(a.timestamp) - const bDate = new Date(b.timestamp) - //console.error(aDate); - return bDate.getTime() - aDate.getTime() - }) + treatments = A.sort(treatments, MealTreatment.Order) let carbsToRemove = 0 let nsCarbsToRemove = 0 @@ -179,5 +174,3 @@ export default function recentCarbs(opts: Options, time: Date): Partial new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ) + temptargets_data = A.sort(temptargets_data, TempTarget.Order) for (let i = 0; i < temptargets_data.length; i++) { const start = new Date(temptargets_data[i].created_at) @@ -101,8 +101,3 @@ export function bound_target_range(target: BgTarget) { min_bg: Math.min(200, Math.max(80, target.low)), } } - -bgTargetsLookup.bgTargetsLookup = bgTargetsLookup // does use log -bgTargetsLookup.lookup = lookup // not used outside -bgTargetsLookup.bound_target_range = bound_target_range // does not log -exports = module.exports = bgTargetsLookup diff --git a/lib/require-utils.ts b/lib/require-utils.ts index 525e06e71..5c77b7810 100644 --- a/lib/require-utils.ts +++ b/lib/require-utils.ts @@ -1,6 +1,6 @@ import * as fs from 'fs' -function safeRequire(path: string) { +export function safeRequire(path: string) { let resolved try { @@ -13,7 +13,7 @@ function safeRequire(path: string) { return resolved } -function safeLoadFile(path: string) { +export function safeLoadFile(path: string) { let resolved try { @@ -25,7 +25,7 @@ function safeLoadFile(path: string) { return resolved } -function requireWithTimestamp(path: string) { +export function requireWithTimestamp(path: string) { const resolved = safeLoadFile(path) if (resolved) { @@ -70,9 +70,3 @@ if (!module.parent) { console.log('test failed') } } - -module.exports = { - safeRequire: safeRequire, - requireWithTimestamp: requireWithTimestamp, - safeLoadFile: safeLoadFile, -} diff --git a/lib/round-basal.ts b/lib/round-basal.ts index 3a65eb147..c2f8805a1 100644 --- a/lib/round-basal.ts +++ b/lib/round-basal.ts @@ -1,6 +1,6 @@ import type { Profile } from './types/Profile' -const round_basal = (basal: number, profile?: Profile) => { +export const round_basal = (basal: number, profile?: Profile) => { /* x23 and x54 pumps change basal increment depending on how much basal is being delivered: 0.025u for 0.025 < x < 0.975 0.05u for 1 < x < 9.95 @@ -28,4 +28,3 @@ const round_basal = (basal: number, profile?: Profile) => { } export default round_basal -exports = module.exports = round_basal diff --git a/lib/types/Autosens.ts b/lib/types/Autosens.ts index 1d3169ed9..2292b2ecd 100644 --- a/lib/types/Autosens.ts +++ b/lib/types/Autosens.ts @@ -1,4 +1,5 @@ import { Schema } from '@effect/schema' +import * as O from 'effect/Order' export const Autosens = Schema.Struct({ timestamp: Schema.String, @@ -7,3 +8,7 @@ export const Autosens = Schema.Struct({ }) export type Autosens = typeof Autosens.Type + +export const Order: O.Order = O.struct({ + timestamp: (a, b) => O.Date(new Date(a.timestamp), new Date(b.timestamp)), +}) diff --git a/lib/types/BasalSchedule.ts b/lib/types/BasalSchedule.ts index dc71d5b4b..808e11628 100644 --- a/lib/types/BasalSchedule.ts +++ b/lib/types/BasalSchedule.ts @@ -1,20 +1,25 @@ import { Schema } from '@effect/schema' import * as O from 'effect/Order' -import { PositiveInt } from './PositiveInt' -import { ScheduleStart } from './ScheduleStart' +import * as ScheduleStart from './ScheduleStart' export const BasalSchedule = Schema.Struct({ - i: Schema.optionalWith(Schema.Int, { exact: true }), - start: Schema.optional(ScheduleStart), - minutes: PositiveInt, - rate: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), + i: Schema.optional(Schema.Int), + start: Schema.optional(ScheduleStart.ScheduleStart), + minutes: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), + // @todo: why can't be zero? I think a basal rate of 0 should be possibile + rate: Schema.Positive, }) export type BasalSchedule = typeof BasalSchedule.Type -export const Order: O.Order = O.combineAll([ - O.make((a, b) => O.number(Number(a.i), Number(b.i))), - O.struct({ - //start: ScheduleStartOrder, - }), -]) +const OrderByI = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : O.number(a, b)), + a => a.i +) + +const OrderByStart = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : ScheduleStart.Order(a, b)), + a => a.start +) + +export const Order: O.Order = O.combineAll([OrderByI, OrderByStart]) diff --git a/lib/types/CarbEntry.ts b/lib/types/CarbEntry.ts new file mode 100644 index 000000000..ec1436aa7 --- /dev/null +++ b/lib/types/CarbEntry.ts @@ -0,0 +1,12 @@ +import { Schema } from '@effect/schema' +import * as O from 'effect/Order' + +export const CarbEntry = Schema.Struct({ + carbs: Schema.optional(Schema.Number), + created_at: Schema.optional(Schema.NonEmptyString), +}) + +export type CarbEntry = typeof CarbEntry.Type + +export const Order: O.Order = (a, b) => + a.created_at && b.created_at ? O.Date(new Date(a.created_at), new Date(a.created_at)) : 0 diff --git a/lib/types/CarbRatioSchedule.ts b/lib/types/CarbRatioSchedule.ts index 9fb29824b..f8366d805 100644 --- a/lib/types/CarbRatioSchedule.ts +++ b/lib/types/CarbRatioSchedule.ts @@ -1,14 +1,24 @@ import { Schema } from '@effect/schema' import * as O from 'effect/Order' -import { ScheduleStart } from './ScheduleStart' +import * as ScheduleStart from './ScheduleStart' export const CarbRatioSchedule = Schema.Struct({ - i: Schema.optionalWith(Schema.Int, { exact: true }), - start: Schema.optional(ScheduleStart), + i: Schema.optional(Schema.Int), + start: Schema.optional(ScheduleStart.ScheduleStart), offset: Schema.Number, ratio: Schema.Number, }) export type CarbRatioSchedule = typeof CarbRatioSchedule.Type -export const Order: O.Order = O.make((a, b) => O.number(Number(a.i), Number(b.i))) +const OrderByI = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : O.number(a, b)), + a => a.i +) + +const OrderByStart = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : ScheduleStart.Order(a, b)), + a => a.start +) + +export const Order: O.Order = O.combineAll([OrderByI, OrderByStart]) diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts index 77e78518d..fb1ea9cf1 100644 --- a/lib/types/GlucoseEntry.ts +++ b/lib/types/GlucoseEntry.ts @@ -1,7 +1,7 @@ import { Schema } from '@effect/schema' -import { Positive } from '@effect/schema/Schema' -import { identity, String } from 'effect' -import { PositiveInt } from './PositiveInt' +import { flow, identity, String } from 'effect' +import * as E from 'effect/Either' +import * as O from 'effect/Order' export const GlucoseType = Schema.Union(Schema.Literal('sgv', 'cal'), Schema.String) export type GlucoseType = typeof GlucoseType.Type @@ -13,28 +13,45 @@ const DateFromDisplayTime = Schema.String.pipe( encode: identity, }) ) -const DateNumber = Schema.Number.pipe(Schema.filter(s => Schema.is(Schema.DateFromNumber)(s) || 'invalid Date number')) -const DisplayTime = Schema.String.pipe(Schema.filter(s => Schema.is(DateFromDisplayTime)(s) || 'invalid Display Time')) -const DateString = Schema.String.pipe(Schema.filter(s => Schema.is(Schema.DateFromString)(s) || 'invalid Date string')) - -export const GlucoseEntry = Schema.Struct({ - date: Schema.optional(DateNumber), - display_time: Schema.optional(DisplayTime), - dateString: Schema.optional(DateString), - sgv: Schema.optional(PositiveInt), - glucose: Schema.optional(Positive), - type: Schema.optional(GlucoseType), - device: Schema.optional(Schema.String), - noise: Schema.optional(Schema.Number), - xDrip_started_at: Schema.optional(Schema.Unknown), -}).annotations({ + +const validParsableDate = (decoder: Schema.Schema) => + Schema.filter>(flow(Schema.decodeEither(decoder.pipe(Schema.validDate())), E.isRight), { + title: 'ParsableDate', + }) + +const DisplayTime = Schema.String.pipe(validParsableDate(DateFromDisplayTime)) +const DateString = Schema.String.pipe(validParsableDate(Schema.DateFromString)) + +const GlucoseField = Schema.Struct({ + glucose: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), +}) +const NightscoutSgvField = Schema.Struct({ + sgv: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), +}) + +export const GlucoseEntry = Schema.extend( + Schema.Union(GlucoseField, NightscoutSgvField), + Schema.Struct({ + date: Schema.optional(Schema.Int.pipe(validParsableDate(Schema.DateFromNumber))), + display_time: Schema.optional(DisplayTime), + dateString: Schema.optional(DateString), + //sgv: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + //glucose: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + type: Schema.optional(GlucoseType), + device: Schema.optional(Schema.String), + noise: Schema.optional(Schema.Number), + xDrip_started_at: Schema.optional(Schema.Unknown), + }) +).annotations({ title: 'GlucoseEntry', description: 'Glucose Entry', }) export type GlucoseEntry = typeof GlucoseEntry.Type -export const getDate = (entry: GlucoseEntry): Date | undefined => { +export const getGlucose = (entry: GlucoseEntry) => (Schema.is(GlucoseField)(entry) ? entry.glucose : entry.sgv) + +export const getDate = (entry: GlucoseEntry): Date => { if (entry.date) { return Schema.decodeSync(Schema.DateFromNumber)(entry.date) } else if (entry.dateString) { @@ -43,5 +60,22 @@ export const getDate = (entry: GlucoseEntry): Date | undefined => { return Schema.decodeSync(DateFromDisplayTime)(entry.display_time) } - return undefined + throw new Error('Unable to get a valid glucose entry date') } + +const OrderByDate = O.mapInput( + (a, b) => (!a || !b ? 0 : O.Date(a, b)), + a => (a.date ? Schema.decodeSync(Schema.DateFromNumber)(a.date) : undefined) +) + +const OrderByDateString = O.mapInput( + (a, b) => (!a || !b ? 0 : O.Date(a, b)), + a => (a.dateString ? Schema.decodeSync(Schema.DateFromString)(a.dateString) : undefined) +) + +const OrderByDisplayTime = O.mapInput( + (a, b) => (!a || !b ? 0 : O.Date(a, b)), + a => (a.display_time ? Schema.decodeSync(DateFromDisplayTime)(a.display_time) : undefined) +) + +export const Order: O.Order = O.combineAll([OrderByDate, OrderByDateString, OrderByDisplayTime]) diff --git a/lib/types/IOB.ts b/lib/types/IOB.ts new file mode 100644 index 000000000..205a8a0a3 --- /dev/null +++ b/lib/types/IOB.ts @@ -0,0 +1,28 @@ +import { Schema } from '@effect/schema' + +const SimpleIOB = Schema.Struct({ + iob: Schema.Number, + activity: Schema.Number, + basaliob: Schema.Number, + bolusiob: Schema.Number, + netbasalinsulin: Schema.Number, + bolusinsulin: Schema.Number, + time: Schema.String, +}) + +const LastTemp = Schema.Struct({ + rate: Schema.Number, + timestamp: Schema.String, + started_at: Schema.String, + date: Schema.Number, + duration: Schema.Number, +}) + +export const IOB = Schema.Struct({ + ...SimpleIOB.fields, + iobWithZeroTemp: SimpleIOB, + lastBolusTime: Schema.optional(Schema.Int), + lastTemp: Schema.optional(LastTemp), +}) + +export type IOB = typeof IOB.Type diff --git a/lib/types/ISFSensitivity.ts b/lib/types/ISFSensitivity.ts index 32161c19f..de1c6e94e 100644 --- a/lib/types/ISFSensitivity.ts +++ b/lib/types/ISFSensitivity.ts @@ -1,21 +1,30 @@ import { Schema } from '@effect/schema' import * as O from 'effect/Order' -import { ScheduleStart } from './ScheduleStart' +import * as ScheduleStart from './ScheduleStart' export const ISFSensitivity = Schema.Struct({ offset: Schema.Number, endOffset: Schema.optional(Schema.Number), sensitivity: Schema.Number, i: Schema.optional(Schema.Number), - start: Schema.optional(ScheduleStart), + start: Schema.optional(ScheduleStart.ScheduleStart), x: Schema.optional(Schema.Number), }) export type ISFSensitivity = typeof ISFSensitivity.Type -export const Order: O.Order = O.combineAll([ - O.make((a, b) => O.number(Number(a), Number(b))), - O.struct({ - //start: ScheduleStartOrder, - }), -]) +const OrderByOffset: O.Order = O.struct({ + offset: O.number, +}) + +const OrderByI = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : O.number(a, b)), + a => a.i +) + +const OrderByStart = O.mapInput( + (a, b) => (a === undefined || b === undefined ? 0 : ScheduleStart.Order(a, b)), + a => a.start +) + +export const Order: O.Order = O.combineAll([OrderByOffset, OrderByI, OrderByStart]) diff --git a/lib/types/LastGlucose.ts b/lib/types/LastGlucose.ts new file mode 100644 index 000000000..5aa6304bf --- /dev/null +++ b/lib/types/LastGlucose.ts @@ -0,0 +1,14 @@ +import { Schema } from '@effect/schema' + +export const LastGlucose = Schema.Struct({ + delta: Schema.Number, + glucose: Schema.Number, + noise: Schema.Number, + short_avgdelta: Schema.Number, + long_avgdelta: Schema.Number, + date: Schema.Number, + last_cal: Schema.Number, + device: Schema.optional(Schema.String), +}) + +export type LastGlucose = typeof LastGlucose.Type diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts index 3b84be2c3..8bc641247 100644 --- a/lib/types/NightscoutTreatment.ts +++ b/lib/types/NightscoutTreatment.ts @@ -1,34 +1,37 @@ import { Schema } from '@effect/schema' +import * as O from 'effect/Order' import { EventType } from './EventType' import { PumpHistoryEvent } from './PumpHistoryEvent' export const NightscoutTreatment = Schema.Struct({ eventType: EventType, created_at: Schema.String, - id: Schema.optional(Schema.String), - duration: Schema.optional(Schema.Number), - rawDuration: Schema.optional(PumpHistoryEvent), - rawRate: Schema.optional(PumpHistoryEvent), - absolute: Schema.optional(Schema.Number), - rate: Schema.optional(Schema.Number), - enteredBy: Schema.optional(Schema.String), - bolus: Schema.optional(PumpHistoryEvent), + id: Schema.optionalWith(Schema.String, { nullable: true }), + duration: Schema.optionalWith(Schema.Number, { nullable: true }), + rawDuration: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), + rawRate: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), + absolute: Schema.optionalWith(Schema.Number, { nullable: true }), + rate: Schema.optionalWith(Schema.Number, { nullable: true }), + enteredBy: Schema.optionalWith(Schema.String, { nullable: true }), + bolus: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), insulin: Schema.optionalWith(Schema.Number, { nullable: true }), - notes: Schema.optional(Schema.String), + notes: Schema.optionalWith(Schema.String, { nullable: true }), carbs: Schema.optionalWith(Schema.Number, { nullable: true }), - fat: Schema.optional(Schema.Number), - protein: Schema.optional(Schema.Number), - foodType: Schema.optional(Schema.String), - targetTop: Schema.optional(Schema.Number), - targetBottom: Schema.optional(Schema.Number), - glucoseType: Schema.optional(Schema.String), - glucose: Schema.optional(Schema.Number), - units: Schema.optional(Schema.String), - fpuID: Schema.optional(Schema.String), - amount: Schema.optional(Schema.Number), + fat: Schema.optionalWith(Schema.Number, { nullable: true }), + protein: Schema.optionalWith(Schema.Number, { nullable: true }), + foodType: Schema.optionalWith(Schema.String, { nullable: true }), + targetTop: Schema.optionalWith(Schema.Number, { nullable: true }), + targetBottom: Schema.optionalWith(Schema.Number, { nullable: true }), + glucoseType: Schema.optionalWith(Schema.String, { nullable: true }), + glucose: Schema.optionalWith(Schema.Number, { nullable: true }), + units: Schema.optionalWith(Schema.String, { nullable: true }), + fpuID: Schema.optionalWith(Schema.String, { nullable: true }), + amount: Schema.optionalWith(Schema.Number, { nullable: true }), }).annotations({ identifier: 'NightscoutTreatment', title: 'Nightscout Treatment', }) export type NightscoutTreatment = typeof NightscoutTreatment.Type + +export const Order: O.Order = O.mapInput(O.Date, a => new Date(a.created_at)) diff --git a/lib/types/PositiveInt.ts b/lib/types/PositiveInt.ts deleted file mode 100644 index e96894f55..000000000 --- a/lib/types/PositiveInt.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Schema } from '@effect/schema' -import { PositiveNumber } from './PositiveNumber' - -export const PositiveIntBrand = Symbol.for('PositiveInt') -export const PositiveInt = PositiveNumber.pipe(Schema.brand(PositiveIntBrand)).annotations({ - identifier: 'PositiveInt', - title: 'PositiveInt', -}) -export type PositiveInt = typeof PositiveInt.Type diff --git a/lib/types/PositiveNumber.ts b/lib/types/PositiveNumber.ts deleted file mode 100644 index 3cb688d5d..000000000 --- a/lib/types/PositiveNumber.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Schema } from '@effect/schema' - -export const PositiveNumberBrand = Symbol.for('PositiveNumber') -export const PositiveNumber = Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)) - .pipe(Schema.brand(PositiveNumberBrand)) - .annotations({ - identifier: 'PositiveInt', - title: 'PositiveInt', - }) -export type PositiveNumber = typeof PositiveNumber.Type diff --git a/lib/types/PumpHistoryEvent.ts b/lib/types/PumpHistoryEvent.ts index 941bacc53..570234f73 100644 --- a/lib/types/PumpHistoryEvent.ts +++ b/lib/types/PumpHistoryEvent.ts @@ -1,4 +1,5 @@ import { Schema } from '@effect/schema' +import * as O from 'effect/Order' import { EventType } from './EventType' export const TempType = Schema.Literal('absolute', 'percent') @@ -21,3 +22,5 @@ export const PumpHistoryEvent = Schema.Struct({ }) export type PumpHistoryEvent = typeof PumpHistoryEvent.Type + +export const Order: O.Order = O.mapInput(O.Date, a => new Date(a.timestamp)) diff --git a/lib/types/ScheduleStart.ts b/lib/types/ScheduleStart.ts index 86e51de45..6bd614daf 100644 --- a/lib/types/ScheduleStart.ts +++ b/lib/types/ScheduleStart.ts @@ -1,14 +1,10 @@ import { Schema } from '@effect/schema' import * as O from 'effect/Order' -export const ScheduleStartBrand = Symbol.for('ScheduleStart') - const pattern = '^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$' -export const ScheduleStart = Schema.String.pipe(Schema.pattern(new RegExp(pattern))) - .pipe(Schema.brand(ScheduleStartBrand)) - .annotations({ - description: 'Time in HH:MM format', - }) +export const ScheduleStart = Schema.String.pipe(Schema.pattern(new RegExp(pattern))).annotations({ + description: 'Time in HH:MM:SS format', +}) /** * Time in HH:MM diff --git a/lib/types/TempBasal.ts b/lib/types/TempBasal.ts new file mode 100644 index 000000000..5359bbac5 --- /dev/null +++ b/lib/types/TempBasal.ts @@ -0,0 +1,9 @@ +import { Schema } from '@effect/schema' + +export const TempBasal = Schema.Struct({ + duration: Schema.Int, + temp: Schema.Literal('absolute', 'percent'), + rate: Schema.Number, +}) + +export type TempBasal = typeof TempBasal.Type diff --git a/lib/types/TempTarget.ts b/lib/types/TempTarget.ts index c54c60e57..75f429f61 100644 --- a/lib/types/TempTarget.ts +++ b/lib/types/TempTarget.ts @@ -1,6 +1,8 @@ import { Schema } from '@effect/schema' +import * as O from 'effect/Order' export const TempTarget = Schema.Struct({ + // @todo: should we just use strings? created_at: Schema.Union(Schema.DateFromSelf, Schema.String.pipe(Schema.nonEmptyString())), duration: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), targetTop: Schema.Int, @@ -13,3 +15,7 @@ export const TempTarget = Schema.Struct({ export type TempTarget = typeof TempTarget.Type export const decode = Schema.decodeUnknownSync(TempTarget) + +export const Order: O.Order = O.mapInput(O.Date, a => + a.created_at instanceof Date ? a.created_at : new Date(a.created_at) +) diff --git a/package.json b/package.json index 7a71030f6..587f600b8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "test": "jest", "global-install": "npm install && npm run build && sudo npm link && sudo npm link oref0 && sudo npm install -g && npm install -g", "build": "tsc", - "lint": "eslint lib/" + "lint": "eslint lib/", + "watch": "tsc --watch" }, "repository": { "type": "git", diff --git a/tests/command-behavior.tests.sh b/tests/command-behavior.tests.sh index 6af334c91..64f0656d2 100755 --- a/tests/command-behavior.tests.sh +++ b/tests/command-behavior.tests.sh @@ -55,7 +55,7 @@ test-ns-status () { # Run ns-status and capture output ../bin/ns-status.js clock-zoned.json iob.json suggested.json enacted.json battery.json reservoir.json status.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "ns-status error: \n$ERROR_LINES" @@ -83,7 +83,7 @@ test-ns-status () { # Run ns-status with mmtune information and capture output ../bin/ns-status.js clock-zoned.json iob.json suggested.json enacted.json battery.json reservoir.json status.json mmtune.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "ns-status error: \n$ERROR_LINES" @@ -93,7 +93,7 @@ test-ns-status () { # Run ns-status with uploader option and capture output ../bin/ns-status.js clock-zoned.json iob.json suggested.json enacted.json battery.json reservoir.json status.json --uploader uploader.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "ns-status error: \n$ERROR_LINES" @@ -108,7 +108,7 @@ test-autotune-core () { # Run autotune-core and capture output ../bin/oref0-autotune-core.js autotune.data.json profile.json pumpprofile.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) cat stderr_output | grep -q CRTotalCarbs || fail_test "oref0-autotune-core didn't contain expected stderr output" @@ -124,10 +124,11 @@ test-autotune-prep () { # Run autotune-prep and capture output ../bin/oref0-autotune-prep.js autotune.treatments.json profile.json autotune.entries.json profile.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) - [[ $(cat stderr_output | grep mealCOB | wc -l) -eq 82 ]] || fail_test "oref0-autotune-prep didn't contain expected stderr output" + cat stdout_output + [[ $(cat stderr_output | grep mealCOB | wc -l | sed 's/^ *//') -eq 82 ]] || fail_test "oref0-autotune-prep didn't contain expected stderr output" # Make sure output has expected data cat stdout_output | jq ".CRData | first" | grep -q CRInitialBG || fail_test "oref0-autotune-prep didn't contain expected CR Data output" @@ -173,7 +174,7 @@ test-calculate-iob () { # Run calculate-iob and capture output ../bin/oref0-calculate-iob.js pumphistory_zoned.json profile.json clock-zoned.json 2>stderr_output 1>stdout_output - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "oref0-calculate-iob error: \n$ERROR_LINES" @@ -187,7 +188,7 @@ test-calculate-iob () { ../bin/oref0-calculate-iob.js pumphistory_zoned.json profile.json clock-zoned.json autosens.json 2>stderr_output 1>stdout_output # NOTE: oref0-calculate-iob doesn't print an error if autosens file is unable to be read - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "oref0-calculate-iob error: \n$ERROR_LINES" @@ -201,7 +202,7 @@ test-calculate-iob () { ../bin/oref0-calculate-iob.js pumphistory_zoned.json profile.json clock-zoned.json autosens.json pumphistory_zoned.json 2>stderr_output 1>stdout_output # NOTE: oref0-calculate-iob doesn't print an error if autosens or 24 hour pump history files are unable to be read - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "oref0-calculate-iob error: \n$ERROR_LINES" @@ -280,7 +281,7 @@ test-find-insulin-uses () { ../bin/oref0-find-insulin-uses.js pumphistory_zoned.json profile.json 2>stderr_output 1>stdout_output # Make sure stderr output contains expected string - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "find-insulin-uses error: \n$ERROR_LINES" @@ -307,7 +308,7 @@ test-get-profile () { ../bin/oref0-get-profile.js settings.json bg_targets.json insulin_sensitivities.json basal_profile.json preferences.json carb_ratios.json temptargets.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "get-profile error: \n$ERROR_LINES" @@ -319,7 +320,7 @@ test-get-profile () { ../bin/oref0-get-profile.js settings.json bg_targets.json insulin_sensitivities.json basal_profile.json preferences.json carb_ratios.json temptargets.json --model=model.json --autotune profile.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "get-profile error: \n$ERROR_LINES" @@ -336,7 +337,7 @@ test-html () { ../bin/oref0-html.js glucose.json iob.json basal_profile.json temp_basal.json suggested.json enacted.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "html error: \n$ERROR_LINES" @@ -347,7 +348,7 @@ test-html () { ../bin/oref0-html.js glucose.json iob.json basal_profile.json temp_basal.json suggested.json enacted.json meal.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "html error: \n$ERROR_LINES" @@ -364,7 +365,7 @@ test-meal () { ../bin/oref0-meal.js pumphistory_zoned.json profile.json clock-zoned.json glucose.json basal_profile.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "meal error: \n$ERROR_LINES" @@ -375,7 +376,7 @@ test-meal () { ../bin/oref0-meal.js pumphistory_zoned.json profile.json clock-zoned.json glucose.json basal_profile.json carbhistory.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "meal error: \n$ERROR_LINES" @@ -392,7 +393,7 @@ test-normalize-temps () { ../bin/oref0-normalize-temps.js pumphistory_zoned.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "normalize-temps error: \n$ERROR_LINES" @@ -408,24 +409,24 @@ test-raw () { ../bin/oref0-raw.js raw_glucose.json cal.json 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "raw error: \n$ERROR_LINES" # Make sure output has expected number of glucose values - cat stdout_output | jq ".[] | .glucose" | wc -l | grep -q "288" || fail_test "oref0-raw did not report correct number of glucose readings" + cat stdout_output | jq ".[] | .glucose" | wc -l | sed 's/^ *//' | grep -q "288" || fail_test "oref0-raw did not report correct number of glucose readings" # Run raw and capture output ../bin/oref0-raw.js raw_glucose.json cal.json 120 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "raw error: \n$ERROR_LINES" # Make sure output has expected number of glucose values above MAX_RAW - cat stdout_output | jq ".[] | .noise" | grep "3" | wc -l | grep -q 175 || fail_test "oref0-raw did not report correct glucose readings above MAX_RAW" + cat stdout_output | jq ".[] | .noise" | grep "3" | wc -l | sed 's/^ *//' | grep -q 175 || fail_test "oref0-raw did not report correct glucose readings above MAX_RAW" # If we made it here, the test passed echo "oref0-raw test passed" @@ -436,7 +437,7 @@ test-set-local-temptarget () { ../bin/oref0-set-local-temptarget.js 80 60 2>stderr_output 1>stdout_output # Make sure stderr output doesn't have anything - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "set-local-temptarget error: \n$ERROR_LINES" @@ -447,7 +448,7 @@ test-set-local-temptarget () { ../bin/oref0-set-local-temptarget.js 80 60 2017-10-18:00:15:00.000Z 2>stderr_output 1>stdout_output # Make sure stderr output contains expected string - ERROR_LINE_COUNT=$( cat stderr_output | wc -l ) + ERROR_LINE_COUNT=$( cat stderr_output | wc -l | sed 's/^ *//' ) ERROR_LINES=$( cat stderr_output ) [[ $ERROR_LINE_COUNT = 0 ]] || fail_test "set-local-temptarget error: \n$ERROR_LINES" diff --git a/tests/profile.test.ts b/tests/profile.test.ts index 83b5e6d7e..49c639eae 100644 --- a/tests/profile.test.ts +++ b/tests/profile.test.ts @@ -93,7 +93,7 @@ describe('Profile', function ( ) { }); it('should error with a current basal of 0', function () { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {basals: [{minutes: 0, rate: 0}]})); + var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { basals: [{ minutes: 0, rate: 0 }] })); profile.should.equal(-1); }); From 6b350344e1bf1a5f11b72654c01435700c1afb0d Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Tue, 13 Aug 2024 23:21:53 +0200 Subject: [PATCH 14/15] complete TS migration of lib and parse inputs with schemas `generate` functions must be the entry-points for unknown inputs. Inputs must be validated and eventually transformed. --- .eslintrc.js | 2 +- bin/ns-status.js | 2 +- bin/oref0-autosens-history.js | 4 +- bin/oref0-autotune-core.js | 2 +- bin/oref0-autotune-prep.js | 114 ++++----- bin/oref0-calculate-iob.js | 4 +- bin/oref0-detect-sensitivity.js | 2 +- bin/oref0-determine-basal.js | 8 +- bin/oref0-find-insulin-uses.js | 2 +- bin/oref0-get-ns-entries.js | 8 +- bin/oref0-get-profile.js | 2 +- bin/oref0-meal.js | 2 +- bin/oref0-normalize-temps.js | 10 +- bin/oref0-raw.js | 2 +- bin/oref0-shared-node.js | 6 +- lib/autotune-prep/categorize.ts | 28 ++- lib/autotune-prep/index.ts | 10 +- lib/autotune/index.ts | 4 +- lib/basal-set-temp.ts | 5 +- lib/bolus.js | 186 --------------- lib/bolus.ts | 199 ++++++++++++++++ lib/calc-glucose-stats.ts | 2 + lib/determine-basal/autosens.ts | 52 ++--- lib/determine-basal/cob.ts | 29 +-- lib/determine-basal/determine-basal.ts | 37 +-- lib/glucose-get-last.ts | 14 +- lib/iob/calculate.ts | 4 +- lib/iob/history.ts | 10 +- lib/iob/index.ts | 13 +- lib/iob/total.ts | 10 +- lib/meal/history.ts | 4 +- lib/meal/index.ts | 10 +- lib/meal/total.ts | 6 +- lib/medtronic-clock.ts | 4 +- lib/percentile.ts | 4 +- lib/profile/carbs.ts | 2 +- lib/profile/index.ts | 11 +- lib/profile/targets.ts | 2 +- lib/{pump.js => pump.ts} | 21 +- lib/{temps.js => temps.ts} | 30 ++- lib/types/Autosens.ts | 9 +- lib/types/GlucoseEntry.ts | 90 +++++-- lib/types/NightscoutTreatment.ts | 180 ++++++++++++-- lib/types/Preferences.ts | 2 +- lib/types/Profile.ts | 4 +- lib/types/PumpHistoryEvent.ts | 18 +- ...ith-raw-glucose.js => with-raw-glucose.ts} | 17 +- package.json | 3 +- tests/bolus.test.ts | 7 +- tests/check-syntax.test.ts | 14 +- tests/determine-basal.data.test.ts | 44 ++-- tests/determine-basal.test.ts | 22 +- tests/get-last-glucose.test.ts | 4 +- tests/glucose-noise.test.ts | 4 +- tests/iob.test.ts | 221 +++++++++++++----- tests/profile.test.ts | 18 +- tests/set-temp-basal.test.ts | 5 +- tests/tests-in-shell.test.ts | 6 +- tests/with-raw-glucose.test.ts | 5 +- tsconfig.build.json | 6 + tsconfig.json | 4 +- tsconfig.test.json | 1 + 62 files changed, 964 insertions(+), 587 deletions(-) delete mode 100644 lib/bolus.js create mode 100644 lib/bolus.ts rename lib/{pump.js => pump.ts} (56%) rename lib/{temps.js => temps.ts} (50%) rename lib/{with-raw-glucose.js => with-raw-glucose.ts} (84%) create mode 100644 tsconfig.build.json diff --git a/.eslintrc.js b/.eslintrc.js index fc41ecea5..7ce3daa2e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,7 @@ module.exports = { parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.json', + project: './tsconfig.test.json', }, env: { node: true, diff --git a/bin/ns-status.js b/bin/ns-status.js index 50d9ed3d1..12a546648 100755 --- a/bin/ns-status.js +++ b/bin/ns-status.js @@ -91,7 +91,7 @@ var ns_status = function ns_status(argv_params) { .help('help'); var params = argv.argv; var inputs = params._; - + var clock_input = inputs[0]; var iob_input = inputs[1]; var suggested_input = inputs[2]; diff --git a/bin/oref0-autosens-history.js b/bin/oref0-autosens-history.js index 8d42e0771..e0ae2cae9 100755 --- a/bin/oref0-autosens-history.js +++ b/bin/oref0-autosens-history.js @@ -16,8 +16,8 @@ THE SOFTWARE. */ -var basal = require('../dist/profile/basal'); -var detectSensitivity = require('../dist/determine-basal/autosens'); +var basal = require('../dist/profile/basal').default; +var detectSensitivity = require('../dist/determine-basal/autosens').default; if (!module.parent) { //var detectsensitivity = init(); // I don't see where this variable is used, so deleted it. diff --git a/bin/oref0-autotune-core.js b/bin/oref0-autotune-core.js index 8d90fc019..257d7a9d4 100755 --- a/bin/oref0-autotune-core.js +++ b/bin/oref0-autotune-core.js @@ -19,7 +19,7 @@ THE SOFTWARE. */ -var autotune = require('../dist/autotune'); +var autotune = require('../dist/autotune').default; var stringify = require('json-stable-stringify'); if (!module.parent) { diff --git a/bin/oref0-autotune-prep.js b/bin/oref0-autotune-prep.js index ad43d6c0f..0a91d62d1 100755 --- a/bin/oref0-autotune-prep.js +++ b/bin/oref0-autotune-prep.js @@ -20,25 +20,24 @@ */ -var generate = require('../dist/autotune-prep'); -var _ = require('lodash'); -var moment = require('moment'); +const generate = require('../dist/autotune-prep').default if (!module.parent) { - - var argv = require('yargs') - .usage("$0 [] [--categorize_uam_as_basal] [--tune-insulin-curve] [--output-file=]") + const argv = require('yargs') + .usage( + '$0 [] [--categorize_uam_as_basal] [--tune-insulin-curve] [--output-file=]' + ) .option('categorize_uam_as_basal', { alias: 'u', boolean: true, - describe: "Categorize UAM as basal", - default: false + describe: 'Categorize UAM as basal', + default: false, }) .option('tune-insulin-curve', { alias: 'i', boolean: true, - describe: "Tune peak time and end time", - default: false + describe: 'Tune peak time and end time', + default: false, }) .option('output-file', { alias: 'o', @@ -46,98 +45,101 @@ if (!module.parent) { default: null, }) .strict(true) - .help('help'); + .help('help') - var params = argv.argv; - var inputs = params._; + const params = argv.argv + let inputs = params._ if (inputs.length < 4 || inputs.length > 5) { - argv.showHelp(); - console.log('{ "error": "Insufficient arguments" }'); - process.exit(1); + argv.showHelp() + console.log('{ "error": "Insufficient arguments" }') + process.exit(1) } - var pumphistory_input = inputs[0]; - var profile_input = inputs[1]; - var glucose_input = inputs[2]; - var pumpprofile_input = inputs[3]; - var carb_input = inputs[4]; + const pumphistory_input = inputs[0] + const profile_input = inputs[1] + const glucose_input = inputs[2] + const pumpprofile_input = inputs[3] + const carb_input = inputs[4] - var fs = require('fs'); + const fs = require('fs') try { - var pumphistory_data = JSON.parse(fs.readFileSync(pumphistory_input, 'utf8')); - var profile_data = JSON.parse(fs.readFileSync(profile_input, 'utf8')); + var pumphistory_data = JSON.parse(fs.readFileSync(pumphistory_input, 'utf8')) + var profile_data = JSON.parse(fs.readFileSync(profile_input, 'utf8')) } catch (e) { - console.log('{ "error": "Could not parse input data" }'); - return console.error("Could not parse input data: ", e); + console.log('{ "error": "Could not parse input data" }') + return console.error('Could not parse input data: ', e) } - var pumpprofile_data = { }; + let pumpprofile_data = {} if (typeof pumpprofile_input !== 'undefined') { try { - pumpprofile_data = JSON.parse(fs.readFileSync(pumpprofile_input, 'utf8')); + pumpprofile_data = JSON.parse(fs.readFileSync(pumpprofile_input, 'utf8')) } catch (e) { - console.error("Warning: could not parse "+pumpprofile_input); + console.error(`Warning: could not parse ${pumpprofile_input}`) } } // disallow impossibly low carbRatios due to bad decoding - if ( typeof(profile_data.carb_ratio) === 'undefined' || profile_data.carb_ratio < 2 ) { - if ( typeof(pumpprofile_data.carb_ratio) === 'undefined' || pumpprofile_data.carb_ratio < 2 ) { - console.log('{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ' + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + ' out of bounds" }'); - return console.error("Error: carb_ratios " + profile_data.carb_ratio + ' and ' + pumpprofile_data.carb_ratio + " out of bounds"); + if (typeof profile_data.carb_ratio === 'undefined' || profile_data.carb_ratio < 2) { + if (typeof pumpprofile_data.carb_ratio === 'undefined' || pumpprofile_data.carb_ratio < 2) { + console.log( + `{ "carbs": 0, "mealCOB": 0, "reason": "carb_ratios ${profile_data.carb_ratio} and ${pumpprofile_data.carb_ratio} out of bounds" }` + ) + return console.error( + `Error: carb_ratios ${profile_data.carb_ratio} and ${pumpprofile_data.carb_ratio} out of bounds` + ) } else { - profile_data.carb_ratio = pumpprofile_data.carb_ratio; + profile_data.carb_ratio = pumpprofile_data.carb_ratio } } // get insulin curve from pump profile that is maintained - profile_data.curve = pumpprofile_data.curve; + profile_data.curve = pumpprofile_data.curve // Pump profile has an up to date copy of useCustomPeakTime from preferences // If the preferences file has useCustomPeakTime use the previous autotune dia and PeakTime. // Otherwise, use data from pump profile. if (!pumpprofile_data.useCustomPeakTime) { - profile_data.dia = pumpprofile_data.dia; - profile_data.insulinPeakTime = pumpprofile_data.insulinPeakTime; + profile_data.dia = pumpprofile_data.dia + profile_data.insulinPeakTime = pumpprofile_data.insulinPeakTime } // Always keep the curve value up to date with what's in the user preferences - profile_data.curve = pumpprofile_data.curve; + profile_data.curve = pumpprofile_data.curve let glucose_data = [] try { - glucose_data = JSON.parse(fs.readFileSync(glucose_input, 'utf8')); + glucose_data = JSON.parse(fs.readFileSync(glucose_input, 'utf8')) } catch (e) { - return console.error("Warning: could not parse "+glucose_input, e); + return console.error(`Warning: could not parse ${glucose_input}`, e) } - var carb_data = []; + let carb_data = [] if (typeof carb_input !== 'undefined') { try { - carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')); + carb_data = JSON.parse(fs.readFileSync(carb_input, 'utf8')) } catch (e) { - console.error("Warning: could not parse "+carb_input); + console.error(`Warning: could not parse ${carb_input}`) } } // Have to sort history - NS sort doesn't account for different zulu and local timestamps - pumphistory_data = _.orderBy(pumphistory_data, [function (o) { return moment(o.created_at).valueOf(); }], ['desc']); + pumphistory_data.sort((a, b) => new Date(a.created_at) >= new Date(b.created_at)) inputs = { - history: pumphistory_data - , profile: profile_data - , pumpprofile: pumpprofile_data - , carbs: carb_data - , glucose: glucose_data - , categorize_uam_as_basal: params.categorize_uam_as_basal - , tune_insulin_curve: params['tune-insulin-curve'] - }; - - var prepped_glucose = generate(inputs); + history: pumphistory_data, + profile: profile_data, + pumpprofile: pumpprofile_data, + carbs: carb_data, + glucose: glucose_data, + categorize_uam_as_basal: params.categorize_uam_as_basal, + tune_insulin_curve: params['tune-insulin-curve'], + } + + const prepped_glucose = generate(inputs) if (params['output-file']) { fs.writeFileSync(params['output-file'], JSON.stringify(prepped_glucose)) } else { - console.log(JSON.stringify(prepped_glucose)); + console.log(JSON.stringify(prepped_glucose)) } } - diff --git a/bin/oref0-calculate-iob.js b/bin/oref0-calculate-iob.js index 1f07f48bd..8cbed7029 100755 --- a/bin/oref0-calculate-iob.js +++ b/bin/oref0-calculate-iob.js @@ -18,7 +18,7 @@ */ -var generate = require('../dist/iob'); +var generate = require('../dist/iob').default; var fs = require('fs'); function usage ( ) { console.log('usage: ', process.argv.slice(0, 2), ' [autosens.json] [pumphistory-24h-zoned.json]'); @@ -27,7 +27,7 @@ function usage ( ) { -var oref0_calculate_iob = function oref0_calculate_iob(argv_params) { +var oref0_calculate_iob = function oref0_calculate_iob(argv_params) { var argv = require('yargs')(argv_params) .usage("$0 [] []") .strict(true) diff --git a/bin/oref0-detect-sensitivity.js b/bin/oref0-detect-sensitivity.js index a983ae278..03105f1f8 100755 --- a/bin/oref0-detect-sensitivity.js +++ b/bin/oref0-detect-sensitivity.js @@ -14,7 +14,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var detectSensitivity = require('../dist/determine-basal/autosens'); +var detectSensitivity = require('../dist/determine-basal/autosens').default; if (!module.parent) { var argv = require('yargs') diff --git a/bin/oref0-determine-basal.js b/bin/oref0-determine-basal.js index f38fec9d4..8a3264799 100755 --- a/bin/oref0-determine-basal.js +++ b/bin/oref0-determine-basal.js @@ -14,8 +14,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -var getLastGlucose = require('../dist/glucose-get-last'); -var determine_basal = require('../dist/determine-basal/determine-basal'); +var getLastGlucose = require('../dist/glucose-get-last').default; +var determine_basal = require('../dist/determine-basal/determine-basal').default; /* istanbul ignore next */ if (!module.parent) { @@ -116,7 +116,7 @@ if (!module.parent) { } //console.log(carbratio_data); - var meal_data = []; + var meal_data = {}; //console.error("meal_input",meal_input); if (meal_input && typeof meal_input !== 'undefined') { try { @@ -222,7 +222,7 @@ if (!module.parent) { currentTime: currentTime } - var rT = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens_data, meal_data, tempBasalFunctions, params['microbolus'], reservoir_data, currentTime); + var rT = determine_basal(input); if(typeof rT.error === 'undefined') { console.log(JSON.stringify(rT)); diff --git a/bin/oref0-find-insulin-uses.js b/bin/oref0-find-insulin-uses.js index 5423684cf..7f00d19e9 100755 --- a/bin/oref0-find-insulin-uses.js +++ b/bin/oref0-find-insulin-uses.js @@ -18,7 +18,7 @@ */ -var find_insulin = require('../dist/iob/history'); +var find_insulin = require('../dist/iob/history').default; if (!module.parent) { var argv = require('yargs') diff --git a/bin/oref0-get-ns-entries.js b/bin/oref0-get-ns-entries.js index e0b5496d6..2f532e7e4 100755 --- a/bin/oref0-get-ns-entries.js +++ b/bin/oref0-get-ns-entries.js @@ -31,7 +31,7 @@ var console_error = shared_node.console_error; var console_log = shared_node.console_log; var initFinalResults = shared_node.initFinalResults; -var oref0_get_ns_engtires = function oref0_get_ns_engtires(argv_params, print_callback, final_result) { +var oref0_get_ns_engtires = function oref0_get_ns_engtires(argv_params, print_callback, final_result) { var safe_errors = ['ECONNREFUSED', 'ESOCKETTIMEDOUT', 'ETIMEDOUT']; var log_errors = true; @@ -126,7 +126,7 @@ var oref0_get_ns_engtires = function oref0_get_ns_engtires(argv_params, print_ca } else { print_callback(final_result); } - + }); return false; @@ -172,7 +172,7 @@ var oref0_get_ns_engtires = function oref0_get_ns_engtires(argv_params, print_ca var tokenAuth = ""; if (apisecret.startsWith("token=")) { tokenAuth = "&" + apisecret; - } else { + } else { headers = { 'api-secret': apisecret }; } @@ -233,7 +233,7 @@ function print_callback(final_result) { if (!module.parent) { var final_result = initFinalResults(); - + // remove the first parameter. var command = process.argv; command.shift(); diff --git a/bin/oref0-get-profile.js b/bin/oref0-get-profile.js index 1cab20626..0f9265ae9 100755 --- a/bin/oref0-get-profile.js +++ b/bin/oref0-get-profile.js @@ -18,7 +18,7 @@ */ var fs = require('fs'); -var generate = require('../dist/profile/'); +var generate = require('../dist/profile/').default; var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; diff --git a/bin/oref0-meal.js b/bin/oref0-meal.js index fc7606d3a..7813d1d33 100755 --- a/bin/oref0-meal.js +++ b/bin/oref0-meal.js @@ -20,7 +20,7 @@ */ -var generate = require('../dist/meal') +var generate = require('../dist/meal').default; var shared_node_utils = require('../dist/bin/utils'); var console_error = shared_node_utils.console_error; var console_log = shared_node_utils.console_log; diff --git a/bin/oref0-normalize-temps.js b/bin/oref0-normalize-temps.js index f720390f2..6ed0c5fa4 100755 --- a/bin/oref0-normalize-temps.js +++ b/bin/oref0-normalize-temps.js @@ -15,14 +15,14 @@ */ -var find_insulin = require('../dist/temps'); -var find_bolus = require('../dist/bolus'); -var describe_pump = require('../dist/pump'); +var find_insulin = require('../dist/temps').default; +var find_bolus = require('../dist/bolus').default; +var describe_pump = require('../dist/pump').default; var fs = require('fs'); - -var oref0_normalize_temps = function oref0_normalize_temps(argv_params) { + +var oref0_normalize_temps = function oref0_normalize_temps(argv_params) { var argv = require('yargs')(argv_params) .usage('$0 ') .demand(1) diff --git a/bin/oref0-raw.js b/bin/oref0-raw.js index e9fc1d5aa..24bb84443 100755 --- a/bin/oref0-raw.js +++ b/bin/oref0-raw.js @@ -2,7 +2,7 @@ 'use strict'; var safeRequire = require('../dist/require-utils').safeRequire; -var withRawGlucose = require('../dist/with-raw-glucose'); +var withRawGlucose = require('../dist/with-raw-glucose').default; /* Fills CGM data doesn't already contain an EVG, if we have unfiltered, filtered, and a cal diff --git a/bin/oref0-shared-node.js b/bin/oref0-shared-node.js index ce1841a97..88f512204 100644 --- a/bin/oref0-shared-node.js +++ b/bin/oref0-shared-node.js @@ -67,7 +67,7 @@ function serverListen() { console.log('read data', data.toString()); var command = data.toString().split(' '); - // Split by space except for inside quotes + // Split by space except for inside quotes // (https://stackoverflow.com/questions/16261635/javascript-split-string-by-space-but-ignore-space-in-quotes-notice-not-to-spli) var command = data.toString().match(/\\?.|^$/g).reduce((p, c) => { if (c === '"') { @@ -258,7 +258,7 @@ function jsonWrapper(argv_params) { if (!params.filtering_code) { return [console.error('Error: No filtering_code'), 1]; } - + var data = requireUtils.safeLoadFile(params.input_file); if (!data) { // file is empty. For this files json returns nothing @@ -270,7 +270,7 @@ function jsonWrapper(argv_params) { console.error('Error: data is not an array.') return ["", 1]; } - + var condFuncs = funcWithReturnFromSnippet(params.filtering_code); var filtered = []; for (var i = 0; i < data.length; i++) { diff --git a/lib/autotune-prep/categorize.ts b/lib/autotune-prep/categorize.ts index 60533c4f2..e40db3e29 100644 --- a/lib/autotune-prep/categorize.ts +++ b/lib/autotune-prep/categorize.ts @@ -1,4 +1,4 @@ -import { pipe } from 'effect' +import { flow, pipe } from 'effect' import * as A from 'effect/Array' import * as Option from 'effect/Option' import * as O from 'effect/Order' @@ -12,7 +12,7 @@ import * as GlucoseEntry from '../types/GlucoseEntry' import type { ISFSensitivity } from '../types/ISFSensitivity' import type { NightscoutTreatment } from '../types/NightscoutTreatment' import type { Profile } from '../types/Profile' -import dosed from './dosed' +import { insulinDosed } from './dosed' interface Input { treatments: ReadonlyArray @@ -26,6 +26,7 @@ interface Input { type CSFUAMGlucoseData = GlucoseEntry.GlucoseEntry & { glucose: number + date: number dateString: string avgDelta: number BGI: number @@ -35,7 +36,7 @@ type CSFUAMGlucoseData = GlucoseEntry.GlucoseEntry & { mealAbsorption?: string | undefined } -function categorizeBGDatums(opts: Input) { +export function categorizeBGDatums(opts: Input) { // this sorts the treatments collection in order. let treatments = A.sort( opts.treatments, @@ -53,14 +54,13 @@ function categorizeBGDatums(opts: Input) { } const glucoseData = pipe( - A.filterMap(opts.glucose || [], a => - pipe( - Option.some({ - ...a, - glucose: GlucoseEntry.getGlucose(a), - dateString: GlucoseEntry.getDate(a).toISOString(), - }), - Option.filter(b => b.glucose >= 39) + opts.glucose || [], + A.filterMap( + flow( + GlucoseEntry.setGlucoseField, + GlucoseEntry.setDateFields, + GlucoseEntry.filterWithGlucose, + Option.filter(({ glucose }) => glucose > 39) ) ), A.sort(O.reverse(GlucoseEntry.Order)) @@ -77,9 +77,7 @@ function categorizeBGDatums(opts: Input) { const UAMGlucoseData: CSFUAMGlucoseData[] = [] let CRData = [] - const bucketedData: Array = [] - // why this? just to handle dates to strings? - bucketedData[0] = glucoseData[0] + const bucketedData = [glucoseData[0]] let j = 0 let k = 0 // index of first value used by bucket //for loop to validate and bucket the data @@ -420,7 +418,7 @@ function categorizeBGDatums(opts: Input) { }) CRData = CRData.map(CRDatum => ({ ...CRDatum, - CRInsulin: dosed({ + CRInsulin: insulinDosed({ treatments: insulinTreatments, start: CRDatum.CRInitialCarbTime!, end: CRDatum.CREndTime, diff --git a/lib/autotune-prep/index.ts b/lib/autotune-prep/index.ts index 4e2e4ae6f..b1feca1ed 100644 --- a/lib/autotune-prep/index.ts +++ b/lib/autotune-prep/index.ts @@ -1,12 +1,12 @@ // Prep step before autotune.js can run; pulls in meal (carb) data and calls categorize.js import { Schema } from '@effect/schema' -import find_meals from '../meal/history' +import { findMeals } from '../meal/history' import { CarbEntry } from '../types/CarbEntry' import * as GlucoseEntry from '../types/GlucoseEntry' import { NightscoutTreatment } from '../types/NightscoutTreatment' import { Profile } from '../types/Profile' -import categorize from './categorize' +import { categorizeBGDatums as categorize } from './categorize' const Input = Schema.Struct({ history: Schema.Array(NightscoutTreatment), @@ -18,10 +18,10 @@ const Input = Schema.Struct({ tune_insulin_curve: Schema.optionalWith(Schema.Boolean, { default: () => false }), }) -function generate(input: unknown) { +export function generate(input: unknown) { //console.error(inputs); const inputs = Schema.decodeUnknownSync(Input)(input) - const treatments = find_meals(inputs) + const treatments = findMeals(inputs) const profile = inputs.profile const opts = { @@ -191,4 +191,4 @@ function generate(input: unknown) { return { ...categorize(opts), diaDeviations, peakDeviations } } -export default module.exports = generate +export default generate diff --git a/lib/autotune/index.ts b/lib/autotune/index.ts index 3306be9b6..5f59a9d05 100644 --- a/lib/autotune/index.ts +++ b/lib/autotune/index.ts @@ -1,4 +1,4 @@ -import percentile from '../percentile' +import { percentile } from '../percentile' // does three things - tunes basals, ISF, and CSF @@ -735,4 +735,4 @@ function tuneAllTheThings(inputs: any) { return autotuneOutput } -export default module.exports = tuneAllTheThings +export default tuneAllTheThings diff --git a/lib/basal-set-temp.ts b/lib/basal-set-temp.ts index a8cc01514..db743b256 100644 --- a/lib/basal-set-temp.ts +++ b/lib/basal-set-temp.ts @@ -1,3 +1,4 @@ +import { round_basal } from './round-basal' import type { Profile } from './types/Profile' interface RT { @@ -27,11 +28,11 @@ export function getMaxSafeBasal(profile: Profile) { ) } -export function setTempBasal(rate: number, duration: number, profile: Profile, rT: RT, currenttemp?: Temp) { +export function setTempBasal(rateInput: number, duration: number, profile: Profile, rT: RT, currenttemp?: Temp) { //var maxSafeBasal = Math.min(profile.max_basal, 3 * profile.max_daily_basal, 4 * profile.current_basal); const maxSafeBasal = getMaxSafeBasal(profile) - const round_basal = require('./round-basal') + let rate = rateInput if (rate < 0) { rate = 0 diff --git a/lib/bolus.js b/lib/bolus.js deleted file mode 100644 index 46f94f53b..000000000 --- a/lib/bolus.js +++ /dev/null @@ -1,186 +0,0 @@ -'use strict' - -function reduce(treatments) { - const results = [] - - let state = {} - const previous = [] - - function in_previous(ev) { - let found = false - previous.forEach(elem => { - if (elem.timestamp === ev.timestamp && ev._type === elem._type) { - found = true - } - }) - - return found - } - - function within_minutes_from(origin, tail, minutes) { - const ms = minutes * 1000 * 60 - const ts = Date.parse(origin.timestamp) - return /* candidates */ tail.slice().filter(elem => { - const dt = Date.parse(elem.timestamp) - return ts - dt <= ms - }) - } - - function bolus(ev, remaining) { - if (!ev) { - console.error('XXX', ev, remaining) - return - } - if (ev._type === 'BolusWizard') { - state.carbs = ev.carb_input.toString() - state.ratio = ev.carb_ratio.toString() - if (ev.bg) { - state.bg = ev.bg.toString() - state.glucose = ev.bg.toString() - state.glucoseType = ev._type - } - state.wizard = ev - state.created_at = state.timestamp = ev.timestamp - previous.push(ev) - } - - if (ev._type === 'Bolus') { - state.duration = ev.duration.toString() - // if (state.square || state.bolus) { } - // state.insulin = (state.insulin ? state.insulin : 0) + ev.amount; - if (ev.duration && ev.duration > 0) { - state.square = ev - } else { - if (state.bolus) { - state.bolus.amount = state.bolus.amount + ev.amount - } else { - state.bolus = ev - } - } - state.created_at = state.timestamp = ev.timestamp - previous.push(ev) - } - - if (remaining && remaining.length > 0) { - if (state.bolus && state.wizard) { - // skip to end - return bolus({}, []) - } - // keep recursing - return bolus(remaining[0], remaining.slice(1)) - } else { - // console.error("state", state); - // console.error("remaining", remaining); - state.eventType = '' - - state.insulin = - (state.insulin ? state.insulin : 0) + - (state.square ? state.square.amount : 0) + - (state.bolus ? state.bolus.amount : 0) - const has_insulin = state.insulin && state.insulin > 0 - const has_carbs = state.carbs && state.carbs > 0 - if (state.square && state.bolus) { - annotate('DualWave bolus for', state.square.duration, 'minutes') - } else if (state.square && state.wizard) { - annotate('Square wave bolus for', state.square.duration, 'minutes') - } else if (state.square) { - annotate('Solo Square wave bolus for', state.square.duration, 'minutes') - annotate('No bolus wizard used.') - } else if (state.bolus && state.wizard) { - annotate('Normal bolus with wizard.') - } else if (state.bolus) { - annotate('Normal bolus (solo, no bolus wizard).') - } - - if (has_insulin) { - const iobFile = './monitor/iob.json' - const fs = require('fs') - if (fs.existsSync(iobFile)) { - const iob = JSON.parse(fs.readFileSync(iobFile)) - if (iob && Array.isArray(iob) && iob.length) { - annotate('Calculated IOB:', iob[0].iob) - } - } - } - - if (state.bolus) { - annotate('Programmed bolus', state.bolus.programmed) - annotate('Delivered bolus', state.bolus.amount) - annotate('Percent delivered: ', `${((state.bolus.amount / state.bolus.programmed) * 100).toString()}%`) - } - if (state.square) { - annotate('Programmed square', state.square.programmed) - annotate('Delivered square', state.square.amount) - annotate('Success: ', `${((state.square.amount / state.square.programmed) * 100).toString()}%`) - } - if (state.wizard) { - state.created_at = state.wizard.timestamp - annotate('Food estimate', state.wizard.food_estimate) - annotate('Correction estimate', state.wizard.correction_estimate) - annotate('Bolus estimate', state.wizard.bolus_estimate) - annotate('Target low', state.wizard.bg_target_low) - annotate('Target high', state.wizard.bg_target_high) - const delta = state.wizard.sensitivity * state.insulin * -1 - annotate('Hypothetical glucose delta', delta) - if (state.bg && state.bg > 0) { - annotate('Glucose was:', state.bg) - // state.glucose = state.bg; - // TODO: annotate prediction - } - } - if (has_carbs && has_insulin) { - state.eventType = 'Meal Bolus' - } else { - if (has_carbs && !has_insulin) { - state.eventType = 'Carb Correction' - } - if (!has_carbs && has_insulin) { - state.eventType = 'Correction Bolus' - } - } - if (state.notes && state.notes.length > 0) { - state.notes = state.notes.join('\n') - } - if (state.insulin) { - state.insulin = state.insulin.toString() - } - - results.push(state) - state = {} - } - } - - function annotate(msg) { - const args = [].slice.apply(arguments) - msg = args.join(' ') - if (!state.notes) { - state.notes = [] - } - state.notes.push(msg) - } - - function step(current, index) { - if (in_previous(current)) { - return - } - switch (current._type) { - case 'Bolus': - case 'BolusWizard': - var tail = within_minutes_from(current, treatments.slice(index + 1), 2) - bolus(current, tail) - break - case 'JournalEntryMealMarker': - current.carbs = current.carb_input - current.eventType = 'Carb Correction' - results.push(current) - break - default: - results.push(current) - break - } - } - treatments.forEach(step) - return results -} - -exports = module.exports = reduce diff --git a/lib/bolus.ts b/lib/bolus.ts new file mode 100644 index 000000000..6f0f4876b --- /dev/null +++ b/lib/bolus.ts @@ -0,0 +1,199 @@ +import { Schema } from '@effect/schema' +import * as fs from 'fs' +import type { NightscoutTreatment } from './types/NightscoutTreatment' +import { PumpHistoryEvent } from './types/PumpHistoryEvent' + +function withinMinutesFrom(origin: A, tail: A[], minutes: number) { + const ms = minutes * 1000 * 60 + const ts = Date.parse(origin.timestamp) + return tail.slice().filter(elem => { + const dt = Date.parse(elem.timestamp) + return ts - dt <= ms + }) +} + +function annotate(state: A, ...a: (string | number | undefined)[]): A { + const notes = state.notes || '' + return { + ...state, + notes: (notes.length > 0 ? '\n' : '') + a.join(' '), + } +} + +export function generate(treatments: unknown) { + return reduce(Schema.decodeUnknownSync(Schema.Array(PumpHistoryEvent))(treatments)) +} + +export function reduce(treatments: ReadonlyArray) { + const results: (NightscoutTreatment | PumpHistoryEvent)[] = [] + + const previous: PumpHistoryEvent[] = [] + let state: NightscoutTreatment = { + eventType: '', + created_at: new Date(0).toISOString(), + } + + function bolus(ev: PumpHistoryEvent | null, remaining: PumpHistoryEvent[]) { + if (ev && ev._type === 'BolusWizard') { + state = { + ...state, + carbs: ev.carb_input, + ratio: ev.carb_ratio, + wizard: ev, + timestamp: ev.timestamp, + created_at: ev.timestamp, + ...(ev.bg + ? { + bg: ev.bg, + glucose: ev.bg, + glucoseType: ev._type, + } + : undefined), + } + previous.push(ev) + } else if (ev && ev._type === 'Bolus') { + state = { + ...state, + duration: ev.duration, + timestamp: ev.timestamp, + created_at: ev.timestamp, + } + // if (state.square || state.bolus) { } + // state.insulin = (state.insulin ? state.insulin : 0) + ev.amount; + if (ev.duration && ev.duration > 0) { + state = { ...state, square: ev } + } else { + if (state.bolus) { + state = { + ...state, + bolus: { + ...state.bolus, + amount: (state.bolus.amount || 0) + (ev.amount || 0), + }, + } + } else { + state = { ...state, bolus: ev } + } + } + + previous.push(ev) + } + + if (remaining && remaining.length > 0) { + if (state.bolus && state.wizard) { + // skip to end + return bolus(null, []) + } + // keep recursing + return bolus(remaining[0], remaining.slice(1)) + } else { + // console.error("state", state); + // console.error("remaining", remaining); + state = { + ...state, + eventType: '', + insulin: (state.insulin || 0) + (state.square?.amount || 0) + (state.bolus?.amount || 0), + } + const has_insulin = (state.insulin || 0) > 0 + const has_carbs = (state.carbs || 0) > 0 + if (state.square && state.bolus) { + state = annotate(state, 'DualWave bolus for', state.square.duration, 'minutes') + } else if (state.square && state.wizard) { + state = annotate(state, 'Square wave bolus for', state.square.duration, 'minutes') + } else if (state.square) { + state = annotate(state, 'Solo Square wave bolus for', state.square.duration, 'minutes') + state = annotate(state, 'No bolus wizard used.') + } else if (state.bolus && state.wizard) { + state = annotate(state, 'Normal bolus with wizard.') + } else if (state.bolus) { + state = annotate(state, 'Normal bolus (solo, no bolus wizard).') + } + + if (has_insulin) { + const iobFile = './monitor/iob.json' + if (fs.existsSync(iobFile)) { + const iob = JSON.parse(fs.readFileSync(iobFile).toString()) + if (iob && Array.isArray(iob) && iob.length) { + state = annotate(state, 'Calculated IOB:', iob[0].iob) + } + } + } + + if (state.bolus) { + const amount = state.bolus.amount! + const programmed = state.bolus.programmed !== undefined ? state.bolus.programmed : amount + state = annotate(state, 'Programmed bolus', programmed) + state = annotate(state, 'Delivered bolus', programmed) + state = annotate(state, 'Percent delivered: ', `${((amount / programmed) * 100).toString()}%`) + } + if (state.square) { + const square = state.square + const amount = square.amount! + const programmed = square.programmed !== undefined ? square.programmed : amount + state = annotate(state, 'Programmed square', programmed) + state = annotate(state, 'Delivered square', amount) + state = annotate(state, 'Success: ', `${((amount / programmed) * 100).toString()}%`) + } + if (state.wizard) { + const wizard = state.wizard + state = { ...state, created_at: wizard.timestamp } + state = annotate(state, 'Food estimate', wizard.food_estimate) + state = annotate(state, 'Correction estimate', wizard.correction_estimate) + state = annotate(state, 'Bolus estimate', wizard.bolus_estimate) + state = annotate(state, 'Target low', wizard.bg_target_low) + state = annotate(state, 'Target high', wizard.bg_target_high) + const delta = wizard.sensitivity! * Number(state.insulin) * -1 + state = annotate(state, 'Hypothetical glucose delta', delta) + if (state.bg && Number(state.bg) > 0) { + state = annotate(state, 'Glucose was:', state.bg) + // state.glucose = state.bg; + // TODO: annotate prediction + } + } + if (has_carbs && has_insulin) { + state = { ...state, eventType: 'Meal Bolus' } + } else if (has_carbs && !has_insulin) { + state = { ...state, eventType: 'Carb Correction' } + } else if (!has_carbs && has_insulin) { + state = { ...state, eventType: 'Correction Bolus' } + } else { + // else??? + } + + results.push(state) + state = { + eventType: '', + created_at: new Date(0).toISOString(), + } + } + } + + function step(current: PumpHistoryEvent, index: number) { + const inPrevious = previous.some(elem => elem.timestamp === current.timestamp && current._type === elem._type) + if (inPrevious) { + return + } + switch (current._type) { + case 'Bolus': + case 'BolusWizard': + bolus(current, withinMinutesFrom(current, treatments.slice(index + 1), 2)) + break + case 'JournalEntryMealMarker': + results.push({ + ...current, + carbs: current.carb_input, + eventType: 'Carb Correction', + }) + break + default: + results.push({ + ...current, + }) + break + } + } + treatments.forEach(step) + return results +} + +export default generate diff --git a/lib/calc-glucose-stats.ts b/lib/calc-glucose-stats.ts index 869a3fc12..0b9c66a35 100644 --- a/lib/calc-glucose-stats.ts +++ b/lib/calc-glucose-stats.ts @@ -30,3 +30,5 @@ export function updateGlucoseStats(options: Options) { return options.glucose_hist } + +export default updateGlucoseStats diff --git a/lib/determine-basal/autosens.ts b/lib/determine-basal/autosens.ts index d12039d13..18b5b2311 100644 --- a/lib/determine-basal/autosens.ts +++ b/lib/determine-basal/autosens.ts @@ -1,38 +1,38 @@ +import { Schema } from '@effect/schema' import * as A from 'effect/Array' import { tz } from '../date' import { getIob } from '../iob' import { findInsulin } from '../iob/history' import * as MealTreatment from '../meal/MealTreatment' -import find_meals from '../meal/history' -import percentile from '../percentile' +import { findMeals } from '../meal/history' +import { percentile } from '../percentile' import { basalLookup } from '../profile/basal' import { isfLookup } from '../profile/isf' -import type { BasalSchedule } from '../types/BasalSchedule' -import type { CarbEntry } from '../types/CarbEntry' -import type { GlucoseEntry } from '../types/GlucoseEntry' -import { getDate, getGlucose } from '../types/GlucoseEntry' +import { BasalSchedule } from '../types/BasalSchedule' +import { CarbEntry } from '../types/CarbEntry' +import { GlucoseEntry, reduceWithGlucoseAndDate } from '../types/GlucoseEntry' import type { ISFSensitivity } from '../types/ISFSensitivity' import * as TempTarget from '../types/TempTarget' -interface Inputs { - glucose_data: GlucoseEntry[] - iob_inputs: any - basalprofile: BasalSchedule[] - retrospective?: boolean - carbs: CarbEntry[] - temptargets: TempTarget.TempTarget[] - deviations?: number +const Inputs = Schema.Struct({ + glucose_data: Schema.Array(GlucoseEntry), + iob_inputs: Schema.Any, + basalprofile: Schema.Array(BasalSchedule), + retrospective: Schema.optional(Schema.Boolean), + carbs: Schema.Array(CarbEntry), + temptargets: Schema.Array(TempTarget.TempTarget), + deviations: Schema.optional(Schema.Number), +}) + +type Inputs = typeof Inputs.Type + +export default function generate(inputs: unknown) { + return detectSensitivity(Schema.decodeUnknownSync(Inputs)(inputs)) } -function detectSensitivity(inputs: Inputs) { +export function detectSensitivity(inputs: Inputs) { //console.error(inputs.glucose_data[0]); - const glucose_data = inputs.glucose_data.map(obj => { - //Support the NS sgv field to avoid having to convert in a custom way - return { - ...obj, - glucose: getGlucose(obj), - } - }) + const glucose_data = reduceWithGlucoseAndDate(inputs.glucose_data) //console.error(glucose_data[0]); const iob_inputs = inputs.iob_inputs const basalprofile = inputs.basalprofile @@ -41,7 +41,7 @@ function detectSensitivity(inputs: Inputs) { let lastSiteChange = new Date(new Date().getTime() - 24 * 60 * 60 * 1000) // use last 24h worth of data by default if (inputs.retrospective) { - const firstDate = getDate(glucose_data[0]) + const firstDate = new Date(glucose_data[0].date) if (!firstDate) { throw new Error('Unable to find glucose date for first item') } @@ -75,7 +75,7 @@ function detectSensitivity(inputs: Inputs) { glucose: inputs.glucose_data, //, prepped_glucose: prepped_glucose_data } - const meals = A.sort(find_meals(mealinputs), MealTreatment.Order) + const meals = A.sort(findMeals(mealinputs), MealTreatment.Order) //console.error(meals); const avgDeltas = [] @@ -479,7 +479,7 @@ function detectSensitivity(inputs: Inputs) { } } -function tempTargetRunning(temptargets: TempTarget.TempTarget[], time: Date) { +function tempTargetRunning(temptargets: ReadonlyArray, time: Date) { // sort tempTargets by date so we can process most recent first const temptargets_data = A.sort(temptargets, TempTarget.Order) //console.error(temptargets_data); @@ -503,5 +503,3 @@ function tempTargetRunning(temptargets: TempTarget.TempTarget[], time: Date) { return 0 } - -export default exports.module = detectSensitivity diff --git a/lib/determine-basal/cob.ts b/lib/determine-basal/cob.ts index 8bba6d483..0f9446130 100644 --- a/lib/determine-basal/cob.ts +++ b/lib/determine-basal/cob.ts @@ -4,7 +4,7 @@ import { findInsulin } from '../iob/history' import * as basal from '../profile/basal' import { isfLookup } from '../profile/isf' import type { BasalSchedule } from '../types/BasalSchedule' -import { getGlucose, type GlucoseEntry } from '../types/GlucoseEntry' +import { reduceWithGlucoseAndDate, type GlucoseEntry } from '../types/GlucoseEntry' export interface DetectCOBInput { glucose_data: ReadonlyArray @@ -14,29 +14,12 @@ export interface DetectCOBInput { ciTime?: number } -function getDateFromEntry(entry: GlucoseEntry) { - if (entry.date) { - return entry.date - } else if (entry.display_time) { - return Date.parse(entry.display_time) - } else if (entry.dateString) { - return Date.parse(entry.dateString) - } - - throw new TypeError('Unable to find a date in GlucoseEntry') -} - /** * @todo: does it works with profile.carb_ratio === undefined? */ -function detectCarbAbsorption(inputs: DetectCOBInput) { - const glucose_data = inputs.glucose_data.reduce( - (b, a) => { - const glucose = getGlucose(a) - return glucose ? [...b, { ...a, glucose, date: getDateFromEntry(a) }] : b - }, - [] as (GlucoseEntry & { glucose: number; date: number })[] - ) +export function detectCarbAbsorption(inputs: DetectCOBInput) { + const glucose_data = reduceWithGlucoseAndDate(inputs.glucose_data) + let iob_inputs = inputs.iob_inputs const basalprofile = inputs.basalprofile /* TODO why does declaring profile break tests-command-behavior.tests.sh? @@ -55,7 +38,7 @@ function detectCarbAbsorption(inputs: DetectCOBInput) { } let carbsAbsorbed = 0 - const bucketed_data = glucose_data.slice(0, 1) + const bucketed_data: Array<{ date: number; glucose: number }> = glucose_data.slice(0, 1) let j = 0 let foundPreMealBG = false let lastbgi = 0 @@ -242,4 +225,4 @@ function detectCarbAbsorption(inputs: DetectCOBInput) { } } -export default module.exports = detectCarbAbsorption +export default detectCarbAbsorption diff --git a/lib/determine-basal/determine-basal.ts b/lib/determine-basal/determine-basal.ts index be28aa696..8d3268125 100644 --- a/lib/determine-basal/determine-basal.ts +++ b/lib/determine-basal/determine-basal.ts @@ -18,7 +18,7 @@ import { Schema } from '@effect/schema' import { getMaxSafeBasal, setTempBasal } from '../basal-set-temp' import { RecentCarbs } from '../meal/RecentCarbs' -import round_basal from '../round-basal' +import { round_basal } from '../round-basal' import { Autosens } from '../types/Autosens' import { IOB } from '../types/IOB' import { LastGlucose } from '../types/LastGlucose' @@ -140,16 +140,21 @@ function enable_smb( const Input = Schema.Struct({ glucose: LastGlucose, currenttemp: TempBasal, - iobTicks: Schema.Array(IOB), + iobTicks: Schema.Union(IOB, Schema.Array(IOB)), profile: Profile, - autosens: Schema.optional(Autosens), - meal: RecentCarbs, - microBolusAllowed: Schema.Boolean, - reservoir: Schema.Number, - currentTime: Schema.optional(Schema.Union(Schema.DateFromSelf, Schema.DateFromString).pipe(Schema.validDate())), + autosens: Schema.optionalWith(Autosens, { nullable: true }), + meal: Schema.optionalWith(RecentCarbs, { nullable: true, default: () => RecentCarbs.make({}) }), + microBolusAllowed: Schema.optional(Schema.Boolean), + reservoir: Schema.optionalWith(Schema.Union(Schema.Number, Schema.NumberFromString), { + nullable: true, + }), + currentTime: Schema.optionalWith( + Schema.Union(Schema.DateFromSelf, Schema.DateFromString).pipe(Schema.validDate()), + { nullable: true } + ), }) -function generate(input: unknown) { +export function generate(input: unknown) { const r = Schema.decodeUnknownSync(Input)(input) return determine_basal( r.glucose, @@ -167,13 +172,13 @@ function generate(input: unknown) { export const determine_basal = function determine_basal( glucose_status: LastGlucose, currenttemp: TempBasal, - iob_ticks: ReadonlyArray, + iob_ticks: IOB | ReadonlyArray, profile: Profile, autosens_data: Autosens | undefined, meal_data: RecentCarbs, - microBolusAllowed: boolean, - reservoir_data: number, - currentTime?: Date + microBolusAllowed: boolean = true, + reservoir_data: number | undefined = undefined, + currentTime: Date | undefined = undefined ) { // Set variables required for evaluating error conditions let rT: { @@ -661,7 +666,8 @@ export const determine_basal = function determine_basal( const predCIs: number[] = [] let COBpredBG: number | undefined let UAMpredBG: number | undefined - try { + + if (Array.isArray(iob_ticks)) { iob_ticks.forEach(iobTick => { //console.error(iobTick); const predBGI = round(-iobTick.activity * sens * 5, 2) @@ -756,8 +762,7 @@ export const determine_basal = function determine_basal( }) // set eventualBG to include effect of carbs //console.error("PredBGs:",JSON.stringify(predBGs)); - } catch (e) { - console.error(e) + } else { console.error('Problem with iobArray. Optional feature Advanced Meal Assist disabled') } if (meal_data.mealCOB) { @@ -1376,4 +1381,4 @@ export const determine_basal = function determine_basal( } } -export default module.exports = generate +export default generate diff --git a/lib/glucose-get-last.ts b/lib/glucose-get-last.ts index d0b623bc3..80fe9a3a2 100644 --- a/lib/glucose-get-last.ts +++ b/lib/glucose-get-last.ts @@ -1,5 +1,5 @@ import { Schema } from '@effect/schema' -import { getDate, getGlucose, GlucoseEntry } from './types/GlucoseEntry' +import { reduceWithGlucose, getDate, GlucoseEntry } from './types/GlucoseEntry' import type { LastGlucose } from './types/LastGlucose' function getDateFromEntry(entry: GlucoseEntry) { @@ -12,19 +12,13 @@ function getDateFromEntry(entry: GlucoseEntry) { return date } -function generate(input: unknown) { +export default function generate(input: unknown) { const glucoseData = Schema.decodeUnknownSync(Schema.Array(GlucoseEntry))(input) return getLastGlucose(glucoseData) } export const getLastGlucose = function (input: ReadonlyArray): LastGlucose { - const data = input.reduce( - (b, a) => { - const glucose = getGlucose(a) - return [...b, { ...a, glucose }] - }, - [] as (GlucoseEntry & { glucose: number })[] - ) + const data = reduceWithGlucose(input) const now = data[0] let now_date = getDateFromEntry(now).getTime() @@ -112,5 +106,3 @@ export const getLastGlucose = function (input: ReadonlyArray): Las device: now.device, } } - -export default module.exports = generate diff --git a/lib/iob/calculate.ts b/lib/iob/calculate.ts index 99f288e78..11214153b 100644 --- a/lib/iob/calculate.ts +++ b/lib/iob/calculate.ts @@ -7,7 +7,7 @@ interface IobCalcResult { iobContrib?: number } -export default function iobCalc( +export function calculate( treatment: InsulinTreatment, time: Date | undefined, curve: 'bilinear' | string, @@ -146,3 +146,5 @@ function iobCalcExponential(treatment: BolusTreatment, minsAgo: number, dia: num iobContrib: iobContrib, } } + +export default calculate diff --git a/lib/iob/history.ts b/lib/iob/history.ts index 6f80899a8..f742922f8 100644 --- a/lib/iob/history.ts +++ b/lib/iob/history.ts @@ -1,4 +1,6 @@ import { Schema } from '@effect/schema' +import * as A from 'effect/Array' +import * as Order from 'effect/Order' import { tz } from '../date' import * as date from '../date' import * as basalprofile from '../profile/basal' @@ -197,7 +199,7 @@ function splitAroundSuspends( return events } -export default function generate(input: unknown, zeroTempDuration?: number) { +export function generate(input: unknown, zeroTempDuration?: number) { const inputs = Schema.decodeUnknownSync(Input)(input) return findInsulin(inputs, zeroTempDuration) } @@ -246,8 +248,8 @@ export function findInsulin(inputs: Input, zeroTempDuration?: number): InsulinTr } } - pumpSuspends = pumpSuspends.sort((a, b) => a.date - b.date) - pumpResumes = pumpResumes.sort((a, b) => a.date - b.date) + pumpSuspends = A.sort(pumpSuspends, Order.struct({ date: Order.number })) + pumpResumes = A.sort(pumpResumes, Order.struct({ date: Order.number })) if (pumpResumes.length > 0) { firstResumeTime = pumpResumes[0].timestamp @@ -656,4 +658,4 @@ export function findInsulin(inputs: Input, zeroTempDuration?: number): InsulinTr return all_data.sort((a, b) => a.date - b.date) } -//exports = module.exports = generate +export default generate diff --git a/lib/iob/index.ts b/lib/iob/index.ts index 7542a1551..548e70590 100644 --- a/lib/iob/index.ts +++ b/lib/iob/index.ts @@ -4,7 +4,7 @@ import { Input } from './Input' import type { InsulinTreatment } from './InsulinTreatment' import { isBasalTreatment, isBolusTreatment } from './InsulinTreatment' import { findInsulin } from './history' -import sum from './total' +import { iobTotal as sum } from './total' interface IOB { iob: number @@ -33,6 +33,7 @@ export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTrea // calculate IOB based on continuous future zero temping as well treatmentsWithZeroTemp = findInsulin(inputs, 240) } + //console.error(treatments.length, treatmentsWithZeroTemp.length); //console.error(treatments[treatments.length-1], treatmentsWithZeroTemp[treatmentsWithZeroTemp.length-1]) @@ -63,6 +64,7 @@ export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTrea date: new Date(0).getTime(), //clock.getTime()); duration: 0, } + //console.error(treatments[treatments.length-1]); treatments.forEach(treatment => { if (isBolusTreatment(treatment) && treatment.insulin > 0) { @@ -73,8 +75,10 @@ export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTrea //console.error(treatment.insulin,treatment.started_at,lastBolusTime); } else if (isBasalTreatment(treatment) && treatment.duration > 0) { if (treatment.date > lastTemp.date) { - lastTemp = treatment - lastTemp.duration = Math.round(lastTemp.duration * 100) / 100 + lastTemp = { + ...treatment, + duration: Math.round(treatment.duration * 100) / 100, + } } //console.error(treatment.rate, treatment.duration, treatment.started_at,lastTemp.started_at) @@ -106,6 +110,7 @@ export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTrea //console.error(iobArray.length-1, iobArray[iobArray.length-1]); iobArray[iobArray.length - 1].iobWithZeroTemp = iobWithZeroTemp } + //console.error(lastBolusTime); iobArray[0].lastBolusTime = lastBolusTime iobArray[0].lastTemp = lastTemp @@ -113,6 +118,6 @@ export const getIob = (inputs: Input, currentIOBOnly: boolean = false, inputTrea } export default function generate(input: unknown) { - const inputs = Schema.decodeUnknownSync(Input)(input) + const inputs = Schema.decodeUnknownSync(Input)(input, { errors: 'all' }) return getIob(inputs) } diff --git a/lib/iob/total.ts b/lib/iob/total.ts index 08daf8c73..50e7cab18 100644 --- a/lib/iob/total.ts +++ b/lib/iob/total.ts @@ -4,7 +4,7 @@ import { InsulineCurve } from '../types/InsulineCurve' import type { Profile } from '../types/Profile' import type { InsulinTreatment } from './InsulinTreatment' import { isBolusTreatment } from './InsulinTreatment' -import calculate from './calculate' +import { calculate } from './calculate' interface Options { treatments: InsulinTreatment[] @@ -12,9 +12,8 @@ interface Options { autosens?: Autosens | undefined } -export default function iobTotal(opts: Options, time: Date) { +export function iobTotal(opts: Options, time: Date) { const now = time.getTime() - const iobCalc = calculate const treatments = opts.treatments const profile_data = opts.profile let dia = profile_data.dia || 3 @@ -87,7 +86,8 @@ export default function iobTotal(opts: Options, time: Date) { const dia_ago = now - dia * 60 * 60 * 1000 if (treatment.date > dia_ago) { // tIOB = total IOB - const tIOB = iobCalc(treatment, time, curve, dia, peak, profile_data) + const tIOB = calculate(treatment, time, curve, dia, peak, profile_data) + if (tIOB && tIOB.iobContrib) { iob += tIOB.iobContrib } @@ -123,3 +123,5 @@ export default function iobTotal(opts: Options, time: Date) { time: time, } } + +export default iobTotal diff --git a/lib/meal/history.ts b/lib/meal/history.ts index 23e199eaf..07dbcf6f6 100644 --- a/lib/meal/history.ts +++ b/lib/meal/history.ts @@ -30,7 +30,7 @@ const createMeal = (timestamp: string, partial: Partial): TempMea hasCarbs: partial.carbs !== undefined, }) -export default function findMealInputs(inputs: Input): MealTreatment[] { +export function findMeals(inputs: Input): MealTreatment[] { const pumpHistory = inputs.history const carbHistory = inputs.carbs const mealInputs: TempMealTreatment[] = [] @@ -145,3 +145,5 @@ export default function findMealInputs(inputs: Input): MealTreatment[] { }) return sort(dedupeWith(mealInputs, eq), Order) } + +export default findMeals diff --git a/lib/meal/index.ts b/lib/meal/index.ts index b69aefe4c..13271919b 100644 --- a/lib/meal/index.ts +++ b/lib/meal/index.ts @@ -6,8 +6,8 @@ import { GlucoseEntry } from '../types/GlucoseEntry' import { NightscoutTreatment } from '../types/NightscoutTreatment' import { Profile } from '../types/Profile' import { PumpHistoryEvent } from '../types/PumpHistoryEvent' -import find_meals from './history' -import sum from './total' +import { findMeals } from './history' +import { totalRecentCarbs as sum } from './total' const Input = Schema.Struct({ history: Schema.Array(Schema.Union(NightscoutTreatment, PumpHistoryEvent)), @@ -18,9 +18,9 @@ const Input = Schema.Struct({ clock: Schema.String, }) -function generate(input: unknown) { +export function generate(input: unknown) { const inputs = Schema.decodeUnknownSync(Input)(input) - const treatments = find_meals(inputs) + const treatments = findMeals(inputs) const opts = { treatments: treatments, @@ -36,4 +36,4 @@ function generate(input: unknown) { return /* meal_data */ sum(opts, clock) } -export default module.exports = generate +export default generate diff --git a/lib/meal/total.ts b/lib/meal/total.ts index 44d8ab33b..fb6187530 100644 --- a/lib/meal/total.ts +++ b/lib/meal/total.ts @@ -1,7 +1,7 @@ import * as A from 'effect/Array' import { tz } from '../date' import type { DetectCOBInput } from '../determine-basal/cob' -import detectCarbAbsorption from '../determine-basal/cob' +import { detectCarbAbsorption as detectCarbAbsorption } from '../determine-basal/cob' import type { BasalSchedule } from '../types/BasalSchedule' import type { GlucoseEntry } from '../types/GlucoseEntry' import type { NightscoutTreatment } from '../types/NightscoutTreatment' @@ -19,7 +19,7 @@ export interface Options { clock: string } -export default function recentCarbs(opts: Options, time: Date): RecentCarbs { +export function totalRecentCarbs(opts: Options, time: Date): RecentCarbs { let treatments = opts.treatments const profile_data = opts.profile const glucose_data = opts.glucose @@ -174,3 +174,5 @@ export default function recentCarbs(opts: Options, time: Date): RecentCarbs { bwFound: bwFound, } } + +export default totalRecentCarbs diff --git a/lib/medtronic-clock.ts b/lib/medtronic-clock.ts index dd2cf05d4..94ac9b943 100644 --- a/lib/medtronic-clock.ts +++ b/lib/medtronic-clock.ts @@ -1,4 +1,4 @@ -export default function getTime(minutes: number) { +export function getTime(minutes: number) { const baseTime = new Date() baseTime.setHours(0) baseTime.setMinutes(0) @@ -6,3 +6,5 @@ export default function getTime(minutes: number) { return baseTime.getTime() + minutes * 60 * 1000 } + +export default getTime diff --git a/lib/percentile.ts b/lib/percentile.ts index 9d5d23149..52c6e1a94 100644 --- a/lib/percentile.ts +++ b/lib/percentile.ts @@ -1,7 +1,7 @@ // From https://gist.github.com/IceCreamYou/6ffa1b18c4c8f6aeaad2 // Returns the value at a given percentile in a sorted numeric array. // "Linear interpolation between closest ranks" method -export default function percentile(arr: number[], p: number) { +export function percentile(arr: number[], p: number) { if (arr.length === 0) { return 0 } @@ -25,3 +25,5 @@ export default function percentile(arr: number[], p: number) { } return arr[lower] * (1 - weight) + arr[upper] * weight } + +export default percentile diff --git a/lib/profile/carbs.ts b/lib/profile/carbs.ts index 2484192fa..4cbec0bcf 100644 --- a/lib/profile/carbs.ts +++ b/lib/profile/carbs.ts @@ -1,6 +1,6 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' -import getTime from '../medtronic-clock' +import { getTime } from '../medtronic-clock' import type { Preferences } from '../types/Preferences' export function carbRatioLookup(final_result: FinalResult, inputs: Preferences) { diff --git a/lib/profile/index.ts b/lib/profile/index.ts index d5185264c..9f42569c4 100644 --- a/lib/profile/index.ts +++ b/lib/profile/index.ts @@ -1,9 +1,10 @@ import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' +import type { BasalSchedule } from '../types/BasalSchedule' import type { Preferences } from '../types/Preferences' import { maxDailyBasal, basalLookup, maxBasalLookup } from './basal' //import { carbRatioLookup } from './carbs' -import carbRatioLookup from './carbs' +import { carbRatioLookup } from './carbs' import { isfLookup } from './isf' import * as targets from './targets' @@ -100,7 +101,7 @@ export function displayedDefaults(final_result: FinalResult) { return profile } -export default function generate(final_result: FinalResult, inputs: Preferences, opts: any) { +function generate(final_result: FinalResult, inputs: Preferences, opts?: any) { const profile = opts && opts.type ? opts : defaults() const preferences = inputs @@ -136,9 +137,9 @@ export default function generate(final_result: FinalResult, inputs: Preferences, console.error('ERROR: bad basal schedule', profile.current_basal) return -1 } - profile.basalprofile = preferences.basals.map((basalentry: any) => ({ + profile.basalprofile = preferences.basals.map((basalentry: BasalSchedule) => ({ ...basalentry, - rate: Math.round(basalentry.rate * 100) / 100, + rate: Math.round(basalentry.rate * 1000) / 1000, })) profile.max_daily_basal = maxDailyBasal(preferences) @@ -188,3 +189,5 @@ export default function generate(final_result: FinalResult, inputs: Preferences, return profile } + +export default generate diff --git a/lib/profile/targets.ts b/lib/profile/targets.ts index 182feaa1e..e5cceeaa7 100644 --- a/lib/profile/targets.ts +++ b/lib/profile/targets.ts @@ -1,7 +1,7 @@ import * as A from 'effect/Array' import type { FinalResult } from '../bin/utils' import { console_error } from '../bin/utils' -import getTime from '../medtronic-clock' +import { getTime } from '../medtronic-clock' import type { Preferences } from '../types/Preferences' import * as TempTarget from '../types/TempTarget' diff --git a/lib/pump.js b/lib/pump.ts similarity index 56% rename from lib/pump.js rename to lib/pump.ts index 6c61959b7..e985cb074 100644 --- a/lib/pump.js +++ b/lib/pump.ts @@ -1,15 +1,14 @@ -'use strict' +export function translate(treatments: ReadonlyArray): A[] { + const results: any[] = [] -function translate(treatments) { - const results = [] - - function step(current) { + function step(current: any) { let invalid = false - switch (current._type) { + const item = { ...current } + switch (item._type) { case 'CalBGForPH': - current.eventType = 'BG Check' - current.glucose = current.amount - current.glucoseType = 'Finger' + item.eventType = 'BG Check' + item.glucose = item.amount + item.glucoseType = 'Finger' break case 'BasalProfileStart': case 'ResultDailyTotal': @@ -24,11 +23,11 @@ function translate(treatments) { } if (!invalid) { - results.push(current) + results.push(item) } } treatments.forEach(step) return results } -exports = module.exports = translate +export default translate diff --git a/lib/temps.js b/lib/temps.ts similarity index 50% rename from lib/temps.js rename to lib/temps.ts index e7f06c7f1..709d8ff2e 100644 --- a/lib/temps.js +++ b/lib/temps.ts @@ -1,17 +1,27 @@ -'use strict' +import type { NightscoutTreatment } from './types/NightscoutTreatment' +import type { PumpHistoryEvent } from './types/PumpHistoryEvent' -function filter(treatments) { - const results = [] +export function filter(treatments: ReadonlyArray) { + const results: any[] = [] - let state = {} - function temp(ev) { + let state: { + eventType?: string + invalid?: boolean + duration?: string + raw_duration?: PumpHistoryEvent + raw_rate?: PumpHistoryEvent + rate?: number | undefined + timestamp?: string | undefined + [k: string]: unknown + } = {} + function temp(ev: NightscoutTreatment | PumpHistoryEvent) { if ('duration (min)' in ev) { - state.duration = ev['duration (min)'].toString() + state.duration = ev['duration (min)']!.toString() state.raw_duration = ev } - if ('rate' in ev) { - state[ev.temp] = ev.rate.toString() + if ('rate' in ev && 'temp' in ev && ev.temp) { + state[ev.temp] = ev.rate!.toString() state.rate = ev.rate state.raw_rate = ev } @@ -29,7 +39,7 @@ function filter(treatments) { } } - function step(current) { + function step(current: any) { switch (current._type) { case 'TempBasalDuration': case 'TempBasal': @@ -44,4 +54,4 @@ function filter(treatments) { return results } -exports = module.exports = filter +export default filter diff --git a/lib/types/Autosens.ts b/lib/types/Autosens.ts index 2292b2ecd..59244771d 100644 --- a/lib/types/Autosens.ts +++ b/lib/types/Autosens.ts @@ -1,14 +1,9 @@ import { Schema } from '@effect/schema' -import * as O from 'effect/Order' -export const Autosens = Schema.Struct({ - timestamp: Schema.String, +export const Autosens = /*#__PURE__*/ Schema.Struct({ + timestamp: Schema.optional(Schema.String), ratio: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0)), newisf: Schema.optional(Schema.Number), }) export type Autosens = typeof Autosens.Type - -export const Order: O.Order = O.struct({ - timestamp: (a, b) => O.Date(new Date(a.timestamp), new Date(b.timestamp)), -}) diff --git a/lib/types/GlucoseEntry.ts b/lib/types/GlucoseEntry.ts index fb1ea9cf1..ae7448720 100644 --- a/lib/types/GlucoseEntry.ts +++ b/lib/types/GlucoseEntry.ts @@ -1,6 +1,8 @@ import { Schema } from '@effect/schema' -import { flow, identity, String } from 'effect' +import { flow, identity, pipe, String } from 'effect' +import * as A from 'effect/Array' import * as E from 'effect/Either' +import * as Option from 'effect/Option' import * as O from 'effect/Order' export const GlucoseType = Schema.Union(Schema.Literal('sgv', 'cal'), Schema.String) @@ -25,24 +27,24 @@ const DateString = Schema.String.pipe(validParsableDate(Schema.DateFromString)) const GlucoseField = Schema.Struct({ glucose: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), }) +/* const NightscoutSgvField = Schema.Struct({ sgv: Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), }) +*/ -export const GlucoseEntry = Schema.extend( - Schema.Union(GlucoseField, NightscoutSgvField), - Schema.Struct({ - date: Schema.optional(Schema.Int.pipe(validParsableDate(Schema.DateFromNumber))), - display_time: Schema.optional(DisplayTime), - dateString: Schema.optional(DateString), - //sgv: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), - //glucose: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), - type: Schema.optional(GlucoseType), - device: Schema.optional(Schema.String), - noise: Schema.optional(Schema.Number), - xDrip_started_at: Schema.optional(Schema.Unknown), - }) -).annotations({ +// @todo: another glucose meter exists: `mbg`. Should we skip it? +export const GlucoseEntry = Schema.Struct({ + date: Schema.optional(Schema.Int.pipe(validParsableDate(Schema.DateFromNumber))), + display_time: Schema.optional(DisplayTime), + dateString: Schema.optional(DateString), + sgv: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + glucose: Schema.optional(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0))), + type: Schema.optional(GlucoseType), + device: Schema.optional(Schema.String), + noise: Schema.optional(Schema.Number), + xDrip_started_at: Schema.optional(Schema.Unknown), +}).annotations({ title: 'GlucoseEntry', description: 'Glucose Entry', }) @@ -51,6 +53,64 @@ export type GlucoseEntry = typeof GlucoseEntry.Type export const getGlucose = (entry: GlucoseEntry) => (Schema.is(GlucoseField)(entry) ? entry.glucose : entry.sgv) +/** + * Set the `glucose` field from `svg` if not already set. + */ +export const setGlucoseField = (a: A) => ({ + ...a, + glucose: getGlucose(a), +}) + +export const hasGlucose = (a: A): a is A & { glucose: number } => a.glucose !== undefined + +export const filterWithGlucose = (a: A) => pipe(Option.some(a), Option.filter(hasGlucose)) + +/** + * Retrieve the record date and set `date` and `dateString` fields. + * + * @throws {Error} on record with invalid date + */ +export const setDateFields = (a: A) => { + const date = getDate(a) + return setDate(date)(a) +} + +/** + * Set `date` and `dateString` fields. + */ +export const setDate = + (date: Date) => + (entry: A): A & { date: number; dateString: string } => ({ + ...entry, + date: date.getTime(), + dateString: date.toISOString(), + }) + +/** + * Reduce filtering records with only readable glucose (`glucose` or `sgv`). + * The `glucose` field will be set. + */ +export const reduceWithGlucose = (iter: Iterable) => + A.reduce(iter, [] as Array, (b, a) => { + const glucose = getGlucose(a) + return glucose ? [...b, { ...a, glucose }] : b + }) + +/** + * Reduce filtering records with only readable glucose (`glucose` or `sgv`). + * The `glucose` and `date` fields will be set. + * + * @throws {Error} on record with invalid date + */ +export const reduceWithGlucoseAndDate = (iter: Iterable) => + A.reduce(iter, [] as Array, (b, a) => { + const glucose = getGlucose(a) + return glucose ? [...b, setDateFields({ ...a, glucose })] : b + }) + +/** + * @throws {Error} on record with invalid date + */ export const getDate = (entry: GlucoseEntry): Date => { if (entry.date) { return Schema.decodeSync(Schema.DateFromNumber)(entry.date) diff --git a/lib/types/NightscoutTreatment.ts b/lib/types/NightscoutTreatment.ts index 8bc641247..988994eff 100644 --- a/lib/types/NightscoutTreatment.ts +++ b/lib/types/NightscoutTreatment.ts @@ -1,35 +1,183 @@ import { Schema } from '@effect/schema' import * as O from 'effect/Order' -import { EventType } from './EventType' import { PumpHistoryEvent } from './PumpHistoryEvent' +// https://github.com/nightscout/cgm-remote-monitor/blob/21e0591d49235845acba58cf8b3cc7339921185b/lib/api3/swagger.json + +const EventType = Schema.NonEmptyString.annotations({ + description: 'The type of treatment event', + examples: [ + 'BG Check', + 'Snack Bolus', + 'Meal Bolus', + 'Correction Bolus', + 'Carb Correction', + 'Combo Bolus', + 'Announcement', + 'Note', + 'Question', + 'Exercise', + 'Site Change', + 'Sensor Start', + 'Sensor Change', + 'Pump Battery Change', + 'Insulin Change', + 'Temp Basal', + 'Profile Switch', + 'D.A.D. Alert', + 'Temporary Target', + 'OpenAPS Offline', + 'Bolus Wizard', + ], +}) + export const NightscoutTreatment = Schema.Struct({ eventType: EventType, - created_at: Schema.String, + created_at: Schema.NonEmptyString, id: Schema.optionalWith(Schema.String, { nullable: true }), - duration: Schema.optionalWith(Schema.Number, { nullable: true }), + + // from Nightscout DocumentBase + identifier: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: + 'Main addressing, required field that identifies document in the collection.\n\nThe client should not create the identifier, the server automatically assigns it when the document is inserted.\n\nThe server calculates the identifier in such a way that duplicate records are automatically merged (deduplicating is made by `date`, `device` and `eventType` fields).\n\nThe best practise for all applications is not to loose identifiers from received documents, but save them carefully for other consumer applications/systems.\n\nAPI v3 has a fallback mechanism in place, for documents without `identifier` field the `identifier` is set to internal `_id`, when reading or addressing these documents.\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: ['53409478-105f-11e9-ab14-d663bd873d93'], + }), + date: Schema.optional(Schema.Int).annotations({ + description: + "Required timestamp when the record or event occured, you can choose from three input formats\n- Unix epoch in milliseconds (1525383610088)\n- Unix epoch in seconds (1525383610)\n- ISO 8601 with optional timezone ('2018-05-03T21:40:10.088Z' or '2018-05-03T23:40:10.088+02:00')\n\nThe date is always stored in a normalized form - UTC with zero offset. If UTC offset was present, it is going to be set in the `utcOffset` field.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + examples: [1525383610088], + }), + utcOffset: Schema.optional(Schema.Int).annotations({ + description: + 'Local UTC offset (timezone) of the event in minutes. This field can be set either directly by the client (in the incoming document) or it is automatically parsed from the `date` field.\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: [120], + }), + app: Schema.optional(Schema.String).annotations({ + description: + 'Application or system in which the record was entered by human or device for the first time.\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: ['xdrip'], + }), + device: Schema.optional(Schema.String).annotations({ + description: + 'The device from which the data originated (including serial number of the device, if it is relevant and safe).\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: ['dexcom G5'], + }), + _id: Schema.optional(Schema.String).annotations({ + description: + 'Internally assigned database id. This field is for internal server purposes only, clients communicate with API by using identifier field.', + examples: ['58e9dfbc166d88cc18683aac'], + }), + srvCreated: Schema.optional(Schema.Int).annotations({ + description: + "The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3.\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + examples: [1525383610088], + }), + subject: Schema.optional(Schema.String).annotations({ + description: + 'Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed JWT.\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: ['uploader'], + }), + srvModified: Schema.optional(Schema.Int).annotations({ + description: + "The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted).\n\nNote: this field is immutable by the client (it cannot be updated or patched)", + examples: [1525383610088], + }), + modifiedBy: Schema.optional(Schema.String).annotations({ + description: + 'Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server.\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: ['admin'], + }), + isValid: Schema.optional(Schema.Boolean).annotations({ + description: + 'A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value)\n\nNote: this field is immutable by the client (it cannot be updated or patched)', + examples: [false], + }), + isReadOnly: Schema.optional(Schema.Boolean).annotations({ + description: + 'A flag set by client that locks the document from any changes. Every document marked with `isReadOnly=true` is forever immutable and cannot even be deleted.\n\nAny attempt to modify the read-only document will end with status 422 UNPROCESSABLE ENTITY.', + examples: [true], + }), + + // from Nightscout Treatment + glucose: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Current glucose', + }), + glucoseType: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: 'Method used to obtain glucose, Finger or Sensor', + examples: ['Sensor', 'Finger', 'Manual'], + }), + units: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: + 'The units for the glucose value, mg/dl or mmol/l. It is strongly recommended to fill in this field when `glucose` is entered', + examples: ['mg/dl', 'mmol/l'], + }), + carbs: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Amount of carbs given', + }), + protein: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Amount of protein given', + }), + fat: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Amount of fat given', + }), + insulin: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Amount of insulin, if any', + }), + duration: Schema.optionalWith(Schema.Int, { nullable: true }).annotations({ + description: 'Duration in minutes', + }), + preBolus: Schema.optionalWith(Schema.Int, { nullable: true }).annotations({ + description: 'How many minutes the bolus was given before the meal started', + }), + splitNow: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Immediate part of combo bolus (in percent)', + }), + splitExt: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Extended part of combo bolus (in percent)', + }), + percent: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Eventual basal change in percent', + }), + absolute: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Eventual basal change in absolute value (insulin units per hour)', + }), + targetTop: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Top limit of temporary target', + }), + targetBottom: Schema.optionalWith(Schema.Number, { nullable: true }).annotations({ + description: 'Bottom limit of temporary target', + }), + profile: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: 'Name of the profile to which the pump has been switched', + }), + reason: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: + 'For example the reason why the profile has been switched or why the temporary target has been set', + }), + notes: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: 'Description/notes of treatment', + }), + enteredBy: Schema.optionalWith(Schema.String, { nullable: true }).annotations({ + description: 'Who entered the treatment', + }), + + // not found in Nightscout swagger doc + timestamp: Schema.optionalWith(Schema.String, { nullable: true }), + ratio: Schema.optionalWith(Schema.Number, { nullable: true }), rawDuration: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), rawRate: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), - absolute: Schema.optionalWith(Schema.Number, { nullable: true }), - rate: Schema.optionalWith(Schema.Number, { nullable: true }), - enteredBy: Schema.optionalWith(Schema.String, { nullable: true }), bolus: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), - insulin: Schema.optionalWith(Schema.Number, { nullable: true }), - notes: Schema.optionalWith(Schema.String, { nullable: true }), - carbs: Schema.optionalWith(Schema.Number, { nullable: true }), - fat: Schema.optionalWith(Schema.Number, { nullable: true }), - protein: Schema.optionalWith(Schema.Number, { nullable: true }), + wizard: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), + square: Schema.optionalWith(PumpHistoryEvent, { nullable: true }), + rate: Schema.optionalWith(Schema.Number, { nullable: true }), foodType: Schema.optionalWith(Schema.String, { nullable: true }), - targetTop: Schema.optionalWith(Schema.Number, { nullable: true }), - targetBottom: Schema.optionalWith(Schema.Number, { nullable: true }), - glucoseType: Schema.optionalWith(Schema.String, { nullable: true }), - glucose: Schema.optionalWith(Schema.Number, { nullable: true }), - units: Schema.optionalWith(Schema.String, { nullable: true }), fpuID: Schema.optionalWith(Schema.String, { nullable: true }), amount: Schema.optionalWith(Schema.Number, { nullable: true }), + bg: Schema.optionalWith(Schema.Number, { nullable: true }), }).annotations({ identifier: 'NightscoutTreatment', title: 'Nightscout Treatment', + description: 'T1D compensation action', }) export type NightscoutTreatment = typeof NightscoutTreatment.Type diff --git a/lib/types/Preferences.ts b/lib/types/Preferences.ts index 167e3bf10..5d177c5e3 100644 --- a/lib/types/Preferences.ts +++ b/lib/types/Preferences.ts @@ -41,7 +41,7 @@ type Targets = typeof Targets.Type const Basals = Schema.NonEmptyArray(BasalSchedule) type Basals = typeof Basals.Type -export const Preferences = Schema.Struct({ +export const Preferences = /*#__PURE__*/ Schema.Struct({ max_iob: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { nullable: true, default: () => 0, diff --git a/lib/types/Profile.ts b/lib/types/Profile.ts index ac2bdd686..bbc53589c 100644 --- a/lib/types/Profile.ts +++ b/lib/types/Profile.ts @@ -14,7 +14,7 @@ export const ISFProfile = Schema.Struct({ export type ISFProfile = typeof ISFProfile.Type -export const ProfileDefaults = Schema.Struct({ +export const ProfileDefaults = /*#__PURE__*/ Schema.Struct({ max_iob: Schema.optionalWith(Schema.Int.pipe(Schema.greaterThanOrEqualTo(0)), { nullable: true, default: () => 0, @@ -151,12 +151,12 @@ export interface ProfileDefaults extends Schema.Schema.Type 1 && Math.abs(glucose[0].glucose - glucose[1].glucose) < 5) { @@ -95,13 +95,25 @@ function generate(iob, currenttemp, glucose, profile, autosens = null, meal = nu } } var glucose_status = getLastGlucose(glucose); - + // In case Basal Rate been set in midleware if (profile.set_basal && profile.basal_rate) { console.log("Basal Rate set by middleware to " + profile.basal_rate + " U/h."); } - - return determine_basal(glucose_status, currenttemp, iob, profile, autosens_data, meal_data, basalFunctions, microbolusAllowed, reservoir_data, clock); + + const input = { + glucose: glucose_status, + currenttemp: currenttemp, + iobTicks: iob, + profile: profile, + autosens: autosens_data, + meal: meal_data, + microBolusAllowed: microbolusAllowed, + reservoir: reservoir_data, + currentTime: clock ? new Date(clock) : undefined, + } + + return determine_basal(input); } // The Dynamic ISF layer @@ -113,9 +125,9 @@ function dynisf(profile, autosens_data, dynamicVariables, glucose) { if (profile.highTemptargetRaisesSensitivity || profile.exerciseMode || dynamicVariables.isEnabled) { exerciseSetting = true; } - + const target = profile.min_bg; - + // Turn dynISF off when using a temp target >= 118 (6.5 mol/l) and if an exercise setting is enabled. if (target >= 118 && exerciseSetting) { //dynISFenabled = false; @@ -148,7 +160,7 @@ function dynisf(profile, autosens_data, dynamicVariables, glucose) { insulinPA = 50; break; } - + if (ucpk) { insulinFactor = 120 - ipt; console.log("Custom insulinpeakTime set to: " + ipt + ", " + "insulinFactor: " + insulinFactor); @@ -173,13 +185,13 @@ function dynisf(profile, autosens_data, dynamicVariables, glucose) { // Account for TDD of insulin. Compare last 2 hours with total data (up to 10 days) var tdd_factor = weighted_average / average14; // weighted average TDD / total data average TDD - + const enable_sigmoid = profile.sigmoid; var newRatio = 1; const sensitivity = profile.sens; const adjustmentFactor = profile.adjustmentFactor; - + var BG = 100; if (glucose.length > 0) { BG = glucose[0].glucose; diff --git a/tests/determine-basal.test.ts b/tests/determine-basal.test.ts index bfbdda2db..fc07407f8 100644 --- a/tests/determine-basal.test.ts +++ b/tests/determine-basal.test.ts @@ -1,8 +1,9 @@ -var should = require('should'); +require('should') +import round_basal from '../lib/round-basal' +import { determine_basal } from '../lib/determine-basal/determine-basal'; +import * as tempBasalFunctions from '../lib/basal-set-temp'; describe('round_basal', function ( ) { - var round_basal = require('../lib/round-basal'); - it('should round correctly without profile being passed in', function() { var basal = 0.025; var output = round_basal(basal); @@ -56,9 +57,6 @@ describe('round_basal', function ( ) { }); describe('determine-basal', function ( ) { - var determine_basal = require('../lib/determine-basal/determine-basal'); - var tempBasalFunctions = require('../lib/basal-set-temp'); - //function determine_basal(glucose_status, currenttemp, iob_data, profile) // standard initial conditions for all determine-basal test cases unless overridden @@ -70,7 +68,7 @@ describe('determine-basal', function ( ) { var meal_data = {"carbs":50,"nsCarbs":50,"bwCarbs":0,"journalCarbs":0,"mealCOB":0,"currentDeviation":0,"maxDeviation":0,"minDeviation":0,"slopeFromMaxDeviation":0,"slopeFromMinDeviation":0,"allDeviations":[0,0,0,0,0],"bwFound":false} it('should cancel high temp when in range w/o IOB', function () { - var currenttemp = {"duration":30,"rate":1.5,"temp":"absolute"}; + var currenttemp = { "duration": 30, "rate": 1.5, "temp": "absolute" }; var output = determine_basal(glucose_status, currenttemp, iob_data, profile, autosens, meal_data, tempBasalFunctions); //output.rate.should.equal(0); //output.duration.should.equal(0); @@ -445,10 +443,13 @@ describe('determine-basal', function ( ) { //output.reason.should.match(/.* > 2.*insulinReq. Setting temp.*/); //}); + // @todo enable when testing the Profile Schema + /* it('should profile.current_basal be undefined return error', function () { var result = determine_basal(undefined,undefined,undefined,undefined); result.error.should.equal('Error: could not get current basal rate'); }); + */ it('should let low-temp run when bg < 30 (Dexcom is in ???)', function () { var currenttemp = {"duration":30,"rate":0,"temp":"absolute"}; @@ -466,10 +467,13 @@ describe('determine-basal', function ( ) { output.reason.should.match(/Replacing high temp/); }); + // @todo enable when testing the Profile Schema + /* it('profile should contain min_bg,max_bg', function () { - var result = determine_basal({glucose:100},undefined, undefined, {"current_basal":0.0}, autosens, meal_data, tempBasalFunctions); - result.error.should.equal('Error: could not determine target_bg. '); + var result = determine_basal({ glucose: 100 }, undefined, undefined, { "current_basal": 0.0 }, autosens, meal_data, tempBasalFunctions); + //result.error.should.equal('Error: could not determine target_bg. '); }); + */ it('iob_data should not be undefined', function () { var result = determine_basal({glucose:100},undefined, undefined, {"current_basal":0.0, "max_bg":100,"min_bg":1100}, autosens, meal_data, tempBasalFunctions); diff --git a/tests/get-last-glucose.test.ts b/tests/get-last-glucose.test.ts index c7ec95651..130fe9804 100644 --- a/tests/get-last-glucose.test.ts +++ b/tests/get-last-glucose.test.ts @@ -1,11 +1,9 @@ -'use strict'; require('should'); +import { getLastGlucose } from '../lib/glucose-get-last' describe('getLastGlucose', function ( ) { - var getLastGlucose = require('../lib/glucose-get-last'); - it('should handle NS sgv fields', function () { var glucose_status = getLastGlucose([{date: 1467942845000, sgv: 100}, {date: 1467942544500, sgv: 95}, {date: 1467942244000, sgv: 85}, {date: 1467941944000, sgv: 70}]); //console.log(glucose_status); diff --git a/tests/glucose-noise.test.ts b/tests/glucose-noise.test.ts index b4977c250..eac8826e4 100644 --- a/tests/glucose-noise.test.ts +++ b/tests/glucose-noise.test.ts @@ -1,9 +1,7 @@ -'use strict'; require('should'); -var moment = require('moment'); -var stats = require('../lib/calc-glucose-stats'); +import * as stats from '../lib/calc-glucose-stats' describe('NOISE', function() { it('should calculate Clean Sensor Noise', () => { diff --git a/tests/iob.test.ts b/tests/iob.test.ts index cce380be6..487ef66ba 100644 --- a/tests/iob.test.ts +++ b/tests/iob.test.ts @@ -1,9 +1,9 @@ -'use strict'; require('should'); -var moment = require('moment'); -var iob = require('../lib/iob'); +import moment from 'moment' +import iob from '../lib/iob' +import { Preferences } from '../lib/types/Preferences'; describe('IOB', function() { @@ -16,7 +16,7 @@ describe('IOB', function() { 'minutes': 0 }]; - var now = Date.now(), + var now = Date.parse('2024-08-13T13:30:00.000Z'), timestamp = new Date(now).toISOString(), inputs = { clock: timestamp, @@ -30,7 +30,11 @@ describe('IOB', function() { //bolussnooze_dia_divisor: 2, basalprofile: basalprofile, current_basal: 1, - max_daily_basal: 1 + max_daily_basal: 1, + sens: 5, + min_bg: 100, + max_bg: 100, + curve: 'bilinear', } }; @@ -80,7 +84,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'ultra-rapid' + curve: 'ultra-rapid', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -132,7 +139,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'ultra-rapid' + curve: 'ultra-rapid', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -183,7 +193,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'ultra-rapid' + curve: 'ultra-rapid', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -236,7 +249,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'ultra-rapid' + curve: 'ultra-rapid', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -290,7 +306,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'ultra-rapid' + curve: 'ultra-rapid', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -342,7 +361,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'rapid-acting' + curve: 'rapid-acting', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -389,7 +411,10 @@ describe('IOB', function() { basalprofile: basalprofile, current_basal: 1, max_daily_basal: 1, - curve: 'rapid-acting' + curve: 'rapid-acting', + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -472,7 +497,10 @@ describe('IOB', function() { current_basal: 1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -480,8 +508,6 @@ describe('IOB', function() { var iobRes = iob(iobInputs) var iobNow = iobRes[0]; - console.log('iobRes', iobRes) - //console.log(iobNow); iobNow.iob.should.be.lessThan(1); iobNow.iob.should.be.greaterThan(0.5); @@ -533,12 +559,18 @@ describe('IOB', function() { profile: { dia: 3, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + current_basal: 1, + max_daily_basal: 1, + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, + curve: 'bilinear', } }; var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 01:30:00.000'); + hourLaterInputs.clock = new Date('2016-06-13 01:30:00.000').toISOString(); var hourLater = iob(hourLaterInputs)[0]; hourLater.iob.should.be.lessThan(0.5); hourLater.iob.should.be.greaterThan(0.4); @@ -597,12 +629,16 @@ describe('IOB', function() { current_basal: 0.1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, + curve: 'bilinear', } }; var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 00:45:00.000'); //new Date(now + (30 * 60 * 1000)).toISOString(); + hourLaterInputs.clock = new Date('2016-06-13 00:45:00.000').toISOString(); var hourLater = iob(hourLaterInputs)[0]; hourLater.iob.should.be.lessThan(0.8); @@ -619,7 +655,7 @@ describe('IOB', function() { }]; var startingPoint = moment('2016-06-13 00:30:00.000'); - var timestamp = startingPoint; + var timestamp = startingPoint.toISOString(); var timestampEarly = startingPoint.clone().subtract(30, 'minutes'); var inputs = { @@ -640,7 +676,11 @@ describe('IOB', function() { current_basal: 0.1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, + curve: 'bilinear', } }; @@ -667,12 +707,15 @@ describe('IOB', function() { current_basal: 0.1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-13 00:30:00.000'); + hourLaterInputs.clock = new Date('2016-06-13 00:30:00.000').toISOString(); var hourLater = iob(hourLaterInputs)[0]; var inputs = { @@ -713,7 +756,10 @@ describe('IOB', function() { current_basal: 0.1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -772,12 +818,15 @@ describe('IOB', function() { current_basal: 0.1, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - basalprofile: basalprofile + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; var hourLaterInputs = inputs; - hourLaterInputs.clock = moment('2016-06-14 00:45:00.000'); + hourLaterInputs.clock = new Date('2016-06-14 00:45:00.000').toISOString(); var hourLater = iob(hourLaterInputs)[0]; hourLater.iob.should.be.lessThan(1); @@ -829,7 +878,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -865,7 +917,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -931,7 +986,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -966,7 +1024,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1033,7 +1094,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1088,7 +1152,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1144,7 +1211,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1178,7 +1248,10 @@ describe('IOB', function() { current_basal: 1, suspend_zeros_iob: true, max_daily_basal: 1, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1236,7 +1309,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1267,7 +1343,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1323,7 +1402,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1354,7 +1436,10 @@ describe('IOB', function() { suspend_zeros_iob: true, max_daily_basal: 1, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1384,34 +1469,37 @@ describe('IOB', function() { var endPoint = new Date('2016-06-13 01:00:00.000'); var inputs = { - clock: endPoint, + clock: endPoint.toISOString(), history: [{ _type: 'TempBasalDuration', 'duration (min)': 30, - date: startingPoint, - timestamp: startingPoint + date: startingPoint.getTime(), + timestamp: startingPoint.toISOString() }, { _type: 'TempBasal', rate: 0.1, - date: startingPoint, - timestamp: startingPoint + date: startingPoint.getTime(), + timestamp: startingPoint.toISOString() }, { _type: 'TempBasal', rate: 2, - date: startingPoint2, - timestamp: startingPoint2 + date: startingPoint2.getTime(), + timestamp: startingPoint2.toISOString() }, { _type: 'TempBasalDuration', 'duration (min)': 30, - date: startingPoint2, - timestamp: startingPoint2 + date: startingPoint2.getTime(), + timestamp: startingPoint2.toISOString() }], profile: { dia: 3, current_basal: 2, max_daily_basal: 2, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1459,7 +1547,10 @@ describe('IOB', function() { dia: 3, current_basal: 1, max_daily_basal: 1, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1511,7 +1602,10 @@ describe('IOB', function() { current_basal: 2, max_daily_basal: 2, //bolussnooze_dia_divisor: 2, - 'basalprofile': basalprofile + 'basalprofile': basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1523,6 +1617,8 @@ describe('IOB', function() { hourLater.iob.should.be.greaterThan(-1); }); + // oref 0.8: basalprofile should be always defined + /* it('should show 0 IOB with Temp Basals if duration is not found', function() { var now = Date.now(), @@ -1541,7 +1637,10 @@ describe('IOB', function() { dia: 3, current_basal: 1, max_daily_basal: 1, - //bolussnooze_dia_divisor: 2 + //bolussnooze_dia_divisor: 2, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1551,9 +1650,17 @@ describe('IOB', function() { hourLater.iob.should.equal(0); }); + */ it('should show 0 IOB with Temp Basals if basal is percentage based', function() { + var basalprofile = [{ + 'i': 0, + 'start': '00:00:00', + 'rate': 2, + 'minutes': 0 + }]; + var now = Date.now(), timestamp = new Date(now).toISOString(), timestampEarly = new Date(now - (60 * 60 * 1000)).toISOString(), @@ -1577,7 +1684,11 @@ describe('IOB', function() { dia: 3, current_basal: 1, max_daily_basal: 1, - //bolussnooze_dia_divisor: 2 + //bolussnooze_dia_divisor: 2, + basalprofile: basalprofile, + sens: 5, + min_bg: 100, + max_bg: 100, } }; @@ -1612,7 +1723,11 @@ describe('IOB', function() { //bolussnooze_dia_divisor: 2, basalprofile: basalprofile, current_basal: 1, - max_daily_basal: 1 + max_daily_basal: 1, + sens: 5, + min_bg: 100, + max_bg: 100, + curve: 'bilinear', } }; diff --git a/tests/profile.test.ts b/tests/profile.test.ts index 49c639eae..7c91dac2e 100644 --- a/tests/profile.test.ts +++ b/tests/profile.test.ts @@ -1,9 +1,9 @@ -'use strict'; import { initFinalResults } from "../lib/bin/utils"; +import generate from '../lib/profile' require('should'); -var _ = require('lodash'); +import _ from 'lodash' describe('Profile', function ( ) { @@ -36,7 +36,7 @@ describe('Profile', function ( ) { it('should should create a profile from inputs', function () { const finalResult = initFinalResults() - var profile = require('../lib/profile')(finalResult, baseInputs); + var profile = generate(finalResult, baseInputs); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -51,7 +51,7 @@ describe('Profile', function ( ) { var creationDate = new Date(currentTime.getTime() - (5 * 60 * 1000)); it('should create a profile with temptarget set', function() { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': creationDate}]})); + var profile = generate(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': creationDate}]})); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -65,7 +65,7 @@ describe('Profile', function ( ) { var pastDate = new Date(currentTime.getTime() - 90*60*1000); it('should create a profile ignoring an out of date temptarget', function() { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': pastDate}]})); + var profile = generate(initFinalResults(), _.merge({}, baseInputs, { temptargets: [{'eventType':'Temporary Target', 'reason':'Eating Soon', 'targetTop':80, 'targetBottom':80, 'duration':20, 'created_at': pastDate}]})); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -76,7 +76,7 @@ describe('Profile', function ( ) { }); it('should create a profile ignoring a temptarget with 0 duration', function() { - var profile = require('../lib/profile')({}, _.merge({}, baseInputs, { temptargets: [{ 'eventType': 'Temporary Target', 'reason': 'Eating Soon', 'targetTop': 80, 'targetBottom': 80, 'duration': 0, 'created_at': creationDate }] })); + var profile = generate({}, _.merge({}, baseInputs, { temptargets: [{ 'eventType': 'Temporary Target', 'reason': 'Eating Soon', 'targetTop': 80, 'targetBottom': 80, 'duration': 0, 'created_at': creationDate }] })); profile.max_iob.should.equal(0); profile.dia.should.equal(3); profile.sens.should.equal(100); @@ -88,18 +88,18 @@ describe('Profile', function ( ) { it('should error with invalid DIA', function () { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, {settings: {insulin_action_curve: 1}})); + var profile = generate(initFinalResults(), _.merge({}, baseInputs, {settings: {insulin_action_curve: 1}})); profile.should.equal(-1); }); it('should error with a current basal of 0', function () { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { basals: [{ minutes: 0, rate: 0 }] })); + var profile = generate(initFinalResults(), _.merge({}, baseInputs, { basals: [{ minutes: 0, rate: 0 }] })); profile.should.equal(-1); }); it('should set the profile model from input', function () { - var profile = require('../lib/profile')(initFinalResults(), _.merge({}, baseInputs, { model: 554 })); + var profile = generate(initFinalResults(), _.merge({}, baseInputs, { model: 554 })); profile.model.should.equal(554); }); diff --git a/tests/set-temp-basal.test.ts b/tests/set-temp-basal.test.ts index 171d7337b..e2de73e1e 100644 --- a/tests/set-temp-basal.test.ts +++ b/tests/set-temp-basal.test.ts @@ -1,10 +1,9 @@ -'use strict'; require('should'); +import * as tempBasalFunctions from '../lib/basal-set-temp' describe('tempBasalFunctions.setTempBasal', function ( ) { - var tempBasalFunctions = require('../lib/basal-set-temp'); //function tempBasalFunctions.setTempBasal(rate, duration, profile, requestedTemp) @@ -57,7 +56,7 @@ describe('tempBasalFunctions.setTempBasal', function ( ) { requestedTemp.rate.should.equal(2.8); requestedTemp.duration.should.equal(30); }); - + it('should temp to 0 when requested rate is less then 0 * current_basal', function () { var profile = { "current_basal":0.7,"max_daily_basal":1.3,"max_basal":10.0 }; var requestedTemp = tempBasalFunctions.setTempBasal(-1, 30, profile, rt); diff --git a/tests/tests-in-shell.test.ts b/tests/tests-in-shell.test.ts index 4cdfba916..e6bf00e90 100644 --- a/tests/tests-in-shell.test.ts +++ b/tests/tests-in-shell.test.ts @@ -2,7 +2,7 @@ // oref0/tests directory whose name ends in .sh, generates a separate test // which runs it and asserts that it exits with status 0 (success). -var should = require('should'); +require('should') import * as fs from 'fs' import * as path from 'path' import * as child_process from 'child_process' @@ -13,14 +13,14 @@ describe("shell-script tests", function() { if(filename.endsWith(".sh")) bashUnitTestFiles.push(path.join("tests", filename)); }); - + bashUnitTestFiles.forEach(function(testFile) { it(testFile, function() { var utilProcess = child_process.spawnSync(testFile, [], { timeout: 120000, //milliseconds encoding: "utf-8", }); - + //console.error("================="); //console.error(testFile); //console.error("================="); diff --git a/tests/with-raw-glucose.test.ts b/tests/with-raw-glucose.test.ts index 98b95d213..7412318e3 100644 --- a/tests/with-raw-glucose.test.ts +++ b/tests/with-raw-glucose.test.ts @@ -1,7 +1,6 @@ -'use strict'; -var should = require('should'); -var withRawGlucose = require('../lib/with-raw-glucose'); +require('should') +import {withRawGlucose} from '../lib/with-raw-glucose' var cals = [{ scale: 1 diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 000000000..60785551a --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "lib/", + ] +} diff --git a/tsconfig.json b/tsconfig.json index 12d557b29..b55932ae9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,8 @@ "node_modules" ], "include": [ - "lib/" + "bin/", + "lib/", + "tests/", ] } diff --git a/tsconfig.test.json b/tsconfig.test.json index 128d072e7..f32525bcc 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "include": [ + "lib/**/*", "test/**/*" ] } From f9a57afbf4b80161017d5fc9317265f01a39616e Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Tue, 13 Aug 2024 23:38:59 +0200 Subject: [PATCH 15/15] support schedule start HH:MM --- lib/types/ScheduleStart.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/types/ScheduleStart.ts b/lib/types/ScheduleStart.ts index 6bd614daf..cc2e26e5e 100644 --- a/lib/types/ScheduleStart.ts +++ b/lib/types/ScheduleStart.ts @@ -1,10 +1,18 @@ import { Schema } from '@effect/schema' +import { identity } from 'effect' import * as O from 'effect/Order' -const pattern = '^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$' -export const ScheduleStart = Schema.String.pipe(Schema.pattern(new RegExp(pattern))).annotations({ - description: 'Time in HH:MM:SS format', -}) +const pattern = '^([01][0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9]))?$' +export const ScheduleStart = Schema.String.pipe(Schema.pattern(new RegExp(pattern))) + .pipe( + Schema.transform(Schema.String, { + decode: a => (a.split(':').length === 2 ? `${a}:00` : a), + encode: identity, + }) + ) + .annotations({ + description: 'Time in HH:MM:SS format', + }) /** * Time in HH:MM