Skip to content
110 changes: 108 additions & 2 deletions DoseMathTests/DoseMathTests.swift

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions Loop/Extensions/NSUserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extension UserDefaults {
case GlucoseTargetRangeSchedule = "com.loudnate.Naterade.GlucoseTargetRangeSchedule"
case MaximumBasalRatePerHour = "com.loudnate.Naterade.MaximumBasalRatePerHour"
case MaximumBolus = "com.loudnate.Naterade.MaximumBolus"
case MaximumIOB = "com.loudnate.Naterade.MaximumIOB"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New defaults keys should have the com.loopkit prefix.

case PreferredInsulinDataSource = "com.loudnate.Loop.PreferredInsulinDataSource"
case FetchEnliteDataEnabled = "com.loopkit.Loop.FetchEnliteDataEnabled"
case PumpID = "com.loudnate.Naterade.PumpID"
Expand Down Expand Up @@ -151,6 +152,20 @@ extension UserDefaults {
}
}
}
var maximumIOB: Double? {
get {
let value = double(forKey: Key.MaximumIOB.rawValue)

return value > 0 ? value : nil
}
set {
if let maximumIOB = newValue {
set(maximumIOB, forKey: Key.MaximumIOB.rawValue)
} else {
removeObject(forKey: Key.MaximumIOB.rawValue)
}
}
}

var preferredInsulinDataSource: InsulinDataSource? {
get {
Expand Down
4 changes: 4 additions & 0 deletions Loop/Managers/AnalyticsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ final class AnalyticsManager {
logEvent("Maximum bolus change")
}

func didChangeMaximumIOB() {
logEvent("Maximum IOB change")
}

func didChangeMinimumBGGuard() {
logEvent("Minimum BG Guard change")
}
Expand Down
8 changes: 8 additions & 0 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,14 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter
}
}

var maximumIOB: Double? = UserDefaults.standard.maximumIOB {
didSet {
UserDefaults.standard.maximumIOB = maximumIOB

AnalyticsManager.sharedManager.didChangeMaximumIOB()
}
}

// MARK: - CarbKit

let carbStore: CarbStore?
Expand Down
24 changes: 20 additions & 4 deletions Loop/Managers/DoseMath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import HealthKit
import InsulinKit
import LoopKit


