From dcb4e873f29ff0c42e576c0a5374e3ceb7149bfa Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Mon, 25 Sep 2023 15:49:30 -0300 Subject: [PATCH 01/26] [COASTAL-1291] plugin identifier is no longer class property (#6) --- MinimedKit/PumpManager/MinimedPumpManager.swift | 12 +++++++----- MinimedKitUI/MinimedHUDProvider.swift | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index eced913..61da788 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -17,7 +17,9 @@ public protocol MinimedPumpManagerStateObserver: AnyObject { public class MinimedPumpManager: RileyLinkPumpManager { - public static let pluginIdentifier = "Minimed500" + public static let managerIdentifier = "Minimed500" + + public var pluginIdentifier: String { Self.managerIdentifier } // Primarily used for testing public let dateGenerator: () -> Date @@ -28,7 +30,7 @@ public class MinimedPumpManager: RileyLinkPumpManager { self.dateGenerator = dateGenerator self.hkDevice = HKDevice( - name: MinimedPumpManager.pluginIdentifier, + name: MinimedPumpManager.managerIdentifier, manufacturer: "Medtronic", model: state.pumpModel.rawValue, hardwareVersion: nil, @@ -489,7 +491,7 @@ extension MinimedPumpManager { } private static var pumpBatteryLowAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpBatteryLow") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpBatteryLow") } private var pumpBatteryLowAlert: Alert { @@ -554,7 +556,7 @@ extension MinimedPumpManager { } private static var pumpReservoirEmptyAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpReservoirEmpty") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpReservoirEmpty") } private var pumpReservoirEmptyAlert: Alert { @@ -565,7 +567,7 @@ extension MinimedPumpManager { } private static var pumpReservoirLowAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpReservoirLow") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpReservoirLow") } private func pumpReservoirLowAlertForAmount(_ units: Double, andTimeRemaining remaining: TimeInterval?) -> Alert { diff --git a/MinimedKitUI/MinimedHUDProvider.swift b/MinimedKitUI/MinimedHUDProvider.swift index d02e8b8..b88f481 100644 --- a/MinimedKitUI/MinimedHUDProvider.swift +++ b/MinimedKitUI/MinimedHUDProvider.swift @@ -15,7 +15,7 @@ import SwiftUI class MinimedHUDProvider: HUDProvider { var managerIdentifier: String { - return MinimedPumpManager.pluginIdentifier + return MinimedPumpManager.managerIdentifier } private var state: MinimedPumpManagerState { From 474972ebb7a36d67823a127c01f5b083ba896225 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Mon, 25 Sep 2023 15:49:30 -0300 Subject: [PATCH 02/26] [COASTAL-1291] plugin identifier is no longer class property (#6) --- MinimedKit/PumpManager/MinimedPumpManager.swift | 12 +++++++----- MinimedKitUI/MinimedHUDProvider.swift | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index eced913..61da788 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -17,7 +17,9 @@ public protocol MinimedPumpManagerStateObserver: AnyObject { public class MinimedPumpManager: RileyLinkPumpManager { - public static let pluginIdentifier = "Minimed500" + public static let managerIdentifier = "Minimed500" + + public var pluginIdentifier: String { Self.managerIdentifier } // Primarily used for testing public let dateGenerator: () -> Date @@ -28,7 +30,7 @@ public class MinimedPumpManager: RileyLinkPumpManager { self.dateGenerator = dateGenerator self.hkDevice = HKDevice( - name: MinimedPumpManager.pluginIdentifier, + name: MinimedPumpManager.managerIdentifier, manufacturer: "Medtronic", model: state.pumpModel.rawValue, hardwareVersion: nil, @@ -489,7 +491,7 @@ extension MinimedPumpManager { } private static var pumpBatteryLowAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpBatteryLow") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpBatteryLow") } private var pumpBatteryLowAlert: Alert { @@ -554,7 +556,7 @@ extension MinimedPumpManager { } private static var pumpReservoirEmptyAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpReservoirEmpty") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpReservoirEmpty") } private var pumpReservoirEmptyAlert: Alert { @@ -565,7 +567,7 @@ extension MinimedPumpManager { } private static var pumpReservoirLowAlertIdentifier: Alert.Identifier { - return Alert.Identifier(managerIdentifier: pluginIdentifier, alertIdentifier: "PumpReservoirLow") + return Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "PumpReservoirLow") } private func pumpReservoirLowAlertForAmount(_ units: Double, andTimeRemaining remaining: TimeInterval?) -> Alert { diff --git a/MinimedKitUI/MinimedHUDProvider.swift b/MinimedKitUI/MinimedHUDProvider.swift index d02e8b8..b88f481 100644 --- a/MinimedKitUI/MinimedHUDProvider.swift +++ b/MinimedKitUI/MinimedHUDProvider.swift @@ -15,7 +15,7 @@ import SwiftUI class MinimedHUDProvider: HUDProvider { var managerIdentifier: String { - return MinimedPumpManager.pluginIdentifier + return MinimedPumpManager.managerIdentifier } private var state: MinimedPumpManagerState { From 2d708c3df59988db2b779c10097ec7fd45587fa2 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 1 Mar 2024 15:13:20 -0600 Subject: [PATCH 03/26] Types moved to LoopAlgorithm --- MinimedKit/PumpManager/EnliteSensorDisplayable.swift | 2 +- MinimedKit/PumpManager/MinimedPumpManager.swift | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift index c567b11..4dd0d28 100644 --- a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift +++ b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift @@ -9,7 +9,7 @@ import Foundation import HealthKit import LoopKit - +import LoopAlgorithm struct EnliteSensorDisplayable: Equatable, GlucoseDisplayable { public let isStateValid: Bool diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 74152d1..3e574f5 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -757,9 +757,11 @@ extension MinimedPumpManager { // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in + var dose = event.dose + dose?.insulinType = insulinType return NewPumpEvent( date: event.date, - dose: event.dose?.annotated(with: insulinType), + dose: dose, raw: event.raw, title: event.title, type: event.type) From 915d30d6460d58dbf007cd96cfd276e44fd3e3a2 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 1 Mar 2024 15:13:20 -0600 Subject: [PATCH 04/26] Types moved to LoopAlgorithm --- MinimedKit/PumpManager/EnliteSensorDisplayable.swift | 2 +- MinimedKit/PumpManager/MinimedPumpManager.swift | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift index c567b11..4dd0d28 100644 --- a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift +++ b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift @@ -9,7 +9,7 @@ import Foundation import HealthKit import LoopKit - +import LoopAlgorithm struct EnliteSensorDisplayable: Equatable, GlucoseDisplayable { public let isStateValid: Bool diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 74152d1..3e574f5 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -757,9 +757,11 @@ extension MinimedPumpManager { // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in + var dose = event.dose + dose?.insulinType = insulinType return NewPumpEvent( date: event.date, - dose: event.dose?.annotated(with: insulinType), + dose: dose, raw: event.raw, title: event.title, type: event.type) From 66503e4fd61a41334f9645f275578cd20e6decba Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 7 Jun 2024 14:12:52 -0300 Subject: [PATCH 05/26] [LOOP-4801] adding pump inoperable (#9) --- MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index e3144d0..e35759e 100644 --- a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift +++ b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift @@ -161,7 +161,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { switch basalDeliveryState { case .active(_), .initiatingTempBasal: return true - case .tempBasal(_), .cancelingTempBasal, .suspending, .suspended(_), .resuming, .none: + default: return false } } @@ -188,8 +188,6 @@ class MinimedPumpSettingsViewModel: ObservableObject { var basalDeliveryRate: Double? { switch basalDeliveryState { - case .suspending, .resuming, .suspended, .none, .initiatingTempBasal, .cancelingTempBasal: - return nil case .active: // return scheduled basal rate var calendar = Calendar(identifier: .gregorian) @@ -197,6 +195,8 @@ class MinimedPumpSettingsViewModel: ObservableObject { return pumpManager.state.basalSchedule.currentRate(using: calendar) case .tempBasal(let dose): return dose.unitsPerHour + default: + return nil } } @@ -273,7 +273,7 @@ extension PumpManagerStatus.BasalDeliveryState { var shownAction: SuspendResumeAction { switch self { - case .active, .suspending, .tempBasal, .cancelingTempBasal, .initiatingTempBasal: + case .active, .suspending, .tempBasal, .cancelingTempBasal, .initiatingTempBasal, .pumpInoperable: return .suspend case .suspended, .resuming: return .resume @@ -286,7 +286,7 @@ extension PumpManagerStatus.BasalDeliveryState { return LocalizedString("Suspend Delivery", comment: "Title text for button to suspend insulin delivery") case .suspending: return LocalizedString("Suspending", comment: "Title text for button when insulin delivery is in the process of being stopped") - case .suspended: + case .suspended, .pumpInoperable: return LocalizedString("Resume Delivery", comment: "Title text for button to resume insulin delivery") case .resuming: return LocalizedString("Resuming", comment: "Title text for button when insulin delivery is in the process of being resumed") From 50b0fe10436201af1ea872769fd1bbb2a8f607e1 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 7 Jun 2024 14:12:52 -0300 Subject: [PATCH 06/26] [LOOP-4801] adding pump inoperable (#9) --- MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index e3144d0..e35759e 100644 --- a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift +++ b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift @@ -161,7 +161,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { switch basalDeliveryState { case .active(_), .initiatingTempBasal: return true - case .tempBasal(_), .cancelingTempBasal, .suspending, .suspended(_), .resuming, .none: + default: return false } } @@ -188,8 +188,6 @@ class MinimedPumpSettingsViewModel: ObservableObject { var basalDeliveryRate: Double? { switch basalDeliveryState { - case .suspending, .resuming, .suspended, .none, .initiatingTempBasal, .cancelingTempBasal: - return nil case .active: // return scheduled basal rate var calendar = Calendar(identifier: .gregorian) @@ -197,6 +195,8 @@ class MinimedPumpSettingsViewModel: ObservableObject { return pumpManager.state.basalSchedule.currentRate(using: calendar) case .tempBasal(let dose): return dose.unitsPerHour + default: + return nil } } @@ -273,7 +273,7 @@ extension PumpManagerStatus.BasalDeliveryState { var shownAction: SuspendResumeAction { switch self { - case .active, .suspending, .tempBasal, .cancelingTempBasal, .initiatingTempBasal: + case .active, .suspending, .tempBasal, .cancelingTempBasal, .initiatingTempBasal, .pumpInoperable: return .suspend case .suspended, .resuming: return .resume @@ -286,7 +286,7 @@ extension PumpManagerStatus.BasalDeliveryState { return LocalizedString("Suspend Delivery", comment: "Title text for button to suspend insulin delivery") case .suspending: return LocalizedString("Suspending", comment: "Title text for button when insulin delivery is in the process of being stopped") - case .suspended: + case .suspended, .pumpInoperable: return LocalizedString("Resume Delivery", comment: "Title text for button to resume insulin delivery") case .resuming: return LocalizedString("Resuming", comment: "Title text for button when insulin delivery is in the process of being resumed") From 08b7a931d2b5003f09e6cfde0a635ea2959befbd Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 21 Nov 2024 15:48:22 -0800 Subject: [PATCH 07/26] [LOOP-5153] Remove HealthKit dependency from LoopAlgorithm --- .../MySentryPumpStatusMessageBody+CGMManager.swift | 4 ++-- .../PumpManager/EnliteSensorDisplayable.swift | 5 ++--- MinimedKit/PumpManager/MinimedPumpManager.swift | 13 +++++++------ .../Views/MinimedPumpSettingsViewModel.swift | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift b/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift index 35f7c9e..7f506a3 100644 --- a/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift +++ b/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift @@ -6,7 +6,7 @@ // Copyright © 2016 Nathan Racklyeft. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit @@ -39,7 +39,7 @@ extension MySentryPumpStatusMessageBody: GlucoseDisplayable { } } - public var trendRate: HKQuantity? { + public var trendRate: LoopQuantity? { return nil } diff --git a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift index 4dd0d28..d67ccb1 100644 --- a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift +++ b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift @@ -7,14 +7,13 @@ // import Foundation -import HealthKit import LoopKit import LoopAlgorithm struct EnliteSensorDisplayable: Equatable, GlucoseDisplayable { public let isStateValid: Bool public let trendType: LoopKit.GlucoseTrend? - public let trendRate: HKQuantity? + public let trendRate: LoopQuantity? public let isLocal: Bool // TODO Placeholder. This functionality will come with LOOP-1311 @@ -50,7 +49,7 @@ extension MinimedKit.RelativeTimestampedGlucoseEvent { return nil } - var trendRate: HKQuantity? { + var trendRate: LoopQuantity? { return nil } diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 3e574f5..0cc7b26 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -6,6 +6,7 @@ // import HealthKit +import LoopAlgorithm import LoopKit import RileyLinkKit import RileyLinkBLEKit @@ -430,7 +431,7 @@ extension MinimedPumpManager { if let date = glucoseDateComponents?.date { let sample = NewGlucoseSample( date: date, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose)), condition: nil, trend: status.glucoseTrend.loopKitGlucoseTrend, trendRate: nil, @@ -1430,13 +1431,13 @@ extension MinimedPumpManager: PumpManager { try session.setMaxBasalRate(unitsPerHour: maxBasalRate) } - if let maxBolus = deliveryLimits.maximumBolus?.doubleValue(for: .internationalUnit()) { + if let maxBolus = deliveryLimits.maximumBolus?.doubleValue(for: .internationalUnit) { try session.setMaxBolus(units: maxBolus) } let settings = try session.getSettings() - let storedDeliveryLimits = DeliveryLimits(maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: settings.maxBasal), - maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: settings.maxBolus)) + let storedDeliveryLimits = DeliveryLimits(maximumBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: settings.maxBasal), + maximumBolus: LoopQuantity(unit: .internationalUnit, doubleValue: settings.maxBolus)) completion(.success(storedDeliveryLimits)) } catch let error { self.log.error("Save delivery limit settings failed: %{public}@", String(describing: error)) @@ -1556,13 +1557,13 @@ extension MinimedPumpManager: CGMManager { self.recents.sensorState = EnliteSensorDisplayable(latestSensorEvent) } - let unit = HKUnit.milligramsPerDeciliter + let unit = LoopUnit.milligramsPerDeciliter let glucoseValues: [NewGlucoseSample] = events // TODO: Is the { $0.date > latestGlucoseDate } filter duplicative? .filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > latestGlucoseDate }) .map { let glucoseEvent = $0.glucoseEvent as! SensorValueGlucoseEvent - let quantity = HKQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv)) + let quantity = LoopQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv)) return NewGlucoseSample(date: $0.date, quantity: quantity, condition: nil, trend: glucoseEvent.trendType, trendRate: glucoseEvent.trendRate, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: glucoseEvent.glucoseSyncIdentifier ?? UUID().uuidString, device: self.device) } diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index e35759e..473aafd 100644 --- a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift +++ b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift @@ -11,7 +11,7 @@ import MinimedKit import LoopKit import SwiftUI import LoopKitUI -import HealthKit +import LoopAlgorithm enum MinimedSettingsViewAlert: Identifiable { @@ -96,7 +96,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { }() let reservoirVolumeFormatter = { - let formatter = QuantityFormatter(for: .internationalUnit()) + let formatter = QuantityFormatter(for: .internationalUnit) formatter.numberFormatter.maximumFractionDigits = 1 return formatter }() @@ -225,7 +225,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { } func reservoirText(for units: Double) -> String { - let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: units) + let quantity = LoopQuantity(unit: .internationalUnit, doubleValue: units) return reservoirVolumeFormatter.string(from: quantity) ?? "" } From e9884223b05fd8ad43e36950b14a28ac966356e1 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 21 Nov 2024 15:48:22 -0800 Subject: [PATCH 08/26] [LOOP-5153] Remove HealthKit dependency from LoopAlgorithm --- .../MySentryPumpStatusMessageBody+CGMManager.swift | 4 ++-- .../PumpManager/EnliteSensorDisplayable.swift | 5 ++--- MinimedKit/PumpManager/MinimedPumpManager.swift | 13 +++++++------ .../Views/MinimedPumpSettingsViewModel.swift | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift b/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift index 35f7c9e..7f506a3 100644 --- a/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift +++ b/MinimedKit/CGMManager/MySentryPumpStatusMessageBody+CGMManager.swift @@ -6,7 +6,7 @@ // Copyright © 2016 Nathan Racklyeft. All rights reserved. // -import HealthKit +import LoopAlgorithm import LoopKit @@ -39,7 +39,7 @@ extension MySentryPumpStatusMessageBody: GlucoseDisplayable { } } - public var trendRate: HKQuantity? { + public var trendRate: LoopQuantity? { return nil } diff --git a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift index 4dd0d28..d67ccb1 100644 --- a/MinimedKit/PumpManager/EnliteSensorDisplayable.swift +++ b/MinimedKit/PumpManager/EnliteSensorDisplayable.swift @@ -7,14 +7,13 @@ // import Foundation -import HealthKit import LoopKit import LoopAlgorithm struct EnliteSensorDisplayable: Equatable, GlucoseDisplayable { public let isStateValid: Bool public let trendType: LoopKit.GlucoseTrend? - public let trendRate: HKQuantity? + public let trendRate: LoopQuantity? public let isLocal: Bool // TODO Placeholder. This functionality will come with LOOP-1311 @@ -50,7 +49,7 @@ extension MinimedKit.RelativeTimestampedGlucoseEvent { return nil } - var trendRate: HKQuantity? { + var trendRate: LoopQuantity? { return nil } diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 3e574f5..0cc7b26 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -6,6 +6,7 @@ // import HealthKit +import LoopAlgorithm import LoopKit import RileyLinkKit import RileyLinkBLEKit @@ -430,7 +431,7 @@ extension MinimedPumpManager { if let date = glucoseDateComponents?.date { let sample = NewGlucoseSample( date: date, - quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose)), + quantity: LoopQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose)), condition: nil, trend: status.glucoseTrend.loopKitGlucoseTrend, trendRate: nil, @@ -1430,13 +1431,13 @@ extension MinimedPumpManager: PumpManager { try session.setMaxBasalRate(unitsPerHour: maxBasalRate) } - if let maxBolus = deliveryLimits.maximumBolus?.doubleValue(for: .internationalUnit()) { + if let maxBolus = deliveryLimits.maximumBolus?.doubleValue(for: .internationalUnit) { try session.setMaxBolus(units: maxBolus) } let settings = try session.getSettings() - let storedDeliveryLimits = DeliveryLimits(maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: settings.maxBasal), - maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: settings.maxBolus)) + let storedDeliveryLimits = DeliveryLimits(maximumBasalRate: LoopQuantity(unit: .internationalUnitsPerHour, doubleValue: settings.maxBasal), + maximumBolus: LoopQuantity(unit: .internationalUnit, doubleValue: settings.maxBolus)) completion(.success(storedDeliveryLimits)) } catch let error { self.log.error("Save delivery limit settings failed: %{public}@", String(describing: error)) @@ -1556,13 +1557,13 @@ extension MinimedPumpManager: CGMManager { self.recents.sensorState = EnliteSensorDisplayable(latestSensorEvent) } - let unit = HKUnit.milligramsPerDeciliter + let unit = LoopUnit.milligramsPerDeciliter let glucoseValues: [NewGlucoseSample] = events // TODO: Is the { $0.date > latestGlucoseDate } filter duplicative? .filter({ $0.glucoseEvent is SensorValueGlucoseEvent && $0.date > latestGlucoseDate }) .map { let glucoseEvent = $0.glucoseEvent as! SensorValueGlucoseEvent - let quantity = HKQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv)) + let quantity = LoopQuantity(unit: unit, doubleValue: Double(glucoseEvent.sgv)) return NewGlucoseSample(date: $0.date, quantity: quantity, condition: nil, trend: glucoseEvent.trendType, trendRate: glucoseEvent.trendRate, isDisplayOnly: false, wasUserEntered: false, syncIdentifier: glucoseEvent.glucoseSyncIdentifier ?? UUID().uuidString, device: self.device) } diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index e35759e..473aafd 100644 --- a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift +++ b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift @@ -11,7 +11,7 @@ import MinimedKit import LoopKit import SwiftUI import LoopKitUI -import HealthKit +import LoopAlgorithm enum MinimedSettingsViewAlert: Identifiable { @@ -96,7 +96,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { }() let reservoirVolumeFormatter = { - let formatter = QuantityFormatter(for: .internationalUnit()) + let formatter = QuantityFormatter(for: .internationalUnit) formatter.numberFormatter.maximumFractionDigits = 1 return formatter }() @@ -225,7 +225,7 @@ class MinimedPumpSettingsViewModel: ObservableObject { } func reservoirText(for units: Double) -> String { - let quantity = HKQuantity(unit: .internationalUnit(), doubleValue: units) + let quantity = LoopQuantity(unit: .internationalUnit, doubleValue: units) return reservoirVolumeFormatter.string(from: quantity) ?? "" } From 8c390a4e3a75fbb60f33d4c0cefd92e1ea731983 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 21 Nov 2024 22:12:10 -0800 Subject: [PATCH 09/26] [LOOP-5153] Remove HealthKit dependency from LoopAlgorithm --- MinimedKit.xcodeproj/project.pbxproj | 4 ---- MinimedKit/Extensions/HKUnit.swift | 23 ----------------------- 2 files changed, 27 deletions(-) delete mode 100644 MinimedKit/Extensions/HKUnit.swift diff --git a/MinimedKit.xcodeproj/project.pbxproj b/MinimedKit.xcodeproj/project.pbxproj index f023f78..6062e4e 100644 --- a/MinimedKit.xcodeproj/project.pbxproj +++ b/MinimedKit.xcodeproj/project.pbxproj @@ -285,7 +285,6 @@ C1E34B7D29C7B075009A50A5 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E34B7C29C7B075009A50A5 /* RileyLinkKit.framework */; }; C1E34B8129C7B155009A50A5 /* PumpOpsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8029C7B155009A50A5 /* PumpOpsSession.swift */; }; C1E34B8329C7B1AB009A50A5 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */; }; - C1E34B8529C7B1D4009A50A5 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */; }; C1E34B8829C7B292009A50A5 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8729C7B292009A50A5 /* Comparable.swift */; }; C1E34B8A29C7B2FC009A50A5 /* LocalisedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8929C7B2FC009A50A5 /* LocalisedString.swift */; }; C1E34B8C29C7B334009A50A5 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8B29C7B334009A50A5 /* IdentifiableClass.swift */; }; @@ -695,7 +694,6 @@ C1E34B7C29C7B075009A50A5 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C1E34B8029C7B155009A50A5 /* PumpOpsSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsSession.swift; sourceTree = ""; }; C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; - C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; C1E34B8729C7B292009A50A5 /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; C1E34B8929C7B2FC009A50A5 /* LocalisedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalisedString.swift; sourceTree = ""; }; C1E34B8B29C7B334009A50A5 /* IdentifiableClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = ""; }; @@ -1147,7 +1145,6 @@ C1E34B7829C7AFF7009A50A5 /* TimeInterval.swift */, C1E34B7A29C7B044009A50A5 /* OSLog.swift */, C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */, - C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */, ); path = Extensions; sourceTree = ""; @@ -1580,7 +1577,6 @@ C1E34AC329C7A98F009A50A5 /* DataFrameMessageBody.swift in Sources */, C1E34A7629C7A98F009A50A5 /* ChangeSensorRateOfChangeAlertSetupPumpEvent.swift in Sources */, C1E34AA929C7A98F009A50A5 /* MySentryAlertType.swift in Sources */, - C1E34B8529C7B1D4009A50A5 /* HKUnit.swift in Sources */, C1E34ABC29C7A98F009A50A5 /* ChangeMaxBolusMessageBody.swift in Sources */, C1E34A4229C7A98F009A50A5 /* PumpSettings.swift in Sources */, C1E34AA129C7A98F009A50A5 /* DeviceLinkMessageBody.swift in Sources */, diff --git a/MinimedKit/Extensions/HKUnit.swift b/MinimedKit/Extensions/HKUnit.swift deleted file mode 100644 index 33ebde2..0000000 --- a/MinimedKit/Extensions/HKUnit.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// HKUnit.swift -// MinimedKit -// -// Created by Pete Schwamb on 3/19/23. -// - -import HealthKit - -extension HKUnit { - static let milligramsPerDeciliter: HKUnit = { - return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - }() - - static let millimolesPerLiter: HKUnit = { - return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) - }() - - static let internationalUnitsPerHour: HKUnit = { - return HKUnit.internationalUnit().unitDivided(by: .hour()) - }() - -} From 7d0a5e7277208e9ef7a24264c7964cead46f906d Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Thu, 21 Nov 2024 22:12:10 -0800 Subject: [PATCH 10/26] [LOOP-5153] Remove HealthKit dependency from LoopAlgorithm --- MinimedKit.xcodeproj/project.pbxproj | 4 ---- MinimedKit/Extensions/HKUnit.swift | 23 ----------------------- 2 files changed, 27 deletions(-) delete mode 100644 MinimedKit/Extensions/HKUnit.swift diff --git a/MinimedKit.xcodeproj/project.pbxproj b/MinimedKit.xcodeproj/project.pbxproj index f023f78..6062e4e 100644 --- a/MinimedKit.xcodeproj/project.pbxproj +++ b/MinimedKit.xcodeproj/project.pbxproj @@ -285,7 +285,6 @@ C1E34B7D29C7B075009A50A5 /* RileyLinkKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E34B7C29C7B075009A50A5 /* RileyLinkKit.framework */; }; C1E34B8129C7B155009A50A5 /* PumpOpsSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8029C7B155009A50A5 /* PumpOpsSession.swift */; }; C1E34B8329C7B1AB009A50A5 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */; }; - C1E34B8529C7B1D4009A50A5 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */; }; C1E34B8829C7B292009A50A5 /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8729C7B292009A50A5 /* Comparable.swift */; }; C1E34B8A29C7B2FC009A50A5 /* LocalisedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8929C7B2FC009A50A5 /* LocalisedString.swift */; }; C1E34B8C29C7B334009A50A5 /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E34B8B29C7B334009A50A5 /* IdentifiableClass.swift */; }; @@ -695,7 +694,6 @@ C1E34B7C29C7B075009A50A5 /* RileyLinkKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RileyLinkKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C1E34B8029C7B155009A50A5 /* PumpOpsSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpOpsSession.swift; sourceTree = ""; }; C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; - C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; C1E34B8729C7B292009A50A5 /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; C1E34B8929C7B2FC009A50A5 /* LocalisedString.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalisedString.swift; sourceTree = ""; }; C1E34B8B29C7B334009A50A5 /* IdentifiableClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = ""; }; @@ -1147,7 +1145,6 @@ C1E34B7829C7AFF7009A50A5 /* TimeInterval.swift */, C1E34B7A29C7B044009A50A5 /* OSLog.swift */, C1E34B8229C7B1AB009A50A5 /* TimeZone.swift */, - C1E34B8429C7B1D4009A50A5 /* HKUnit.swift */, ); path = Extensions; sourceTree = ""; @@ -1580,7 +1577,6 @@ C1E34AC329C7A98F009A50A5 /* DataFrameMessageBody.swift in Sources */, C1E34A7629C7A98F009A50A5 /* ChangeSensorRateOfChangeAlertSetupPumpEvent.swift in Sources */, C1E34AA929C7A98F009A50A5 /* MySentryAlertType.swift in Sources */, - C1E34B8529C7B1D4009A50A5 /* HKUnit.swift in Sources */, C1E34ABC29C7A98F009A50A5 /* ChangeMaxBolusMessageBody.swift in Sources */, C1E34A4229C7A98F009A50A5 /* PumpSettings.swift in Sources */, C1E34AA129C7A98F009A50A5 /* DeviceLinkMessageBody.swift in Sources */, diff --git a/MinimedKit/Extensions/HKUnit.swift b/MinimedKit/Extensions/HKUnit.swift deleted file mode 100644 index 33ebde2..0000000 --- a/MinimedKit/Extensions/HKUnit.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// HKUnit.swift -// MinimedKit -// -// Created by Pete Schwamb on 3/19/23. -// - -import HealthKit - -extension HKUnit { - static let milligramsPerDeciliter: HKUnit = { - return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - }() - - static let millimolesPerLiter: HKUnit = { - return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) - }() - - static let internationalUnitsPerHour: HKUnit = { - return HKUnit.internationalUnit().unitDivided(by: .hour()) - }() - -} From 6be76ed3826649c6a77ab3e5625b67ed05d3f886 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Fri, 6 Dec 2024 14:33:57 -0800 Subject: [PATCH 11/26] Bump to iOS 17 --- MinimedKit.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MinimedKit.xcodeproj/project.pbxproj b/MinimedKit.xcodeproj/project.pbxproj index 6062e4e..68b81e9 100644 --- a/MinimedKit.xcodeproj/project.pbxproj +++ b/MinimedKit.xcodeproj/project.pbxproj @@ -1958,7 +1958,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFCopyLocalizedString, @@ -2020,7 +2020,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFCopyLocalizedString, From c7f831d9827fc0b8d1defc175d671ec402b9c584 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Fri, 6 Dec 2024 14:33:57 -0800 Subject: [PATCH 12/26] Bump to iOS 17 --- MinimedKit.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MinimedKit.xcodeproj/project.pbxproj b/MinimedKit.xcodeproj/project.pbxproj index 6062e4e..68b81e9 100644 --- a/MinimedKit.xcodeproj/project.pbxproj +++ b/MinimedKit.xcodeproj/project.pbxproj @@ -1958,7 +1958,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFCopyLocalizedString, @@ -2020,7 +2020,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.2; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LOCALIZED_STRING_MACRO_NAMES = ( NSLocalizedString, CFCopyLocalizedString, From 60c2af6f8376f1c407178d57889f528f73bd2d45 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 29 May 2025 12:54:11 -0500 Subject: [PATCH 13/26] Updated PumpManagerDelegate protocol (#12) --- MinimedKitTests/Mocks/MockPumpManagerDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift index 643b963..48e68a7 100644 --- a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift +++ b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift @@ -10,6 +10,8 @@ import Foundation import LoopKit class MockPumpManagerDelegate: PumpManagerDelegate { + var automatedTreatmentState: LoopKit.AutomatedTreatmentState? + var automaticDosingEnabled = true var historyFetchStartDate = Date() From 578ae138f0c8fd2d859099d8bb643ea7cffa5807 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 29 May 2025 12:54:11 -0500 Subject: [PATCH 14/26] Updated PumpManagerDelegate protocol (#12) --- MinimedKitTests/Mocks/MockPumpManagerDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift index 643b963..48e68a7 100644 --- a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift +++ b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift @@ -10,6 +10,8 @@ import Foundation import LoopKit class MockPumpManagerDelegate: PumpManagerDelegate { + var automatedTreatmentState: LoopKit.AutomatedTreatmentState? + var automaticDosingEnabled = true var historyFetchStartDate = Date() From 10169ca5c8e0c7710ff49d4ea0ce7a7a1aae211a Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Mon, 2 Jun 2025 13:27:30 -0700 Subject: [PATCH 15/26] [LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent --- MinimedKit/PumpManager/DoseStore.swift | 6 ++++-- .../PumpManager/MinimedPumpManager.swift | 9 ++++----- MinimedKit/PumpManager/UnfinalizedDose.swift | 20 +++++++++++++++---- MinimedKitTests/MinimedPumpManagerTests.swift | 6 +++--- MinimedKitTests/ReconciliationTests.swift | 10 +++++----- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/MinimedKit/PumpManager/DoseStore.swift b/MinimedKit/PumpManager/DoseStore.swift index e051955..7bd03d2 100644 --- a/MinimedKit/PumpManager/DoseStore.swift +++ b/MinimedKit/PumpManager/DoseStore.swift @@ -42,7 +42,7 @@ extension Collection where Element == TimestampedHistoryEvent { if !bolus.wasRemotelyTriggered { automatic = false } - dose = DoseEntry(type: .bolus, startDate: event.date, endDate: bolusEndDate, value: bolus.programmed, unit: .units, deliveredUnits: bolus.amount, automatic: automatic, isMutable: bolus.isMutable(atDate: now, forPump: model), wasProgrammedByPumpUI: !bolus.wasRemotelyTriggered) + dose = DoseEntry(type: .bolus, startDate: event.date, endDate: bolusEndDate, value: bolus.programmed, unit: .units, decisionId: nil, deliveredUnits: bolus.amount, automatic: automatic, isMutable: bolus.isMutable(atDate: now, forPump: model), wasProgrammedByPumpUI: !bolus.wasRemotelyTriggered) case let suspendEvent as SuspendPumpEvent: title = LocalizedString("Suspend", comment: "Event title for suspend") dose = DoseEntry(suspendDate: event.date, wasProgrammedByPumpUI: !suspendEvent.wasRemotelyTriggered) @@ -52,7 +52,7 @@ extension Collection where Element == TimestampedHistoryEvent { dose = DoseEntry(resumeDate: event.date, wasProgrammedByPumpUI: !resumeEvent.wasRemotelyTriggered) case let temp as TempBasalPumpEvent: if case .Absolute = temp.rateType { - lastTempBasal = DoseEntry(type: .tempBasal, startDate: event.date, value: temp.rate, unit: .unitsPerHour, isMutable: false, wasProgrammedByPumpUI: !temp.wasRemotelyTriggered) + lastTempBasal = DoseEntry(type: .tempBasal, startDate: event.date, value: temp.rate, unit: .unitsPerHour, decisionId: nil, isMutable: false, wasProgrammedByPumpUI: !temp.wasRemotelyTriggered) continue } else { title = LocalizedString("Percent Temp Basal", comment: "Event title for percent based temp basal") @@ -78,6 +78,7 @@ extension Collection where Element == TimestampedHistoryEvent { endDate: endDate, value: lastTemp.unitsPerHour, unit: .unitsPerHour, + decisionId: nil, automatic: false, // If this was automatic dose, it should be set as such during reconciliation isMutable: isMutable, wasProgrammedByPumpUI: lastTemp.wasProgrammedByPumpUI @@ -92,6 +93,7 @@ extension Collection where Element == TimestampedHistoryEvent { endDate: event.date.addingTimeInterval(.hours(24)), value: basal.scheduleEntry.rate, unit: .unitsPerHour, + decisionId: nil, isMutable: false ) case is RewindPumpEvent: diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 0cc7b26..f105d0b 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -913,7 +913,6 @@ extension MinimedPumpManager { // MARK: - PumpManager extension MinimedPumpManager: PumpManager { - public static let localizedTitle = LocalizedString("Minimed 500/700 Series", comment: "Generic title of the minimed pump manager") public var localizedTitle: String { @@ -1204,7 +1203,7 @@ extension MinimedPumpManager: PumpManager { self.state.pumpModel.bolusDeliveryTime(units: units) } - public func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (PumpManagerError?) -> Void) { + public func enactBolus(decisionId: UUID?, units: Double, activationType: BolusActivationType, completion: @escaping (PumpManagerError?) -> Void) { let enactUnits = roundToSupportedBolusVolume(units: units) guard enactUnits > 0 else { @@ -1282,7 +1281,7 @@ extension MinimedPumpManager: PumpManager { let commsOffset = TimeInterval(seconds: -2) let doseStart = self.dateGenerator().addingTimeInterval(commsOffset) - let dose = UnfinalizedDose(bolusAmount: enactUnits, startTime: doseStart, duration: deliveryTime, insulinType: insulinType, automatic: activationType.isAutomatic) + let dose = UnfinalizedDose(decisionId: decisionId, bolusAmount: enactUnits, startTime: doseStart, duration: deliveryTime, insulinType: insulinType, automatic: activationType.isAutomatic) self.setState({ (state) in state.unfinalizedBolus = dose }) @@ -1312,7 +1311,7 @@ extension MinimedPumpManager: PumpManager { } } - public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) { + public func enactTempBasal(decisionId: UUID?, unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) { guard let insulinType = insulinType else { completion(.configuration(MinimedPumpManagerError.insulinTypeNotConfigured)) return @@ -1332,7 +1331,7 @@ extension MinimedPumpManager: PumpManager { case .success: let now = self.dateGenerator() - let dose = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration, insulinType: insulinType, automatic: true) + let dose = UnfinalizedDose(decisionId: decisionId, tempBasalRate: unitsPerHour, startTime: now, duration: duration, insulinType: insulinType, automatic: true) self.recents.tempBasalEngageState = .stable diff --git a/MinimedKit/PumpManager/UnfinalizedDose.swift b/MinimedKit/PumpManager/UnfinalizedDose.swift index de9d0f7..9df9b19 100644 --- a/MinimedKit/PumpManager/UnfinalizedDose.swift +++ b/MinimedKit/PumpManager/UnfinalizedDose.swift @@ -29,6 +29,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti var uuid: UUID let insulinType: InsulinType? let automatic: Bool? + + var decisionId: UUID? var finishTime: Date { get { @@ -64,7 +66,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti return units } - init(bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool, isReconciledWithHistory: Bool = false) { + init(decisionId: UUID?, bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool, isReconciledWithHistory: Bool = false) { + self.decisionId = decisionId self.doseType = .bolus self.units = bolusAmount self.startTime = startTime @@ -76,7 +79,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti self.automatic = automatic } - init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool = true, isReconciledWithHistory: Bool = false) { + init(decisionId: UUID?, tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool = true, isReconciledWithHistory: Bool = false) { + self.decisionId = decisionId self.doseType = .tempBasal self.units = tempBasalRate * duration.hours self.startTime = startTime @@ -195,6 +199,10 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti self.units = units self.startTime = startTime self.duration = duration + + if let decisionIdString = rawValue["decisionId"] as? String { + self.decisionId = UUID(uuidString: decisionIdString)! + } if let scheduledUnits = rawValue["scheduledUnits"] as? Double { self.programmedUnits = scheduledUnits @@ -236,6 +244,10 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti "isReconciledWithHistory": isReconciledWithHistory, "uuid": uuid.uuidString, ] + + if let decisionId { + rawValue["decisionId"] = decisionId.uuidString + } if let scheduledUnits = programmedUnits { rawValue["scheduledUnits"] = scheduledUnits @@ -283,10 +295,10 @@ extension DoseEntry { init (_ dose: UnfinalizedDose, forceFinalization: Bool = false) { switch dose.doseType { case .bolus: - self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization) + self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, decisionId: dose.decisionId, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization) case .tempBasal: let isMutable = !forceFinalization && (!dose.isReconciledWithHistory || !dose.isFinished) - self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: isMutable) + self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, decisionId: dose.decisionId, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: isMutable) case .suspend: self = DoseEntry(suspendDate: dose.startTime, isMutable: !dose.isReconciledWithHistory) case .resume: diff --git a/MinimedKitTests/MinimedPumpManagerTests.swift b/MinimedKitTests/MinimedPumpManagerTests.swift index 58a5555..c9f2d65 100644 --- a/MinimedKitTests/MinimedPumpManagerTests.swift +++ b/MinimedKitTests/MinimedPumpManagerTests.swift @@ -73,7 +73,7 @@ class MinimedPumpManagerTests: XCTestCase { func testBolusWithInvalidResponse() { let exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 2.3, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 2.3, activationType: .manualNoRecommendation) { error in XCTAssertNotNil(error) exp.fulfill() } @@ -87,7 +87,7 @@ class MinimedPumpManagerTests: XCTestCase { ] let exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 2.3, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 2.3, activationType: .manualNoRecommendation) { error in XCTAssertNotNil(error) exp.fulfill() } @@ -109,7 +109,7 @@ class MinimedPumpManagerTests: XCTestCase { ] var exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 3.2, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 3.2, activationType: .manualNoRecommendation) { error in XCTAssertNil(error) exp.fulfill() } diff --git a/MinimedKitTests/ReconciliationTests.swift b/MinimedKitTests/ReconciliationTests.swift index b5c9faa..c597aa4 100644 --- a/MinimedKitTests/ReconciliationTests.swift +++ b/MinimedKitTests/ReconciliationTests.swift @@ -37,10 +37,10 @@ final class ReconciliationTests: XCTestCase { let cancelTime = bolusEventTime.addingTimeInterval(TimeInterval(minutes: 1)) - let unfinalizedBolus = UnfinalizedDose(bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false) + let unfinalizedBolus = UnfinalizedDose(decisionId: nil, bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false) // 5.4 bolus interrupted at 1.0 units - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, deliveredUnits: 1.0) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, decisionId: nil, deliveredUnits: 1.0) let bolusEvent = NewPumpEvent( date: bolusEventTime, @@ -74,9 +74,9 @@ final class ReconciliationTests: XCTestCase { let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount) - let unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false) + let unfinalizedBolus = UnfinalizedDose(decisionId: nil, bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false) - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, decisionId: nil, deliveredUnits: bolusAmount) let bolusEvent = NewPumpEvent( date: bolusEventTime, @@ -111,7 +111,7 @@ final class ReconciliationTests: XCTestCase { let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount) - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, decisionId: nil, deliveredUnits: bolusAmount) let bolusEvent = NewPumpEvent( date: bolusEventTime, From dc1ce149c5976fe17126ab095f16daf540833441 Mon Sep 17 00:00:00 2001 From: Cameron Ingham Date: Mon, 2 Jun 2025 13:27:30 -0700 Subject: [PATCH 16/26] [LOOP-5295] decisionId on DoseEntry and PersistedPumpEvent --- MinimedKit/PumpManager/DoseStore.swift | 6 ++++-- .../PumpManager/MinimedPumpManager.swift | 9 ++++----- MinimedKit/PumpManager/UnfinalizedDose.swift | 20 +++++++++++++++---- MinimedKitTests/MinimedPumpManagerTests.swift | 6 +++--- MinimedKitTests/ReconciliationTests.swift | 10 +++++----- 5 files changed, 32 insertions(+), 19 deletions(-) diff --git a/MinimedKit/PumpManager/DoseStore.swift b/MinimedKit/PumpManager/DoseStore.swift index e051955..7bd03d2 100644 --- a/MinimedKit/PumpManager/DoseStore.swift +++ b/MinimedKit/PumpManager/DoseStore.swift @@ -42,7 +42,7 @@ extension Collection where Element == TimestampedHistoryEvent { if !bolus.wasRemotelyTriggered { automatic = false } - dose = DoseEntry(type: .bolus, startDate: event.date, endDate: bolusEndDate, value: bolus.programmed, unit: .units, deliveredUnits: bolus.amount, automatic: automatic, isMutable: bolus.isMutable(atDate: now, forPump: model), wasProgrammedByPumpUI: !bolus.wasRemotelyTriggered) + dose = DoseEntry(type: .bolus, startDate: event.date, endDate: bolusEndDate, value: bolus.programmed, unit: .units, decisionId: nil, deliveredUnits: bolus.amount, automatic: automatic, isMutable: bolus.isMutable(atDate: now, forPump: model), wasProgrammedByPumpUI: !bolus.wasRemotelyTriggered) case let suspendEvent as SuspendPumpEvent: title = LocalizedString("Suspend", comment: "Event title for suspend") dose = DoseEntry(suspendDate: event.date, wasProgrammedByPumpUI: !suspendEvent.wasRemotelyTriggered) @@ -52,7 +52,7 @@ extension Collection where Element == TimestampedHistoryEvent { dose = DoseEntry(resumeDate: event.date, wasProgrammedByPumpUI: !resumeEvent.wasRemotelyTriggered) case let temp as TempBasalPumpEvent: if case .Absolute = temp.rateType { - lastTempBasal = DoseEntry(type: .tempBasal, startDate: event.date, value: temp.rate, unit: .unitsPerHour, isMutable: false, wasProgrammedByPumpUI: !temp.wasRemotelyTriggered) + lastTempBasal = DoseEntry(type: .tempBasal, startDate: event.date, value: temp.rate, unit: .unitsPerHour, decisionId: nil, isMutable: false, wasProgrammedByPumpUI: !temp.wasRemotelyTriggered) continue } else { title = LocalizedString("Percent Temp Basal", comment: "Event title for percent based temp basal") @@ -78,6 +78,7 @@ extension Collection where Element == TimestampedHistoryEvent { endDate: endDate, value: lastTemp.unitsPerHour, unit: .unitsPerHour, + decisionId: nil, automatic: false, // If this was automatic dose, it should be set as such during reconciliation isMutable: isMutable, wasProgrammedByPumpUI: lastTemp.wasProgrammedByPumpUI @@ -92,6 +93,7 @@ extension Collection where Element == TimestampedHistoryEvent { endDate: event.date.addingTimeInterval(.hours(24)), value: basal.scheduleEntry.rate, unit: .unitsPerHour, + decisionId: nil, isMutable: false ) case is RewindPumpEvent: diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 0cc7b26..f105d0b 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -913,7 +913,6 @@ extension MinimedPumpManager { // MARK: - PumpManager extension MinimedPumpManager: PumpManager { - public static let localizedTitle = LocalizedString("Minimed 500/700 Series", comment: "Generic title of the minimed pump manager") public var localizedTitle: String { @@ -1204,7 +1203,7 @@ extension MinimedPumpManager: PumpManager { self.state.pumpModel.bolusDeliveryTime(units: units) } - public func enactBolus(units: Double, activationType: BolusActivationType, completion: @escaping (PumpManagerError?) -> Void) { + public func enactBolus(decisionId: UUID?, units: Double, activationType: BolusActivationType, completion: @escaping (PumpManagerError?) -> Void) { let enactUnits = roundToSupportedBolusVolume(units: units) guard enactUnits > 0 else { @@ -1282,7 +1281,7 @@ extension MinimedPumpManager: PumpManager { let commsOffset = TimeInterval(seconds: -2) let doseStart = self.dateGenerator().addingTimeInterval(commsOffset) - let dose = UnfinalizedDose(bolusAmount: enactUnits, startTime: doseStart, duration: deliveryTime, insulinType: insulinType, automatic: activationType.isAutomatic) + let dose = UnfinalizedDose(decisionId: decisionId, bolusAmount: enactUnits, startTime: doseStart, duration: deliveryTime, insulinType: insulinType, automatic: activationType.isAutomatic) self.setState({ (state) in state.unfinalizedBolus = dose }) @@ -1312,7 +1311,7 @@ extension MinimedPumpManager: PumpManager { } } - public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) { + public func enactTempBasal(decisionId: UUID?, unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerError?) -> Void) { guard let insulinType = insulinType else { completion(.configuration(MinimedPumpManagerError.insulinTypeNotConfigured)) return @@ -1332,7 +1331,7 @@ extension MinimedPumpManager: PumpManager { case .success: let now = self.dateGenerator() - let dose = UnfinalizedDose(tempBasalRate: unitsPerHour, startTime: now, duration: duration, insulinType: insulinType, automatic: true) + let dose = UnfinalizedDose(decisionId: decisionId, tempBasalRate: unitsPerHour, startTime: now, duration: duration, insulinType: insulinType, automatic: true) self.recents.tempBasalEngageState = .stable diff --git a/MinimedKit/PumpManager/UnfinalizedDose.swift b/MinimedKit/PumpManager/UnfinalizedDose.swift index de9d0f7..9df9b19 100644 --- a/MinimedKit/PumpManager/UnfinalizedDose.swift +++ b/MinimedKit/PumpManager/UnfinalizedDose.swift @@ -29,6 +29,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti var uuid: UUID let insulinType: InsulinType? let automatic: Bool? + + var decisionId: UUID? var finishTime: Date { get { @@ -64,7 +66,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti return units } - init(bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool, isReconciledWithHistory: Bool = false) { + init(decisionId: UUID?, bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool, isReconciledWithHistory: Bool = false) { + self.decisionId = decisionId self.doseType = .bolus self.units = bolusAmount self.startTime = startTime @@ -76,7 +79,8 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti self.automatic = automatic } - init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool = true, isReconciledWithHistory: Bool = false) { + init(decisionId: UUID?, tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool = true, isReconciledWithHistory: Bool = false) { + self.decisionId = decisionId self.doseType = .tempBasal self.units = tempBasalRate * duration.hours self.startTime = startTime @@ -195,6 +199,10 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti self.units = units self.startTime = startTime self.duration = duration + + if let decisionIdString = rawValue["decisionId"] as? String { + self.decisionId = UUID(uuidString: decisionIdString)! + } if let scheduledUnits = rawValue["scheduledUnits"] as? Double { self.programmedUnits = scheduledUnits @@ -236,6 +244,10 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti "isReconciledWithHistory": isReconciledWithHistory, "uuid": uuid.uuidString, ] + + if let decisionId { + rawValue["decisionId"] = decisionId.uuidString + } if let scheduledUnits = programmedUnits { rawValue["scheduledUnits"] = scheduledUnits @@ -283,10 +295,10 @@ extension DoseEntry { init (_ dose: UnfinalizedDose, forceFinalization: Bool = false) { switch dose.doseType { case .bolus: - self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization) + self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, decisionId: dose.decisionId, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization) case .tempBasal: let isMutable = !forceFinalization && (!dose.isReconciledWithHistory || !dose.isFinished) - self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: isMutable) + self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, decisionId: dose.decisionId, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: isMutable) case .suspend: self = DoseEntry(suspendDate: dose.startTime, isMutable: !dose.isReconciledWithHistory) case .resume: diff --git a/MinimedKitTests/MinimedPumpManagerTests.swift b/MinimedKitTests/MinimedPumpManagerTests.swift index 58a5555..c9f2d65 100644 --- a/MinimedKitTests/MinimedPumpManagerTests.swift +++ b/MinimedKitTests/MinimedPumpManagerTests.swift @@ -73,7 +73,7 @@ class MinimedPumpManagerTests: XCTestCase { func testBolusWithInvalidResponse() { let exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 2.3, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 2.3, activationType: .manualNoRecommendation) { error in XCTAssertNotNil(error) exp.fulfill() } @@ -87,7 +87,7 @@ class MinimedPumpManagerTests: XCTestCase { ] let exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 2.3, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 2.3, activationType: .manualNoRecommendation) { error in XCTAssertNotNil(error) exp.fulfill() } @@ -109,7 +109,7 @@ class MinimedPumpManagerTests: XCTestCase { ] var exp = expectation(description: "enactBolus callback") - pumpManager.enactBolus(units: 3.2, activationType: .manualNoRecommendation) { error in + pumpManager.enactBolus(decisionId: nil, units: 3.2, activationType: .manualNoRecommendation) { error in XCTAssertNil(error) exp.fulfill() } diff --git a/MinimedKitTests/ReconciliationTests.swift b/MinimedKitTests/ReconciliationTests.swift index b5c9faa..c597aa4 100644 --- a/MinimedKitTests/ReconciliationTests.swift +++ b/MinimedKitTests/ReconciliationTests.swift @@ -37,10 +37,10 @@ final class ReconciliationTests: XCTestCase { let cancelTime = bolusEventTime.addingTimeInterval(TimeInterval(minutes: 1)) - let unfinalizedBolus = UnfinalizedDose(bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false) + let unfinalizedBolus = UnfinalizedDose(decisionId: nil, bolusAmount: 5.4, startTime: bolusTime, duration: TimeInterval(200), insulinType: .novolog, automatic: false, isReconciledWithHistory: false) // 5.4 bolus interrupted at 1.0 units - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, deliveredUnits: 1.0) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: cancelTime, value: unfinalizedBolus.units, unit: .units, decisionId: nil, deliveredUnits: 1.0) let bolusEvent = NewPumpEvent( date: bolusEventTime, @@ -74,9 +74,9 @@ final class ReconciliationTests: XCTestCase { let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount) - let unfinalizedBolus = UnfinalizedDose(bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false) + let unfinalizedBolus = UnfinalizedDose(decisionId: nil, bolusAmount: bolusAmount, startTime: bolusTime, duration: bolusDuration, insulinType: .novolog, automatic: false, isReconciledWithHistory: false) - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, decisionId: nil, deliveredUnits: bolusAmount) let bolusEvent = NewPumpEvent( date: bolusEventTime, @@ -111,7 +111,7 @@ final class ReconciliationTests: XCTestCase { let bolusDuration = PumpModel.model523.bolusDeliveryTime(units: bolusAmount) - let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, deliveredUnits: bolusAmount) + let eventDose = DoseEntry(type: .bolus, startDate: bolusEventTime, endDate: bolusEventTime.addingTimeInterval(bolusDuration), value: bolusAmount, unit: .units, decisionId: nil, deliveredUnits: bolusAmount) let bolusEvent = NewPumpEvent( date: bolusEventTime, From 4a6e63cea26ba2748490884b7656a090a743b3b8 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 29 Jul 2025 09:14:25 -0500 Subject: [PATCH 17/26] LOOP-5235 Enable scheduled presets (#14) * async updates * Changes for protocol updates --- .../PumpManager/MinimedPumpManager.swift | 28 +++++++++++++------ MinimedKitTests/MinimedPumpManagerTests.swift | 3 ++ .../Mocks/MockPumpManagerDelegate.swift | 12 ++++++-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index f105d0b..8db0b7f 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -267,7 +267,9 @@ public class MinimedPumpManager: RileyLinkPumpManager { let identifier = Alert.Identifier(managerIdentifier: self.pluginIdentifier, alertIdentifier: "lowRLBattery") let alertBody = String(format: LocalizedString("\"%1$@\" has a low battery", comment: "Format string for low battery alert body for RileyLink. (1: device name)"), device.name ?? "unnamed") let content = Alert.Content(title: LocalizedString("Low RileyLink Battery", comment: "Title for RileyLink low battery alert"), body: alertBody, acknowledgeActionButtonLabel: LocalizedString("OK", comment: "Acknowledge button label for RileyLink low battery alert")) - delegate?.issueAlert(Alert(identifier: identifier, foregroundContent: content, backgroundContent: content, trigger: .immediate)) + Task { + await delegate?.issueAlert(Alert(identifier: identifier, foregroundContent: content, backgroundContent: content, trigger: .immediate)) + } } } } @@ -510,13 +512,17 @@ extension MinimedPumpManager { } if oldBatteryPercentage != newBatteryPercentage, newBatteryPercentage == 0 { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpBatteryLowAlert) + Task { + await delegate?.issueAlert(self.pumpBatteryLowAlert) + } } } if let oldBatteryPercentage = oldBatteryPercentage, newBatteryPercentage - oldBatteryPercentage >= batteryReplacementDetectionThreshold { pumpDelegate.notify { (delegate) in - delegate?.retractAlert(identifier: Self.pumpBatteryLowAlertIdentifier) + Task { + await delegate?.retractAlert(identifier: Self.pumpBatteryLowAlertIdentifier) + } } } } @@ -600,7 +606,9 @@ extension MinimedPumpManager { if let previousVolume = lastValue?.unitVolume { guard newValue.unitVolume > 0 else { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpReservoirEmptyAlert) + Task { + await delegate?.issueAlert(self.pumpReservoirEmptyAlert) + } } return } @@ -610,7 +618,9 @@ extension MinimedPumpManager { for threshold in warningThresholds { if newValue.unitVolume <= threshold && previousVolume > threshold { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpReservoirLowAlertForAmount(newValue.unitVolume, andTimeRemaining: nil)) + Task { + await delegate?.issueAlert(self.pumpReservoirLowAlertForAmount(newValue.unitVolume, andTimeRemaining: nil)) + } } break } @@ -620,7 +630,9 @@ extension MinimedPumpManager { // TODO: report this as a pump event, or? //self.analyticsServicesManager.reservoirWasRewound() pumpDelegate.notify { (delegate) in - delegate?.retractAlert(identifier: Self.pumpReservoirLowAlertIdentifier) + Task { + await delegate?.retractAlert(identifier: Self.pumpReservoirLowAlertIdentifier) + } } } } @@ -1577,9 +1589,7 @@ extension MinimedPumpManager: CGMManager { // MARK: - AlertResponder implementation extension MinimedPumpManager { - public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) { - completion(nil) - } + public func acknowledgeAlert(alertIdentifier: LoopKit.Alert.AlertIdentifier) async throws { } } // MARK: - AlertSoundVendor implementation diff --git a/MinimedKitTests/MinimedPumpManagerTests.swift b/MinimedKitTests/MinimedPumpManagerTests.swift index c9f2d65..00403bd 100644 --- a/MinimedKitTests/MinimedPumpManagerTests.swift +++ b/MinimedKitTests/MinimedPumpManagerTests.swift @@ -38,6 +38,7 @@ class MinimedPumpManagerTests: XCTestCase { } + @MainActor override func setUpWithError() throws { let device = MockRileyLinkDevice() @@ -80,6 +81,7 @@ class MinimedPumpManagerTests: XCTestCase { waitForExpectations(timeout: 2) } + @MainActor func testBolusWithUncertainResponseIsReported() { mockMessageSender.responses = [ .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: false))], @@ -101,6 +103,7 @@ class MinimedPumpManagerTests: XCTestCase { XCTAssertEqual(event.dose!.deliveredUnits, 2.3) } + @MainActor func testPendingBolusRemovedIfMissingFromHistory() { mockMessageSender.responses = [ diff --git a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift index 48e68a7..f698c89 100644 --- a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift +++ b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift @@ -70,11 +70,17 @@ class MockPumpManagerDelegate: PumpManagerDelegate { func retractAlert(identifier: Alert.Identifier) {} - func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Result) -> Void) {} + func doesIssuedAlertExist(identifier: LoopKit.Alert.Identifier) async throws -> Bool { + return false + } - func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) {} + func lookupAllUnretracted(managerIdentifier: String) async throws -> [LoopKit.PersistedAlert] { + return [] + } - func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) {} + func lookupAllUnacknowledgedUnretracted(managerIdentifier: String) async throws -> [LoopKit.PersistedAlert] { + return [] + } func recordRetractedAlert(_ alert: Alert, at date: Date) {} From 0e2c1c7f085b405724249e9dd62ee8487d1d342d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 29 Jul 2025 09:14:25 -0500 Subject: [PATCH 18/26] LOOP-5235 Enable scheduled presets (#14) * async updates * Changes for protocol updates --- .../PumpManager/MinimedPumpManager.swift | 28 +++++++++++++------ MinimedKitTests/MinimedPumpManagerTests.swift | 3 ++ .../Mocks/MockPumpManagerDelegate.swift | 12 ++++++-- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index f105d0b..8db0b7f 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -267,7 +267,9 @@ public class MinimedPumpManager: RileyLinkPumpManager { let identifier = Alert.Identifier(managerIdentifier: self.pluginIdentifier, alertIdentifier: "lowRLBattery") let alertBody = String(format: LocalizedString("\"%1$@\" has a low battery", comment: "Format string for low battery alert body for RileyLink. (1: device name)"), device.name ?? "unnamed") let content = Alert.Content(title: LocalizedString("Low RileyLink Battery", comment: "Title for RileyLink low battery alert"), body: alertBody, acknowledgeActionButtonLabel: LocalizedString("OK", comment: "Acknowledge button label for RileyLink low battery alert")) - delegate?.issueAlert(Alert(identifier: identifier, foregroundContent: content, backgroundContent: content, trigger: .immediate)) + Task { + await delegate?.issueAlert(Alert(identifier: identifier, foregroundContent: content, backgroundContent: content, trigger: .immediate)) + } } } } @@ -510,13 +512,17 @@ extension MinimedPumpManager { } if oldBatteryPercentage != newBatteryPercentage, newBatteryPercentage == 0 { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpBatteryLowAlert) + Task { + await delegate?.issueAlert(self.pumpBatteryLowAlert) + } } } if let oldBatteryPercentage = oldBatteryPercentage, newBatteryPercentage - oldBatteryPercentage >= batteryReplacementDetectionThreshold { pumpDelegate.notify { (delegate) in - delegate?.retractAlert(identifier: Self.pumpBatteryLowAlertIdentifier) + Task { + await delegate?.retractAlert(identifier: Self.pumpBatteryLowAlertIdentifier) + } } } } @@ -600,7 +606,9 @@ extension MinimedPumpManager { if let previousVolume = lastValue?.unitVolume { guard newValue.unitVolume > 0 else { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpReservoirEmptyAlert) + Task { + await delegate?.issueAlert(self.pumpReservoirEmptyAlert) + } } return } @@ -610,7 +618,9 @@ extension MinimedPumpManager { for threshold in warningThresholds { if newValue.unitVolume <= threshold && previousVolume > threshold { pumpDelegate.notify { (delegate) in - delegate?.issueAlert(self.pumpReservoirLowAlertForAmount(newValue.unitVolume, andTimeRemaining: nil)) + Task { + await delegate?.issueAlert(self.pumpReservoirLowAlertForAmount(newValue.unitVolume, andTimeRemaining: nil)) + } } break } @@ -620,7 +630,9 @@ extension MinimedPumpManager { // TODO: report this as a pump event, or? //self.analyticsServicesManager.reservoirWasRewound() pumpDelegate.notify { (delegate) in - delegate?.retractAlert(identifier: Self.pumpReservoirLowAlertIdentifier) + Task { + await delegate?.retractAlert(identifier: Self.pumpReservoirLowAlertIdentifier) + } } } } @@ -1577,9 +1589,7 @@ extension MinimedPumpManager: CGMManager { // MARK: - AlertResponder implementation extension MinimedPumpManager { - public func acknowledgeAlert(alertIdentifier: Alert.AlertIdentifier, completion: @escaping (Error?) -> Void) { - completion(nil) - } + public func acknowledgeAlert(alertIdentifier: LoopKit.Alert.AlertIdentifier) async throws { } } // MARK: - AlertSoundVendor implementation diff --git a/MinimedKitTests/MinimedPumpManagerTests.swift b/MinimedKitTests/MinimedPumpManagerTests.swift index c9f2d65..00403bd 100644 --- a/MinimedKitTests/MinimedPumpManagerTests.swift +++ b/MinimedKitTests/MinimedPumpManagerTests.swift @@ -38,6 +38,7 @@ class MinimedPumpManagerTests: XCTestCase { } + @MainActor override func setUpWithError() throws { let device = MockRileyLinkDevice() @@ -80,6 +81,7 @@ class MinimedPumpManagerTests: XCTestCase { waitForExpectations(timeout: 2) } + @MainActor func testBolusWithUncertainResponseIsReported() { mockMessageSender.responses = [ .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: false))], @@ -101,6 +103,7 @@ class MinimedPumpManagerTests: XCTestCase { XCTAssertEqual(event.dose!.deliveredUnits, 2.3) } + @MainActor func testPendingBolusRemovedIfMissingFromHistory() { mockMessageSender.responses = [ diff --git a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift index 48e68a7..f698c89 100644 --- a/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift +++ b/MinimedKitTests/Mocks/MockPumpManagerDelegate.swift @@ -70,11 +70,17 @@ class MockPumpManagerDelegate: PumpManagerDelegate { func retractAlert(identifier: Alert.Identifier) {} - func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Result) -> Void) {} + func doesIssuedAlertExist(identifier: LoopKit.Alert.Identifier) async throws -> Bool { + return false + } - func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) {} + func lookupAllUnretracted(managerIdentifier: String) async throws -> [LoopKit.PersistedAlert] { + return [] + } - func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) {} + func lookupAllUnacknowledgedUnretracted(managerIdentifier: String) async throws -> [LoopKit.PersistedAlert] { + return [] + } func recordRetractedAlert(_ alert: Alert, at date: Date) {} From 92ce32c48882bf965b4b4be6d210f19b88c095e7 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 20 Aug 2025 17:39:22 -0500 Subject: [PATCH 19/26] Updates for pumpmanager delegate protocol (#15) --- .../PumpManager/MinimedPumpManager.swift | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 8db0b7f..46017d0 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -754,68 +754,68 @@ extension MinimedPumpManager { } self.pumpOps.runSession(withName: "Fetch Pump History", using: device) { (session) in - do { - guard let startDate = self.pumpDelegate.call({ (delegate) in - return delegate?.startDateToFilterNewPumpEvents(for: self) - }) else { - preconditionFailure("pumpManagerDelegate cannot be nil") - } - - // Include events up to a minute before startDate, since pump event time and pending event time might be off - self.log.default("Fetching history since %{public}@", String(describing: startDate.addingTimeInterval(.minutes(-1)))) - let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1))) - - // Reconcile history with pending doses - let newPumpEvents = historyEvents.pumpEvents(from: model) - - // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType - let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in - var dose = event.dose - dose?.insulinType = insulinType - return NewPumpEvent( - date: event.date, - dose: dose, - raw: event.raw, - title: event.title, - type: event.type) - } - - self.pumpDelegate.notify({ (delegate) in - guard let delegate = delegate else { + Task { + do { + guard let startDate = await self.pumpDelegate.delegate?.startDateToFilterNewPumpEvents(for: self) else { preconditionFailure("pumpManagerDelegate cannot be nil") } - - let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent() }) - - self.log.default("Reporting new pump events: %{public}@", String(describing: remainingHistoryEvents + pendingEvents)) - - delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.state.lastReconciliation, replacePendingEvents: true) { (error) in - // Called on an unknown queue by the delegate - if error == nil { - self.recents.lastAddedPumpEvents = self.dateGenerator() - self.setState({ (state) in - // Remove any pending doses that have been reconciled and are finished - if let bolus = state.unfinalizedBolus, bolus.isReconciledWithHistory, bolus.isFinished { - state.unfinalizedBolus = nil - } - if let tempBasal = state.unfinalizedTempBasal, tempBasal.isReconciledWithHistory, tempBasal.isFinished { - state.unfinalizedTempBasal = nil - } - state.pendingDoses.removeAll(where: { (dose) -> Bool in - if dose.isReconciledWithHistory && dose.isFinished { - print("Removing stored, finished, reconciled dose: \(dose)") + + // Include events up to a minute before startDate, since pump event time and pending event time might be off + self.log.default("Fetching history since %{public}@", String(describing: startDate.addingTimeInterval(.minutes(-1)))) + let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1))) + + // Reconcile history with pending doses + let newPumpEvents = historyEvents.pumpEvents(from: model) + + // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType + let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in + var dose = event.dose + dose?.insulinType = insulinType + return NewPumpEvent( + date: event.date, + dose: dose, + raw: event.raw, + title: event.title, + type: event.type) + } + + self.pumpDelegate.notify({ (delegate) in + guard let delegate = delegate else { + preconditionFailure("pumpManagerDelegate cannot be nil") + } + + let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent() }) + + self.log.default("Reporting new pump events: %{public}@", String(describing: remainingHistoryEvents + pendingEvents)) + + delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.state.lastReconciliation, replacePendingEvents: true) { (error) in + // Called on an unknown queue by the delegate + if error == nil { + self.recents.lastAddedPumpEvents = self.dateGenerator() + self.setState({ (state) in + // Remove any pending doses that have been reconciled and are finished + if let bolus = state.unfinalizedBolus, bolus.isReconciledWithHistory, bolus.isFinished { + state.unfinalizedBolus = nil + } + if let tempBasal = state.unfinalizedTempBasal, tempBasal.isReconciledWithHistory, tempBasal.isFinished { + state.unfinalizedTempBasal = nil } - return dose.isReconciledWithHistory && dose.isFinished + state.pendingDoses.removeAll(where: { (dose) -> Bool in + if dose.isReconciledWithHistory && dose.isFinished { + print("Removing stored, finished, reconciled dose: \(dose)") + } + return dose.isReconciledWithHistory && dose.isFinished + }) }) - }) + } + completion(error) } - completion(error) - } - }) - } catch let error { - self.troubleshootPumpComms(using: device) + }) + } catch let error { + self.troubleshootPumpComms(using: device) - completion(PumpManagerError.communication(error as? LocalizedError)) + completion(PumpManagerError.communication(error as? LocalizedError)) + } } } } From 01fc106ca49b3770e9ce0bd21fb6c0d254627e04 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 20 Aug 2025 17:39:22 -0500 Subject: [PATCH 20/26] Updates for pumpmanager delegate protocol (#15) --- .../PumpManager/MinimedPumpManager.swift | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 8db0b7f..46017d0 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -754,68 +754,68 @@ extension MinimedPumpManager { } self.pumpOps.runSession(withName: "Fetch Pump History", using: device) { (session) in - do { - guard let startDate = self.pumpDelegate.call({ (delegate) in - return delegate?.startDateToFilterNewPumpEvents(for: self) - }) else { - preconditionFailure("pumpManagerDelegate cannot be nil") - } - - // Include events up to a minute before startDate, since pump event time and pending event time might be off - self.log.default("Fetching history since %{public}@", String(describing: startDate.addingTimeInterval(.minutes(-1)))) - let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1))) - - // Reconcile history with pending doses - let newPumpEvents = historyEvents.pumpEvents(from: model) - - // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType - let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in - var dose = event.dose - dose?.insulinType = insulinType - return NewPumpEvent( - date: event.date, - dose: dose, - raw: event.raw, - title: event.title, - type: event.type) - } - - self.pumpDelegate.notify({ (delegate) in - guard let delegate = delegate else { + Task { + do { + guard let startDate = await self.pumpDelegate.delegate?.startDateToFilterNewPumpEvents(for: self) else { preconditionFailure("pumpManagerDelegate cannot be nil") } - - let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent() }) - - self.log.default("Reporting new pump events: %{public}@", String(describing: remainingHistoryEvents + pendingEvents)) - - delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.state.lastReconciliation, replacePendingEvents: true) { (error) in - // Called on an unknown queue by the delegate - if error == nil { - self.recents.lastAddedPumpEvents = self.dateGenerator() - self.setState({ (state) in - // Remove any pending doses that have been reconciled and are finished - if let bolus = state.unfinalizedBolus, bolus.isReconciledWithHistory, bolus.isFinished { - state.unfinalizedBolus = nil - } - if let tempBasal = state.unfinalizedTempBasal, tempBasal.isReconciledWithHistory, tempBasal.isFinished { - state.unfinalizedTempBasal = nil - } - state.pendingDoses.removeAll(where: { (dose) -> Bool in - if dose.isReconciledWithHistory && dose.isFinished { - print("Removing stored, finished, reconciled dose: \(dose)") + + // Include events up to a minute before startDate, since pump event time and pending event time might be off + self.log.default("Fetching history since %{public}@", String(describing: startDate.addingTimeInterval(.minutes(-1)))) + let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1))) + + // Reconcile history with pending doses + let newPumpEvents = historyEvents.pumpEvents(from: model) + + // During reconciliation, some pump events may be reconciled as pending doses and removed. Remaining events should be annotated with current insulinType + let remainingHistoryEvents = self.reconcilePendingDosesWith(newPumpEvents, fetchedAt: self.dateGenerator()).map { (event) -> NewPumpEvent in + var dose = event.dose + dose?.insulinType = insulinType + return NewPumpEvent( + date: event.date, + dose: dose, + raw: event.raw, + title: event.title, + type: event.type) + } + + self.pumpDelegate.notify({ (delegate) in + guard let delegate = delegate else { + preconditionFailure("pumpManagerDelegate cannot be nil") + } + + let pendingEvents = (self.state.pendingDoses + [self.state.unfinalizedBolus, self.state.unfinalizedTempBasal]).compactMap({ $0?.newPumpEvent() }) + + self.log.default("Reporting new pump events: %{public}@", String(describing: remainingHistoryEvents + pendingEvents)) + + delegate.pumpManager(self, hasNewPumpEvents: remainingHistoryEvents + pendingEvents, lastReconciliation: self.state.lastReconciliation, replacePendingEvents: true) { (error) in + // Called on an unknown queue by the delegate + if error == nil { + self.recents.lastAddedPumpEvents = self.dateGenerator() + self.setState({ (state) in + // Remove any pending doses that have been reconciled and are finished + if let bolus = state.unfinalizedBolus, bolus.isReconciledWithHistory, bolus.isFinished { + state.unfinalizedBolus = nil + } + if let tempBasal = state.unfinalizedTempBasal, tempBasal.isReconciledWithHistory, tempBasal.isFinished { + state.unfinalizedTempBasal = nil } - return dose.isReconciledWithHistory && dose.isFinished + state.pendingDoses.removeAll(where: { (dose) -> Bool in + if dose.isReconciledWithHistory && dose.isFinished { + print("Removing stored, finished, reconciled dose: \(dose)") + } + return dose.isReconciledWithHistory && dose.isFinished + }) }) - }) + } + completion(error) } - completion(error) - } - }) - } catch let error { - self.troubleshootPumpComms(using: device) + }) + } catch let error { + self.troubleshootPumpComms(using: device) - completion(PumpManagerError.communication(error as? LocalizedError)) + completion(PumpManagerError.communication(error as? LocalizedError)) + } } } } From 628bfe06122d1a8207abe44f8a78530192e7a47b Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 24 Oct 2025 17:32:28 -0300 Subject: [PATCH 21/26] [LOOP-5496] adding loop status checks (#16) --- .../PumpManager/MinimedPumpManager.swift | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 46017d0..a83dcda 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -475,7 +475,7 @@ extension MinimedPumpManager { state: .warning) } - if date.timeIntervalSince(lastSync(for: state, recents: recents) ?? .distantPast) > .minutes(12) { + if isSignalLost(at: date) { return PumpStatusHighlight( localizedMessage: LocalizedString("Signal Loss", comment: "Status highlight when communications with the pod haven't happened recently."), imageName: "exclamationmark.circle.fill", @@ -483,7 +483,10 @@ extension MinimedPumpManager { } return nil } - + + private func isSignalLost(at date: Date = Date()) -> Bool { + date.timeIntervalSince(lastSync(for: state, recents: recents) ?? .distantPast) > .minutes(12) + } private func checkRileyLinkBattery() { rileyLinkDeviceProvider.getDevices { devices in @@ -925,6 +928,14 @@ extension MinimedPumpManager { // MARK: - PumpManager extension MinimedPumpManager: PumpManager { + public var inSignalLoss: Bool { + isSignalLost() + } + + public var isInoperable: Bool { + basalDeliveryState(for: recents) == .pumpInoperable + } + public static let localizedTitle = LocalizedString("Minimed 500/700 Series", comment: "Generic title of the minimed pump manager") public var localizedTitle: String { @@ -1003,32 +1014,9 @@ extension MinimedPumpManager: PumpManager { } private func status(for state: MinimedPumpManagerState, recents: MinimedPumpManagerRecents) -> PumpManagerStatus { - let basalDeliveryState: PumpManagerStatus.BasalDeliveryState + let basalDeliveryState = basalDeliveryState(for: recents) + - switch recents.suspendEngageState { - case .engaging: - basalDeliveryState = .suspending - case .disengaging: - basalDeliveryState = .resuming - case .stable: - switch recents.tempBasalEngageState { - case .engaging: - basalDeliveryState = .initiatingTempBasal - case .disengaging: - basalDeliveryState = .cancelingTempBasal - case .stable: - switch self.state.suspendState { - case .suspended(let date): - basalDeliveryState = .suspended(date) - case .resumed(let date): - if let tempBasal = state.unfinalizedTempBasal { - basalDeliveryState = .tempBasal(DoseEntry(tempBasal)) - } else { - basalDeliveryState = .active(date) - } - } - } - } let bolusState: PumpManagerStatus.BolusState @@ -1055,6 +1043,33 @@ extension MinimedPumpManager: PumpManager { ) } + private func basalDeliveryState(for recents: MinimedPumpManagerRecents) -> PumpManagerStatus.BasalDeliveryState { + switch recents.suspendEngageState { + case .engaging: + return .suspending + case .disengaging: + return .resuming + case .stable: + switch recents.tempBasalEngageState { + case .engaging: + return .initiatingTempBasal + case .disengaging: + return .cancelingTempBasal + case .stable: + switch self.state.suspendState { + case .suspended(let date): + return .suspended(date) + case .resumed(let date): + if let tempBasal = state.unfinalizedTempBasal { + return .tempBasal(DoseEntry(tempBasal)) + } else { + return .active(date) + } + } + } + } + } + public var status: PumpManagerStatus { // Acquire the locks just once let state = self.state From 3510e12e96f78b46383fba7e1e23d3757b600600 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 24 Oct 2025 17:32:28 -0300 Subject: [PATCH 22/26] [LOOP-5496] adding loop status checks (#16) --- .../PumpManager/MinimedPumpManager.swift | 69 +++++++++++-------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index 46017d0..a83dcda 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -475,7 +475,7 @@ extension MinimedPumpManager { state: .warning) } - if date.timeIntervalSince(lastSync(for: state, recents: recents) ?? .distantPast) > .minutes(12) { + if isSignalLost(at: date) { return PumpStatusHighlight( localizedMessage: LocalizedString("Signal Loss", comment: "Status highlight when communications with the pod haven't happened recently."), imageName: "exclamationmark.circle.fill", @@ -483,7 +483,10 @@ extension MinimedPumpManager { } return nil } - + + private func isSignalLost(at date: Date = Date()) -> Bool { + date.timeIntervalSince(lastSync(for: state, recents: recents) ?? .distantPast) > .minutes(12) + } private func checkRileyLinkBattery() { rileyLinkDeviceProvider.getDevices { devices in @@ -925,6 +928,14 @@ extension MinimedPumpManager { // MARK: - PumpManager extension MinimedPumpManager: PumpManager { + public var inSignalLoss: Bool { + isSignalLost() + } + + public var isInoperable: Bool { + basalDeliveryState(for: recents) == .pumpInoperable + } + public static let localizedTitle = LocalizedString("Minimed 500/700 Series", comment: "Generic title of the minimed pump manager") public var localizedTitle: String { @@ -1003,32 +1014,9 @@ extension MinimedPumpManager: PumpManager { } private func status(for state: MinimedPumpManagerState, recents: MinimedPumpManagerRecents) -> PumpManagerStatus { - let basalDeliveryState: PumpManagerStatus.BasalDeliveryState + let basalDeliveryState = basalDeliveryState(for: recents) + - switch recents.suspendEngageState { - case .engaging: - basalDeliveryState = .suspending - case .disengaging: - basalDeliveryState = .resuming - case .stable: - switch recents.tempBasalEngageState { - case .engaging: - basalDeliveryState = .initiatingTempBasal - case .disengaging: - basalDeliveryState = .cancelingTempBasal - case .stable: - switch self.state.suspendState { - case .suspended(let date): - basalDeliveryState = .suspended(date) - case .resumed(let date): - if let tempBasal = state.unfinalizedTempBasal { - basalDeliveryState = .tempBasal(DoseEntry(tempBasal)) - } else { - basalDeliveryState = .active(date) - } - } - } - } let bolusState: PumpManagerStatus.BolusState @@ -1055,6 +1043,33 @@ extension MinimedPumpManager: PumpManager { ) } + private func basalDeliveryState(for recents: MinimedPumpManagerRecents) -> PumpManagerStatus.BasalDeliveryState { + switch recents.suspendEngageState { + case .engaging: + return .suspending + case .disengaging: + return .resuming + case .stable: + switch recents.tempBasalEngageState { + case .engaging: + return .initiatingTempBasal + case .disengaging: + return .cancelingTempBasal + case .stable: + switch self.state.suspendState { + case .suspended(let date): + return .suspended(date) + case .resumed(let date): + if let tempBasal = state.unfinalizedTempBasal { + return .tempBasal(DoseEntry(tempBasal)) + } else { + return .active(date) + } + } + } + } + } + public var status: PumpManagerStatus { // Acquire the locks just once let state = self.state From 803121af63c9dc87789827f83ffcfc702b5dfb3d Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 6 Feb 2026 13:37:31 -0400 Subject: [PATCH 23/26] [LOOP-5743] using pumpStatusHighlight (#17) --- MinimedKitUI/MinimedPumpManager+UI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MinimedKitUI/MinimedPumpManager+UI.swift b/MinimedKitUI/MinimedPumpManager+UI.swift index c7ae90d..3c1b9e2 100644 --- a/MinimedKitUI/MinimedPumpManager+UI.swift +++ b/MinimedKitUI/MinimedPumpManager+UI.swift @@ -70,7 +70,7 @@ extension MinimedPumpManager: PumpManagerUI { // MARK: - PumpStatusIndicator extension MinimedPumpManager { - public var pumpStatusHighlight: DeviceStatusHighlight? { + public var pumpStatusHighlight: PumpStatusHighlight? { return buildPumpStatusHighlight(for: state, recents: recents, andDate: dateGenerator()) } From 1642f4402ca2f0460eeb216722f69558bc47777f Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Fri, 6 Feb 2026 13:37:31 -0400 Subject: [PATCH 24/26] [LOOP-5743] using pumpStatusHighlight (#17) --- MinimedKitUI/MinimedPumpManager+UI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MinimedKitUI/MinimedPumpManager+UI.swift b/MinimedKitUI/MinimedPumpManager+UI.swift index c7ae90d..3c1b9e2 100644 --- a/MinimedKitUI/MinimedPumpManager+UI.swift +++ b/MinimedKitUI/MinimedPumpManager+UI.swift @@ -70,7 +70,7 @@ extension MinimedPumpManager: PumpManagerUI { // MARK: - PumpStatusIndicator extension MinimedPumpManager { - public var pumpStatusHighlight: DeviceStatusHighlight? { + public var pumpStatusHighlight: PumpStatusHighlight? { return buildPumpStatusHighlight(for: state, recents: recents, andDate: dateGenerator()) } From f19f99cc0b552fc6996412285f9a1e450a4f4961 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 11 May 2026 12:43:44 -0500 Subject: [PATCH 25/26] MinimedPumpSettingsViewModel: restore DIY CAGE/IAGE properties The 2026-05-11 tidepool sync conflict resolution used git checkout --theirs on this file because the only conflict marker was a trivial whitespace hunk (an extra blank line). But that whole-file checkout wiped DIY's CAGE/IAGE viewModel additions outside the conflict region: - @Published var timeSinceLastSetChange: String? - @Published var timeSinceLastRewind: String? - formatDateToDaysHours(_:) helper - Init + state-observer wiring for both properties MinimedPumpSettingsView still references these properties so the build broke. Restored the file to DIY's pre-merge version (tidepool-sync/2026-03-10) which already has Tidepool's import/QuantityFormatter migrations plus DIY's CAGE/IAGE work. --- .../Views/MinimedPumpSettingsViewModel.swift | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift index 473aafd..be0e5a0 100644 --- a/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift +++ b/MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift @@ -13,7 +13,6 @@ import SwiftUI import LoopKitUI import LoopAlgorithm - enum MinimedSettingsViewAlert: Identifiable { case suspendError(Error) case resumeError(Error) @@ -82,6 +81,8 @@ class MinimedPumpSettingsViewModel: ObservableObject { @Published var activeAlert: MinimedSettingsViewAlert? @Published var suspendResumeButtonEnabled: Bool = false @Published var synchronizingTime: Bool = false + @Published var timeSinceLastSetChange: String? + @Published var timeSinceLastRewind: String? var pumpManager: MinimedPumpManager @@ -109,6 +110,13 @@ class MinimedPumpSettingsViewModel: ObservableObject { self.preferredDataSource = pumpManager.preferredInsulinDataSource self.mySentryConfig = pumpManager.useMySentry ? .useMySentry : .doNotUseMySentry + if let lastSetChangeDate = pumpManager.state.lastSetChangeDate { + self.timeSinceLastSetChange = formatDateToDaysHours(lastSetChangeDate) + } + if let lastRewindDate = pumpManager.state.lastRewindDate { + self.timeSinceLastRewind = formatDateToDaysHours(lastRewindDate) + } + self.pumpManager.addStatusObserver(self, queue: DispatchQueue.main) pumpManager.stateObservers.insert(self, queue: .main) } @@ -247,6 +255,17 @@ class MinimedPumpSettingsViewModel: ObservableObject { } +private func formatDateToDaysHours(_ date: Date) -> String { + let components = Calendar.current.dateComponents([.day, .hour], from: date, to: Date()) + let days = components.day ?? 0 + let hours = components.hour ?? 0 + + let dayString = days == 1 ? LocalizedString("day", comment: "Singular day unit") : LocalizedString("days", comment: "Plural days unit") + let hourString = hours == 1 ? LocalizedString("hour", comment: "Singular hour unit") : LocalizedString("hours", comment: "Plural hours unit") + + return "\(days) \(dayString), \(hours) \(hourString)" +} + extension MinimedPumpSettingsViewModel: PumpManagerStatusObserver { public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) { basalDeliveryState = status.basalDeliveryState @@ -259,6 +278,12 @@ extension MinimedPumpSettingsViewModel: MinimedPumpManagerStateObserver { batteryChemistryType = state.batteryChemistry preferredDataSource = state.preferredInsulinDataSource mySentryConfig = state.useMySentry ? .useMySentry : .doNotUseMySentry + if let lastSetChangeDate = state.lastSetChangeDate { + timeSinceLastSetChange = formatDateToDaysHours(lastSetChangeDate) + } + if let lastRewindDate = state.lastRewindDate { + timeSinceLastRewind = formatDateToDaysHours(lastRewindDate) + } } } From 50b942bdb44c198b611d3d4530458a78ffe9e9e4 Mon Sep 17 00:00:00 2001 From: loopkitdev Date: Sun, 31 May 2026 12:44:43 -0500 Subject: [PATCH 26/26] Fix off-queue BLE crash in fetchPumpHistory The runSession body was wrapped in Task { } so it could await startDateToFilterNewPumpEvents, but that hops onto the cooperative pool and trips PeripheralManager.runCommand's onQueue precondition. Move the await outside runSession and keep the BLE work synchronous within the session closure, matching every other runSession call site. --- .../PumpManager/MinimedPumpManager.swift | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/MinimedKit/PumpManager/MinimedPumpManager.swift b/MinimedKit/PumpManager/MinimedPumpManager.swift index a34b534..4387032 100644 --- a/MinimedKit/PumpManager/MinimedPumpManager.swift +++ b/MinimedKit/PumpManager/MinimedPumpManager.swift @@ -750,19 +750,19 @@ extension MinimedPumpManager { return } - rileyLinkDeviceProvider.getDevices { (devices) in - guard let device = devices.firstConnected else { - completion(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)) - return + Task { + guard let startDate = await self.pumpDelegate.delegate?.startDateToFilterNewPumpEvents(for: self) else { + preconditionFailure("pumpManagerDelegate cannot be nil") } - - self.pumpOps.runSession(withName: "Fetch Pump History", using: device) { (session) in - Task { - do { - guard let startDate = await self.pumpDelegate.delegate?.startDateToFilterNewPumpEvents(for: self) else { - preconditionFailure("pumpManagerDelegate cannot be nil") - } + self.rileyLinkDeviceProvider.getDevices { (devices) in + guard let device = devices.firstConnected else { + completion(PumpManagerError.connection(MinimedPumpManagerError.noRileyLink)) + return + } + + self.pumpOps.runSession(withName: "Fetch Pump History", using: device) { (session) in + do { // Include events up to a minute before startDate, since pump event time and pending event time might be off self.log.default("Fetching history since %{public}@", String(describing: startDate.addingTimeInterval(.minutes(-1)))) let (historyEvents, model) = try session.getHistoryEvents(since: startDate.addingTimeInterval(.minutes(-1)))