From 7ef3ad38c6b13ac95b7b00b8101b6794088e1d4a Mon Sep 17 00:00:00 2001
From: marionbarker <marionbarker@earthlink.net>
Date: Wed, 14 Dec 2022 09:58:09 -0800
Subject: [PATCH] DRAFT addition of safety features: maxCarbEntry, maxAutoIOB

---
 Loop.xcodeproj/project.pbxproj                |   4 ++
 Loop/Managers/DoseMath.swift                  |  44 +++++++++++--
 Loop/Managers/LoopDataManager.swift           |  12 +++-
 .../CarbEntryViewController.swift             |   6 ++
 Settings.bundle/Root.plist                    |  61 ++++++++++++++++++
 Settings.bundle/en.lproj/Root.strings         | Bin 0 -> 546 bytes
 6 files changed, 118 insertions(+), 9 deletions(-)
 create mode 100644 Settings.bundle/Root.plist
 create mode 100644 Settings.bundle/en.lproj/Root.strings

diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj
index f4cdd7cd3..5e324d05b 100644
--- a/Loop.xcodeproj/project.pbxproj
+++ b/Loop.xcodeproj/project.pbxproj
@@ -455,6 +455,7 @@
 		B4E96D5D248A82A2002DABAD /* StatusBarHUDView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B4E96D5C248A82A2002DABAD /* StatusBarHUDView.xib */; };
 		B4F3D25124AF890C0095CE44 /* BluetoothStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */; };
 		B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FEEF7C24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift */; };
+		B633A70E291011EE00A39E1D /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B633A70D291011EE00A39E1D /* Settings.bundle */; };
 		C10B28461EA9BA5E006EA1FC /* far_future_high_bg_forecast.json in Resources */ = {isa = PBXBuildFile; fileRef = C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */; };
 		C11B9D5B286778A800500CF8 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C11B9D5A286778A800500CF8 /* SwiftCharts */; };
 		C11B9D5E286778D000500CF8 /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C11B9D5D286778D000500CF8 /* LoopKitUI.framework */; };
@@ -1438,6 +1439,7 @@
 		B4E96D5C248A82A2002DABAD /* StatusBarHUDView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = StatusBarHUDView.xib; sourceTree = "<group>"; };
 		B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStateManager.swift; sourceTree = "<group>"; };
 		B4FEEF7C24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceDataManager+DeviceStatus.swift"; sourceTree = "<group>"; };
+		B633A70D291011EE00A39E1D /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
 		C101947127DD473C004E7EB8 /* MockKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MockKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		C10B28451EA9BA5E006EA1FC /* far_future_high_bg_forecast.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = far_future_high_bg_forecast.json; sourceTree = "<group>"; };
 		C11AA5C7258736CF00BDE12F /* DerivedAssetsBase.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssetsBase.xcassets; sourceTree = "<group>"; };
@@ -1883,6 +1885,7 @@
 		43776F831B8022E90074EA36 = {
 			isa = PBXGroup;
 			children = (
+				B633A70D291011EE00A39E1D /* Settings.bundle */,
 				C18A491122FCC20B00FDA733 /* Scripts */,
 				4FF4D0FA1E1834BD00846527 /* Common */,
 				43776F8E1B8022E90074EA36 /* Loop */,
@@ -3295,6 +3298,7 @@
 				A966152723EA5A26005D8B29 /* DerivedAssets.xcassets in Resources */,
 				7D70764F1FE06EE1004AC8EA /* InfoPlist.strings in Resources */,
 				7D7076631FE06EE4004AC8EA /* Localizable.strings in Resources */,