struct DoseMath {
/// The allowed precision
static let basalStrokes: Double = 40
Expand Down Expand Up @@ -59,9 +58,11 @@ struct DoseMath {
atDate date: Date = Date(),
lastTempBasal: DoseEntry?,
maxBasalRate: Double,
maxIOB: Double,
glucoseTargetRange: GlucoseRangeSchedule,
insulinSensitivity: InsulinSensitivitySchedule,
basalRateSchedule: BasalRateSchedule,
insulinOnBoard: Double?,
minimumBGGuard: GlucoseThreshold
) -> (rate: Double, duration: TimeInterval)? {
guard glucose.count > 1 else {
Expand Down Expand Up @@ -122,7 +123,13 @@ struct DoseMath {
duration = TimeInterval(0)
}
}

if let iob = insulinOnBoard {
if iob > maxIOB {
// We hit the max IOB, cancel the one in progress.
rate = 0
duration = TimeInterval(0)
}
}
if let rate = rate {
return (rate: rate, duration: duration)
} else {
Expand All @@ -147,9 +154,11 @@ struct DoseMath {
static func recommendBolusFromPredictedGlucose(_ glucose: [GlucoseValue],
atDate date: Date = Date(),
maxBolus: Double,
maxIOB: Double,
glucoseTargetRange: GlucoseRangeSchedule,
insulinSensitivity: InsulinSensitivitySchedule,
basalRateSchedule: BasalRateSchedule,
insulinOnBoard: Double?,
pendingInsulin: Double,
minimumBGGuard: GlucoseThreshold
) -> BolusRecommendation {
Expand All @@ -175,7 +184,7 @@ struct DoseMath {
let roundedAmount = round(max(0, (doseUnits - pendingInsulin)) * 40) / 40

// Cap at max bolus amount
let cappedAmount = min(maxBolus, max(0, roundedAmount))
var cappedAmount = min(maxBolus, max(0, roundedAmount))

let notice: BolusRecommendationNotice?
if cappedAmount > 0 && minGlucose.quantity.doubleValue(for: glucoseTargetRange.unit) < eventualGlucoseTargets.minValue {
Expand All @@ -187,7 +196,14 @@ struct DoseMath {
} else {
notice = nil
}


// Cap at maxium iob, if provided.
if let iob = insulinOnBoard {
if iob + cappedAmount + pendingInsulin > maxIOB, maxIOB > 0 {
cappedAmount = max(0, maxIOB - iob - pendingInsulin)
}
}

return BolusRecommendation(amount: cappedAmount, pendingInsulin: pendingInsulin, notice: notice)
}
}
8 changes: 8 additions & 0 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ final class LoopDataManager {

guard let
maxBasal = deviceDataManager.maximumBasalRatePerHour,
let maxIOB = deviceDataManager.maximumIOB,
let glucoseTargetRange = deviceDataManager.glucoseTargetRangeSchedule,
let insulinSensitivity = deviceDataManager.insulinSensitivitySchedule,
let basalRates = deviceDataManager.basalRateSchedule
Expand All @@ -585,9 +586,11 @@ final class LoopDataManager {
let tempBasal = DoseMath.recommendTempBasalFromPredictedGlucose(predictedGlucose,
lastTempBasal: lastTempBasal,
maxBasalRate: maxBasal,
maxIOB: maxIOB,
glucoseTargetRange: glucoseTargetRange,
insulinSensitivity: insulinSensitivity,
basalRateSchedule: basalRates,
insulinOnBoard: self.insulinOnBoard?.value,
minimumBGGuard: minimumBGGuard
)
else {
Expand Down Expand Up @@ -632,6 +635,7 @@ final class LoopDataManager {
glucose = self.predictedGlucose,
let glucoseWithoutMomentum = self.predictedGlucoseWithoutMomentum,
let maxBolus = self.deviceDataManager.maximumBolus,
let maxIOB = self.deviceDataManager.maximumIOB,
let glucoseTargetRange = self.deviceDataManager.glucoseTargetRangeSchedule,
let insulinSensitivity = self.deviceDataManager.insulinSensitivitySchedule,
let basalRates = self.deviceDataManager.basalRateSchedule
Expand All @@ -653,18 +657,22 @@ final class LoopDataManager {

let recommendationWithMomentum = DoseMath.recommendBolusFromPredictedGlucose(glucose,
maxBolus: maxBolus,
maxIOB: maxIOB,
glucoseTargetRange: glucoseTargetRange,
insulinSensitivity: insulinSensitivity,
basalRateSchedule: basalRates,
insulinOnBoard: self.insulinOnBoard?.value,
pendingInsulin: pendingInsulin,
minimumBGGuard: minimumBGGuard
)

let recommendationWithoutMomentum = DoseMath.recommendBolusFromPredictedGlucose(glucoseWithoutMomentum,
maxBolus: maxBolus,
maxIOB: maxIOB,
glucoseTargetRange: glucoseTargetRange,
insulinSensitivity: insulinSensitivity,
basalRateSchedule: basalRates,
insulinOnBoard: self.insulinOnBoard?.value,
pendingInsulin: pendingInsulin,
minimumBGGuard: minimumBGGuard
)
Expand Down
28 changes: 23 additions & 5 deletions Loop/View Controllers/SettingsTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
case insulinSensitivity
case maxBasal
case maxBolus
case maxIOB
}

fileprivate enum ServiceRow: Int, CaseCountable {
Expand Down Expand Up @@ -334,8 +335,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
} else {
configCell.detailTextLabel?.text = TapToSetString
}
case .maxIOB:
configCell.textLabel?.text = NSLocalizedString("Maximum IOB", comment: "The title text for the maximum insulin-on-board value")

if let maxIOB = dataManager.maximumIOB {
configCell.detailTextLabel?.text = "\(valueNumberFormatter.string(from: NSNumber(value: maxIOB))!) U"
} else {
configCell.detailTextLabel?.text = TapToSetString
}
}

cell = configCell
case .devices:
let deviceCell = tableView.dequeueReusableCell(withIdentifier: RileyLinkDeviceTableViewCell.className) as! RileyLinkDeviceTableViewCell
Expand Down Expand Up @@ -406,9 +414,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
switch Section(rawValue: indexPath.section)! {
case .pump:
let row = PumpRow(rawValue: indexPath.row)!
switch row {
case .pumpID:
let vc: TextFieldTableViewController
//switch row {
var vc: TextFieldTableViewController
switch row {
case .pumpID:
vc = PumpIDTableViewController(pumpID: dataManager.pumpID, region: dataManager.pumpState?.pumpRegion)
Expand All @@ -420,13 +427,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
vc.delegate = self

show(vc, sender: indexPath)
/*
case .batteryChemistry:
let vc = RadioSelectionTableViewController.batteryChemistryType(dataManager.batteryChemistry)
vc.title = sender?.textLabel?.text
vc.delegate = self

show(vc, sender: sender)
}
* */
case .cgm:
let row = CGMRow(rawValue: indexPath.row)!
switch row {
Expand Down Expand Up @@ -455,7 +464,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
case .configuration:
let row = ConfigurationRow(rawValue: indexPath.row)!
switch row {
case .insulinActionDuration, .maxBasal, .maxBolus:
case .insulinActionDuration, .maxBasal, .maxBolus, .maxIOB:
let vc: TextFieldTableViewController

switch row {
Expand All @@ -465,6 +474,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu
vc = .maxBasal(dataManager.maximumBasalRatePerHour)
case .maxBolus:
vc = .maxBolus(dataManager.maximumBolus)
case .maxIOB:
vc = .maxIOB(dataManager.maximumIOB)
default:
fatalError()
}
Expand Down Expand Up @@ -862,9 +873,16 @@ extension SettingsTableViewController: TextFieldTableViewControllerDelegate {
} else {
dataManager.maximumBolus = nil
}
case .maxIOB:
if let value = controller.value, let units = valueNumberFormatter.number(from: value)?.doubleValue {
dataManager.maximumIOB = units
} else {
dataManager.maximumIOB = nil
}
default:
assertionFailure()
}

default:
assertionFailure()
}
Expand Down
17 changes: 16 additions & 1 deletion Loop/View Controllers/TextFieldTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,20 @@ extension TextFieldTableViewController {
}

return vc
}
}

static func maxIOB(_ value: Double?) -> T {
let vc = T()

vc.placeholder = NSLocalizedString("Enter a number of units", comment: "The placeholder text instructing users how to enter a maximum insulin on board")
vc.keyboardType = .decimalPad
vc.unit = NSLocalizedString("Units", comment: "The unit string for units")

if let maxIOB = value {
vc.value = valueNumberFormatter.string(from: NSNumber(value: maxIOB))
}

return vc
}

}