From dcb4e873f29ff0c42e576c0a5374e3ceb7149bfa Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Mon, 25 Sep 2023 15:49:30 -0300 Subject: [PATCH 01/25] [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/25] [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/25] 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/25] 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/25] [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/25] [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/25] [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/25] [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/25] [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/25] [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/25] 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/25] 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/25] 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/25] 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/25] [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/25] [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/25] 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/25] 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/25] 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/25] 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/25] [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/25] [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/25] [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/25] [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/25] 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) + } } }