├── ABSwitcherAndProfileUploader.txt ├── ABSwitcherPatch.txt ├── NightscoutProfilePatch.txt └── README.md /ABSwitcherAndProfileUploader.txt: -------------------------------------------------------------------------------- 1 | From 21c3fa7ae880b90484386004f7e2af9834a4d8e4 Mon Sep 17 00:00:00 2001 2 | From: Jon Fawcett 3 | Date: Tue, 21 Sep 2021 12:20:40 -0400 4 | Subject: [PATCH] AB Dosing Automatic Switcher Patch 5 | 6 | --- 7 | Loop/Managers/LoopDataManager.swift | 8 ++ 8 | .../SettingsTableViewController.swift | 97 +++++++++++++++++-- 9 | LoopCore/LoopSettings.swift | 20 +++- 10 | 3 files changed, 114 insertions(+), 11 deletions(-) 11 | 12 | diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift 13 | index ef861a680a..d8ece1a20a 100644 14 | --- a/Loop/Managers/LoopDataManager.swift 15 | +++ b/Loop/Managers/LoopDataManager.swift 16 | @@ -1200,6 +1200,14 @@ extension LoopDataManager { 17 | self.predictedGlucose = predictedGlucose 18 | let predictedGlucoseIncludingPendingInsulin = try predictGlucose(using: settings.enabledEffects, includingPendingInsulin: true) 19 | self.predictedGlucoseIncludingPendingInsulin = predictedGlucoseIncludingPendingInsulin 20 | + 21 | + if( settings.dosingStrategyAutomationEnabled && settings.dosingStrategyThreshold?.rawValue != nil){ 22 | + if( glucose.quantity > HKQuantity(unit : settings.glucoseUnit ?? .milligramsPerDeciliter, doubleValue: settings.dosingStrategyThreshold!.value)){ 23 | + settings.dosingStrategy = .automaticBolus 24 | + } else { 25 | + settings.dosingStrategy = .tempBasalOnly 26 | + } 27 | + } 28 | 29 | guard 30 | let maxBasal = settings.maximumBasalRatePerHour, 31 | diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift 32 | index c8a0af8513..933404139d 100644 33 | --- a/Loop/View Controllers/SettingsTableViewController.swift 34 | +++ b/Loop/View Controllers/SettingsTableViewController.swift 35 | @@ -46,6 +46,7 @@ final class SettingsTableViewController: UITableViewController { 36 | case pump 37 | case cgm 38 | case configuration 39 | + case strategy 40 | case services 41 | case testingPumpDataDeletion 42 | case testingCGMDataDeletion 43 | @@ -70,10 +71,15 @@ final class SettingsTableViewController: UITableViewController { 44 | case basalRate 45 | case deliveryLimits 46 | case insulinModel 47 | - case dosingStrategy 48 | case carbRatio 49 | case insulinSensitivity 50 | } 51 | + 52 | + fileprivate enum StrategyRow: Int, CaseCountable { 53 | + case dosingStrategy = 0 54 | + case dosingStrategyAutomationEnabled 55 | + case dosingStrategyThreshold 56 | + } 57 | 58 | fileprivate enum ServiceRow: Int, CaseCountable { 59 | case nightscout = 0 60 | @@ -147,6 +153,8 @@ final class SettingsTableViewController: UITableViewController { 61 | return CGMRow.count 62 | case .configuration: 63 | return ConfigurationRow.count 64 | + case .strategy: 65 | + return StrategyRow.count 66 | case .services: 67 | return ServiceRow.count 68 | case .testingPumpDataDeletion, .testingCGMDataDeletion: 69 | @@ -269,9 +277,6 @@ final class SettingsTableViewController: UITableViewController { 70 | } else { 71 | configCell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString 72 | } 73 | - case .dosingStrategy: 74 | - configCell.textLabel?.text = NSLocalizedString("Dosing Strategy", comment: "The title text for the dosing strategy setting row") 75 | - configCell.detailTextLabel?.text = dataManager.loopManager.settings.dosingStrategy.title 76 | case .deliveryLimits: 77 | configCell.textLabel?.text = NSLocalizedString("Delivery Limits", comment: "Title text for delivery limits") 78 | 79 | @@ -290,6 +295,36 @@ final class SettingsTableViewController: UITableViewController { 80 | } 81 | } 82 | 83 | + configCell.accessoryType = .disclosureIndicator 84 | + return configCell 85 | + case .strategy: 86 | + let configCell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) 87 | + 88 | + switch StrategyRow(rawValue: indexPath.row)! { 89 | + case .dosingStrategy: 90 | + configCell.textLabel?.text = NSLocalizedString("Dosing Strategy", comment: "The title text for the dosing strategy setting row") 91 | + configCell.detailTextLabel?.text = dataManager.loopManager.settings.dosingStrategy.title 92 | + case .dosingStrategyAutomationEnabled: 93 | + let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell 94 | + 95 | + switchCell.selectionStyle = .none 96 | + switchCell.switch?.isOn = dataManager.loopManager.settings.dosingStrategyAutomationEnabled 97 | + switchCell.textLabel?.text = NSLocalizedString("Auto Strategy Switching", comment: "The title text for the Dosing Strategy enabled switch cell") 98 | + 99 | + switchCell.switch?.addTarget(self, action: #selector(dosingStrategyAutomationEnabledChanged(_:)), for: .valueChanged) 100 | + 101 | + return switchCell 102 | + case .dosingStrategyThreshold: 103 | + configCell.textLabel?.text = NSLocalizedString("Dose Switching Threshold", comment: "The title text in settings") 104 | + 105 | + if let dosingStrategyThreshold = dataManager.loopManager.settings.dosingStrategyThreshold { 106 | + let value = valueNumberFormatter.string(from: dosingStrategyThreshold.value, unit: dosingStrategyThreshold.unit) ?? SettingsTableViewCell.TapToSetString 107 | + configCell.detailTextLabel?.text = value 108 | + } else { 109 | + configCell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString 110 | + } 111 | + } 112 | + 113 | configCell.accessoryType = .disclosureIndicator 114 | return configCell 115 | case .services: 116 | @@ -342,6 +377,8 @@ final class SettingsTableViewController: UITableViewController { 117 | return NSLocalizedString("Continuous Glucose Monitor", comment: "The title of the continuous glucose monitor section in settings") 118 | case .configuration: 119 | return NSLocalizedString("Configuration", comment: "The title of the configuration section in settings") 120 | + case .strategy: 121 | + return NSLocalizedString("Dosing Strategy", comment: "The title of the strategy section in settings") 122 | case .services: 123 | return NSLocalizedString("Services", comment: "The title of the services section in settings") 124 | case .testingPumpDataDeletion, .testingCGMDataDeletion: 125 | @@ -514,8 +551,6 @@ final class SettingsTableViewController: UITableViewController { 126 | } 127 | case .insulinModel: 128 | performSegue(withIdentifier: InsulinModelSettingsViewController.className, sender: sender) 129 | - case .dosingStrategy: 130 | - performSegue(withIdentifier: DosingStrategySelectionViewController.className, sender: sender) 131 | case .deliveryLimits: 132 | let vc = DeliveryLimitSettingsTableViewController(style: .grouped) 133 | 134 | @@ -558,6 +593,32 @@ final class SettingsTableViewController: UITableViewController { 135 | 136 | show(vc, sender: sender) 137 | } 138 | + case .strategy: 139 | + let row = StrategyRow(rawValue: indexPath.row)! 140 | + switch row { 141 | + case .dosingStrategy: 142 | + performSegue(withIdentifier: DosingStrategySelectionViewController.className, sender: sender) 143 | + case .dosingStrategyAutomationEnabled: 144 | + break 145 | + case .dosingStrategyThreshold: 146 | + if let dosingStrategyThreshold = dataManager.loopManager.settings.dosingStrategyThreshold { 147 | + let vc = GlucoseThresholdTableViewController(threshold: dosingStrategyThreshold.value, glucoseUnit: dosingStrategyThreshold.unit) 148 | + vc.delegate = self 149 | + vc.indexPath = indexPath 150 | + vc.title = sender?.textLabel?.text 151 | + vc.placeholder = "Enter switching threshold" 152 | + vc.contextHelp = "If Auto Strategy Switching is enabled, when current glucose is above the switching threshold, Loop will use Automatic Bolus strategy. When glucose is at or below the switching threshold, Loop will use Temp Basal Only strategy." 153 | + self.show(vc, sender: sender) 154 | + } else if let unit = dataManager.loopManager.glucoseStore.preferredUnit { 155 | + let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnit: unit) 156 | + vc.delegate = self 157 | + vc.indexPath = indexPath 158 | + vc.title = sender?.textLabel?.text 159 | + vc.placeholder = "Enter switching threshold" 160 | + vc.contextHelp = "If Auto Strategy Switching is enabled, when current glucose is above the switching threshold, Loop will use Automatic Bolus strategy. When glucose is at or below the switching threshold, Loop will use Temp Basal Only strategy." 161 | + self.show(vc, sender: sender) 162 | + } 163 | + } 164 | case .loop: 165 | switch LoopRow(rawValue: indexPath.row)! { 166 | case .diagnostic: 167 | @@ -617,6 +678,10 @@ final class SettingsTableViewController: UITableViewController { 168 | @objc private func dosingEnabledChanged(_ sender: UISwitch) { 169 | dataManager.loopManager.settings.dosingEnabled = sender.isOn 170 | } 171 | + 172 | + @objc private func dosingStrategyAutomationEnabledChanged(_ sender: UISwitch) { 173 | + dataManager.loopManager.settings.dosingStrategyAutomationEnabled = sender.isOn 174 | + } 175 | } 176 | 177 | // MARK: - DeviceManager view controller delegation 178 | @@ -810,8 +875,8 @@ extension SettingsTableViewController: DosingStrategySelectionViewControllerDele 179 | } 180 | 181 | switch sections[indexPath.section] { 182 | - case .configuration: 183 | - switch ConfigurationRow(rawValue: indexPath.row)! { 184 | + case .strategy: 185 | + switch StrategyRow(rawValue: indexPath.row)! { 186 | case .dosingStrategy: 187 | if let strategy = controller.dosingStrategy { 188 | dataManager.loopManager.settings.dosingStrategy = strategy 189 | @@ -844,8 +909,20 @@ extension SettingsTableViewController: LoopKitUI.TextFieldTableViewControllerDel 190 | default: 191 | assertionFailure() 192 | } 193 | - default: 194 | - assertionFailure() 195 | + case .strategy: 196 | + switch StrategyRow(rawValue: indexPath.row)! { 197 | + case .dosingStrategyThreshold: 198 | + if let controller = controller as? GlucoseThresholdTableViewController, 199 | + let value = controller.value, let dosingStrategyThreshold = valueNumberFormatter.number(from: value)?.doubleValue { 200 | + dataManager.loopManager.settings.dosingStrategyThreshold = GlucoseThreshold(unit: controller.glucoseUnit, value: dosingStrategyThreshold) 201 | + } else { 202 | + dataManager.loopManager.settings.dosingStrategyThreshold = nil 203 | + } 204 | + default: 205 | + assertionFailure() 206 | + } 207 | + default: 208 | + assertionFailure() 209 | } 210 | } 211 | 212 | diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift 213 | index 34859c8d6c..3223350ffc 100644 214 | --- a/LoopCore/LoopSettings.swift 215 | +++ b/LoopCore/LoopSettings.swift 216 | @@ -68,6 +68,10 @@ public struct LoopSettings: Equatable { 217 | public let retrospectiveCorrectionEnabled = true 218 | 219 | public var dosingStrategy: DosingStrategy = .tempBasalOnly 220 | + 221 | + public var dosingStrategyAutomationEnabled = false 222 | + 223 | + public var dosingStrategyThreshold: GlucoseThreshold? = nil 224 | 225 | /// The interval over which to aggregate changes in glucose for retrospective correction 226 | public let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) 227 | @@ -132,13 +136,17 @@ public struct LoopSettings: Equatable { 228 | glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil, 229 | maximumBasalRatePerHour: Double? = nil, 230 | maximumBolus: Double? = nil, 231 | - suspendThreshold: GlucoseThreshold? = nil 232 | + suspendThreshold: GlucoseThreshold? = nil, 233 | + dosingStrategyAutomationEnabled: Bool = false, 234 | + dosingStrategyThreshold: GlucoseThreshold? = nil 235 | ) { 236 | self.dosingEnabled = dosingEnabled 237 | self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule 238 | self.maximumBasalRatePerHour = maximumBasalRatePerHour 239 | self.maximumBolus = maximumBolus 240 | self.suspendThreshold = suspendThreshold 241 | + self.dosingStrategyAutomationEnabled = dosingStrategyAutomationEnabled 242 | + self.dosingStrategyThreshold = dosingStrategyThreshold 243 | } 244 | } 245 | 246 | @@ -277,6 +285,14 @@ extension LoopSettings: RawRepresentable { 247 | let dosingStrategy = DosingStrategy(rawValue: rawDosingStrategy) { 248 | self.dosingStrategy = dosingStrategy 249 | } 250 | + 251 | + if let dosingStrategyAutomationEnabled = rawValue["dosingStrategyAutomationEnabled"] as? Bool { 252 | + self.dosingStrategyAutomationEnabled = dosingStrategyAutomationEnabled 253 | + } 254 | + 255 | + if let rawDosingStrategyThreshold = rawValue["dosingStrategyThreshold"] as? GlucoseThreshold.RawValue { 256 | + self.dosingStrategyThreshold = GlucoseThreshold(rawValue: rawDosingStrategyThreshold) 257 | + } 258 | 259 | } 260 | 261 | @@ -295,6 +311,8 @@ extension LoopSettings: RawRepresentable { 262 | raw["maximumBolus"] = maximumBolus 263 | raw["minimumBGGuard"] = suspendThreshold?.rawValue 264 | raw["dosingStrategy"] = dosingStrategy.rawValue 265 | + raw["dosingStrategyAutomationEnabled"] = dosingStrategyAutomationEnabled 266 | + raw["dosingStrategyThreshold"] = dosingStrategyThreshold?.rawValue 267 | 268 | return raw 269 | } 270 | 271 | From 81800dddab5c6de6036ca399512e611077f760ab Mon Sep 17 00:00:00 2001 272 | From: Jon Fawcett 273 | Date: Tue, 21 Sep 2021 12:20:08 -0400 274 | Subject: [PATCH] Nightscout Profile Upload Patch 275 | 276 | --- 277 | Loop/Managers/LoopDataManager.swift | 16 ++++++++++++++++ 278 | Loop/Managers/NightscoutDataManager.swift | 4 ++-- 279 | 2 files changed, 18 insertions(+), 2 deletions(-) 280 | 281 | diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift 282 | index bf44d9f17b..ef861a680a 100644 283 | --- a/Loop/Managers/LoopDataManager.swift 284 | +++ b/Loop/Managers/LoopDataManager.swift 285 | @@ -152,6 +152,9 @@ final class LoopDataManager { 286 | } 287 | UserDefaults.appGroup?.loopSettings = settings 288 | notify(forChange: .preferences) 289 | + if settings.overridePresets != oldValue.overridePresets || settings.glucoseTargetRangeSchedule != oldValue.glucoseTargetRangeSchedule { 290 | + self.notifyUpload(forChange: .preferences) 291 | + } 292 | AnalyticsManager.shared.didChangeLoopSettings(from: oldValue, to: settings) 293 | } 294 | } 295 | @@ -330,6 +333,7 @@ extension LoopDataManager { 296 | doseStore.basalProfile = newValue 297 | UserDefaults.appGroup?.basalRateSchedule = newValue 298 | notify(forChange: .preferences) 299 | + notifyUpload(forChange: .preferences) 300 | 301 | if let newValue = newValue, let oldValue = doseStore.basalProfile, newValue.items != oldValue.items { 302 | AnalyticsManager.shared.didChangeBasalRateSchedule() 303 | @@ -357,6 +361,7 @@ extension LoopDataManager { 304 | carbsOnBoard = nil 305 | 306 | notify(forChange: .preferences) 307 | + notifyUpload(forChange: .preferences) 308 | } 309 | } 310 | 311 | @@ -408,6 +413,7 @@ extension LoopDataManager { 312 | self.insulinEffect = nil 313 | 314 | self.notify(forChange: .preferences) 315 | + self.notifyUpload(forChange: .preferences) 316 | } 317 | } 318 | } 319 | @@ -859,6 +865,15 @@ extension LoopDataManager { 320 | ] 321 | ) 322 | } 323 | + 324 | + private func notifyUpload(forChange context: LoopUpdateContext) { 325 | + NotificationCenter.default.post(name: .LoopDataUpload, 326 | + object: self, 327 | + userInfo: [ 328 | + type(of: self).LoopUpdateContextKey: context.rawValue 329 | + ] 330 | + ) 331 | + } 332 | 333 | /// Computes amount of insulin from boluses that have been issued and not confirmed, and 334 | /// remaining insulin delivery from temporary basal rate adjustments above scheduled rate 335 | @@ -1572,6 +1587,7 @@ extension LoopDataManager { 336 | 337 | extension Notification.Name { 338 | static let LoopDataUpdated = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpdated") 339 | + static let LoopDataUpload = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpload") 340 | static let LoopRunning = Notification.Name(rawValue: "com.loopkit.Loop.LoopRunning") 341 | static let LoopCompleted = Notification.Name(rawValue: "com.loopkit.Loop.LoopCompleted") 342 | } 343 | diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift 344 | index e2d020d398..48823116ee 100644 345 | --- a/Loop/Managers/NightscoutDataManager.swift 346 | +++ b/Loop/Managers/NightscoutDataManager.swift 347 | @@ -35,11 +35,11 @@ final class NightscoutDataManager { 348 | self.deviceManager = deviceDataManager 349 | 350 | NotificationCenter.default.addObserver(self, selector: #selector(loopCompleted(_:)), name: .LoopCompleted, object: deviceDataManager.loopManager) 351 | - NotificationCenter.default.addObserver(self, selector: #selector(loopDataUpdated(_:)), name: .LoopDataUpdated, object: deviceDataManager.loopManager) 352 | + NotificationCenter.default.addObserver(self, selector: #selector(loopDataUpload(_:)), name: .LoopDataUpload, object: deviceDataManager.loopManager) 353 | } 354 | 355 | 356 | - @objc func loopDataUpdated(_ note: Notification) { 357 | + @objc func loopDataUpload(_ note: Notification) { 358 | guard 359 | let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue, 360 | let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext), 361 | -------------------------------------------------------------------------------- /ABSwitcherPatch.txt: -------------------------------------------------------------------------------- 1 | From 21c3fa7ae880b90484386004f7e2af9834a4d8e4 Mon Sep 17 00:00:00 2001 2 | From: Jon Fawcett 3 | Date: Tue, 21 Sep 2021 12:20:40 -0400 4 | Subject: [PATCH] AB Dosing Automatic Switcher Patch 5 | 6 | --- 7 | Loop/Managers/LoopDataManager.swift | 8 ++ 8 | .../SettingsTableViewController.swift | 97 +++++++++++++++++-- 9 | LoopCore/LoopSettings.swift | 20 +++- 10 | 3 files changed, 114 insertions(+), 11 deletions(-) 11 | 12 | diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift 13 | index ef861a680a..d8ece1a20a 100644 14 | --- a/Loop/Managers/LoopDataManager.swift 15 | +++ b/Loop/Managers/LoopDataManager.swift 16 | @@ -1200,6 +1200,14 @@ extension LoopDataManager { 17 | self.predictedGlucose = predictedGlucose 18 | let predictedGlucoseIncludingPendingInsulin = try predictGlucose(using: settings.enabledEffects, includingPendingInsulin: true) 19 | self.predictedGlucoseIncludingPendingInsulin = predictedGlucoseIncludingPendingInsulin 20 | + 21 | + if( settings.dosingStrategyAutomationEnabled && settings.dosingStrategyThreshold?.rawValue != nil){ 22 | + if( glucose.quantity > HKQuantity(unit : settings.glucoseUnit ?? .milligramsPerDeciliter, doubleValue: settings.dosingStrategyThreshold!.value)){ 23 | + settings.dosingStrategy = .automaticBolus 24 | + } else { 25 | + settings.dosingStrategy = .tempBasalOnly 26 | + } 27 | + } 28 | 29 | guard 30 | let maxBasal = settings.maximumBasalRatePerHour, 31 | diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift 32 | index c8a0af8513..933404139d 100644 33 | --- a/Loop/View Controllers/SettingsTableViewController.swift 34 | +++ b/Loop/View Controllers/SettingsTableViewController.swift 35 | @@ -46,6 +46,7 @@ final class SettingsTableViewController: UITableViewController { 36 | case pump 37 | case cgm 38 | case configuration 39 | + case strategy 40 | case services 41 | case testingPumpDataDeletion 42 | case testingCGMDataDeletion 43 | @@ -70,10 +71,15 @@ final class SettingsTableViewController: UITableViewController { 44 | case basalRate 45 | case deliveryLimits 46 | case insulinModel 47 | - case dosingStrategy 48 | case carbRatio 49 | case insulinSensitivity 50 | } 51 | + 52 | + fileprivate enum StrategyRow: Int, CaseCountable { 53 | + case dosingStrategy = 0 54 | + case dosingStrategyAutomationEnabled 55 | + case dosingStrategyThreshold 56 | + } 57 | 58 | fileprivate enum ServiceRow: Int, CaseCountable { 59 | case nightscout = 0 60 | @@ -147,6 +153,8 @@ final class SettingsTableViewController: UITableViewController { 61 | return CGMRow.count 62 | case .configuration: 63 | return ConfigurationRow.count 64 | + case .strategy: 65 | + return StrategyRow.count 66 | case .services: 67 | return ServiceRow.count 68 | case .testingPumpDataDeletion, .testingCGMDataDeletion: 69 | @@ -269,9 +277,6 @@ final class SettingsTableViewController: UITableViewController { 70 | } else { 71 | configCell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString 72 | } 73 | - case .dosingStrategy: 74 | - configCell.textLabel?.text = NSLocalizedString("Dosing Strategy", comment: "The title text for the dosing strategy setting row") 75 | - configCell.detailTextLabel?.text = dataManager.loopManager.settings.dosingStrategy.title 76 | case .deliveryLimits: 77 | configCell.textLabel?.text = NSLocalizedString("Delivery Limits", comment: "Title text for delivery limits") 78 | 79 | @@ -290,6 +295,36 @@ final class SettingsTableViewController: UITableViewController { 80 | } 81 | } 82 | 83 | + configCell.accessoryType = .disclosureIndicator 84 | + return configCell 85 | + case .strategy: 86 | + let configCell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) 87 | + 88 | + switch StrategyRow(rawValue: indexPath.row)! { 89 | + case .dosingStrategy: 90 | + configCell.textLabel?.text = NSLocalizedString("Dosing Strategy", comment: "The title text for the dosing strategy setting row") 91 | + configCell.detailTextLabel?.text = dataManager.loopManager.settings.dosingStrategy.title 92 | + case .dosingStrategyAutomationEnabled: 93 | + let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell 94 | + 95 | + switchCell.selectionStyle = .none 96 | + switchCell.switch?.isOn = dataManager.loopManager.settings.dosingStrategyAutomationEnabled 97 | + switchCell.textLabel?.text = NSLocalizedString("Auto Strategy Switching", comment: "The title text for the Dosing Strategy enabled switch cell") 98 | + 99 | + switchCell.switch?.addTarget(self, action: #selector(dosingStrategyAutomationEnabledChanged(_:)), for: .valueChanged) 100 | + 101 | + return switchCell 102 | + case .dosingStrategyThreshold: 103 | + configCell.textLabel?.text = NSLocalizedString("Dose Switching Threshold", comment: "The title text in settings") 104 | + 105 | + if let dosingStrategyThreshold = dataManager.loopManager.settings.dosingStrategyThreshold { 106 | + let value = valueNumberFormatter.string(from: dosingStrategyThreshold.value, unit: dosingStrategyThreshold.unit) ?? SettingsTableViewCell.TapToSetString 107 | + configCell.detailTextLabel?.text = value 108 | + } else { 109 | + configCell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString 110 | + } 111 | + } 112 | + 113 | configCell.accessoryType = .disclosureIndicator 114 | return configCell 115 | case .services: 116 | @@ -342,6 +377,8 @@ final class SettingsTableViewController: UITableViewController { 117 | return NSLocalizedString("Continuous Glucose Monitor", comment: "The title of the continuous glucose monitor section in settings") 118 | case .configuration: 119 | return NSLocalizedString("Configuration", comment: "The title of the configuration section in settings") 120 | + case .strategy: 121 | + return NSLocalizedString("Dosing Strategy", comment: "The title of the strategy section in settings") 122 | case .services: 123 | return NSLocalizedString("Services", comment: "The title of the services section in settings") 124 | case .testingPumpDataDeletion, .testingCGMDataDeletion: 125 | @@ -514,8 +551,6 @@ final class SettingsTableViewController: UITableViewController { 126 | } 127 | case .insulinModel: 128 | performSegue(withIdentifier: InsulinModelSettingsViewController.className, sender: sender) 129 | - case .dosingStrategy: 130 | - performSegue(withIdentifier: DosingStrategySelectionViewController.className, sender: sender) 131 | case .deliveryLimits: 132 | let vc = DeliveryLimitSettingsTableViewController(style: .grouped) 133 | 134 | @@ -558,6 +593,32 @@ final class SettingsTableViewController: UITableViewController { 135 | 136 | show(vc, sender: sender) 137 | } 138 | + case .strategy: 139 | + let row = StrategyRow(rawValue: indexPath.row)! 140 | + switch row { 141 | + case .dosingStrategy: 142 | + performSegue(withIdentifier: DosingStrategySelectionViewController.className, sender: sender) 143 | + case .dosingStrategyAutomationEnabled: 144 | + break 145 | + case .dosingStrategyThreshold: 146 | + if let dosingStrategyThreshold = dataManager.loopManager.settings.dosingStrategyThreshold { 147 | + let vc = GlucoseThresholdTableViewController(threshold: dosingStrategyThreshold.value, glucoseUnit: dosingStrategyThreshold.unit) 148 | + vc.delegate = self 149 | + vc.indexPath = indexPath 150 | + vc.title = sender?.textLabel?.text 151 | + vc.placeholder = "Enter switching threshold" 152 | + vc.contextHelp = "If Auto Strategy Switching is enabled, when current glucose is above the switching threshold, Loop will use Automatic Bolus strategy. When glucose is at or below the switching threshold, Loop will use Temp Basal Only strategy." 153 | + self.show(vc, sender: sender) 154 | + } else if let unit = dataManager.loopManager.glucoseStore.preferredUnit { 155 | + let vc = GlucoseThresholdTableViewController(threshold: nil, glucoseUnit: unit) 156 | + vc.delegate = self 157 | + vc.indexPath = indexPath 158 | + vc.title = sender?.textLabel?.text 159 | + vc.placeholder = "Enter switching threshold" 160 | + vc.contextHelp = "If Auto Strategy Switching is enabled, when current glucose is above the switching threshold, Loop will use Automatic Bolus strategy. When glucose is at or below the switching threshold, Loop will use Temp Basal Only strategy." 161 | + self.show(vc, sender: sender) 162 | + } 163 | + } 164 | case .loop: 165 | switch LoopRow(rawValue: indexPath.row)! { 166 | case .diagnostic: 167 | @@ -617,6 +678,10 @@ final class SettingsTableViewController: UITableViewController { 168 | @objc private func dosingEnabledChanged(_ sender: UISwitch) { 169 | dataManager.loopManager.settings.dosingEnabled = sender.isOn 170 | } 171 | + 172 | + @objc private func dosingStrategyAutomationEnabledChanged(_ sender: UISwitch) { 173 | + dataManager.loopManager.settings.dosingStrategyAutomationEnabled = sender.isOn 174 | + } 175 | } 176 | 177 | // MARK: - DeviceManager view controller delegation 178 | @@ -810,8 +875,8 @@ extension SettingsTableViewController: DosingStrategySelectionViewControllerDele 179 | } 180 | 181 | switch sections[indexPath.section] { 182 | - case .configuration: 183 | - switch ConfigurationRow(rawValue: indexPath.row)! { 184 | + case .strategy: 185 | + switch StrategyRow(rawValue: indexPath.row)! { 186 | case .dosingStrategy: 187 | if let strategy = controller.dosingStrategy { 188 | dataManager.loopManager.settings.dosingStrategy = strategy 189 | @@ -844,8 +909,20 @@ extension SettingsTableViewController: LoopKitUI.TextFieldTableViewControllerDel 190 | default: 191 | assertionFailure() 192 | } 193 | - default: 194 | - assertionFailure() 195 | + case .strategy: 196 | + switch StrategyRow(rawValue: indexPath.row)! { 197 | + case .dosingStrategyThreshold: 198 | + if let controller = controller as? GlucoseThresholdTableViewController, 199 | + let value = controller.value, let dosingStrategyThreshold = valueNumberFormatter.number(from: value)?.doubleValue { 200 | + dataManager.loopManager.settings.dosingStrategyThreshold = GlucoseThreshold(unit: controller.glucoseUnit, value: dosingStrategyThreshold) 201 | + } else { 202 | + dataManager.loopManager.settings.dosingStrategyThreshold = nil 203 | + } 204 | + default: 205 | + assertionFailure() 206 | + } 207 | + default: 208 | + assertionFailure() 209 | } 210 | } 211 | 212 | diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift 213 | index 34859c8d6c..3223350ffc 100644 214 | --- a/LoopCore/LoopSettings.swift 215 | +++ b/LoopCore/LoopSettings.swift 216 | @@ -68,6 +68,10 @@ public struct LoopSettings: Equatable { 217 | public let retrospectiveCorrectionEnabled = true 218 | 219 | public var dosingStrategy: DosingStrategy = .tempBasalOnly 220 | + 221 | + public var dosingStrategyAutomationEnabled = false 222 | + 223 | + public var dosingStrategyThreshold: GlucoseThreshold? = nil 224 | 225 | /// The interval over which to aggregate changes in glucose for retrospective correction 226 | public let retrospectiveCorrectionGroupingInterval = TimeInterval(minutes: 30) 227 | @@ -132,13 +136,17 @@ public struct LoopSettings: Equatable { 228 | glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil, 229 | maximumBasalRatePerHour: Double? = nil, 230 | maximumBolus: Double? = nil, 231 | - suspendThreshold: GlucoseThreshold? = nil 232 | + suspendThreshold: GlucoseThreshold? = nil, 233 | + dosingStrategyAutomationEnabled: Bool = false, 234 | + dosingStrategyThreshold: GlucoseThreshold? = nil 235 | ) { 236 | self.dosingEnabled = dosingEnabled 237 | self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule 238 | self.maximumBasalRatePerHour = maximumBasalRatePerHour 239 | self.maximumBolus = maximumBolus 240 | self.suspendThreshold = suspendThreshold 241 | + self.dosingStrategyAutomationEnabled = dosingStrategyAutomationEnabled 242 | + self.dosingStrategyThreshold = dosingStrategyThreshold 243 | } 244 | } 245 | 246 | @@ -277,6 +285,14 @@ extension LoopSettings: RawRepresentable { 247 | let dosingStrategy = DosingStrategy(rawValue: rawDosingStrategy) { 248 | self.dosingStrategy = dosingStrategy 249 | } 250 | + 251 | + if let dosingStrategyAutomationEnabled = rawValue["dosingStrategyAutomationEnabled"] as? Bool { 252 | + self.dosingStrategyAutomationEnabled = dosingStrategyAutomationEnabled 253 | + } 254 | + 255 | + if let rawDosingStrategyThreshold = rawValue["dosingStrategyThreshold"] as? GlucoseThreshold.RawValue { 256 | + self.dosingStrategyThreshold = GlucoseThreshold(rawValue: rawDosingStrategyThreshold) 257 | + } 258 | 259 | } 260 | 261 | @@ -295,6 +311,8 @@ extension LoopSettings: RawRepresentable { 262 | raw["maximumBolus"] = maximumBolus 263 | raw["minimumBGGuard"] = suspendThreshold?.rawValue 264 | raw["dosingStrategy"] = dosingStrategy.rawValue 265 | + raw["dosingStrategyAutomationEnabled"] = dosingStrategyAutomationEnabled 266 | + raw["dosingStrategyThreshold"] = dosingStrategyThreshold?.rawValue 267 | 268 | return raw 269 | } 270 | -------------------------------------------------------------------------------- /NightscoutProfilePatch.txt: -------------------------------------------------------------------------------- 1 | From 81800dddab5c6de6036ca399512e611077f760ab Mon Sep 17 00:00:00 2001 2 | From: Jon Fawcett 3 | Date: Tue, 21 Sep 2021 12:20:08 -0400 4 | Subject: [PATCH] Nightscout Profile Upload Patch 5 | 6 | --- 7 | Loop/Managers/LoopDataManager.swift | 16 ++++++++++++++++ 8 | Loop/Managers/NightscoutDataManager.swift | 4 ++-- 9 | 2 files changed, 18 insertions(+), 2 deletions(-) 10 | 11 | diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift 12 | index bf44d9f17b..ef861a680a 100644 13 | --- a/Loop/Managers/LoopDataManager.swift 14 | +++ b/Loop/Managers/LoopDataManager.swift 15 | @@ -152,6 +152,9 @@ final class LoopDataManager { 16 | } 17 | UserDefaults.appGroup?.loopSettings = settings 18 | notify(forChange: .preferences) 19 | + if settings.overridePresets != oldValue.overridePresets || settings.glucoseTargetRangeSchedule != oldValue.glucoseTargetRangeSchedule { 20 | + self.notifyUpload(forChange: .preferences) 21 | + } 22 | AnalyticsManager.shared.didChangeLoopSettings(from: oldValue, to: settings) 23 | } 24 | } 25 | @@ -330,6 +333,7 @@ extension LoopDataManager { 26 | doseStore.basalProfile = newValue 27 | UserDefaults.appGroup?.basalRateSchedule = newValue 28 | notify(forChange: .preferences) 29 | + notifyUpload(forChange: .preferences) 30 | 31 | if let newValue = newValue, let oldValue = doseStore.basalProfile, newValue.items != oldValue.items { 32 | AnalyticsManager.shared.didChangeBasalRateSchedule() 33 | @@ -357,6 +361,7 @@ extension LoopDataManager { 34 | carbsOnBoard = nil 35 | 36 | notify(forChange: .preferences) 37 | + notifyUpload(forChange: .preferences) 38 | } 39 | } 40 | 41 | @@ -408,6 +413,7 @@ extension LoopDataManager { 42 | self.insulinEffect = nil 43 | 44 | self.notify(forChange: .preferences) 45 | + self.notifyUpload(forChange: .preferences) 46 | } 47 | } 48 | } 49 | @@ -859,6 +865,15 @@ extension LoopDataManager { 50 | ] 51 | ) 52 | } 53 | + 54 | + private func notifyUpload(forChange context: LoopUpdateContext) { 55 | + NotificationCenter.default.post(name: .LoopDataUpload, 56 | + object: self, 57 | + userInfo: [ 58 | + type(of: self).LoopUpdateContextKey: context.rawValue 59 | + ] 60 | + ) 61 | + } 62 | 63 | /// Computes amount of insulin from boluses that have been issued and not confirmed, and 64 | /// remaining insulin delivery from temporary basal rate adjustments above scheduled rate 65 | @@ -1572,6 +1587,7 @@ extension LoopDataManager { 66 | 67 | extension Notification.Name { 68 | static let LoopDataUpdated = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpdated") 69 | + static let LoopDataUpload = Notification.Name(rawValue: "com.loopkit.Loop.LoopDataUpload") 70 | static let LoopRunning = Notification.Name(rawValue: "com.loopkit.Loop.LoopRunning") 71 | static let LoopCompleted = Notification.Name(rawValue: "com.loopkit.Loop.LoopCompleted") 72 | } 73 | diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift 74 | index e2d020d398..48823116ee 100644 75 | --- a/Loop/Managers/NightscoutDataManager.swift 76 | +++ b/Loop/Managers/NightscoutDataManager.swift 77 | @@ -35,11 +35,11 @@ final class NightscoutDataManager { 78 | self.deviceManager = deviceDataManager 79 | 80 | NotificationCenter.default.addObserver(self, selector: #selector(loopCompleted(_:)), name: .LoopCompleted, object: deviceDataManager.loopManager) 81 | - NotificationCenter.default.addObserver(self, selector: #selector(loopDataUpdated(_:)), name: .LoopDataUpdated, object: deviceDataManager.loopManager) 82 | + NotificationCenter.default.addObserver(self, selector: #selector(loopDataUpload(_:)), name: .LoopDataUpload, object: deviceDataManager.loopManager) 83 | } 84 | 85 | 86 | - @objc func loopDataUpdated(_ note: Notification) { 87 | + @objc func loopDataUpload(_ note: Notification) { 88 | guard 89 | let rawContext = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as? LoopDataManager.LoopUpdateContext.RawValue, 90 | let context = LoopDataManager.LoopUpdateContext(rawValue: rawContext), 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loop Patches 2 | 3 | *** 4 | 5 | # Do not use these patches for Loop Dev or the upcoming Loop v 3.0 6 | 7 | New patches for Dev and future versions are available at [https://github.com/CustomTypeOne/LoopPatches](https://github.com/CustomTypeOne/LoopPatches) 8 | 9 | *** 10 | 11 | ```diff 12 | - Using the patch command in git allows you to apply code from other developers, pull requests, 13 | - or even saved customizations you regularly make. If you are applying code from other developers, 14 | - please ensure that you look through the code and understand what the code changes are before 15 | - incorporating it. It is your responsibility to understand and implement your Loop system and 16 | - you do this at your own risk. As of 2022, I am no longer maintaining this current patch due 17 | - to work on the upcoming Loop 3.0. I expect this will continue to work for Loop 2.x but it 18 | - is your responsibility to test it before use. 19 | ``` 20 | 21 | # Automatic Dose Strategy Switching 22 | ABSwitcherPatch.txt 23 | 24 | In my experience Automatic Bolus works great to combat highs, but can be too aggressive overnight or when dealing with lows. This automatic switcher allows you to set a BG threshold for switching between temp basal and automatic bolus. For instance if set at 130 mg/dl, Loop will automatically use temp basals under 130 mg/dl and switch to automatic boluses over 130 mg/dl. This has greatly solved the issue of needing to have AB aggressive enough to combat high BG while needing it less aggressive in lower range. 25 | 26 | Only apply this patch if you want the switcher without the profile fix. 27 | 28 | # Nightscout Profile Upload 29 | NightscoutProfilePatch.txt 30 | 31 | Typically Loop uploads a new profile everytime any setting is changed. This includes starting/stopping an override as well as when the dosing strategy is changed. This can generates thousands of extra profile saves to Nightscout each month which can cause other issues. The patch limits Loop to only upload a new profile if Basal, Carb Ratio, ISF, target range, or an override setting is changed. 32 | 33 | Only apply this patch if you want the profile fix without the switcher. 34 | 35 | # Combined Patch 36 | ABSwitcherAndProfileUploader.txt 37 | 38 | This patch incorporates the Strategy Switch er and Nightscout profile uploader fix together. Both are needed in order to ensure you do not overload your NS database profile table while using the switcher patch. 39 | 40 | Apple this patch if you want both the switcher and profile updload fix. This is recommended. 41 | 42 | Please watch my YouTube video for directions on installation. Update to video: The NS patch now handles uploading override modifications correctly. 43 | https://www.youtube.com/watch?v=m6Cy4nlkaNQ&t=4s 44 | --------------------------------------------------------------------------------