+				B633A70E291011EE00A39E1D /* Settings.bundle in Resources */,
 				43776F971B8022E90074EA36 /* Main.storyboard in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift
index 01c9bedfe..f6fd378a0 100644
--- a/Loop/Managers/DoseMath.swift
+++ b/Loop/Managers/DoseMath.swift
@@ -234,7 +234,9 @@ extension Collection where Element: GlucoseValue {
         at date: Date,
         suspendThreshold: HKQuantity,
         sensitivity: HKQuantity,
-        model: InsulinModel
+        model: InsulinModel,
+        maxAutoIOB: Double?,
+        insulinOnBoard: Double?
     ) -> InsulinCorrection? {
         var minGlucose: GlucoseValue?
         var eventualGlucose: GlucoseValue?
@@ -322,14 +324,32 @@ extension Collection where Element: GlucoseValue {
                 return nil
             }
 
+            // Question: this is the entirelyBelow section of code
+            //   I did not add maxAutoIOB check here as I don't think it is needed
+
             return .entirelyBelowRange(
                 min: min,
                 minTarget: minGlucoseTargets.lowerBound,
                 units: units
             )
         } else if eventual.quantity > eventualGlucoseTargets.upperBound,
-            let minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
+            var minCorrectionUnits = minCorrectionUnits, let correctingGlucose = correctingGlucose
         {
+            // Limit automatic dosing to prevent insulinOnBoard > maxAutoIOB
+            if minCorrectionUnits > 0 &&
+                insulinOnBoard != nil &&
+                maxAutoIOB != nil &&
+                maxAutoIOB! > 0 {
+                let checkMaxAutoIOB = maxAutoIOB! - (insulinOnBoard! + minCorrectionUnits)
+                if checkMaxAutoIOB < 0 {
+                    print("*** maxAutoIOB Feature enacted in aboveRange")
+                    print("***  Initial minCorrectionUnits ", round(100*minCorrectionUnits)/100)
+                    print("***  maxAutoIOB, insulinOnBoard, checkMaxAutoIOB ", round(100*maxAutoIOB!)/100, round(100*insulinOnBoard!)/100, round(100*checkMaxAutoIOB)/100)
+                    minCorrectionUnits = Swift.max(minCorrectionUnits+checkMaxAutoIOB, 0)
+                    print("***  Adjusted minCorrectionUnits ", round(100*minCorrectionUnits)/100)
+                }
+            }
+
             return .aboveRange(
                 min: min,
                 correcting: correctingGlucose,
@@ -371,14 +391,18 @@ extension Collection where Element: GlucoseValue {
         rateRounder: ((Double) -> Double)? = nil,
         isBasalRateScheduleOverrideActive: Bool = false,
         duration: TimeInterval = .minutes(30),
-        continuationInterval: TimeInterval = .minutes(11)
+        continuationInterval: TimeInterval = .minutes(11),
+        insulinOnBoard: Double?,
+        maxAutoIOB: Double?
     ) -> TempBasalRecommendation? {
         let correction = self.insulinCorrection(
             to: correctionRange,
             at: date,
             suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
             sensitivity: sensitivity.quantity(at: date),
-            model: model
+            model: model,
+            maxAutoIOB: maxAutoIOB,
+            insulinOnBoard: insulinOnBoard
         )
 
         let scheduledBasalRate = basalRates.value(at: date)
@@ -439,14 +463,18 @@ extension Collection where Element: GlucoseValue {
         rateRounder: ((Double) -> Double)? = nil,
         isBasalRateScheduleOverrideActive: Bool = false,
         duration: TimeInterval = .minutes(30),
-        continuationInterval: TimeInterval = .minutes(11)
+        continuationInterval: TimeInterval = .minutes(11),
+        insulinOnBoard: Double?,
+        maxAutoIOB: Double?
     ) -> AutomaticDoseRecommendation? {
         guard let correction = self.insulinCorrection(
             to: correctionRange,
             at: date,
             suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
             sensitivity: sensitivity.quantity(at: date),
-            model: model
+            model: model,
+            maxAutoIOB: maxAutoIOB,
+            insulinOnBoard: insulinOnBoard
         ) else {
             return nil
         }
@@ -516,7 +544,9 @@ extension Collection where Element: GlucoseValue {
             at: date,
             suspendThreshold: suspendThreshold ?? correctionRange.quantityRange(at: date).lowerBound,
             sensitivity: sensitivity.quantity(at: date),
-            model: model
+            model: model,
+            maxAutoIOB: nil,
+            insulinOnBoard: nil
         ) else {
             return ManualBolusRecommendation(amount: 0, pendingInsulin: pendingInsulin)
         }
diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift
index bc422867f..2e7d7685e 100644
--- a/Loop/Managers/LoopDataManager.swift
+++ b/Loop/Managers/LoopDataManager.swift
@@ -1616,6 +1616,10 @@ extension LoopDataManager {
 
             let dosingRecommendation: AutomaticDoseRecommendation?
 
+            // DRAFT - cheat with Settings Bundle: later - add maxAutoIOB properly to Loop:
+            //    if not entered in iOS Settings, maxAutoIOB is 0.0
+            let maxAutoIOB = UserDefaults.standard.double(forKey: "maxAutoIOB")
+
             switch settings.automaticDosingStrategy {
             case .automaticBolus:
                 let volumeRounder = { (_ units: Double) in
@@ -1634,7 +1638,9 @@ extension LoopDataManager {
                     lastTempBasal: lastTempBasal,
                     volumeRounder: volumeRounder,
                     rateRounder: rateRounder,
-                    isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
+                    isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
+                    insulinOnBoard: dosingDecision.insulinOnBoard?.value,
+                    maxAutoIOB: maxAutoIOB
                 )
             case .tempBasalOnly:
                 let temp = predictedGlucose.recommendedTempBasal(
@@ -1647,7 +1653,9 @@ extension LoopDataManager {
                     maxBasalRate: maxBasal!,
                     lastTempBasal: lastTempBasal,
                     rateRounder: rateRounder,
-                    isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true
+                    isBasalRateScheduleOverrideActive: settings.scheduleOverride?.isBasalRateScheduleOverriden(at: startDate) == true,
+                    insulinOnBoard: dosingDecision.insulinOnBoard?.value,
+                    maxAutoIOB: maxAutoIOB
                 )
                 dosingRecommendation = AutomaticDoseRecommendation(basalAdjustment: temp)
             }
diff --git a/Loop/View Controllers/CarbEntryViewController.swift b/Loop/View Controllers/CarbEntryViewController.swift
index d553682a9..a4c79f30a 100644
--- a/Loop/View Controllers/CarbEntryViewController.swift	
+++ b/Loop/View Controllers/CarbEntryViewController.swift	
@@ -526,6 +526,12 @@ final class CarbEntryViewController: LoopChartsTableViewController, Identifiable
         }
 
         guard let quantity = quantity, quantity.doubleValue(for: preferredCarbUnit) > 0 else { return false }
+
+        // DRAFT - cheat with Settings Bundle: later - add maxCarbEntry properly to Loop:
+        let maxCarbEntry = UserDefaults.standard.double(forKey: "maxCarbEntry")
+        if (maxCarbEntry > 0) {
+            maxQuantity = HKQuantity(unit: .gram(), doubleValue: maxCarbEntry)
+        }
         guard quantity.compare(maxQuantity) != .orderedDescending else {
             navigationDelegate.showMaxQuantityValidationWarning(for: self, maxQuantityGrams: maxQuantity.doubleValue(for: .gram()))
             return false
diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist
new file mode 100644
index 000000000..a584b45ca
--- /dev/null
+++ b/Settings.bundle/Root.plist
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>StringsTable</key>
+	<string>Root</string>
+	<key>PreferenceSpecifiers</key>
+	<array>
+		<dict>
+			<key>Type</key>
+			<string>PSGroupSpecifier</string>
+			<key>Title</key>
+			<string>Max Allowed Carb Entry</string>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSMultiValueSpecifier</string>
+			<key>Title</key>
+			<string>Max Allowed Carb Entry</string>
+			<key>Key</key>
+			<string>maxCarbEntry</string>
+			<key>DefaultValue</key>
+			<string>250g</string>
+			<key>Values</key>
+			<array>
+				<real>50</real>
+				<real>99</real>
+				<real>150</real>
+				<real>200</real>
+				<real>250</real>
+				<real>300</real>
+			</array>
+			<key>Titles</key>
+			<array>
+				<string>50g</string>
+				<string>99g</string>
+				<string>150g</string>
+				<string>200g</string>
+				<string>250g</string>
+				<string>300g</string>
+			</array>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSGroupSpecifier</string>
+			<key>Title</key>
+			<string>max Auto IOB</string>
+		</dict>
+		<dict>
+			<key>Type</key>
+			<string>PSTextFieldSpecifier</string>
+			<key>Title</key>
+			<string>maxAutoIOB</string>
+			<key>Key</key>
+			<string>maxAutoIOB</string>
+			<key>KeyboardType</key>
+			<string>NumbersAndPunctuation</string>
+		</dict>
+	</array>
+</dict>
+</plist>
diff --git a/Settings.bundle/en.lproj/Root.strings b/Settings.bundle/en.lproj/Root.strings
new file mode 100644
index 0000000000000000000000000000000000000000..8cd87b9d6b20c1fbf87bd4db3db267fca5ad4df9
GIT binary patch
